diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 50c86bcee..2eed52dd8 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -50,6 +50,9 @@ jobs: ATINTERNET_API_SECRET_KEY: ${{ secrets.ATINTERNET_API_SECRET_KEY }} ATINTERNET_API_ACCESS_KEY: ${{ secrets.ATINTERNET_API_ACCESS_KEY }} PLAUSIBLE_API_KEY: ${{ secrets.PLAUSIBLE_API_KEY }} + CRISP_API_IDENTIFIER: ${{ secrets.CRISP_API_IDENTIFIER }} + CRISP_API_KEY: ${{ secrets.CRISP_API_KEY }} + CRISP_WEBSITE_ID: ${{ secrets.CRISP_WEBSITE_ID }} - name: Build app run: yarn workspace site build env: @@ -101,6 +104,9 @@ jobs: ATINTERNET_API_SECRET_KEY: ${{ secrets.ATINTERNET_API_SECRET_KEY }} ATINTERNET_API_ACCESS_KEY: ${{ secrets.ATINTERNET_API_ACCESS_KEY }} PLAUSIBLE_API_KEY: ${{ secrets.PLAUSIBLE_API_KEY }} + CRISP_API_IDENTIFIER: ${{ secrets.CRISP_API_IDENTIFIER }} + CRISP_API_KEY: ${{ secrets.CRISP_API_KEY }} + CRISP_WEBSITE_ID: ${{ secrets.CRISP_WEBSITE_ID }} - name: Build Storybook run: yarn workspace site build:storybook - uses: actions/upload-artifact@v2 diff --git a/.github/workflows/pr-cleanup.yaml b/.github/workflows/pr-cleanup.yaml index 4672c3622..6403f0bd6 100644 --- a/.github/workflows/pr-cleanup.yaml +++ b/.github/workflows/pr-cleanup.yaml @@ -25,6 +25,9 @@ jobs: ATINTERNET_API_SECRET_KEY: ${{ secrets.ATINTERNET_API_SECRET_KEY }} ATINTERNET_API_ACCESS_KEY: ${{ secrets.ATINTERNET_API_ACCESS_KEY }} PLAUSIBLE_API_KEY: ${{ secrets.PLAUSIBLE_API_KEY }} + CRISP_API_IDENTIFIER: ${{ secrets.CRISP_API_IDENTIFIER }} + CRISP_API_KEY: ${{ secrets.CRISP_API_KEY }} + CRISP_WEBSITE_ID: ${{ secrets.CRISP_WEBSITE_ID }} - name: Remove temporary algolia index run: yarn workspace site algolia:clean env: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ecd731c59..15d6a6db3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -21,6 +21,9 @@ jobs: ATINTERNET_API_SECRET_KEY: ${{ secrets.ATINTERNET_API_SECRET_KEY }} ATINTERNET_API_ACCESS_KEY: ${{ secrets.ATINTERNET_API_ACCESS_KEY }} PLAUSIBLE_API_KEY: ${{ secrets.PLAUSIBLE_API_KEY }} + CRISP_API_IDENTIFIER: ${{ secrets.CRISP_API_IDENTIFIER }} + CRISP_API_KEY: ${{ secrets.CRISP_API_KEY }} + CRISP_WEBSITE_ID: ${{ secrets.CRISP_WEBSITE_ID }} - run: yarn test:type unit: diff --git a/package.json b/package.json index f740b2dee..49abafd1a 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,12 @@ "exoneration-covid", "api", "site", - "standup-mattermost-bot" + "server" ], "scripts": { "scalingo-postbuild": "echo \"$APP_DIR\" ; CI=true ; yarn test:type && yarn \"build:$APP_DIR\" && yarn workspaces focus \"$APP_DIR\" --production", "build:api": "yarn workspaces focus api && yarn workspace api run build && yarn workspaces focus --all && yarn test", - "build:standup-mattermost-bot": "yarn workspaces focus standup-mattermost-bot && yarn workspace standup-mattermost-bot run build", + "build:server": "yarn workspaces focus server && yarn workspace server run build", "lint:eslintrc": "npx eslint-config-prettier .eslintrc.cjs", "lint:eslint": "NODE_OPTIONS='--max-old-space-size=4096' eslint .", "lint:eslint:fix": "yarn lint:eslint --fix", @@ -34,7 +34,7 @@ "i18n:check": "yarn workspace site i18n:check", "i18n:translate": "yarn workspace site i18n:translate" }, - "//": "Resolve conflicts with @types/... in api and standup-mattermost-bot", + "//": "Resolve conflicts with @types/... in api and server", "resolutions": { "@types/cacheable-request": "8.3.1", "@types/responselike": "^1.0.0", diff --git a/standup-mattermost-bot/.env.template b/server/.env.template similarity index 65% rename from standup-mattermost-bot/.env.template rename to server/.env.template index 43f0da7a1..3f40691b3 100644 --- a/standup-mattermost-bot/.env.template +++ b/server/.env.template @@ -2,3 +2,6 @@ CLIENT_ID= CLIENT_SECRET= MONGO_URL=mongodb://root:example@localhost:27017/ ORIGIN=http://localhost:4000 +CRISP_API_IDENTIFIER= +CRISP_API_KEY= +CRISP_WEBSITE_ID= diff --git a/standup-mattermost-bot/README.md b/server/README.md similarity index 52% rename from standup-mattermost-bot/README.md rename to server/README.md index c5fc172a1..ce5a4362a 100644 --- a/standup-mattermost-bot/README.md +++ b/server/README.md @@ -1,4 +1,9 @@ -# Bot Mattermost pour stand-up asynchrone +# Server : serveur back-end de Mon-Entreprise + +Serveur hébergé sur Scalingo, utilisé pour faire tourner le bot Stand-up ainsi que pour mettre à disposition des fonctions back-end Node.js. + +## Jobs +### Bot Mattermost pour stand-up asynchrone Ce bot envoie une notification périodique sur un canal Mattermost dédié afin que chaque membre de l'équipe puisse écrire en réponse où il en est dans ses tâches. @@ -6,7 +11,13 @@ Il désigne également qui sera l'animateur pour chaque jour de la semaine à ve Et peut-être plus à venir... -# Détail technique +#### Détail technique Le bot peut être installé facilement sur Scalingo, vous pouvez aller voir le `Procfile` à la racine du projet. Il nécessite une base de données Mongodb pour stocker les token d'authentification et la liste des animateurs de la semaine. + +## Functions + +### Send Crisp message + +Cette fonction utilise l'API Crisp pour rediriger les messages en provenance de notre formulaire de suggestions vers la messagerie Crisp afin de prendre en charge ces retours. diff --git a/standup-mattermost-bot/docker-compose.yml b/server/docker-compose.yml similarity index 100% rename from standup-mattermost-bot/docker-compose.yml rename to server/docker-compose.yml diff --git a/standup-mattermost-bot/package.json b/server/package.json similarity index 85% rename from standup-mattermost-bot/package.json rename to server/package.json index af7fefc4c..6fab3d37f 100644 --- a/standup-mattermost-bot/package.json +++ b/server/package.json @@ -1,12 +1,12 @@ { - "name": "standup-mattermost-bot", + "name": "server", "license": "MIT", "version": "2.0.0", - "description": "Code source du standup-mattermost-bot", + "description": "Code source du serveur backend mon-entreprise", "repository": { "type": "git", "url": "https://github.com/betagouv/mon-entreprise.git", - "directory": "standup-mattermost-bot" + "directory": "server" }, "private": true, "type": "module", @@ -20,9 +20,11 @@ "@koa/cors": "^3.4.1", "@koa/router": "^12.0.0", "bree": "^9.1.2", + "crisp-api": "6.3.0", "dotenv": "^16.0.3", "got": "^12.5.1", "koa": "^2.13.4", + "koa-body": "^5.0.0", "mongodb": "^4.10.0", "nodemon": "^2.0.20" }, diff --git a/standup-mattermost-bot/source/config.ts b/server/source/config.ts similarity index 100% rename from standup-mattermost-bot/source/config.ts rename to server/source/config.ts diff --git a/server/source/functions/send-crisp-message.ts b/server/source/functions/send-crisp-message.ts new file mode 100644 index 000000000..68a51b916 --- /dev/null +++ b/server/source/functions/send-crisp-message.ts @@ -0,0 +1,85 @@ +import Crisp from 'crisp-api' + +export type BodyType = { + subject: string + message: string + email: string +} + +type SendMessageParamsType = { + type: string + content: string + from: string + origin: string + subject: string +} + +type ConversationMetaType = { + email: string + subject: string + nickname: string +} + +type CrispType = { + authenticateTier: ( + type: string, + identifier: string | undefined, + key: string | undefined + ) => void + website: { + createNewConversation: (id: string) => Promise<{ session_id: string }> + sendMessageInConversation: ( + website_id: string, + session_id: string, + params: SendMessageParamsType + ) => void + updateConversationMetas: ( + website_id: string, + session_id: string, + meta: ConversationMetaType + ) => Promise + } +} + +export const sendCrispMessage = async (body: BodyType) => { + try { + const { message, email, subject } = body || {} + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const CrispClient = new Crisp() as unknown as CrispType + + CrispClient.authenticateTier( + 'plugin', + process.env.CRISP_API_IDENTIFIER, + process.env.CRISP_API_KEY + ) + + const WEBSITE_ID = process.env.CRISP_WEBSITE_ID as string + + const result = await CrispClient.website.createNewConversation(WEBSITE_ID) + + const { session_id: sessionId } = result + + await CrispClient.website.updateConversationMetas(WEBSITE_ID, sessionId, { + email, + nickname: email, + subject, + }) + + CrispClient.website.sendMessageInConversation( + WEBSITE_ID, + sessionId, + + { + type: 'text', + content: message, + subject, + from: 'operator', + origin: 'chat', + } + ) + } catch (e) { + // eslint-disable-next-line no-console + console.error(e) + } +} diff --git a/standup-mattermost-bot/source/index.ts b/server/source/index.ts similarity index 100% rename from standup-mattermost-bot/source/index.ts rename to server/source/index.ts diff --git a/standup-mattermost-bot/source/jobs.ts b/server/source/jobs.ts similarity index 100% rename from standup-mattermost-bot/source/jobs.ts rename to server/source/jobs.ts diff --git a/standup-mattermost-bot/source/jobs/daily-stand-up.ts b/server/source/jobs/daily-stand-up.ts similarity index 100% rename from standup-mattermost-bot/source/jobs/daily-stand-up.ts rename to server/source/jobs/daily-stand-up.ts diff --git a/standup-mattermost-bot/source/jobs/refresh-token.ts b/server/source/jobs/refresh-token.ts similarity index 100% rename from standup-mattermost-bot/source/jobs/refresh-token.ts rename to server/source/jobs/refresh-token.ts diff --git a/standup-mattermost-bot/source/jobs/weekly-randomizer.ts b/server/source/jobs/weekly-randomizer.ts similarity index 100% rename from standup-mattermost-bot/source/jobs/weekly-randomizer.ts rename to server/source/jobs/weekly-randomizer.ts diff --git a/standup-mattermost-bot/source/mattermost.ts b/server/source/mattermost.ts similarity index 100% rename from standup-mattermost-bot/source/mattermost.ts rename to server/source/mattermost.ts diff --git a/standup-mattermost-bot/source/mongodb.ts b/server/source/mongodb.ts similarity index 100% rename from standup-mattermost-bot/source/mongodb.ts rename to server/source/mongodb.ts diff --git a/standup-mattermost-bot/source/oauth.ts b/server/source/oauth.ts similarity index 100% rename from standup-mattermost-bot/source/oauth.ts rename to server/source/oauth.ts diff --git a/standup-mattermost-bot/source/server.ts b/server/source/server.ts similarity index 78% rename from standup-mattermost-bot/source/server.ts rename to server/source/server.ts index d257fe60e..6097df335 100644 --- a/standup-mattermost-bot/source/server.ts +++ b/server/source/server.ts @@ -2,6 +2,7 @@ import cors from '@koa/cors' import Router from '@koa/router' import 'dotenv/config' import Koa from 'koa' +import koaBody from 'koa-body' import { clientId, clientSecret, @@ -9,10 +10,11 @@ import { redirectUri, serverUrl, } from './config.js' +import { BodyType, sendCrispMessage } from './functions/send-crisp-message.js' import { bree } from './jobs.js' import { initMongodb } from './mongodb.js' import { getAccessToken } from './oauth.js' -import { snakeToCamelCaseKeys } from './utils.js' +import { snakeToCamelCaseKeys, validateCrispBody } from './utils.js' const mongo = await initMongodb() @@ -81,6 +83,21 @@ router.get('/oauth', async (ctx) => { } }) +router.post('/send-crisp-message', koaBody(), async (ctx) => { + try { + const body = validateCrispBody(ctx.request.body as BodyType) + + await sendCrispMessage(body) + + ctx.status = 200 + } catch (err) { + // eslint-disable-next-line no-console + console.error(err) + + ctx.status = 400 + } +}) + app.use(router.routes()) app.use(router.allowedMethods()) diff --git a/standup-mattermost-bot/source/utils.ts b/server/source/utils.ts similarity index 63% rename from standup-mattermost-bot/source/utils.ts rename to server/source/utils.ts index 39d0a1383..9f0a68fd9 100644 --- a/standup-mattermost-bot/source/utils.ts +++ b/server/source/utils.ts @@ -1,3 +1,5 @@ +import { BodyType } from './functions/send-crisp-message.js' + type CamelCase = S extends `${infer P1}_${infer P2}${infer P3}` ? `${Lowercase}${Uppercase}${CamelCase}` @@ -33,3 +35,23 @@ export const shuffleArray = (array: T[]) => { return shuffle } + +const isStringAndNotEmpty = (value: string) => + value !== undefined && value !== '' && typeof value === 'string' + +const SHORT_MAX_LENGTH = 254 + +export const validateCrispBody = (body: BodyType): BodyType => { + const { subject, message, email } = body || {} + if ( + isStringAndNotEmpty(subject) && + subject.length <= SHORT_MAX_LENGTH && + isStringAndNotEmpty(message) && + isStringAndNotEmpty(email) && + email.length <= SHORT_MAX_LENGTH + ) { + return body + } + + throw Error('Body validation failed.') +} diff --git a/standup-mattermost-bot/tsconfig.json b/server/tsconfig.json similarity index 100% rename from standup-mattermost-bot/tsconfig.json rename to server/tsconfig.json diff --git a/standup-mattermost-bot/types/index.d.ts b/server/types/index.d.ts similarity index 100% rename from standup-mattermost-bot/types/index.d.ts rename to server/types/index.d.ts diff --git a/site/netlify.base.toml b/site/netlify.base.toml index afa64ea78..77aed80bc 100644 --- a/site/netlify.base.toml +++ b/site/netlify.base.toml @@ -1,7 +1,7 @@ [[headers]] for = "/*" [headers.values] -Content-Security-Policy = "default-src 'self' mon-entreprise.fr; style-src 'self' 'unsafe-inline' mon-entreprise.zammad.com; connect-src 'self' *.incubateur.net raw.githubusercontent.com tm.urssaf.fr mon-entreprise.zammad.com api.recherche-entreprises.fabrique.social.gouv.fr geo.api.gouv.fr *.algolia.net *.algolianet.com polyfill.io code.jquery.com jedonnemonavis.numerique.gouv.fr user-images.githubusercontent.com; form-action 'self' *.sibforms.com *.incubateur.net mon-entreprise.zammad.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' tm.urssaf.fr *.incubateur.net stonly.com code.jquery.com mon-entreprise.zammad.com polyfill.io; img-src 'self' data: mon-entreprise.urssaf.fr tm.urssaf.fr user-images.githubusercontent.com jedonnemonavis.numerique.gouv.fr; frame-src 'self' https://www.youtube-nocookie.com https://codesandbox.io https://place-des-entreprises.beta.gouv.fr https://reso-staging.osc-fr1.scalingo.io https://stackblitz.com" +Content-Security-Policy = "default-src 'self' mon-entreprise.fr; style-src 'self' 'unsafe-inline' mon-entreprise.zammad.com; connect-src 'self' *.incubateur.net raw.githubusercontent.com tm.urssaf.fr mon-entreprise.zammad.com api.recherche-entreprises.fabrique.social.gouv.fr geo.api.gouv.fr *.algolia.net *.algolianet.com polyfill.io jedonnemonavis.numerique.gouv.fr user-images.githubusercontent.com; form-action 'self' *.sibforms.com *.incubateur.net mon-entreprise.zammad.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' tm.urssaf.fr *.incubateur.net stonly.com code.jquery.com mon-entreprise.zammad.com polyfill.io; img-src 'self' data: mon-entreprise.urssaf.fr tm.urssaf.fr user-images.githubusercontent.com jedonnemonavis.numerique.gouv.fr; frame-src 'self' https://www.youtube-nocookie.com https://codesandbox.io https://place-des-entreprises.beta.gouv.fr https://reso-staging.osc-fr1.scalingo.io https://stackblitz.com" [dev] autoLaunch = false diff --git a/site/scripts/fetch-stats.js b/site/scripts/fetch-stats.js index 65c4426b8..869988614 100644 --- a/site/scripts/fetch-stats.js +++ b/site/scripts/fetch-stats.js @@ -311,7 +311,6 @@ async function fetchDailyVisits() { } async function fetchMonthlyVisits() { - console.log('fetchMonthlyVisits') const pages = uniformiseData([ ...flattenPage(await fetchApi(buildSimulateursQuery(last36Months, 'M'))), ...flattenPage(await fetchApi(buildCreerQuery(last36Months, 'M'))), @@ -341,7 +340,80 @@ async function fetchMonthlyVisits() { } } -async function fetchUserAnswersStats() { +const getISODatesStartEndPreviousMonth = () => { + const dateFirstDayPreviousMonth = new Date() + // On prend le premier jour du mois dernier + dateFirstDayPreviousMonth.setMonth(dateFirstDayPreviousMonth.getMonth() - 1) + dateFirstDayPreviousMonth.setDate(1) + dateFirstDayPreviousMonth.setUTCHours(0, 0, 0, 0) + const dateLastDayPreviousMonth = new Date(dateFirstDayPreviousMonth) + // Ici l'index 0 permet de récupérer le dernier jour du mois précédent + dateLastDayPreviousMonth.setDate(0) + dateLastDayPreviousMonth.setUTCHours(23, 59, 59, 999) + + return { + startISODatePreviousMonth: dateFirstDayPreviousMonth.toISOString(), + endISODatePreviousMonth: dateLastDayPreviousMonth.toISOString(), + } +} +async function fetchPaginatedCrispConversations(pageNumber, urlParams) { + const response = await fetch( + `https://api.crisp.chat/v1/website/d8247abb-cac5-4db6-acb2-cea0c00d8524/conversations/${pageNumber}${ + urlParams ? `?${urlParams}` : '' + }`, + { + method: 'get', + headers: { + Authorization: `Basic ${btoa( + `${process.env.CRISP_API_IDENTIFIER}:${process.env.CRISP_API_KEY}` + )}`, + 'X-Crisp-Tier': 'plugin', + }, + } + ) + + const result = await response.json() + + return result?.data +} + +async function fetchAllCrispConversations({ urlParams }) { + try { + let isEndPagination = false + let pageCount = 1 + const dataConversations = [] + + while (!isEndPagination) { + const paginatedData = await fetchPaginatedCrispConversations( + pageCount, + urlParams + ) + // Array vide : plus rien à fetch de plus + if (paginatedData.length === 0) { + isEndPagination = true + } + + dataConversations.push(...(paginatedData || [])) + pageCount++ + } + + return dataConversations + } catch (e) { + console.log('error', e) + } +} + +async function fetchCrispAnsweredConversationsLastMonth() { + const { startISODatePreviousMonth, endISODatePreviousMonth } = + getISODatesStartEndPreviousMonth() + + const conversations = await fetchAllCrispConversations({ + urlParams: `filter_resolved=1&filter_date_start=${startISODatePreviousMonth}&filter_date_end=${endISODatePreviousMonth}`, + }) + return conversations?.length ?? 0 +} + +async function fetchZammadUserAnswersStats() { const ticketLists = await fetch( 'https://mon-entreprise.zammad.com/api/v1/ticket_overviews?view=tickets_repondus_le_mois_dernier', { @@ -354,20 +426,21 @@ async function fetchUserAnswersStats() { return answer.index.count } -async function fetchUserFeedbackIssues() { - const tags = await fetch( - 'https://mon-entreprise.zammad.com/api/v1/tag_list', - { - headers: new Headers({ - Authorization: `Token token=${process.env.ZAMMAD_API_SECRET_KEY}`, - }), - } - ).then((r) => r.json()) - const sortedTags = tags - .sort((t1, t2) => t2.count - t1.count) - .filter(({ name }) => /#[\d]+/.exec(name)) +async function fetchAllUserAnswerStats() { + const zammadAnswersCount = await fetchZammadUserAnswersStats() + const cripsAnswersCount = await fetchCrispAnsweredConversationsLastMonth() + + return zammadAnswersCount + cripsAnswersCount +} + +async function fetchGithubIssuesFromTags(tags) { + if (!tags || tags?.length === 0) { + console.error(`❌ Error: no tags to fetch issues from`) + return [] + } + const query = `query { - repository(owner:"betagouv", name:"mon-entreprise") {${sortedTags + repository(owner:"betagouv", name:"mon-entreprise") {${tags .map( ({ name, count }, i) => ` @@ -402,13 +475,79 @@ async function fetchUserFeedbackIssues() { .filter(([, value]) => !!value) .map(([k, value]) => ({ ...value, count: +/[\d]+$/.exec(k)[0] })) + return issues +} + +async function fetchCrispUserFeedbackIssues() { + const conversationsResolved = await fetchAllCrispConversations({ + urlParams: 'filter_resolved=1&search_query=issue&search_type=segment', + }) + + const sortedSegments = conversationsResolved + ?.reduce((acc, conversation) => { + const newAcc = [...acc] + const conversationSegments = conversation.meta.segments + + conversationSegments + .filter((segment) => /#[\d]+/.exec(segment)) + .forEach((segment) => { + const segmentObjectIndex = newAcc.findIndex( + (segmentObject) => segmentObject.name === segment + ) + if (segmentObjectIndex < 0) { + newAcc.push({ name: segment, count: 1 }) + } else { + newAcc[segmentObjectIndex].count += 1 + } + }) + return newAcc + }, []) + ?.sort((t1, t2) => t2.count - t1.count) + + return fetchGithubIssuesFromTags(sortedSegments) +} + +async function fetchZammadUserFeedbackIssues() { + const tags = await fetch( + 'https://mon-entreprise.zammad.com/api/v1/tag_list', + { + headers: new Headers({ + Authorization: `Token token=${process.env.ZAMMAD_API_SECRET_KEY}`, + }), + } + ).then((r) => r.json()) + const sortedTags = tags + .sort((t1, t2) => t2.count - t1.count) + .filter(({ name }) => /#[\d]+/.exec(name)) + + return fetchGithubIssuesFromTags(sortedTags) +} + +async function fetchAllUserFeedbackIssues() { + const crispFeedbackIssues = await fetchCrispUserFeedbackIssues() + const zammadFeedbackIssues = await fetchZammadUserFeedbackIssues() + + const allIssues = [...(crispFeedbackIssues || [])] + + zammadFeedbackIssues.forEach((zammadIssue) => { + const issueIndex = allIssues.findIndex( + (issue) => issue.number === zammadIssue.number + ) + if (issueIndex > 0) { + allIssues[issueIndex].count += zammadIssue.count + } else { + allIssues.push(zammadIssue) + } + }) + return { - open: issues.filter((s) => !s.closedAt), - closed: issues + open: allIssues.filter((s) => !s.closedAt), + closed: allIssues .filter((s) => s.closedAt) .sort((i1, i2) => new Date(i2.closedAt) - new Date(i1.closedAt)), } } + async function main() { createDataDir() // In case we cannot fetch the release (the API is down or the Authorization @@ -443,8 +582,8 @@ async function main() { fetchDailyVisits(), fetchMonthlyVisits(), fetchApi(buildSatisfactionQuery()), - fetchUserFeedbackIssues(), - fetchUserAnswersStats(), + fetchAllUserFeedbackIssues(), + fetchAllUserAnswerStats(), ]) const satisfaction = uniformiseData(flattenPage(await rawSatisfaction)).map( (page) => { diff --git a/site/source/components/Feedback/FeedbackForm.tsx b/site/source/components/Feedback/FeedbackForm.tsx index 03068de63..be26b1790 100644 --- a/site/source/components/Feedback/FeedbackForm.tsx +++ b/site/source/components/Feedback/FeedbackForm.tsx @@ -1,77 +1,97 @@ import { ScrollToElement } from '@/components/utils/Scroll' -import { useEffect } from 'react' +import { TextAreaField, TextField } from '@/design-system' +import { Button } from '@/design-system/buttons' +import { Body } from '@/design-system/typography/paragraphs' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocation } from 'react-router-dom' import styled from 'styled-components' -declare global { - interface JQuery { - ZammadForm(options: any): void - } -} +const SERVER_URL = import.meta.env.DEV + ? 'http://localhost:4000' + : 'https://mon-entreprise-server.osc-fr1.scalingo.io' + +const SHORT_MAX_LENGTH = 254 -// TODO: we could implement the form logic ourselves to avoid including -// https://mon-entreprise.zammad.com and https://code.jquery.com scripts export default function FeedbackForm() { - // const tracker = useContext(TrackerContext) + const [isSubmittedSuccessfully, setIsSubmittedSuccessfully] = useState(false) + const [isLoading, setIsLoading] = useState(false) const pathname = useLocation().pathname const page = pathname.split('/').slice(-1)[0] - const isSimulateur = pathname.includes('simulateurs') - const lang = useTranslation().i18n.language - useEffect(() => { - const script = document.createElement('script') - script.src = 'https://code.jquery.com/jquery-2.1.4.min.js' + const { t } = useTranslation() - document.body.appendChild(script) - setTimeout(() => { - const script = document.createElement('script') - script.id = 'zammad_form_script' - script.async = true - script.onload = () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - $('#feedback-form').ZammadForm({ - messageTitle: `Remarque sur ${ - isSimulateur ? 'le simulateur' : 'la page' - } ${page}`, - messageSubmit: 'Envoyer', - messageThankYou: 'Merci de votre retour !', - lang, - attributes: [ - { - display: - "Que pouvons-nous améliorer afin de mieux répondre à vos attentes ? (ne pas mettre d'informations personnelles)", - name: 'body', - tag: 'textarea', - placeholder: 'Your Message...', - defaultValue: '', - rows: 7, - }, - { - display: 'Nom', - name: 'name', - tag: 'input', - type: 'text', - defaultValue: '-', - }, - { - display: 'Email (pour recevoir une réponse)', - name: 'email', - tag: 'input', - type: 'email', - placeholder: 'Your Email', - }, - ], - }) - } - script.src = 'https://mon-entreprise.zammad.com/assets/form/form.js' - document.body.appendChild(script) - }, 100) - }, []) + const sendMessage = async () => { + setIsLoading(true) + const messageValue = ( + document.getElementById('message') as HTMLTextAreaElement + )?.value + const emailValue = (document.getElementById('email') as HTMLInputElement) + ?.value + + try { + await fetch(`${SERVER_URL}/send-crisp-message`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + subject: `Suggestion sur la page : ${page}`, + message: messageValue, + email: emailValue, + }), + }) + setIsSubmittedSuccessfully(true) + } catch (e) { + // Show error message + } + } return ( - + {isSubmittedSuccessfully && ( + Merci de votre retour ! + )} + {!isSubmittedSuccessfully && ( + +
{ + e.preventDefault() + void sendMessage() + }} + > + + Que pouvons-nous améliorer pour mieux répondre à vos attentes ? + + + + + + + {t('Envoyer')} + + +
+ )}
) } @@ -82,15 +102,39 @@ const StyledFeedback = styled.div` font-family: ${({ theme }) => theme.fonts.main}; text-align: left; - textarea, - input { - width: 100%; - font-family: inherit; - font-size: inherit; - line-height: inherit; - padding: ${({ theme }) => theme.spacings.sm}; - margin-top: ${({ theme }) => theme.spacings.xs}; - border-radius: ${({ theme }) => theme.box.borderRadius}; - border: 1px solid ${({ theme }) => theme.colors.extended.grey[500]}; + label { + margin-bottom: 0.5rem; + display: block; } ` + +const StyledTextArea = styled(TextAreaField)` + width: 100%; + font-size: 1rem; + line-height: 1.5rem; + padding: ${({ theme }) => theme.spacings.sm}; + border-radius: ${({ theme }) => theme.box.borderRadius}; + font-family: ${({ theme }) => theme.fonts.main}; +` + +const StyledTextField = styled(TextField)` + font-size: 1rem; + line-height: 1.5rem; + padding: ${({ theme }) => theme.spacings.sm}; + font-family: ${({ theme }) => theme.fonts.main}; +` + +const StyledButton = styled(Button)` + margin-top: 1rem; +` + +const StyledBody = styled(Body)` + font-size: 1.25rem; + font-family: ${({ theme }) => theme.fonts.main}; + text-align: center; + padding: 1rem 0; +` + +const StyledDiv = styled.div` + margin-top: 1rem; +` diff --git a/site/source/components/Feedback/index.tsx b/site/source/components/Feedback/index.tsx index 61d19ec9c..648f73d76 100644 --- a/site/source/components/Feedback/index.tsx +++ b/site/source/components/Feedback/index.tsx @@ -145,7 +145,7 @@ export default function PageFeedback({ customMessage }: PageFeedbackProps) { {state.showForm && ( setState({ showThanks: true, showForm: false })} small diff --git a/site/source/design-system/field/TextAreaField.tsx b/site/source/design-system/field/TextAreaField.tsx new file mode 100644 index 000000000..2af5216b3 --- /dev/null +++ b/site/source/design-system/field/TextAreaField.tsx @@ -0,0 +1,214 @@ +import { AriaTextFieldOptions, useTextField } from '@react-aria/textfield' +import { ExtraSmallBody } from '@/design-system/typography/paragraphs' +import { HTMLAttributes, RefObject, useRef } from 'react' +import styled, { css } from 'styled-components' + +const LABEL_HEIGHT = '1rem' + +type TextAreaFieldProps = AriaTextFieldOptions<'textarea'> & { + inputRef?: RefObject + small?: boolean + rows?: number | undefined +} + +export default function TextAreaField(props: TextAreaFieldProps) { + const ref = useRef(null) + + const { labelProps, inputProps, descriptionProps, errorMessageProps } = + useTextField( + { ...props, inputElementType: 'textarea' }, + props.inputRef || ref + ) + + return ( + + + )} + {...(inputProps as HTMLAttributes)} + placeholder={inputProps.placeholder ?? ''} + ref={props.inputRef || ref} + /> + {props.label && ( + + {props.label} + + )} + + {props.errorMessage && ( + + {props.errorMessage} + + )} + {props.description && ( + + {props.description} + + )} + + ) +} + +export const StyledContainer = styled.div` + width: 100%; +` +export const StyledTextArea = styled.textarea` + font-size: 1rem; + line-height: 1.5rem; + border: none; + width: 100%; + background: none; + font-family: ${({ theme }) => theme.fonts.main}; + height: 100%; + outline: none; + transition: color 0.2s; + ::placeholder { + ${({ theme }) => + theme.darkMode && + css` + opacity: 0.6; + `} + color: ${({ theme }) => + theme.colors.extended.grey[theme.darkMode ? 200 : 600]}; + } + ${({ theme }) => + theme.darkMode && + css` + @media not print { + color: ${theme.colors.extended.grey[100]} !important; + } + `} +` + +export const StyledLabel = styled.label` + top: 0%; + left: 0; + pointer-events: none; + transform: translateY(0%); + font-size: 0.75rem; + line-height: ${LABEL_HEIGHT}; + font-family: ${({ theme }) => theme.fonts.main}; + padding: ${({ theme }) => `${theme.spacings.xxs} ${theme.spacings.sm}`}; + position: absolute; + will-change: transform top font-size line-height color; + transition: all 0.1s; + ${({ theme }) => + theme.darkMode && + css` + @media not print { + color: ${theme.colors.extended.grey[100]} !important; + } + `} +` + +export const StyledDescription = styled(ExtraSmallBody)` + padding: ${({ theme }) => `${theme.spacings.xxs} ${theme.spacings.sm}`}; + will-change: color; + transition: color 0.2s; + margin-top: 0; +` + +export const StyledErrorMessage = styled(StyledDescription)` + color: ${({ theme }) => theme.colors.extended.error[400]} !important; +` + +export const StyledSuffix = styled.span` + font-size: 1rem; + line-height: 1.5rem; + font-family: ${({ theme }) => theme.fonts.main}; +` + +export const StyledTextAreaContainer = styled.div<{ + hasError: boolean + hasLabel: boolean + small?: boolean +}>` + border-radius: ${({ theme }) => theme.box.borderRadius}; + border: ${({ theme }) => + `${theme.box.borderWidth} solid ${ + theme.darkMode + ? theme.colors.extended.grey[100] + : theme.colors.extended.grey[700] + }`}; + outline: transparent solid 1px; + position: relative; + display: flex; + background-color: ${({ theme }) => + theme.darkMode + ? 'rgba(255, 255, 255, 20%)' + : theme.colors.extended.grey[100]}; + align-items: center; + transition: all 0.2s; + + :focus-within { + outline-color: ${({ theme, hasError }) => + hasError + ? theme.colors.extended.error[400] + : theme.darkMode + ? theme.colors.bases.primary[100] + : theme.colors.bases.primary[700]}; + } + :focus-within ${StyledLabel} { + color: ${({ theme }) => theme.colors.bases.primary[800]}; + } + + :focus-within + ${StyledDescription} { + ${({ theme }) => + !theme.darkMode && + css` + color: ${theme.colors.bases.primary[800]}; + `} + } + + ${({ hasLabel }) => + hasLabel && + css` + ${StyledTextArea}:not(:focus):placeholder-shown { + color: transparent; + } + ${StyledTextArea}:not(:focus):placeholder-shown + ${StyledSuffix} { + color: transparent; + } + `} + + ${StyledTextArea}:not(:focus):placeholder-shown:not(:empty) + ${StyledLabel} { + font-size: 1rem; + line-height: 1.5rem; + top: 50%; + transform: translateY(-50%); + } + + ${({ theme, hasError }) => + hasError && + css` + && { + border-color: ${theme.colors.extended.error[400]}; + } + &&& label { + color: ${theme.colors.extended.error[400]}; + } + `} + + ${StyledTextArea}, ${StyledSuffix} { + padding: ${({ hasLabel, theme, small }) => + small + ? css` + ${theme.spacings.xxs} ${theme.spacings.xs} + ` + : css`calc(${hasLabel ? LABEL_HEIGHT : '0rem'} + ${ + theme.spacings.xs + }) ${theme.spacings.sm} ${theme.spacings.xs}`}; + } + + ${({ small }) => + small && + css` + ${StyledSuffix}, ${StyledTextArea} { + font-size: 1rem; + line-height: 1.25rem; + } + `} +` diff --git a/site/source/design-system/field/TextField.tsx b/site/source/design-system/field/TextField.tsx index 21e65eccb..df6ef4781 100644 --- a/site/source/design-system/field/TextField.tsx +++ b/site/source/design-system/field/TextField.tsx @@ -104,6 +104,7 @@ export const StyledDescription = styled(ExtraSmallBody)` padding: ${({ theme }) => `${theme.spacings.xxs} ${theme.spacings.sm}`}; will-change: color; transition: color 0.2s; + margin-top: 0; ` export const StyledErrorMessage = styled(StyledDescription)` diff --git a/site/source/design-system/field/index.ts b/site/source/design-system/field/index.ts index f6727fadb..a2fc2dec6 100644 --- a/site/source/design-system/field/index.ts +++ b/site/source/design-system/field/index.ts @@ -4,3 +4,4 @@ export { default as DateField } from './DateField' export { default as NumberField } from './NumberField' export { default as SearchField } from './SearchField' export { default as TextField } from './TextField' +export { default as TextAreaField } from './TextAreaField' diff --git a/yarn.lock b/yarn.lock index 5caca99b6..59ec9972d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6481,6 +6481,13 @@ __metadata: languageName: node linkType: hard +"@socket.io/component-emitter@npm:~3.0.0": + version: 3.0.0 + resolution: "@socket.io/component-emitter@npm:3.0.0" + checksum: b5e909dbb16bcf27958d1bfb8319f3255f3a50f62fde78ecf9a584f39f916b928fdc5661519892eea912da082c6413d671c1e67bde70725c75ee62956aa67c26 + languageName: node + linkType: hard + "@storybook/addon-actions@npm:6.5.0-alpha.55, @storybook/addon-actions@npm:^6.5.0-alpha.49": version: 6.5.0-alpha.55 resolution: "@storybook/addon-actions@npm:6.5.0-alpha.55" @@ -10419,7 +10426,7 @@ __metadata: languageName: node linkType: hard -"asap@npm:^2.0.0": +"asap@npm:^2.0.0, asap@npm:~2.0.3": version: 2.0.6 resolution: "asap@npm:2.0.6" checksum: b296c92c4b969e973260e47523207cd5769abd27c245a68c26dc7a0fe8053c55bb04360237cb51cab1df52be939da77150ace99ad331fb7fb13b3423ed73ff3d @@ -10843,6 +10850,13 @@ __metadata: languageName: node linkType: hard +"backo2@npm:~1.0.2": + version: 1.0.2 + resolution: "backo2@npm:1.0.2" + checksum: fda8d0a0f4810068d23715f2f45153146d6ee8f62dd827ce1e0b6cc3c8328e84ad61e11399a83931705cef702fe7cbb457856bf99b9bd10c4ed57b0786252385 + languageName: node + linkType: hard + "backoff@npm:^2.5.0": version: 2.5.0 resolution: "backoff@npm:2.5.0" @@ -12960,6 +12974,17 @@ __metadata: languageName: node linkType: hard +"crisp-api@npm:6.3.0": + version: 6.3.0 + resolution: "crisp-api@npm:6.3.0" + dependencies: + fbemitter: 3.0.0 + got: 9.6.0 + socket.io-client: 4.4.1 + checksum: 684000db238933c59f752aed87833ca27f1da7a3fe23b1f46d9c8b166cd8a7f99f35417d5fa48bb3aa23f39206527de91b26568d5147ccbe238419189de99cb3 + languageName: node + linkType: hard + "cron-parser@npm:^4.1.0, cron-parser@npm:^4.2.1": version: 4.5.0 resolution: "cron-parser@npm:4.5.0" @@ -13408,7 +13433,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -14354,6 +14379,30 @@ __metadata: languageName: node linkType: hard +"engine.io-client@npm:~6.1.1": + version: 6.1.1 + resolution: "engine.io-client@npm:6.1.1" + dependencies: + "@socket.io/component-emitter": ~3.0.0 + debug: ~4.3.1 + engine.io-parser: ~5.0.0 + has-cors: 1.1.0 + parseqs: 0.0.6 + parseuri: 0.0.6 + ws: ~8.2.3 + xmlhttprequest-ssl: ~2.0.0 + yeast: 0.1.2 + checksum: c2e1cec87ac8cf45842527bd072d1b2c5f14fbf9e57f110b4120335ed7bf5310a86da0d33b5906dd4774094ee499d534a498db467d3c1cb53c7a1109a593b05d + languageName: node + linkType: hard + +"engine.io-parser@npm:~5.0.0": + version: 5.0.4 + resolution: "engine.io-parser@npm:5.0.4" + checksum: d4ad0cef6ff63c350e35696da9bb3dbd180f67b56e93e90375010cc40393e6c0639b780d5680807e1d93a7e2e3d7b4a1c3b27cf75db28eb8cbf605bc1497da03 + languageName: node + linkType: hard + "enhanced-resolve@npm:^4.5.0": version: 4.5.0 resolution: "enhanced-resolve@npm:4.5.0" @@ -16103,6 +16152,37 @@ __metadata: languageName: node linkType: hard +"fbemitter@npm:3.0.0": + version: 3.0.0 + resolution: "fbemitter@npm:3.0.0" + dependencies: + fbjs: ^3.0.0 + checksum: 069690b8cdff3521ade3c9beb92ba0a38d818a86ef36dff8690e66749aef58809db4ac0d6938eb1cacea2dbef5f2a508952d455669590264cdc146bbe839f605 + languageName: node + linkType: hard + +"fbjs-css-vars@npm:^1.0.0": + version: 1.0.2 + resolution: "fbjs-css-vars@npm:1.0.2" + checksum: 72baf6d22c45b75109118b4daecb6c8016d4c83c8c0f23f683f22e9d7c21f32fff6201d288df46eb561e3c7d4bb4489b8ad140b7f56444c453ba407e8bd28511 + languageName: node + linkType: hard + +"fbjs@npm:^3.0.0": + version: 3.0.4 + resolution: "fbjs@npm:3.0.4" + dependencies: + cross-fetch: ^3.1.5 + fbjs-css-vars: ^1.0.0 + loose-envify: ^1.0.0 + object-assign: ^4.1.0 + promise: ^7.1.1 + setimmediate: ^1.0.5 + ua-parser-js: ^0.7.30 + checksum: 8b23a3550fcda8a9109fca9475a3416590c18bb6825ea884192864ed686f67fcd618e308a140c9e5444fbd0168732e1ff3c092ba3d0c0ae1768969f32ba280c7 + languageName: node + linkType: hard + "fd-slicer@npm:~1.1.0": version: 1.1.0 resolution: "fd-slicer@npm:1.1.0" @@ -17335,6 +17415,25 @@ __metadata: languageName: node linkType: hard +"got@npm:9.6.0, got@npm:^9.6.0": + version: 9.6.0 + resolution: "got@npm:9.6.0" + dependencies: + "@sindresorhus/is": ^0.14.0 + "@szmarczak/http-timer": ^1.1.2 + cacheable-request: ^6.0.0 + decompress-response: ^3.3.0 + duplexer3: ^0.1.4 + get-stream: ^4.1.0 + lowercase-keys: ^1.0.1 + mimic-response: ^1.0.1 + p-cancelable: ^1.0.0 + to-readable-stream: ^1.0.0 + url-parse-lax: ^3.0.0 + checksum: 941807bd9704bacf5eb401f0cc1212ffa1f67c6642f2d028fd75900471c221b1da2b8527f4553d2558f3faeda62ea1cf31665f8b002c6137f5de8732f07370b0 + languageName: node + linkType: hard + "got@npm:^10.0.0, got@npm:^10.7.0": version: 10.7.0 resolution: "got@npm:10.7.0" @@ -17402,25 +17501,6 @@ __metadata: languageName: node linkType: hard -"got@npm:^9.6.0": - version: 9.6.0 - resolution: "got@npm:9.6.0" - dependencies: - "@sindresorhus/is": ^0.14.0 - "@szmarczak/http-timer": ^1.1.2 - cacheable-request: ^6.0.0 - decompress-response: ^3.3.0 - duplexer3: ^0.1.4 - get-stream: ^4.1.0 - lowercase-keys: ^1.0.1 - mimic-response: ^1.0.1 - p-cancelable: ^1.0.0 - to-readable-stream: ^1.0.0 - url-parse-lax: ^3.0.0 - checksum: 941807bd9704bacf5eb401f0cc1212ffa1f67c6642f2d028fd75900471c221b1da2b8527f4553d2558f3faeda62ea1cf31665f8b002c6137f5de8732f07370b0 - languageName: node - linkType: hard - "graceful-fs@npm:^4.0.0, graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.3, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.10 resolution: "graceful-fs@npm:4.2.10" @@ -17489,6 +17569,13 @@ __metadata: languageName: node linkType: hard +"has-cors@npm:1.1.0": + version: 1.1.0 + resolution: "has-cors@npm:1.1.0" + checksum: 549ce94113fd23895b22d71ade9809918577b8558cd4d701fe79045d8b1d58d87eba870260b28f6a3229be933a691c55653afd496d0fc52e98fd2ff577f01197 + languageName: node + linkType: hard + "has-flag@npm:^3.0.0": version: 3.0.0 resolution: "has-flag@npm:3.0.0" @@ -23256,6 +23343,20 @@ __metadata: languageName: node linkType: hard +"parseqs@npm:0.0.6": + version: 0.0.6 + resolution: "parseqs@npm:0.0.6" + checksum: 7fc4ff4ba59764060bb8529875f6d4313056ea6939ff579b22dd7bd6f6033035e1fd2d6a559ab48ef0a7fa29a9d7731c982bfd1594e9115141fe1c328485ce9e + languageName: node + linkType: hard + +"parseuri@npm:0.0.6": + version: 0.0.6 + resolution: "parseuri@npm:0.0.6" + checksum: fa430e40f0c75293a28e5f1023da5f51a5038d5e34c48c517b0d5187143f6bcc67d3091a062b68765db4a22757e488c7d15854f9d1921f2c2b9afa5ca0629a84 + languageName: node + linkType: hard + "parseurl@npm:^1.3.2, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" @@ -23929,6 +24030,15 @@ __metadata: languageName: node linkType: hard +"promise@npm:^7.1.1": + version: 7.3.1 + resolution: "promise@npm:7.3.1" + dependencies: + asap: ~2.0.3 + checksum: 475bb069130179fbd27ed2ab45f26d8862376a137a57314cf53310bdd85cc986a826fd585829be97ebc0aaf10e9d8e68be1bfe5a4a0364144b1f9eedfa940cf1 + languageName: node + linkType: hard + "prompts@npm:^2.4.0": version: 2.4.2 resolution: "prompts@npm:2.4.2" @@ -25951,6 +26061,31 @@ __metadata: languageName: node linkType: hard +"server@workspace:server": + version: 0.0.0-use.local + resolution: "server@workspace:server" + dependencies: + "@breejs/later": ^4.1.0 + "@koa/cors": ^3.4.1 + "@koa/router": ^12.0.0 + "@types/koa": ^2.13.5 + "@types/koa__cors": ^3.3.0 + "@types/koa__router": ^12.0.0 + "@types/node": ^18.8.3 + "@types/safe-timers": ^1.1.0 + bree: ^9.1.2 + crisp-api: 6.3.0 + dotenv: ^16.0.3 + got: ^12.5.1 + koa: ^2.13.4 + koa-body: ^5.0.0 + mongodb: ^4.10.0 + nodemon: ^2.0.20 + ts-node: ^10.9.1 + typescript: ^4.8.4 + languageName: unknown + linkType: soft + "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -25970,7 +26105,7 @@ __metadata: languageName: node linkType: hard -"setimmediate@npm:^1.0.4": +"setimmediate@npm:^1.0.4, setimmediate@npm:^1.0.5": version: 1.0.5 resolution: "setimmediate@npm:1.0.5" checksum: c9a6f2c5b51a2dabdc0247db9c46460152ffc62ee139f3157440bd48e7c59425093f42719ac1d7931f054f153e2d26cf37dfeb8da17a794a58198a2705e527fd @@ -26312,6 +26447,30 @@ __metadata: languageName: node linkType: hard +"socket.io-client@npm:4.4.1": + version: 4.4.1 + resolution: "socket.io-client@npm:4.4.1" + dependencies: + "@socket.io/component-emitter": ~3.0.0 + backo2: ~1.0.2 + debug: ~4.3.2 + engine.io-client: ~6.1.1 + parseuri: 0.0.6 + socket.io-parser: ~4.1.1 + checksum: 4c7f77f439b72f851fa162603b45bdabc94154ad6bd7b1d1c1658b45cd25d7f495293c08f71cd3ce96bccc477045d99840c10a92e1da9c13e9a5ff939edde30d + languageName: node + linkType: hard + +"socket.io-parser@npm:~4.1.1": + version: 4.1.2 + resolution: "socket.io-parser@npm:4.1.2" + dependencies: + "@socket.io/component-emitter": ~3.0.0 + debug: ~4.3.1 + checksum: cd13cdbda929cce610b39fbf7f2c6aa59e55cfc58f13b38c592d7eb45b19d5110bcb81150607a88f8644959f5d0a384467a2083d29c12e224c010a406377649b + languageName: node + linkType: hard + "socks-proxy-agent@npm:^7.0.0": version: 7.0.0 resolution: "socks-proxy-agent@npm:7.0.0" @@ -26636,29 +26795,6 @@ __metadata: languageName: node linkType: hard -"standup-mattermost-bot@workspace:standup-mattermost-bot": - version: 0.0.0-use.local - resolution: "standup-mattermost-bot@workspace:standup-mattermost-bot" - dependencies: - "@breejs/later": ^4.1.0 - "@koa/cors": ^3.4.1 - "@koa/router": ^12.0.0 - "@types/koa": ^2.13.5 - "@types/koa__cors": ^3.3.0 - "@types/koa__router": ^12.0.0 - "@types/node": ^18.8.3 - "@types/safe-timers": ^1.1.0 - bree: ^9.1.2 - dotenv: ^16.0.3 - got: ^12.5.1 - koa: ^2.13.4 - mongodb: ^4.10.0 - nodemon: ^2.0.20 - ts-node: ^10.9.1 - typescript: ^4.8.4 - languageName: unknown - linkType: soft - "state-toggle@npm:^1.0.0": version: 1.0.3 resolution: "state-toggle@npm:1.0.3" @@ -28270,6 +28406,13 @@ __metadata: languageName: node linkType: hard +"ua-parser-js@npm:^0.7.30": + version: 0.7.31 + resolution: "ua-parser-js@npm:0.7.31" + checksum: e2f8324a83d1715601576af85b2b6c03890699aaa7272950fc77ea925c70c5e4f75060ae147dc92124e49f7f0e3d6dd2b0a91e7f40d267e92df8894be967ba8b + languageName: node + linkType: hard + "uglify-js@npm:^3.1.4": version: 3.16.2 resolution: "uglify-js@npm:3.16.2" @@ -29937,6 +30080,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:~8.2.3": + version: 8.2.3 + resolution: "ws@npm:8.2.3" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: c869296ccb45f218ac6d32f8f614cd85b50a21fd434caf11646008eef92173be53490810c5c23aea31bc527902261fbfd7b062197eea341b26128d4be56a85e4 + languageName: node + linkType: hard + "x-default-browser@npm:^0.4.0": version: 0.4.0 resolution: "x-default-browser@npm:0.4.0" @@ -29975,6 +30133,13 @@ __metadata: languageName: node linkType: hard +"xmlhttprequest-ssl@npm:~2.0.0": + version: 2.0.0 + resolution: "xmlhttprequest-ssl@npm:2.0.0" + checksum: 1e98df67f004fec15754392a131343ea92e6ab5ac4d77e842378c5c4e4fd5b6a9134b169d96842cc19422d77b1606b8df84a5685562b3b698cb68441636f827e + languageName: node + linkType: hard + "xtend@npm:^4.0.0, xtend@npm:^4.0.1, xtend@npm:~4.0.0, xtend@npm:~4.0.1": version: 4.0.2 resolution: "xtend@npm:4.0.2" @@ -30107,6 +30272,13 @@ __metadata: languageName: node linkType: hard +"yeast@npm:0.1.2": + version: 0.1.2 + resolution: "yeast@npm:0.1.2" + checksum: 81a250b69f601fed541e9518eb2972e75631dd81231689503d7f288612d4eec793b29c208d6807fd6bfc4c2a43614d0c6db233739a4ae6223e244aaed6a885c0 + languageName: node + linkType: hard + "ylru@npm:^1.2.0": version: 1.3.2 resolution: "ylru@npm:1.3.2"