From db76929d0f41675cfb5f001753e21fb6dc283296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 9 May 2024 18:33:51 +0200 Subject: [PATCH 01/52] Adding support for transcripts, recording and AI summarization --- server/webhook.go | 176 +- server/zoom/webhook.go | 28 +- webapp/package-lock.json | 3359 +++++++++-------- webapp/package.json | 3 +- .../post_type_zoom/post_type_zoom.jsx | 3 + webapp/src/index.js | 2 + webapp/webpack.config.js | 6 + 7 files changed, 1917 insertions(+), 1660 deletions(-) diff --git a/server/webhook.go b/server/webhook.go index f22b0a29..42916369 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -10,6 +10,7 @@ import ( "io" "math" "net/http" + "strconv" "strings" "time" @@ -56,11 +57,16 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) { } } + p.API.LogWarn("New event received", "even_type", webhook.Event, "payload", string(b)) switch webhook.Event { case zoom.EventTypeMeetingEnded: p.handleMeetingEnded(w, r, b) case zoom.EventTypeValidateWebhook: p.handleValidateZoomWebhook(w, r, b) + case zoom.EventTypeRecordingCompleted: + p.handleRecordingCompleted(w, r, b) + case zoom.EventTypeTranscriptCompleted: + p.handleTranscriptCompleted(w, r, b) default: w.WriteHeader(http.StatusOK) } @@ -123,11 +129,177 @@ func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body return } - if appErr = p.deleteMeetingPostID(meetingPostID); appErr != nil { - p.API.LogWarn("failed to delete db entry", "error", appErr.Error()) + // TODO: Delete the meeting post if is no longer needed + // if appErr = p.deleteMeetingPostID(meetingPostID); appErr != nil { + // p.API.LogWarn("failed to delete db entry", "error", appErr.Error()) + // return + // } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(post); err != nil { + p.API.LogWarn("failed to write response", "error", err.Error()) + } +} + +func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Request, body []byte) { + p.API.LogError("RUNNING THE UPDATE MEETING TRANSCRIPT") + var webhook zoom.RecordingWebhook + if err := json.Unmarshal(body, &webhook); err != nil { + p.API.LogError("Error unmarshaling meeting webhook", "err", err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + meetingPostID := webhook.Payload.Object.ID + postID, appErr := p.fetchMeetingPostID(strconv.Itoa(meetingPostID)) + if appErr != nil { + http.Error(w, appErr.Error(), appErr.StatusCode) return } + post, appErr := p.API.GetPost(postID) + if appErr != nil { + p.API.LogWarn("Could not get meeting post by id", "err", appErr) + http.Error(w, appErr.Error(), appErr.StatusCode) + return + } + + _, appErr = p.API.UpdatePost(post) + if appErr != nil { + p.API.LogWarn("Could not update the post", "err", appErr) + http.Error(w, appErr.Error(), appErr.StatusCode) + return + } + + newPost := &model.Post{ + UserId: p.botUserID, + ChannelId: post.ChannelId, + RootId: post.Id, + Message: "Here's the zoom meeting transcription", + FileIds: []string{}, + Type: "custom_zoom_transcript", + } + + p.API.LogError("UPDATING MEETING TRANSCRIPT") + for _, recording := range webhook.Payload.Object.RecordingFiles { + if recording.RecordingType == zoom.RecordingTypeAudioTranscript { + p.API.LogError("MEETING TRANSCRIPT UPDATED") + post.Props["meeting_transcript"] = recording.PlayURL + if webhook.Payload.Object.Password != "" { + post.Props["meeting_password"] = webhook.Payload.Object.Password + } + request, err := http.NewRequest(http.MethodGet, recording.DownloadURL, nil) + if err != nil { + p.API.LogWarn("Unable to get the transcription", "err", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + request.Header.Set("Authorization", "Bearer "+webhook.DownloadToken) + response, err := http.DefaultClient.Do(request) + if err != nil { + p.API.LogWarn("Unable to get the transcription", "err", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + defer response.Body.Close() + transcription, err := io.ReadAll(response.Body) + if err != nil { + p.API.LogWarn("Unable to get the transcription", "err", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + fileInfo, appErr := p.API.UploadFile(transcription, post.ChannelId, "transcription.txt") + if appErr != nil { + p.API.LogWarn("Unable to get the transcription", "err", appErr) + http.Error(w, appErr.Error(), http.StatusBadRequest) + return + } + newPost.FileIds = append(newPost.FileIds, fileInfo.Id) + post.Props["captions"] = []map[string]string{{"file_id": fileInfo.Id}} + } + } + + _, appErr = p.API.UpdatePost(post) + if appErr != nil { + p.API.LogWarn("Could not update the post", "err", appErr) + http.Error(w, appErr.Error(), appErr.StatusCode) + return + } + + _, appErr = p.API.CreatePost(newPost) + + if appErr != nil { + p.API.LogWarn("Could not create the transcription post", "err", appErr) + http.Error(w, appErr.Error(), appErr.StatusCode) + return + } + + // TODO: Delete the meeting post if is no longer needed + // if appErr = p.deleteMeetingPostID(meetingPostID); appErr != nil { + // p.API.LogWarn("failed to delete db entry", "error", appErr.Error()) + // return + // } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(post); err != nil { + p.API.LogWarn("failed to write response", "error", err.Error()) + } +} + +func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request, body []byte) { + p.API.LogError("RUNNING THE UPDATE MEETING RECORDING") + var webhook zoom.RecordingWebhook + if err := json.Unmarshal(body, &webhook); err != nil { + p.API.LogError("Error unmarshaling meeting webhook", "err", err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + meetingPostID := webhook.Payload.Object.ID + postID, appErr := p.fetchMeetingPostID(strconv.Itoa(meetingPostID)) + if appErr != nil { + http.Error(w, appErr.Error(), appErr.StatusCode) + return + } + + post, appErr := p.API.GetPost(postID) + if appErr != nil { + p.API.LogWarn("Could not get meeting post by id", "err", appErr) + http.Error(w, appErr.Error(), appErr.StatusCode) + return + } + + p.API.LogError("UPDATING MEETING RECORDING") + for _, recording := range webhook.Payload.Object.RecordingFiles { + if recording.RecordingType == zoom.RecordingTypeVideo { + p.API.LogError("MEETING RECORDING UPDATED") + post.Props["meeting_recording"] = recording.PlayURL + if webhook.Payload.Object.Password != "" { + post.Props["meeting_password"] = webhook.Payload.Object.Password + } + } + } + + _, appErr = p.API.UpdatePost(post) + if appErr != nil { + p.API.LogWarn("Could not update the post", "err", appErr) + http.Error(w, appErr.Error(), appErr.StatusCode) + return + } + + _, appErr = p.API.CreatePost(&model.Post{ + UserId: p.botUserID, + ChannelId: post.ChannelId, + RootId: post.Id, + Message: "Here's the zoom meeting recording:\n**Link:** [Meeting Recording](" + post.Props["meeting_recording"].(string) + ")\n**Password:** " + post.Props["meeting_password"].(string), + }) + + // TODO: Delete the meeting post if is no longer needed + // if appErr = p.deleteMeetingPostID(meetingPostID); appErr != nil { + // p.API.LogWarn("failed to delete db entry", "error", appErr.Error()) + // return + // } + w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(post); err != nil { p.API.LogWarn("failed to write response", "error", err.Error()) diff --git a/server/zoom/webhook.go b/server/zoom/webhook.go index 307c4c29..1b70d1b7 100644 --- a/server/zoom/webhook.go +++ b/server/zoom/webhook.go @@ -15,9 +15,14 @@ const ( RecordingWebhookTypeComplete = "RECORDING_MEETING_COMPLETED" RecentlyCreated = "RECENTLY_CREATED" - EventTypeMeetingStarted EventType = "meeting.started" - EventTypeMeetingEnded EventType = "meeting.ended" - EventTypeValidateWebhook EventType = "endpoint.url_validation" + EventTypeMeetingStarted EventType = "meeting.started" + EventTypeMeetingEnded EventType = "meeting.ended" + EventTypeTranscriptCompleted EventType = "recording.transcript_completed" + EventTypeRecordingCompleted EventType = "recording.completed" + EventTypeValidateWebhook EventType = "endpoint.url_validation" + + RecordingTypeAudioTranscript = "audio_transcript" + RecordingTypeVideo = "shared_screen_with_speaker_view" ) type MeetingWebhookObject struct { @@ -63,13 +68,20 @@ type Webhook struct { } type RecordingWebhook struct { - Type string `schema:"type"` - Content string `schema:"content"` + Type string `schema:"type"` + DownloadToken string `json:"download_token"` + Payload RecordingWebhookPayload `schema:"content"` } -type RecordingWebhookContent struct { +type RecordingWebhookPayload struct { + AccountID string `json:"account_id"` + Object RecordingWebhookObject `json:"object"` +} + +type RecordingWebhookObject struct { UUID string `json:"uuid"` MeetingNumber int `json:"meeting_number"` + ID int `json:"id"` AccountID string `json:"account_id"` HostID string `json:"host_id"` Topic string `json:"topic"` @@ -79,6 +91,7 @@ type RecordingWebhookContent struct { Duration int `json:"duration"` TotalSize int `json:"total_size"` RecordingCount int `json:"recording_count"` + Password string `json:"password"` RecordingFiles []struct { ID string `json:"id"` MeetingID string `json:"meeting_id"` @@ -88,6 +101,9 @@ type RecordingWebhookContent struct { FileSize int `json:"file_size"` FilePath string `json:"file_path"` Status string `json:"status"` + DownloadURL string `json:"download_url"` + PlayURL string `json:"play_url"` + RecordingType string `json:"recording_type"` } `json:"recording_files"` } diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 4efd1d85..ee7755d4 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -12,10 +12,11 @@ "core-js": "3.7.0", "mattermost-redux": "5.33.1", "prop-types": "15.7.2", - "react": "17.0.1", + "react": "18.3.1", "react-intl": "4.7.6", "react-redux": "7.2.2", "redux": "4.0.5", + "styled-components": "6.1.10", "typescript": "4.6.4" }, "devDependencies": { @@ -165,11 +166,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", - "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", + "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", "dependencies": { - "@babel/types": "^7.24.0", + "@babel/types": "^7.24.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -224,18 +225,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.4.tgz", - "integrity": "sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", + "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.24.5", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.24.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-split-export-declaration": "^7.24.5", "semver": "^6.3.1" }, "engines": { @@ -354,11 +355,11 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", + "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", "dependencies": { - "@babel/types": "^7.23.0" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -376,15 +377,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", + "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^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" }, "engines": { "node": ">=6.9.0" @@ -405,9 +406,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", - "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", + "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", "engines": { "node": ">=6.9.0" } @@ -445,11 +446,11 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", + "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -467,28 +468,28 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", "engines": { "node": ">=6.9.0" } @@ -515,13 +516,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz", - "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", + "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", "dependencies": { "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0" + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -542,9 +543,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", - "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1449,14 +1450,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.1.tgz", - "integrity": "sha512-pTHxDVa0BpUbvAgX3Gat+7cSciXqUcY9j2VZKTbSB6+VQGpNgNO9ailxTGHSXlqOnX1Hcx1Enme2+yv7VqP9bg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.5.tgz", + "integrity": "sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==", "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-create-class-features-plugin": "^7.24.5", + "@babel/helper-plugin-utils": "^7.24.5", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { @@ -1528,12 +1529,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.1.tgz", - "integrity": "sha512-kDJgnPujTmAZ/9q2CN4m2/lRsUUPDvsG3+tSHWUJIzMGTt5U/b/fwWd3RO3n+5mjLrsBrVa5eKFRVSQbi3dF1w==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.5.tgz", + "integrity": "sha512-RtCJoUO2oYrYwFPtR1/jkoBEcFuI1ae9a9IMxeyAVa3a1Ap4AnxmyIKG2b2FaJKqkidw/0cxRbWN+HOs6ZWd1w==", "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -1623,9 +1624,9 @@ } }, "node_modules/@babel/plugin-transform-runtime/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz", - "integrity": "sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", "peer": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -1639,13 +1640,13 @@ } }, "node_modules/@babel/plugin-transform-runtime/node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz", - "integrity": "sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ==", + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", "peer": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.1", + "@babel/helper-define-polyfill-provider": "^0.6.2", "semver": "^6.3.1" }, "peerDependencies": { @@ -1666,12 +1667,12 @@ } }, "node_modules/@babel/plugin-transform-runtime/node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz", - "integrity": "sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", "peer": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.1" + "@babel/helper-define-polyfill-provider": "^0.6.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -2043,18 +2044,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", - "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", + "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", "dependencies": { - "@babel/code-frame": "^7.24.1", - "@babel/generator": "^7.24.1", + "@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.22.6", - "@babel/parser": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/types": "^7.24.5", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2084,12 +2085,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", "to-fast-properties": "^2.0.0" }, "engines": { @@ -2191,11 +2192,18 @@ "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==", "dev": true }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, "node_modules/@emotion/memoize": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", - "dev": true + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "node_modules/@emotion/react": { "version": "11.11.0", @@ -2261,8 +2269,7 @@ "node_modules/@emotion/unitless": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", - "dev": true + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", @@ -2660,7 +2667,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -2673,7 +2679,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -2682,7 +2687,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -2692,20 +2696,19 @@ } }, "node_modules/@react-native-community/cli": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-12.3.6.tgz", - "integrity": "sha512-647OSi6xBb8FbwFqX9zsJxOzu685AWtrOUWHfOkbKD+5LOpGORw+GQo0F9rWZnB68rLQyfKUZWJeaD00pGv5fw==", - "peer": true, - "dependencies": { - "@react-native-community/cli-clean": "12.3.6", - "@react-native-community/cli-config": "12.3.6", - "@react-native-community/cli-debugger-ui": "12.3.6", - "@react-native-community/cli-doctor": "12.3.6", - "@react-native-community/cli-hermes": "12.3.6", - "@react-native-community/cli-plugin-metro": "12.3.6", - "@react-native-community/cli-server-api": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", - "@react-native-community/cli-types": "12.3.6", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-13.6.6.tgz", + "integrity": "sha512-IqclB7VQ84ye8Fcs89HOpOscY4284VZg2pojHNl8H0Lzd4DadXJWQoxC7zWm8v2f8eyeX2kdhxp2ETD5tceIgA==", + "peer": true, + "dependencies": { + "@react-native-community/cli-clean": "13.6.6", + "@react-native-community/cli-config": "13.6.6", + "@react-native-community/cli-debugger-ui": "13.6.6", + "@react-native-community/cli-doctor": "13.6.6", + "@react-native-community/cli-hermes": "13.6.6", + "@react-native-community/cli-server-api": "13.6.6", + "@react-native-community/cli-tools": "13.6.6", + "@react-native-community/cli-types": "13.6.6", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", @@ -2724,14 +2727,15 @@ } }, "node_modules/@react-native-community/cli-clean": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-12.3.6.tgz", - "integrity": "sha512-gUU29ep8xM0BbnZjwz9MyID74KKwutq9x5iv4BCr2im6nly4UMf1B1D+V225wR7VcDGzbgWjaezsJShLLhC5ig==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-13.6.6.tgz", + "integrity": "sha512-cBwJTwl0NyeA4nyMxbhkWZhxtILYkbU3TW3k8AXLg+iGphe0zikYMGB3T+haTvTc6alTyEFwPbimk9bGIqkjAQ==", "peer": true, "dependencies": { - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-tools": "13.6.6", "chalk": "^4.1.2", - "execa": "^5.0.0" + "execa": "^5.0.0", + "fast-glob": "^3.3.2" } }, "node_modules/@react-native-community/cli-clean/node_modules/ansi-styles": { @@ -2836,18 +2840,6 @@ "node": ">=10.17.0" } }, - "node_modules/@react-native-community/cli-clean/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@react-native-community/cli-clean/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2861,16 +2853,16 @@ } }, "node_modules/@react-native-community/cli-config": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-12.3.6.tgz", - "integrity": "sha512-JGWSYQ9EAK6m2v0abXwFLEfsqJ1zkhzZ4CV261QZF9MoUNB6h57a274h1MLQR9mG6Tsh38wBUuNfEPUvS1vYew==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-13.6.6.tgz", + "integrity": "sha512-mbG425zCKr8JZhv/j11382arezwS/70juWMsn8j2lmrGTrP1cUdW0MF15CCIFtJsqyK3Qs+FTmqttRpq81QfSg==", "peer": true, "dependencies": { - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-tools": "13.6.6", "chalk": "^4.1.2", "cosmiconfig": "^5.1.0", "deepmerge": "^4.3.0", - "glob": "^7.1.3", + "fast-glob": "^3.3.2", "joi": "^17.2.1" } }, @@ -2995,24 +2987,25 @@ } }, "node_modules/@react-native-community/cli-debugger-ui": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-12.3.6.tgz", - "integrity": "sha512-SjUKKsx5FmcK9G6Pb6UBFT0s9JexVStK5WInmANw75Hm7YokVvHEgtprQDz2Uvy5znX5g2ujzrkIU//T15KQzA==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-13.6.6.tgz", + "integrity": "sha512-Vv9u6eS4vKSDAvdhA0OiQHoA7y39fiPIgJ6biT32tN4avHDtxlc6TWZGiqv7g98SBvDWvoVAmdPLcRf3kU+c8g==", "peer": true, "dependencies": { "serve-static": "^1.13.1" } }, "node_modules/@react-native-community/cli-doctor": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-12.3.6.tgz", - "integrity": "sha512-fvBDv2lTthfw4WOQKkdTop2PlE9GtfrlNnpjB818MhcdEnPjfQw5YaTUcnNEGsvGomdCs1MVRMgYXXwPSN6OvQ==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-13.6.6.tgz", + "integrity": "sha512-TWZb5g6EmQe2Ua2TEWNmyaEayvlWH4GmdD9ZC+p8EpKFpB1NpDGMK6sXbpb42TDvwZg5s4TDRplK0PBEA/SVDg==", "peer": true, "dependencies": { - "@react-native-community/cli-config": "12.3.6", - "@react-native-community/cli-platform-android": "12.3.6", - "@react-native-community/cli-platform-ios": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-config": "13.6.6", + "@react-native-community/cli-platform-android": "13.6.6", + "@react-native-community/cli-platform-apple": "13.6.6", + "@react-native-community/cli-platform-ios": "13.6.6", + "@react-native-community/cli-tools": "13.6.6", "chalk": "^4.1.2", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", @@ -3138,38 +3131,11 @@ "node": ">=10.17.0" } }, - "node_modules/@react-native-community/cli-doctor/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@react-native-community/cli-doctor/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.1.tgz", + "integrity": "sha512-f/vbBsu+fOiYt+lmwZV0rVwJScl46HppnOA1ZvIuBWKOTlllpyJ3bfVax76/OrhCH38dyxoDIA8K7uB963IYgA==", "peer": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -3201,16 +3167,10 @@ "node": ">=8" } }, - "node_modules/@react-native-community/cli-doctor/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "peer": true - }, "node_modules/@react-native-community/cli-doctor/node_modules/yaml": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", - "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", + "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", "peer": true, "bin": { "yaml": "bin.mjs" @@ -3220,13 +3180,13 @@ } }, "node_modules/@react-native-community/cli-hermes": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-hermes/-/cli-hermes-12.3.6.tgz", - "integrity": "sha512-sNGwfOCl8OAIjWCkwuLpP8NZbuO0dhDI/2W7NeOGDzIBsf4/c4MptTrULWtGIH9okVPLSPX0NnRyGQ+mSwWyuQ==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-hermes/-/cli-hermes-13.6.6.tgz", + "integrity": "sha512-La5Ie+NGaRl3klei6WxKoOxmCUSGGxpOk6vU5pEGf0/O7ky+Ay0io+zXYUZqlNMi/cGpO7ZUijakBYOB/uyuFg==", "peer": true, "dependencies": { - "@react-native-community/cli-platform-android": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-platform-android": "13.6.6", + "@react-native-community/cli-tools": "13.6.6", "chalk": "^4.1.2", "hermes-profile-transformer": "^0.0.6" } @@ -3302,16 +3262,16 @@ } }, "node_modules/@react-native-community/cli-platform-android": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-12.3.6.tgz", - "integrity": "sha512-DeDDAB8lHpuGIAPXeeD9Qu2+/wDTFPo99c8uSW49L0hkmZJixzvvvffbGQAYk32H0TmaI7rzvzH+qzu7z3891g==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-13.6.6.tgz", + "integrity": "sha512-/tMwkBeNxh84syiSwNlYtmUz/Ppc+HfKtdopL/5RB+fd3SV1/5/NPNjMlyLNgFKnpxvKCInQ7dnl6jGHJjeHjg==", "peer": true, "dependencies": { - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-tools": "13.6.6", "chalk": "^4.1.2", "execa": "^5.0.0", + "fast-glob": "^3.3.2", "fast-xml-parser": "^4.2.4", - "glob": "^7.1.3", "logkitty": "^0.7.1" } }, @@ -3417,18 +3377,6 @@ "node": ">=10.17.0" } }, - "node_modules/@react-native-community/cli-platform-android/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@react-native-community/cli-platform-android/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3441,21 +3389,21 @@ "node": ">=8" } }, - "node_modules/@react-native-community/cli-platform-ios": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-12.3.6.tgz", - "integrity": "sha512-3eZ0jMCkKUO58wzPWlvAPRqezVKm9EPZyaPyHbRPWU8qw7JqkvnRlWIaYDGpjCJgVW4k2hKsEursLtYKb188tg==", + "node_modules/@react-native-community/cli-platform-apple": { + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-13.6.6.tgz", + "integrity": "sha512-bOmSSwoqNNT3AmCRZXEMYKz1Jf1l2F86Nhs7qBcXdY/sGiJ+Flng564LOqvdAlVLTbkgz47KjNKCS2pP4Jg0Mg==", "peer": true, "dependencies": { - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-tools": "13.6.6", "chalk": "^4.1.2", "execa": "^5.0.0", + "fast-glob": "^3.3.2", "fast-xml-parser": "^4.0.12", - "glob": "^7.1.3", "ora": "^5.4.1" } }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/ansi-styles": { + "node_modules/@react-native-community/cli-platform-apple/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", @@ -3470,7 +3418,7 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/chalk": { + "node_modules/@react-native-community/cli-platform-apple/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", @@ -3486,7 +3434,7 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/color-convert": { + "node_modules/@react-native-community/cli-platform-apple/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", @@ -3498,13 +3446,13 @@ "node": ">=7.0.0" } }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/color-name": { + "node_modules/@react-native-community/cli-platform-apple/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "peer": true }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/execa": { + "node_modules/@react-native-community/cli-platform-apple/node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", @@ -3527,7 +3475,7 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/get-stream": { + "node_modules/@react-native-community/cli-platform-apple/node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", @@ -3539,7 +3487,7 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/has-flag": { + "node_modules/@react-native-community/cli-platform-apple/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", @@ -3548,7 +3496,7 @@ "node": ">=8" } }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/human-signals": { + "node_modules/@react-native-community/cli-platform-apple/node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", @@ -3557,19 +3505,7 @@ "node": ">=10.17.0" } }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli-platform-ios/node_modules/supports-color": { + "node_modules/@react-native-community/cli-platform-apple/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", @@ -3581,37 +3517,50 @@ "node": ">=8" } }, - "node_modules/@react-native-community/cli-plugin-metro": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-12.3.6.tgz", - "integrity": "sha512-3jxSBQt4fkS+KtHCPSyB5auIT+KKIrPCv9Dk14FbvOaEh9erUWEm/5PZWmtboW1z7CYeNbFMeXm9fM2xwtVOpg==", - "peer": true + "node_modules/@react-native-community/cli-platform-ios": { + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-13.6.6.tgz", + "integrity": "sha512-vjDnRwhlSN5ryqKTas6/DPkxuouuyFBAqAROH4FR1cspTbn6v78JTZKDmtQy9JMMo7N5vZj1kASU5vbFep9IOQ==", + "peer": true, + "dependencies": { + "@react-native-community/cli-platform-apple": "13.6.6" + } }, "node_modules/@react-native-community/cli-server-api": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-12.3.6.tgz", - "integrity": "sha512-80NIMzo8b2W+PL0Jd7NjiJW9mgaT8Y8wsIT/lh6mAvYH7mK0ecDJUYUTAAv79Tbo1iCGPAr3T295DlVtS8s4yQ==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-13.6.6.tgz", + "integrity": "sha512-ZtCXxoFlM7oDv3iZ3wsrT3SamhtUJuIkX2WePLPlN5bcbq7zimbPm2lHyicNJtpcGQ5ymsgpUWPCNZsWQhXBqQ==", "peer": true, "dependencies": { - "@react-native-community/cli-debugger-ui": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-debugger-ui": "13.6.6", + "@react-native-community/cli-tools": "13.6.6", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", "nocache": "^3.0.1", "pretty-format": "^26.6.2", "serve-static": "^1.13.1", - "ws": "^7.5.1" + "ws": "^6.2.2" + } + }, + "node_modules/@react-native-community/cli-server-api/node_modules/ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "peer": true, + "dependencies": { + "async-limiter": "~1.0.0" } }, "node_modules/@react-native-community/cli-tools": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-12.3.6.tgz", - "integrity": "sha512-FPEvZn19UTMMXUp/piwKZSh8cMEfO8G3KDtOwo53O347GTcwNrKjgZGtLSPELBX2gr+YlzEft3CoRv2Qmo83fQ==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-13.6.6.tgz", + "integrity": "sha512-ptOnn4AJczY5njvbdK91k4hcYazDnGtEPrqIwEI+k/CTBHNdb27Rsm2OZ7ye6f7otLBqF8gj/hK6QzJs8CEMgw==", "peer": true, "dependencies": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", + "execa": "^5.0.0", "find-up": "^5.0.0", "mime": "^2.4.1", "node-fetch": "^2.6.0", @@ -3671,6 +3620,29 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "peer": true }, + "node_modules/@react-native-community/cli-tools/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/@react-native-community/cli-tools/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3687,6 +3659,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@react-native-community/cli-tools/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@react-native-community/cli-tools/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3696,6 +3680,15 @@ "node": ">=8" } }, + "node_modules/@react-native-community/cli-tools/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "peer": true, + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/@react-native-community/cli-tools/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3711,38 +3704,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@react-native-community/cli-tools/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@react-native-community/cli-tools/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "peer": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/@react-native-community/cli-tools/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3783,13 +3744,10 @@ } }, "node_modules/@react-native-community/cli-tools/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.1.tgz", + "integrity": "sha512-f/vbBsu+fOiYt+lmwZV0rVwJScl46HppnOA1ZvIuBWKOTlllpyJ3bfVax76/OrhCH38dyxoDIA8K7uB963IYgA==", "peer": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -3809,16 +3767,10 @@ "node": ">=8" } }, - "node_modules/@react-native-community/cli-tools/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "peer": true - }, "node_modules/@react-native-community/cli-types": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-12.3.6.tgz", - "integrity": "sha512-xPqTgcUtZowQ8WKOkI9TLGBwH2bGggOC4d2FFaIRST3gTcjrEeGRNeR5aXCzJFIgItIft8sd7p2oKEdy90+01Q==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-13.6.6.tgz", + "integrity": "sha512-733iaYzlmvNK7XYbnWlMjdE+2k0hlTBJW071af/xb6Bs+hbJqBP9c03FZuYH2hFFwDDntwj05bkri/P7VgSxug==", "peer": true, "dependencies": { "joi": "^17.2.1" @@ -3948,18 +3900,6 @@ "node": ">=10.17.0" } }, - "node_modules/@react-native-community/cli/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@react-native-community/cli/node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -3972,18 +3912,6 @@ "node": ">=8" } }, - "node_modules/@react-native-community/cli/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@react-native-community/cli/node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -4030,13 +3958,10 @@ } }, "node_modules/@react-native-community/cli/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.1.tgz", + "integrity": "sha512-f/vbBsu+fOiYt+lmwZV0rVwJScl46HppnOA1ZvIuBWKOTlllpyJ3bfVax76/OrhCH38dyxoDIA8K7uB963IYgA==", "peer": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -4056,51 +3981,38 @@ "node": ">=8" } }, - "node_modules/@react-native-community/cli/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "peer": true - }, - "node_modules/@react-native-community/netinfo": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-4.7.0.tgz", - "integrity": "sha512-a/sDB+AsLEUNmhAUlAaTYeXKyQdFGBUfatqKkX5jluBo2CB3OAuTHfm7rSjcaLB9EmG5iSq3fOTpync2E7EYTA==", - "peerDependencies": { - "react-native": ">=0.59" - } - }, "node_modules/@react-native/assets-registry": { - "version": "0.73.1", - "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.73.1.tgz", - "integrity": "sha512-2FgAbU7uKM5SbbW9QptPPZx8N9Ke2L7bsHb+EhAanZjFZunA9PaYtyjUQ1s7HD+zDVqOQIvjkpXSv7Kejd2tqg==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.74.83.tgz", + "integrity": "sha512-2vkLMVnp+YTZYTNSDIBZojSsjz8sl5PscP3j4GcV6idD8V978SZfwFlk8K0ti0BzRs11mzL0Pj17km597S/eTQ==", "peer": true, "engines": { "node": ">=18" } }, "node_modules/@react-native/babel-plugin-codegen": { - "version": "0.73.4", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.73.4.tgz", - "integrity": "sha512-XzRd8MJGo4Zc5KsphDHBYJzS1ryOHg8I2gOZDAUCGcwLFhdyGu1zBNDJYH2GFyDrInn9TzAbRIf3d4O+eltXQQ==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.74.83.tgz", + "integrity": "sha512-+S0st3t4Ro00bi9gjT1jnK8qTFOU+CwmziA7U9odKyWrCoRJrgmrvogq/Dr1YXlpFxexiGIupGut1VHxr+fxJA==", "peer": true, "dependencies": { - "@react-native/codegen": "0.73.3" + "@react-native/codegen": "0.74.83" }, "engines": { "node": ">=18" } }, "node_modules/@react-native/babel-preset": { - "version": "0.73.21", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.73.21.tgz", - "integrity": "sha512-WlFttNnySKQMeujN09fRmrdWqh46QyJluM5jdtDNrkl/2Hx6N4XeDUGhABvConeK95OidVO7sFFf7sNebVXogA==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.74.83.tgz", + "integrity": "sha512-KJuu3XyVh3qgyUer+rEqh9a/JoUxsDOzkJNfRpDyXiAyjDRoVch60X/Xa/NcEQ93iCVHAWs0yQ+XGNGIBCYE6g==", "peer": true, "dependencies": { "@babel/core": "^7.20.0", "@babel/plugin-proposal-async-generator-functions": "^7.0.0", "@babel/plugin-proposal-class-properties": "^7.18.0", "@babel/plugin-proposal-export-default-from": "^7.0.0", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.0", "@babel/plugin-proposal-numeric-separator": "^7.0.0", "@babel/plugin-proposal-object-rest-spread": "^7.20.0", @@ -4136,7 +4048,7 @@ "@babel/plugin-transform-typescript": "^7.5.0", "@babel/plugin-transform-unicode-regex": "^7.0.0", "@babel/template": "^7.0.0", - "@react-native/babel-plugin-codegen": "0.73.4", + "@react-native/babel-plugin-codegen": "0.74.83", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" }, @@ -4148,21 +4060,21 @@ } }, "node_modules/@react-native/babel-preset/node_modules/@babel/core": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", - "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.4", + "@babel/generator": "^7.24.5", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.4", - "@babel/parser": "^7.24.4", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -4271,14 +4183,14 @@ } }, "node_modules/@react-native/codegen": { - "version": "0.73.3", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.73.3.tgz", - "integrity": "sha512-sxslCAAb8kM06vGy9Jyh4TtvjhcP36k/rvj2QE2Jdhdm61KvfafCATSIsOfc0QvnduWFcpXUPvAVyYwuv7PYDg==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.74.83.tgz", + "integrity": "sha512-GgvgHS3Aa2J8/mp1uC/zU8HuTh8ZT5jz7a4mVMWPw7+rGyv70Ba8uOVBq6UH2Q08o617IATYc+0HfyzAfm4n0w==", "peer": true, "dependencies": { "@babel/parser": "^7.20.0", - "flow-parser": "^0.206.0", "glob": "^7.1.1", + "hermes-parser": "0.19.1", "invariant": "^2.2.4", "jscodeshift": "^0.14.0", "mkdirp": "^0.5.1", @@ -4292,21 +4204,22 @@ } }, "node_modules/@react-native/community-cli-plugin": { - "version": "0.73.17", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.73.17.tgz", - "integrity": "sha512-F3PXZkcHg+1ARIr6FRQCQiB7ZAA+MQXGmq051metRscoLvgYJwj7dgC8pvgy0kexzUkHu5BNKrZeySzUft3xuQ==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.74.83.tgz", + "integrity": "sha512-7GAFjFOg1mFSj8bnFNQS4u8u7+QtrEeflUIDVZGEfBZQ3wMNI5ycBzbBGycsZYiq00Xvoc6eKFC7kvIaqeJpUQ==", "peer": true, "dependencies": { - "@react-native-community/cli-server-api": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", - "@react-native/dev-middleware": "0.73.8", - "@react-native/metro-babel-transformer": "0.73.15", + "@react-native-community/cli-server-api": "13.6.6", + "@react-native-community/cli-tools": "13.6.6", + "@react-native/dev-middleware": "0.74.83", + "@react-native/metro-babel-transformer": "0.74.83", "chalk": "^4.0.0", "execa": "^5.1.1", "metro": "^0.80.3", "metro-config": "^0.80.3", "metro-core": "^0.80.3", "node-fetch": "^2.2.0", + "querystring": "^0.2.1", "readline": "^1.3.0" }, "engines": { @@ -4415,36 +4328,14 @@ "node": ">=10.17.0" } }, - "node_modules/@react-native/community-cli-plugin/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/@react-native/community-cli-plugin/node_modules/querystring": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", + "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", "peer": true, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "peer": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">=0.4.x" } }, "node_modules/@react-native/community-cli-plugin/node_modules/supports-color": { @@ -4460,28 +4351,30 @@ } }, "node_modules/@react-native/debugger-frontend": { - "version": "0.73.3", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.73.3.tgz", - "integrity": "sha512-RgEKnWuoo54dh7gQhV7kvzKhXZEhpF9LlMdZolyhGxHsBqZ2gXdibfDlfcARFFifPIiaZ3lXuOVVa4ei+uPgTw==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.74.83.tgz", + "integrity": "sha512-RGQlVUegBRxAUF9c1ss1ssaHZh6CO+7awgtI9sDeU0PzDZY/40ImoPD5m0o0SI6nXoVzbPtcMGzU+VO590pRfA==", "peer": true, "engines": { "node": ">=18" } }, "node_modules/@react-native/dev-middleware": { - "version": "0.73.8", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.73.8.tgz", - "integrity": "sha512-oph4NamCIxkMfUL/fYtSsE+JbGOnrlawfQ0kKtDQ5xbOjPKotKoXqrs1eGwozNKv7FfQ393stk1by9a6DyASSg==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.74.83.tgz", + "integrity": "sha512-UH8iriqnf7N4Hpi20D7M2FdvSANwTVStwFCSD7VMU9agJX88Yk0D1T6Meh2RMhUu4kY2bv8sTkNRm7LmxvZqgA==", "peer": true, "dependencies": { "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.73.3", + "@react-native/debugger-frontend": "0.74.83", + "@rnx-kit/chromium-edge-launcher": "^1.0.0", "chrome-launcher": "^0.15.2", - "chromium-edge-launcher": "^1.0.0", "connect": "^3.6.5", "debug": "^2.2.0", "node-fetch": "^2.2.0", + "nullthrows": "^1.1.1", "open": "^7.0.3", + "selfsigned": "^2.4.1", "serve-static": "^1.13.1", "temp-dir": "^2.0.0", "ws": "^6.2.2" @@ -4490,26 +4383,6 @@ "node": ">=18" } }, - "node_modules/@react-native/dev-middleware/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "peer": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/@react-native/dev-middleware/node_modules/open": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", @@ -4536,32 +4409,32 @@ } }, "node_modules/@react-native/gradle-plugin": { - "version": "0.73.4", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.73.4.tgz", - "integrity": "sha512-PMDnbsZa+tD55Ug+W8CfqXiGoGneSSyrBZCMb5JfiB3AFST3Uj5e6lw8SgI/B6SKZF7lG0BhZ6YHZsRZ5MlXmg==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.74.83.tgz", + "integrity": "sha512-Pw2BWVyOHoBuJVKxGVYF6/GSZRf6+v1Ygc+ULGz5t20N8qzRWPa2fRZWqoxsN7TkNLPsECYY8gooOl7okOcPAQ==", "peer": true, "engines": { "node": ">=18" } }, "node_modules/@react-native/js-polyfills": { - "version": "0.73.1", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.73.1.tgz", - "integrity": "sha512-ewMwGcumrilnF87H4jjrnvGZEaPFCAC4ebraEK+CurDDmwST/bIicI4hrOAv+0Z0F7DEK4O4H7r8q9vH7IbN4g==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.74.83.tgz", + "integrity": "sha512-/t74n8r6wFhw4JEoOj3bN71N1NDLqaawB75uKAsSjeCwIR9AfCxlzZG0etsXtOexkY9KMeZIQ7YwRPqUdNXuqw==", "peer": true, "engines": { "node": ">=18" } }, "node_modules/@react-native/metro-babel-transformer": { - "version": "0.73.15", - "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.73.15.tgz", - "integrity": "sha512-LlkSGaXCz+xdxc9819plmpsl4P4gZndoFtpjN3GMBIu6f7TBV0GVbyJAU4GE8fuAWPVSVL5ArOcdkWKSbI1klw==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.74.83.tgz", + "integrity": "sha512-hGdx5N8diu8y+GW/ED39vTZa9Jx1di2ZZ0aapbhH4egN1agIAusj5jXTccfNBwwWF93aJ5oVbRzfteZgjbutKg==", "peer": true, "dependencies": { "@babel/core": "^7.20.0", - "@react-native/babel-preset": "0.73.21", - "hermes-parser": "0.15.0", + "@react-native/babel-preset": "0.74.83", + "hermes-parser": "0.19.1", "nullthrows": "^1.1.1" }, "engines": { @@ -4572,21 +4445,21 @@ } }, "node_modules/@react-native/metro-babel-transformer/node_modules/@babel/core": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", - "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.4", + "@babel/generator": "^7.24.5", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.4", - "@babel/parser": "^7.24.4", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -4640,11 +4513,76 @@ } }, "node_modules/@react-native/normalize-colors": { - "version": "0.73.2", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.73.2.tgz", - "integrity": "sha512-bRBcb2T+I88aG74LMVHaKms2p/T8aQd8+BZ7LuuzXlRfog1bMWWn/C5i0HVuvW4RPtXQYgIlGiXVDy9Ir1So/w==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.83.tgz", + "integrity": "sha512-jhCY95gRDE44qYawWVvhTjTplW1g+JtKTKM3f8xYT1dJtJ8QWv+gqEtKcfmOHfDkSDaMKG0AGBaDTSK8GXLH8Q==", "peer": true }, + "node_modules/@rnx-kit/chromium-edge-launcher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rnx-kit/chromium-edge-launcher/-/chromium-edge-launcher-1.0.0.tgz", + "integrity": "sha512-lzD84av1ZQhYUS+jsGqJiCMaJO2dn9u+RTT9n9q6D3SaKVwWqv+7AoRKqBu19bkwyE+iFRl1ymr40QS90jVFYg==", + "peer": true, + "dependencies": { + "@types/node": "^18.0.0", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=14.15" + } + }, + "node_modules/@rnx-kit/chromium-edge-launcher/node_modules/@types/node": { + "version": "18.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.33.tgz", + "integrity": "sha512-NR9+KrpSajr2qBVp/Yt5TU/rp+b5Mayi3+OlMlcg2cVCfRmcG5PWZ7S4+MG9PZ5gWBoc9Pd0BKSRViuBCRPu0A==", + "peer": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@rnx-kit/chromium-edge-launcher/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@rnx-kit/chromium-edge-launcher/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "peer": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rnx-kit/chromium-edge-launcher/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -4774,6 +4712,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -4786,9 +4733,9 @@ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" }, "node_modules/@types/react": { - "version": "16.9.56", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.56.tgz", - "integrity": "sha512-gIkl4J44G/qxbuC6r2Xh+D3CGZpJ+NdWTItAPmZbR5mUS+JQ8Zvzpl0ea5qT/ZT3ZNTUcDKUVqV3xBE8wv/DyQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", + "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4800,6 +4747,11 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "peer": true }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -6248,6 +6200,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001587", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz", @@ -6347,59 +6307,6 @@ "node": ">=6.0" } }, - "node_modules/chromium-edge-launcher": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-1.0.0.tgz", - "integrity": "sha512-pgtgjNKZ7i5U++1g1PWv75umkHvhVTDOQIZ+sjeUX9483S7Y6MUvO0lrd7ShGlQlFHMN4SwKTCq/X8hWrbv2KA==", - "peer": true, - "dependencies": { - "@types/node": "*", - "escape-string-regexp": "^4.0.0", - "is-wsl": "^2.2.0", - "lighthouse-logger": "^1.0.0", - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - } - }, - "node_modules/chromium-edge-launcher/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/chromium-edge-launcher/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "peer": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/chromium-edge-launcher/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -6816,15 +6723,33 @@ "node": "*" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/csstype": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.4.tgz", - "integrity": "sha512-xc8DUsCLmjvCfoD7LTGE0ou2MIWLx0K9RCZwSHMOdynqRsP4MtUcLeqh1HcQ2dInwDTqn+3CE0/FZh1et+p4jA==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", + "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==", "peer": true }, "node_modules/debug": { @@ -6924,31 +6849,6 @@ "node": ">= 0.8" } }, - "node_modules/deprecated-react-native-prop-types": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-5.0.0.tgz", - "integrity": "sha512-cIK8KYiiGVOFsKdPMmm1L3tA/Gl+JopXL6F5+C7x39MyPsQYnP57Im/D6bNUzcborD7fcMwiwZqcBdBXXZucYQ==", - "peer": true, - "dependencies": { - "@react-native/normalize-colors": "^0.73.0", - "invariant": "^2.2.4", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/deprecated-react-native-prop-types/node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "peer": true, - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "node_modules/des.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", @@ -7084,16 +6984,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "optional": true, - "peer": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -7770,18 +7660,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/execa/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7792,7 +7670,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -7842,7 +7719,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -7955,9 +7831,9 @@ "peer": true }, "node_modules/flow-parser": { - "version": "0.206.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.206.0.tgz", - "integrity": "sha512-HVzoK3r6Vsg+lKvlIZzaWNBVai+FXTX1wdYhz/wVlH13tb/gOdLXmlTqy6odmTBhT5UoWUbq0k8263Qhr9d88w==", + "version": "0.235.1", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.235.1.tgz", + "integrity": "sha512-s04193L4JE+ntEcQXbD6jxRRlyj9QXcgEl2W6xSjH4l9x4b0eHoCHfbYHjqf9LdZFUiM5LhgpiqsvLj/AyOyYQ==", "peer": true, "engines": { "node": ">=0.4.0" @@ -8119,7 +7995,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -8286,18 +8161,18 @@ } }, "node_modules/hermes-estree": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.15.0.tgz", - "integrity": "sha512-lLYvAd+6BnOqWdnNbP/Q8xfl8LOGw4wVjfrNd9Gt8eoFzhNBRVD95n4l2ksfMVOoxuVyegs85g83KS9QOsxbVQ==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.19.1.tgz", + "integrity": "sha512-daLGV3Q2MKk8w4evNMKwS8zBE/rcpA800nu1Q5kM08IKijoSnPe9Uo1iIxzPKRkn95IxxsgBMPeYHt3VG4ej2g==", "peer": true }, "node_modules/hermes-parser": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.15.0.tgz", - "integrity": "sha512-Q1uks5rjZlE9RjMMjSUCkGrEIPI5pKJILeCtK1VmTj7U4pf3wVPoo+cxfu+s4cBAPy2JzikIIdCZgBoR6x7U1Q==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.19.1.tgz", + "integrity": "sha512-Vp+bXzxYJWrpEuJ/vXxUsLnt0+y4q9zyi4zUlkLqD8FKv4LjIfOvP69R/9Lty3dCyKh0E2BU7Eypqr63/rKT/A==", "peer": true, "dependencies": { - "hermes-estree": "0.15.0" + "hermes-estree": "0.19.1" } }, "node_modules/hermes-profile-transformer": { @@ -8386,19 +8261,6 @@ "node": ">=8.12.0" } }, - "node_modules/iconv-lite": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", - "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", - "optional": true, - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -8728,7 +8590,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8745,7 +8606,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -8806,6 +8666,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", @@ -9007,9 +8878,9 @@ } }, "node_modules/jest-message-util/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "peer": true }, "node_modules/jest-message-util/node_modules/slash": { @@ -9236,9 +9107,9 @@ } }, "node_modules/jest-validate/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "peer": true }, "node_modules/jest-validate/node_modules/supports-color": { @@ -9292,9 +9163,9 @@ } }, "node_modules/joi": { - "version": "17.12.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.3.tgz", - "integrity": "sha512-2RRziagf555owrm9IRVtdKynOBeITiDpuZqIpgwqXShPncPKNiRQoiGsl/T8SQdq+8ugRzH2LqY67irr2y/d+g==", + "version": "17.13.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.1.tgz", + "integrity": "sha512-vaBlIKCyo4FCUtCm7Eu4QZd/q02bWcxfUO6YSXAZOWF6gzcLBeba8kwotUdYJjDLW8Cz8RywsSOqiNJZW0mNvg==", "peer": true, "dependencies": { "@hapi/hoek": "^9.3.0", @@ -10096,15 +9967,14 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } }, "node_modules/metro": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.80.8.tgz", - "integrity": "sha512-in7S0W11mg+RNmcXw+2d9S3zBGmCARDxIwoXJAmLUQOQoYsRP3cpGzyJtc7WOw8+FXfpgXvceD0u+PZIHXEL7g==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.80.9.tgz", + "integrity": "sha512-Bc57Xf3GO2Xe4UWQsBj/oW6YfLPABEu8jfDVDiNmJvoQW4CO34oDPuYKe4KlXzXhcuNsqOtSxpbjCRRVjhhREg==", "peer": true, "dependencies": { "@babel/code-frame": "^7.0.0", @@ -10128,18 +9998,18 @@ "jest-worker": "^29.6.3", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.80.8", - "metro-cache": "0.80.8", - "metro-cache-key": "0.80.8", - "metro-config": "0.80.8", - "metro-core": "0.80.8", - "metro-file-map": "0.80.8", - "metro-resolver": "0.80.8", - "metro-runtime": "0.80.8", - "metro-source-map": "0.80.8", - "metro-symbolicate": "0.80.8", - "metro-transform-plugins": "0.80.8", - "metro-transform-worker": "0.80.8", + "metro-babel-transformer": "0.80.9", + "metro-cache": "0.80.9", + "metro-cache-key": "0.80.9", + "metro-config": "0.80.9", + "metro-core": "0.80.9", + "metro-file-map": "0.80.9", + "metro-resolver": "0.80.9", + "metro-runtime": "0.80.9", + "metro-source-map": "0.80.9", + "metro-symbolicate": "0.80.9", + "metro-transform-plugins": "0.80.9", + "metro-transform-worker": "0.80.9", "mime-types": "^2.1.27", "node-fetch": "^2.2.0", "nullthrows": "^1.1.1", @@ -10159,9 +10029,9 @@ } }, "node_modules/metro-babel-transformer": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.80.8.tgz", - "integrity": "sha512-TTzNwRZb2xxyv4J/+yqgtDAP2qVqH3sahsnFu6Xv4SkLqzrivtlnyUbaeTdJ9JjtADJUEjCbgbFgUVafrXdR9Q==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.80.9.tgz", + "integrity": "sha512-d76BSm64KZam1nifRZlNJmtwIgAeZhZG3fi3K+EmPOlrR8rDtBxQHDSN3fSGeNB9CirdTyabTMQCkCup6BXFSQ==", "peer": true, "dependencies": { "@babel/core": "^7.20.0", @@ -10173,21 +10043,21 @@ } }, "node_modules/metro-babel-transformer/node_modules/@babel/core": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", - "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.4", + "@babel/generator": "^7.24.5", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.4", - "@babel/parser": "^7.24.4", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -10256,12 +10126,12 @@ } }, "node_modules/metro-cache": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.80.8.tgz", - "integrity": "sha512-5svz+89wSyLo7BxdiPDlwDTgcB9kwhNMfNhiBZPNQQs1vLFXxOkILwQiV5F2EwYT9DEr6OPZ0hnJkZfRQ8lDYQ==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.80.9.tgz", + "integrity": "sha512-ujEdSI43QwI+Dj2xuNax8LMo8UgKuXJEdxJkzGPU6iIx42nYa1byQ+aADv/iPh5sh5a//h5FopraW5voXSgm2w==", "peer": true, "dependencies": { - "metro-core": "0.80.8", + "metro-core": "0.80.9", "rimraf": "^3.0.2" }, "engines": { @@ -10269,9 +10139,9 @@ } }, "node_modules/metro-cache-key": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.80.8.tgz", - "integrity": "sha512-qWKzxrLsRQK5m3oH8ePecqCc+7PEhR03cJE6Z6AxAj0idi99dHOSitTmY0dclXVB9vP2tQIAE8uTd8xkYGk8fA==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.80.9.tgz", + "integrity": "sha512-hRcYGhEiWIdM87hU0fBlcGr+tHDEAT+7LYNCW89p5JhErFt/QaAkVx4fb5bW3YtXGv5BTV7AspWPERoIb99CXg==", "peer": true, "engines": { "node": ">=18" @@ -10293,18 +10163,18 @@ } }, "node_modules/metro-config": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.80.8.tgz", - "integrity": "sha512-VGQJpfJawtwRzGzGXVUoohpIkB0iPom4DmSbAppKfumdhtLA8uVeEPp2GM61kL9hRvdbMhdWA7T+hZFDlo4mJA==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.80.9.tgz", + "integrity": "sha512-28wW7CqS3eJrunRGnsibWldqgwRP9ywBEf7kg+uzUHkSFJNKPM1K3UNSngHmH0EZjomizqQA2Zi6/y6VdZMolg==", "peer": true, "dependencies": { "connect": "^3.6.5", "cosmiconfig": "^5.0.5", "jest-validate": "^29.6.3", - "metro": "0.80.8", - "metro-cache": "0.80.8", - "metro-core": "0.80.8", - "metro-runtime": "0.80.8" + "metro": "0.80.9", + "metro-cache": "0.80.9", + "metro-core": "0.80.9", + "metro-runtime": "0.80.9" }, "engines": { "node": ">=18" @@ -10361,22 +10231,22 @@ } }, "node_modules/metro-core": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.80.8.tgz", - "integrity": "sha512-g6lud55TXeISRTleW6SHuPFZHtYrpwNqbyFIVd9j9Ofrb5IReiHp9Zl8xkAfZQp8v6ZVgyXD7c130QTsCz+vBw==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.80.9.tgz", + "integrity": "sha512-tbltWQn+XTdULkGdzHIxlxk4SdnKxttvQQV3wpqqFbHDteR4gwCyTR2RyYJvxgU7HELfHtrVbqgqAdlPByUSbg==", "peer": true, "dependencies": { "lodash.throttle": "^4.1.1", - "metro-resolver": "0.80.8" + "metro-resolver": "0.80.9" }, "engines": { "node": ">=18" } }, "node_modules/metro-file-map": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.80.8.tgz", - "integrity": "sha512-eQXMFM9ogTfDs2POq7DT2dnG7rayZcoEgRbHPXvhUWkVwiKkro2ngcBE++ck/7A36Cj5Ljo79SOkYwHaWUDYDw==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.80.9.tgz", + "integrity": "sha512-sBUjVtQMHagItJH/wGU9sn3k2u0nrCl0CdR4SFMO1tksXLKbkigyQx4cbpcyPVOAmGTVuy3jyvBlELaGCAhplQ==", "peer": true, "dependencies": { "anymatch": "^3.0.3", @@ -10437,9 +10307,9 @@ } }, "node_modules/metro-minify-terser": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.80.8.tgz", - "integrity": "sha512-y8sUFjVvdeUIINDuW1sejnIjkZfEF+7SmQo0EIpYbWmwh+kq/WMj74yVaBWuqNjirmUp1YNfi3alT67wlbBWBQ==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.80.9.tgz", + "integrity": "sha512-FEeCeFbkvvPuhjixZ1FYrXtO0araTpV6UbcnGgDUpH7s7eR5FG/PiJz3TsuuPP/HwCK19cZtQydcA2QrCw446A==", "peer": true, "dependencies": { "terser": "^5.15.0" @@ -10449,18 +10319,18 @@ } }, "node_modules/metro-resolver": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.80.8.tgz", - "integrity": "sha512-JdtoJkP27GGoZ2HJlEsxs+zO7jnDUCRrmwXJozTlIuzLHMRrxgIRRby9fTCbMhaxq+iA9c+wzm3iFb4NhPmLbQ==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.80.9.tgz", + "integrity": "sha512-wAPIjkN59BQN6gocVsAvvpZ1+LQkkqUaswlT++cJafE/e54GoVkMNCmrR4BsgQHr9DknZ5Um/nKueeN7kaEz9w==", "peer": true, "engines": { "node": ">=18" } }, "node_modules/metro-runtime": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.80.8.tgz", - "integrity": "sha512-2oScjfv6Yb79PelU1+p8SVrCMW9ZjgEiipxq7jMRn8mbbtWzyv3g8Mkwr+KwOoDFI/61hYPUbY8cUnu278+x1g==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.80.9.tgz", + "integrity": "sha512-8PTVIgrVcyU+X/rVCy/9yxNlvXsBCk5JwwkbAm/Dm+Abo6NBGtNjWF0M1Xo/NWCb4phamNWcD7cHdR91HhbJvg==", "peer": true, "dependencies": { "@babel/runtime": "^7.0.0" @@ -10470,17 +10340,17 @@ } }, "node_modules/metro-source-map": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.80.8.tgz", - "integrity": "sha512-+OVISBkPNxjD4eEKhblRpBf463nTMk3KMEeYS8Z4xM/z3qujGJGSsWUGRtH27+c6zElaSGtZFiDMshEb8mMKQg==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.80.9.tgz", + "integrity": "sha512-RMn+XS4VTJIwMPOUSj61xlxgBvPeY4G6s5uIn6kt6HB6A/k9ekhr65UkkDD7WzHYs3a9o869qU8tvOZvqeQzgw==", "peer": true, "dependencies": { "@babel/traverse": "^7.20.0", "@babel/types": "^7.20.0", "invariant": "^2.2.4", - "metro-symbolicate": "0.80.8", + "metro-symbolicate": "0.80.9", "nullthrows": "^1.1.1", - "ob1": "0.80.8", + "ob1": "0.80.9", "source-map": "^0.5.6", "vlq": "^1.0.0" }, @@ -10489,13 +10359,13 @@ } }, "node_modules/metro-symbolicate": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.80.8.tgz", - "integrity": "sha512-nwhYySk79jQhwjL9QmOUo4wS+/0Au9joEryDWw7uj4kz2yvw1uBjwmlql3BprQCBzRdB3fcqOP8kO8Es+vE31g==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.80.9.tgz", + "integrity": "sha512-Ykae12rdqSs98hg41RKEToojuIW85wNdmSe/eHUgMkzbvCFNVgcC0w3dKZEhSsqQOXapXRlLtHkaHLil0UD/EA==", "peer": true, "dependencies": { "invariant": "^2.2.4", - "metro-source-map": "0.80.8", + "metro-source-map": "0.80.9", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "through2": "^2.0.1", @@ -10509,9 +10379,9 @@ } }, "node_modules/metro-transform-plugins": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.80.8.tgz", - "integrity": "sha512-sSu8VPL9Od7w98MftCOkQ1UDeySWbsIAS5I54rW22BVpPnI3fQ42srvqMLaJUQPjLehUanq8St6OMBCBgH/UWw==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.80.9.tgz", + "integrity": "sha512-UlDk/uc8UdfLNJhPbF3tvwajyuuygBcyp+yBuS/q0z3QSuN/EbLllY3rK8OTD9n4h00qZ/qgxGv/lMFJkwP4vg==", "peer": true, "dependencies": { "@babel/core": "^7.20.0", @@ -10525,21 +10395,21 @@ } }, "node_modules/metro-transform-plugins/node_modules/@babel/core": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", - "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.4", + "@babel/generator": "^7.24.5", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.4", - "@babel/parser": "^7.24.4", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -10593,22 +10463,22 @@ } }, "node_modules/metro-transform-worker": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.80.8.tgz", - "integrity": "sha512-+4FG3TQk3BTbNqGkFb2uCaxYTfsbuFOCKMMURbwu0ehCP8ZJuTUramkaNZoATS49NSAkRgUltgmBa4YaKZ5mqw==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.80.9.tgz", + "integrity": "sha512-c/IrzMUVnI0hSVVit4TXzt3A1GiUltGVlzCmLJWxNrBGHGrJhvgePj38+GXl1Xf4Fd4vx6qLUkKMQ3ux73bFLQ==", "peer": true, "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.0", "@babel/parser": "^7.20.0", "@babel/types": "^7.20.0", - "metro": "0.80.8", - "metro-babel-transformer": "0.80.8", - "metro-cache": "0.80.8", - "metro-cache-key": "0.80.8", - "metro-minify-terser": "0.80.8", - "metro-source-map": "0.80.8", - "metro-transform-plugins": "0.80.8", + "metro": "0.80.9", + "metro-babel-transformer": "0.80.9", + "metro-cache": "0.80.9", + "metro-cache-key": "0.80.9", + "metro-minify-terser": "0.80.9", + "metro-source-map": "0.80.9", + "metro-transform-plugins": "0.80.9", "nullthrows": "^1.1.1" }, "engines": { @@ -10616,21 +10486,21 @@ } }, "node_modules/metro-transform-worker/node_modules/@babel/core": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", - "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.4", + "@babel/generator": "^7.24.5", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.4", - "@babel/parser": "^7.24.4", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -10684,21 +10554,21 @@ } }, "node_modules/metro/node_modules/@babel/core": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", - "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.4", + "@babel/generator": "^7.24.5", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.4", - "@babel/parser": "^7.24.4", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -10851,26 +10721,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "peer": true }, - "node_modules/metro/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "peer": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/metro/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -11104,16 +10954,45 @@ "node": ">= 0.10.5" } }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "peer": true - }, - "node_modules/node-libs-browser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", - "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "peer": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "peer": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "peer": true + }, + "node_modules/node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", "dev": true, "dependencies": { "assert": "^1.1.1", @@ -11225,9 +11104,9 @@ "peer": true }, "node_modules/ob1": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.80.8.tgz", - "integrity": "sha512-QHJQk/lXMmAW8I7AIM3in1MSlwe1umR72Chhi8B7Xnq6mzjhBKkA6Fy/zAhQnGkA4S912EPCEvTij5yh+EQTAA==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.80.9.tgz", + "integrity": "sha512-v9yOxowkZbxWhKOaaTyLjIm1aLy4ebMNcSn4NYJKOAI/Qv+SkfEfszpLr2GIxsccmb2Y2HA9qtsqiIJ80ucpVA==", "peer": true, "engines": { "node": ">=18" @@ -11801,6 +11680,55 @@ "node": ">=6" } }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -12051,7 +11979,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -12096,27 +12023,39 @@ } }, "node_modules/react": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz", - "integrity": "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-devtools-core": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.28.5.tgz", - "integrity": "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-5.2.0.tgz", + "integrity": "sha512-vZK+/gvxxsieAoAyYaiRIVFxlajb7KXhgBDV7OsoMzaAE+IqGpoxusBjIgq5ibqA2IloKu0p9n7tE68z1xs18A==", "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, "node_modules/react-intl": { "version": "4.7.6", "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-4.7.6.tgz", @@ -12145,162 +12084,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/react-native": { - "version": "0.73.6", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.73.6.tgz", - "integrity": "sha512-oqmZe8D2/VolIzSPZw+oUd6j/bEmeRHwsLn1xLA5wllEYsZ5zNuMsDus235ONOnCRwexqof/J3aztyQswSmiaA==", - "peer": true, - "dependencies": { - "@jest/create-cache-key-function": "^29.6.3", - "@react-native-community/cli": "12.3.6", - "@react-native-community/cli-platform-android": "12.3.6", - "@react-native-community/cli-platform-ios": "12.3.6", - "@react-native/assets-registry": "0.73.1", - "@react-native/codegen": "0.73.3", - "@react-native/community-cli-plugin": "0.73.17", - "@react-native/gradle-plugin": "0.73.4", - "@react-native/js-polyfills": "0.73.1", - "@react-native/normalize-colors": "0.73.2", - "@react-native/virtualized-lists": "0.73.4", - "abort-controller": "^3.0.0", - "anser": "^1.4.9", - "ansi-regex": "^5.0.0", - "base64-js": "^1.5.1", - "chalk": "^4.0.0", - "deprecated-react-native-prop-types": "^5.0.0", - "event-target-shim": "^5.0.1", - "flow-enums-runtime": "^0.0.6", - "invariant": "^2.2.4", - "jest-environment-node": "^29.6.3", - "jsc-android": "^250231.0.0", - "memoize-one": "^5.0.0", - "metro-runtime": "^0.80.3", - "metro-source-map": "^0.80.3", - "mkdirp": "^0.5.1", - "nullthrows": "^1.1.1", - "pretty-format": "^26.5.2", - "promise": "^8.3.0", - "react-devtools-core": "^4.27.7", - "react-refresh": "^0.14.0", - "react-shallow-renderer": "^16.15.0", - "regenerator-runtime": "^0.13.2", - "scheduler": "0.24.0-canary-efb381bbf-20230505", - "stacktrace-parser": "^0.1.10", - "whatwg-fetch": "^3.0.0", - "ws": "^6.2.2", - "yargs": "^17.6.2" - }, - "bin": { - "react-native": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react": "18.2.0" - } - }, - "node_modules/react-native/node_modules/@react-native/virtualized-lists": { - "version": "0.73.4", - "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.73.4.tgz", - "integrity": "sha512-HpmLg1FrEiDtrtAbXiwCgXFYyloK/dOIPIuWW3fsqukwJEWAiTzm1nXGJ7xPU5XTHiWZ4sKup5Ebaj8z7iyWog==", - "peer": true, - "dependencies": { - "invariant": "^2.2.4", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react-native": "*" - } - }, - "node_modules/react-native/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-native/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-native/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "peer": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-native/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "peer": true - }, - "node_modules/react-native/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-native/node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "peer": true - }, - "node_modules/react-native/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-native/node_modules/ws": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", - "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", - "peer": true, - "dependencies": { - "async-limiter": "~1.0.0" - } - }, "node_modules/react-redux": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz", @@ -12342,9 +12125,9 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/react-refresh": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", - "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "peer": true, "engines": { "node": ">=0.10.0" @@ -12527,6 +12310,203 @@ "redux": ">=3" } }, + "node_modules/redux-offline/node_modules/@react-native-community/netinfo": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-4.7.0.tgz", + "integrity": "sha512-a/sDB+AsLEUNmhAUlAaTYeXKyQdFGBUfatqKkX5jluBo2CB3OAuTHfm7rSjcaLB9EmG5iSq3fOTpync2E7EYTA==", + "peerDependencies": { + "react-native": ">=0.59" + } + }, + "node_modules/redux-offline/node_modules/@react-native/virtualized-lists": { + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.74.83.tgz", + "integrity": "sha512-rmaLeE34rj7py4FxTod7iMTC7BAsm+HrGA8WxYmEJeyTV7WSaxAkosKoYBz8038mOiwnG9VwA/7FrB6bEQvn1A==", + "peer": true, + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^18.2.6", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/redux-offline/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/redux-offline/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/redux-offline/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/redux-offline/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "peer": true + }, + "node_modules/redux-offline/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux-offline/node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/redux-offline/node_modules/react-native": { + "version": "0.74.1", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.74.1.tgz", + "integrity": "sha512-0H2XpmghwOtfPpM2LKqHIN7gxy+7G/r1hwJHKLV6uoyXGC/gCojRtoo5NqyKrWpFC8cqyT6wTYCLuG7CxEKilg==", + "peer": true, + "dependencies": { + "@jest/create-cache-key-function": "^29.6.3", + "@react-native-community/cli": "13.6.6", + "@react-native-community/cli-platform-android": "13.6.6", + "@react-native-community/cli-platform-ios": "13.6.6", + "@react-native/assets-registry": "0.74.83", + "@react-native/codegen": "0.74.83", + "@react-native/community-cli-plugin": "0.74.83", + "@react-native/gradle-plugin": "0.74.83", + "@react-native/js-polyfills": "0.74.83", + "@react-native/normalize-colors": "0.74.83", + "@react-native/virtualized-lists": "0.74.83", + "abort-controller": "^3.0.0", + "anser": "^1.4.9", + "ansi-regex": "^5.0.0", + "base64-js": "^1.5.1", + "chalk": "^4.0.0", + "event-target-shim": "^5.0.1", + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "jest-environment-node": "^29.6.3", + "jsc-android": "^250231.0.0", + "memoize-one": "^5.0.0", + "metro-runtime": "^0.80.3", + "metro-source-map": "^0.80.3", + "mkdirp": "^0.5.1", + "nullthrows": "^1.1.1", + "pretty-format": "^26.5.2", + "promise": "^8.3.0", + "react-devtools-core": "^5.0.0", + "react-refresh": "^0.14.0", + "react-shallow-renderer": "^16.15.0", + "regenerator-runtime": "^0.13.2", + "scheduler": "0.24.0-canary-efb381bbf-20230505", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0", + "ws": "^6.2.2", + "yargs": "^17.6.2" + }, + "bin": { + "react-native": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^18.2.6", + "react": "18.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/redux-offline/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "peer": true + }, + "node_modules/redux-offline/node_modules/scheduler": { + "version": "0.24.0-canary-efb381bbf-20230505", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.24.0-canary-efb381bbf-20230505.tgz", + "integrity": "sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/redux-offline/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux-offline/node_modules/ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "peer": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, "node_modules/redux-persist": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-4.9.1.tgz", @@ -12747,7 +12727,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -12789,7 +12768,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -12813,13 +12791,6 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "optional": true, - "peer": true - }, "node_modules/sc-channel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/sc-channel/-/sc-channel-1.2.0.tgz", @@ -12839,9 +12810,9 @@ "integrity": "sha512-9PbqYBpCq+OoEeRQ3QfFIGE6qwjjBcd2j7UjgDlhnZbtSnuGgHdcRklPKYGuYFH82V/dwd+AIpu8XvA1zqTd+A==" }, "node_modules/scheduler": { - "version": "0.24.0-canary-efb381bbf-20230505", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.24.0-canary-efb381bbf-20230505.tgz", - "integrity": "sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "peer": true, "dependencies": { "loose-envify": "^1.1.0" @@ -12865,6 +12836,19 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "peer": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -13027,6 +13011,11 @@ "resolved": "https://registry.npmjs.org/shallow-equals/-/shallow-equals-1.0.0.tgz", "integrity": "sha1-JLdL8cY0wR7Uxxgqbfb7MA3OQ5A=" }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -13160,6 +13149,14 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -13478,6 +13475,43 @@ "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", "peer": true }, + "node_modules/styled-components": { + "version": "6.1.10", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.10.tgz", + "integrity": "sha512-4K8IKcn7iOt76riGLjvBhRyNPTkUKTvmnwoRFBOtJLswVvzy2VsoE2KOrfl9FJLQUYbITLJY2wfIZ3tjbkA/Zw==", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.38", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" + }, + "node_modules/styled-components/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -14269,9 +14303,9 @@ } }, "node_modules/whatwg-fetch": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.5.0.tgz", - "integrity": "sha512-jXkLtsR42xhXg7akoDKvKWE40eJeI+2KZqcp2h3NsOrRnDvtWX36KcKl30dy+hxECivdk2BVUHVNrPtoMBUx6A==", + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "peer": true }, "node_modules/whatwg-url": { @@ -14637,11 +14671,11 @@ } }, "@babel/generator": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", - "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", + "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", "requires": { - "@babel/types": "^7.24.0", + "@babel/types": "^7.24.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -14683,18 +14717,18 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.4.tgz", - "integrity": "sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", + "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", "requires": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.24.5", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.24.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-split-export-declaration": "^7.24.5", "semver": "^6.3.1" }, "dependencies": { @@ -14778,11 +14812,11 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", + "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", "requires": { - "@babel/types": "^7.23.0" + "@babel/types": "^7.24.5" } }, "@babel/helper-module-imports": { @@ -14794,15 +14828,15 @@ } }, "@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", + "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", "requires": { "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^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-optimise-call-expression": { @@ -14814,9 +14848,9 @@ } }, "@babel/helper-plugin-utils": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", - "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==" + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", + "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==" }, "@babel/helper-remap-async-to-generator": { "version": "7.22.20", @@ -14839,11 +14873,11 @@ } }, "@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", + "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", "requires": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.5" } }, "@babel/helper-skip-transparent-expression-wrappers": { @@ -14855,22 +14889,22 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", "requires": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.5" } }, "@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==" + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==" }, "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==" }, "@babel/helper-validator-option": { "version": "7.23.5", @@ -14888,13 +14922,13 @@ } }, "@babel/helpers": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz", - "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", + "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", "requires": { "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0" + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5" } }, "@babel/highlight": { @@ -14909,9 +14943,9 @@ } }, "@babel/parser": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", - "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==" + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.23.3", @@ -15462,14 +15496,14 @@ } }, "@babel/plugin-transform-private-property-in-object": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.1.tgz", - "integrity": "sha512-pTHxDVa0BpUbvAgX3Gat+7cSciXqUcY9j2VZKTbSB6+VQGpNgNO9ailxTGHSXlqOnX1Hcx1Enme2+yv7VqP9bg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.5.tgz", + "integrity": "sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==", "peer": true, "requires": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-create-class-features-plugin": "^7.24.5", + "@babel/helper-plugin-utils": "^7.24.5", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" } }, @@ -15511,12 +15545,12 @@ } }, "@babel/plugin-transform-react-jsx-self": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.1.tgz", - "integrity": "sha512-kDJgnPujTmAZ/9q2CN4m2/lRsUUPDvsG3+tSHWUJIzMGTt5U/b/fwWd3RO3n+5mjLrsBrVa5eKFRVSQbi3dF1w==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.5.tgz", + "integrity": "sha512-RtCJoUO2oYrYwFPtR1/jkoBEcFuI1ae9a9IMxeyAVa3a1Ap4AnxmyIKG2b2FaJKqkidw/0cxRbWN+HOs6ZWd1w==", "peer": true, "requires": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.5" } }, "@babel/plugin-transform-react-jsx-source": { @@ -15570,9 +15604,9 @@ }, "dependencies": { "@babel/helper-define-polyfill-provider": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz", - "integrity": "sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", "peer": true, "requires": { "@babel/helper-compilation-targets": "^7.22.6", @@ -15583,13 +15617,13 @@ } }, "babel-plugin-polyfill-corejs2": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz", - "integrity": "sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ==", + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", "peer": true, "requires": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.1", + "@babel/helper-define-polyfill-provider": "^0.6.2", "semver": "^6.3.1" } }, @@ -15604,12 +15638,12 @@ } }, "babel-plugin-polyfill-regenerator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz", - "integrity": "sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", "peer": true, "requires": { - "@babel/helper-define-polyfill-provider": "^0.6.1" + "@babel/helper-define-polyfill-provider": "^0.6.2" } }, "debug": { @@ -15883,18 +15917,18 @@ } }, "@babel/traverse": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", - "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", + "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", "requires": { - "@babel/code-frame": "^7.24.1", - "@babel/generator": "^7.24.1", + "@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.22.6", - "@babel/parser": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/types": "^7.24.5", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -15915,12 +15949,12 @@ } }, "@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", "requires": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", "to-fast-properties": "^2.0.0" } }, @@ -16006,11 +16040,18 @@ "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==", "dev": true }, + "@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "requires": { + "@emotion/memoize": "^0.8.1" + } + }, "@emotion/memoize": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", - "dev": true + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "@emotion/react": { "version": "11.11.0", @@ -16067,8 +16108,7 @@ "@emotion/unitless": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", - "dev": true + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, "@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", @@ -16403,7 +16443,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "requires": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -16412,34 +16451,31 @@ "@nodelib/fs.stat": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" }, "@nodelib/fs.walk": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "requires": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "@react-native-community/cli": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-12.3.6.tgz", - "integrity": "sha512-647OSi6xBb8FbwFqX9zsJxOzu685AWtrOUWHfOkbKD+5LOpGORw+GQo0F9rWZnB68rLQyfKUZWJeaD00pGv5fw==", - "peer": true, - "requires": { - "@react-native-community/cli-clean": "12.3.6", - "@react-native-community/cli-config": "12.3.6", - "@react-native-community/cli-debugger-ui": "12.3.6", - "@react-native-community/cli-doctor": "12.3.6", - "@react-native-community/cli-hermes": "12.3.6", - "@react-native-community/cli-plugin-metro": "12.3.6", - "@react-native-community/cli-server-api": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", - "@react-native-community/cli-types": "12.3.6", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-13.6.6.tgz", + "integrity": "sha512-IqclB7VQ84ye8Fcs89HOpOscY4284VZg2pojHNl8H0Lzd4DadXJWQoxC7zWm8v2f8eyeX2kdhxp2ETD5tceIgA==", + "peer": true, + "requires": { + "@react-native-community/cli-clean": "13.6.6", + "@react-native-community/cli-config": "13.6.6", + "@react-native-community/cli-debugger-ui": "13.6.6", + "@react-native-community/cli-doctor": "13.6.6", + "@react-native-community/cli-hermes": "13.6.6", + "@react-native-community/cli-server-api": "13.6.6", + "@react-native-community/cli-tools": "13.6.6", + "@react-native-community/cli-types": "13.6.6", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", @@ -16536,12 +16572,6 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "peer": true }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "peer": true - }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -16551,15 +16581,6 @@ "p-locate": "^4.1.0" } }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "peer": true, - "requires": { - "yallist": "^4.0.0" - } - }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -16591,13 +16612,10 @@ "peer": true }, "semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "peer": true, - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.1.tgz", + "integrity": "sha512-f/vbBsu+fOiYt+lmwZV0rVwJScl46HppnOA1ZvIuBWKOTlllpyJ3bfVax76/OrhCH38dyxoDIA8K7uB963IYgA==", + "peer": true }, "supports-color": { "version": "7.2.0", @@ -16605,26 +16623,21 @@ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "peer": true, "requires": { - "has-flag": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "peer": true + "has-flag": "^4.0.0" + } } } }, "@react-native-community/cli-clean": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-12.3.6.tgz", - "integrity": "sha512-gUU29ep8xM0BbnZjwz9MyID74KKwutq9x5iv4BCr2im6nly4UMf1B1D+V225wR7VcDGzbgWjaezsJShLLhC5ig==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-13.6.6.tgz", + "integrity": "sha512-cBwJTwl0NyeA4nyMxbhkWZhxtILYkbU3TW3k8AXLg+iGphe0zikYMGB3T+haTvTc6alTyEFwPbimk9bGIqkjAQ==", "peer": true, "requires": { - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-tools": "13.6.6", "chalk": "^4.1.2", - "execa": "^5.0.0" + "execa": "^5.0.0", + "fast-glob": "^3.3.2" }, "dependencies": { "ansi-styles": { @@ -16696,12 +16709,6 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "peer": true }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "peer": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -16714,16 +16721,16 @@ } }, "@react-native-community/cli-config": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-12.3.6.tgz", - "integrity": "sha512-JGWSYQ9EAK6m2v0abXwFLEfsqJ1zkhzZ4CV261QZF9MoUNB6h57a274h1MLQR9mG6Tsh38wBUuNfEPUvS1vYew==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-13.6.6.tgz", + "integrity": "sha512-mbG425zCKr8JZhv/j11382arezwS/70juWMsn8j2lmrGTrP1cUdW0MF15CCIFtJsqyK3Qs+FTmqttRpq81QfSg==", "peer": true, "requires": { - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-tools": "13.6.6", "chalk": "^4.1.2", "cosmiconfig": "^5.1.0", "deepmerge": "^4.3.0", - "glob": "^7.1.3", + "fast-glob": "^3.3.2", "joi": "^17.2.1" }, "dependencies": { @@ -16817,24 +16824,25 @@ } }, "@react-native-community/cli-debugger-ui": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-12.3.6.tgz", - "integrity": "sha512-SjUKKsx5FmcK9G6Pb6UBFT0s9JexVStK5WInmANw75Hm7YokVvHEgtprQDz2Uvy5znX5g2ujzrkIU//T15KQzA==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-13.6.6.tgz", + "integrity": "sha512-Vv9u6eS4vKSDAvdhA0OiQHoA7y39fiPIgJ6biT32tN4avHDtxlc6TWZGiqv7g98SBvDWvoVAmdPLcRf3kU+c8g==", "peer": true, "requires": { "serve-static": "^1.13.1" } }, "@react-native-community/cli-doctor": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-12.3.6.tgz", - "integrity": "sha512-fvBDv2lTthfw4WOQKkdTop2PlE9GtfrlNnpjB818MhcdEnPjfQw5YaTUcnNEGsvGomdCs1MVRMgYXXwPSN6OvQ==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-13.6.6.tgz", + "integrity": "sha512-TWZb5g6EmQe2Ua2TEWNmyaEayvlWH4GmdD9ZC+p8EpKFpB1NpDGMK6sXbpb42TDvwZg5s4TDRplK0PBEA/SVDg==", "peer": true, "requires": { - "@react-native-community/cli-config": "12.3.6", - "@react-native-community/cli-platform-android": "12.3.6", - "@react-native-community/cli-platform-ios": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-config": "13.6.6", + "@react-native-community/cli-platform-android": "13.6.6", + "@react-native-community/cli-platform-apple": "13.6.6", + "@react-native-community/cli-platform-ios": "13.6.6", + "@react-native-community/cli-tools": "13.6.6", "chalk": "^4.1.2", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", @@ -16924,29 +16932,11 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "peer": true }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "peer": true - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "peer": true, - "requires": { - "yallist": "^4.0.0" - } - }, "semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "peer": true, - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.1.tgz", + "integrity": "sha512-f/vbBsu+fOiYt+lmwZV0rVwJScl46HppnOA1ZvIuBWKOTlllpyJ3bfVax76/OrhCH38dyxoDIA8K7uB963IYgA==", + "peer": true }, "strip-ansi": { "version": "5.2.0", @@ -16966,28 +16956,22 @@ "has-flag": "^4.0.0" } }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "peer": true - }, "yaml": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", - "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", + "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", "peer": true } } }, "@react-native-community/cli-hermes": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-hermes/-/cli-hermes-12.3.6.tgz", - "integrity": "sha512-sNGwfOCl8OAIjWCkwuLpP8NZbuO0dhDI/2W7NeOGDzIBsf4/c4MptTrULWtGIH9okVPLSPX0NnRyGQ+mSwWyuQ==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-hermes/-/cli-hermes-13.6.6.tgz", + "integrity": "sha512-La5Ie+NGaRl3klei6WxKoOxmCUSGGxpOk6vU5pEGf0/O7ky+Ay0io+zXYUZqlNMi/cGpO7ZUijakBYOB/uyuFg==", "peer": true, "requires": { - "@react-native-community/cli-platform-android": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-platform-android": "13.6.6", + "@react-native-community/cli-tools": "13.6.6", "chalk": "^4.1.2", "hermes-profile-transformer": "^0.0.6" }, @@ -17044,16 +17028,16 @@ } }, "@react-native-community/cli-platform-android": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-12.3.6.tgz", - "integrity": "sha512-DeDDAB8lHpuGIAPXeeD9Qu2+/wDTFPo99c8uSW49L0hkmZJixzvvvffbGQAYk32H0TmaI7rzvzH+qzu7z3891g==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-13.6.6.tgz", + "integrity": "sha512-/tMwkBeNxh84syiSwNlYtmUz/Ppc+HfKtdopL/5RB+fd3SV1/5/NPNjMlyLNgFKnpxvKCInQ7dnl6jGHJjeHjg==", "peer": true, "requires": { - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-tools": "13.6.6", "chalk": "^4.1.2", "execa": "^5.0.0", + "fast-glob": "^3.3.2", "fast-xml-parser": "^4.2.4", - "glob": "^7.1.3", "logkitty": "^0.7.1" }, "dependencies": { @@ -17126,12 +17110,6 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "peer": true }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "peer": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -17143,17 +17121,17 @@ } } }, - "@react-native-community/cli-platform-ios": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-12.3.6.tgz", - "integrity": "sha512-3eZ0jMCkKUO58wzPWlvAPRqezVKm9EPZyaPyHbRPWU8qw7JqkvnRlWIaYDGpjCJgVW4k2hKsEursLtYKb188tg==", + "@react-native-community/cli-platform-apple": { + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-13.6.6.tgz", + "integrity": "sha512-bOmSSwoqNNT3AmCRZXEMYKz1Jf1l2F86Nhs7qBcXdY/sGiJ+Flng564LOqvdAlVLTbkgz47KjNKCS2pP4Jg0Mg==", "peer": true, "requires": { - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-tools": "13.6.6", "chalk": "^4.1.2", "execa": "^5.0.0", + "fast-glob": "^3.3.2", "fast-xml-parser": "^4.0.12", - "glob": "^7.1.3", "ora": "^5.4.1" }, "dependencies": { @@ -17226,12 +17204,6 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "peer": true }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "peer": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -17243,37 +17215,52 @@ } } }, - "@react-native-community/cli-plugin-metro": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-12.3.6.tgz", - "integrity": "sha512-3jxSBQt4fkS+KtHCPSyB5auIT+KKIrPCv9Dk14FbvOaEh9erUWEm/5PZWmtboW1z7CYeNbFMeXm9fM2xwtVOpg==", - "peer": true + "@react-native-community/cli-platform-ios": { + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-13.6.6.tgz", + "integrity": "sha512-vjDnRwhlSN5ryqKTas6/DPkxuouuyFBAqAROH4FR1cspTbn6v78JTZKDmtQy9JMMo7N5vZj1kASU5vbFep9IOQ==", + "peer": true, + "requires": { + "@react-native-community/cli-platform-apple": "13.6.6" + } }, "@react-native-community/cli-server-api": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-12.3.6.tgz", - "integrity": "sha512-80NIMzo8b2W+PL0Jd7NjiJW9mgaT8Y8wsIT/lh6mAvYH7mK0ecDJUYUTAAv79Tbo1iCGPAr3T295DlVtS8s4yQ==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-13.6.6.tgz", + "integrity": "sha512-ZtCXxoFlM7oDv3iZ3wsrT3SamhtUJuIkX2WePLPlN5bcbq7zimbPm2lHyicNJtpcGQ5ymsgpUWPCNZsWQhXBqQ==", "peer": true, "requires": { - "@react-native-community/cli-debugger-ui": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-debugger-ui": "13.6.6", + "@react-native-community/cli-tools": "13.6.6", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", "nocache": "^3.0.1", "pretty-format": "^26.6.2", "serve-static": "^1.13.1", - "ws": "^7.5.1" + "ws": "^6.2.2" + }, + "dependencies": { + "ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "peer": true, + "requires": { + "async-limiter": "~1.0.0" + } + } } }, "@react-native-community/cli-tools": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-12.3.6.tgz", - "integrity": "sha512-FPEvZn19UTMMXUp/piwKZSh8cMEfO8G3KDtOwo53O347GTcwNrKjgZGtLSPELBX2gr+YlzEft3CoRv2Qmo83fQ==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-13.6.6.tgz", + "integrity": "sha512-ptOnn4AJczY5njvbdK91k4hcYazDnGtEPrqIwEI+k/CTBHNdb27Rsm2OZ7ye6f7otLBqF8gj/hK6QzJs8CEMgw==", "peer": true, "requires": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", + "execa": "^5.0.0", "find-up": "^5.0.0", "mime": "^2.4.1", "node-fetch": "^2.6.0", @@ -17318,6 +17305,23 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "peer": true }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "peer": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -17328,12 +17332,24 @@ "path-exists": "^4.0.0" } }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "peer": true + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "peer": true }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "peer": true + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -17343,24 +17359,6 @@ "p-locate": "^5.0.0" } }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "peer": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "peer": true, - "requires": { - "whatwg-url": "^5.0.0" - } - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -17386,13 +17384,10 @@ "peer": true }, "semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "peer": true, - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.1.tgz", + "integrity": "sha512-f/vbBsu+fOiYt+lmwZV0rVwJScl46HppnOA1ZvIuBWKOTlllpyJ3bfVax76/OrhCH38dyxoDIA8K7uB963IYgA==", + "peer": true }, "supports-color": { "version": "7.2.0", @@ -17402,55 +17397,44 @@ "requires": { "has-flag": "^4.0.0" } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "peer": true } } }, "@react-native-community/cli-types": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-12.3.6.tgz", - "integrity": "sha512-xPqTgcUtZowQ8WKOkI9TLGBwH2bGggOC4d2FFaIRST3gTcjrEeGRNeR5aXCzJFIgItIft8sd7p2oKEdy90+01Q==", + "version": "13.6.6", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-13.6.6.tgz", + "integrity": "sha512-733iaYzlmvNK7XYbnWlMjdE+2k0hlTBJW071af/xb6Bs+hbJqBP9c03FZuYH2hFFwDDntwj05bkri/P7VgSxug==", "peer": true, "requires": { "joi": "^17.2.1" } }, - "@react-native-community/netinfo": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-4.7.0.tgz", - "integrity": "sha512-a/sDB+AsLEUNmhAUlAaTYeXKyQdFGBUfatqKkX5jluBo2CB3OAuTHfm7rSjcaLB9EmG5iSq3fOTpync2E7EYTA==", - "requires": {} - }, "@react-native/assets-registry": { - "version": "0.73.1", - "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.73.1.tgz", - "integrity": "sha512-2FgAbU7uKM5SbbW9QptPPZx8N9Ke2L7bsHb+EhAanZjFZunA9PaYtyjUQ1s7HD+zDVqOQIvjkpXSv7Kejd2tqg==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.74.83.tgz", + "integrity": "sha512-2vkLMVnp+YTZYTNSDIBZojSsjz8sl5PscP3j4GcV6idD8V978SZfwFlk8K0ti0BzRs11mzL0Pj17km597S/eTQ==", "peer": true }, "@react-native/babel-plugin-codegen": { - "version": "0.73.4", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.73.4.tgz", - "integrity": "sha512-XzRd8MJGo4Zc5KsphDHBYJzS1ryOHg8I2gOZDAUCGcwLFhdyGu1zBNDJYH2GFyDrInn9TzAbRIf3d4O+eltXQQ==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.74.83.tgz", + "integrity": "sha512-+S0st3t4Ro00bi9gjT1jnK8qTFOU+CwmziA7U9odKyWrCoRJrgmrvogq/Dr1YXlpFxexiGIupGut1VHxr+fxJA==", "peer": true, "requires": { - "@react-native/codegen": "0.73.3" + "@react-native/codegen": "0.74.83" } }, "@react-native/babel-preset": { - "version": "0.73.21", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.73.21.tgz", - "integrity": "sha512-WlFttNnySKQMeujN09fRmrdWqh46QyJluM5jdtDNrkl/2Hx6N4XeDUGhABvConeK95OidVO7sFFf7sNebVXogA==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.74.83.tgz", + "integrity": "sha512-KJuu3XyVh3qgyUer+rEqh9a/JoUxsDOzkJNfRpDyXiAyjDRoVch60X/Xa/NcEQ93iCVHAWs0yQ+XGNGIBCYE6g==", "peer": true, "requires": { "@babel/core": "^7.20.0", "@babel/plugin-proposal-async-generator-functions": "^7.0.0", "@babel/plugin-proposal-class-properties": "^7.18.0", "@babel/plugin-proposal-export-default-from": "^7.0.0", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.0", "@babel/plugin-proposal-numeric-separator": "^7.0.0", "@babel/plugin-proposal-object-rest-spread": "^7.20.0", @@ -17486,27 +17470,27 @@ "@babel/plugin-transform-typescript": "^7.5.0", "@babel/plugin-transform-unicode-regex": "^7.0.0", "@babel/template": "^7.0.0", - "@react-native/babel-plugin-codegen": "0.73.4", + "@react-native/babel-plugin-codegen": "0.74.83", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" }, "dependencies": { "@babel/core": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", - "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.4", + "@babel/generator": "^7.24.5", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.4", - "@babel/parser": "^7.24.4", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -17578,14 +17562,14 @@ } }, "@react-native/codegen": { - "version": "0.73.3", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.73.3.tgz", - "integrity": "sha512-sxslCAAb8kM06vGy9Jyh4TtvjhcP36k/rvj2QE2Jdhdm61KvfafCATSIsOfc0QvnduWFcpXUPvAVyYwuv7PYDg==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.74.83.tgz", + "integrity": "sha512-GgvgHS3Aa2J8/mp1uC/zU8HuTh8ZT5jz7a4mVMWPw7+rGyv70Ba8uOVBq6UH2Q08o617IATYc+0HfyzAfm4n0w==", "peer": true, "requires": { "@babel/parser": "^7.20.0", - "flow-parser": "^0.206.0", "glob": "^7.1.1", + "hermes-parser": "0.19.1", "invariant": "^2.2.4", "jscodeshift": "^0.14.0", "mkdirp": "^0.5.1", @@ -17593,21 +17577,22 @@ } }, "@react-native/community-cli-plugin": { - "version": "0.73.17", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.73.17.tgz", - "integrity": "sha512-F3PXZkcHg+1ARIr6FRQCQiB7ZAA+MQXGmq051metRscoLvgYJwj7dgC8pvgy0kexzUkHu5BNKrZeySzUft3xuQ==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.74.83.tgz", + "integrity": "sha512-7GAFjFOg1mFSj8bnFNQS4u8u7+QtrEeflUIDVZGEfBZQ3wMNI5ycBzbBGycsZYiq00Xvoc6eKFC7kvIaqeJpUQ==", "peer": true, "requires": { - "@react-native-community/cli-server-api": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", - "@react-native/dev-middleware": "0.73.8", - "@react-native/metro-babel-transformer": "0.73.15", + "@react-native-community/cli-server-api": "13.6.6", + "@react-native-community/cli-tools": "13.6.6", + "@react-native/dev-middleware": "0.74.83", + "@react-native/metro-babel-transformer": "0.74.83", "chalk": "^4.0.0", "execa": "^5.1.1", "metro": "^0.80.3", "metro-config": "^0.80.3", "metro-core": "^0.80.3", "node-fetch": "^2.2.0", + "querystring": "^0.2.1", "readline": "^1.3.0" }, "dependencies": { @@ -17680,21 +17665,12 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "peer": true }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "querystring": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", + "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", "peer": true }, - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "peer": true, - "requires": { - "whatwg-url": "^5.0.0" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -17707,39 +17683,32 @@ } }, "@react-native/debugger-frontend": { - "version": "0.73.3", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.73.3.tgz", - "integrity": "sha512-RgEKnWuoo54dh7gQhV7kvzKhXZEhpF9LlMdZolyhGxHsBqZ2gXdibfDlfcARFFifPIiaZ3lXuOVVa4ei+uPgTw==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.74.83.tgz", + "integrity": "sha512-RGQlVUegBRxAUF9c1ss1ssaHZh6CO+7awgtI9sDeU0PzDZY/40ImoPD5m0o0SI6nXoVzbPtcMGzU+VO590pRfA==", "peer": true }, "@react-native/dev-middleware": { - "version": "0.73.8", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.73.8.tgz", - "integrity": "sha512-oph4NamCIxkMfUL/fYtSsE+JbGOnrlawfQ0kKtDQ5xbOjPKotKoXqrs1eGwozNKv7FfQ393stk1by9a6DyASSg==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.74.83.tgz", + "integrity": "sha512-UH8iriqnf7N4Hpi20D7M2FdvSANwTVStwFCSD7VMU9agJX88Yk0D1T6Meh2RMhUu4kY2bv8sTkNRm7LmxvZqgA==", "peer": true, "requires": { "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.73.3", + "@react-native/debugger-frontend": "0.74.83", + "@rnx-kit/chromium-edge-launcher": "^1.0.0", "chrome-launcher": "^0.15.2", - "chromium-edge-launcher": "^1.0.0", "connect": "^3.6.5", "debug": "^2.2.0", "node-fetch": "^2.2.0", + "nullthrows": "^1.1.1", "open": "^7.0.3", + "selfsigned": "^2.4.1", "serve-static": "^1.13.1", "temp-dir": "^2.0.0", "ws": "^6.2.2" }, "dependencies": { - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "peer": true, - "requires": { - "whatwg-url": "^5.0.0" - } - }, "open": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", @@ -17762,45 +17731,45 @@ } }, "@react-native/gradle-plugin": { - "version": "0.73.4", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.73.4.tgz", - "integrity": "sha512-PMDnbsZa+tD55Ug+W8CfqXiGoGneSSyrBZCMb5JfiB3AFST3Uj5e6lw8SgI/B6SKZF7lG0BhZ6YHZsRZ5MlXmg==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.74.83.tgz", + "integrity": "sha512-Pw2BWVyOHoBuJVKxGVYF6/GSZRf6+v1Ygc+ULGz5t20N8qzRWPa2fRZWqoxsN7TkNLPsECYY8gooOl7okOcPAQ==", "peer": true }, "@react-native/js-polyfills": { - "version": "0.73.1", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.73.1.tgz", - "integrity": "sha512-ewMwGcumrilnF87H4jjrnvGZEaPFCAC4ebraEK+CurDDmwST/bIicI4hrOAv+0Z0F7DEK4O4H7r8q9vH7IbN4g==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.74.83.tgz", + "integrity": "sha512-/t74n8r6wFhw4JEoOj3bN71N1NDLqaawB75uKAsSjeCwIR9AfCxlzZG0etsXtOexkY9KMeZIQ7YwRPqUdNXuqw==", "peer": true }, "@react-native/metro-babel-transformer": { - "version": "0.73.15", - "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.73.15.tgz", - "integrity": "sha512-LlkSGaXCz+xdxc9819plmpsl4P4gZndoFtpjN3GMBIu6f7TBV0GVbyJAU4GE8fuAWPVSVL5ArOcdkWKSbI1klw==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.74.83.tgz", + "integrity": "sha512-hGdx5N8diu8y+GW/ED39vTZa9Jx1di2ZZ0aapbhH4egN1agIAusj5jXTccfNBwwWF93aJ5oVbRzfteZgjbutKg==", "peer": true, "requires": { "@babel/core": "^7.20.0", - "@react-native/babel-preset": "0.73.21", - "hermes-parser": "0.15.0", + "@react-native/babel-preset": "0.74.83", + "hermes-parser": "0.19.1", "nullthrows": "^1.1.1" }, "dependencies": { "@babel/core": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", - "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.4", + "@babel/generator": "^7.24.5", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.4", - "@babel/parser": "^7.24.4", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -17838,11 +17807,57 @@ } }, "@react-native/normalize-colors": { - "version": "0.73.2", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.73.2.tgz", - "integrity": "sha512-bRBcb2T+I88aG74LMVHaKms2p/T8aQd8+BZ7LuuzXlRfog1bMWWn/C5i0HVuvW4RPtXQYgIlGiXVDy9Ir1So/w==", + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.83.tgz", + "integrity": "sha512-jhCY95gRDE44qYawWVvhTjTplW1g+JtKTKM3f8xYT1dJtJ8QWv+gqEtKcfmOHfDkSDaMKG0AGBaDTSK8GXLH8Q==", "peer": true }, + "@rnx-kit/chromium-edge-launcher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rnx-kit/chromium-edge-launcher/-/chromium-edge-launcher-1.0.0.tgz", + "integrity": "sha512-lzD84av1ZQhYUS+jsGqJiCMaJO2dn9u+RTT9n9q6D3SaKVwWqv+7AoRKqBu19bkwyE+iFRl1ymr40QS90jVFYg==", + "peer": true, + "requires": { + "@types/node": "^18.0.0", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "dependencies": { + "@types/node": { + "version": "18.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.33.tgz", + "integrity": "sha512-NR9+KrpSajr2qBVp/Yt5TU/rp+b5Mayi3+OlMlcg2cVCfRmcG5PWZ7S4+MG9PZ5gWBoc9Pd0BKSRViuBCRPu0A==", + "peer": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "peer": true + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "peer": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "peer": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, "@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -17972,6 +17987,15 @@ "undici-types": "~5.26.4" } }, + "@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "peer": true, + "requires": { + "@types/node": "*" + } + }, "@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -17984,9 +18008,9 @@ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" }, "@types/react": { - "version": "16.9.56", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.56.tgz", - "integrity": "sha512-gIkl4J44G/qxbuC6r2Xh+D3CGZpJ+NdWTItAPmZbR5mUS+JQ8Zvzpl0ea5qT/ZT3ZNTUcDKUVqV3xBE8wv/DyQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", + "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -17998,6 +18022,11 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "peer": true }, + "@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" + }, "@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -19091,6 +19120,11 @@ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "peer": true }, + "camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==" + }, "caniuse-lite": { "version": "1.0.30001587", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz", @@ -19152,43 +19186,6 @@ "tslib": "^1.9.0" } }, - "chromium-edge-launcher": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-1.0.0.tgz", - "integrity": "sha512-pgtgjNKZ7i5U++1g1PWv75umkHvhVTDOQIZ+sjeUX9483S7Y6MUvO0lrd7ShGlQlFHMN4SwKTCq/X8hWrbv2KA==", - "peer": true, - "requires": { - "@types/node": "*", - "escape-string-regexp": "^4.0.0", - "is-wsl": "^2.2.0", - "lighthouse-logger": "^1.0.0", - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "dependencies": { - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "peer": true - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "peer": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "peer": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, "ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -19529,15 +19526,30 @@ "randomfill": "^1.0.3" } }, + "css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==" + }, + "css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "requires": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "csstype": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.4.tgz", - "integrity": "sha512-xc8DUsCLmjvCfoD7LTGE0ou2MIWLx0K9RCZwSHMOdynqRsP4MtUcLeqh1HcQ2dInwDTqn+3CE0/FZh1et+p4jA==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", + "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==", "peer": true }, "debug": { @@ -19615,30 +19627,6 @@ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "peer": true }, - "deprecated-react-native-prop-types": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-5.0.0.tgz", - "integrity": "sha512-cIK8KYiiGVOFsKdPMmm1L3tA/Gl+JopXL6F5+C7x39MyPsQYnP57Im/D6bNUzcborD7fcMwiwZqcBdBXXZucYQ==", - "peer": true, - "requires": { - "@react-native/normalize-colors": "^0.73.0", - "invariant": "^2.2.4", - "prop-types": "^15.8.1" - }, - "dependencies": { - "prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "peer": true, - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - } - } - }, "des.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", @@ -19757,16 +19745,6 @@ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "peer": true }, - "encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "optional": true, - "peer": true, - "requires": { - "iconv-lite": "^0.6.2" - } - }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -20277,14 +20255,6 @@ "onetime": "^5.1.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" - }, - "dependencies": { - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - } } }, "fast-deep-equal": { @@ -20297,7 +20267,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -20331,7 +20300,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "requires": { "reusify": "^1.0.4" } @@ -20426,9 +20394,9 @@ "peer": true }, "flow-parser": { - "version": "0.206.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.206.0.tgz", - "integrity": "sha512-HVzoK3r6Vsg+lKvlIZzaWNBVai+FXTX1wdYhz/wVlH13tb/gOdLXmlTqy6odmTBhT5UoWUbq0k8263Qhr9d88w==", + "version": "0.235.1", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.235.1.tgz", + "integrity": "sha512-s04193L4JE+ntEcQXbD6jxRRlyj9QXcgEl2W6xSjH4l9x4b0eHoCHfbYHjqf9LdZFUiM5LhgpiqsvLj/AyOyYQ==", "peer": true }, "form-data": { @@ -20547,7 +20515,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -20665,18 +20632,18 @@ } }, "hermes-estree": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.15.0.tgz", - "integrity": "sha512-lLYvAd+6BnOqWdnNbP/Q8xfl8LOGw4wVjfrNd9Gt8eoFzhNBRVD95n4l2ksfMVOoxuVyegs85g83KS9QOsxbVQ==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.19.1.tgz", + "integrity": "sha512-daLGV3Q2MKk8w4evNMKwS8zBE/rcpA800nu1Q5kM08IKijoSnPe9Uo1iIxzPKRkn95IxxsgBMPeYHt3VG4ej2g==", "peer": true }, "hermes-parser": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.15.0.tgz", - "integrity": "sha512-Q1uks5rjZlE9RjMMjSUCkGrEIPI5pKJILeCtK1VmTj7U4pf3wVPoo+cxfu+s4cBAPy2JzikIIdCZgBoR6x7U1Q==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.19.1.tgz", + "integrity": "sha512-Vp+bXzxYJWrpEuJ/vXxUsLnt0+y4q9zyi4zUlkLqD8FKv4LjIfOvP69R/9Lty3dCyKh0E2BU7Eypqr63/rKT/A==", "peer": true, "requires": { - "hermes-estree": "0.15.0" + "hermes-estree": "0.19.1" } }, "hermes-profile-transformer": { @@ -20754,16 +20721,6 @@ "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "dev": true }, - "iconv-lite": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", - "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", - "optional": true, - "peer": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -20997,8 +20954,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-fullwidth-code-point": { "version": "2.0.0", @@ -21009,7 +20965,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -21049,6 +21004,11 @@ "has-symbols": "^1.0.1" } }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, "is-string": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", @@ -21192,9 +21152,9 @@ } }, "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "peer": true }, "slash": { @@ -21364,9 +21324,9 @@ } }, "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "peer": true }, "supports-color": { @@ -21409,9 +21369,9 @@ } }, "joi": { - "version": "17.12.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.3.tgz", - "integrity": "sha512-2RRziagf555owrm9IRVtdKynOBeITiDpuZqIpgwqXShPncPKNiRQoiGsl/T8SQdq+8ugRzH2LqY67irr2y/d+g==", + "version": "17.13.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.1.tgz", + "integrity": "sha512-vaBlIKCyo4FCUtCm7Eu4QZd/q02bWcxfUO6YSXAZOWF6gzcLBeba8kwotUdYJjDLW8Cz8RywsSOqiNJZW0mNvg==", "peer": true, "requires": { "@hapi/hoek": "^9.3.0", @@ -22059,13 +22019,12 @@ "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, "metro": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.80.8.tgz", - "integrity": "sha512-in7S0W11mg+RNmcXw+2d9S3zBGmCARDxIwoXJAmLUQOQoYsRP3cpGzyJtc7WOw8+FXfpgXvceD0u+PZIHXEL7g==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.80.9.tgz", + "integrity": "sha512-Bc57Xf3GO2Xe4UWQsBj/oW6YfLPABEu8jfDVDiNmJvoQW4CO34oDPuYKe4KlXzXhcuNsqOtSxpbjCRRVjhhREg==", "peer": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -22089,18 +22048,18 @@ "jest-worker": "^29.6.3", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.80.8", - "metro-cache": "0.80.8", - "metro-cache-key": "0.80.8", - "metro-config": "0.80.8", - "metro-core": "0.80.8", - "metro-file-map": "0.80.8", - "metro-resolver": "0.80.8", - "metro-runtime": "0.80.8", - "metro-source-map": "0.80.8", - "metro-symbolicate": "0.80.8", - "metro-transform-plugins": "0.80.8", - "metro-transform-worker": "0.80.8", + "metro-babel-transformer": "0.80.9", + "metro-cache": "0.80.9", + "metro-cache-key": "0.80.9", + "metro-config": "0.80.9", + "metro-core": "0.80.9", + "metro-file-map": "0.80.9", + "metro-resolver": "0.80.9", + "metro-runtime": "0.80.9", + "metro-source-map": "0.80.9", + "metro-symbolicate": "0.80.9", + "metro-transform-plugins": "0.80.9", + "metro-transform-worker": "0.80.9", "mime-types": "^2.1.27", "node-fetch": "^2.2.0", "nullthrows": "^1.1.1", @@ -22114,21 +22073,21 @@ }, "dependencies": { "@babel/core": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", - "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.4", + "@babel/generator": "^7.24.5", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.4", - "@babel/parser": "^7.24.4", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -22243,15 +22202,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "peer": true }, - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "peer": true, - "requires": { - "whatwg-url": "^5.0.0" - } - }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -22285,9 +22235,9 @@ } }, "metro-babel-transformer": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.80.8.tgz", - "integrity": "sha512-TTzNwRZb2xxyv4J/+yqgtDAP2qVqH3sahsnFu6Xv4SkLqzrivtlnyUbaeTdJ9JjtADJUEjCbgbFgUVafrXdR9Q==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.80.9.tgz", + "integrity": "sha512-d76BSm64KZam1nifRZlNJmtwIgAeZhZG3fi3K+EmPOlrR8rDtBxQHDSN3fSGeNB9CirdTyabTMQCkCup6BXFSQ==", "peer": true, "requires": { "@babel/core": "^7.20.0", @@ -22296,21 +22246,21 @@ }, "dependencies": { "@babel/core": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", - "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.4", + "@babel/generator": "^7.24.5", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.4", - "@babel/parser": "^7.24.4", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -22363,12 +22313,12 @@ } }, "metro-cache": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.80.8.tgz", - "integrity": "sha512-5svz+89wSyLo7BxdiPDlwDTgcB9kwhNMfNhiBZPNQQs1vLFXxOkILwQiV5F2EwYT9DEr6OPZ0hnJkZfRQ8lDYQ==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.80.9.tgz", + "integrity": "sha512-ujEdSI43QwI+Dj2xuNax8LMo8UgKuXJEdxJkzGPU6iIx42nYa1byQ+aADv/iPh5sh5a//h5FopraW5voXSgm2w==", "peer": true, "requires": { - "metro-core": "0.80.8", + "metro-core": "0.80.9", "rimraf": "^3.0.2" }, "dependencies": { @@ -22384,24 +22334,24 @@ } }, "metro-cache-key": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.80.8.tgz", - "integrity": "sha512-qWKzxrLsRQK5m3oH8ePecqCc+7PEhR03cJE6Z6AxAj0idi99dHOSitTmY0dclXVB9vP2tQIAE8uTd8xkYGk8fA==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.80.9.tgz", + "integrity": "sha512-hRcYGhEiWIdM87hU0fBlcGr+tHDEAT+7LYNCW89p5JhErFt/QaAkVx4fb5bW3YtXGv5BTV7AspWPERoIb99CXg==", "peer": true }, "metro-config": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.80.8.tgz", - "integrity": "sha512-VGQJpfJawtwRzGzGXVUoohpIkB0iPom4DmSbAppKfumdhtLA8uVeEPp2GM61kL9hRvdbMhdWA7T+hZFDlo4mJA==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.80.9.tgz", + "integrity": "sha512-28wW7CqS3eJrunRGnsibWldqgwRP9ywBEf7kg+uzUHkSFJNKPM1K3UNSngHmH0EZjomizqQA2Zi6/y6VdZMolg==", "peer": true, "requires": { "connect": "^3.6.5", "cosmiconfig": "^5.0.5", "jest-validate": "^29.6.3", - "metro": "0.80.8", - "metro-cache": "0.80.8", - "metro-core": "0.80.8", - "metro-runtime": "0.80.8" + "metro": "0.80.9", + "metro-cache": "0.80.9", + "metro-core": "0.80.9", + "metro-runtime": "0.80.9" }, "dependencies": { "cosmiconfig": { @@ -22445,19 +22395,19 @@ } }, "metro-core": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.80.8.tgz", - "integrity": "sha512-g6lud55TXeISRTleW6SHuPFZHtYrpwNqbyFIVd9j9Ofrb5IReiHp9Zl8xkAfZQp8v6ZVgyXD7c130QTsCz+vBw==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.80.9.tgz", + "integrity": "sha512-tbltWQn+XTdULkGdzHIxlxk4SdnKxttvQQV3wpqqFbHDteR4gwCyTR2RyYJvxgU7HELfHtrVbqgqAdlPByUSbg==", "peer": true, "requires": { "lodash.throttle": "^4.1.1", - "metro-resolver": "0.80.8" + "metro-resolver": "0.80.9" } }, "metro-file-map": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.80.8.tgz", - "integrity": "sha512-eQXMFM9ogTfDs2POq7DT2dnG7rayZcoEgRbHPXvhUWkVwiKkro2ngcBE++ck/7A36Cj5Ljo79SOkYwHaWUDYDw==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.80.9.tgz", + "integrity": "sha512-sBUjVtQMHagItJH/wGU9sn3k2u0nrCl0CdR4SFMO1tksXLKbkigyQx4cbpcyPVOAmGTVuy3jyvBlELaGCAhplQ==", "peer": true, "requires": { "anymatch": "^3.0.3", @@ -22503,53 +22453,53 @@ } }, "metro-minify-terser": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.80.8.tgz", - "integrity": "sha512-y8sUFjVvdeUIINDuW1sejnIjkZfEF+7SmQo0EIpYbWmwh+kq/WMj74yVaBWuqNjirmUp1YNfi3alT67wlbBWBQ==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.80.9.tgz", + "integrity": "sha512-FEeCeFbkvvPuhjixZ1FYrXtO0araTpV6UbcnGgDUpH7s7eR5FG/PiJz3TsuuPP/HwCK19cZtQydcA2QrCw446A==", "peer": true, "requires": { "terser": "^5.15.0" } }, "metro-resolver": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.80.8.tgz", - "integrity": "sha512-JdtoJkP27GGoZ2HJlEsxs+zO7jnDUCRrmwXJozTlIuzLHMRrxgIRRby9fTCbMhaxq+iA9c+wzm3iFb4NhPmLbQ==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.80.9.tgz", + "integrity": "sha512-wAPIjkN59BQN6gocVsAvvpZ1+LQkkqUaswlT++cJafE/e54GoVkMNCmrR4BsgQHr9DknZ5Um/nKueeN7kaEz9w==", "peer": true }, "metro-runtime": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.80.8.tgz", - "integrity": "sha512-2oScjfv6Yb79PelU1+p8SVrCMW9ZjgEiipxq7jMRn8mbbtWzyv3g8Mkwr+KwOoDFI/61hYPUbY8cUnu278+x1g==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.80.9.tgz", + "integrity": "sha512-8PTVIgrVcyU+X/rVCy/9yxNlvXsBCk5JwwkbAm/Dm+Abo6NBGtNjWF0M1Xo/NWCb4phamNWcD7cHdR91HhbJvg==", "peer": true, "requires": { "@babel/runtime": "^7.0.0" } }, "metro-source-map": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.80.8.tgz", - "integrity": "sha512-+OVISBkPNxjD4eEKhblRpBf463nTMk3KMEeYS8Z4xM/z3qujGJGSsWUGRtH27+c6zElaSGtZFiDMshEb8mMKQg==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.80.9.tgz", + "integrity": "sha512-RMn+XS4VTJIwMPOUSj61xlxgBvPeY4G6s5uIn6kt6HB6A/k9ekhr65UkkDD7WzHYs3a9o869qU8tvOZvqeQzgw==", "peer": true, "requires": { "@babel/traverse": "^7.20.0", "@babel/types": "^7.20.0", "invariant": "^2.2.4", - "metro-symbolicate": "0.80.8", + "metro-symbolicate": "0.80.9", "nullthrows": "^1.1.1", - "ob1": "0.80.8", + "ob1": "0.80.9", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "metro-symbolicate": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.80.8.tgz", - "integrity": "sha512-nwhYySk79jQhwjL9QmOUo4wS+/0Au9joEryDWw7uj4kz2yvw1uBjwmlql3BprQCBzRdB3fcqOP8kO8Es+vE31g==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.80.9.tgz", + "integrity": "sha512-Ykae12rdqSs98hg41RKEToojuIW85wNdmSe/eHUgMkzbvCFNVgcC0w3dKZEhSsqQOXapXRlLtHkaHLil0UD/EA==", "peer": true, "requires": { "invariant": "^2.2.4", - "metro-source-map": "0.80.8", + "metro-source-map": "0.80.9", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "through2": "^2.0.1", @@ -22557,9 +22507,9 @@ } }, "metro-transform-plugins": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.80.8.tgz", - "integrity": "sha512-sSu8VPL9Od7w98MftCOkQ1UDeySWbsIAS5I54rW22BVpPnI3fQ42srvqMLaJUQPjLehUanq8St6OMBCBgH/UWw==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.80.9.tgz", + "integrity": "sha512-UlDk/uc8UdfLNJhPbF3tvwajyuuygBcyp+yBuS/q0z3QSuN/EbLllY3rK8OTD9n4h00qZ/qgxGv/lMFJkwP4vg==", "peer": true, "requires": { "@babel/core": "^7.20.0", @@ -22570,21 +22520,21 @@ }, "dependencies": { "@babel/core": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", - "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.4", + "@babel/generator": "^7.24.5", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.4", - "@babel/parser": "^7.24.4", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -22622,41 +22572,41 @@ } }, "metro-transform-worker": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.80.8.tgz", - "integrity": "sha512-+4FG3TQk3BTbNqGkFb2uCaxYTfsbuFOCKMMURbwu0ehCP8ZJuTUramkaNZoATS49NSAkRgUltgmBa4YaKZ5mqw==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.80.9.tgz", + "integrity": "sha512-c/IrzMUVnI0hSVVit4TXzt3A1GiUltGVlzCmLJWxNrBGHGrJhvgePj38+GXl1Xf4Fd4vx6qLUkKMQ3ux73bFLQ==", "peer": true, "requires": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.0", "@babel/parser": "^7.20.0", "@babel/types": "^7.20.0", - "metro": "0.80.8", - "metro-babel-transformer": "0.80.8", - "metro-cache": "0.80.8", - "metro-cache-key": "0.80.8", - "metro-minify-terser": "0.80.8", - "metro-source-map": "0.80.8", - "metro-transform-plugins": "0.80.8", + "metro": "0.80.9", + "metro-babel-transformer": "0.80.9", + "metro-cache": "0.80.9", + "metro-cache-key": "0.80.9", + "metro-minify-terser": "0.80.9", + "metro-source-map": "0.80.9", + "metro-transform-plugins": "0.80.9", "nullthrows": "^1.1.1" }, "dependencies": { "@babel/core": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", - "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.4", + "@babel/generator": "^7.24.5", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.4", - "@babel/parser": "^7.24.4", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -22838,6 +22788,21 @@ "minimatch": "^3.0.2" } }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "peer": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "peer": true + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -22945,9 +22910,9 @@ "peer": true }, "ob1": { - "version": "0.80.8", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.80.8.tgz", - "integrity": "sha512-QHJQk/lXMmAW8I7AIM3in1MSlwe1umR72Chhi8B7Xnq6mzjhBKkA6Fy/zAhQnGkA4S912EPCEvTij5yh+EQTAA==", + "version": "0.80.9", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.80.9.tgz", + "integrity": "sha512-v9yOxowkZbxWhKOaaTyLjIm1aLy4ebMNcSn4NYJKOAI/Qv+SkfEfszpLr2GIxsccmb2Y2HA9qtsqiIJ80ucpVA==", "peer": true }, "object-assign": { @@ -23361,6 +23326,28 @@ } } }, + "postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "requires": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "dependencies": { + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" + } + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -23565,8 +23552,7 @@ "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" }, "randombytes": { "version": "2.1.0", @@ -23594,24 +23580,33 @@ "peer": true }, "react": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz", - "integrity": "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "react-devtools-core": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.28.5.tgz", - "integrity": "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-5.2.0.tgz", + "integrity": "sha512-vZK+/gvxxsieAoAyYaiRIVFxlajb7KXhgBDV7OsoMzaAE+IqGpoxusBjIgq5ibqA2IloKu0p9n7tE68z1xs18A==", "peer": true, "requires": { "shell-quote": "^1.6.1", "ws": "^7" } }, + "react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + } + }, "react-intl": { "version": "4.7.6", "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-4.7.6.tgz", @@ -23637,128 +23632,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "react-native": { - "version": "0.73.6", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.73.6.tgz", - "integrity": "sha512-oqmZe8D2/VolIzSPZw+oUd6j/bEmeRHwsLn1xLA5wllEYsZ5zNuMsDus235ONOnCRwexqof/J3aztyQswSmiaA==", - "peer": true, - "requires": { - "@jest/create-cache-key-function": "^29.6.3", - "@react-native-community/cli": "12.3.6", - "@react-native-community/cli-platform-android": "12.3.6", - "@react-native-community/cli-platform-ios": "12.3.6", - "@react-native/assets-registry": "0.73.1", - "@react-native/codegen": "0.73.3", - "@react-native/community-cli-plugin": "0.73.17", - "@react-native/gradle-plugin": "0.73.4", - "@react-native/js-polyfills": "0.73.1", - "@react-native/normalize-colors": "0.73.2", - "@react-native/virtualized-lists": "0.73.4", - "abort-controller": "^3.0.0", - "anser": "^1.4.9", - "ansi-regex": "^5.0.0", - "base64-js": "^1.5.1", - "chalk": "^4.0.0", - "deprecated-react-native-prop-types": "^5.0.0", - "event-target-shim": "^5.0.1", - "flow-enums-runtime": "^0.0.6", - "invariant": "^2.2.4", - "jest-environment-node": "^29.6.3", - "jsc-android": "^250231.0.0", - "memoize-one": "^5.0.0", - "metro-runtime": "^0.80.3", - "metro-source-map": "^0.80.3", - "mkdirp": "^0.5.1", - "nullthrows": "^1.1.1", - "pretty-format": "^26.5.2", - "promise": "^8.3.0", - "react-devtools-core": "^4.27.7", - "react-refresh": "^0.14.0", - "react-shallow-renderer": "^16.15.0", - "regenerator-runtime": "^0.13.2", - "scheduler": "0.24.0-canary-efb381bbf-20230505", - "stacktrace-parser": "^0.1.10", - "whatwg-fetch": "^3.0.0", - "ws": "^6.2.2", - "yargs": "^17.6.2" - }, - "dependencies": { - "@react-native/virtualized-lists": { - "version": "0.73.4", - "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.73.4.tgz", - "integrity": "sha512-HpmLg1FrEiDtrtAbXiwCgXFYyloK/dOIPIuWW3fsqukwJEWAiTzm1nXGJ7xPU5XTHiWZ4sKup5Ebaj8z7iyWog==", - "peer": true, - "requires": { - "invariant": "^2.2.4", - "nullthrows": "^1.1.1" - } - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "peer": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "peer": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "peer": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "peer": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "peer": true - }, - "regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "peer": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "peer": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "ws": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", - "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", - "peer": true, - "requires": { - "async-limiter": "~1.0.0" - } - } - } - }, "react-redux": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz", @@ -23787,9 +23660,9 @@ } }, "react-refresh": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", - "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "peer": true }, "react-shallow-renderer": { @@ -23936,6 +23809,151 @@ "requires": { "@react-native-community/netinfo": "^4.1.3", "redux-persist": "^4.5.0" + }, + "dependencies": { + "@react-native-community/netinfo": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-4.7.0.tgz", + "integrity": "sha512-a/sDB+AsLEUNmhAUlAaTYeXKyQdFGBUfatqKkX5jluBo2CB3OAuTHfm7rSjcaLB9EmG5iSq3fOTpync2E7EYTA==", + "requires": {} + }, + "@react-native/virtualized-lists": { + "version": "0.74.83", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.74.83.tgz", + "integrity": "sha512-rmaLeE34rj7py4FxTod7iMTC7BAsm+HrGA8WxYmEJeyTV7WSaxAkosKoYBz8038mOiwnG9VwA/7FrB6bEQvn1A==", + "peer": true, + "requires": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "peer": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "peer": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "peer": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "peer": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "peer": true + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-native": { + "version": "0.74.1", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.74.1.tgz", + "integrity": "sha512-0H2XpmghwOtfPpM2LKqHIN7gxy+7G/r1hwJHKLV6uoyXGC/gCojRtoo5NqyKrWpFC8cqyT6wTYCLuG7CxEKilg==", + "peer": true, + "requires": { + "@jest/create-cache-key-function": "^29.6.3", + "@react-native-community/cli": "13.6.6", + "@react-native-community/cli-platform-android": "13.6.6", + "@react-native-community/cli-platform-ios": "13.6.6", + "@react-native/assets-registry": "0.74.83", + "@react-native/codegen": "0.74.83", + "@react-native/community-cli-plugin": "0.74.83", + "@react-native/gradle-plugin": "0.74.83", + "@react-native/js-polyfills": "0.74.83", + "@react-native/normalize-colors": "0.74.83", + "@react-native/virtualized-lists": "0.74.83", + "abort-controller": "^3.0.0", + "anser": "^1.4.9", + "ansi-regex": "^5.0.0", + "base64-js": "^1.5.1", + "chalk": "^4.0.0", + "event-target-shim": "^5.0.1", + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "jest-environment-node": "^29.6.3", + "jsc-android": "^250231.0.0", + "memoize-one": "^5.0.0", + "metro-runtime": "^0.80.3", + "metro-source-map": "^0.80.3", + "mkdirp": "^0.5.1", + "nullthrows": "^1.1.1", + "pretty-format": "^26.5.2", + "promise": "^8.3.0", + "react-devtools-core": "^5.0.0", + "react-refresh": "^0.14.0", + "react-shallow-renderer": "^16.15.0", + "regenerator-runtime": "^0.13.2", + "scheduler": "0.24.0-canary-efb381bbf-20230505", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0", + "ws": "^6.2.2", + "yargs": "^17.6.2" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "peer": true + }, + "scheduler": { + "version": "0.24.0-canary-efb381bbf-20230505", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.24.0-canary-efb381bbf-20230505.tgz", + "integrity": "sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "peer": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "peer": true, + "requires": { + "async-limiter": "~1.0.0" + } + } } }, "redux-persist": { @@ -24115,8 +24133,7 @@ "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" }, "rimraf": { "version": "2.6.3", @@ -24150,7 +24167,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "requires": { "queue-microtask": "^1.2.2" } @@ -24160,13 +24176,6 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "optional": true, - "peer": true - }, "sc-channel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/sc-channel/-/sc-channel-1.2.0.tgz", @@ -24186,9 +24195,9 @@ "integrity": "sha512-9PbqYBpCq+OoEeRQ3QfFIGE6qwjjBcd2j7UjgDlhnZbtSnuGgHdcRklPKYGuYFH82V/dwd+AIpu8XvA1zqTd+A==" }, "scheduler": { - "version": "0.24.0-canary-efb381bbf-20230505", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.24.0-canary-efb381bbf-20230505.tgz", - "integrity": "sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "peer": true, "requires": { "loose-envify": "^1.1.0" @@ -24205,6 +24214,16 @@ "ajv-keywords": "^3.4.1" } }, + "selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "peer": true, + "requires": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + } + }, "semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -24336,6 +24355,11 @@ "resolved": "https://registry.npmjs.org/shallow-equals/-/shallow-equals-1.0.0.tgz", "integrity": "sha1-JLdL8cY0wR7Uxxgqbfb7MA3OQ5A=" }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -24441,6 +24465,11 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, + "source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==" + }, "source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -24708,6 +24737,34 @@ "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", "peer": true }, + "styled-components": { + "version": "6.1.10", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.10.tgz", + "integrity": "sha512-4K8IKcn7iOt76riGLjvBhRyNPTkUKTvmnwoRFBOtJLswVvzy2VsoE2KOrfl9FJLQUYbITLJY2wfIZ3tjbkA/Zw==", + "requires": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.38", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "dependencies": { + "stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, "stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -25278,9 +25335,9 @@ "dev": true }, "whatwg-fetch": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.5.0.tgz", - "integrity": "sha512-jXkLtsR42xhXg7akoDKvKWE40eJeI+2KZqcp2h3NsOrRnDvtWX36KcKl30dy+hxECivdk2BVUHVNrPtoMBUx6A==", + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "peer": true }, "whatwg-url": { diff --git a/webapp/package.json b/webapp/package.json index b395e86e..2a5f7131 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -22,10 +22,11 @@ "core-js": "3.7.0", "mattermost-redux": "5.33.1", "prop-types": "15.7.2", - "react": "17.0.1", + "react": "18.3.1", "react-intl": "4.7.6", "react-redux": "7.2.2", "redux": "4.0.5", + "styled-components": "6.1.10", "typescript": "4.6.4" }, "devDependencies": { diff --git a/webapp/src/components/post_type_zoom/post_type_zoom.jsx b/webapp/src/components/post_type_zoom/post_type_zoom.jsx index 4850f205..a418ed3b 100644 --- a/webapp/src/components/post_type_zoom/post_type_zoom.jsx +++ b/webapp/src/components/post_type_zoom/post_type_zoom.jsx @@ -162,6 +162,9 @@ export default class PostTypeZoom extends React.PureComponent { {'Date: ' + start}
{'Meeting Length: ' + length + ' minute(s)'} + {props.meeting_recording && (
{'View Meeting Recording'}
)} + {props.meeting_transcript && (
{'View Meeting Transcript'}
)} + {props.meeting_password && (
{'Meeting Password: ' + props.meeting_password}
)} ); } else if (props.meeting_status === 'RECENTLY_CREATED') { diff --git a/webapp/src/index.js b/webapp/src/index.js index e9c4f138..e93a610b 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -7,6 +7,7 @@ import manifest from './manifest'; import ChannelHeaderIcon from './components/channel-header-icon'; import PostTypeZoom from './components/post_type_zoom'; +import {PostTypeTranscription} from './components/post_type_transcription'; import {startMeeting} from './actions'; import Client from './client'; import {getPluginURL, getServerRoute} from './selectors'; @@ -50,6 +51,7 @@ class Plugin { ); registry.registerPostTypeComponent('custom_zoom', PostTypeZoom); + registry.registerPostTypeComponent('custom_zoom_transcript', PostTypeTranscription); Client.setServerRoute(getServerRoute(store.getState())); } } diff --git a/webapp/webpack.config.js b/webapp/webpack.config.js index 9de57c95..de72ca1a 100644 --- a/webapp/webpack.config.js +++ b/webapp/webpack.config.js @@ -1,4 +1,5 @@ var path = require('path'); +var webpack = require('webpack'); module.exports = { entry: [ @@ -28,6 +29,11 @@ module.exports = { }, ], }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), + }) + ], externals: { react: 'React', 'react-dom': 'ReactDOM', From a305b43c76553c675e5973de834c287ddc445d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 May 2024 10:50:17 +0200 Subject: [PATCH 02/52] WIP --- server/webhook.go | 2 +- webapp/src/components/ai_icon.jsx | 33 +++++++++ .../components/post_type_transcription.jsx | 72 +++++++++++++++++++ webapp/src/components/svg.jsx | 13 ++++ 4 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 webapp/src/components/ai_icon.jsx create mode 100644 webapp/src/components/post_type_transcription.jsx create mode 100644 webapp/src/components/svg.jsx diff --git a/server/webhook.go b/server/webhook.go index 42916369..21ecd6be 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -215,7 +215,7 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques return } newPost.FileIds = append(newPost.FileIds, fileInfo.Id) - post.Props["captions"] = []map[string]string{{"file_id": fileInfo.Id}} + newPost.AddProp("captions", []any{map[string]any{"file_id": fileInfo.Id}}) } } diff --git a/webapp/src/components/ai_icon.jsx b/webapp/src/components/ai_icon.jsx new file mode 100644 index 00000000..abca38a5 --- /dev/null +++ b/webapp/src/components/ai_icon.jsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import Svg from './svg'; + +const IconAI = () => ( + + + + + + +); + +export default IconAI; diff --git a/webapp/src/components/post_type_transcription.jsx b/webapp/src/components/post_type_transcription.jsx new file mode 100644 index 00000000..9223354c --- /dev/null +++ b/webapp/src/components/post_type_transcription.jsx @@ -0,0 +1,72 @@ +import React from 'react'; +import {FormattedMessage} from 'react-intl'; +import {useSelector} from 'react-redux'; +import IconAI from 'src/components/ai_icon'; +import styled from 'styled-components'; + +const useAIAvailable = () => { + //@ts-ignore plugins state is a thing + return useSelector((state) => Boolean(state.plugins?.plugins?.[aiPluginID])); +}; + +const aiPluginID = 'mattermost-ai'; + +const useCallsPostButtonClicked = () => { + return useSelector((state) => { + //@ts-ignore plugins state is a thing + return state['plugins-' + aiPluginID]?.callsPostButtonClickedTranscription; + }); +}; + +const CreateMeetingSummaryButton = styled.button` + display: flex; + border: none; + height: 24px; + padding: 4px 10px; + margin-top: 8px; + margin-bottom: 8px; + align-items: center; + justify-content: center; + gap: 6px; + border-radius: 4px; + background: rgba(var(--center-channel-color-rgb), 0.08); + color: rgba(var(--center-channel-color-rgb), 0.64); + font-size: 12px; + font-weight: 600; + line-height: 16px; + + &:hover { + background: rgba(var(--center-channel-color-rgb), 0.12); + color: rgba(var(--center-channel-color-rgb), 0.72); + } + + &:active { + background: rgba(var(--button-bg-rgb), 0.08); + color: var(--button-bg); + } +`; + +export const PostTypeTranscription = (props) => { + const aiAvailable = useAIAvailable(); + const callsPostButtonClicked = useCallsPostButtonClicked(); + + const createMeetingSummary = () => { + callsPostButtonClicked?.(props.post); + }; + + const msg = props.post.message; + + return ( +
+ {msg} + {aiAvailable && callsPostButtonClicked && + + + + + } +
+ ); +}; diff --git a/webapp/src/components/svg.jsx b/webapp/src/components/svg.jsx new file mode 100644 index 00000000..8430fb0b --- /dev/null +++ b/webapp/src/components/svg.jsx @@ -0,0 +1,13 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import styled from 'styled-components'; + +// Hat-tip: https://www.pinkdroids.com/blog/svg-react-styled-components/ +const Svg = styled.svg.attrs({ + version: '1.1', + xmlns: 'http://www.w3.org/2000/svg', + xmlnsXlink: 'http://www.w3.org/1999/xlink', +})``; + +export default Svg; From dd47bbf74e60c81b3a0b5f61bfb10129c416b8c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 May 2024 15:48:20 +0200 Subject: [PATCH 03/52] Adding chat support --- server/webhook.go | 154 ++++++++++++++---- server/zoom/webhook.go | 1 + .../post_type_zoom/post_type_zoom.jsx | 1 + webapp/src/index.js | 1 + 4 files changed, 123 insertions(+), 34 deletions(-) diff --git a/server/webhook.go b/server/webhook.go index 21ecd6be..60004047 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -171,23 +171,11 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques return } - newPost := &model.Post{ - UserId: p.botUserID, - ChannelId: post.ChannelId, - RootId: post.Id, - Message: "Here's the zoom meeting transcription", - FileIds: []string{}, - Type: "custom_zoom_transcript", - } - p.API.LogError("UPDATING MEETING TRANSCRIPT") for _, recording := range webhook.Payload.Object.RecordingFiles { if recording.RecordingType == zoom.RecordingTypeAudioTranscript { p.API.LogError("MEETING TRANSCRIPT UPDATED") - post.Props["meeting_transcript"] = recording.PlayURL - if webhook.Payload.Object.Password != "" { - post.Props["meeting_password"] = webhook.Payload.Object.Password - } + request, err := http.NewRequest(http.MethodGet, recording.DownloadURL, nil) if err != nil { p.API.LogWarn("Unable to get the transcription", "err", err) @@ -195,12 +183,39 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques return } request.Header.Set("Authorization", "Bearer "+webhook.DownloadToken) - response, err := http.DefaultClient.Do(request) - if err != nil { - p.API.LogWarn("Unable to get the transcription", "err", err) + + retries := 3 + var response *http.Response + for retries > 0 { + var err error + response, err = http.DefaultClient.Do(request) + if err != nil { + p.API.LogWarn("Unable to get the transcription", "err", err) + time.Sleep(1 * time.Second) + retries -= 1 + continue + } + if response.StatusCode != http.StatusOK { + p.API.LogWarn("Unable to get the transcription", "err", "bad status code "+strconv.Itoa(response.StatusCode)) + time.Sleep(1 * time.Second) + retries -= 1 + continue + } + break + } + + if response == nil { + p.API.LogWarn("Unable to get the transcription", "err", "response is nil") http.Error(w, err.Error(), http.StatusBadRequest) return } + + post, appErr := p.API.GetPost(postID) + if appErr != nil { + p.API.LogWarn("Could not get meeting post by id", "err", appErr) + http.Error(w, appErr.Error(), appErr.StatusCode) + return + } defer response.Body.Close() transcription, err := io.ReadAll(response.Body) if err != nil { @@ -214,8 +229,29 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques http.Error(w, appErr.Error(), http.StatusBadRequest) return } + newPost := &model.Post{ + UserId: p.botUserID, + ChannelId: post.ChannelId, + RootId: post.Id, + Message: "Here's the zoom meeting transcription", + FileIds: []string{}, + Type: "custom_zoom_transcript", + } + newPost.FileIds = append(newPost.FileIds, fileInfo.Id) newPost.AddProp("captions", []any{map[string]any{"file_id": fileInfo.Id}}) + + _, appErr = p.API.CreatePost(newPost) + if appErr != nil { + p.API.LogWarn("Could not update the post", "err", appErr) + http.Error(w, appErr.Error(), appErr.StatusCode) + return + } + + post.Props["meeting_transcript"] = recording.PlayURL + if post.Props["meeting_password"] == "" && webhook.Payload.Object.Password != "" { + post.Props["meeting_password"] = webhook.Payload.Object.Password + } } } @@ -226,14 +262,6 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques return } - _, appErr = p.API.CreatePost(newPost) - - if appErr != nil { - p.API.LogWarn("Could not create the transcription post", "err", appErr) - http.Error(w, appErr.Error(), appErr.StatusCode) - return - } - // TODO: Delete the meeting post if is no longer needed // if appErr = p.deleteMeetingPostID(meetingPostID); appErr != nil { // p.API.LogWarn("failed to delete db entry", "error", appErr.Error()) @@ -269,13 +297,78 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request return } - p.API.LogError("UPDATING MEETING RECORDING") + p.API.LogError("UPDATING MEETING RECORDING AND CHAT") for _, recording := range webhook.Payload.Object.RecordingFiles { + if post.Props["meeting_password"] == "" && webhook.Payload.Object.Password != "" { + post.Props["meeting_password"] = webhook.Payload.Object.Password + } + if recording.RecordingType == zoom.RecordingTypeVideo { p.API.LogError("MEETING RECORDING UPDATED") post.Props["meeting_recording"] = recording.PlayURL - if webhook.Payload.Object.Password != "" { - post.Props["meeting_password"] = webhook.Payload.Object.Password + _, appErr = p.API.CreatePost(&model.Post{ + UserId: p.botUserID, + ChannelId: post.ChannelId, + RootId: post.Id, + Message: "Here's the zoom meeting recording:\n**Link:** [Meeting Recording](" + recording.PlayURL + ")\n**Password:** " + webhook.Payload.Object.Password, + }) + if appErr != nil { + p.API.LogWarn("Could not update the post", "err", appErr) + http.Error(w, appErr.Error(), appErr.StatusCode) + return + } + + } + + if recording.RecordingType == zoom.RecordingTypeChat { + p.API.LogError("MEETING CHAT UPDATED") + post.Props["meeting_chat"] = recording.PlayURL + request, err := http.NewRequest(http.MethodGet, recording.DownloadURL, nil) + if err != nil { + p.API.LogWarn("Unable to get the chat", "err", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + request.Header.Set("Authorization", "Bearer "+webhook.DownloadToken) + response, err := http.DefaultClient.Do(request) + if err != nil { + p.API.LogWarn("Unable to get the chat", "err", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + defer response.Body.Close() + chat, err := io.ReadAll(response.Body) + if err != nil { + p.API.LogWarn("Unable to get the chat", "err", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + fileInfo, appErr := p.API.UploadFile(chat, post.ChannelId, "chat.txt") + if appErr != nil { + p.API.LogWarn("Unable to get the chat", "err", appErr) + http.Error(w, appErr.Error(), http.StatusBadRequest) + return + } + + newPost := &model.Post{ + UserId: p.botUserID, + ChannelId: post.ChannelId, + RootId: post.Id, + Message: "Here's the zoom meeting chat history", + FileIds: []string{}, + Type: "custom_zoom_chat", + } + + newPost.FileIds = append(newPost.FileIds, fileInfo.Id) + newPost.AddProp("captions", []any{map[string]any{"file_id": fileInfo.Id}}) + + p.API.LogError("SENDING THE NEW POST FOR THE CHAT HISTORY") + + _, appErr = p.API.CreatePost(newPost) + if appErr != nil { + p.API.LogWarn("Could not update the post", "err", appErr) + http.Error(w, appErr.Error(), appErr.StatusCode) + return } } } @@ -287,13 +380,6 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request return } - _, appErr = p.API.CreatePost(&model.Post{ - UserId: p.botUserID, - ChannelId: post.ChannelId, - RootId: post.Id, - Message: "Here's the zoom meeting recording:\n**Link:** [Meeting Recording](" + post.Props["meeting_recording"].(string) + ")\n**Password:** " + post.Props["meeting_password"].(string), - }) - // TODO: Delete the meeting post if is no longer needed // if appErr = p.deleteMeetingPostID(meetingPostID); appErr != nil { // p.API.LogWarn("failed to delete db entry", "error", appErr.Error()) diff --git a/server/zoom/webhook.go b/server/zoom/webhook.go index 1b70d1b7..95541d96 100644 --- a/server/zoom/webhook.go +++ b/server/zoom/webhook.go @@ -22,6 +22,7 @@ const ( EventTypeValidateWebhook EventType = "endpoint.url_validation" RecordingTypeAudioTranscript = "audio_transcript" + RecordingTypeChat = "chat_file" RecordingTypeVideo = "shared_screen_with_speaker_view" ) diff --git a/webapp/src/components/post_type_zoom/post_type_zoom.jsx b/webapp/src/components/post_type_zoom/post_type_zoom.jsx index a418ed3b..4b53a69c 100644 --- a/webapp/src/components/post_type_zoom/post_type_zoom.jsx +++ b/webapp/src/components/post_type_zoom/post_type_zoom.jsx @@ -164,6 +164,7 @@ export default class PostTypeZoom extends React.PureComponent { {'Meeting Length: ' + length + ' minute(s)'} {props.meeting_recording && (
{'View Meeting Recording'}
)} {props.meeting_transcript && (
{'View Meeting Transcript'}
)} + {props.meeting_chat && (
{'View Meeting Chat History'}
)} {props.meeting_password && (
{'Meeting Password: ' + props.meeting_password}
)} ); diff --git a/webapp/src/index.js b/webapp/src/index.js index e93a610b..cab935c3 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -52,6 +52,7 @@ class Plugin { registry.registerPostTypeComponent('custom_zoom', PostTypeZoom); registry.registerPostTypeComponent('custom_zoom_transcript', PostTypeTranscription); + registry.registerPostTypeComponent('custom_zoom_chat', PostTypeTranscription); Client.setServerRoute(getServerRoute(store.getState())); } } From 12815afeb05e09cbf6c849adeceab19c21e7736e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 May 2024 18:27:50 +0200 Subject: [PATCH 04/52] WIP --- server/webhook.go | 222 +++++++----------- server/zoom/webhook.go | 54 +++-- webapp/src/components/post_type_chat.jsx | 81 +++++++ .../post_type_zoom/post_type_zoom.jsx | 4 - webapp/src/index.js | 3 +- 5 files changed, 200 insertions(+), 164 deletions(-) create mode 100644 webapp/src/components/post_type_chat.jsx diff --git a/server/webhook.go b/server/webhook.go index 60004047..bd3120dc 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -141,6 +141,71 @@ func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body } } +func (p *Plugin) handleTranscript(recording zoom.RecordingFile, postID, channelID, downloadToken string) error { + request, err := http.NewRequest(http.MethodGet, recording.DownloadURL, nil) + if err != nil { + p.API.LogWarn("Unable to get the transcription", "err", err) + return err + } + request.Header.Set("Authorization", "Bearer "+downloadToken) + + retries := 5 + var response *http.Response + for retries > 0 { + var err error + response, err = http.DefaultClient.Do(request) + if err != nil { + p.API.LogWarn("Unable to get the transcription", "err", err) + time.Sleep(1 * time.Second) + retries -= 1 + continue + } + if response.StatusCode != http.StatusOK { + p.API.LogWarn("Unable to get the transcription", "err", "bad status code "+strconv.Itoa(response.StatusCode)) + time.Sleep(1 * time.Second) + retries -= 1 + continue + } + break + } + + if response == nil { + p.API.LogWarn("Unable to get the transcription", "err", "response is nil") + return err + } + + defer response.Body.Close() + transcription, err := io.ReadAll(response.Body) + if err != nil { + p.API.LogWarn("Unable to get the transcription", "err", err) + return err + } + fileInfo, appErr := p.API.UploadFile(transcription, channelID, "transcription.txt") + if appErr != nil { + p.API.LogWarn("Unable to get the transcription", "err", appErr) + return appErr + } + newPost := &model.Post{ + UserId: p.botUserID, + ChannelId: channelID, + RootId: postID, + Message: "Here's the zoom meeting transcription", + FileIds: []string{}, + Type: "custom_zoom_transcript", + } + + newPost.FileIds = append(newPost.FileIds, fileInfo.Id) + newPost.AddProp("captions", []any{map[string]any{"file_id": fileInfo.Id}}) + + _, appErr = p.API.CreatePost(newPost) + if appErr != nil { + p.API.LogWarn("Could not update the post", "err", appErr) + return appErr + } + + return nil +} + func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Request, body []byte) { p.API.LogError("RUNNING THE UPDATE MEETING TRANSCRIPT") var webhook zoom.RecordingWebhook @@ -164,104 +229,18 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques return } - _, appErr = p.API.UpdatePost(post) - if appErr != nil { - p.API.LogWarn("Could not update the post", "err", appErr) - http.Error(w, appErr.Error(), appErr.StatusCode) - return - } - p.API.LogError("UPDATING MEETING TRANSCRIPT") for _, recording := range webhook.Payload.Object.RecordingFiles { if recording.RecordingType == zoom.RecordingTypeAudioTranscript { p.API.LogError("MEETING TRANSCRIPT UPDATED") - - request, err := http.NewRequest(http.MethodGet, recording.DownloadURL, nil) + err := p.handleTranscript(recording, post.Id, post.ChannelId, webhook.DownloadToken) if err != nil { - p.API.LogWarn("Unable to get the transcription", "err", err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - request.Header.Set("Authorization", "Bearer "+webhook.DownloadToken) - - retries := 3 - var response *http.Response - for retries > 0 { - var err error - response, err = http.DefaultClient.Do(request) - if err != nil { - p.API.LogWarn("Unable to get the transcription", "err", err) - time.Sleep(1 * time.Second) - retries -= 1 - continue - } - if response.StatusCode != http.StatusOK { - p.API.LogWarn("Unable to get the transcription", "err", "bad status code "+strconv.Itoa(response.StatusCode)) - time.Sleep(1 * time.Second) - retries -= 1 - continue - } - break - } - - if response == nil { - p.API.LogWarn("Unable to get the transcription", "err", "response is nil") - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - post, appErr := p.API.GetPost(postID) - if appErr != nil { - p.API.LogWarn("Could not get meeting post by id", "err", appErr) http.Error(w, appErr.Error(), appErr.StatusCode) - return - } - defer response.Body.Close() - transcription, err := io.ReadAll(response.Body) - if err != nil { - p.API.LogWarn("Unable to get the transcription", "err", err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - fileInfo, appErr := p.API.UploadFile(transcription, post.ChannelId, "transcription.txt") - if appErr != nil { - p.API.LogWarn("Unable to get the transcription", "err", appErr) - http.Error(w, appErr.Error(), http.StatusBadRequest) - return - } - newPost := &model.Post{ - UserId: p.botUserID, - ChannelId: post.ChannelId, - RootId: post.Id, - Message: "Here's the zoom meeting transcription", - FileIds: []string{}, - Type: "custom_zoom_transcript", - } - - newPost.FileIds = append(newPost.FileIds, fileInfo.Id) - newPost.AddProp("captions", []any{map[string]any{"file_id": fileInfo.Id}}) - - _, appErr = p.API.CreatePost(newPost) - if appErr != nil { - p.API.LogWarn("Could not update the post", "err", appErr) - http.Error(w, appErr.Error(), appErr.StatusCode) - return - } - - post.Props["meeting_transcript"] = recording.PlayURL - if post.Props["meeting_password"] == "" && webhook.Payload.Object.Password != "" { - post.Props["meeting_password"] = webhook.Payload.Object.Password + continue } } } - _, appErr = p.API.UpdatePost(post) - if appErr != nil { - p.API.LogWarn("Could not update the post", "err", appErr) - http.Error(w, appErr.Error(), appErr.StatusCode) - return - } - // TODO: Delete the meeting post if is no longer needed // if appErr = p.deleteMeetingPostID(meetingPostID); appErr != nil { // p.API.LogWarn("failed to delete db entry", "error", appErr.Error()) @@ -298,31 +277,17 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request } p.API.LogError("UPDATING MEETING RECORDING AND CHAT") + newPost := &model.Post{ + UserId: p.botUserID, + ChannelId: post.ChannelId, + RootId: post.Id, + Message: "", + FileIds: []string{}, + } for _, recording := range webhook.Payload.Object.RecordingFiles { - if post.Props["meeting_password"] == "" && webhook.Payload.Object.Password != "" { - post.Props["meeting_password"] = webhook.Payload.Object.Password - } - - if recording.RecordingType == zoom.RecordingTypeVideo { - p.API.LogError("MEETING RECORDING UPDATED") - post.Props["meeting_recording"] = recording.PlayURL - _, appErr = p.API.CreatePost(&model.Post{ - UserId: p.botUserID, - ChannelId: post.ChannelId, - RootId: post.Id, - Message: "Here's the zoom meeting recording:\n**Link:** [Meeting Recording](" + recording.PlayURL + ")\n**Password:** " + webhook.Payload.Object.Password, - }) - if appErr != nil { - p.API.LogWarn("Could not update the post", "err", appErr) - http.Error(w, appErr.Error(), appErr.StatusCode) - return - } - - } - + p.API.LogError("RECORDING COMPLETE RECORDING", "recording", recording) if recording.RecordingType == zoom.RecordingTypeChat { p.API.LogError("MEETING CHAT UPDATED") - post.Props["meeting_chat"] = recording.PlayURL request, err := http.NewRequest(http.MethodGet, recording.DownloadURL, nil) if err != nil { p.API.LogWarn("Unable to get the chat", "err", err) @@ -350,36 +315,27 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request return } - newPost := &model.Post{ - UserId: p.botUserID, - ChannelId: post.ChannelId, - RootId: post.Id, - Message: "Here's the zoom meeting chat history", - FileIds: []string{}, - Type: "custom_zoom_chat", - } - newPost.FileIds = append(newPost.FileIds, fileInfo.Id) newPost.AddProp("captions", []any{map[string]any{"file_id": fileInfo.Id}}) - - p.API.LogError("SENDING THE NEW POST FOR THE CHAT HISTORY") - - _, appErr = p.API.CreatePost(newPost) - if appErr != nil { - p.API.LogWarn("Could not update the post", "err", appErr) - http.Error(w, appErr.Error(), appErr.StatusCode) - return + newPost.Type = "custom_zoom_chat" + if newPost.Message == "" { + newPost.Message = " " } } - } - _, appErr = p.API.UpdatePost(post) - if appErr != nil { - p.API.LogWarn("Could not update the post", "err", appErr) - http.Error(w, appErr.Error(), appErr.StatusCode) - return + if recording.RecordingType == zoom.RecordingTypeVideo { + newPost.Message = "Here's the zoom meeting recording:\n**Link:** [Meeting Recording](" + recording.PlayURL + ")\n**Password:** " + webhook.Payload.Object.Password + } } + if newPost.Message != "" { + _, appErr = p.API.CreatePost(newPost) + if appErr != nil { + p.API.LogWarn("Could not update the post", "err", appErr) + http.Error(w, appErr.Error(), appErr.StatusCode) + return + } + } // TODO: Delete the meeting post if is no longer needed // if appErr = p.deleteMeetingPostID(meetingPostID); appErr != nil { // p.API.LogWarn("failed to delete db entry", "error", appErr.Error()) diff --git a/server/zoom/webhook.go b/server/zoom/webhook.go index 95541d96..cd43ee71 100644 --- a/server/zoom/webhook.go +++ b/server/zoom/webhook.go @@ -79,33 +79,35 @@ type RecordingWebhookPayload struct { Object RecordingWebhookObject `json:"object"` } +type RecordingFile struct { + ID string `json:"id"` + MeetingID string `json:"meeting_id"` + RecordingStart time.Time `json:"recording_start"` + RecordingEnd time.Time `json:"recording_end"` + FileType string `json:"file_type"` + FileSize int `json:"file_size"` + FilePath string `json:"file_path"` + Status string `json:"status"` + DownloadURL string `json:"download_url"` + PlayURL string `json:"play_url"` + RecordingType string `json:"recording_type"` +} + type RecordingWebhookObject struct { - UUID string `json:"uuid"` - MeetingNumber int `json:"meeting_number"` - ID int `json:"id"` - AccountID string `json:"account_id"` - HostID string `json:"host_id"` - Topic string `json:"topic"` - StartTime time.Time `json:"start_time"` - Timezone string `json:"timezone"` - HostEmail string `json:"host_email"` - Duration int `json:"duration"` - TotalSize int `json:"total_size"` - RecordingCount int `json:"recording_count"` - Password string `json:"password"` - RecordingFiles []struct { - ID string `json:"id"` - MeetingID string `json:"meeting_id"` - RecordingStart time.Time `json:"recording_start"` - RecordingEnd time.Time `json:"recording_end"` - FileType string `json:"file_type"` - FileSize int `json:"file_size"` - FilePath string `json:"file_path"` - Status string `json:"status"` - DownloadURL string `json:"download_url"` - PlayURL string `json:"play_url"` - RecordingType string `json:"recording_type"` - } `json:"recording_files"` + UUID string `json:"uuid"` + MeetingNumber int `json:"meeting_number"` + ID int `json:"id"` + AccountID string `json:"account_id"` + HostID string `json:"host_id"` + Topic string `json:"topic"` + StartTime time.Time `json:"start_time"` + Timezone string `json:"timezone"` + HostEmail string `json:"host_email"` + Duration int `json:"duration"` + TotalSize int `json:"total_size"` + RecordingCount int `json:"recording_count"` + Password string `json:"password"` + RecordingFiles []RecordingFile `json:"recording_files"` } type DeauthorizationEvent struct { diff --git a/webapp/src/components/post_type_chat.jsx b/webapp/src/components/post_type_chat.jsx new file mode 100644 index 00000000..e616ff7f --- /dev/null +++ b/webapp/src/components/post_type_chat.jsx @@ -0,0 +1,81 @@ +import React from 'react'; +import {FormattedMessage} from 'react-intl'; +import {useSelector} from 'react-redux'; +import IconAI from 'src/components/ai_icon'; +import styled from 'styled-components'; + +const useAIAvailable = () => { + //@ts-ignore plugins state is a thing + return useSelector((state) => Boolean(state.plugins?.plugins?.[aiPluginID])); +}; + +const aiPluginID = 'mattermost-ai'; + +const useCallsPostButtonClicked = () => { + return useSelector((state) => { + //@ts-ignore plugins state is a thing + return state['plugins-' + aiPluginID]?.callsPostButtonClickedTranscription; + }); +}; + +const CreateMeetingSummaryButton = styled.button` + display: flex; + border: none; + height: 24px; + padding: 4px 10px; + margin-top: 8px; + margin-bottom: 8px; + align-items: center; + justify-content: center; + gap: 6px; + border-radius: 4px; + background: rgba(var(--center-channel-color-rgb), 0.08); + color: rgba(var(--center-channel-color-rgb), 0.64); + font-size: 12px; + font-weight: 600; + line-height: 16px; + + &:hover { + background: rgba(var(--center-channel-color-rgb), 0.12); + color: rgba(var(--center-channel-color-rgb), 0.72); + } + + &:active { + background: rgba(var(--button-bg-rgb), 0.08); + color: var(--button-bg); + } +`; + +export const PostTypeChat = (props) => { + const aiAvailable = useAIAvailable(); + const callsPostButtonClicked = useCallsPostButtonClicked(); + + const createMeetingSummary = () => { + callsPostButtonClicked?.(props.post); + }; + + const msg = props.post.message; + + const renderPostWithMarkdown = (msg) => { + const {formatText, messageHtmlToComponent} = window.PostUtils; + + return messageHtmlToComponent( + formatText(msg, {}), + false, + ); + } + + return ( +
+ {renderPostWithMarkdown(msg)} + {aiAvailable && callsPostButtonClicked && + + + + + } +
+ ); +}; diff --git a/webapp/src/components/post_type_zoom/post_type_zoom.jsx b/webapp/src/components/post_type_zoom/post_type_zoom.jsx index 4b53a69c..4850f205 100644 --- a/webapp/src/components/post_type_zoom/post_type_zoom.jsx +++ b/webapp/src/components/post_type_zoom/post_type_zoom.jsx @@ -162,10 +162,6 @@ export default class PostTypeZoom extends React.PureComponent { {'Date: ' + start}
{'Meeting Length: ' + length + ' minute(s)'} - {props.meeting_recording && (
{'View Meeting Recording'}
)} - {props.meeting_transcript && (
{'View Meeting Transcript'}
)} - {props.meeting_chat && (
{'View Meeting Chat History'}
)} - {props.meeting_password && (
{'Meeting Password: ' + props.meeting_password}
)} ); } else if (props.meeting_status === 'RECENTLY_CREATED') { diff --git a/webapp/src/index.js b/webapp/src/index.js index cab935c3..33d610ef 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -8,6 +8,7 @@ import manifest from './manifest'; import ChannelHeaderIcon from './components/channel-header-icon'; import PostTypeZoom from './components/post_type_zoom'; import {PostTypeTranscription} from './components/post_type_transcription'; +import {PostTypeChat} from './components/post_type_chat'; import {startMeeting} from './actions'; import Client from './client'; import {getPluginURL, getServerRoute} from './selectors'; @@ -52,7 +53,7 @@ class Plugin { registry.registerPostTypeComponent('custom_zoom', PostTypeZoom); registry.registerPostTypeComponent('custom_zoom_transcript', PostTypeTranscription); - registry.registerPostTypeComponent('custom_zoom_chat', PostTypeTranscription); + registry.registerPostTypeComponent('custom_zoom_chat', PostTypeChat); Client.setServerRoute(getServerRoute(store.getState())); } } From 161b66044d8947b00e2c930b1d517ad605723182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 15 May 2024 19:51:18 +0200 Subject: [PATCH 05/52] Adding support for subscription of meetings to channels --- server/command.go | 60 ++++++++++++--- server/http.go | 43 +++++++++-- server/store.go | 36 ++++++++- server/webhook.go | 190 ++++++++++++++++++++++++++++++---------------- 4 files changed, 244 insertions(+), 85 deletions(-) diff --git a/server/command.go b/server/command.go index 2d6cdd60..3b331dc6 100644 --- a/server/command.go +++ b/server/command.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "strconv" "strings" "github.com/mattermost/mattermost-plugin-zoom/server/zoom" @@ -25,11 +26,13 @@ const ( ) const ( - actionConnect = "connect" - actionStart = "start" - actionDisconnect = "disconnect" - actionHelp = "help" - settings = "settings" + actionConnect = "connect" + actionSubscribe = "subscribe" + actionUnsubscribe = "unsubscribe" + actionStart = "start" + actionDisconnect = "disconnect" + actionHelp = "help" + settings = "settings" ) func (p *Plugin) getCommand() (*model.Command, error) { @@ -64,7 +67,7 @@ func (p *Plugin) postCommandResponse(args *model.CommandArgs, text string) { _ = p.API.SendEphemeralPost(args.UserId, post) } -func (p *Plugin) parseCommand(rawCommand string) (cmd, action, topic string) { +func (p *Plugin) parseCommand(rawCommand string) (cmd, action, topic string, meetingID int) { split := strings.Fields(rawCommand) cmd = split[0] if len(split) > 1 { @@ -73,11 +76,14 @@ func (p *Plugin) parseCommand(rawCommand string) (cmd, action, topic string) { if action == actionStart { topic = strings.Join(split[2:], " ") } - return cmd, action, topic + if len(split) > 2 && (action == actionSubscribe || action == actionUnsubscribe) { + meetingID, _ = strconv.Atoi(split[2]) + } + return cmd, action, topic, meetingID } func (p *Plugin) executeCommand(c *plugin.Context, args *model.CommandArgs) (string, error) { - command, action, topic := p.parseCommand(args.Command) + command, action, topic, meetingID := p.parseCommand(args.Command) if command != "/zoom" { return fmt.Sprintf("Command '%s' is not /zoom. Please try again.", command), nil @@ -96,6 +102,10 @@ func (p *Plugin) executeCommand(c *plugin.Context, args *model.CommandArgs) (str switch action { case actionConnect: return p.runConnectCommand(user, args) + case actionSubscribe: + return p.runSubscribeCommand(user, args, meetingID) + case actionUnsubscribe: + return p.runUnsubscribeCommand(user, args, meetingID) case actionStart: return p.runStartCommand(args, user, topic) case actionDisconnect: @@ -180,7 +190,12 @@ func (p *Plugin) runStartCommand(args *model.CommandArgs, user *model.User, topi } } - if postMeetingErr := p.postMeeting(user, meetingID, args.ChannelId, args.RootId, topic); postMeetingErr != nil { + meeting, err := p.getMeeting(user, meetingID) + if err != nil { + return "", errors.Wrap(err, "failed to get the meeting") + } + + if postMeetingErr := p.postMeeting(user, meetingID, meeting.UUID, args.ChannelId, args.RootId, topic); postMeetingErr != nil { return "", postMeetingErr } @@ -225,6 +240,23 @@ func (p *Plugin) runConnectCommand(user *model.User, extra *model.CommandArgs) ( return oauthMsg, nil } +func (p *Plugin) runSubscribeCommand(user *model.User, extra *model.CommandArgs, meetingID int) (string, error) { + if appErr := p.storeChannelForMeeting(meetingID, extra.ChannelId); appErr != nil { + return "", errors.Wrap(appErr, "cannot subscribing to meeting") + } + return "Channel subscribed to meeting", nil +} + +func (p *Plugin) runUnsubscribeCommand(user *model.User, extra *model.CommandArgs, meetingID int) (string, error) { + if channelID, appErr := p.fetchChannelForMeeting(meetingID); appErr != nil || channelID == "" { + return "Can not unsubscribe from meeting: meeting not found", errors.New("meeting not found") + } + if appErr := p.deleteChannelForMeeting(meetingID); appErr != nil { + return "Can not unsubscribe from meeting: unable to delete the meeting subscription", errors.Wrap(appErr, "cannot unsubscribing from meeting") + } + return "Channel unsubscribed from meeting", nil +} + // runDisconnectCommand runs command to disconnect from Zoom. Will fail if user cannot connect. func (p *Plugin) runDisconnectCommand(user *model.User) (string, error) { if !p.canConnect(user) { @@ -285,9 +317,9 @@ func (p *Plugin) updateUserPersonalSettings(usePMIValue, userID string) *model.A func (p *Plugin) getAutocompleteData() *model.AutocompleteData { canConnect := !p.configuration.AccountLevelApp - available := "start, help, settings" + available := "start, help, settings, subscribe, unsubscribe" if canConnect { - available = "start, connect, disconnect, help, settings" + available = "start, connect, disconnect, help, settings, subscribe, unsubscribe" } zoom := model.NewAutocompleteData("zoom", "[command]", fmt.Sprintf("Available commands: %s", available)) @@ -306,6 +338,12 @@ func (p *Plugin) getAutocompleteData() *model.AutocompleteData { setting := model.NewAutocompleteData("settings", "", "Update your meeting ID preferences") zoom.AddCommand(setting) + subscribe := model.NewAutocompleteData("subscribe", "[meeting id]", "Subscribe this channel to a Zoom meeting") + zoom.AddCommand(subscribe) + + unsubscribe := model.NewAutocompleteData("unsubscribe", "[meeting id]", "Unsubscribe this channel from a Zoom meeting") + zoom.AddCommand(unsubscribe) + help := model.NewAutocompleteData("help", "", "Display usage") zoom.AddCommand(help) diff --git a/server/http.go b/server/http.go index bc4bd35f..56d17820 100644 --- a/server/http.go +++ b/server/http.go @@ -180,7 +180,13 @@ func (p *Plugin) startMeeting(action, userID, channelID, rootID string) { } } - if postMeetingErr := p.postMeeting(user, meetingID, channelID, rootID, defaultMeetingTopic); postMeetingErr != nil { + meeting, err := p.getMeeting(user, meetingID) + if err != nil { + p.API.LogWarn("failed to get the meeting", "Error", err.Error()) + return + } + + if postMeetingErr := p.postMeeting(user, meetingID, meeting.UUID, channelID, rootID, defaultMeetingTopic); postMeetingErr != nil { p.API.LogWarn("failed to post the meeting", "Error", postMeetingErr.Error()) return } @@ -365,7 +371,8 @@ func (p *Plugin) completeUserOAuthToZoom(w http.ResponseWriter, r *http.Request) } meetingID := meeting.ID - if err = p.postMeeting(user, meetingID, channelID, "", ""); err != nil { + meetingUUID := meeting.UUID + if err = p.postMeeting(user, meetingID, meetingUUID, channelID, "", ""); err != nil { p.API.LogWarn("Failed to post the meeting", "error", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -392,14 +399,14 @@ func (p *Plugin) completeUserOAuthToZoom(w http.ResponseWriter, r *http.Request) } } -func (p *Plugin) postMeeting(creator *model.User, meetingID int, channelID string, rootID string, topic string) error { +func (p *Plugin) postMeeting(creator *model.User, meetingID int, meetingUUID string, channelID string, rootID string, topic string) error { meetingURL := p.getMeetingURL(creator, meetingID) if topic == "" { topic = defaultMeetingTopic } - if !p.API.HasPermissionToChannel(creator.Id, channelID, model.PermissionCreatePost) { + if p.botUserID != creator.Id && !p.API.HasPermissionToChannel(creator.Id, channelID, model.PermissionCreatePost) { return errors.New("this channel is not accessible, you might not have permissions to write in this channel. Contact the administrator of this channel to find out if you have access permissions") } @@ -418,6 +425,7 @@ func (p *Plugin) postMeeting(creator *model.User, meetingID int, channelID strin Props: map[string]interface{}{ "attachments": []*model.SlackAttachment{&slackAttachment}, "meeting_id": meetingID, + "meeting_uuid": meetingUUID, "meeting_link": meetingURL, "meeting_status": zoom.WebhookStatusStarted, "meeting_personal": false, @@ -432,7 +440,8 @@ func (p *Plugin) postMeeting(creator *model.User, meetingID int, channelID strin return appErr } - if appErr = p.storeMeetingPostID(meetingID, createdPost.Id); appErr != nil { + p.API.LogWarn("MEETING UUID ID", "uuid", meetingUUID) + if appErr = p.storeMeetingPostID(meetingUUID, createdPost.Id); appErr != nil { p.API.LogDebug("failed to store post id", "error", appErr) } @@ -621,7 +630,14 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) { } } - if err = p.postMeeting(user, meetingID, req.ChannelID, req.RootID, topic); err != nil { + meeting, err := p.getMeeting(user, meetingID) + if err != nil { + p.API.LogWarn("failed to get the meeting", "Error", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err = p.postMeeting(user, meetingID, meeting.UUID, req.ChannelID, req.RootID, topic); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -655,6 +671,21 @@ func (p *Plugin) createMeetingWithoutPMI(user *model.User, zoomUser *zoom.User, return meeting.ID, nil } +func (p *Plugin) getMeeting(user *model.User, meetingID int) (*zoom.Meeting, error) { + client, _, err := p.getActiveClient(user) + if err != nil { + p.API.LogWarn("could not get the active zoom client", "error", err.Error()) + return nil, err + } + + meeting, err := client.GetMeeting(meetingID) + if err != nil { + p.API.LogDebug("failed to get meeting") + return nil, err + } + return meeting, nil +} + func (p *Plugin) getMeetingURL(user *model.User, meetingID int) string { defaultURL := fmt.Sprintf("%s/j/%v", p.getZoomURL(), meetingID) client, _, err := p.getActiveClient(user) diff --git a/server/store.go b/server/store.go index f06b8ed1..aeb09361 100644 --- a/server/store.go +++ b/server/store.go @@ -16,6 +16,7 @@ import ( const ( postMeetingKey = "post_meeting_" + meetingChannelKey = "meeting_channel_" zoomStateKeyPrefix = "zoomuserstate" zoomUserByMMID = "zoomtoken_" zoomUserByZoomID = "zoomtokenbyzoomid_" @@ -134,14 +135,14 @@ func (p *Plugin) deleteUserState(userID string) *model.AppError { return p.API.KVDelete(key) } -func (p *Plugin) storeMeetingPostID(meetingID int, postID string) *model.AppError { - key := fmt.Sprintf("%v%v", postMeetingKey, meetingID) +func (p *Plugin) storeMeetingPostID(meetingUUID string, postID string) *model.AppError { + key := fmt.Sprintf("%v%v", postMeetingKey, meetingUUID) bytes := []byte(postID) return p.API.KVSetWithExpiry(key, bytes, meetingPostIDTTL) } -func (p *Plugin) fetchMeetingPostID(meetingID string) (string, *model.AppError) { - key := fmt.Sprintf("%v%v", postMeetingKey, meetingID) +func (p *Plugin) fetchMeetingPostID(meetingUUID string) (string, *model.AppError) { + key := fmt.Sprintf("%v%v", postMeetingKey, meetingUUID) postID, appErr := p.API.KVGet(key) if appErr != nil { p.API.LogDebug("Could not get meeting post from KVStore", "error", appErr.Error()) @@ -161,6 +162,33 @@ func (p *Plugin) deleteMeetingPostID(postID string) *model.AppError { return p.API.KVDelete(key) } +func (p *Plugin) storeChannelForMeeting(meetingID int, channelID string) *model.AppError { + key := fmt.Sprintf("%v%v", meetingChannelKey, meetingID) + bytes := []byte(channelID) + return p.API.KVSet(key, bytes) +} + +func (p *Plugin) fetchChannelForMeeting(meetingID int) (string, *model.AppError) { + key := fmt.Sprintf("%v%v", meetingChannelKey, meetingID) + channelID, appErr := p.API.KVGet(key) + if appErr != nil { + p.API.LogDebug("Could not get channel meeting from KVStore", "error", appErr.Error()) + return "", appErr + } + + if channelID == nil { + p.API.LogWarn("Stored channel meeting not found") + return "", appErr + } + + return string(channelID), nil +} + +func (p *Plugin) deleteChannelForMeeting(meetingID int) *model.AppError { + key := fmt.Sprintf("%v%v", meetingChannelKey, meetingID) + return p.API.KVDelete(key) +} + // getOAuthUserStateKey generates and returns the key for storing the OAuth user state in the KV store. func getOAuthUserStateKey(userID string) string { return fmt.Sprintf("%s_%s", zoomStateKeyPrefix, userID) diff --git a/server/webhook.go b/server/webhook.go index bd3120dc..cc718202 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -59,6 +59,8 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) { p.API.LogWarn("New event received", "even_type", webhook.Event, "payload", string(b)) switch webhook.Event { + case zoom.EventTypeMeetingStarted: + p.handleMeetingStarted(w, r, b) case zoom.EventTypeMeetingEnded: p.handleMeetingEnded(w, r, b) case zoom.EventTypeValidateWebhook: @@ -72,6 +74,54 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) { } } +func (p *Plugin) handleMeetingStarted(w http.ResponseWriter, r *http.Request, body []byte) { + var webhook zoom.MeetingWebhook + if err := json.Unmarshal(body, &webhook); err != nil { + p.API.LogError("Error unmarshaling meeting webhook", "err", err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + meetingID, err := strconv.Atoi(webhook.Payload.Object.ID) + if err != nil { + p.API.LogError("Failed to get meeting ID", "err", err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + channelID, appErr := p.fetchChannelForMeeting(meetingID) + if appErr != nil { + http.Error(w, appErr.Error(), appErr.StatusCode) + return + } + + if channelID == "" { + return + } + + p.API.LogError("RUNNING THIS ON CHANNEL", "channel", channelID) + + botUser, appErr := p.API.GetUser(p.botUserID) + if appErr != nil { + p.API.LogError("Failed to get bot user", "err", appErr.Error()) + http.Error(w, appErr.Error(), http.StatusBadRequest) + return + } + + p.API.LogError("POSTING MEETING TO CHANNEL", "channel", channelID, "meetingID", meetingID) + + if postMeetingErr := p.postMeeting(botUser, meetingID, webhook.Payload.Object.UUID, channelID, "", webhook.Payload.Object.ID); postMeetingErr != nil { + p.API.LogError("Failed to post the zoom message in the channel", "err", postMeetingErr.Error()) + http.Error(w, postMeetingErr.Error(), http.StatusBadRequest) + return + } + + p.API.LogError("POSTED MEETING TO CHANNEL", "channel", channelID, "meetingID", meetingID) + + p.trackMeetingStart(p.botUserID, telemetryStartSourceCommand) + p.trackMeetingType(p.botUserID, false) +} + func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body []byte) { var webhook zoom.MeetingWebhook if err := json.Unmarshal(body, &webhook); err != nil { @@ -80,7 +130,8 @@ func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body return } - meetingPostID := webhook.Payload.Object.ID + meetingPostID := webhook.Payload.Object.UUID + p.API.LogWarn("MEETING ID", "uuid", meetingPostID) postID, appErr := p.fetchMeetingPostID(meetingPostID) if appErr != nil { http.Error(w, appErr.Error(), appErr.StatusCode) @@ -207,7 +258,6 @@ func (p *Plugin) handleTranscript(recording zoom.RecordingFile, postID, channelI } func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Request, body []byte) { - p.API.LogError("RUNNING THE UPDATE MEETING TRANSCRIPT") var webhook zoom.RecordingWebhook if err := json.Unmarshal(body, &webhook); err != nil { p.API.LogError("Error unmarshaling meeting webhook", "err", err.Error()) @@ -215,8 +265,8 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques return } - meetingPostID := webhook.Payload.Object.ID - postID, appErr := p.fetchMeetingPostID(strconv.Itoa(meetingPostID)) + meetingPostID := webhook.Payload.Object.UUID + postID, appErr := p.fetchMeetingPostID(meetingPostID) if appErr != nil { http.Error(w, appErr.Error(), appErr.StatusCode) return @@ -229,15 +279,18 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques return } - p.API.LogError("UPDATING MEETING TRANSCRIPT") + var lastTranscription *zoom.RecordingFile for _, recording := range webhook.Payload.Object.RecordingFiles { if recording.RecordingType == zoom.RecordingTypeAudioTranscript { - p.API.LogError("MEETING TRANSCRIPT UPDATED") - err := p.handleTranscript(recording, post.Id, post.ChannelId, webhook.DownloadToken) - if err != nil { - http.Error(w, appErr.Error(), appErr.StatusCode) - continue - } + lastTranscription = &recording + } + } + + if lastTranscription != nil { + err := p.handleTranscript(*lastTranscription, post.Id, post.ChannelId, webhook.DownloadToken) + if err != nil { + http.Error(w, appErr.Error(), appErr.StatusCode) + return } } @@ -254,7 +307,6 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques } func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request, body []byte) { - p.API.LogError("RUNNING THE UPDATE MEETING RECORDING") var webhook zoom.RecordingWebhook if err := json.Unmarshal(body, &webhook); err != nil { p.API.LogError("Error unmarshaling meeting webhook", "err", err.Error()) @@ -262,8 +314,8 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request return } - meetingPostID := webhook.Payload.Object.ID - postID, appErr := p.fetchMeetingPostID(strconv.Itoa(meetingPostID)) + meetingPostID := webhook.Payload.Object.UUID + postID, appErr := p.fetchMeetingPostID(meetingPostID) if appErr != nil { http.Error(w, appErr.Error(), appErr.StatusCode) return @@ -276,66 +328,76 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request return } - p.API.LogError("UPDATING MEETING RECORDING AND CHAT") - newPost := &model.Post{ - UserId: p.botUserID, - ChannelId: post.ChannelId, - RootId: post.Id, - Message: "", - FileIds: []string{}, - } + recordings := make(map[time.Time][]zoom.RecordingFile) + for _, recording := range webhook.Payload.Object.RecordingFiles { - p.API.LogError("RECORDING COMPLETE RECORDING", "recording", recording) if recording.RecordingType == zoom.RecordingTypeChat { - p.API.LogError("MEETING CHAT UPDATED") - request, err := http.NewRequest(http.MethodGet, recording.DownloadURL, nil) - if err != nil { - p.API.LogWarn("Unable to get the chat", "err", err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - request.Header.Set("Authorization", "Bearer "+webhook.DownloadToken) - response, err := http.DefaultClient.Do(request) - if err != nil { - p.API.LogWarn("Unable to get the chat", "err", err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - defer response.Body.Close() - chat, err := io.ReadAll(response.Body) - if err != nil { - p.API.LogWarn("Unable to get the chat", "err", err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - fileInfo, appErr := p.API.UploadFile(chat, post.ChannelId, "chat.txt") - if appErr != nil { - p.API.LogWarn("Unable to get the chat", "err", appErr) - http.Error(w, appErr.Error(), http.StatusBadRequest) - return - } - - newPost.FileIds = append(newPost.FileIds, fileInfo.Id) - newPost.AddProp("captions", []any{map[string]any{"file_id": fileInfo.Id}}) - newPost.Type = "custom_zoom_chat" - if newPost.Message == "" { - newPost.Message = " " - } + recordings[recording.RecordingStart] = append(recordings[recording.RecordingStart], recording) } if recording.RecordingType == zoom.RecordingTypeVideo { - newPost.Message = "Here's the zoom meeting recording:\n**Link:** [Meeting Recording](" + recording.PlayURL + ")\n**Password:** " + webhook.Payload.Object.Password + recordings[recording.RecordingStart] = append(recordings[recording.RecordingStart], recording) } } - if newPost.Message != "" { - _, appErr = p.API.CreatePost(newPost) - if appErr != nil { - p.API.LogWarn("Could not update the post", "err", appErr) - http.Error(w, appErr.Error(), appErr.StatusCode) - return + for _, recordingGroup := range recordings { + newPost := &model.Post{ + UserId: p.botUserID, + ChannelId: post.ChannelId, + RootId: post.Id, + Message: "", + FileIds: []string{}, + } + for _, recording := range recordingGroup { + if recording.RecordingType == zoom.RecordingTypeChat { + request, err := http.NewRequest(http.MethodGet, recording.DownloadURL, nil) + if err != nil { + p.API.LogWarn("Unable to get the chat", "err", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + request.Header.Set("Authorization", "Bearer "+webhook.DownloadToken) + response, err := http.DefaultClient.Do(request) + if err != nil { + p.API.LogWarn("Unable to get the chat", "err", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + defer response.Body.Close() + chat, err := io.ReadAll(response.Body) + if err != nil { + p.API.LogWarn("Unable to get the chat", "err", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + fileInfo, appErr := p.API.UploadFile(chat, post.ChannelId, "Chat-history.txt") + if appErr != nil { + p.API.LogWarn("Unable to get the chat", "err", appErr) + http.Error(w, appErr.Error(), http.StatusBadRequest) + return + } + + newPost.FileIds = append(newPost.FileIds, fileInfo.Id) + newPost.AddProp("captions", []any{map[string]any{"file_id": fileInfo.Id}}) + newPost.Type = "custom_zoom_chat" + if newPost.Message == "" { + newPost.Message = " " + } + } + if recording.RecordingType == zoom.RecordingTypeVideo { + newPost.Message = "Here's the zoom meeting recording:\n**Link:** [Meeting Recording](" + recording.PlayURL + ")\n**Password:** " + webhook.Payload.Object.Password + } + } + if newPost.Message != "" { + _, appErr = p.API.CreatePost(newPost) + if appErr != nil { + p.API.LogWarn("Could not update the post", "err", appErr) + http.Error(w, appErr.Error(), appErr.StatusCode) + return + } } } + // TODO: Delete the meeting post if is no longer needed // if appErr = p.deleteMeetingPostID(meetingPostID); appErr != nil { // p.API.LogWarn("failed to delete db entry", "error", appErr.Error()) From 467399e39bbd2018edfb06498ad483eabb4102b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 15 May 2024 19:54:08 +0200 Subject: [PATCH 06/52] Removing debug log messages --- server/http.go | 1 - server/webhook.go | 26 -------------------------- 2 files changed, 27 deletions(-) diff --git a/server/http.go b/server/http.go index 56d17820..7bf8a923 100644 --- a/server/http.go +++ b/server/http.go @@ -440,7 +440,6 @@ func (p *Plugin) postMeeting(creator *model.User, meetingID int, meetingUUID str return appErr } - p.API.LogWarn("MEETING UUID ID", "uuid", meetingUUID) if appErr = p.storeMeetingPostID(meetingUUID, createdPost.Id); appErr != nil { p.API.LogDebug("failed to store post id", "error", appErr) } diff --git a/server/webhook.go b/server/webhook.go index cc718202..5729c2b8 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -57,7 +57,6 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) { } } - p.API.LogWarn("New event received", "even_type", webhook.Event, "payload", string(b)) switch webhook.Event { case zoom.EventTypeMeetingStarted: p.handleMeetingStarted(w, r, b) @@ -99,8 +98,6 @@ func (p *Plugin) handleMeetingStarted(w http.ResponseWriter, r *http.Request, bo return } - p.API.LogError("RUNNING THIS ON CHANNEL", "channel", channelID) - botUser, appErr := p.API.GetUser(p.botUserID) if appErr != nil { p.API.LogError("Failed to get bot user", "err", appErr.Error()) @@ -108,16 +105,12 @@ func (p *Plugin) handleMeetingStarted(w http.ResponseWriter, r *http.Request, bo return } - p.API.LogError("POSTING MEETING TO CHANNEL", "channel", channelID, "meetingID", meetingID) - if postMeetingErr := p.postMeeting(botUser, meetingID, webhook.Payload.Object.UUID, channelID, "", webhook.Payload.Object.ID); postMeetingErr != nil { p.API.LogError("Failed to post the zoom message in the channel", "err", postMeetingErr.Error()) http.Error(w, postMeetingErr.Error(), http.StatusBadRequest) return } - p.API.LogError("POSTED MEETING TO CHANNEL", "channel", channelID, "meetingID", meetingID) - p.trackMeetingStart(p.botUserID, telemetryStartSourceCommand) p.trackMeetingType(p.botUserID, false) } @@ -131,7 +124,6 @@ func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body } meetingPostID := webhook.Payload.Object.UUID - p.API.LogWarn("MEETING ID", "uuid", meetingPostID) postID, appErr := p.fetchMeetingPostID(meetingPostID) if appErr != nil { http.Error(w, appErr.Error(), appErr.StatusCode) @@ -180,12 +172,6 @@ func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body return } - // TODO: Delete the meeting post if is no longer needed - // if appErr = p.deleteMeetingPostID(meetingPostID); appErr != nil { - // p.API.LogWarn("failed to delete db entry", "error", appErr.Error()) - // return - // } - w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(post); err != nil { p.API.LogWarn("failed to write response", "error", err.Error()) @@ -294,12 +280,6 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques } } - // TODO: Delete the meeting post if is no longer needed - // if appErr = p.deleteMeetingPostID(meetingPostID); appErr != nil { - // p.API.LogWarn("failed to delete db entry", "error", appErr.Error()) - // return - // } - w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(post); err != nil { p.API.LogWarn("failed to write response", "error", err.Error()) @@ -398,12 +378,6 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request } } - // TODO: Delete the meeting post if is no longer needed - // if appErr = p.deleteMeetingPostID(meetingPostID); appErr != nil { - // p.API.LogWarn("failed to delete db entry", "error", appErr.Error()) - // return - // } - w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(post); err != nil { p.API.LogWarn("failed to write response", "error", err.Error()) From e4485896ff2b289de721df25d566f8a78e7be1b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 15 May 2024 20:15:54 +0200 Subject: [PATCH 07/52] Fixing tests --- server/plugin_test.go | 5 +++-- server/webhook_test.go | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/server/plugin_test.go b/server/plugin_test.go index 230ea133..73fa7877 100644 --- a/server/plugin_test.go +++ b/server/plugin_test.go @@ -61,10 +61,10 @@ func TestPlugin(t *testing.T) { meetingRequest := httptest.NewRequest("POST", "/api/v1/meetings", strings.NewReader("{\"channel_id\": \"thechannelid\"}")) meetingRequest.Header.Add("Mattermost-User-Id", "theuserid") - endedPayload := `{"event": "meeting.ended", "payload": {"object": {"id": "234"}}}` + endedPayload := `{"event": "meeting.ended", "payload": {"object": {"id": "234", "uuid": "234"}}}` validStoppedWebhookRequest := httptest.NewRequest("POST", "/webhook?secret=thewebhooksecret", strings.NewReader(endedPayload)) - validStartedWebhookRequest := httptest.NewRequest("POST", "/webhook?secret=thewebhooksecret", strings.NewReader(`{"event": "meeting.started"}`)) + validStartedWebhookRequest := httptest.NewRequest("POST", "/webhook?secret=thewebhooksecret", strings.NewReader(`{"event": "meeting.started", "payload": {"object": {"id": "234"}}}`)) noSecretWebhookRequest := httptest.NewRequest("POST", "/webhook", strings.NewReader(endedPayload)) @@ -153,6 +153,7 @@ func TestPlugin(t *testing.T) { api.On("KVGet", fmt.Sprintf("%v%v", postMeetingKey, 234)).Return([]byte("thepostid"), nil) api.On("KVGet", fmt.Sprintf("%v%v", postMeetingKey, 123)).Return([]byte("thepostid"), nil) + api.On("KVGet", fmt.Sprintf("%v%v", meetingChannelKey, 234)).Return([]byte(""), nil) api.On("KVDelete", fmt.Sprintf("%v%v", postMeetingKey, 234)).Return(nil) diff --git a/server/webhook_test.go b/server/webhook_test.go index 70755e23..dbd92510 100644 --- a/server/webhook_test.go +++ b/server/webhook_test.go @@ -2,6 +2,9 @@ package main import ( "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" "encoding/json" "io" "net/http/httptest" @@ -63,10 +66,12 @@ func TestWebhookVerifySignature(t *testing.T) { api.On("LogDebug", "Could not get meeting post from KVStore", "error", "") p.SetAPI(api) - requestBody := `{"payload":{"object": {"id": "123"}},"event":"meeting.ended"}` + requestBody := `{"payload":{"object": {"id": "123", "uuid": "123"}},"event":"meeting.ended"}` ts := "1660149894817" - signature := "v0=7fe2f9e66d133961eff4746eda161096cebe8d677319d66546281d88ea147189" + h := hmac.New(sha256.New, []byte(testConfig.ZoomWebhookSecret)) + _, _ = h.Write([]byte("v0:" + ts + ":" + requestBody)) + signature := "v0=" + hex.EncodeToString(h.Sum(nil)) w := httptest.NewRecorder() reqBody := io.NopCloser(bytes.NewBufferString(requestBody)) From df60c28872203d1d655148bf2d5c0b841ba4d8fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 15 May 2024 20:21:35 +0200 Subject: [PATCH 08/52] fixing linter errors --- webapp/src/components/post_type_chat.jsx | 20 ++++++++++++++----- .../components/post_type_transcription.jsx | 14 +++++++++++-- webapp/webpack.config.js | 5 +++-- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/webapp/src/components/post_type_chat.jsx b/webapp/src/components/post_type_chat.jsx index e616ff7f..e942d31d 100644 --- a/webapp/src/components/post_type_chat.jsx +++ b/webapp/src/components/post_type_chat.jsx @@ -1,9 +1,12 @@ import React from 'react'; +import PropTypes from 'prop-types'; import {FormattedMessage} from 'react-intl'; import {useSelector} from 'react-redux'; -import IconAI from 'src/components/ai_icon'; + import styled from 'styled-components'; +import IconAI from 'src/components/ai_icon'; + const useAIAvailable = () => { //@ts-ignore plugins state is a thing return useSelector((state) => Boolean(state.plugins?.plugins?.[aiPluginID])); @@ -54,7 +57,7 @@ export const PostTypeChat = (props) => { callsPostButtonClicked?.(props.post); }; - const msg = props.post.message; + const markdownMessage = props.post.message; const renderPostWithMarkdown = (msg) => { const {formatText, messageHtmlToComponent} = window.PostUtils; @@ -63,19 +66,26 @@ export const PostTypeChat = (props) => { formatText(msg, {}), false, ); - } + }; return (
- {renderPostWithMarkdown(msg)} + {renderPostWithMarkdown(markdownMessage)} {aiAvailable && callsPostButtonClicked && - + }
); }; + +PostTypeChat.propTypes = { + post: PropTypes.object.isRequired, +}; diff --git a/webapp/src/components/post_type_transcription.jsx b/webapp/src/components/post_type_transcription.jsx index 9223354c..5903f3b3 100644 --- a/webapp/src/components/post_type_transcription.jsx +++ b/webapp/src/components/post_type_transcription.jsx @@ -1,9 +1,12 @@ import React from 'react'; +import PropTypes from 'prop-types'; import {FormattedMessage} from 'react-intl'; import {useSelector} from 'react-redux'; -import IconAI from 'src/components/ai_icon'; + import styled from 'styled-components'; +import IconAI from 'src/components/ai_icon'; + const useAIAvailable = () => { //@ts-ignore plugins state is a thing return useSelector((state) => Boolean(state.plugins?.plugins?.[aiPluginID])); @@ -64,9 +67,16 @@ export const PostTypeTranscription = (props) => { onClick={createMeetingSummary} > - + } ); }; + +PostTypeTranscription.propTypes = { + post: PropTypes.object.isRequired, +}; diff --git a/webapp/webpack.config.js b/webapp/webpack.config.js index de72ca1a..1d91a1a2 100644 --- a/webapp/webpack.config.js +++ b/webapp/webpack.config.js @@ -31,8 +31,9 @@ module.exports = { }, plugins: [ new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), - }) + // eslint-disable-next-line no-process-env + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), + }), ], externals: { react: 'React', From bc696fdb386ed447650a036701c2b14dd53941d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 15 May 2024 20:35:19 +0200 Subject: [PATCH 09/52] Fixing linter errors --- server/store.go | 5 ----- server/webhook.go | 27 ++++++++++++++------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/server/store.go b/server/store.go index aeb09361..4e3862bb 100644 --- a/server/store.go +++ b/server/store.go @@ -157,11 +157,6 @@ func (p *Plugin) fetchMeetingPostID(meetingUUID string) (string, *model.AppError return string(postID), nil } -func (p *Plugin) deleteMeetingPostID(postID string) *model.AppError { - key := fmt.Sprintf("%v%v", postMeetingKey, postID) - return p.API.KVDelete(key) -} - func (p *Plugin) storeChannelForMeeting(meetingID int, channelID string) *model.AppError { key := fmt.Sprintf("%v%v", meetingChannelKey, meetingID) bytes := []byte(channelID) diff --git a/server/webhook.go b/server/webhook.go index 5729c2b8..79194f37 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -189,18 +189,19 @@ func (p *Plugin) handleTranscript(recording zoom.RecordingFile, postID, channelI retries := 5 var response *http.Response for retries > 0 { - var err error response, err = http.DefaultClient.Do(request) if err != nil { p.API.LogWarn("Unable to get the transcription", "err", err) time.Sleep(1 * time.Second) - retries -= 1 + retries-- continue } + if response.StatusCode != http.StatusOK { + response.Body.Close() p.API.LogWarn("Unable to get the transcription", "err", "bad status code "+strconv.Itoa(response.StatusCode)) time.Sleep(1 * time.Second) - retries -= 1 + retries-- continue } break @@ -265,17 +266,17 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques return } - var lastTranscription *zoom.RecordingFile - for _, recording := range webhook.Payload.Object.RecordingFiles { + lastTranscriptionIdx := -1 + for idx, recording := range webhook.Payload.Object.RecordingFiles { if recording.RecordingType == zoom.RecordingTypeAudioTranscript { - lastTranscription = &recording + lastTranscriptionIdx = idx } } - if lastTranscription != nil { - err := p.handleTranscript(*lastTranscription, post.Id, post.ChannelId, webhook.DownloadToken) + if lastTranscriptionIdx != -1 { + err := p.handleTranscript(webhook.Payload.Object.RecordingFiles[lastTranscriptionIdx], post.Id, post.ChannelId, webhook.DownloadToken) if err != nil { - http.Error(w, appErr.Error(), appErr.StatusCode) + http.Error(w, err.Error(), http.StatusBadRequest) return } } @@ -350,10 +351,10 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request http.Error(w, err.Error(), http.StatusBadRequest) return } - fileInfo, appErr := p.API.UploadFile(chat, post.ChannelId, "Chat-history.txt") - if appErr != nil { - p.API.LogWarn("Unable to get the chat", "err", appErr) - http.Error(w, appErr.Error(), http.StatusBadRequest) + fileInfo, appErr2 := p.API.UploadFile(chat, post.ChannelId, "Chat-history.txt") + if appErr2 != nil { + p.API.LogWarn("Unable to get the chat", "err", appErr2) + http.Error(w, appErr2.Error(), http.StatusBadRequest) return } From 513006a9f3e948084c90165265f0434b91cffd7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 16 May 2024 10:39:42 +0200 Subject: [PATCH 10/52] Addressing PR review comments --- server/command.go | 22 ++++++++++++++++++++-- server/webhook.go | 25 +++---------------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/server/command.go b/server/command.go index 3b331dc6..9d95a15f 100644 --- a/server/command.go +++ b/server/command.go @@ -241,18 +241,36 @@ func (p *Plugin) runConnectCommand(user *model.User, extra *model.CommandArgs) ( } func (p *Plugin) runSubscribeCommand(user *model.User, extra *model.CommandArgs, meetingID int) (string, error) { + if !p.API.HasPermissionToChannel(user.Id, extra.ChannelId, model.PermissionCreatePost) { + return "You do not have permission to subscribe to this channel", nil + } + + _, err := p.getMeeting(user, meetingID) + if err != nil { + return "Can not subscribe to meeting: meeting not found", errors.Wrap(err, "meeting not found") + } + if appErr := p.storeChannelForMeeting(meetingID, extra.ChannelId); appErr != nil { - return "", errors.Wrap(appErr, "cannot subscribing to meeting") + return "", errors.Wrap(appErr, "cannot subscribe to meeting") } return "Channel subscribed to meeting", nil } func (p *Plugin) runUnsubscribeCommand(user *model.User, extra *model.CommandArgs, meetingID int) (string, error) { + if !p.API.HasPermissionToChannel(user.Id, extra.ChannelId, model.PermissionCreatePost) { + return "You do not have permission to unsubscribe from this channel", nil + } + + _, err := p.getMeeting(user, meetingID) + if err != nil { + return "Can not unsubscribe from meeting: meeting not found", errors.Wrap(err, "meeting not found") + } + if channelID, appErr := p.fetchChannelForMeeting(meetingID); appErr != nil || channelID == "" { return "Can not unsubscribe from meeting: meeting not found", errors.New("meeting not found") } if appErr := p.deleteChannelForMeeting(meetingID); appErr != nil { - return "Can not unsubscribe from meeting: unable to delete the meeting subscription", errors.Wrap(appErr, "cannot unsubscribing from meeting") + return "Can not unsubscribe from meeting: unable to delete the meeting subscription", errors.Wrap(appErr, "cannot unsubscribe from meeting") } return "Channel unsubscribed from meeting", nil } diff --git a/server/webhook.go b/server/webhook.go index 79194f37..19da6365 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -90,7 +90,6 @@ func (p *Plugin) handleMeetingStarted(w http.ResponseWriter, r *http.Request, bo channelID, appErr := p.fetchChannelForMeeting(meetingID) if appErr != nil { - http.Error(w, appErr.Error(), appErr.StatusCode) return } @@ -101,13 +100,11 @@ func (p *Plugin) handleMeetingStarted(w http.ResponseWriter, r *http.Request, bo botUser, appErr := p.API.GetUser(p.botUserID) if appErr != nil { p.API.LogError("Failed to get bot user", "err", appErr.Error()) - http.Error(w, appErr.Error(), http.StatusBadRequest) return } if postMeetingErr := p.postMeeting(botUser, meetingID, webhook.Payload.Object.UUID, channelID, "", webhook.Payload.Object.ID); postMeetingErr != nil { p.API.LogError("Failed to post the zoom message in the channel", "err", postMeetingErr.Error()) - http.Error(w, postMeetingErr.Error(), http.StatusBadRequest) return } @@ -126,14 +123,12 @@ func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body meetingPostID := webhook.Payload.Object.UUID postID, appErr := p.fetchMeetingPostID(meetingPostID) if appErr != nil { - http.Error(w, appErr.Error(), appErr.StatusCode) return } post, appErr := p.API.GetPost(postID) if appErr != nil { p.API.LogWarn("Could not get meeting post by id", "err", appErr) - http.Error(w, appErr.Error(), appErr.StatusCode) return } @@ -168,7 +163,6 @@ func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body _, appErr = p.API.UpdatePost(post) if appErr != nil { p.API.LogWarn("Could not update the post", "err", appErr) - http.Error(w, appErr.Error(), appErr.StatusCode) return } @@ -213,14 +207,14 @@ func (p *Plugin) handleTranscript(recording zoom.RecordingFile, postID, channelI } defer response.Body.Close() - transcription, err := io.ReadAll(response.Body) + transcriptionBytes, err := io.ReadAll(response.Body) if err != nil { p.API.LogWarn("Unable to get the transcription", "err", err) return err } - fileInfo, appErr := p.API.UploadFile(transcription, channelID, "transcription.txt") + fileInfo, appErr := p.API.UploadFile(transcriptionBytes, channelID, "transcription.txt") if appErr != nil { - p.API.LogWarn("Unable to get the transcription", "err", appErr) + p.API.LogWarn("Unable to save transcription file to the channel", "err", appErr) return appErr } newPost := &model.Post{ @@ -255,14 +249,12 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques meetingPostID := webhook.Payload.Object.UUID postID, appErr := p.fetchMeetingPostID(meetingPostID) if appErr != nil { - http.Error(w, appErr.Error(), appErr.StatusCode) return } post, appErr := p.API.GetPost(postID) if appErr != nil { p.API.LogWarn("Could not get meeting post by id", "err", appErr) - http.Error(w, appErr.Error(), appErr.StatusCode) return } @@ -276,7 +268,6 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques if lastTranscriptionIdx != -1 { err := p.handleTranscript(webhook.Payload.Object.RecordingFiles[lastTranscriptionIdx], post.Id, post.ChannelId, webhook.DownloadToken) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) return } } @@ -298,14 +289,12 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request meetingPostID := webhook.Payload.Object.UUID postID, appErr := p.fetchMeetingPostID(meetingPostID) if appErr != nil { - http.Error(w, appErr.Error(), appErr.StatusCode) return } post, appErr := p.API.GetPost(postID) if appErr != nil { p.API.LogWarn("Could not get meeting post by id", "err", appErr) - http.Error(w, appErr.Error(), appErr.StatusCode) return } @@ -334,36 +323,29 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request request, err := http.NewRequest(http.MethodGet, recording.DownloadURL, nil) if err != nil { p.API.LogWarn("Unable to get the chat", "err", err) - http.Error(w, err.Error(), http.StatusBadRequest) return } request.Header.Set("Authorization", "Bearer "+webhook.DownloadToken) response, err := http.DefaultClient.Do(request) if err != nil { p.API.LogWarn("Unable to get the chat", "err", err) - http.Error(w, err.Error(), http.StatusBadRequest) return } defer response.Body.Close() chat, err := io.ReadAll(response.Body) if err != nil { p.API.LogWarn("Unable to get the chat", "err", err) - http.Error(w, err.Error(), http.StatusBadRequest) return } fileInfo, appErr2 := p.API.UploadFile(chat, post.ChannelId, "Chat-history.txt") if appErr2 != nil { p.API.LogWarn("Unable to get the chat", "err", appErr2) - http.Error(w, appErr2.Error(), http.StatusBadRequest) return } newPost.FileIds = append(newPost.FileIds, fileInfo.Id) newPost.AddProp("captions", []any{map[string]any{"file_id": fileInfo.Id}}) newPost.Type = "custom_zoom_chat" - if newPost.Message == "" { - newPost.Message = " " - } } if recording.RecordingType == zoom.RecordingTypeVideo { newPost.Message = "Here's the zoom meeting recording:\n**Link:** [Meeting Recording](" + recording.PlayURL + ")\n**Password:** " + webhook.Payload.Object.Password @@ -373,7 +355,6 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request _, appErr = p.API.CreatePost(newPost) if appErr != nil { p.API.LogWarn("Could not update the post", "err", appErr) - http.Error(w, appErr.Error(), appErr.StatusCode) return } } From 8b30a5d79d228eef9b20cf9e0c80c0c1ea6e07e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 16 May 2024 10:42:49 +0200 Subject: [PATCH 11/52] Updating webhooks subscriptions needed in the documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 211aa8ab..3ad8a627 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ When a Zoom meeting ends, the original link shared in the channel can be changed ![mattermost_webhook_secret](https://github.com/mattermost/mattermost-plugin-zoom/assets/74422101/58b9ac74-ecf9-4e3e-986e-94fd4c39bfb0) -Select **Add events** and select the **End Meeting** event. +Select **Add events** and select the **Start Meeting**, **End Meeting**, **All Recordings have completed** and **Recording Transcript files have completed** events. ![event_types](https://github.com/mattermost/mattermost-plugin-zoom/assets/74422101/a2f04fe9-e4fa-4e78-b198-1f493e53cf93) From a8100649a7385cd1cf1cb7ae23307502e64e36fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 16 May 2024 10:48:20 +0200 Subject: [PATCH 12/52] Addressing PR review comments --- webapp/src/components/post_type_chat.jsx | 22 +++++++++---------- .../components/post_type_transcription.jsx | 22 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/webapp/src/components/post_type_chat.jsx b/webapp/src/components/post_type_chat.jsx index e942d31d..629be5ab 100644 --- a/webapp/src/components/post_type_chat.jsx +++ b/webapp/src/components/post_type_chat.jsx @@ -71,17 +71,17 @@ export const PostTypeChat = (props) => { return (
{renderPostWithMarkdown(markdownMessage)} - {aiAvailable && callsPostButtonClicked && - - - - - } + {aiAvailable && callsPostButtonClicked && ( + + + + + )}
); }; diff --git a/webapp/src/components/post_type_transcription.jsx b/webapp/src/components/post_type_transcription.jsx index 5903f3b3..6e89292f 100644 --- a/webapp/src/components/post_type_transcription.jsx +++ b/webapp/src/components/post_type_transcription.jsx @@ -62,17 +62,17 @@ export const PostTypeTranscription = (props) => { return (
{msg} - {aiAvailable && callsPostButtonClicked && - - - - - } + {aiAvailable && callsPostButtonClicked && ( + + + + + )}
); }; From 9ae260c6adfb62dc3d123104823d517cbf1afd40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 16 May 2024 10:49:45 +0200 Subject: [PATCH 13/52] Addressing PR review comments --- webapp/package-lock.json | 20 +++++++++++--------- webapp/package.json | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/webapp/package-lock.json b/webapp/package-lock.json index ee7755d4..c4f52b15 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -12,7 +12,7 @@ "core-js": "3.7.0", "mattermost-redux": "5.33.1", "prop-types": "15.7.2", - "react": "18.3.1", + "react": "17.0.2", "react-intl": "4.7.6", "react-redux": "7.2.2", "redux": "4.0.5", @@ -12023,11 +12023,12 @@ } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "dependencies": { - "loose-envify": "^1.1.0" + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" }, "engines": { "node": ">=0.10.0" @@ -23580,11 +23581,12 @@ "peer": true }, "react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "requires": { - "loose-envify": "^1.1.0" + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" } }, "react-devtools-core": { diff --git a/webapp/package.json b/webapp/package.json index 2a5f7131..41e9fda0 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -22,7 +22,7 @@ "core-js": "3.7.0", "mattermost-redux": "5.33.1", "prop-types": "15.7.2", - "react": "18.3.1", + "react": "17.0.2", "react-intl": "4.7.6", "react-redux": "7.2.2", "redux": "4.0.5", From 42bfc3adcdf3a4dc6fb95a6233f3a7228e615e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 16 May 2024 11:00:58 +0200 Subject: [PATCH 14/52] Avoid subscriptions to personal meetings --- server/command.go | 6 +++++- server/zoom/meeting.go | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/server/command.go b/server/command.go index 9d95a15f..1685d41b 100644 --- a/server/command.go +++ b/server/command.go @@ -245,11 +245,15 @@ func (p *Plugin) runSubscribeCommand(user *model.User, extra *model.CommandArgs, return "You do not have permission to subscribe to this channel", nil } - _, err := p.getMeeting(user, meetingID) + meeting, err := p.getMeeting(user, meetingID) if err != nil { return "Can not subscribe to meeting: meeting not found", errors.Wrap(err, "meeting not found") } + if meeting.Type == zoom.MeetingTypePersonal { + return "Can not subscribe to personal meeting", nil + } + if appErr := p.storeChannelForMeeting(meetingID, extra.ChannelId); appErr != nil { return "", errors.Wrap(appErr, "cannot subscribe to meeting") } diff --git a/server/zoom/meeting.go b/server/zoom/meeting.go index 040408c5..9b440620 100644 --- a/server/zoom/meeting.go +++ b/server/zoom/meeting.go @@ -13,6 +13,8 @@ const ( MeetingTypeScheduled MeetingType = 2 // MeetingTypeRecurringWithNoFixedTime meeting MeetingTypeRecurringWithNoFixedTime MeetingType = 3 + // MeetingTypePersonal + MeetingTypePersonal MeetingType = 4 // MeetingTypeRecurringWithFixedTime meeting MeetingTypeRecurringWithFixedTime MeetingType = 8 ) From 4f6233f3282b963b950ac9d423b3cf4a713dd932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 16 May 2024 22:04:31 +0200 Subject: [PATCH 15/52] Migrating to typescript --- webapp/package-lock.json | 25 +++++++++++++ webapp/package.json | 1 + .../components/{ai_icon.jsx => ai_icon.tsx} | 0 ...{post_type_chat.jsx => post_type_chat.tsx} | 36 +++++++++++-------- ...iption.jsx => post_type_transcription.tsx} | 31 +++++++++------- webapp/src/components/{svg.jsx => svg.tsx} | 0 webapp/tsconfig.json | 1 + 7 files changed, 66 insertions(+), 28 deletions(-) rename webapp/src/components/{ai_icon.jsx => ai_icon.tsx} (100%) rename webapp/src/components/{post_type_chat.jsx => post_type_chat.tsx} (68%) rename webapp/src/components/{post_type_transcription.jsx => post_type_transcription.tsx} (71%) rename webapp/src/components/{svg.jsx => svg.tsx} (100%) diff --git a/webapp/package-lock.json b/webapp/package-lock.json index c4f52b15..1d4b7417 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -32,6 +32,7 @@ "@babel/runtime": "7.16.7", "@emotion/babel-preset-css-prop": "11.2.0", "@emotion/react": "11.11.0", + "@types/react-redux": "7.1.33", "@typescript-eslint/eslint-plugin": "5.10.1", "@typescript-eslint/parser": "5.10.1", "babel-eslint": "10.1.0", @@ -4741,6 +4742,18 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-redux": { + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", + "dev": true, + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -18017,6 +18030,18 @@ "csstype": "^3.0.2" } }, + "@types/react-redux": { + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", + "dev": true, + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", diff --git a/webapp/package.json b/webapp/package.json index 41e9fda0..b21bedff 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -42,6 +42,7 @@ "@babel/runtime": "7.16.7", "@emotion/babel-preset-css-prop": "11.2.0", "@emotion/react": "11.11.0", + "@types/react-redux": "7.1.33", "@typescript-eslint/eslint-plugin": "5.10.1", "@typescript-eslint/parser": "5.10.1", "babel-eslint": "10.1.0", diff --git a/webapp/src/components/ai_icon.jsx b/webapp/src/components/ai_icon.tsx similarity index 100% rename from webapp/src/components/ai_icon.jsx rename to webapp/src/components/ai_icon.tsx diff --git a/webapp/src/components/post_type_chat.jsx b/webapp/src/components/post_type_chat.tsx similarity index 68% rename from webapp/src/components/post_type_chat.jsx rename to webapp/src/components/post_type_chat.tsx index 629be5ab..979d4088 100644 --- a/webapp/src/components/post_type_chat.jsx +++ b/webapp/src/components/post_type_chat.tsx @@ -1,23 +1,28 @@ import React from 'react'; -import PropTypes from 'prop-types'; import {FormattedMessage} from 'react-intl'; import {useSelector} from 'react-redux'; +import type {Post} from 'mattermost-redux/types/posts'; +import type {GlobalState} from 'mattermost-redux/types/store'; + import styled from 'styled-components'; import IconAI from 'src/components/ai_icon'; +const aiPluginID = 'mattermost-ai'; + const useAIAvailable = () => { - //@ts-ignore plugins state is a thing - return useSelector((state) => Boolean(state.plugins?.plugins?.[aiPluginID])); + return useSelector((state: any) => Boolean(state.plugins?.plugins?.[aiPluginID])); }; -const aiPluginID = 'mattermost-ai'; - const useCallsPostButtonClicked = () => { - return useSelector((state) => { - //@ts-ignore plugins state is a thing - return state['plugins-' + aiPluginID]?.callsPostButtonClickedTranscription; + return useSelector((state: GlobalState) => { + type StateWithAiPluginState = { + 'plugins-mattermost-ai'?: {callsPostButtonClickedTranscription: (post: Post) => void}; + } + const stateTyped: StateWithAiPluginState = state as StateWithAiPluginState; + const aiPluginState = stateTyped['plugins-mattermost-ai']; + return aiPluginState?.callsPostButtonClickedTranscription; }); }; @@ -49,7 +54,11 @@ const CreateMeetingSummaryButton = styled.button` } `; -export const PostTypeChat = (props) => { +type Props = { + post: Post; +}; + +export const PostTypeChat = (props: Props) => { const aiAvailable = useAIAvailable(); const callsPostButtonClicked = useCallsPostButtonClicked(); @@ -59,8 +68,9 @@ export const PostTypeChat = (props) => { const markdownMessage = props.post.message; - const renderPostWithMarkdown = (msg) => { - const {formatText, messageHtmlToComponent} = window.PostUtils; + const renderPostWithMarkdown = (msg: string) => { + const windowAny: any = window; + const {formatText, messageHtmlToComponent} = windowAny.PostUtils; return messageHtmlToComponent( formatText(msg, {}), @@ -85,7 +95,3 @@ export const PostTypeChat = (props) => { ); }; - -PostTypeChat.propTypes = { - post: PropTypes.object.isRequired, -}; diff --git a/webapp/src/components/post_type_transcription.jsx b/webapp/src/components/post_type_transcription.tsx similarity index 71% rename from webapp/src/components/post_type_transcription.jsx rename to webapp/src/components/post_type_transcription.tsx index 6e89292f..b8316419 100644 --- a/webapp/src/components/post_type_transcription.jsx +++ b/webapp/src/components/post_type_transcription.tsx @@ -1,23 +1,28 @@ import React from 'react'; -import PropTypes from 'prop-types'; import {FormattedMessage} from 'react-intl'; import {useSelector} from 'react-redux'; +import type {Post} from 'mattermost-redux/types/posts'; +import type {GlobalState} from 'mattermost-redux/types/store'; + import styled from 'styled-components'; import IconAI from 'src/components/ai_icon'; +const aiPluginID = 'mattermost-ai'; + const useAIAvailable = () => { - //@ts-ignore plugins state is a thing - return useSelector((state) => Boolean(state.plugins?.plugins?.[aiPluginID])); + return useSelector((state: any) => Boolean(state.plugins?.plugins?.[aiPluginID])); }; -const aiPluginID = 'mattermost-ai'; - const useCallsPostButtonClicked = () => { - return useSelector((state) => { - //@ts-ignore plugins state is a thing - return state['plugins-' + aiPluginID]?.callsPostButtonClickedTranscription; + return useSelector((state: GlobalState) => { + type StateWithAiPluginState = { + 'plugins-mattermost-ai'?: {callsPostButtonClickedTranscription: (post: Post) => void}; + } + const stateTyped: StateWithAiPluginState = state as StateWithAiPluginState; + const aiPluginState = stateTyped['plugins-mattermost-ai']; + return aiPluginState?.callsPostButtonClickedTranscription; }); }; @@ -49,7 +54,11 @@ const CreateMeetingSummaryButton = styled.button` } `; -export const PostTypeTranscription = (props) => { +type Props = { + post: Post; +}; + +export const PostTypeTranscription = (props: Props) => { const aiAvailable = useAIAvailable(); const callsPostButtonClicked = useCallsPostButtonClicked(); @@ -76,7 +85,3 @@ export const PostTypeTranscription = (props) => { ); }; - -PostTypeTranscription.propTypes = { - post: PropTypes.object.isRequired, -}; diff --git a/webapp/src/components/svg.jsx b/webapp/src/components/svg.tsx similarity index 100% rename from webapp/src/components/svg.jsx rename to webapp/src/components/svg.tsx diff --git a/webapp/tsconfig.json b/webapp/tsconfig.json index 76616e9f..2a66c7eb 100644 --- a/webapp/tsconfig.json +++ b/webapp/tsconfig.json @@ -6,6 +6,7 @@ "dom.iterable", "esnext" ], + "jsx": "react", "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, From 0cc74d99de3159feebb7c1cfc6487e70084b05ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 16 May 2024 22:06:40 +0200 Subject: [PATCH 16/52] Migrating to typescript --- webapp/src/components/post_type_chat.tsx | 9 ++------- webapp/src/components/post_type_transcription.tsx | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/webapp/src/components/post_type_chat.tsx b/webapp/src/components/post_type_chat.tsx index 979d4088..54040227 100644 --- a/webapp/src/components/post_type_chat.tsx +++ b/webapp/src/components/post_type_chat.tsx @@ -3,7 +3,6 @@ import {FormattedMessage} from 'react-intl'; import {useSelector} from 'react-redux'; import type {Post} from 'mattermost-redux/types/posts'; -import type {GlobalState} from 'mattermost-redux/types/store'; import styled from 'styled-components'; @@ -16,12 +15,8 @@ const useAIAvailable = () => { }; const useCallsPostButtonClicked = () => { - return useSelector((state: GlobalState) => { - type StateWithAiPluginState = { - 'plugins-mattermost-ai'?: {callsPostButtonClickedTranscription: (post: Post) => void}; - } - const stateTyped: StateWithAiPluginState = state as StateWithAiPluginState; - const aiPluginState = stateTyped['plugins-mattermost-ai']; + return useSelector((state: any) => { + const aiPluginState = state['plugins-'+aiPluginID]; return aiPluginState?.callsPostButtonClickedTranscription; }); }; diff --git a/webapp/src/components/post_type_transcription.tsx b/webapp/src/components/post_type_transcription.tsx index b8316419..e2bc7630 100644 --- a/webapp/src/components/post_type_transcription.tsx +++ b/webapp/src/components/post_type_transcription.tsx @@ -3,7 +3,6 @@ import {FormattedMessage} from 'react-intl'; import {useSelector} from 'react-redux'; import type {Post} from 'mattermost-redux/types/posts'; -import type {GlobalState} from 'mattermost-redux/types/store'; import styled from 'styled-components'; @@ -16,12 +15,8 @@ const useAIAvailable = () => { }; const useCallsPostButtonClicked = () => { - return useSelector((state: GlobalState) => { - type StateWithAiPluginState = { - 'plugins-mattermost-ai'?: {callsPostButtonClickedTranscription: (post: Post) => void}; - } - const stateTyped: StateWithAiPluginState = state as StateWithAiPluginState; - const aiPluginState = stateTyped['plugins-mattermost-ai']; + return useSelector((state: any) => { + const aiPluginState = state['plugins-'+aiPluginID]; return aiPluginState?.callsPostButtonClickedTranscription; }); }; From bf0599dc69db85be9bf5c7cbddfd96f339453042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 17 May 2024 09:32:57 +0200 Subject: [PATCH 17/52] Fixing linter --- webapp/src/components/post_type_chat.tsx | 2 +- webapp/src/components/post_type_transcription.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/post_type_chat.tsx b/webapp/src/components/post_type_chat.tsx index 54040227..086064ba 100644 --- a/webapp/src/components/post_type_chat.tsx +++ b/webapp/src/components/post_type_chat.tsx @@ -16,7 +16,7 @@ const useAIAvailable = () => { const useCallsPostButtonClicked = () => { return useSelector((state: any) => { - const aiPluginState = state['plugins-'+aiPluginID]; + const aiPluginState = state['plugins-' + aiPluginID]; return aiPluginState?.callsPostButtonClickedTranscription; }); }; diff --git a/webapp/src/components/post_type_transcription.tsx b/webapp/src/components/post_type_transcription.tsx index e2bc7630..39ebbb94 100644 --- a/webapp/src/components/post_type_transcription.tsx +++ b/webapp/src/components/post_type_transcription.tsx @@ -16,7 +16,7 @@ const useAIAvailable = () => { const useCallsPostButtonClicked = () => { return useSelector((state: any) => { - const aiPluginState = state['plugins-'+aiPluginID]; + const aiPluginState = state['plugins-' + aiPluginID]; return aiPluginState?.callsPostButtonClickedTranscription; }); }; From 777767074239ede978de063d8b4d580ce58d1ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 17 May 2024 10:12:22 +0200 Subject: [PATCH 18/52] Adding tests for transcript and chat handlers --- server/webhook_test.go | 169 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/server/webhook_test.go b/server/webhook_test.go index dbd92510..7303f858 100644 --- a/server/webhook_test.go +++ b/server/webhook_test.go @@ -7,8 +7,10 @@ import ( "encoding/hex" "encoding/json" "io" + "net/http" "net/http/httptest" "testing" + "time" "github.com/stretchr/testify/require" @@ -112,3 +114,170 @@ func TestWebhookVerifySignatureInvalid(t *testing.T) { body, _ := io.ReadAll(w.Result().Body) t.Log(string(body)) } + +func TestWebhookHandleTranscriptCompleted(t *testing.T) { + api := &plugintest.API{} + p := Plugin{} + p.setConfiguration(testConfig) + + httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte(r.URL.Path)) + })) + defer httpServer.Close() + + oldDefaultClient := http.DefaultClient + http.DefaultClient = httpServer.Client() + defer func() { + http.DefaultClient = oldDefaultClient + }() + + api.On("GetLicense").Return(nil) + api.On("GetPost", "post-id").Return(&model.Post{Id: "post-id", ChannelId: "channel-id"}, nil) + api.On("KVGet", "post_meeting_321").Return([]byte("post-id"), nil) + api.On("UploadFile", []byte("/test"), "channel-id", "transcription.txt").Return(&model.FileInfo{Id: "file-id"}, nil) + api.On("CreatePost", &model.Post{ + ChannelId: "channel-id", + RootId: "post-id", + Message: "Here's the zoom meeting transcription", + Type: "custom_zoom_transcript", + Props: model.StringInterface{ + "captions": []any{map[string]any{"file_id": "file-id"}}, + }, + FileIds: []string{"file-id"}, + }).Return(&model.Post{ + ChannelId: "channel-id", + RootId: "post-id", + Message: "Here's the zoom meeting transcription", + Type: "custom_zoom_transcript", + Props: model.StringInterface{ + "captions": []any{map[string]any{"file_id": "file-id"}}, + }, + FileIds: []string{"file-id"}, + }, nil) + p.SetAPI(api) + + requestBodyBytes, _ := json.Marshal(map[string]any{ + "payload": map[string]any{ + "object": map[string]any{ + "id": 123, + "uuid": "321", + "recording_files": []map[string]any{ + { + "recording_type": "audio_transcript", + "download_url": httpServer.URL + "/test", + }, + }, + }, + }, + "event": "recording.transcript_completed", + "download_token": "test-token", + }) + requestBody := string(requestBodyBytes) + + ts := "1660149894817" + h := hmac.New(sha256.New, []byte(testConfig.ZoomWebhookSecret)) + _, _ = h.Write([]byte("v0:" + ts + ":" + requestBody)) + signature := "v0=" + hex.EncodeToString(h.Sum(nil)) + + w := httptest.NewRecorder() + reqBody := io.NopCloser(bytes.NewBufferString(requestBody)) + request := httptest.NewRequest("POST", "/webhook?secret=webhooksecret", reqBody) + request.Header.Add("Content-Type", "application/json") + request.Header.Add("x-zm-signature", signature) + request.Header.Add("x-zm-request-timestamp", ts) + + p.ServeHTTP(&plugin.Context{}, w, request) + body, _ := io.ReadAll(w.Result().Body) + t.Log(string(body)) + + api.AssertExpectations(t) +} + +func TestWebhookHandleRecordingCompleted(t *testing.T) { + api := &plugintest.API{} + p := Plugin{} + p.setConfiguration(testConfig) + + httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte(r.URL.Path)) + })) + defer httpServer.Close() + + oldDefaultClient := http.DefaultClient + http.DefaultClient = httpServer.Client() + defer func() { + http.DefaultClient = oldDefaultClient + }() + + api.On("GetLicense").Return(nil) + api.On("GetPost", "post-id").Return(&model.Post{Id: "post-id", ChannelId: "channel-id"}, nil) + api.On("KVGet", "post_meeting_321").Return([]byte("post-id"), nil) + api.On("UploadFile", []byte("/chat_file"), "channel-id", "Chat-history.txt").Return(&model.FileInfo{Id: "file-id"}, nil) + api.On("CreatePost", &model.Post{ + ChannelId: "channel-id", + RootId: "post-id", + Message: "Here's the zoom meeting recording:\n**Link:** [Meeting Recording]()\n**Password:** test-password", + Type: "custom_zoom_chat", + Props: model.StringInterface{ + "captions": []any{map[string]any{"file_id": "file-id"}}, + }, + FileIds: []string{"file-id"}, + }).Return(&model.Post{ + ChannelId: "channel-id", + RootId: "post-id", + Message: "Here's the zoom meeting recording:\n**Link:** [Meeting Recording]()\n**Password:** test-password", + Type: "custom_zoom_chat", + Props: model.StringInterface{ + "captions": []any{map[string]any{"file_id": "file-id"}}, + }, + FileIds: []string{"file-id"}, + }, nil) + p.SetAPI(api) + + now := time.Now() + requestBodyBytes, _ := json.Marshal(map[string]any{ + "payload": map[string]any{ + "object": map[string]any{ + "id": 123, + "uuid": "321", + "password": "test-password", + "recording_files": []map[string]any{ + { + "recording_start": now, + "recording_type": "chat_file", + "download_url": httpServer.URL + "/chat_file", + }, + { + "recording_start": now, + "recording_type": "shared_screen_with_speaker_view", + "download_url": httpServer.URL + "/recording_file", + "playURL": httpServer.URL + "/recording_url", + }, + }, + }, + }, + "event": "recording.completed", + "download_token": "test-token", + }) + requestBody := string(requestBodyBytes) + + ts := "1660149894817" + h := hmac.New(sha256.New, []byte(testConfig.ZoomWebhookSecret)) + _, _ = h.Write([]byte("v0:" + ts + ":" + requestBody)) + signature := "v0=" + hex.EncodeToString(h.Sum(nil)) + + w := httptest.NewRecorder() + reqBody := io.NopCloser(bytes.NewBufferString(requestBody)) + request := httptest.NewRequest("POST", "/webhook?secret=webhooksecret", reqBody) + request.Header.Add("Content-Type", "application/json") + request.Header.Add("x-zm-signature", signature) + request.Header.Add("x-zm-request-timestamp", ts) + + p.ServeHTTP(&plugin.Context{}, w, request) + body, _ := io.ReadAll(w.Result().Body) + t.Log(string(body)) + + api.AssertExpectations(t) +} From b67ed8249fb32db185e4a4dee3789816a4db3266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 17 May 2024 10:15:39 +0200 Subject: [PATCH 19/52] Fixing some linter errors --- server/command.go | 2 +- server/webhook_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/command.go b/server/command.go index 1685d41b..669d5afa 100644 --- a/server/command.go +++ b/server/command.go @@ -267,7 +267,7 @@ func (p *Plugin) runUnsubscribeCommand(user *model.User, extra *model.CommandArg _, err := p.getMeeting(user, meetingID) if err != nil { - return "Can not unsubscribe from meeting: meeting not found", errors.Wrap(err, "meeting not found") + return "Can not unsubscribe from meeting: meeting not accesible in zoom", errors.Wrap(err, "meeting not accesible in zoom") } if channelID, appErr := p.fetchChannelForMeeting(meetingID); appErr != nil || channelID == "" { diff --git a/server/webhook_test.go b/server/webhook_test.go index 7303f858..1f763167 100644 --- a/server/webhook_test.go +++ b/server/webhook_test.go @@ -122,7 +122,7 @@ func TestWebhookHandleTranscriptCompleted(t *testing.T) { httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) - w.Write([]byte(r.URL.Path)) + _, _ = w.Write([]byte(r.URL.Path)) })) defer httpServer.Close() @@ -201,7 +201,7 @@ func TestWebhookHandleRecordingCompleted(t *testing.T) { httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) - w.Write([]byte(r.URL.Path)) + _, _ = w.Write([]byte(r.URL.Path)) })) defer httpServer.Close() From 3b4d73dc07381e188ef4c9062ee93458cb1ae4e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 23 May 2024 09:34:30 +0200 Subject: [PATCH 20/52] Addressing some PR review comments --- server/telemetry.go | 5 +++-- server/webhook.go | 2 +- webapp/src/components/post_type_transcription.tsx | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/server/telemetry.go b/server/telemetry.go index ca8013d5..40f0d5e5 100644 --- a/server/telemetry.go +++ b/server/telemetry.go @@ -4,8 +4,9 @@ const ( telemetryOauthModeOauth = "Oauth" telemetryOauthModeOauthAccountLevel = "Oauth Account Level" - telemetryStartSourceWebapp = "webapp" - telemetryStartSourceCommand = "command" + telemetryStartSourceWebapp = "webapp" + telemetryStartSourceCommand = "command" + telemetryStartSourceSubscribeWebhook = "subscribe-webhook" ) func (p *Plugin) TrackEvent(event string, properties map[string]interface{}) { diff --git a/server/webhook.go b/server/webhook.go index 19da6365..8c6623b6 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -108,7 +108,7 @@ func (p *Plugin) handleMeetingStarted(w http.ResponseWriter, r *http.Request, bo return } - p.trackMeetingStart(p.botUserID, telemetryStartSourceCommand) + p.trackMeetingStart(p.botUserID, telemetryStartSourceSubscribeWebhook) p.trackMeetingType(p.botUserID, false) } diff --git a/webapp/src/components/post_type_transcription.tsx b/webapp/src/components/post_type_transcription.tsx index 39ebbb94..0add0206 100644 --- a/webapp/src/components/post_type_transcription.tsx +++ b/webapp/src/components/post_type_transcription.tsx @@ -17,7 +17,7 @@ const useAIAvailable = () => { const useCallsPostButtonClicked = () => { return useSelector((state: any) => { const aiPluginState = state['plugins-' + aiPluginID]; - return aiPluginState?.callsPostButtonClickedTranscription; + return aiPluginState?.callsPostButtonClickedTranscription as ((post: Post) => void) | undefined; }); }; @@ -73,7 +73,7 @@ export const PostTypeTranscription = (props: Props) => { )} From 5efacfd8d30af6cc711921e16444268ab6af9c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 24 May 2024 15:16:16 +0200 Subject: [PATCH 21/52] Fixing types --- webapp/src/components/post_type_transcription.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/post_type_transcription.tsx b/webapp/src/components/post_type_transcription.tsx index 0add0206..91d7b93e 100644 --- a/webapp/src/components/post_type_transcription.tsx +++ b/webapp/src/components/post_type_transcription.tsx @@ -17,7 +17,7 @@ const useAIAvailable = () => { const useCallsPostButtonClicked = () => { return useSelector((state: any) => { const aiPluginState = state['plugins-' + aiPluginID]; - return aiPluginState?.callsPostButtonClickedTranscription as ((post: Post) => void) | undefined; + return aiPluginState?.callsPostButtonClickedTranscription; }); }; From dd108cf862f3baacb4de2e52f27f150aeeaa7c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 12 Jul 2024 18:33:26 +0200 Subject: [PATCH 22/52] Adding the meeting UUID --- server/command.go | 12 ++++-------- server/http.go | 35 ++++++++++++----------------------- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/server/command.go b/server/command.go index 729f8db7..ad145c9c 100644 --- a/server/command.go +++ b/server/command.go @@ -160,6 +160,7 @@ func (p *Plugin) runStartCommand(args *model.CommandArgs, user *model.User, topi } var meetingID int + var meetingUUID string var createMeetingErr error userPMISettingPref, err := p.getPMISettingData(user.Id) @@ -177,25 +178,20 @@ func (p *Plugin) runStartCommand(args *model.CommandArgs, user *model.User, topi meetingID = zoomUser.Pmi if meetingID <= 0 { - meetingID, createMeetingErr = p.createMeetingWithoutPMI(user, zoomUser, args.ChannelId, topic) + meetingID, meetingUUID, createMeetingErr = p.createMeetingWithoutPMI(user, zoomUser, args.ChannelId, topic) if createMeetingErr != nil { return "", errors.Wrap(createMeetingErr, "failed to create the meeting") } p.sendEnableZoomPMISettingMessage(user.Id, args.ChannelId, args.RootId) } default: - meetingID, createMeetingErr = p.createMeetingWithoutPMI(user, zoomUser, args.ChannelId, topic) + meetingID, meetingUUID, createMeetingErr = p.createMeetingWithoutPMI(user, zoomUser, args.ChannelId, topic) if createMeetingErr != nil { return "", errors.Wrap(createMeetingErr, "failed to create the meeting") } } - meeting, err := p.getMeeting(user, meetingID) - if err != nil { - return "", errors.Wrap(err, "failed to get the meeting") - } - - if postMeetingErr := p.postMeeting(user, meetingID, meeting.UUID, args.ChannelId, args.RootId, topic); postMeetingErr != nil { + if postMeetingErr := p.postMeeting(user, meetingID, meetingUUID, args.ChannelId, args.RootId, topic); postMeetingErr != nil { return "", postMeetingErr } diff --git a/server/http.go b/server/http.go index 91da05ff..fb295776 100644 --- a/server/http.go +++ b/server/http.go @@ -158,6 +158,7 @@ func (p *Plugin) startMeeting(action, userID, channelID, rootID string) { } var meetingID int + var meetingUUID string var createMeetingErr error createMeetingWithPMI := false if action == usePersonalMeetingID { @@ -165,7 +166,7 @@ func (p *Plugin) startMeeting(action, userID, channelID, rootID string) { meetingID = zoomUser.Pmi if meetingID <= 0 { - meetingID, createMeetingErr = p.createMeetingWithoutPMI(user, zoomUser, channelID, defaultMeetingTopic) + meetingID, meetingUUID, createMeetingErr = p.createMeetingWithoutPMI(user, zoomUser, channelID, defaultMeetingTopic) if createMeetingErr != nil { p.API.LogWarn("failed to create the meeting", "Error", createMeetingErr.Error()) return @@ -173,20 +174,14 @@ func (p *Plugin) startMeeting(action, userID, channelID, rootID string) { p.sendEnableZoomPMISettingMessage(userID, channelID, rootID) } } else { - meetingID, createMeetingErr = p.createMeetingWithoutPMI(user, zoomUser, channelID, defaultMeetingTopic) + meetingID, meetingUUID, createMeetingErr = p.createMeetingWithoutPMI(user, zoomUser, channelID, defaultMeetingTopic) if createMeetingErr != nil { p.API.LogWarn("failed to create the meeting", "Error", createMeetingErr.Error()) return } } - meeting, err := p.getMeeting(user, meetingID) - if err != nil { - p.API.LogWarn("failed to get the meeting", "Error", err.Error()) - return - } - - if postMeetingErr := p.postMeeting(user, meetingID, meeting.UUID, channelID, rootID, defaultMeetingTopic); postMeetingErr != nil { + if postMeetingErr := p.postMeeting(user, meetingID, meetingUUID, channelID, rootID, defaultMeetingTopic); postMeetingErr != nil { p.API.LogWarn("failed to post the meeting", "Error", postMeetingErr.Error()) return } @@ -594,20 +589,20 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) { } } -func (p *Plugin) createMeetingWithoutPMI(user *model.User, zoomUser *zoom.User, channelID, topic string) (int, error) { +func (p *Plugin) createMeetingWithoutPMI(user *model.User, zoomUser *zoom.User, channelID, topic string) (int, string, error) { client, _, err := p.getActiveClient(user) if err != nil { p.API.LogWarn("Error getting the client", "Error", err.Error()) - return -1, err + return -1, "", err } meeting, err := client.CreateMeeting(zoomUser, topic) if err != nil { p.API.LogWarn("Error creating the meeting", "Error", err.Error()) - return -1, err + return -1, "", err } - return meeting.ID, nil + return meeting.ID, meeting.UUID, nil } func (p *Plugin) getMeeting(user *model.User, meetingID int) (*zoom.Meeting, error) { @@ -888,6 +883,7 @@ func (p *Plugin) slackAttachmentToUpdatePMI(currentValue, channelID string) *mod func (p *Plugin) handleMeetingCreation(channelID, rootID, topic string, user *model.User, zoomUser *zoom.User) (string, error) { var meetingID int + var meetingUUID string var createMeetingErr error userPMISettingPref, err := p.getPMISettingData(user.Id) if err != nil { @@ -904,27 +900,20 @@ func (p *Plugin) handleMeetingCreation(channelID, rootID, topic string, user *mo meetingID = zoomUser.Pmi if meetingID <= 0 { - meetingID, createMeetingErr = p.createMeetingWithoutPMI(user, zoomUser, channelID, topic) + meetingID, meetingUUID, createMeetingErr = p.createMeetingWithoutPMI(user, zoomUser, channelID, topic) if createMeetingErr != nil { return "", createMeetingErr } p.sendEnableZoomPMISettingMessage(user.Id, channelID, rootID) } default: - meetingID, createMeetingErr = p.createMeetingWithoutPMI(user, zoomUser, channelID, topic) + meetingID, meetingUUID, createMeetingErr = p.createMeetingWithoutPMI(user, zoomUser, channelID, topic) if createMeetingErr != nil { return "", createMeetingErr } } - meeting, err := p.getMeeting(user, meetingID) - if err != nil { - p.API.LogWarn("failed to get the meeting", "Error", err.Error()) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if postMeetingErr := p.postMeeting(user, meetingID, meeting.UUID, channelID, rootID, topic); postMeetingErr != nil { + if postMeetingErr := p.postMeeting(user, meetingID, meetingUUID, channelID, rootID, topic); postMeetingErr != nil { return "", createMeetingErr } From e6b0a5059b8d2f4bbc4272a4ab6d3e8faf300076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 12 Jul 2024 18:52:14 +0200 Subject: [PATCH 23/52] Merging master and fixed problems related to the merge --- server/store.go | 6 +++--- server/webhook.go | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/server/store.go b/server/store.go index 77d1cc1f..c0301d9c 100644 --- a/server/store.go +++ b/server/store.go @@ -143,17 +143,17 @@ func (p *Plugin) storeMeetingPostID(meetingUUID string, postID string) *model.Ap func (p *Plugin) fetchMeetingPostID(meetingUUID string) (string, error) { key := fmt.Sprintf("%v%v", postMeetingKey, meetingUUID) - var postID string + var postID []byte if err := p.client.KV.Get(key, &postID); err != nil { p.client.Log.Debug("Could not get meeting post from KVStore", "error", err.Error()) return "", err } - if postID == "" { + if string(postID) == "" { return "", errors.New("stored meeting post ID not found") } - return postID, nil + return string(postID), nil } func (p *Plugin) storeChannelForMeeting(meetingID int, channelID string) error { diff --git a/server/webhook.go b/server/webhook.go index a760e214..6c0bd6ee 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -248,14 +248,14 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques } meetingPostID := webhook.Payload.Object.UUID - postID, appErr := p.fetchMeetingPostID(meetingPostID) - if appErr != nil { + postID, err := p.fetchMeetingPostID(meetingPostID) + if err != nil { return } - post, appErr := p.API.GetPost(postID) - if appErr != nil { - p.API.LogWarn("Could not get meeting post by id", "err", appErr) + post, err := p.client.Post.GetPost(postID) + if err != nil { + p.API.LogWarn("Could not get meeting post by id", "err", err) return } @@ -288,14 +288,14 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request } meetingPostID := webhook.Payload.Object.UUID - postID, appErr := p.fetchMeetingPostID(meetingPostID) - if appErr != nil { + postID, err := p.fetchMeetingPostID(meetingPostID) + if err != nil { return } - post, appErr := p.API.GetPost(postID) - if appErr != nil { - p.API.LogWarn("Could not get meeting post by id", "err", appErr) + post, err := p.client.Post.GetPost(postID) + if err != nil { + p.API.LogWarn("Could not get meeting post by id", "err", err) return } @@ -353,7 +353,7 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request } } if newPost.Message != "" { - _, appErr = p.API.CreatePost(newPost) + _, appErr := p.API.CreatePost(newPost) if appErr != nil { p.API.LogWarn("Could not update the post", "err", appErr) return From d75b8a573bf8244a21c242bf9b01e966264f05cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 12 Jul 2024 20:29:46 +0200 Subject: [PATCH 24/52] Fixing a crash on subscription --- server/store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/store.go b/server/store.go index c0301d9c..28f3831a 100644 --- a/server/store.go +++ b/server/store.go @@ -159,7 +159,7 @@ func (p *Plugin) fetchMeetingPostID(meetingUUID string) (string, error) { func (p *Plugin) storeChannelForMeeting(meetingID int, channelID string) error { key := fmt.Sprintf("%v%v", meetingChannelKey, meetingID) bytes := []byte(channelID) - _, err := p.client.KV.Set(key, bytes, nil) + _, err := p.client.KV.Set(key, bytes) return err } From 5c9a13a20a6bebaf57234a91187a9af67aa45198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 12 Jul 2024 20:53:05 +0200 Subject: [PATCH 25/52] Fixing the alteration on the length after receiving transcriptions/recordings --- server/webhook.go | 2 ++ webapp/src/components/post_type_zoom/post_type_zoom.jsx | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/server/webhook.go b/server/webhook.go index 6c0bd6ee..23f8de7c 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -134,6 +134,7 @@ func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body } start := time.Unix(0, post.CreateAt*int64(time.Millisecond)) + end := model.GetMillis() length := int(math.Ceil(float64((model.GetMillis()-post.CreateAt)/1000) / 60)) startText := start.Format("Mon Jan 2 15:04:05 -0700 MST 2006") topic, ok := post.Props["meeting_topic"].(string) @@ -159,6 +160,7 @@ func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body post.Message = "The meeting has ended." post.Props["meeting_status"] = zoom.WebhookStatusEnded + post.Props["meeting_end_time"] = end post.Props["attachments"] = []*model.SlackAttachment{&slackAttachment} if err = p.client.Post.UpdatePost(post); err != nil { diff --git a/webapp/src/components/post_type_zoom/post_type_zoom.jsx b/webapp/src/components/post_type_zoom/post_type_zoom.jsx index 4850f205..b7d5ad14 100644 --- a/webapp/src/components/post_type_zoom/post_type_zoom.jsx +++ b/webapp/src/components/post_type_zoom/post_type_zoom.jsx @@ -152,7 +152,8 @@ export default class PostTypeZoom extends React.PureComponent { const startDate = new Date(post.create_at); const start = formatDate(startDate); - const length = Math.ceil((new Date(post.update_at) - startDate) / 1000 / 60); + const end = props.meeting_end_time || post.update_at + const length = Math.ceil((new Date(end) - startDate) / 1000 / 60); content = (
From d3067af2d957382de91daabcf5681d8ff326cabc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Sat, 11 Jan 2025 07:35:55 +0100 Subject: [PATCH 26/52] Fixing linter error --- webapp/src/components/post_type_zoom/post_type_zoom.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/post_type_zoom/post_type_zoom.jsx b/webapp/src/components/post_type_zoom/post_type_zoom.jsx index b7d5ad14..679c3ce9 100644 --- a/webapp/src/components/post_type_zoom/post_type_zoom.jsx +++ b/webapp/src/components/post_type_zoom/post_type_zoom.jsx @@ -152,7 +152,7 @@ export default class PostTypeZoom extends React.PureComponent { const startDate = new Date(post.create_at); const start = formatDate(startDate); - const end = props.meeting_end_time || post.update_at + const end = props.meeting_end_time || post.update_at; const length = Math.ceil((new Date(end) - startDate) / 1000 / 60); content = ( From fc9d542647247a7495e29753fb926f0987ad77de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino=20=28aider=29?= Date: Sat, 11 Jan 2025 07:41:06 +0100 Subject: [PATCH 27/52] fix: Add missing mock expectation for KVGet in test case --- server/plugin_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/plugin_test.go b/server/plugin_test.go index 83a5e429..b8fb43c2 100644 --- a/server/plugin_test.go +++ b/server/plugin_test.go @@ -131,6 +131,7 @@ func TestPlugin(t *testing.T) { api.On("KVGet", "mmi_botid").Return([]byte(botUserID), nil) api.On("KVGet", "zoomtoken_theuserid").Return(userInfo, nil) + api.On("KVGet", "meeting_channel_234").Return([]byte("thechannelid"), nil) api.On("SendEphemeralPost", "theuserid", mock.AnythingOfType("*model.Post")).Return(nil) From 191ef217b97075773e047b7586399c7f4f1f65a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino=20=28aider=29?= Date: Sat, 11 Jan 2025 07:41:40 +0100 Subject: [PATCH 28/52] fix: Add missing mock expectation for GetUser in webhook test --- server/plugin_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/plugin_test.go b/server/plugin_test.go index b8fb43c2..b95422a2 100644 --- a/server/plugin_test.go +++ b/server/plugin_test.go @@ -132,6 +132,9 @@ func TestPlugin(t *testing.T) { api.On("KVGet", "mmi_botid").Return([]byte(botUserID), nil) api.On("KVGet", "zoomtoken_theuserid").Return(userInfo, nil) api.On("KVGet", "meeting_channel_234").Return([]byte("thechannelid"), nil) + api.On("GetUser", botUserID).Return(&model.User{ + Id: botUserID, + }, nil) api.On("SendEphemeralPost", "theuserid", mock.AnythingOfType("*model.Post")).Return(nil) From 0af0204f77912214a528105baa0455a00a8b7371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino=20=28aider=29?= Date: Sat, 11 Jan 2025 07:42:12 +0100 Subject: [PATCH 29/52] fix: Add missing mock expectation for bot user's Zoom token --- server/plugin_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/plugin_test.go b/server/plugin_test.go index b95422a2..007013c0 100644 --- a/server/plugin_test.go +++ b/server/plugin_test.go @@ -132,6 +132,7 @@ func TestPlugin(t *testing.T) { api.On("KVGet", "mmi_botid").Return([]byte(botUserID), nil) api.On("KVGet", "zoomtoken_theuserid").Return(userInfo, nil) api.On("KVGet", "meeting_channel_234").Return([]byte("thechannelid"), nil) + api.On("KVGet", "zoomtoken_"+botUserID).Return(userInfo, nil) api.On("GetUser", botUserID).Return(&model.User{ Id: botUserID, }, nil) From d3f70c413834da3b72bae58e4e4c69f999592420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino=20=28aider=29?= Date: Sat, 11 Jan 2025 07:42:44 +0100 Subject: [PATCH 30/52] fix: Add missing KVSetWithExpiry mock for meeting post ID test --- server/plugin_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/plugin_test.go b/server/plugin_test.go index 007013c0..7a2e82ec 100644 --- a/server/plugin_test.go +++ b/server/plugin_test.go @@ -136,6 +136,7 @@ func TestPlugin(t *testing.T) { api.On("GetUser", botUserID).Return(&model.User{ Id: botUserID, }, nil) + api.On("KVSetWithExpiry", "post_meeting_234", mock.AnythingOfType("[]uint8"), mock.AnythingOfType("int64")).Return(nil) api.On("SendEphemeralPost", "theuserid", mock.AnythingOfType("*model.Post")).Return(nil) From 70f1af5874f96d977019b0cdd6815973fffef339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino=20=28aider=29?= Date: Sat, 11 Jan 2025 07:43:15 +0100 Subject: [PATCH 31/52] fix: Add missing mock expectation for KVSetWithExpiry in test --- server/plugin_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/plugin_test.go b/server/plugin_test.go index 7a2e82ec..7035e2d8 100644 --- a/server/plugin_test.go +++ b/server/plugin_test.go @@ -137,6 +137,7 @@ func TestPlugin(t *testing.T) { Id: botUserID, }, nil) api.On("KVSetWithExpiry", "post_meeting_234", mock.AnythingOfType("[]uint8"), mock.AnythingOfType("int64")).Return(nil) + api.On("KVSetWithExpiry", "post_meeting_", mock.AnythingOfType("[]uint8"), mock.AnythingOfType("int64")).Return(nil) api.On("SendEphemeralPost", "theuserid", mock.AnythingOfType("*model.Post")).Return(nil) From 5fe64f00a48ad3db8e369df656ee514eb17b20ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino=20=28aider=29?= Date: Sat, 11 Jan 2025 07:43:46 +0100 Subject: [PATCH 32/52] test: Add mock expectation for PublishWebSocketEvent in test case --- server/plugin_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/plugin_test.go b/server/plugin_test.go index 7035e2d8..d03bb410 100644 --- a/server/plugin_test.go +++ b/server/plugin_test.go @@ -138,6 +138,7 @@ func TestPlugin(t *testing.T) { }, nil) api.On("KVSetWithExpiry", "post_meeting_234", mock.AnythingOfType("[]uint8"), mock.AnythingOfType("int64")).Return(nil) api.On("KVSetWithExpiry", "post_meeting_", mock.AnythingOfType("[]uint8"), mock.AnythingOfType("int64")).Return(nil) + api.On("PublishWebSocketEvent", "meeting_started", map[string]interface{}{"meeting_url": "https://zoom.us/j/234"}, &model.WebsocketBroadcast{UserId: botUserID}).Return() api.On("SendEphemeralPost", "theuserid", mock.AnythingOfType("*model.Post")).Return(nil) From 9dc2a3c11c3be25a678f6e516780849d49fdcb58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino=20=28aider=29?= Date: Sat, 11 Jan 2025 07:44:32 +0100 Subject: [PATCH 33/52] fix: Initialize plugin client in webhook test to resolve nil pointer dereference --- server/webhook_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/webhook_test.go b/server/webhook_test.go index 6e5e311a..4cf3ecec 100644 --- a/server/webhook_test.go +++ b/server/webhook_test.go @@ -138,6 +138,7 @@ func TestWebhookHandleTranscriptCompleted(t *testing.T) { api.On("GetPost", "post-id").Return(&model.Post{Id: "post-id", ChannelId: "channel-id"}, nil) api.On("KVGet", "post_meeting_321").Return([]byte("post-id"), nil) api.On("UploadFile", []byte("/test"), "channel-id", "transcription.txt").Return(&model.FileInfo{Id: "file-id"}, nil) + p.client = pluginapi.NewClient(api, nil) api.On("CreatePost", &model.Post{ ChannelId: "channel-id", RootId: "post-id", From c90d235bedd1e29c0f8d2796a2f9c6585f8a77b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino=20=28aider=29?= Date: Sat, 11 Jan 2025 07:45:08 +0100 Subject: [PATCH 34/52] fix: Initialize plugin client in TestWebhookHandleRecordingCompleted test --- server/webhook_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/webhook_test.go b/server/webhook_test.go index 4cf3ecec..134b8504 100644 --- a/server/webhook_test.go +++ b/server/webhook_test.go @@ -218,6 +218,7 @@ func TestWebhookHandleRecordingCompleted(t *testing.T) { api.On("GetPost", "post-id").Return(&model.Post{Id: "post-id", ChannelId: "channel-id"}, nil) api.On("KVGet", "post_meeting_321").Return([]byte("post-id"), nil) api.On("UploadFile", []byte("/chat_file"), "channel-id", "Chat-history.txt").Return(&model.FileInfo{Id: "file-id"}, nil) + p.client = pluginapi.NewClient(api, nil) api.On("CreatePost", &model.Post{ ChannelId: "channel-id", RootId: "post-id", From fb6ae2ff78e2004c23353a2b07c1ca92594731e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Sat, 11 Jan 2025 07:56:09 +0100 Subject: [PATCH 35/52] Fixing tests --- server/plugin_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/plugin_test.go b/server/plugin_test.go index d03bb410..b0ed872c 100644 --- a/server/plugin_test.go +++ b/server/plugin_test.go @@ -68,7 +68,7 @@ func TestPlugin(t *testing.T) { endedPayload := `{"event": "meeting.ended", "payload": {"object": {"id": "234", "uuid": "234"}}}` validStoppedWebhookRequest := httptest.NewRequest("POST", "/webhook?secret=thewebhooksecret", strings.NewReader(endedPayload)) - validStartedWebhookRequest := httptest.NewRequest("POST", "/webhook?secret=thewebhooksecret", strings.NewReader(`{"event": "meeting.started", "payload": {"object": {"id": "234"}}}`)) + validStartedWebhookRequest := httptest.NewRequest("POST", "/webhook?secret=thewebhooksecret", strings.NewReader(`{"event": "meeting.started", "payload": {"object": {"id": "234", "uuid": "234"}}}`)) noSecretWebhookRequest := httptest.NewRequest("POST", "/webhook", strings.NewReader(endedPayload)) @@ -136,8 +136,6 @@ func TestPlugin(t *testing.T) { api.On("GetUser", botUserID).Return(&model.User{ Id: botUserID, }, nil) - api.On("KVSetWithExpiry", "post_meeting_234", mock.AnythingOfType("[]uint8"), mock.AnythingOfType("int64")).Return(nil) - api.On("KVSetWithExpiry", "post_meeting_", mock.AnythingOfType("[]uint8"), mock.AnythingOfType("int64")).Return(nil) api.On("PublishWebSocketEvent", "meeting_started", map[string]interface{}{"meeting_url": "https://zoom.us/j/234"}, &model.WebsocketBroadcast{UserId: botUserID}).Return() api.On("SendEphemeralPost", "theuserid", mock.AnythingOfType("*model.Post")).Return(nil) From e9ea52a8a64a290be2b71f22b54c4551dd9613d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Sat, 11 Jan 2025 08:03:04 +0100 Subject: [PATCH 36/52] Fixing linter error --- server/webhook.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/webhook.go b/server/webhook.go index 23f8de7c..f942c352 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -20,6 +20,8 @@ import ( "github.com/mattermost/mattermost-plugin-zoom/server/zoom" ) +const bearerString = "Bearer " + func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) { if !p.verifyMattermostWebhookSecret(r) { p.API.LogWarn("Could not verify Mattermost webhook secret") @@ -181,7 +183,7 @@ func (p *Plugin) handleTranscript(recording zoom.RecordingFile, postID, channelI p.API.LogWarn("Unable to get the transcription", "err", err) return err } - request.Header.Set("Authorization", "Bearer "+downloadToken) + request.Header.Set("Authorization", bearerString+downloadToken) retries := 5 var response *http.Response @@ -328,7 +330,7 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request p.API.LogWarn("Unable to get the chat", "err", err) return } - request.Header.Set("Authorization", "Bearer "+webhook.DownloadToken) + request.Header.Set("Authorization", bearerString+webhook.DownloadToken) response, err := http.DefaultClient.Do(request) if err != nil { p.API.LogWarn("Unable to get the chat", "err", err) From a35591eb8eeabdaee86d5b8cf90b9dbce3c8b5e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino=20=28aider=29?= Date: Sat, 11 Jan 2025 08:08:27 +0100 Subject: [PATCH 37/52] test: Add tests for `handleMeetingStarted` webhook functionality --- server/webhook_test.go | 91 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/server/webhook_test.go b/server/webhook_test.go index 134b8504..05e9b409 100644 --- a/server/webhook_test.go +++ b/server/webhook_test.go @@ -12,12 +12,14 @@ import ( "testing" "time" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/plugin/plugintest" "github.com/mattermost/mattermost/server/public/pluginapi" + "github.com/mattermost/mattermost/server/public/pluginapi/experimental/telemetry" "github.com/mattermost/mattermost-plugin-zoom/server/zoom" ) @@ -59,6 +61,95 @@ func TestWebhookValidate(t *testing.T) { require.Equal(t, "2a41c3138d2187a756c51428f78d192e9b88dcf44dd62d1b081ace4ec2241e0a", out.EncryptedToken) } +func TestHandleMeetingStarted(t *testing.T) { + p := Plugin{} + p.setConfiguration(testConfig) + + t.Run("successful meeting start", func(t *testing.T) { + api := &plugintest.API{} + api.On("GetLicense").Return(nil) + api.On("KVGet", "meeting_channel_123").Return([]byte("channel-id"), nil) + api.On("GetUser", "").Return(&model.User{Id: "user-id"}, nil) + api.On("KVGet", "zoomtoken_user-id").Return(nil, &model.AppError{}) + api.On("LogWarn", "could not get the active Zoom client", "error", "could not fetch Zoom OAuth info: must connect user account to Zoom first").Return() + api.On("HasPermissionToChannel", "user-id", "channel-id", mock.AnythingOfType("*model.Permission")).Return(true) + api.On("KVSetWithExpiry", "post_meeting_abc", []byte{}, int64(86400)).Return(nil) + api.On("PublishWebSocketEvent", "meeting_started", map[string]interface{}{"meeting_url": "https://zoom.us/j/123"}, mock.AnythingOfType("*model.WebsocketBroadcast")).Return() + api.On("CreatePost", mock.AnythingOfType("*model.Post")).Return(&model.Post{}, nil) + p.SetAPI(api) + p.client = pluginapi.NewClient(api, nil) + p.botUserID = "" + p.tracker = telemetry.NewTracker(nil, "", "", "", "", "", telemetry.NewTrackerConfig(nil), nil) + + requestBody := `{"payload":{"object": {"id": "123", "uuid": "abc", "topic": "test meeting"}},"event":"meeting.started"}` + w := httptest.NewRecorder() + reqBody := io.NopCloser(bytes.NewBufferString(requestBody)) + request := httptest.NewRequest("POST", "/webhook?secret=webhooksecret", reqBody) + request.Header.Add("Content-Type", "application/json") + + ts := "1660149894817" + h := hmac.New(sha256.New, []byte(testConfig.ZoomWebhookSecret)) + _, _ = h.Write([]byte("v0:" + ts + ":" + requestBody)) + signature := "v0=" + hex.EncodeToString(h.Sum(nil)) + + request.Header.Add("x-zm-signature", signature) + request.Header.Add("x-zm-request-timestamp", ts) + + p.ServeHTTP(&plugin.Context{}, w, request) + require.Equal(t, http.StatusOK, w.Result().StatusCode) + }) + + t.Run("invalid meeting ID", func(t *testing.T) { + api := &plugintest.API{} + api.On("GetLicense").Return(nil) + api.On("LogError", "Failed to get meeting ID", "err", "strconv.Atoi: parsing \"invalid\": invalid syntax").Return() + p.SetAPI(api) + + requestBody := `{"payload":{"object": {"id": "invalid", "uuid": "123-abc"}},"event":"meeting.started"}` + w := httptest.NewRecorder() + reqBody := io.NopCloser(bytes.NewBufferString(requestBody)) + request := httptest.NewRequest("POST", "/webhook?secret=webhooksecret", reqBody) + request.Header.Add("Content-Type", "application/json") + + ts := "1660149894817" + h := hmac.New(sha256.New, []byte(testConfig.ZoomWebhookSecret)) + _, _ = h.Write([]byte("v0:" + ts + ":" + requestBody)) + signature := "v0=" + hex.EncodeToString(h.Sum(nil)) + + request.Header.Add("x-zm-signature", signature) + request.Header.Add("x-zm-request-timestamp", ts) + + p.ServeHTTP(&plugin.Context{}, w, request) + require.Equal(t, http.StatusBadRequest, w.Result().StatusCode) + }) + + t.Run("channel not found", func(t *testing.T) { + api := &plugintest.API{} + api.On("GetLicense").Return(nil) + api.On("KVGet", "meeting_channel_123").Return(nil, &model.AppError{}) + api.On("LogDebug", "Could not get channel meeting from KVStore", "error", "").Return() + api.On("KVSetWithExpiry", "post_meeting_123-abc", []byte{}, int64(86400)).Return(nil) + p.SetAPI(api) + + requestBody := `{"payload":{"object": {"id": "123", "uuid": "123-abc"}},"event":"meeting.started"}` + w := httptest.NewRecorder() + reqBody := io.NopCloser(bytes.NewBufferString(requestBody)) + request := httptest.NewRequest("POST", "/webhook?secret=webhooksecret", reqBody) + request.Header.Add("Content-Type", "application/json") + + ts := "1660149894817" + h := hmac.New(sha256.New, []byte(testConfig.ZoomWebhookSecret)) + _, _ = h.Write([]byte("v0:" + ts + ":" + requestBody)) + signature := "v0=" + hex.EncodeToString(h.Sum(nil)) + + request.Header.Add("x-zm-signature", signature) + request.Header.Add("x-zm-request-timestamp", ts) + + p.ServeHTTP(&plugin.Context{}, w, request) + require.Equal(t, http.StatusOK, w.Result().StatusCode) + }) +} + func TestWebhookVerifySignature(t *testing.T) { api := &plugintest.API{} p := Plugin{} From 80435038bf755fcb293c5c3660981175da4ba6b0 Mon Sep 17 00:00:00 2001 From: avasconcelos114 Date: Wed, 25 Feb 2026 15:39:11 +0200 Subject: [PATCH 38/52] WIP: Unified subscriptions under the same nested command - Added fixes for summaries not working in the case of meetings started with /zoom start - Added /zoom subscriptions list command to get list of subscriptions created (To be changed to fetch per user) - Reduced duplicated code in FE --- server/command.go | 128 +++++++++--- server/http.go | 28 ++- server/plugin_test.go | 15 +- server/store.go | 41 +++- server/webhook.go | 193 ++++++++++++++---- server/webhook_test.go | 37 ++-- webapp/.eslintrc.json | 29 ++- webapp/src/components/ai_icon.tsx | 3 + webapp/src/components/ai_summary_button.tsx | 86 ++++++++ webapp/src/components/post_type_chat.tsx | 97 ++------- .../components/post_type_transcription.tsx | 79 +------ webapp/src/components/svg.tsx | 2 +- webapp/webpack.config.js | 1 + 13 files changed, 497 insertions(+), 242 deletions(-) create mode 100644 webapp/src/components/ai_summary_button.tsx diff --git a/server/command.go b/server/command.go index 518d0df6..75dc1bcd 100644 --- a/server/command.go +++ b/server/command.go @@ -25,10 +25,13 @@ const ( settingHelpText = `* |/zoom settings| - Update your preferences` channelPreferenceHelpText = `* |/zoom channel-settings| - Update your current channel preference` listChannelPreferenceHelpText = `* |/zoom channel-settings list| - List all channel preferences` - alreadyConnectedText = "Already connected" - zoomPreferenceCategory = "plugin:zoom" - zoomPMISettingName = "use-pmi" - zoomPMISettingValueAsk = "ask" + subscriptionHelpText = `* |/zoom subscription add [meetingID]| - Subscribe this channel to a Zoom meeting +* |/zoom subscription remove [meetingID]| - Unsubscribe this channel from a Zoom meeting +* |/zoom subscription list| - List all meeting subscriptions` + alreadyConnectedText = "Already connected" + zoomPreferenceCategory = "plugin:zoom" + zoomPMISettingName = "use-pmi" + zoomPMISettingValueAsk = "ask" ) const ( @@ -36,8 +39,10 @@ const ( actionStart = "start" actionDisconnect = "disconnect" actionHelp = "help" - actionSubscribe = "subscribe" - actionUnsubscribe = "unsubscribe" + actionSubscription = "subscription" + subscriptionActionAdd = "add" + subscriptionActionRemove = "remove" + subscriptionActionList = "list" settings = "settings" actionChannelSettings = "channel-settings" channelSettingsActionList = "list" @@ -55,9 +60,9 @@ func (p *Plugin) getCommand() (*model.Command, error) { canConnect := !p.configuration.AccountLevelApp - autoCompleteDesc := "Available commands: start, help, settings, channel-settings" + autoCompleteDesc := "Available commands: start, help, subscription, settings, channel-settings" if canConnect { - autoCompleteDesc = "Available commands: start, connect, disconnect, help, settings, channel-settings" + autoCompleteDesc = "Available commands: start, connect, disconnect, help, subscription, settings, channel-settings" } return &model.Command{ @@ -79,7 +84,7 @@ func (p *Plugin) postCommandResponse(args *model.CommandArgs, text string) { _ = p.API.SendEphemeralPost(args.UserId, post) } -func (p *Plugin) parseCommand(rawCommand string) (cmd, action, topic string, meetingID int) { +func (p *Plugin) parseCommand(rawCommand string) (cmd, action, topic string) { split := strings.Fields(rawCommand) cmd = split[0] if len(split) > 1 { @@ -88,14 +93,11 @@ func (p *Plugin) parseCommand(rawCommand string) (cmd, action, topic string, mee if action == actionStart { topic = strings.Join(split[2:], " ") } - if len(split) > 2 && (action == actionSubscribe || action == actionUnsubscribe) { - meetingID, _ = strconv.Atoi(split[2]) - } - return cmd, action, topic, meetingID + return cmd, action, topic } func (p *Plugin) executeCommand(c *plugin.Context, args *model.CommandArgs) (string, error) { - command, action, topic, meetingID := p.parseCommand(args.Command) + command, action, topic := p.parseCommand(args.Command) if command != "/zoom" { return fmt.Sprintf("Command '%s' is not /zoom. Please try again.", command), nil @@ -114,10 +116,8 @@ func (p *Plugin) executeCommand(c *plugin.Context, args *model.CommandArgs) (str switch action { case actionConnect: return p.runConnectCommand(user, args) - case actionSubscribe: - return p.runSubscribeCommand(user, args, meetingID) - case actionUnsubscribe: - return p.runUnsubscribeCommand(user, args, meetingID) + case actionSubscription: + return p.runSubscriptionCommand(args, strings.Fields(args.Command)[2:], user) case actionStart: return p.runStartCommand(args, user, topic) case actionDisconnect: @@ -260,6 +260,69 @@ func (p *Plugin) runConnectCommand(user *model.User, extra *model.CommandArgs) ( return oauthMsg, nil } +func (p *Plugin) runSubscriptionCommand(args *model.CommandArgs, params []string, user *model.User) (string, error) { + if len(params) == 0 { + return "Please specify a subscription action: `add`, `remove`, or `list`.\nUsage: `/zoom subscription [action] [meetingID]`", nil + } + + switch params[0] { + case subscriptionActionAdd: + if len(params) < 2 { + return "Please specify a meeting ID. Usage: `/zoom subscription add [meetingID]`", nil + } + meetingID, err := strconv.Atoi(params[1]) + if err != nil { + return "Invalid meeting ID. Please provide a numeric meeting ID.", nil + } + return p.runSubscribeCommand(user, args, meetingID) + case subscriptionActionRemove: + if len(params) < 2 { + return "Please specify a meeting ID. Usage: `/zoom subscription remove [meetingID]`", nil + } + meetingID, err := strconv.Atoi(params[1]) + if err != nil { + return "Invalid meeting ID. Please provide a numeric meeting ID.", nil + } + return p.runUnsubscribeCommand(user, args, meetingID) + case subscriptionActionList: + return p.runSubscriptionListCommand(args) + default: + return fmt.Sprintf("Unknown subscription action: `%s`. Available actions: `add`, `remove`, `list`.", params[0]), nil + } +} + +func (p *Plugin) runSubscriptionListCommand(args *model.CommandArgs) (string, error) { + if !p.API.HasPermissionToChannel(args.UserId, args.ChannelId, model.PermissionCreatePost) { + return "You do not have permission to view subscriptions in this channel.", nil + } + + subs, err := p.listAllMeetingSubscriptions() + if err != nil { + p.client.Log.Error("Unable to list meeting subscriptions", "Error", err.Error()) + return "Unable to list meeting subscriptions.", nil + } + + if len(subs) == 0 { + return "No meeting subscriptions found.", nil + } + + var sb strings.Builder + sb.WriteString("#### Meeting Subscriptions\n\n") + sb.WriteString("| Meeting ID | Channel |\n") + sb.WriteString("| :--- | :--- |\n") + + for meetingID, channelID := range subs { + channel, appErr := p.client.Channel.Get(channelID) + if appErr != nil { + p.client.Log.Error("Unable to get channel for subscription list", "ChannelID", channelID, "Error", appErr.Error()) + continue + } + sb.WriteString(fmt.Sprintf("| %s | ~%s |\n", meetingID, channel.Name)) + } + + return sb.String(), nil +} + func (p *Plugin) runSubscribeCommand(user *model.User, extra *model.CommandArgs, meetingID int) (string, error) { if !p.API.HasPermissionToChannel(user.Id, extra.ChannelId, model.PermissionCreatePost) { return "You do not have permission to subscribe to this channel", nil @@ -267,17 +330,18 @@ func (p *Plugin) runSubscribeCommand(user *model.User, extra *model.CommandArgs, meeting, err := p.getMeeting(user, meetingID) if err != nil { - return "Can not subscribe to meeting: meeting not found", errors.Wrap(err, "meeting not found") + return "Cannot subscribe to meeting: meeting not found", errors.Wrap(err, "meeting not found") } if meeting.Type == zoom.MeetingTypePersonal { - return "Can not subscribe to personal meeting", nil + return "Cannot subscribe to personal meeting", nil } if appErr := p.storeChannelForMeeting(meetingID, extra.ChannelId); appErr != nil { return "", errors.Wrap(appErr, "cannot subscribe to meeting") } - return "Channel subscribed to meeting", nil + + return "Channel subscribed to meeting.", nil } func (p *Plugin) runUnsubscribeCommand(user *model.User, extra *model.CommandArgs, meetingID int) (string, error) { @@ -296,7 +360,8 @@ func (p *Plugin) runUnsubscribeCommand(user *model.User, extra *model.CommandArg if appErr := p.deleteChannelForMeeting(meetingID); appErr != nil { return "Can not unsubscribe from meeting: unable to delete the meeting subscription", errors.Wrap(appErr, "cannot unsubscribe from meeting") } - return "Channel unsubscribed from meeting", nil + + return "Channel unsubscribed from meeting.", nil } // runDisconnectCommand runs command to disconnect from Zoom. Will fail if user cannot connect. @@ -325,7 +390,7 @@ func (p *Plugin) runDisconnectCommand(user *model.User) (string, error) { // runHelpCommand runs command to display help text. func (p *Plugin) runHelpCommand(user *model.User) (string, error) { - text := starterText + strings.ReplaceAll(helpText+"\n"+settingHelpText, "|", "`") + text := starterText + strings.ReplaceAll(helpText+"\n"+settingHelpText+"\n"+subscriptionHelpText, "|", "`") if p.API.HasPermissionTo(user.Id, model.PermissionManageSystem) { text += "\n" + strings.ReplaceAll(channelPreferenceHelpText+"\n"+listChannelPreferenceHelpText, "|", "`") } @@ -479,9 +544,9 @@ func (p *Plugin) runChannelSettingsListCommand(args *model.CommandArgs) (string, func (p *Plugin) getAutocompleteData() *model.AutocompleteData { canConnect := !p.configuration.AccountLevelApp - available := "start, help, subscribe, unsubscribe, settings, channel-settings" + available := "start, help, subscription, settings, channel-settings" if canConnect { - available = "start, connect, disconnect, help, subscribe, unsubscribe, settings, channel-settings" + available = "start, connect, disconnect, help, subscription, settings, channel-settings" } zoom := model.NewAutocompleteData("zoom", "[command]", fmt.Sprintf("Available commands: %s", available)) @@ -500,11 +565,14 @@ func (p *Plugin) getAutocompleteData() *model.AutocompleteData { setting := model.NewAutocompleteData("settings", "", "Update your meeting ID preferences") zoom.AddCommand(setting) - subscribe := model.NewAutocompleteData("subscribe", "[meeting id]", "Subscribe this channel to a Zoom meeting") - zoom.AddCommand(subscribe) - - unsubscribe := model.NewAutocompleteData("unsubscribe", "[meeting id]", "Unsubscribe this channel from a Zoom meeting") - zoom.AddCommand(unsubscribe) + subscription := model.NewAutocompleteData("subscription", "[action]", "Manage meeting subscriptions") + subAdd := model.NewAutocompleteData("add", "[meeting id]", "Subscribe this channel to a Zoom meeting") + subRemove := model.NewAutocompleteData("remove", "[meeting id]", "Unsubscribe this channel from a Zoom meeting") + subList := model.NewAutocompleteData("list", "", "List all meeting subscriptions and their channels") + subscription.AddCommand(subAdd) + subscription.AddCommand(subRemove) + subscription.AddCommand(subList) + zoom.AddCommand(subscription) // channel-settings to update channel preferences channelSettings := model.NewAutocompleteData("channel-settings", "", "Update current channel preference") diff --git a/server/http.go b/server/http.go index dcd29f27..7d2e3e50 100644 --- a/server/http.go +++ b/server/http.go @@ -355,18 +355,25 @@ func (p *Plugin) completeUserOAuthToZoom(w http.ResponseWriter, r *http.Request) storedState, appErr := p.fetchOAuthUserState(authedUserID) if appErr != nil { + p.API.LogWarn("OAuth completion failed: could not fetch stored state", "authed_user_id", authedUserID, "error", appErr.Error()) http.Error(w, "missing stored state", http.StatusNotFound) return } + p.API.LogDebug("OAuth state retrieved", "authed_user_id", authedUserID, "state_length", len(storedState)) + userID, channelID, justConnect, err := parseOAuthUserState(storedState) if err != nil { + p.API.LogWarn("OAuth completion failed: could not parse state", "authed_user_id", authedUserID, "error", err.Error()) http.Error(w, err.Error(), http.StatusBadRequest) return } + p.API.LogDebug("OAuth state parsed", "user_id", userID, "channel_id", channelID, "just_connect", justConnect) + state := r.URL.Query().Get("state") if storedState != state { + p.API.LogWarn("OAuth completion failed: state mismatch", "authed_user_id", authedUserID, "stored_state_length", len(storedState), "query_state_length", len(state)) http.Error(w, "OAuth user state mismatch", http.StatusUnauthorized) return } @@ -493,6 +500,9 @@ func (p *Plugin) postMeeting(creator *model.User, meetingID int, meetingUUID str }, } + existingChannelID, _ := p.fetchChannelForMeeting(meetingID) + post.Props["meeting_subscribed"] = existingChannelID != "" + createdPost, appErr := p.API.CreatePost(post) if appErr != nil { return appErr @@ -502,6 +512,10 @@ func (p *Plugin) postMeeting(creator *model.User, meetingID int, meetingUUID str p.API.LogDebug("failed to store post id", "error", appErr) } + if err := p.storeChannelForMeeting(meetingID, channelID); err != nil { + p.API.LogDebug("failed to store channel for meeting", "meeting_id", meetingID, "error", err) + } + p.client.Frontend.PublishWebSocketEvent( WebsocketEventMeetingStarted, map[string]interface{}{ @@ -941,13 +955,21 @@ func (p *Plugin) completeCompliance(payload zoom.DeauthorizationPayload) error { } // parseOAuthUserState parses the user ID and the channel ID from the given OAuth user state. +// Expected format: "{nonce}_{userID}_{channelID}_{true|false}" func parseOAuthUserState(state string) (userID, channelID string, justConnect bool, err error) { stateComponents := strings.Split(state, "_") if len(stateComponents) != zoomOAuthUserStateLength { - return "", "", false, errors.New("invalid OAuth user state") + return "", "", false, errors.Errorf( + "invalid OAuth user state: expected %d components, got %d (state length=%d)", + zoomOAuthUserStateLength, len(stateComponents), len(state), + ) } - return stateComponents[1], stateComponents[2], stateComponents[3] == trueString, nil + userID = stateComponents[1] + channelID = stateComponents[2] + justConnect = stateComponents[3] == trueString + + return userID, channelID, justConnect, nil } func (p *Plugin) sendUserSettingForm(userID, channelID, rootID string) error { @@ -1110,7 +1132,7 @@ func (p *Plugin) handleMeetingCreation(channelID, rootID, topic string, user *mo } if postMeetingErr := p.postMeeting(user, meetingID, meetingUUID, channelID, rootID, topic); postMeetingErr != nil { - return "", createMeetingErr + return "", postMeetingErr } p.trackMeetingStart(user.Id, telemetryStartSourceCommand) diff --git a/server/plugin_test.go b/server/plugin_test.go index ae90efc1..b181e9ff 100644 --- a/server/plugin_test.go +++ b/server/plugin_test.go @@ -169,6 +169,7 @@ func TestPlugin(t *testing.T) { api.On("KVSetWithOptions", "mutex_mmi_bot_ensure", mock.AnythingOfType("[]uint8"), model.PluginKVSetOptions{Atomic: true, OldValue: []uint8(nil), ExpireInSeconds: 15}).Return(true, nil) api.On("KVSetWithOptions", "mutex_mmi_bot_ensure", []byte(nil), model.PluginKVSetOptions{ExpireInSeconds: 0}).Return(true, nil) api.On("KVSetWithOptions", "post_meeting_234", []byte(nil), model.PluginKVSetOptions{ExpireInSeconds: 0}).Return(true, nil) + api.On("KVSetWithOptions", mock.MatchedBy(func(key string) bool { return strings.HasPrefix(key, meetingChannelKey) }), mock.AnythingOfType("[]uint8"), model.PluginKVSetOptions{}).Return(true, nil) api.On("EnsureBotUser", &model.Bot{ Username: botUserName, @@ -182,8 +183,18 @@ func TestPlugin(t *testing.T) { api.On("KVDelete", fmt.Sprintf("%v%v", postMeetingKey, 234)).Return(nil) - api.On("LogWarn", mock.AnythingOfType("string")).Return() - api.On("LogDebug", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return() + api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogWarn", mock.Anything).Maybe().Return() + api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogDebug", mock.Anything).Maybe().Return() path, err := filepath.Abs("..") require.Nil(t, err) diff --git a/server/store.go b/server/store.go index 46906391..d0568c45 100644 --- a/server/store.go +++ b/server/store.go @@ -6,6 +6,7 @@ package main import ( "encoding/json" "fmt" + "strings" "github.com/mattermost/mattermost/server/public/model" "github.com/pkg/errors" @@ -150,17 +151,17 @@ func (p *Plugin) storeMeetingPostID(meetingUUID string, postID string) *model.Ap func (p *Plugin) fetchMeetingPostID(meetingUUID string) (string, error) { key := fmt.Sprintf("%v%v", postMeetingKey, meetingUUID) - var postID []byte - if err := p.client.KV.Get(key, &postID); err != nil { + var postIDData []byte + if err := p.client.KV.Get(key, &postIDData); err != nil { p.client.Log.Debug("Could not get meeting post from KVStore", "error", err.Error()) return "", err } - if string(postID) == "" { + if postIDData == nil { return "", errors.New("stored meeting post ID not found") } - return string(postID), nil + return string(postIDData), nil } func (p *Plugin) storeChannelForMeeting(meetingID int, channelID string) error { @@ -191,6 +192,38 @@ func (p *Plugin) deleteChannelForMeeting(meetingID int) error { return p.client.KV.Delete(key) } +const kvListPerPage = 100 + +func (p *Plugin) listAllMeetingSubscriptions() (map[string]string, error) { + subscriptions := make(map[string]string) + + for page := 0; ; page++ { + keys, appErr := p.API.KVList(page, kvListPerPage) + if appErr != nil { + return nil, errors.New(appErr.Message) + } + + for _, key := range keys { + if !strings.HasPrefix(key, meetingChannelKey) { + continue + } + + meetingID := strings.TrimPrefix(key, meetingChannelKey) + channelIDBytes, kvErr := p.API.KVGet(key) + if kvErr != nil || channelIDBytes == nil { + continue + } + subscriptions[meetingID] = string(channelIDBytes) + } + + if len(keys) < kvListPerPage { + break + } + } + + return subscriptions, nil +} + // getOAuthUserStateKey generates and returns the key for storing the OAuth user state in the KV store. func getOAuthUserStateKey(userID string) string { return fmt.Sprintf("%s_%s", zoomStateKeyPrefix, userID) diff --git a/server/webhook.go b/server/webhook.go index 4b42307a..a87584bc 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -114,7 +114,7 @@ func (p *Plugin) handleMeetingStarted(w http.ResponseWriter, r *http.Request, bo return } - if postMeetingErr := p.postMeeting(botUser, meetingID, webhook.Payload.Object.UUID, channelID, "", webhook.Payload.Object.ID); postMeetingErr != nil { + if postMeetingErr := p.postMeeting(botUser, meetingID, webhook.Payload.Object.UUID, channelID, "", webhook.Payload.Object.Topic); postMeetingErr != nil { p.API.LogError("Failed to post the zoom message in the channel", "err", postMeetingErr.Error()) return } @@ -131,22 +131,69 @@ func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body return } - meetingPostID := webhook.Payload.Object.UUID - postID, err := p.fetchMeetingPostID(meetingPostID) + webhookMeetingID := webhook.Payload.Object.ID + webhookUUID := webhook.Payload.Object.UUID + p.API.LogDebug("Handling meeting.ended webhook", + "webhook_meeting_id", webhookMeetingID, + "webhook_uuid", webhookUUID, + ) + + postID, err := p.fetchMeetingPostID(webhookUUID) if err != nil { - return + p.API.LogWarn("Could not find meeting post by UUID, attempting lookup by stored UUID on post", + "webhook_uuid", webhookUUID, + "error", err.Error(), + ) + + // The UUID Zoom sends at meeting.ended can differ from the one at creation + // (e.g. PMI meetings, or recurring meetings get a new UUID per occurrence). + // Fall back to finding the post via the meeting_channel mapping and recent posts. + meetingIDInt, atoiErr := strconv.Atoi(webhookMeetingID) + if atoiErr != nil { + p.API.LogWarn("Could not parse meeting ID from webhook for fallback lookup", + "webhook_meeting_id", webhookMeetingID, + "error", atoiErr.Error(), + ) + http.Error(w, "meeting post not found", http.StatusNotFound) + return + } + + postID, err = p.findMeetingPostByMeetingID(meetingIDInt) + if err != nil { + p.API.LogWarn("Fallback meeting post lookup also failed", + "meeting_id", meetingIDInt, + "error", err.Error(), + ) + http.Error(w, "meeting post not found", http.StatusNotFound) + return + } + p.API.LogDebug("Found meeting post via fallback lookup", "post_id", postID, "meeting_id", meetingIDInt) } post, err := p.client.Post.GetPost(postID) if err != nil { - p.client.Log.Warn("Could not get meeting post by id", "err", err.Error()) + p.client.Log.Warn("Could not get meeting post by id", "post_id", postID, "err", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } + p.API.LogDebug("Found meeting post", + "post_id", post.Id, + "channel_id", post.ChannelId, + "meeting_status", post.Props["meeting_status"], + "meeting_id_prop", post.Props["meeting_id"], + "meeting_uuid_prop", post.Props["meeting_uuid"], + ) + + if post.Props["meeting_status"] == zoom.WebhookStatusEnded { + p.API.LogDebug("Meeting post already marked as ended, skipping update", "post_id", post.Id) + w.WriteHeader(http.StatusOK) + return + } + start := time.Unix(0, post.CreateAt*int64(time.Millisecond)) end := model.GetMillis() - length := int(math.Ceil(float64((model.GetMillis()-post.CreateAt)/1000) / 60)) + length := int(math.Ceil(float64((end-post.CreateAt)/1000) / 60)) startText := start.Format("Mon Jan 2 15:04:05 -0700 MST 2006") topic, ok := post.Props["meeting_topic"].(string) if !ok { @@ -180,12 +227,72 @@ func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body return } + p.API.LogDebug("Successfully updated meeting post to ended", "post_id", post.Id) + + subscribed, _ := post.Props["meeting_subscribed"].(bool) + if !subscribed && int(meetingID) != 0 { + if err := p.deleteChannelForMeeting(int(meetingID)); err != nil { + p.client.Log.Debug("Could not clean up channel-meeting mapping", "meeting_id", int(meetingID), "error", err.Error()) + } + } + w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(post); err != nil { p.API.LogWarn("failed to write response", "error", err.Error()) } } +func (p *Plugin) findMeetingPostByMeetingID(meetingID int) (string, error) { + channelID, appErr := p.fetchChannelForMeeting(meetingID) + if appErr != nil || channelID == "" { + return "", errors.Errorf("no channel found for meeting %d", meetingID) + } + + since := model.GetMillis() - meetingPostIDTTL*1000 + postList, appErr := p.API.GetPostsSince(channelID, since) + if appErr != nil { + return "", errors.Wrap(appErr, "could not get recent posts for channel") + } + + for _, post := range postList.Posts { + if post.Type != "custom_zoom" { + continue + } + propID, ok := post.Props["meeting_id"].(float64) + if ok && int(propID) == meetingID && post.Props["meeting_status"] != zoom.WebhookStatusEnded { + return post.Id, nil + } + } + + return "", errors.Errorf("no active meeting post found for meeting %d in channel %s", meetingID, channelID) +} + +// resolveRecordingMeetingPost finds the meeting post by UUID first, falling +// back to a meeting-ID-based search when the UUID doesn't match (PMI / +// recurring meetings get a new UUID per occurrence). +func (p *Plugin) resolveRecordingMeetingPost(webhookUUID string, meetingID int) (*model.Post, error) { + postID, err := p.fetchMeetingPostID(webhookUUID) + if err != nil { + p.API.LogDebug("UUID-based post lookup failed, trying meeting ID fallback", + "webhook_uuid", webhookUUID, + "meeting_id", meetingID, + "error", err.Error(), + ) + + postID, err = p.findMeetingPostByMeetingID(meetingID) + if err != nil { + return nil, errors.Wrapf(err, "could not find meeting post for uuid=%s meeting_id=%d", webhookUUID, meetingID) + } + } + + post, getErr := p.client.Post.GetPost(postID) + if getErr != nil { + return nil, errors.Wrap(getErr, "could not get meeting post by id") + } + + return post, nil +} + func (p *Plugin) handleTranscript(recording zoom.RecordingFile, postID, channelID, downloadToken string) error { request, err := http.NewRequest(http.MethodGet, recording.DownloadURL, nil) if err != nil { @@ -260,15 +367,10 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques return } - meetingPostID := webhook.Payload.Object.UUID - postID, err := p.fetchMeetingPostID(meetingPostID) + post, err := p.resolveRecordingMeetingPost(webhook.Payload.Object.UUID, webhook.Payload.Object.ID) if err != nil { - return - } - - post, err := p.client.Post.GetPost(postID) - if err != nil { - p.API.LogWarn("Could not get meeting post by id", "err", err) + p.API.LogWarn("Could not resolve meeting post for transcript", "error", err.Error()) + http.Error(w, "meeting post not found", http.StatusNotFound) return } @@ -300,15 +402,10 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request return } - meetingPostID := webhook.Payload.Object.UUID - postID, err := p.fetchMeetingPostID(meetingPostID) - if err != nil { - return - } - - post, err := p.client.Post.GetPost(postID) + post, err := p.resolveRecordingMeetingPost(webhook.Payload.Object.UUID, webhook.Payload.Object.ID) if err != nil { - p.API.LogWarn("Could not get meeting post by id", "err", err) + p.API.LogWarn("Could not resolve meeting post for recording", "error", err.Error()) + http.Error(w, "meeting post not found", http.StatusNotFound) return } @@ -334,26 +431,8 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request } for _, recording := range recordingGroup { if recording.RecordingType == zoom.RecordingTypeChat { - request, err := http.NewRequest(http.MethodGet, recording.DownloadURL, nil) - if err != nil { - p.API.LogWarn("Unable to get the chat", "err", err) - return - } - request.Header.Set("Authorization", bearerString+webhook.DownloadToken) - response, err := http.DefaultClient.Do(request) - if err != nil { - p.API.LogWarn("Unable to get the chat", "err", err) - return - } - defer response.Body.Close() - chat, err := io.ReadAll(response.Body) - if err != nil { - p.API.LogWarn("Unable to get the chat", "err", err) - return - } - fileInfo, appErr2 := p.API.UploadFile(chat, post.ChannelId, "Chat-history.txt") - if appErr2 != nil { - p.API.LogWarn("Unable to get the chat", "err", appErr2) + fileInfo, chatErr := p.downloadAndUploadChat(recording, webhook.DownloadToken, post.ChannelId) + if chatErr != nil { return } @@ -380,6 +459,36 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request } } +func (p *Plugin) downloadAndUploadChat(recording zoom.RecordingFile, downloadToken, channelID string) (*model.FileInfo, error) { + request, err := http.NewRequest(http.MethodGet, recording.DownloadURL, nil) + if err != nil { + p.API.LogWarn("Unable to get the chat", "err", err) + return nil, err + } + request.Header.Set("Authorization", bearerString+downloadToken) + + response, err := http.DefaultClient.Do(request) + if err != nil { + p.API.LogWarn("Unable to get the chat", "err", err) + return nil, err + } + defer response.Body.Close() + + chat, err := io.ReadAll(response.Body) + if err != nil { + p.API.LogWarn("Unable to get the chat", "err", err) + return nil, err + } + + fileInfo, appErr := p.API.UploadFile(chat, channelID, "Chat-history.txt") + if appErr != nil { + p.API.LogWarn("Unable to upload the chat file", "err", appErr) + return nil, appErr + } + + return fileInfo, nil +} + func (p *Plugin) verifyMattermostWebhookSecret(r *http.Request) bool { config := p.getConfiguration() return subtle.ConstantTimeCompare([]byte(r.URL.Query().Get("secret")), []byte(config.WebhookSecret)) == 1 diff --git a/server/webhook_test.go b/server/webhook_test.go index c6e8e97f..e7766ed1 100644 --- a/server/webhook_test.go +++ b/server/webhook_test.go @@ -78,6 +78,7 @@ func TestHandleMeetingStarted(t *testing.T) { api.On("LogWarn", "could not get the active Zoom client", "error", "could not fetch Zoom OAuth info: must connect user account to Zoom first").Return() api.On("HasPermissionToChannel", "user-id", "channel-id", mock.AnythingOfType("*model.Permission")).Return(true) api.On("KVSetWithExpiry", "post_meeting_abc", []byte{}, int64(86400)).Return(nil) + api.On("KVSetWithOptions", "meeting_channel_123", mock.AnythingOfType("[]uint8"), model.PluginKVSetOptions{}).Return(true, nil) api.On("PublishWebSocketEvent", "meeting_started", map[string]interface{}{"meeting_url": "https://zoom.us/j/123"}, mock.AnythingOfType("*model.WebsocketBroadcast")).Return() api.On("CreatePost", mock.AnythingOfType("*model.Post")).Return(&model.Post{}, nil) p.SetAPI(api) @@ -91,7 +92,7 @@ func TestHandleMeetingStarted(t *testing.T) { request := httptest.NewRequest("POST", "/webhook?secret=webhooksecret", reqBody) request.Header.Add("Content-Type", "application/json") - ts := "1660149894817" + ts := fmt.Sprintf("%d", time.Now().Unix()) h := hmac.New(sha256.New, []byte(testConfig.ZoomWebhookSecret)) _, _ = h.Write([]byte("v0:" + ts + ":" + requestBody)) signature := "v0=" + hex.EncodeToString(h.Sum(nil)) @@ -115,7 +116,7 @@ func TestHandleMeetingStarted(t *testing.T) { request := httptest.NewRequest("POST", "/webhook?secret=webhooksecret", reqBody) request.Header.Add("Content-Type", "application/json") - ts := "1660149894817" + ts := fmt.Sprintf("%d", time.Now().Unix()) h := hmac.New(sha256.New, []byte(testConfig.ZoomWebhookSecret)) _, _ = h.Write([]byte("v0:" + ts + ":" + requestBody)) signature := "v0=" + hex.EncodeToString(h.Sum(nil)) @@ -141,7 +142,7 @@ func TestHandleMeetingStarted(t *testing.T) { request := httptest.NewRequest("POST", "/webhook?secret=webhooksecret", reqBody) request.Header.Add("Content-Type", "application/json") - ts := "1660149894817" + ts := fmt.Sprintf("%d", time.Now().Unix()) h := hmac.New(sha256.New, []byte(testConfig.ZoomWebhookSecret)) _, _ = h.Write([]byte("v0:" + ts + ":" + requestBody)) signature := "v0=" + hex.EncodeToString(h.Sum(nil)) @@ -161,12 +162,17 @@ func TestWebhookVerifySignature(t *testing.T) { p.setConfiguration(testConfig) api.On("GetLicense").Return(nil) - api.On("KVGet", "post_meeting_123").Return(nil, &model.AppError{StatusCode: 200}) - api.On("LogDebug", "Could not get meeting post from KVStore", "error", "") + api.On("KVGet", "post_meeting_123-abc").Return(nil, &model.AppError{StatusCode: 200}) + api.On("KVGet", "meeting_channel_123").Return(nil, (*model.AppError)(nil)) + api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogWarn", mock.Anything).Maybe().Return() p.SetAPI(api) p.client = pluginapi.NewClient(p.API, p.Driver) - requestBody := `{"payload":{"object": {"id": "123"}},"event":"meeting.ended"}` + requestBody := `{"payload":{"object": {"id": "123", "uuid": "123-abc"}},"event":"meeting.ended"}` ts := fmt.Sprintf("%d", time.Now().Unix()) msg := fmt.Sprintf("v0:%s:%s", ts, requestBody) @@ -184,7 +190,7 @@ func TestWebhookVerifySignature(t *testing.T) { body, _ := io.ReadAll(w.Result().Body) t.Log(string(body)) - require.Equal(t, 200, w.Result().StatusCode) + require.Equal(t, http.StatusNotFound, w.Result().StatusCode) }) t.Run("old timestamp is rejected", func(t *testing.T) { @@ -259,7 +265,12 @@ func TestWebhookEmptyZoomWebhookSecret(t *testing.T) { api.On("GetLicense").Return(nil) api.On("KVGet", "post_meeting_123").Return(nil, &model.AppError{StatusCode: 200}) - api.On("LogDebug", "Could not get meeting post from KVStore", "error", "") + api.On("KVGet", "meeting_channel_123").Return(nil, (*model.AppError)(nil)) + api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On("LogWarn", mock.Anything).Maybe().Return() p.SetAPI(api) p.client = pluginapi.NewClient(p.API, p.Driver) @@ -276,7 +287,7 @@ func TestWebhookEmptyZoomWebhookSecret(t *testing.T) { p.ServeHTTP(&plugin.Context{}, w, request) - require.Equal(t, 200, w.Result().StatusCode) + require.Equal(t, http.StatusNotFound, w.Result().StatusCode) } func TestWebhookVerifySignatureInvalid(t *testing.T) { @@ -392,10 +403,10 @@ func TestWebhookHandleTranscriptCompleted(t *testing.T) { }) requestBody := string(requestBodyBytes) - ts := "1660149894817" + ts := fmt.Sprintf("%d", time.Now().Unix()) h := hmac.New(sha256.New, []byte(testConfig.ZoomWebhookSecret)) _, _ = h.Write([]byte("v0:" + ts + ":" + requestBody)) - signature := "v0=" + hex.EncodeToString(h.Sum(nil)) + signature := fmt.Sprintf("v0=%s", hex.EncodeToString(h.Sum(nil))) w := httptest.NewRecorder() reqBody := io.NopCloser(bytes.NewBufferString(requestBody)) @@ -481,10 +492,10 @@ func TestWebhookHandleRecordingCompleted(t *testing.T) { }) requestBody := string(requestBodyBytes) - ts := "1660149894817" + ts := fmt.Sprintf("%d", time.Now().Unix()) h := hmac.New(sha256.New, []byte(testConfig.ZoomWebhookSecret)) _, _ = h.Write([]byte("v0:" + ts + ":" + requestBody)) - signature := "v0=" + hex.EncodeToString(h.Sum(nil)) + signature := fmt.Sprintf("v0=%s", hex.EncodeToString(h.Sum(nil))) w := httptest.NewRecorder() reqBody := io.NopCloser(bytes.NewBufferString(requestBody)) diff --git a/webapp/.eslintrc.json b/webapp/.eslintrc.json index c279e091..4a542e2c 100644 --- a/webapp/.eslintrc.json +++ b/webapp/.eslintrc.json @@ -485,7 +485,7 @@ 2, "never" ], - "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], + "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx", ".tsx"] }], "react/jsx-first-prop-new-line": [ 2, "multiline" @@ -641,5 +641,30 @@ "onlyEquality": false } ] - } + }, + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json" + }, + "plugins": ["@typescript-eslint"], + "rules": { + "no-undef": "off", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [2, { + "vars": "all", + "args": "after-used" + }], + "no-use-before-define": "off", + "no-shadow": "off", + "@typescript-eslint/no-shadow": [2, { + "hoist": "functions" + }], + "no-redeclare": "off", + "no-dupe-class-members": "off" + } + } + ] } diff --git a/webapp/src/components/ai_icon.tsx b/webapp/src/components/ai_icon.tsx index abca38a5..16620dae 100644 --- a/webapp/src/components/ai_icon.tsx +++ b/webapp/src/components/ai_icon.tsx @@ -1,3 +1,6 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + import React from 'react'; import Svg from './svg'; diff --git a/webapp/src/components/ai_summary_button.tsx b/webapp/src/components/ai_summary_button.tsx new file mode 100644 index 00000000..d02fed46 --- /dev/null +++ b/webapp/src/components/ai_summary_button.tsx @@ -0,0 +1,86 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {FormattedMessage} from 'react-intl'; +import {useSelector} from 'react-redux'; + +import type {Post} from '@mattermost/types/posts'; + +import styled from 'styled-components'; + +import IconAI from 'src/components/ai_icon'; + +const aiPluginID = 'mattermost-ai'; + +export const useAIAvailable = () => { + return useSelector((state: any) => Boolean(state.plugins?.plugins?.[aiPluginID])); +}; + +export const useCallsPostButtonClicked = () => { + return useSelector((state: any) => { + const aiPluginState = state['plugins-' + aiPluginID]; + const handler = aiPluginState?.callsPostButtonClickedTranscription; + if (typeof handler === 'function') { + return handler; + } + return null; + }); +}; + +const SummaryButton = styled.button` + display: flex; + border: none; + height: 24px; + padding: 4px 10px; + margin-top: 8px; + margin-bottom: 8px; + align-items: center; + justify-content: center; + gap: 6px; + border-radius: 4px; + background: rgba(var(--center-channel-color-rgb), 0.08); + color: rgba(var(--center-channel-color-rgb), 0.64); + font-size: 12px; + font-weight: 600; + line-height: 16px; + + &:hover { + background: rgba(var(--center-channel-color-rgb), 0.12); + color: rgba(var(--center-channel-color-rgb), 0.72); + } + + &:active { + background: rgba(var(--button-bg-rgb), 0.08); + color: var(--button-bg); + } +`; + +type Props = { + post: Post; + messageId: string; + defaultMessage: string; +}; + +export const AISummaryButton = ({post, messageId, defaultMessage}: Props) => { + const aiAvailable = useAIAvailable(); + const callsPostButtonClicked = useCallsPostButtonClicked(); + + if (!aiAvailable || !callsPostButtonClicked) { + return null; + } + + const handleClick = () => { + callsPostButtonClicked(post); + }; + + return ( + + + + + ); +}; diff --git a/webapp/src/components/post_type_chat.tsx b/webapp/src/components/post_type_chat.tsx index 086064ba..f8196d1a 100644 --- a/webapp/src/components/post_type_chat.tsx +++ b/webapp/src/components/post_type_chat.tsx @@ -1,92 +1,35 @@ -import React from 'react'; -import {FormattedMessage} from 'react-intl'; -import {useSelector} from 'react-redux'; - -import type {Post} from 'mattermost-redux/types/posts'; - -import styled from 'styled-components'; - -import IconAI from 'src/components/ai_icon'; - -const aiPluginID = 'mattermost-ai'; - -const useAIAvailable = () => { - return useSelector((state: any) => Boolean(state.plugins?.plugins?.[aiPluginID])); -}; - -const useCallsPostButtonClicked = () => { - return useSelector((state: any) => { - const aiPluginState = state['plugins-' + aiPluginID]; - return aiPluginState?.callsPostButtonClickedTranscription; - }); -}; +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. -const CreateMeetingSummaryButton = styled.button` - display: flex; - border: none; - height: 24px; - padding: 4px 10px; - margin-top: 8px; - margin-bottom: 8px; - align-items: center; - justify-content: center; - gap: 6px; - border-radius: 4px; - background: rgba(var(--center-channel-color-rgb), 0.08); - color: rgba(var(--center-channel-color-rgb), 0.64); - font-size: 12px; - font-weight: 600; - line-height: 16px; +import React from 'react'; - &:hover { - background: rgba(var(--center-channel-color-rgb), 0.12); - color: rgba(var(--center-channel-color-rgb), 0.72); - } +import type {Post} from '@mattermost/types/posts'; - &:active { - background: rgba(var(--button-bg-rgb), 0.08); - color: var(--button-bg); - } -`; +import {AISummaryButton} from './ai_summary_button'; type Props = { post: Post; }; -export const PostTypeChat = (props: Props) => { - const aiAvailable = useAIAvailable(); - const callsPostButtonClicked = useCallsPostButtonClicked(); - - const createMeetingSummary = () => { - callsPostButtonClicked?.(props.post); - }; - - const markdownMessage = props.post.message; +const renderPostWithMarkdown = (msg: string) => { + const windowAny: any = window; + const {formatText, messageHtmlToComponent} = windowAny.PostUtils; - const renderPostWithMarkdown = (msg: string) => { - const windowAny: any = window; - const {formatText, messageHtmlToComponent} = windowAny.PostUtils; - - return messageHtmlToComponent( - formatText(msg, {}), - false, - ); - }; + return messageHtmlToComponent( + formatText(msg, {}), + false, + ); +}; +export const PostTypeChat = (props: Props) => { return (
- {renderPostWithMarkdown(markdownMessage)} - {aiAvailable && callsPostButtonClicked && ( - - - - - )} + {renderPostWithMarkdown(props.post.message)} +
); }; diff --git a/webapp/src/components/post_type_transcription.tsx b/webapp/src/components/post_type_transcription.tsx index 91d7b93e..9092c139 100644 --- a/webapp/src/components/post_type_transcription.tsx +++ b/webapp/src/components/post_type_transcription.tsx @@ -1,82 +1,25 @@ -import React from 'react'; -import {FormattedMessage} from 'react-intl'; -import {useSelector} from 'react-redux'; - -import type {Post} from 'mattermost-redux/types/posts'; - -import styled from 'styled-components'; - -import IconAI from 'src/components/ai_icon'; - -const aiPluginID = 'mattermost-ai'; - -const useAIAvailable = () => { - return useSelector((state: any) => Boolean(state.plugins?.plugins?.[aiPluginID])); -}; +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. -const useCallsPostButtonClicked = () => { - return useSelector((state: any) => { - const aiPluginState = state['plugins-' + aiPluginID]; - return aiPluginState?.callsPostButtonClickedTranscription; - }); -}; - -const CreateMeetingSummaryButton = styled.button` - display: flex; - border: none; - height: 24px; - padding: 4px 10px; - margin-top: 8px; - margin-bottom: 8px; - align-items: center; - justify-content: center; - gap: 6px; - border-radius: 4px; - background: rgba(var(--center-channel-color-rgb), 0.08); - color: rgba(var(--center-channel-color-rgb), 0.64); - font-size: 12px; - font-weight: 600; - line-height: 16px; +import React from 'react'; - &:hover { - background: rgba(var(--center-channel-color-rgb), 0.12); - color: rgba(var(--center-channel-color-rgb), 0.72); - } +import type {Post} from '@mattermost/types/posts'; - &:active { - background: rgba(var(--button-bg-rgb), 0.08); - color: var(--button-bg); - } -`; +import {AISummaryButton} from './ai_summary_button'; type Props = { post: Post; }; export const PostTypeTranscription = (props: Props) => { - const aiAvailable = useAIAvailable(); - const callsPostButtonClicked = useCallsPostButtonClicked(); - - const createMeetingSummary = () => { - callsPostButtonClicked?.(props.post); - }; - - const msg = props.post.message; - return (
- {msg} - {aiAvailable && callsPostButtonClicked && ( - - - - - )} + {props.post.message} +
); }; diff --git a/webapp/src/components/svg.tsx b/webapp/src/components/svg.tsx index 8430fb0b..d0b6026a 100644 --- a/webapp/src/components/svg.tsx +++ b/webapp/src/components/svg.tsx @@ -1,4 +1,4 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import styled from 'styled-components'; diff --git a/webapp/webpack.config.js b/webapp/webpack.config.js index 2ab6c79f..823d06f1 100644 --- a/webapp/webpack.config.js +++ b/webapp/webpack.config.js @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. var path = require('path'); + var webpack = require('webpack'); module.exports = { From 4216b5e519ac4bc72a626f80e36e8d1b67f7d8b2 Mon Sep 17 00:00:00 2001 From: avasconcelos114 Date: Wed, 25 Feb 2026 18:00:56 +0200 Subject: [PATCH 39/52] Fixing issues with missing implementation - Making sure mp4 recordings get posted to thread - Improving channel subscription to record more detailed data and filter by user on the list command - Fixing a bug where certain zoom uuids would break KV store saving due to special characters --- server/command.go | 4 +- server/http.go | 3 -- server/plugin_test.go | 16 ++---- server/store.go | 118 ++++++++++++++++++++++++++++++++++------- server/webhook.go | 100 ++++++++++++++++++++++++++-------- server/webhook_test.go | 62 ++++++++++------------ server/zoom/webhook.go | 3 +- 7 files changed, 213 insertions(+), 93 deletions(-) diff --git a/server/command.go b/server/command.go index 75dc1bcd..4418d0d5 100644 --- a/server/command.go +++ b/server/command.go @@ -296,7 +296,7 @@ func (p *Plugin) runSubscriptionListCommand(args *model.CommandArgs) (string, er return "You do not have permission to view subscriptions in this channel.", nil } - subs, err := p.listAllMeetingSubscriptions() + subs, err := p.listAllMeetingSubscriptions(args.UserId) if err != nil { p.client.Log.Error("Unable to list meeting subscriptions", "Error", err.Error()) return "Unable to list meeting subscriptions.", nil @@ -337,7 +337,7 @@ func (p *Plugin) runSubscribeCommand(user *model.User, extra *model.CommandArgs, return "Cannot subscribe to personal meeting", nil } - if appErr := p.storeChannelForMeeting(meetingID, extra.ChannelId); appErr != nil { + if appErr := p.storeSubscriptionForMeeting(meetingID, extra.ChannelId, user.Id); appErr != nil { return "", errors.Wrap(appErr, "cannot subscribe to meeting") } diff --git a/server/http.go b/server/http.go index 7d2e3e50..647ed38e 100644 --- a/server/http.go +++ b/server/http.go @@ -500,9 +500,6 @@ func (p *Plugin) postMeeting(creator *model.User, meetingID int, meetingUUID str }, } - existingChannelID, _ := p.fetchChannelForMeeting(meetingID) - post.Props["meeting_subscribed"] = existingChannelID != "" - createdPost, appErr := p.API.CreatePost(post) if appErr != nil { return appErr diff --git a/server/plugin_test.go b/server/plugin_test.go index b181e9ff..088e839a 100644 --- a/server/plugin_test.go +++ b/server/plugin_test.go @@ -169,7 +169,8 @@ func TestPlugin(t *testing.T) { api.On("KVSetWithOptions", "mutex_mmi_bot_ensure", mock.AnythingOfType("[]uint8"), model.PluginKVSetOptions{Atomic: true, OldValue: []uint8(nil), ExpireInSeconds: 15}).Return(true, nil) api.On("KVSetWithOptions", "mutex_mmi_bot_ensure", []byte(nil), model.PluginKVSetOptions{ExpireInSeconds: 0}).Return(true, nil) api.On("KVSetWithOptions", "post_meeting_234", []byte(nil), model.PluginKVSetOptions{ExpireInSeconds: 0}).Return(true, nil) - api.On("KVSetWithOptions", mock.MatchedBy(func(key string) bool { return strings.HasPrefix(key, meetingChannelKey) }), mock.AnythingOfType("[]uint8"), model.PluginKVSetOptions{}).Return(true, nil) + api.On("KVGet", mock.MatchedBy(func(key string) bool { return strings.HasPrefix(key, meetingChannelKey) })).Return(nil, (*model.AppError)(nil)).Maybe() + api.On("KVSetWithExpiry", mock.MatchedBy(func(key string) bool { return strings.HasPrefix(key, meetingChannelKey) }), mock.AnythingOfType("[]uint8"), int64(adHocMeetingChannelTTL)).Return(nil).Maybe() api.On("EnsureBotUser", &model.Bot{ Username: botUserName, @@ -183,18 +184,7 @@ func TestPlugin(t *testing.T) { api.On("KVDelete", fmt.Sprintf("%v%v", postMeetingKey, 234)).Return(nil) - api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogWarn", mock.Anything).Maybe().Return() - api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogDebug", mock.Anything).Maybe().Return() + allowFlexibleLogging(api) path, err := filepath.Abs("..") require.Nil(t, err) diff --git a/server/store.go b/server/store.go index d0568c45..284ec86f 100644 --- a/server/store.go +++ b/server/store.go @@ -6,6 +6,7 @@ package main import ( "encoding/json" "fmt" + "net/url" "strings" "github.com/mattermost/mattermost/server/public/model" @@ -143,14 +144,20 @@ func (p *Plugin) deleteUserState(userID string) *model.AppError { return p.API.KVDelete(key) } +// meetingPostKey returns a KV-safe key for a given Zoom meeting UUID. +// Zoom UUIDs can contain '/' and '=' which may cause issues in KV keys. +func meetingPostKey(meetingUUID string) string { + return postMeetingKey + url.PathEscape(meetingUUID) +} + func (p *Plugin) storeMeetingPostID(meetingUUID string, postID string) *model.AppError { - key := fmt.Sprintf("%v%v", postMeetingKey, meetingUUID) + key := meetingPostKey(meetingUUID) b := []byte(postID) return p.API.KVSetWithExpiry(key, b, meetingPostIDTTL) } func (p *Plugin) fetchMeetingPostID(meetingUUID string) (string, error) { - key := fmt.Sprintf("%v%v", postMeetingKey, meetingUUID) + key := meetingPostKey(meetingUUID) var postIDData []byte if err := p.client.KV.Get(key, &postIDData); err != nil { p.client.Log.Debug("Could not get meeting post from KVStore", "error", err.Error()) @@ -164,37 +171,94 @@ func (p *Plugin) fetchMeetingPostID(meetingUUID string) (string, error) { return string(postIDData), nil } +// meetingChannelEntry stores metadata about a meeting-to-channel mapping. +type meetingChannelEntry struct { + ChannelID string `json:"channel_id"` + IsSubscription bool `json:"is_subscription"` + CreatedBy string `json:"created_by"` +} + +// Ad-hoc meeting channel entries expire after 24 hours. This must be long +// enough to cover the full meeting duration (which can be many hours) plus +// the post-meeting window for recording/transcript webhooks to arrive. +const adHocMeetingChannelTTL = 60 * 60 * 24 + +func meetingChannelKVKey(meetingID int) string { + return fmt.Sprintf("%v%v", meetingChannelKey, meetingID) +} + +func (p *Plugin) storeSubscriptionForMeeting(meetingID int, channelID, userID string) error { + entry := meetingChannelEntry{ + ChannelID: channelID, + IsSubscription: true, + CreatedBy: userID, + } + data, err := json.Marshal(entry) + if err != nil { + return err + } + return p.API.KVSet(meetingChannelKVKey(meetingID), data) +} + func (p *Plugin) storeChannelForMeeting(meetingID int, channelID string) error { - key := fmt.Sprintf("%v%v", meetingChannelKey, meetingID) - bytes := []byte(channelID) - _, err := p.client.KV.Set(key, bytes) - return err + key := meetingChannelKVKey(meetingID) + + // If a subscription entry already exists for this meeting, don't overwrite + // it with an ad-hoc entry. + if existing, _ := p.getMeetingChannelEntry(meetingID); existing != nil && existing.IsSubscription { + return nil + } + + entry := meetingChannelEntry{ + ChannelID: channelID, + IsSubscription: false, + } + data, err := json.Marshal(entry) + if err != nil { + return err + } + return p.API.KVSetWithExpiry(key, data, adHocMeetingChannelTTL) +} + +func (p *Plugin) getMeetingChannelEntry(meetingID int) (*meetingChannelEntry, *model.AppError) { + raw, appErr := p.API.KVGet(meetingChannelKVKey(meetingID)) + if appErr != nil { + return nil, appErr + } + if raw == nil { + return nil, nil + } + + var entry meetingChannelEntry + if err := json.Unmarshal(raw, &entry); err != nil { + return nil, nil + } + if entry.ChannelID == "" { + return nil, nil + } + return &entry, nil } func (p *Plugin) fetchChannelForMeeting(meetingID int) (string, *model.AppError) { - key := fmt.Sprintf("%v%v", meetingChannelKey, meetingID) - channelID, appErr := p.API.KVGet(key) + entry, appErr := p.getMeetingChannelEntry(meetingID) if appErr != nil { p.API.LogDebug("Could not get channel meeting from KVStore", "error", appErr.Error()) return "", appErr } - - if channelID == nil { - p.API.LogWarn("Stored channel meeting not found") - return "", appErr + if entry == nil { + return "", nil } - - return string(channelID), nil + return entry.ChannelID, nil } func (p *Plugin) deleteChannelForMeeting(meetingID int) error { - key := fmt.Sprintf("%v%v", meetingChannelKey, meetingID) + key := meetingChannelKVKey(meetingID) return p.client.KV.Delete(key) } const kvListPerPage = 100 -func (p *Plugin) listAllMeetingSubscriptions() (map[string]string, error) { +func (p *Plugin) listAllMeetingSubscriptions(userID string) (map[string]string, error) { subscriptions := make(map[string]string) for page := 0; ; page++ { @@ -208,12 +272,26 @@ func (p *Plugin) listAllMeetingSubscriptions() (map[string]string, error) { continue } - meetingID := strings.TrimPrefix(key, meetingChannelKey) - channelIDBytes, kvErr := p.API.KVGet(key) - if kvErr != nil || channelIDBytes == nil { + raw, kvErr := p.API.KVGet(key) + if kvErr != nil || raw == nil { + continue + } + + var entry meetingChannelEntry + if err := json.Unmarshal(raw, &entry); err != nil || entry.ChannelID == "" { + continue + } + + if !entry.IsSubscription { + continue + } + + if entry.CreatedBy != userID { continue } - subscriptions[meetingID] = string(channelIDBytes) + + meetingID := strings.TrimPrefix(key, meetingChannelKey) + subscriptions[meetingID] = entry.ChannelID } if len(keys) < kvListPerPage { diff --git a/server/webhook.go b/server/webhook.go index a87584bc..0c284d85 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -59,6 +59,11 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) { return } + p.API.LogDebug("Received webhook event", + "event", string(webhook.Event), + "body_size", len(b), + ) + if webhook.Event != zoom.EventTypeValidateWebhook { err = p.verifyZoomWebhookSignature(r, b) if err != nil { @@ -80,6 +85,7 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) { case zoom.EventTypeTranscriptCompleted: p.handleTranscriptCompleted(w, r, b) default: + p.API.LogDebug("Received unhandled webhook event type", "event", string(webhook.Event)) w.WriteHeader(http.StatusOK) } } @@ -108,6 +114,26 @@ func (p *Plugin) handleMeetingStarted(w http.ResponseWriter, r *http.Request, bo return } + // If a post already exists for this meeting (e.g. created via /zoom start), + // don't create a duplicate. Just update the stored UUID mapping so that + // meeting.ended can find the post later. + if existingPostID, err := p.findMeetingPostByMeetingID(meetingID); err == nil { + p.API.LogDebug("handleMeetingStarted: meeting post already exists, skipping duplicate and storing UUID mapping", + "meeting_id", meetingID, + "existing_post_id", existingPostID, + "webhook_uuid", webhook.Payload.Object.UUID, + ) + if appErr := p.storeMeetingPostID(webhook.Payload.Object.UUID, existingPostID); appErr != nil { + p.API.LogWarn("handleMeetingStarted: failed to store UUID mapping for existing post", + "uuid", webhook.Payload.Object.UUID, + "post_id", existingPostID, + "error", appErr.Error(), + ) + } + w.WriteHeader(http.StatusOK) + return + } + botUser, appErr := p.API.GetUser(p.botUserID) if appErr != nil { p.API.LogError("Failed to get bot user", "err", appErr.Error()) @@ -229,12 +255,10 @@ func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body p.API.LogDebug("Successfully updated meeting post to ended", "post_id", post.Id) - subscribed, _ := post.Props["meeting_subscribed"].(bool) - if !subscribed && int(meetingID) != 0 { - if err := p.deleteChannelForMeeting(int(meetingID)); err != nil { - p.client.Log.Debug("Could not clean up channel-meeting mapping", "meeting_id", int(meetingID), "error", err.Error()) - } - } + // NOTE: We intentionally do NOT delete the meeting_channel mapping here. + // Recording and transcript webhooks arrive after meeting.ended and need + // the mapping to locate the post. The entry is small and gets overwritten + // if the same meeting ID is reused. w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(post); err != nil { @@ -243,6 +267,13 @@ func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body } func (p *Plugin) findMeetingPostByMeetingID(meetingID int) (string, error) { + return p.findMeetingPostByMeetingIDWithFilter(meetingID, true) +} + +// findMeetingPostByMeetingIDWithFilter searches recent posts in the channel +// associated with meetingID for a custom_zoom post matching that ID. +// When activeOnly is true, posts already marked as ENDED are skipped. +func (p *Plugin) findMeetingPostByMeetingIDWithFilter(meetingID int, activeOnly bool) (string, error) { channelID, appErr := p.fetchChannelForMeeting(meetingID) if appErr != nil || channelID == "" { return "", errors.Errorf("no channel found for meeting %d", meetingID) @@ -254,17 +285,31 @@ func (p *Plugin) findMeetingPostByMeetingID(meetingID int) (string, error) { return "", errors.Wrap(appErr, "could not get recent posts for channel") } + // Prefer the most recently created matching post. + var bestPostID string + var bestCreateAt int64 for _, post := range postList.Posts { if post.Type != "custom_zoom" { continue } propID, ok := post.Props["meeting_id"].(float64) - if ok && int(propID) == meetingID && post.Props["meeting_status"] != zoom.WebhookStatusEnded { - return post.Id, nil + if !ok || int(propID) != meetingID { + continue + } + if activeOnly && post.Props["meeting_status"] == zoom.WebhookStatusEnded { + continue + } + if post.CreateAt > bestCreateAt { + bestPostID = post.Id + bestCreateAt = post.CreateAt } } - return "", errors.Errorf("no active meeting post found for meeting %d in channel %s", meetingID, channelID) + if bestPostID != "" { + return bestPostID, nil + } + + return "", errors.Errorf("no meeting post found for meeting %d in channel %s (active_only=%v)", meetingID, channelID, activeOnly) } // resolveRecordingMeetingPost finds the meeting post by UUID first, falling @@ -279,7 +324,9 @@ func (p *Plugin) resolveRecordingMeetingPost(webhookUUID string, meetingID int) "error", err.Error(), ) - postID, err = p.findMeetingPostByMeetingID(meetingID) + // Recording/transcript webhooks arrive after meeting.ended, so the post + // is already marked ENDED. Use activeOnly=false to include ended posts. + postID, err = p.findMeetingPostByMeetingIDWithFilter(meetingID, false) if err != nil { return nil, errors.Wrapf(err, "could not find meeting post for uuid=%s meeting_id=%d", webhookUUID, meetingID) } @@ -395,16 +442,17 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques } func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request, body []byte) { + var webhook zoom.RecordingWebhook if err := json.Unmarshal(body, &webhook); err != nil { - p.API.LogError("Error unmarshaling meeting webhook", "err", err.Error()) + p.API.LogError("handleRecordingCompleted: failed to unmarshal", "err", err.Error()) http.Error(w, err.Error(), http.StatusBadRequest) return } post, err := p.resolveRecordingMeetingPost(webhook.Payload.Object.UUID, webhook.Payload.Object.ID) if err != nil { - p.API.LogWarn("Could not resolve meeting post for recording", "error", err.Error()) + p.API.LogWarn("handleRecordingCompleted: could not resolve meeting post", "error", err.Error()) http.Error(w, "meeting post not found", http.StatusNotFound) return } @@ -412,11 +460,10 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request recordings := make(map[time.Time][]zoom.RecordingFile) for _, recording := range webhook.Payload.Object.RecordingFiles { - if recording.RecordingType == zoom.RecordingTypeChat { + switch { + case recording.RecordingType == zoom.RecordingTypeChat: recordings[recording.RecordingStart] = append(recordings[recording.RecordingStart], recording) - } - - if recording.RecordingType == zoom.RecordingTypeVideo { + case strings.EqualFold(recording.FileType, zoom.RecordingFileTypeMP4): recordings[recording.RecordingStart] = append(recordings[recording.RecordingStart], recording) } } @@ -433,21 +480,32 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request if recording.RecordingType == zoom.RecordingTypeChat { fileInfo, chatErr := p.downloadAndUploadChat(recording, webhook.DownloadToken, post.ChannelId) if chatErr != nil { + p.API.LogWarn("handleRecordingCompleted: failed to download/upload chat", "error", chatErr.Error()) return } newPost.FileIds = append(newPost.FileIds, fileInfo.Id) newPost.AddProp("captions", []any{map[string]any{"file_id": fileInfo.Id}}) newPost.Type = "custom_zoom_chat" - } - if recording.RecordingType == zoom.RecordingTypeVideo { - newPost.Message = "Here's the zoom meeting recording:\n**Link:** [Meeting Recording](" + recording.PlayURL + ")\n**Password:** " + webhook.Payload.Object.Password + } else if strings.EqualFold(recording.FileType, zoom.RecordingFileTypeMP4) && recording.PlayURL != "" { + msg := "Here's the zoom meeting recording:\n**Link:** [Meeting Recording](" + recording.PlayURL + ")" + if webhook.Payload.Object.Password != "" { + msg += "\n**Password:** `" + webhook.Payload.Object.Password + "`" + } + newPost.Message = msg } } + if newPost.Message != "" { _, appErr := p.API.CreatePost(newPost) if appErr != nil { - p.API.LogWarn("Could not update the post", "err", appErr) + p.API.LogWarn("handleRecordingCompleted: could not create post", "err", appErr) + return + } + } else if len(newPost.FileIds) > 0 { + _, appErr := p.API.CreatePost(newPost) + if appErr != nil { + p.API.LogWarn("handleRecordingCompleted: could not create chat post", "err", appErr) return } } @@ -455,7 +513,7 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(post); err != nil { - p.client.Log.Warn("failed to write response", "error", err.Error()) + p.API.LogWarn("failed to write response", "error", err.Error()) } } diff --git a/server/webhook_test.go b/server/webhook_test.go index e7766ed1..ccabc432 100644 --- a/server/webhook_test.go +++ b/server/webhook_test.go @@ -28,6 +28,18 @@ import ( "github.com/mattermost/mattermost-plugin-zoom/server/zoom" ) +func allowFlexibleLogging(api *plugintest.API) { + for _, method := range []string{"LogDebug", "LogWarn", "LogError"} { + api.On(method, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On(method, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On(method, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On(method, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On(method, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On(method, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() + api.On(method, mock.Anything).Maybe().Return() + } +} + var testConfig = &configuration{ OAuthClientID: "clientid", OAuthClientSecret: "clientsecret", @@ -42,6 +54,7 @@ func TestWebhookValidate(t *testing.T) { p.setConfiguration(testConfig) api.On("GetLicense").Return(nil) + allowFlexibleLogging(api) p.SetAPI(api) requestBody := `{"payload":{"plainToken":"Kn5a3Wv7SP6YP5b4BWfZpg"},"event":"endpoint.url_validation"}` @@ -78,9 +91,11 @@ func TestHandleMeetingStarted(t *testing.T) { api.On("LogWarn", "could not get the active Zoom client", "error", "could not fetch Zoom OAuth info: must connect user account to Zoom first").Return() api.On("HasPermissionToChannel", "user-id", "channel-id", mock.AnythingOfType("*model.Permission")).Return(true) api.On("KVSetWithExpiry", "post_meeting_abc", []byte{}, int64(86400)).Return(nil) - api.On("KVSetWithOptions", "meeting_channel_123", mock.AnythingOfType("[]uint8"), model.PluginKVSetOptions{}).Return(true, nil) + api.On("KVSetWithExpiry", "meeting_channel_123", mock.AnythingOfType("[]uint8"), int64(adHocMeetingChannelTTL)).Return(nil) api.On("PublishWebSocketEvent", "meeting_started", map[string]interface{}{"meeting_url": "https://zoom.us/j/123"}, mock.AnythingOfType("*model.WebsocketBroadcast")).Return() api.On("CreatePost", mock.AnythingOfType("*model.Post")).Return(&model.Post{}, nil) + api.On("GetPostsSince", "channel-id", mock.AnythingOfType("int64")).Return(&model.PostList{}, nil) + allowFlexibleLogging(api) p.SetAPI(api) p.client = pluginapi.NewClient(api, nil) p.botUserID = "" @@ -108,6 +123,7 @@ func TestHandleMeetingStarted(t *testing.T) { api := &plugintest.API{} api.On("GetLicense").Return(nil) api.On("LogError", "Failed to get meeting ID", "err", "strconv.Atoi: parsing \"invalid\": invalid syntax").Return() + allowFlexibleLogging(api) p.SetAPI(api) requestBody := `{"payload":{"object": {"id": "invalid", "uuid": "123-abc"}},"event":"meeting.started"}` @@ -132,8 +148,8 @@ func TestHandleMeetingStarted(t *testing.T) { api := &plugintest.API{} api.On("GetLicense").Return(nil) api.On("KVGet", "meeting_channel_123").Return(nil, &model.AppError{}) - api.On("LogDebug", "Could not get channel meeting from KVStore", "error", "").Return() api.On("KVSetWithExpiry", "post_meeting_123-abc", []byte{}, int64(86400)).Return(nil) + allowFlexibleLogging(api) p.SetAPI(api) requestBody := `{"payload":{"object": {"id": "123", "uuid": "123-abc"}},"event":"meeting.started"}` @@ -164,11 +180,7 @@ func TestWebhookVerifySignature(t *testing.T) { api.On("GetLicense").Return(nil) api.On("KVGet", "post_meeting_123-abc").Return(nil, &model.AppError{StatusCode: 200}) api.On("KVGet", "meeting_channel_123").Return(nil, (*model.AppError)(nil)) - api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogWarn", mock.Anything).Maybe().Return() + allowFlexibleLogging(api) p.SetAPI(api) p.client = pluginapi.NewClient(p.API, p.Driver) @@ -200,6 +212,7 @@ func TestWebhookVerifySignature(t *testing.T) { api.On("GetLicense").Return(nil) api.On("LogWarn", "Could not verify webhook signature: webhook timestamp is too old") + allowFlexibleLogging(api) p.SetAPI(api) requestBody := `{"payload":{"object": {"id": "123"}},"event":"meeting.ended"}` @@ -228,6 +241,7 @@ func TestWebhookVerifySignature(t *testing.T) { api.On("GetLicense").Return(nil) api.On("LogWarn", "Could not verify webhook signature: webhook timestamp is too far in the future") + allowFlexibleLogging(api) p.SetAPI(api) requestBody := `{"payload":{"object": {"id": "123"}},"event":"meeting.ended"}` @@ -266,11 +280,7 @@ func TestWebhookEmptyZoomWebhookSecret(t *testing.T) { api.On("GetLicense").Return(nil) api.On("KVGet", "post_meeting_123").Return(nil, &model.AppError{StatusCode: 200}) api.On("KVGet", "meeting_channel_123").Return(nil, (*model.AppError)(nil)) - api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogDebug", mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogWarn", mock.Anything, mock.Anything, mock.Anything).Maybe().Return() - api.On("LogWarn", mock.Anything).Maybe().Return() + allowFlexibleLogging(api) p.SetAPI(api) p.client = pluginapi.NewClient(p.API, p.Driver) @@ -297,6 +307,7 @@ func TestWebhookVerifySignatureInvalid(t *testing.T) { api.On("GetLicense").Return(nil) api.On("LogWarn", "Could not verify webhook signature: provided signature does not match") + allowFlexibleLogging(api) p.SetAPI(api) requestBody := `{"payload":{"object": {"id": "123"}},"event":"meeting.ended"}` @@ -362,6 +373,7 @@ func TestWebhookHandleTranscriptCompleted(t *testing.T) { api.On("GetLicense").Return(nil) api.On("GetPost", "post-id").Return(&model.Post{Id: "post-id", ChannelId: "channel-id"}, nil) api.On("KVGet", "post_meeting_321").Return([]byte("post-id"), nil) + allowFlexibleLogging(api) api.On("UploadFile", []byte("/test"), "channel-id", "transcription.txt").Return(&model.FileInfo{Id: "file-id"}, nil) p.client = pluginapi.NewClient(api, nil) api.On("CreatePost", &model.Post{ @@ -442,27 +454,10 @@ func TestWebhookHandleRecordingCompleted(t *testing.T) { api.On("GetLicense").Return(nil) api.On("GetPost", "post-id").Return(&model.Post{Id: "post-id", ChannelId: "channel-id"}, nil) api.On("KVGet", "post_meeting_321").Return([]byte("post-id"), nil) + allowFlexibleLogging(api) api.On("UploadFile", []byte("/chat_file"), "channel-id", "Chat-history.txt").Return(&model.FileInfo{Id: "file-id"}, nil) p.client = pluginapi.NewClient(api, nil) - api.On("CreatePost", &model.Post{ - ChannelId: "channel-id", - RootId: "post-id", - Message: "Here's the zoom meeting recording:\n**Link:** [Meeting Recording]()\n**Password:** test-password", - Type: "custom_zoom_chat", - Props: model.StringInterface{ - "captions": []any{map[string]any{"file_id": "file-id"}}, - }, - FileIds: []string{"file-id"}, - }).Return(&model.Post{ - ChannelId: "channel-id", - RootId: "post-id", - Message: "Here's the zoom meeting recording:\n**Link:** [Meeting Recording]()\n**Password:** test-password", - Type: "custom_zoom_chat", - Props: model.StringInterface{ - "captions": []any{map[string]any{"file_id": "file-id"}}, - }, - FileIds: []string{"file-id"}, - }, nil) + api.On("CreatePost", mock.AnythingOfType("*model.Post")).Return(&model.Post{}, nil) p.SetAPI(api) now := time.Now() @@ -480,9 +475,10 @@ func TestWebhookHandleRecordingCompleted(t *testing.T) { }, { "recording_start": now, - "recording_type": "shared_screen_with_speaker_view", + "recording_type": "shared_screen_with_speaker_view(CC)", + "file_type": "MP4", "download_url": httpServer.URL + "/recording_file", - "playURL": httpServer.URL + "/recording_url", + "play_url": httpServer.URL + "/recording_url", }, }, }, diff --git a/server/zoom/webhook.go b/server/zoom/webhook.go index afc032e9..860cb5e0 100644 --- a/server/zoom/webhook.go +++ b/server/zoom/webhook.go @@ -23,7 +23,8 @@ const ( RecordingTypeAudioTranscript = "audio_transcript" RecordingTypeChat = "chat_file" - RecordingTypeVideo = "shared_screen_with_speaker_view" + + RecordingFileTypeMP4 = "MP4" ) type MeetingWebhookObject struct { From 33464b4c086f06aa4d1c61b9c27c4393e42e042f Mon Sep 17 00:00:00 2001 From: avasconcelos114 Date: Thu, 26 Feb 2026 13:02:57 +0200 Subject: [PATCH 40/52] Ensuring recurring meetings create a new post on each iteration of the meeting - Trimming out excess debug logs added during dev process --- server/http.go | 12 ++------ server/store.go | 18 +++++++++--- server/webhook.go | 75 ++++++++++------------------------------------- 3 files changed, 32 insertions(+), 73 deletions(-) diff --git a/server/http.go b/server/http.go index 647ed38e..0912144c 100644 --- a/server/http.go +++ b/server/http.go @@ -355,25 +355,18 @@ func (p *Plugin) completeUserOAuthToZoom(w http.ResponseWriter, r *http.Request) storedState, appErr := p.fetchOAuthUserState(authedUserID) if appErr != nil { - p.API.LogWarn("OAuth completion failed: could not fetch stored state", "authed_user_id", authedUserID, "error", appErr.Error()) http.Error(w, "missing stored state", http.StatusNotFound) return } - p.API.LogDebug("OAuth state retrieved", "authed_user_id", authedUserID, "state_length", len(storedState)) - userID, channelID, justConnect, err := parseOAuthUserState(storedState) if err != nil { - p.API.LogWarn("OAuth completion failed: could not parse state", "authed_user_id", authedUserID, "error", err.Error()) http.Error(w, err.Error(), http.StatusBadRequest) return } - p.API.LogDebug("OAuth state parsed", "user_id", userID, "channel_id", channelID, "just_connect", justConnect) - state := r.URL.Query().Get("state") if storedState != state { - p.API.LogWarn("OAuth completion failed: state mismatch", "authed_user_id", authedUserID, "stored_state_length", len(storedState), "query_state_length", len(state)) http.Error(w, "OAuth user state mismatch", http.StatusUnauthorized) return } @@ -506,11 +499,11 @@ func (p *Plugin) postMeeting(creator *model.User, meetingID int, meetingUUID str } if appErr = p.storeMeetingPostID(meetingUUID, createdPost.Id); appErr != nil { - p.API.LogDebug("failed to store post id", "error", appErr) + p.API.LogWarn("failed to store meeting post ID", "error", appErr.Error()) } if err := p.storeChannelForMeeting(meetingID, channelID); err != nil { - p.API.LogDebug("failed to store channel for meeting", "meeting_id", meetingID, "error", err) + p.API.LogWarn("failed to store channel for meeting", "error", err.Error()) } p.client.Frontend.PublishWebSocketEvent( @@ -763,7 +756,6 @@ func (p *Plugin) getMeeting(user *model.User, meetingID int) (*zoom.Meeting, err meeting, err := client.GetMeeting(meetingID) if err != nil { - p.API.LogDebug("failed to get meeting") return nil, err } return meeting, nil diff --git a/server/store.go b/server/store.go index 284ec86f..ceba473f 100644 --- a/server/store.go +++ b/server/store.go @@ -197,7 +197,10 @@ func (p *Plugin) storeSubscriptionForMeeting(meetingID int, channelID, userID st if err != nil { return err } - return p.API.KVSet(meetingChannelKVKey(meetingID), data) + if appErr := p.API.KVSet(meetingChannelKVKey(meetingID), data); appErr != nil { + return appErr + } + return nil } func (p *Plugin) storeChannelForMeeting(meetingID int, channelID string) error { @@ -217,11 +220,15 @@ func (p *Plugin) storeChannelForMeeting(meetingID int, channelID string) error { if err != nil { return err } - return p.API.KVSetWithExpiry(key, data, adHocMeetingChannelTTL) + if appErr := p.API.KVSetWithExpiry(key, data, adHocMeetingChannelTTL); appErr != nil { + return appErr + } + return nil } func (p *Plugin) getMeetingChannelEntry(meetingID int) (*meetingChannelEntry, *model.AppError) { - raw, appErr := p.API.KVGet(meetingChannelKVKey(meetingID)) + key := meetingChannelKVKey(meetingID) + raw, appErr := p.API.KVGet(key) if appErr != nil { return nil, appErr } @@ -231,6 +238,10 @@ func (p *Plugin) getMeetingChannelEntry(meetingID int) (*meetingChannelEntry, *m var entry meetingChannelEntry if err := json.Unmarshal(raw, &entry); err != nil { + p.API.LogWarn("failed to unmarshal meeting channel entry", + "key", key, + "error", err.Error(), + ) return nil, nil } if entry.ChannelID == "" { @@ -242,7 +253,6 @@ func (p *Plugin) getMeetingChannelEntry(meetingID int) (*meetingChannelEntry, *m func (p *Plugin) fetchChannelForMeeting(meetingID int) (string, *model.AppError) { entry, appErr := p.getMeetingChannelEntry(meetingID) if appErr != nil { - p.API.LogDebug("Could not get channel meeting from KVStore", "error", appErr.Error()) return "", appErr } if entry == nil { diff --git a/server/webhook.go b/server/webhook.go index 0c284d85..c16049f6 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -59,11 +59,6 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) { return } - p.API.LogDebug("Received webhook event", - "event", string(webhook.Event), - "body_size", len(b), - ) - if webhook.Event != zoom.EventTypeValidateWebhook { err = p.verifyZoomWebhookSignature(r, b) if err != nil { @@ -85,7 +80,6 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) { case zoom.EventTypeTranscriptCompleted: p.handleTranscriptCompleted(w, r, b) default: - p.API.LogDebug("Received unhandled webhook event type", "event", string(webhook.Event)) w.WriteHeader(http.StatusOK) } } @@ -105,33 +99,27 @@ func (p *Plugin) handleMeetingStarted(w http.ResponseWriter, r *http.Request, bo return } - channelID, appErr := p.fetchChannelForMeeting(meetingID) - if appErr != nil { + entry, appErr := p.getMeetingChannelEntry(meetingID) + if appErr != nil || entry == nil || entry.ChannelID == "" { return } - if channelID == "" { - return - } + channelID := entry.ChannelID - // If a post already exists for this meeting (e.g. created via /zoom start), - // don't create a duplicate. Just update the stored UUID mapping so that + // For ad-hoc meetings (started via /zoom start), a post already exists. + // Don't create a duplicate — just update the stored UUID mapping so that // meeting.ended can find the post later. - if existingPostID, err := p.findMeetingPostByMeetingID(meetingID); err == nil { - p.API.LogDebug("handleMeetingStarted: meeting post already exists, skipping duplicate and storing UUID mapping", - "meeting_id", meetingID, - "existing_post_id", existingPostID, - "webhook_uuid", webhook.Payload.Object.UUID, - ) - if appErr := p.storeMeetingPostID(webhook.Payload.Object.UUID, existingPostID); appErr != nil { - p.API.LogWarn("handleMeetingStarted: failed to store UUID mapping for existing post", - "uuid", webhook.Payload.Object.UUID, - "post_id", existingPostID, - "error", appErr.Error(), - ) + // Subscription meetings should always create a new post. + if !entry.IsSubscription { + if existingPostID, err := p.findMeetingPostByMeetingID(meetingID); err == nil { + if appErr := p.storeMeetingPostID(webhook.Payload.Object.UUID, existingPostID); appErr != nil { + p.API.LogWarn("failed to store UUID mapping for existing post", + "error", appErr.Error(), + ) + } + w.WriteHeader(http.StatusOK) + return } - w.WriteHeader(http.StatusOK) - return } botUser, appErr := p.API.GetUser(p.botUserID) @@ -159,41 +147,27 @@ func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body webhookMeetingID := webhook.Payload.Object.ID webhookUUID := webhook.Payload.Object.UUID - p.API.LogDebug("Handling meeting.ended webhook", - "webhook_meeting_id", webhookMeetingID, - "webhook_uuid", webhookUUID, - ) postID, err := p.fetchMeetingPostID(webhookUUID) if err != nil { - p.API.LogWarn("Could not find meeting post by UUID, attempting lookup by stored UUID on post", - "webhook_uuid", webhookUUID, - "error", err.Error(), - ) - // The UUID Zoom sends at meeting.ended can differ from the one at creation // (e.g. PMI meetings, or recurring meetings get a new UUID per occurrence). // Fall back to finding the post via the meeting_channel mapping and recent posts. meetingIDInt, atoiErr := strconv.Atoi(webhookMeetingID) if atoiErr != nil { - p.API.LogWarn("Could not parse meeting ID from webhook for fallback lookup", - "webhook_meeting_id", webhookMeetingID, - "error", atoiErr.Error(), - ) http.Error(w, "meeting post not found", http.StatusNotFound) return } postID, err = p.findMeetingPostByMeetingID(meetingIDInt) if err != nil { - p.API.LogWarn("Fallback meeting post lookup also failed", + p.API.LogWarn("could not find meeting post", "meeting_id", meetingIDInt, "error", err.Error(), ) http.Error(w, "meeting post not found", http.StatusNotFound) return } - p.API.LogDebug("Found meeting post via fallback lookup", "post_id", postID, "meeting_id", meetingIDInt) } post, err := p.client.Post.GetPost(postID) @@ -203,16 +177,7 @@ func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body return } - p.API.LogDebug("Found meeting post", - "post_id", post.Id, - "channel_id", post.ChannelId, - "meeting_status", post.Props["meeting_status"], - "meeting_id_prop", post.Props["meeting_id"], - "meeting_uuid_prop", post.Props["meeting_uuid"], - ) - if post.Props["meeting_status"] == zoom.WebhookStatusEnded { - p.API.LogDebug("Meeting post already marked as ended, skipping update", "post_id", post.Id) w.WriteHeader(http.StatusOK) return } @@ -253,8 +218,6 @@ func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body return } - p.API.LogDebug("Successfully updated meeting post to ended", "post_id", post.Id) - // NOTE: We intentionally do NOT delete the meeting_channel mapping here. // Recording and transcript webhooks arrive after meeting.ended and need // the mapping to locate the post. The entry is small and gets overwritten @@ -318,12 +281,6 @@ func (p *Plugin) findMeetingPostByMeetingIDWithFilter(meetingID int, activeOnly func (p *Plugin) resolveRecordingMeetingPost(webhookUUID string, meetingID int) (*model.Post, error) { postID, err := p.fetchMeetingPostID(webhookUUID) if err != nil { - p.API.LogDebug("UUID-based post lookup failed, trying meeting ID fallback", - "webhook_uuid", webhookUUID, - "meeting_id", meetingID, - "error", err.Error(), - ) - // Recording/transcript webhooks arrive after meeting.ended, so the post // is already marked ENDED. Use activeOnly=false to include ended posts. postID, err = p.findMeetingPostByMeetingIDWithFilter(meetingID, false) From 59e778a96c9140a22e531a9cab8963be25b6e56b Mon Sep 17 00:00:00 2001 From: avasconcelos114 Date: Thu, 26 Feb 2026 16:14:07 +0200 Subject: [PATCH 41/52] Refactoring for readability and final bug fixes - Added user ID checks when running subscription commands - Added map for subscription commands that take a meeting ID value - Centralized logic to downloading files from zoom --- server/command.go | 60 +++++++++++++------------ server/webhook.go | 110 +++++++++++++++++----------------------------- 2 files changed, 73 insertions(+), 97 deletions(-) diff --git a/server/command.go b/server/command.go index 4418d0d5..7ce9cb2a 100644 --- a/server/command.go +++ b/server/command.go @@ -260,35 +260,38 @@ func (p *Plugin) runConnectCommand(user *model.User, extra *model.CommandArgs) ( return oauthMsg, nil } +// subscriptionMeetingIDActions maps subscription actions that require a meeting ID +// to their handler functions. Add new actions here to extend the command. +var subscriptionMeetingIDActions = map[string]func(p *Plugin, user *model.User, args *model.CommandArgs, meetingID int) (string, error){ + subscriptionActionAdd: (*Plugin).runSubscribeCommand, + subscriptionActionRemove: (*Plugin).runUnsubscribeCommand, +} + func (p *Plugin) runSubscriptionCommand(args *model.CommandArgs, params []string, user *model.User) (string, error) { if len(params) == 0 { return "Please specify a subscription action: `add`, `remove`, or `list`.\nUsage: `/zoom subscription [action] [meetingID]`", nil } - switch params[0] { - case subscriptionActionAdd: - if len(params) < 2 { - return "Please specify a meeting ID. Usage: `/zoom subscription add [meetingID]`", nil - } - meetingID, err := strconv.Atoi(params[1]) - if err != nil { - return "Invalid meeting ID. Please provide a numeric meeting ID.", nil - } - return p.runSubscribeCommand(user, args, meetingID) - case subscriptionActionRemove: - if len(params) < 2 { - return "Please specify a meeting ID. Usage: `/zoom subscription remove [meetingID]`", nil - } - meetingID, err := strconv.Atoi(params[1]) - if err != nil { - return "Invalid meeting ID. Please provide a numeric meeting ID.", nil - } - return p.runUnsubscribeCommand(user, args, meetingID) - case subscriptionActionList: + action := params[0] + + if action == subscriptionActionList { return p.runSubscriptionListCommand(args) - default: - return fmt.Sprintf("Unknown subscription action: `%s`. Available actions: `add`, `remove`, `list`.", params[0]), nil } + + handler, ok := subscriptionMeetingIDActions[action] + if !ok { + return fmt.Sprintf("Unknown subscription action: `%s`. Available actions: `add`, `remove`, `list`.", action), nil + } + + if len(params) < 2 { + return fmt.Sprintf("Please specify a meeting ID. Usage: `/zoom subscription %s [meetingID]`", action), nil + } + meetingID, err := strconv.Atoi(params[1]) + if err != nil { + return "Invalid meeting ID. Please provide a numeric meeting ID.", nil + } + + return handler(p, user, args, meetingID) } func (p *Plugin) runSubscriptionListCommand(args *model.CommandArgs) (string, error) { @@ -349,16 +352,17 @@ func (p *Plugin) runUnsubscribeCommand(user *model.User, extra *model.CommandArg return "You do not have permission to unsubscribe from this channel", nil } - _, err := p.getMeeting(user, meetingID) - if err != nil { - return "Can not unsubscribe from meeting: meeting not accesible in zoom", errors.Wrap(err, "meeting not accesible in zoom") + entry, _ := p.getMeetingChannelEntry(meetingID) + if entry == nil || !entry.IsSubscription { + return "No subscription found for this meeting.", nil } - if channelID, appErr := p.fetchChannelForMeeting(meetingID); appErr != nil || channelID == "" { - return "Can not unsubscribe from meeting: meeting not found", errors.New("meeting not found") + if entry.CreatedBy != user.Id { + return "You can only remove subscriptions you created.", nil } + if appErr := p.deleteChannelForMeeting(meetingID); appErr != nil { - return "Can not unsubscribe from meeting: unable to delete the meeting subscription", errors.Wrap(appErr, "cannot unsubscribe from meeting") + return "Unable to delete the meeting subscription.", errors.Wrap(appErr, "cannot unsubscribe from meeting") } return "Channel unsubscribed from meeting.", nil diff --git a/server/webhook.go b/server/webhook.go index c16049f6..4fd24edf 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -84,7 +84,7 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) { } } -func (p *Plugin) handleMeetingStarted(w http.ResponseWriter, r *http.Request, body []byte) { +func (p *Plugin) handleMeetingStarted(w http.ResponseWriter, _ *http.Request, body []byte) { var webhook zoom.MeetingWebhook if err := json.Unmarshal(body, &webhook); err != nil { p.API.LogError("Error unmarshaling meeting webhook", "err", err.Error()) @@ -137,7 +137,7 @@ func (p *Plugin) handleMeetingStarted(w http.ResponseWriter, r *http.Request, bo p.trackMeetingType(p.botUserID, false) } -func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, r *http.Request, body []byte) { +func (p *Plugin) handleMeetingEnded(w http.ResponseWriter, _ *http.Request, body []byte) { var webhook zoom.MeetingWebhook if err := json.Unmarshal(body, &webhook); err != nil { p.client.Log.Error("Error unmarshalling meeting webhook", "err", err.Error()) @@ -297,73 +297,79 @@ func (p *Plugin) resolveRecordingMeetingPost(webhookUUID string, meetingID int) return post, nil } -func (p *Plugin) handleTranscript(recording zoom.RecordingFile, postID, channelID, downloadToken string) error { - request, err := http.NewRequest(http.MethodGet, recording.DownloadURL, nil) +// downloadZoomFile fetches a file from Zoom using the given download token, +// retrying up to maxRetries times on failure, then uploads it to the channel. +func (p *Plugin) downloadZoomFile(downloadURL, downloadToken, channelID, filename string, maxRetries int) (*model.FileInfo, error) { + request, err := http.NewRequest(http.MethodGet, downloadURL, nil) if err != nil { - p.API.LogWarn("Unable to get the transcription", "err", err) - return err + return nil, err } request.Header.Set("Authorization", bearerString+downloadToken) - retries := 5 var response *http.Response - for retries > 0 { + for attempt := 0; attempt <= maxRetries; attempt++ { + if attempt > 0 { + time.Sleep(1 * time.Second) + } response, err = http.DefaultClient.Do(request) if err != nil { - p.API.LogWarn("Unable to get the transcription", "err", err) - time.Sleep(1 * time.Second) - retries-- continue } - if response.StatusCode != http.StatusOK { response.Body.Close() - p.API.LogWarn("Unable to get the transcription", "err", "bad status code "+strconv.Itoa(response.StatusCode)) - time.Sleep(1 * time.Second) - retries-- + response = nil continue } break } if response == nil { - p.API.LogWarn("Unable to get the transcription", "err", "response is nil") - return err + if err != nil { + return nil, err + } + return nil, errors.New("download failed with non-200 status") } - defer response.Body.Close() - transcriptionBytes, err := io.ReadAll(response.Body) + + data, err := io.ReadAll(response.Body) if err != nil { - p.API.LogWarn("Unable to get the transcription", "err", err) - return err + return nil, err } - fileInfo, appErr := p.API.UploadFile(transcriptionBytes, channelID, "transcription.txt") + + fileInfo, appErr := p.API.UploadFile(data, channelID, filename) if appErr != nil { - p.API.LogWarn("Unable to save transcription file to the channel", "err", appErr) - return appErr + return nil, appErr + } + + return fileInfo, nil +} + +func (p *Plugin) handleTranscript(recording zoom.RecordingFile, postID, channelID, downloadToken string) error { + fileInfo, err := p.downloadZoomFile(recording.DownloadURL, downloadToken, channelID, "transcription.txt", 5) + if err != nil { + p.API.LogWarn("Unable to download transcription", "err", err.Error()) + return err } + newPost := &model.Post{ UserId: p.botUserID, ChannelId: channelID, RootId: postID, Message: "Here's the zoom meeting transcription", - FileIds: []string{}, + FileIds: []string{fileInfo.Id}, Type: "custom_zoom_transcript", } - - newPost.FileIds = append(newPost.FileIds, fileInfo.Id) newPost.AddProp("captions", []any{map[string]any{"file_id": fileInfo.Id}}) - _, appErr = p.API.CreatePost(newPost) - if appErr != nil { - p.API.LogWarn("Could not update the post", "err", appErr) + if _, appErr := p.API.CreatePost(newPost); appErr != nil { + p.API.LogWarn("Could not create transcription post", "err", appErr.Error()) return appErr } return nil } -func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Request, body []byte) { +func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, _ *http.Request, body []byte) { var webhook zoom.RecordingWebhook if err := json.Unmarshal(body, &webhook); err != nil { p.API.LogError("Error unmarshaling meeting webhook", "err", err.Error()) @@ -398,8 +404,7 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, r *http.Reques } } -func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request, body []byte) { - +func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, _ *http.Request, body []byte) { var webhook zoom.RecordingWebhook if err := json.Unmarshal(body, &webhook); err != nil { p.API.LogError("handleRecordingCompleted: failed to unmarshal", "err", err.Error()) @@ -453,18 +458,11 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request } } - if newPost.Message != "" { - _, appErr := p.API.CreatePost(newPost) - if appErr != nil { + if newPost.Message != "" || len(newPost.FileIds) > 0 { + if _, appErr := p.API.CreatePost(newPost); appErr != nil { p.API.LogWarn("handleRecordingCompleted: could not create post", "err", appErr) return } - } else if len(newPost.FileIds) > 0 { - _, appErr := p.API.CreatePost(newPost) - if appErr != nil { - p.API.LogWarn("handleRecordingCompleted: could not create chat post", "err", appErr) - return - } } } @@ -475,33 +473,7 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, r *http.Request } func (p *Plugin) downloadAndUploadChat(recording zoom.RecordingFile, downloadToken, channelID string) (*model.FileInfo, error) { - request, err := http.NewRequest(http.MethodGet, recording.DownloadURL, nil) - if err != nil { - p.API.LogWarn("Unable to get the chat", "err", err) - return nil, err - } - request.Header.Set("Authorization", bearerString+downloadToken) - - response, err := http.DefaultClient.Do(request) - if err != nil { - p.API.LogWarn("Unable to get the chat", "err", err) - return nil, err - } - defer response.Body.Close() - - chat, err := io.ReadAll(response.Body) - if err != nil { - p.API.LogWarn("Unable to get the chat", "err", err) - return nil, err - } - - fileInfo, appErr := p.API.UploadFile(chat, channelID, "Chat-history.txt") - if appErr != nil { - p.API.LogWarn("Unable to upload the chat file", "err", appErr) - return nil, appErr - } - - return fileInfo, nil + return p.downloadZoomFile(recording.DownloadURL, downloadToken, channelID, "Chat-history.txt", 0) } func (p *Plugin) verifyMattermostWebhookSecret(r *http.Request) bool { From c5ae8e9cda97e29d4db626ee88ab52f734e82b2d Mon Sep 17 00:00:00 2001 From: avasconcelos114 Date: Thu, 26 Feb 2026 17:02:39 +0200 Subject: [PATCH 42/52] Adding domain check so we only trust the ones configured in the plugin settings --- server/webhook.go | 27 ++++++++++++++++++++++++++- server/webhook_test.go | 10 ++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/server/webhook.go b/server/webhook.go index 4fd24edf..45667eba 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -13,6 +13,7 @@ import ( "io" "math" "net/http" + "net/url" "strconv" "strings" "time" @@ -297,10 +298,34 @@ func (p *Plugin) resolveRecordingMeetingPost(webhookUUID string, meetingID int) return post, nil } +func (p *Plugin) isZoomDownloadURL(rawURL string) bool { + parsed, err := url.Parse(rawURL) + if err != nil || parsed.Host == "" { + return false + } + host := strings.ToLower(parsed.Hostname()) + + for _, trusted := range []string{p.getZoomURL(), p.getZoomAPIURL()} { + trustedURL, err := url.Parse(trusted) + if err != nil || trustedURL.Host == "" { + continue + } + if strings.ToLower(trustedURL.Hostname()) == host { + return true + } + } + + return false +} + // downloadZoomFile fetches a file from Zoom using the given download token, // retrying up to maxRetries times on failure, then uploads it to the channel. func (p *Plugin) downloadZoomFile(downloadURL, downloadToken, channelID, filename string, maxRetries int) (*model.FileInfo, error) { - request, err := http.NewRequest(http.MethodGet, downloadURL, nil) + if !p.isZoomDownloadURL(downloadURL) { + return nil, errors.Errorf("refusing to download from untrusted URL: %s", downloadURL) + } + + request, err := http.NewRequest(http.MethodGet, downloadURL, nil) // #nosec G107 -- URL is validated by isZoomDownloadURL above if err != nil { return nil, err } diff --git a/server/webhook_test.go b/server/webhook_test.go index ccabc432..1707e37e 100644 --- a/server/webhook_test.go +++ b/server/webhook_test.go @@ -356,7 +356,6 @@ func TestWebhookBodyTooLarge(t *testing.T) { func TestWebhookHandleTranscriptCompleted(t *testing.T) { api := &plugintest.API{} p := Plugin{} - p.setConfiguration(testConfig) httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) @@ -364,6 +363,10 @@ func TestWebhookHandleTranscriptCompleted(t *testing.T) { })) defer httpServer.Close() + cfg := *testConfig + cfg.ZoomURL = httpServer.URL + p.setConfiguration(&cfg) + oldDefaultClient := http.DefaultClient http.DefaultClient = httpServer.Client() defer func() { @@ -437,7 +440,6 @@ func TestWebhookHandleTranscriptCompleted(t *testing.T) { func TestWebhookHandleRecordingCompleted(t *testing.T) { api := &plugintest.API{} p := Plugin{} - p.setConfiguration(testConfig) httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) @@ -445,6 +447,10 @@ func TestWebhookHandleRecordingCompleted(t *testing.T) { })) defer httpServer.Close() + cfg := *testConfig + cfg.ZoomURL = httpServer.URL + p.setConfiguration(&cfg) + oldDefaultClient := http.DefaultClient http.DefaultClient = httpServer.Client() defer func() { From fe4ecbccc53728fcc0fdc3b56699d824c4cb9e58 Mon Sep 17 00:00:00 2001 From: avasconcelos114 Date: Fri, 27 Feb 2026 19:29:24 +0200 Subject: [PATCH 43/52] Applying fixes for AI review findings --- server/command.go | 8 +++++++- server/http.go | 6 ++++-- server/store.go | 19 ++++++++++++++++--- server/webhook.go | 9 +++++++++ webapp/.eslintrc.json | 2 +- webapp/src/components/post_type_chat.tsx | 12 +++++++----- .../post_type_zoom/post_type_zoom.jsx | 7 +++++-- webapp/tsconfig.json | 2 +- webapp/webpack.config.js | 2 +- 9 files changed, 51 insertions(+), 16 deletions(-) diff --git a/server/command.go b/server/command.go index 7ce9cb2a..c1768cd5 100644 --- a/server/command.go +++ b/server/command.go @@ -352,10 +352,16 @@ func (p *Plugin) runUnsubscribeCommand(user *model.User, extra *model.CommandArg return "You do not have permission to unsubscribe from this channel", nil } - entry, _ := p.getMeetingChannelEntry(meetingID) + entry, appErr := p.getMeetingChannelEntry(meetingID) + if appErr != nil { + return "Unable to load the meeting subscription.", errors.Wrap(appErr, "cannot fetch meeting subscription") + } if entry == nil || !entry.IsSubscription { return "No subscription found for this meeting.", nil } + if entry.ChannelID != extra.ChannelId { + return "This meeting is subscribed to a different channel.", nil + } if entry.CreatedBy != user.Id { return "You can only remove subscriptions you created.", nil diff --git a/server/http.go b/server/http.go index 0912144c..41896a88 100644 --- a/server/http.go +++ b/server/http.go @@ -498,8 +498,10 @@ func (p *Plugin) postMeeting(creator *model.User, meetingID int, meetingUUID str return appErr } - if appErr = p.storeMeetingPostID(meetingUUID, createdPost.Id); appErr != nil { - p.API.LogWarn("failed to store meeting post ID", "error", appErr.Error()) + if meetingUUID != "" { + if appErr = p.storeMeetingPostID(meetingUUID, createdPost.Id); appErr != nil { + p.API.LogWarn("failed to store meeting post ID", "error", appErr.Error()) + } } if err := p.storeChannelForMeeting(meetingID, channelID); err != nil { diff --git a/server/store.go b/server/store.go index ceba473f..f8bd4ff0 100644 --- a/server/store.go +++ b/server/store.go @@ -188,6 +188,17 @@ func meetingChannelKVKey(meetingID int) string { } func (p *Plugin) storeSubscriptionForMeeting(meetingID int, channelID, userID string) error { + existing, appErr := p.getMeetingChannelEntry(meetingID) + if appErr != nil { + return appErr + } + if existing != nil && existing.IsSubscription { + if existing.ChannelID == channelID && existing.CreatedBy == userID { + return nil + } + return errors.New("meeting already has an existing subscription") + } + entry := meetingChannelEntry{ ChannelID: channelID, IsSubscription: true, @@ -206,9 +217,11 @@ func (p *Plugin) storeSubscriptionForMeeting(meetingID int, channelID, userID st func (p *Plugin) storeChannelForMeeting(meetingID int, channelID string) error { key := meetingChannelKVKey(meetingID) - // If a subscription entry already exists for this meeting, don't overwrite - // it with an ad-hoc entry. - if existing, _ := p.getMeetingChannelEntry(meetingID); existing != nil && existing.IsSubscription { + existing, appErr := p.getMeetingChannelEntry(meetingID) + if appErr != nil { + return appErr + } + if existing != nil && existing.IsSubscription { return nil } diff --git a/server/webhook.go b/server/webhook.go index 45667eba..7d64e3e8 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -303,6 +303,9 @@ func (p *Plugin) isZoomDownloadURL(rawURL string) bool { if err != nil || parsed.Host == "" { return false } + if !strings.EqualFold(parsed.Scheme, "https") { + return false + } host := strings.ToLower(parsed.Hostname()) for _, trusted := range []string{p.getZoomURL(), p.getZoomAPIURL()} { @@ -310,6 +313,9 @@ func (p *Plugin) isZoomDownloadURL(rawURL string) bool { if err != nil || trustedURL.Host == "" { continue } + if !strings.EqualFold(trustedURL.Scheme, "https") { + continue + } if strings.ToLower(trustedURL.Hostname()) == host { return true } @@ -419,6 +425,7 @@ func (p *Plugin) handleTranscriptCompleted(w http.ResponseWriter, _ *http.Reques if lastTranscriptionIdx != -1 { err := p.handleTranscript(webhook.Payload.Object.RecordingFiles[lastTranscriptionIdx], post.Id, post.ChannelId, webhook.DownloadToken) if err != nil { + http.Error(w, "failed to process transcript", http.StatusInternalServerError) return } } @@ -468,6 +475,7 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, _ *http.Request fileInfo, chatErr := p.downloadAndUploadChat(recording, webhook.DownloadToken, post.ChannelId) if chatErr != nil { p.API.LogWarn("handleRecordingCompleted: failed to download/upload chat", "error", chatErr.Error()) + http.Error(w, "failed to process recording webhook", http.StatusInternalServerError) return } @@ -486,6 +494,7 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, _ *http.Request if newPost.Message != "" || len(newPost.FileIds) > 0 { if _, appErr := p.API.CreatePost(newPost); appErr != nil { p.API.LogWarn("handleRecordingCompleted: could not create post", "err", appErr) + http.Error(w, "failed to create recording post", http.StatusInternalServerError) return } } diff --git a/webapp/.eslintrc.json b/webapp/.eslintrc.json index 4a542e2c..37bc62a4 100644 --- a/webapp/.eslintrc.json +++ b/webapp/.eslintrc.json @@ -644,7 +644,7 @@ }, "overrides": [ { - "files": ["*.ts", "*.tsx"], + "files": ["**/*.ts", "**/*.tsx"], "parser": "@typescript-eslint/parser", "parserOptions": { "project": "./tsconfig.json" diff --git a/webapp/src/components/post_type_chat.tsx b/webapp/src/components/post_type_chat.tsx index f8196d1a..013b8772 100644 --- a/webapp/src/components/post_type_chat.tsx +++ b/webapp/src/components/post_type_chat.tsx @@ -12,18 +12,20 @@ type Props = { }; const renderPostWithMarkdown = (msg: string) => { - const windowAny: any = window; - const {formatText, messageHtmlToComponent} = windowAny.PostUtils; + const postUtils = (window as any).PostUtils; + if (!postUtils?.formatText || !postUtils?.messageHtmlToComponent) { + return {msg}; + } - return messageHtmlToComponent( - formatText(msg, {}), + return postUtils.messageHtmlToComponent( + postUtils.formatText(msg, {}), false, ); }; export const PostTypeChat = (props: Props) => { return ( -
+
{renderPostWithMarkdown(props.post.message)} = startTime ? endTime - startTime : 0; + const length = Math.ceil(durationMs / 60000); content = (
diff --git a/webapp/tsconfig.json b/webapp/tsconfig.json index 2a66c7eb..405ad4c1 100644 --- a/webapp/tsconfig.json +++ b/webapp/tsconfig.json @@ -6,7 +6,7 @@ "dom.iterable", "esnext" ], - "jsx": "react", + "jsx": "react-jsx", "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, diff --git a/webapp/webpack.config.js b/webapp/webpack.config.js index 823d06f1..7be91e0c 100644 --- a/webapp/webpack.config.js +++ b/webapp/webpack.config.js @@ -36,7 +36,7 @@ module.exports = { plugins: [ new webpack.DefinePlugin({ // eslint-disable-next-line no-process-env - 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), }), ], externals: { From f52c56b45428d632a941fcf6938e52b974d0ce39 Mon Sep 17 00:00:00 2001 From: avasconcelos114 Date: Fri, 27 Feb 2026 19:40:22 +0200 Subject: [PATCH 44/52] Fixing tests to match scheme validation --- server/webhook_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/webhook_test.go b/server/webhook_test.go index 1707e37e..32a21218 100644 --- a/server/webhook_test.go +++ b/server/webhook_test.go @@ -357,7 +357,7 @@ func TestWebhookHandleTranscriptCompleted(t *testing.T) { api := &plugintest.API{} p := Plugin{} - httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + httpServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) _, _ = w.Write([]byte(r.URL.Path)) })) @@ -441,7 +441,7 @@ func TestWebhookHandleRecordingCompleted(t *testing.T) { api := &plugintest.API{} p := Plugin{} - httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + httpServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) _, _ = w.Write([]byte(r.URL.Path)) })) From 0c6f2a8897af39639caac8b9ddcc1472d246e06a Mon Sep 17 00:00:00 2001 From: avasconcelos114 Date: Fri, 6 Mar 2026 15:16:28 +0200 Subject: [PATCH 45/52] Applying fixes for PR review - Allowing System Admins to delete any subscription - Added limit of 10MB limit for transcript uploads (to be reviewed) - Improving subscription system to use an index for better scalability - Other misc fixes --- server/command.go | 6 +- server/plugin.go | 13 +++ server/store.go | 108 ++++++++++++++------ server/webhook.go | 15 +-- server/webhook_test.go | 12 +-- webapp/src/components/ai_summary_button.tsx | 14 ++- 6 files changed, 114 insertions(+), 54 deletions(-) diff --git a/server/command.go b/server/command.go index c1768cd5..8c34b45e 100644 --- a/server/command.go +++ b/server/command.go @@ -363,7 +363,7 @@ func (p *Plugin) runUnsubscribeCommand(user *model.User, extra *model.CommandArg return "This meeting is subscribed to a different channel.", nil } - if entry.CreatedBy != user.Id { + if entry.CreatedBy != user.Id && !user.IsSystemAdmin() { return "You can only remove subscriptions you created.", nil } @@ -371,6 +371,10 @@ func (p *Plugin) runUnsubscribeCommand(user *model.User, extra *model.CommandArg return "Unable to delete the meeting subscription.", errors.Wrap(appErr, "cannot unsubscribe from meeting") } + if err := p.removeFromSubscriptionIndex(entry.CreatedBy, meetingID); err != nil { + p.API.LogWarn("failed to update subscription index on removal", "error", err.Error()) + } + return "Channel unsubscribed from meeting.", nil } diff --git a/server/plugin.go b/server/plugin.go index ee49ba21..50e6c9f6 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -5,9 +5,11 @@ package main import ( "fmt" + "net/http" "os" "path/filepath" "sync" + "time" "github.com/pkg/errors" "golang.org/x/oauth2" @@ -29,8 +31,14 @@ const ( falseString = "false" zoomProviderName = "Zoom" + + defaultDownloadTimeout = 5 * time.Minute ) +var defaultDownloadClient = &http.Client{ + Timeout: defaultDownloadTimeout, +} + type Plugin struct { plugin.MattermostPlugin @@ -50,11 +58,16 @@ type Plugin struct { telemetryClient telemetry.Client tracker telemetry.Tracker + + // downloadClient is the HTTP client used for downloading files from Zoom. + // Initialized in OnActivate; tests may override it before exercising handlers. + downloadClient *http.Client } // OnActivate checks if the configurations is valid and ensures the bot account exists func (p *Plugin) OnActivate() error { p.client = pluginapi.NewClient(p.API, p.Driver) + p.downloadClient = defaultDownloadClient config := p.getConfiguration() if err := config.IsValid(p.isCloudLicense()); err != nil { diff --git a/server/store.go b/server/store.go index f8bd4ff0..9ebd959a 100644 --- a/server/store.go +++ b/server/store.go @@ -7,7 +7,6 @@ import ( "encoding/json" "fmt" "net/url" - "strings" "github.com/mattermost/mattermost/server/public/model" "github.com/pkg/errors" @@ -211,6 +210,11 @@ func (p *Plugin) storeSubscriptionForMeeting(meetingID int, channelID, userID st if appErr := p.API.KVSet(meetingChannelKVKey(meetingID), data); appErr != nil { return appErr } + + if err := p.addToSubscriptionIndex(userID, meetingID); err != nil { + p.API.LogWarn("failed to update subscription index", "error", err.Error()) + } + return nil } @@ -279,47 +283,87 @@ func (p *Plugin) deleteChannelForMeeting(meetingID int) error { return p.client.KV.Delete(key) } -const kvListPerPage = 100 +const subscriptionIndexKey = "subscription_index_" -func (p *Plugin) listAllMeetingSubscriptions(userID string) (map[string]string, error) { - subscriptions := make(map[string]string) +type subscriptionIndex struct { + MeetingIDs []int `json:"meeting_ids"` +} - for page := 0; ; page++ { - keys, appErr := p.API.KVList(page, kvListPerPage) - if appErr != nil { - return nil, errors.New(appErr.Message) - } +func subscriptionIndexKVKey(userID string) string { + return subscriptionIndexKey + userID +} - for _, key := range keys { - if !strings.HasPrefix(key, meetingChannelKey) { - continue - } +func (p *Plugin) getSubscriptionIndex(userID string) (*subscriptionIndex, error) { + raw, appErr := p.API.KVGet(subscriptionIndexKVKey(userID)) + if appErr != nil { + return nil, appErr + } + if raw == nil { + return &subscriptionIndex{}, nil + } + var idx subscriptionIndex + if err := json.Unmarshal(raw, &idx); err != nil { + return &subscriptionIndex{}, nil + } + return &idx, nil +} - raw, kvErr := p.API.KVGet(key) - if kvErr != nil || raw == nil { - continue - } +func (p *Plugin) saveSubscriptionIndex(userID string, idx *subscriptionIndex) error { + data, err := json.Marshal(idx) + if err != nil { + return err + } + if appErr := p.API.KVSet(subscriptionIndexKVKey(userID), data); appErr != nil { + return appErr + } + return nil +} - var entry meetingChannelEntry - if err := json.Unmarshal(raw, &entry); err != nil || entry.ChannelID == "" { - continue - } +func (p *Plugin) addToSubscriptionIndex(userID string, meetingID int) error { + idx, err := p.getSubscriptionIndex(userID) + if err != nil { + return err + } + for _, id := range idx.MeetingIDs { + if id == meetingID { + return nil + } + } + idx.MeetingIDs = append(idx.MeetingIDs, meetingID) + return p.saveSubscriptionIndex(userID, idx) +} - if !entry.IsSubscription { - continue - } +func (p *Plugin) removeFromSubscriptionIndex(userID string, meetingID int) error { + idx, err := p.getSubscriptionIndex(userID) + if err != nil { + return err + } + filtered := idx.MeetingIDs[:0] + for _, id := range idx.MeetingIDs { + if id != meetingID { + filtered = append(filtered, id) + } + } + idx.MeetingIDs = filtered + return p.saveSubscriptionIndex(userID, idx) +} - if entry.CreatedBy != userID { - continue - } +func (p *Plugin) listAllMeetingSubscriptions(userID string) (map[string]string, error) { + idx, err := p.getSubscriptionIndex(userID) + if err != nil { + return nil, err + } - meetingID := strings.TrimPrefix(key, meetingChannelKey) - subscriptions[meetingID] = entry.ChannelID + subscriptions := make(map[string]string) + for _, meetingID := range idx.MeetingIDs { + entry, appErr := p.getMeetingChannelEntry(meetingID) + if appErr != nil || entry == nil || !entry.IsSubscription { + continue } - - if len(keys) < kvListPerPage { - break + if entry.CreatedBy != userID { + continue } + subscriptions[fmt.Sprintf("%d", meetingID)] = entry.ChannelID } return subscriptions, nil diff --git a/server/webhook.go b/server/webhook.go index 7d64e3e8..3a72d050 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -26,6 +26,7 @@ import ( const bearerString = "Bearer " const maxWebhookBodySize = 1 << 20 // 1MB +const maxDownloadSize = 10 << 20 // 10MB func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) { if !p.verifyMattermostWebhookSecret(r) { @@ -342,7 +343,7 @@ func (p *Plugin) downloadZoomFile(downloadURL, downloadToken, channelID, filenam if attempt > 0 { time.Sleep(1 * time.Second) } - response, err = http.DefaultClient.Do(request) + response, err = p.downloadClient.Do(request) if err != nil { continue } @@ -362,7 +363,7 @@ func (p *Plugin) downloadZoomFile(downloadURL, downloadToken, channelID, filenam } defer response.Body.Close() - data, err := io.ReadAll(response.Body) + data, err := io.ReadAll(io.LimitReader(response.Body, maxDownloadSize)) if err != nil { return nil, err } @@ -472,7 +473,7 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, _ *http.Request } for _, recording := range recordingGroup { if recording.RecordingType == zoom.RecordingTypeChat { - fileInfo, chatErr := p.downloadAndUploadChat(recording, webhook.DownloadToken, post.ChannelId) + fileInfo, chatErr := p.downloadZoomFile(recording.DownloadURL, webhook.DownloadToken, post.ChannelId, "Chat-history.txt", 5) if chatErr != nil { p.API.LogWarn("handleRecordingCompleted: failed to download/upload chat", "error", chatErr.Error()) http.Error(w, "failed to process recording webhook", http.StatusInternalServerError) @@ -483,6 +484,10 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, _ *http.Request newPost.AddProp("captions", []any{map[string]any{"file_id": fileInfo.Id}}) newPost.Type = "custom_zoom_chat" } else if strings.EqualFold(recording.FileType, zoom.RecordingFileTypeMP4) && recording.PlayURL != "" { + if !p.isZoomDownloadURL(recording.PlayURL) { + p.API.LogWarn("handleRecordingCompleted: refusing to post untrusted play URL", "url", recording.PlayURL) + continue + } msg := "Here's the zoom meeting recording:\n**Link:** [Meeting Recording](" + recording.PlayURL + ")" if webhook.Payload.Object.Password != "" { msg += "\n**Password:** `" + webhook.Payload.Object.Password + "`" @@ -506,10 +511,6 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, _ *http.Request } } -func (p *Plugin) downloadAndUploadChat(recording zoom.RecordingFile, downloadToken, channelID string) (*model.FileInfo, error) { - return p.downloadZoomFile(recording.DownloadURL, downloadToken, channelID, "Chat-history.txt", 0) -} - func (p *Plugin) verifyMattermostWebhookSecret(r *http.Request) bool { config := p.getConfiguration() return subtle.ConstantTimeCompare([]byte(r.URL.Query().Get("secret")), []byte(config.WebhookSecret)) == 1 diff --git a/server/webhook_test.go b/server/webhook_test.go index 32a21218..82d1a423 100644 --- a/server/webhook_test.go +++ b/server/webhook_test.go @@ -367,11 +367,7 @@ func TestWebhookHandleTranscriptCompleted(t *testing.T) { cfg.ZoomURL = httpServer.URL p.setConfiguration(&cfg) - oldDefaultClient := http.DefaultClient - http.DefaultClient = httpServer.Client() - defer func() { - http.DefaultClient = oldDefaultClient - }() + p.downloadClient = httpServer.Client() api.On("GetLicense").Return(nil) api.On("GetPost", "post-id").Return(&model.Post{Id: "post-id", ChannelId: "channel-id"}, nil) @@ -451,11 +447,7 @@ func TestWebhookHandleRecordingCompleted(t *testing.T) { cfg.ZoomURL = httpServer.URL p.setConfiguration(&cfg) - oldDefaultClient := http.DefaultClient - http.DefaultClient = httpServer.Client() - defer func() { - http.DefaultClient = oldDefaultClient - }() + p.downloadClient = httpServer.Client() api.On("GetLicense").Return(nil) api.On("GetPost", "post-id").Return(&model.Post{Id: "post-id", ChannelId: "channel-id"}, nil) diff --git a/webapp/src/components/ai_summary_button.tsx b/webapp/src/components/ai_summary_button.tsx index d02fed46..0fc09c91 100644 --- a/webapp/src/components/ai_summary_button.tsx +++ b/webapp/src/components/ai_summary_button.tsx @@ -5,6 +5,7 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; import {useSelector} from 'react-redux'; +import type {GlobalState} from '@mattermost/types/store'; import type {Post} from '@mattermost/types/posts'; import styled from 'styled-components'; @@ -13,16 +14,21 @@ import IconAI from 'src/components/ai_icon'; const aiPluginID = 'mattermost-ai'; +type PluginState = GlobalState & { + plugins?: {plugins?: Record}; + [key: string]: unknown; +}; + export const useAIAvailable = () => { - return useSelector((state: any) => Boolean(state.plugins?.plugins?.[aiPluginID])); + return useSelector((state: PluginState) => Boolean(state.plugins?.plugins?.[aiPluginID])); }; export const useCallsPostButtonClicked = () => { - return useSelector((state: any) => { - const aiPluginState = state['plugins-' + aiPluginID]; + return useSelector((state: PluginState) => { + const aiPluginState = state['plugins-' + aiPluginID] as Record | undefined; const handler = aiPluginState?.callsPostButtonClickedTranscription; if (typeof handler === 'function') { - return handler; + return handler as (post: Post) => void; } return null; }); From 3a530eaf856835ebc7458d7effccef8decabba05 Mon Sep 17 00:00:00 2001 From: avasconcelos114 Date: Fri, 6 Mar 2026 15:35:33 +0200 Subject: [PATCH 46/52] Fixing URL validation so zoom subdomains arent blocked --- server/webhook.go | 3 ++- server/webhook_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/server/webhook.go b/server/webhook.go index 3a72d050..a194f1df 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -317,7 +317,8 @@ func (p *Plugin) isZoomDownloadURL(rawURL string) bool { if !strings.EqualFold(trustedURL.Scheme, "https") { continue } - if strings.ToLower(trustedURL.Hostname()) == host { + trustedHost := strings.ToLower(trustedURL.Hostname()) + if host == trustedHost || strings.HasSuffix(host, "."+trustedHost) { return true } } diff --git a/server/webhook_test.go b/server/webhook_test.go index 82d1a423..ae5d00d6 100644 --- a/server/webhook_test.go +++ b/server/webhook_test.go @@ -504,3 +504,32 @@ func TestWebhookHandleRecordingCompleted(t *testing.T) { api.AssertExpectations(t) } + +func TestIsZoomDownloadURL(t *testing.T) { + p := Plugin{} + p.setConfiguration(testConfig) + + tests := []struct { + name string + url string + want bool + }{ + {"exact zoom.us host", "https://zoom.us/rec/download/abc", true}, + {"org subdomain of zoom.us", "https://mattermost.zoom.us/rec/webhook_download/abc", true}, + {"deep subdomain of zoom.us", "https://a.b.zoom.us/rec/download/abc", true}, + {"exact api.zoom.us host", "https://api.zoom.us/v2/recordings/download", true}, + {"subdomain of api.zoom.us", "https://sub.api.zoom.us/v2/download", true}, + {"http scheme rejected", "http://zoom.us/rec/download/abc", false}, + {"unrelated host", "https://random.com/rec/download/abc", false}, + {"suffix trick (notzoom.us)", "https://notzoom.us/rec/download/abc", false}, + {"empty string", "", false}, + {"no host", "https:///path", false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := p.isZoomDownloadURL(tc.url) + require.Equal(t, tc.want, got, "isZoomDownloadURL(%q)", tc.url) + }) + } +} From b01c85970fb720f5e3be8222cb9f54979d4cc170 Mon Sep 17 00:00:00 2001 From: avasconcelos114 Date: Fri, 6 Mar 2026 15:53:43 +0200 Subject: [PATCH 47/52] Fixing other round of reviews --- server/command.go | 1 + server/plugin_test.go | 3 +- server/store.go | 110 +++++++++++--------- server/webhook.go | 11 +- server/webhook_test.go | 41 ++++++-- webapp/src/components/ai_summary_button.tsx | 48 ++++----- 6 files changed, 133 insertions(+), 81 deletions(-) diff --git a/server/command.go b/server/command.go index 8c34b45e..81c2d028 100644 --- a/server/command.go +++ b/server/command.go @@ -318,6 +318,7 @@ func (p *Plugin) runSubscriptionListCommand(args *model.CommandArgs) (string, er channel, appErr := p.client.Channel.Get(channelID) if appErr != nil { p.client.Log.Error("Unable to get channel for subscription list", "ChannelID", channelID, "Error", appErr.Error()) + sb.WriteString(fmt.Sprintf("| %s | (unknown channel %s) |\n", meetingID, channelID)) continue } sb.WriteString(fmt.Sprintf("| %s | ~%s |\n", meetingID, channel.Name)) diff --git a/server/plugin_test.go b/server/plugin_test.go index 088e839a..334e413a 100644 --- a/server/plugin_test.go +++ b/server/plugin_test.go @@ -131,7 +131,8 @@ func TestPlugin(t *testing.T) { api.On("KVGet", "mmi_botid").Return([]byte(botUserID), nil) api.On("KVGet", "zoomtoken_theuserid").Return(userInfo, nil) - api.On("KVGet", "meeting_channel_234").Return([]byte("thechannelid"), nil) + meetingEntry, _ := json.Marshal(meetingChannelEntry{ChannelID: "thechannelid"}) + api.On("KVGet", "meeting_channel_234").Return(meetingEntry, nil) api.On("KVGet", "zoomtoken_"+botUserID).Return(userInfo, nil) api.On("GetUser", botUserID).Return(&model.User{ Id: botUserID, diff --git a/server/store.go b/server/store.go index 9ebd959a..c0345e3d 100644 --- a/server/store.go +++ b/server/store.go @@ -6,6 +6,7 @@ package main import ( "encoding/json" "fmt" + "net/http" "net/url" "github.com/mattermost/mattermost/server/public/model" @@ -259,7 +260,7 @@ func (p *Plugin) getMeetingChannelEntry(meetingID int) (*meetingChannelEntry, *m "key", key, "error", err.Error(), ) - return nil, nil + return nil, model.NewAppError("getMeetingChannelEntry", "zoom.store.unmarshal_meeting_channel", nil, err.Error(), http.StatusInternalServerError) } if entry.ChannelID == "" { return nil, nil @@ -293,65 +294,80 @@ func subscriptionIndexKVKey(userID string) string { return subscriptionIndexKey + userID } -func (p *Plugin) getSubscriptionIndex(userID string) (*subscriptionIndex, error) { - raw, appErr := p.API.KVGet(subscriptionIndexKVKey(userID)) - if appErr != nil { - return nil, appErr - } - if raw == nil { - return &subscriptionIndex{}, nil - } - var idx subscriptionIndex - if err := json.Unmarshal(raw, &idx); err != nil { - return &subscriptionIndex{}, nil - } - return &idx, nil -} +const subscriptionIndexMaxRetries = 5 -func (p *Plugin) saveSubscriptionIndex(userID string, idx *subscriptionIndex) error { - data, err := json.Marshal(idx) - if err != nil { - return err - } - if appErr := p.API.KVSet(subscriptionIndexKVKey(userID), data); appErr != nil { - return appErr +func (p *Plugin) updateSubscriptionIndex(userID string, mutate func(*subscriptionIndex) *subscriptionIndex) error { + key := subscriptionIndexKVKey(userID) + + for i := 0; i < subscriptionIndexMaxRetries; i++ { + oldRaw, appErr := p.API.KVGet(key) + if appErr != nil { + return appErr + } + + var idx subscriptionIndex + if oldRaw != nil { + _ = json.Unmarshal(oldRaw, &idx) + } + + updated := mutate(&idx) + if updated == nil { + return nil + } + + newRaw, err := json.Marshal(updated) + if err != nil { + return err + } + + ok, appErr := p.API.KVSetWithOptions(key, newRaw, model.PluginKVSetOptions{ + Atomic: true, + OldValue: oldRaw, + }) + if appErr != nil { + return appErr + } + if ok { + return nil + } } - return nil + + return errors.New("updateSubscriptionIndex: too many concurrent updates") } func (p *Plugin) addToSubscriptionIndex(userID string, meetingID int) error { - idx, err := p.getSubscriptionIndex(userID) - if err != nil { - return err - } - for _, id := range idx.MeetingIDs { - if id == meetingID { - return nil + return p.updateSubscriptionIndex(userID, func(idx *subscriptionIndex) *subscriptionIndex { + for _, id := range idx.MeetingIDs { + if id == meetingID { + return nil + } } - } - idx.MeetingIDs = append(idx.MeetingIDs, meetingID) - return p.saveSubscriptionIndex(userID, idx) + idx.MeetingIDs = append(idx.MeetingIDs, meetingID) + return idx + }) } func (p *Plugin) removeFromSubscriptionIndex(userID string, meetingID int) error { - idx, err := p.getSubscriptionIndex(userID) - if err != nil { - return err - } - filtered := idx.MeetingIDs[:0] - for _, id := range idx.MeetingIDs { - if id != meetingID { - filtered = append(filtered, id) + return p.updateSubscriptionIndex(userID, func(idx *subscriptionIndex) *subscriptionIndex { + filtered := make([]int, 0, len(idx.MeetingIDs)) + for _, id := range idx.MeetingIDs { + if id != meetingID { + filtered = append(filtered, id) + } } - } - idx.MeetingIDs = filtered - return p.saveSubscriptionIndex(userID, idx) + idx.MeetingIDs = filtered + return idx + }) } func (p *Plugin) listAllMeetingSubscriptions(userID string) (map[string]string, error) { - idx, err := p.getSubscriptionIndex(userID) - if err != nil { - return nil, err + raw, appErr := p.API.KVGet(subscriptionIndexKVKey(userID)) + if appErr != nil { + return nil, appErr + } + var idx subscriptionIndex + if raw != nil { + _ = json.Unmarshal(raw, &idx) } subscriptions := make(map[string]string) diff --git a/server/webhook.go b/server/webhook.go index a194f1df..d7cd7f42 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -102,7 +102,16 @@ func (p *Plugin) handleMeetingStarted(w http.ResponseWriter, _ *http.Request, bo } entry, appErr := p.getMeetingChannelEntry(meetingID) - if appErr != nil || entry == nil || entry.ChannelID == "" { + if appErr != nil { + p.API.LogWarn("handleMeetingStarted: failed to get meeting channel entry", + "meeting_id", meetingID, + "error", appErr.Error(), + ) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + if entry == nil || entry.ChannelID == "" { + w.WriteHeader(http.StatusOK) return } diff --git a/server/webhook_test.go b/server/webhook_test.go index ae5d00d6..c70e4b45 100644 --- a/server/webhook_test.go +++ b/server/webhook_test.go @@ -85,11 +85,12 @@ func TestHandleMeetingStarted(t *testing.T) { t.Run("successful meeting start", func(t *testing.T) { api := &plugintest.API{} api.On("GetLicense").Return(nil) - api.On("KVGet", "meeting_channel_123").Return([]byte("channel-id"), nil) - api.On("GetUser", "").Return(&model.User{Id: "user-id"}, nil) - api.On("KVGet", "zoomtoken_user-id").Return(nil, &model.AppError{}) + meetingEntry, _ := json.Marshal(meetingChannelEntry{ChannelID: "channel-id"}) + api.On("KVGet", "meeting_channel_123").Return(meetingEntry, nil) + api.On("GetUser", "test-bot-id").Return(&model.User{Id: "test-bot-id"}, nil) + api.On("KVGet", "zoomtoken_test-bot-id").Return(nil, &model.AppError{}) api.On("LogWarn", "could not get the active Zoom client", "error", "could not fetch Zoom OAuth info: must connect user account to Zoom first").Return() - api.On("HasPermissionToChannel", "user-id", "channel-id", mock.AnythingOfType("*model.Permission")).Return(true) + api.On("HasPermissionToChannel", "test-bot-id", "channel-id", mock.AnythingOfType("*model.Permission")).Return(true) api.On("KVSetWithExpiry", "post_meeting_abc", []byte{}, int64(86400)).Return(nil) api.On("KVSetWithExpiry", "meeting_channel_123", mock.AnythingOfType("[]uint8"), int64(adHocMeetingChannelTTL)).Return(nil) api.On("PublishWebSocketEvent", "meeting_started", map[string]interface{}{"meeting_url": "https://zoom.us/j/123"}, mock.AnythingOfType("*model.WebsocketBroadcast")).Return() @@ -98,7 +99,7 @@ func TestHandleMeetingStarted(t *testing.T) { allowFlexibleLogging(api) p.SetAPI(api) p.client = pluginapi.NewClient(api, nil) - p.botUserID = "" + p.botUserID = "test-bot-id" p.tracker = telemetry.NewTracker(nil, "", "", "", "", "", telemetry.NewTrackerConfig(nil), nil) requestBody := `{"payload":{"object": {"id": "123", "uuid": "abc", "topic": "test meeting"}},"event":"meeting.started"}` @@ -144,11 +145,35 @@ func TestHandleMeetingStarted(t *testing.T) { require.Equal(t, http.StatusBadRequest, w.Result().StatusCode) }) - t.Run("channel not found", func(t *testing.T) { + t.Run("channel lookup error returns 500", func(t *testing.T) { api := &plugintest.API{} api.On("GetLicense").Return(nil) - api.On("KVGet", "meeting_channel_123").Return(nil, &model.AppError{}) - api.On("KVSetWithExpiry", "post_meeting_123-abc", []byte{}, int64(86400)).Return(nil) + api.On("KVGet", "meeting_channel_123").Return(nil, &model.AppError{Message: "kv error"}) + allowFlexibleLogging(api) + p.SetAPI(api) + + requestBody := `{"payload":{"object": {"id": "123", "uuid": "123-abc"}},"event":"meeting.started"}` + w := httptest.NewRecorder() + reqBody := io.NopCloser(bytes.NewBufferString(requestBody)) + request := httptest.NewRequest("POST", "/webhook?secret=webhooksecret", reqBody) + request.Header.Add("Content-Type", "application/json") + + ts := fmt.Sprintf("%d", time.Now().Unix()) + h := hmac.New(sha256.New, []byte(testConfig.ZoomWebhookSecret)) + _, _ = h.Write([]byte("v0:" + ts + ":" + requestBody)) + signature := "v0=" + hex.EncodeToString(h.Sum(nil)) + + request.Header.Add("x-zm-signature", signature) + request.Header.Add("x-zm-request-timestamp", ts) + + p.ServeHTTP(&plugin.Context{}, w, request) + require.Equal(t, http.StatusInternalServerError, w.Result().StatusCode) + }) + + t.Run("no channel entry returns 200", func(t *testing.T) { + api := &plugintest.API{} + api.On("GetLicense").Return(nil) + api.On("KVGet", "meeting_channel_123").Return(nil, (*model.AppError)(nil)) allowFlexibleLogging(api) p.SetAPI(api) diff --git a/webapp/src/components/ai_summary_button.tsx b/webapp/src/components/ai_summary_button.tsx index 0fc09c91..82283985 100644 --- a/webapp/src/components/ai_summary_button.tsx +++ b/webapp/src/components/ai_summary_button.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; +import React, {useCallback} from 'react'; import {FormattedMessage} from 'react-intl'; import {useSelector} from 'react-redux'; @@ -35,31 +35,31 @@ export const useCallsPostButtonClicked = () => { }; const SummaryButton = styled.button` - display: flex; - border: none; - height: 24px; - padding: 4px 10px; - margin-top: 8px; - margin-bottom: 8px; - align-items: center; - justify-content: center; - gap: 6px; - border-radius: 4px; - background: rgba(var(--center-channel-color-rgb), 0.08); + display: flex; + border: none; + height: 24px; + padding: 4px 10px; + margin-top: 8px; + margin-bottom: 8px; + align-items: center; + justify-content: center; + gap: 6px; + border-radius: 4px; + background: rgba(var(--center-channel-color-rgb), 0.08); color: rgba(var(--center-channel-color-rgb), 0.64); - font-size: 12px; - font-weight: 600; - line-height: 16px; + font-size: 12px; + font-weight: 600; + line-height: 16px; - &:hover { - background: rgba(var(--center-channel-color-rgb), 0.12); + &:hover { + background: rgba(var(--center-channel-color-rgb), 0.12); color: rgba(var(--center-channel-color-rgb), 0.72); - } + } - &:active { - background: rgba(var(--button-bg-rgb), 0.08); - color: var(--button-bg); - } + &:active { + background: rgba(var(--button-bg-rgb), 0.08); + color: var(--button-bg); + } `; type Props = { @@ -76,9 +76,9 @@ export const AISummaryButton = ({post, messageId, defaultMessage}: Props) => { return null; } - const handleClick = () => { + const handleClick = useCallback(() => { callsPostButtonClicked(post); - }; + }, [callsPostButtonClicked, post]); return ( From c34752835a0f536a158bf8e819c76329d0206a48 Mon Sep 17 00:00:00 2001 From: avasconcelos114 Date: Fri, 6 Mar 2026 17:15:32 +0200 Subject: [PATCH 48/52] Added recording password setting - Other misc fixes and improvements for logging and error handling --- plugin.json | 9 +++++++++ server/configuration.go | 3 +++ server/store.go | 4 +++- server/webhook.go | 6 ++++-- webapp/src/components/ai_summary_button.tsx | 10 ++++++---- 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/plugin.json b/plugin.json index 9faac08e..cf53e81d 100644 --- a/plugin.json +++ b/plugin.json @@ -102,6 +102,15 @@ "regenerate_help_text": "", "placeholder": "", "default": false + }, + { + "key": "EnablePostingRecordingPassword", + "display_name": "Enable Posting Recording Password:", + "type": "bool", + "help_text": "Enable posting the recording password to the channel when the recording is posted.", + "regenerate_help_text": "", + "placeholder": "", + "default": false } ] } diff --git a/server/configuration.go b/server/configuration.go index 90aac514..51f749a3 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -45,6 +45,9 @@ type configuration struct { // RestrictMeetingCreation allows the admin to by default restrict Zoom meetings to only private channels. // The admin can also edit each channel's behavior with the `/zoom channel-settings` command RestrictMeetingCreation bool + + // EnablePostingRecordingPassword allows the admin to enable posting the recording password to the channel when the recording is posted. + EnablePostingRecordingPassword bool } // Clone shallow copies the configuration. Your implementation may require a deep copy if diff --git a/server/store.go b/server/store.go index c0345e3d..7aeae6da 100644 --- a/server/store.go +++ b/server/store.go @@ -307,7 +307,9 @@ func (p *Plugin) updateSubscriptionIndex(userID string, mutate func(*subscriptio var idx subscriptionIndex if oldRaw != nil { - _ = json.Unmarshal(oldRaw, &idx) + if err := json.Unmarshal(oldRaw, &idx); err != nil { + p.API.LogWarn("failed to unmarshal subscription index", "error", err.Error()) + } } updated := mutate(&idx) diff --git a/server/webhook.go b/server/webhook.go index d7cd7f42..7876f7f0 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -136,11 +136,13 @@ func (p *Plugin) handleMeetingStarted(w http.ResponseWriter, _ *http.Request, bo botUser, appErr := p.API.GetUser(p.botUserID) if appErr != nil { p.API.LogError("Failed to get bot user", "err", appErr.Error()) + http.Error(w, "internal error", http.StatusInternalServerError) return } if postMeetingErr := p.postMeeting(botUser, meetingID, webhook.Payload.Object.UUID, channelID, "", webhook.Payload.Object.Topic); postMeetingErr != nil { p.API.LogError("Failed to post the zoom message in the channel", "err", postMeetingErr.Error()) + http.Error(w, "internal error", http.StatusInternalServerError) return } @@ -351,7 +353,7 @@ func (p *Plugin) downloadZoomFile(downloadURL, downloadToken, channelID, filenam var response *http.Response for attempt := 0; attempt <= maxRetries; attempt++ { if attempt > 0 { - time.Sleep(1 * time.Second) + time.Sleep(time.Duration(1< { const aiAvailable = useAIAvailable(); const callsPostButtonClicked = useCallsPostButtonClicked(); + const handleClick = useCallback(() => { + if (callsPostButtonClicked) { + callsPostButtonClicked(post); + } + }, [callsPostButtonClicked, post]); + if (!aiAvailable || !callsPostButtonClicked) { return null; } - const handleClick = useCallback(() => { - callsPostButtonClicked(post); - }, [callsPostButtonClicked, post]); - return ( From 97c20715098df42e0b8fcf3a06f44311c4f7a663 Mon Sep 17 00:00:00 2001 From: avasconcelos114 Date: Fri, 6 Mar 2026 18:27:06 +0200 Subject: [PATCH 49/52] Improving UX, clarity of password setting, and error handling --- plugin.json | 2 +- server/command.go | 2 +- server/store.go | 15 +++++++++++++-- server/webhook.go | 10 ++++++++-- server/webhook_test.go | 2 ++ server/zoom/meeting.go | 2 +- server/zoom/webhook.go | 4 ++-- 7 files changed, 28 insertions(+), 9 deletions(-) diff --git a/plugin.json b/plugin.json index cf53e81d..26adb1d7 100644 --- a/plugin.json +++ b/plugin.json @@ -107,7 +107,7 @@ "key": "EnablePostingRecordingPassword", "display_name": "Enable Posting Recording Password:", "type": "bool", - "help_text": "Enable posting the recording password to the channel when the recording is posted.", + "help_text": "When enabled, the recording password is posted to the channel alongside the recording link. This makes the password visible to all channel members and persisted in channel history and compliance exports. Only enable for channels where all members are trusted to access the recording.", "regenerate_help_text": "", "placeholder": "", "default": false diff --git a/server/command.go b/server/command.go index 81c2d028..c9884cf5 100644 --- a/server/command.go +++ b/server/command.go @@ -286,7 +286,7 @@ func (p *Plugin) runSubscriptionCommand(args *model.CommandArgs, params []string if len(params) < 2 { return fmt.Sprintf("Please specify a meeting ID. Usage: `/zoom subscription %s [meetingID]`", action), nil } - meetingID, err := strconv.Atoi(params[1]) + meetingID, err := strconv.Atoi(strings.Join(params[1:], "")) if err != nil { return "Invalid meeting ID. Please provide a numeric meeting ID.", nil } diff --git a/server/store.go b/server/store.go index 7aeae6da..536d99cd 100644 --- a/server/store.go +++ b/server/store.go @@ -213,7 +213,12 @@ func (p *Plugin) storeSubscriptionForMeeting(meetingID int, channelID, userID st } if err := p.addToSubscriptionIndex(userID, meetingID); err != nil { - p.API.LogWarn("failed to update subscription index", "error", err.Error()) + p.API.LogWarn("failed to update subscription index, rolling back", + "meeting_id", meetingID, + "error", err.Error(), + ) + _ = p.deleteChannelForMeeting(meetingID) + return errors.Wrap(err, "failed to update subscription index") } return nil @@ -369,7 +374,13 @@ func (p *Plugin) listAllMeetingSubscriptions(userID string) (map[string]string, } var idx subscriptionIndex if raw != nil { - _ = json.Unmarshal(raw, &idx) + if err := json.Unmarshal(raw, &idx); err != nil { + p.API.LogWarn("failed to unmarshal subscription index", + "user_id", userID, + "error", err.Error(), + ) + return nil, errors.Wrap(err, "corrupted subscription index") + } } subscriptions := make(map[string]string) diff --git a/server/webhook.go b/server/webhook.go index 7876f7f0..1485b4da 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -375,10 +375,13 @@ func (p *Plugin) downloadZoomFile(downloadURL, downloadToken, channelID, filenam } defer response.Body.Close() - data, err := io.ReadAll(io.LimitReader(response.Body, maxDownloadSize)) + data, err := io.ReadAll(io.LimitReader(response.Body, maxDownloadSize+1)) if err != nil { return nil, err } + if int64(len(data)) > maxDownloadSize { + return nil, errors.Errorf("download exceeds maximum size of %d bytes", maxDownloadSize) + } fileInfo, appErr := p.API.UploadFile(data, channelID, filename) if appErr != nil { @@ -504,7 +507,10 @@ func (p *Plugin) handleRecordingCompleted(w http.ResponseWriter, _ *http.Request if webhook.Payload.Object.Password != "" && p.getConfiguration().EnablePostingRecordingPassword { msg += "\n**Password:** `" + webhook.Payload.Object.Password + "`" } - newPost.Message = msg + if newPost.Message != "" { + newPost.Message += "\n\n" + } + newPost.Message += msg } } diff --git a/server/webhook_test.go b/server/webhook_test.go index c70e4b45..71eaf961 100644 --- a/server/webhook_test.go +++ b/server/webhook_test.go @@ -452,6 +452,7 @@ func TestWebhookHandleTranscriptCompleted(t *testing.T) { request.Header.Add("x-zm-request-timestamp", ts) p.ServeHTTP(&plugin.Context{}, w, request) + require.Equal(t, http.StatusOK, w.Result().StatusCode) body, _ := io.ReadAll(w.Result().Body) t.Log(string(body)) @@ -524,6 +525,7 @@ func TestWebhookHandleRecordingCompleted(t *testing.T) { request.Header.Add("x-zm-request-timestamp", ts) p.ServeHTTP(&plugin.Context{}, w, request) + require.Equal(t, http.StatusOK, w.Result().StatusCode) body, _ := io.ReadAll(w.Result().Body) t.Log(string(body)) diff --git a/server/zoom/meeting.go b/server/zoom/meeting.go index e9520b40..e29d3bc2 100644 --- a/server/zoom/meeting.go +++ b/server/zoom/meeting.go @@ -13,7 +13,7 @@ const ( MeetingTypeScheduled MeetingType = 2 // MeetingTypeRecurringWithNoFixedTime meeting MeetingTypeRecurringWithNoFixedTime MeetingType = 3 - // MeetingTypePersonal + // MeetingTypePersonal meeting MeetingTypePersonal MeetingType = 4 // MeetingTypeRecurringWithFixedTime meeting MeetingTypeRecurringWithFixedTime MeetingType = 8 diff --git a/server/zoom/webhook.go b/server/zoom/webhook.go index 860cb5e0..cfdf9c84 100644 --- a/server/zoom/webhook.go +++ b/server/zoom/webhook.go @@ -70,9 +70,9 @@ type Webhook struct { } type RecordingWebhook struct { - Type string `schema:"type"` + Type string `json:"event"` DownloadToken string `json:"download_token"` - Payload RecordingWebhookPayload `schema:"content"` + Payload RecordingWebhookPayload `json:"payload"` } type RecordingWebhookPayload struct { From 7284ab73748cf129a80e6b14d3c71028cb8ef4f5 Mon Sep 17 00:00:00 2001 From: avasconcelos114 Date: Fri, 6 Mar 2026 19:24:21 +0200 Subject: [PATCH 50/52] Handling cases of empty UUID in webhook payload --- server/store.go | 6 +++++- server/webhook.go | 7 ++++++- webapp/src/components/ai_summary_button.tsx | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/server/store.go b/server/store.go index 536d99cd..75298b3f 100644 --- a/server/store.go +++ b/server/store.go @@ -313,7 +313,11 @@ func (p *Plugin) updateSubscriptionIndex(userID string, mutate func(*subscriptio var idx subscriptionIndex if oldRaw != nil { if err := json.Unmarshal(oldRaw, &idx); err != nil { - p.API.LogWarn("failed to unmarshal subscription index", "error", err.Error()) + p.API.LogWarn("updateSubscriptionIndex: corrupted index data", + "user_id", userID, + "error", err.Error(), + ) + return errors.Wrap(err, "corrupted subscription index") } } diff --git a/server/webhook.go b/server/webhook.go index 1485b4da..17d4a9b8 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -123,7 +123,12 @@ func (p *Plugin) handleMeetingStarted(w http.ResponseWriter, _ *http.Request, bo // Subscription meetings should always create a new post. if !entry.IsSubscription { if existingPostID, err := p.findMeetingPostByMeetingID(meetingID); err == nil { - if appErr := p.storeMeetingPostID(webhook.Payload.Object.UUID, existingPostID); appErr != nil { + if webhook.Payload.Object.UUID == "" { + p.API.LogWarn("handleMeetingStarted: skipping UUID mapping — webhook UUID is empty", + "meeting_id", meetingID, + "post_id", existingPostID, + ) + } else if appErr := p.storeMeetingPostID(webhook.Payload.Object.UUID, existingPostID); appErr != nil { p.API.LogWarn("failed to store UUID mapping for existing post", "error", appErr.Error(), ) diff --git a/webapp/src/components/ai_summary_button.tsx b/webapp/src/components/ai_summary_button.tsx index c4ce031d..51710c8a 100644 --- a/webapp/src/components/ai_summary_button.tsx +++ b/webapp/src/components/ai_summary_button.tsx @@ -83,7 +83,7 @@ export const AISummaryButton = ({post, messageId, defaultMessage}: Props) => { } return ( - + Date: Fri, 6 Mar 2026 19:29:35 +0200 Subject: [PATCH 51/52] Fixing linter error --- webapp/src/components/ai_summary_button.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/webapp/src/components/ai_summary_button.tsx b/webapp/src/components/ai_summary_button.tsx index 51710c8a..8537a93a 100644 --- a/webapp/src/components/ai_summary_button.tsx +++ b/webapp/src/components/ai_summary_button.tsx @@ -83,7 +83,10 @@ export const AISummaryButton = ({post, messageId, defaultMessage}: Props) => { } return ( - + Date: Wed, 11 Mar 2026 16:10:06 +0200 Subject: [PATCH 52/52] Added handling for lost permissions and permission checks on subscription --- server/command.go | 13 +++++++++++++ server/webhook.go | 12 ++++++++++++ server/webhook_test.go | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/server/command.go b/server/command.go index c9884cf5..cd5c2cce 100644 --- a/server/command.go +++ b/server/command.go @@ -341,6 +341,19 @@ func (p *Plugin) runSubscribeCommand(user *model.User, extra *model.CommandArgs, return "Cannot subscribe to personal meeting", nil } + if !user.IsSystemAdmin() { + zoomUser, authErr := p.authenticateAndFetchZoomUser(user) + if authErr != nil { + if appErr := p.storeOAuthUserState(user.Id, extra.ChannelId, false); appErr != nil { + p.API.LogWarn("failed to store user state") + } + return authErr.Message, authErr.Err + } + if meeting.HostID != zoomUser.ID { + return "You can only subscribe to meetings you host. Contact a system admin to subscribe to other meetings.", nil + } + } + if appErr := p.storeSubscriptionForMeeting(meetingID, extra.ChannelId, user.Id); appErr != nil { return "", errors.Wrap(appErr, "cannot subscribe to meeting") } diff --git a/server/webhook.go b/server/webhook.go index 17d4a9b8..e26a6c63 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -117,6 +117,18 @@ func (p *Plugin) handleMeetingStarted(w http.ResponseWriter, _ *http.Request, bo channelID := entry.ChannelID + if entry.IsSubscription && entry.CreatedBy != "" { + if !p.API.HasPermissionToChannel(entry.CreatedBy, channelID, model.PermissionCreatePost) { + p.API.LogWarn("handleMeetingStarted: subscription creator lost channel access, skipping post", + "meeting_id", meetingID, + "channel_id", channelID, + "created_by", entry.CreatedBy, + ) + w.WriteHeader(http.StatusOK) + return + } + } + // For ad-hoc meetings (started via /zoom start), a post already exists. // Don't create a duplicate — just update the stored UUID mapping so that // meeting.ended can find the post later. diff --git a/server/webhook_test.go b/server/webhook_test.go index 71eaf961..f440b6ce 100644 --- a/server/webhook_test.go +++ b/server/webhook_test.go @@ -170,6 +170,38 @@ func TestHandleMeetingStarted(t *testing.T) { require.Equal(t, http.StatusInternalServerError, w.Result().StatusCode) }) + t.Run("subscription creator lost channel access returns 200 without posting", func(t *testing.T) { + api := &plugintest.API{} + api.On("GetLicense").Return(nil) + meetingEntry, _ := json.Marshal(meetingChannelEntry{ + ChannelID: "channel-id", + IsSubscription: true, + CreatedBy: "creator-user-id", + }) + api.On("KVGet", "meeting_channel_123").Return(meetingEntry, nil) + api.On("HasPermissionToChannel", "creator-user-id", "channel-id", model.PermissionCreatePost).Return(false) + allowFlexibleLogging(api) + p.SetAPI(api) + + requestBody := `{"payload":{"object": {"id": "123", "uuid": "abc"}},"event":"meeting.started"}` + w := httptest.NewRecorder() + reqBody := io.NopCloser(bytes.NewBufferString(requestBody)) + request := httptest.NewRequest("POST", "/webhook?secret=webhooksecret", reqBody) + request.Header.Add("Content-Type", "application/json") + + ts := fmt.Sprintf("%d", time.Now().Unix()) + h := hmac.New(sha256.New, []byte(testConfig.ZoomWebhookSecret)) + _, _ = h.Write([]byte("v0:" + ts + ":" + requestBody)) + signature := "v0=" + hex.EncodeToString(h.Sum(nil)) + + request.Header.Add("x-zm-signature", signature) + request.Header.Add("x-zm-request-timestamp", ts) + + p.ServeHTTP(&plugin.Context{}, w, request) + require.Equal(t, http.StatusOK, w.Result().StatusCode) + api.AssertNotCalled(t, "CreatePost", mock.Anything) + }) + t.Run("no channel entry returns 200", func(t *testing.T) { api := &plugintest.API{} api.On("GetLicense").Return(nil)