diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 913e76deb..0fbe7613f 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: node-version: - - 16.14.0-buster + - 18.16.0-buster container: image: node:${{ matrix.node-version }} @@ -108,7 +108,7 @@ jobs: - name: Login to GAR uses: docker/login-action@v1 with: - registry: us-east4-docker.pkg.dev + registry: us-west1-docker.pkg.dev username: _json_key password: ${{ secrets.GCR_JSON_KEY }} @@ -120,8 +120,8 @@ jobs: build-args: | SPOKE_VERSION=${{ steps.image-tags.outputs.version }} tags: | - us-east4-docker.pkg.dev/spoke-rewired/spoke/core:latest - us-east4-docker.pkg.dev/spoke-rewired/spoke/core:${{ steps.image-tags.outputs.version }} - us-east4-docker.pkg.dev/spoke-rewired/spoke/core:${{ steps.image-tags.outputs.sha }} + us-west1-docker.pkg.dev/spoke-407503/spoke/core:latest + us-west1-docker.pkg.dev/spoke-407503/spoke/core:${{ steps.image-tags.outputs.version }} + us-west1-docker.pkg.dev/spoke-407503/spoke/core:${{ steps.image-tags.outputs.sha }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1b89252c8..455a1d8bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: cut-gh-release: runs-on: ubuntu-latest env: - DOCKER_IMAGE: us-east4-docker.pkg.dev/spoke-rewired/spoke/core + DOCKER_IMAGE: us-west1-docker.pkg.dev/spoke-407503/spoke/core BODY_PATH: release-body.txt steps: - name: Checkout diff --git a/.nvmrc b/.nvmrc index 0cf077e6b..b492b0863 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16.14 +18.16 diff --git a/CHANGELOG.md b/CHANGELOG.md index bf681b6f7..e9ba3ef31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [8.0.1](https://github.com/politics-rewired/spoke/compare/v8.0.0...v8.0.1) (2024-02-15) + + +### Bug Fixes + +* **mms:** get feature for max sms segment length ([#4](https://github.com/politics-rewired/spoke/issues/4)) ([f19b366](https://github.com/politics-rewired/spoke/commit/f19b366e2c83a742409756d6544644fed5dd274c)) + +## [8.0.0](https://github.com/politics-rewired/spoke/compare/v7.3.0...v8.0.0) (2024-02-15) + + +### Features + +* support sending mms without media ([#2](https://github.com/politics-rewired/spoke/issues/2)) ([3db7d25](https://github.com/politics-rewired/spoke/commit/3db7d25474601b087895be04aa2edc92d00bbb13)) + ## [7.3.0](https://github.com/politics-rewired/spoke/compare/v7.2.0...v7.3.0) (2023-12-10) diff --git a/Dockerfile b/Dockerfile index e777a5d2e..f446fb055 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN jq '{ dependencies, devDependencies, resolutions }' < /tmp/package.json > /t ### Fat Build ### ------------------------- -FROM node:16.14.0 AS builder +FROM node:18.16.0 AS builder WORKDIR /usr/Spoke @@ -36,7 +36,7 @@ RUN yarn run build ### Slim Deploy ### ------------------------- -FROM node:16.14.0 +FROM node:18.16.0 WORKDIR /usr/Spoke diff --git a/README.md b/README.md index a2e61e6e9..ab64f8327 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,12 @@ that goal. See [`HOWTO_migrate-from-moveon-main.md`](./docs/HOWTO_migrate-from-m Runtimes and package managers: -- Node (^16.14) -- Yarn (>= 1.19.1) +- Node (^18.16) -- See [How to Install Node](https://nodejs.dev/learn/how-to-install-nodejs) +- Yarn (>= 1.19.1) -- See [Installing Yarn](https://classic.yarnpkg.com/en/docs/install) External services: -- Postgres (>= 11) +- Postgres (>= 11) -- See [install](https://postgresql.org/download) and [start](https://www.postgresql.org/docs/current/server-start.html) documentation Recommended: diff --git a/libs/gql-schema/organization-settings.ts b/libs/gql-schema/organization-settings.ts index b3110fca4..326e4b2d4 100644 --- a/libs/gql-schema/organization-settings.ts +++ b/libs/gql-schema/organization-settings.ts @@ -13,6 +13,7 @@ export const schema = ` showDoNotAssignMessage: Boolean doNotAssignMessage: String defaultAutosendingControlsMode: AutosendingControlsMode + maxSmsSegmentLength: Int # Superadmin startCampaignRequiresApproval: Boolean @@ -28,6 +29,7 @@ export const schema = ` confirmationClickForScriptLinks: Boolean! showDoNotAssignMessage: Boolean! doNotAssignMessage: String! + maxSmsSegmentLength: Int # Supervolunteer startCampaignRequiresApproval: Boolean diff --git a/libs/spoke-codegen/src/graphql/general-settings.graphql b/libs/spoke-codegen/src/graphql/general-settings.graphql index 96a20c03e..bd2cf064c 100644 --- a/libs/spoke-codegen/src/graphql/general-settings.graphql +++ b/libs/spoke-codegen/src/graphql/general-settings.graphql @@ -76,3 +76,26 @@ mutation UpdateAutosendingSettings( defaultAutosendingControlsMode } } + +query GetMessageSendingSettings($organizationId: String!) { + organization(id: $organizationId) { + id + settings { + id + maxSmsSegmentLength + } + } +} + +mutation UpdateMessageSendingSettings( + $organizationId: String! + $maxSmsSegmentLength: Int +) { + editOrganizationSettings( + id: $organizationId + input: { maxSmsSegmentLength: $maxSmsSegmentLength } + ) { + id + maxSmsSegmentLength + } +} diff --git a/libs/spoke-codegen/src/graphql/spoke-context.graphql b/libs/spoke-codegen/src/graphql/spoke-context.graphql index 61f05a4a9..ccdc523c8 100644 --- a/libs/spoke-codegen/src/graphql/spoke-context.graphql +++ b/libs/spoke-codegen/src/graphql/spoke-context.graphql @@ -20,4 +20,5 @@ fragment OrganizationSettingsInfo on OrganizationSettings { scriptPreviewForSupervolunteers defaultCampaignBuilderMode defaultAutosendingControlsMode + maxSmsSegmentLength } diff --git a/package.json b/package.json index 7c4d417cc..43f4c18f5 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "spoke", - "version": "7.3.0", + "version": "8.0.1", "description": "Spoke", "main": "src/server", "engines": { "npm": "please-use-yarn", "yarn": ">= 1.19.1", - "node": "^16.14.0" + "node": "^18.16.0" }, "scripts": { "preinstall": "node -e \"if(process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('spoke must be installed with Yarn: https://yarnpkg.com/')\"", @@ -129,7 +129,7 @@ "graphql-type-json": "^0.3.2", "graphql-upload": "^13.0.0", "history": "^5.0.0", - "hot-shots": "^8.3.0", + "hot-shots": "^10.0.0", "humps": "^1.1.0", "iconv-lite": "^0.5.1", "immer": "^8.0.1", diff --git a/src/components/MessageLengthInfo.tsx b/src/components/MessageLengthInfo.tsx index 6dabe7e59..e81e72407 100644 --- a/src/components/MessageLengthInfo.tsx +++ b/src/components/MessageLengthInfo.tsx @@ -1,14 +1,13 @@ -import { getCharCount } from "@trt2/gsm-charset-utils"; import PropTypes from "prop-types"; import React from "react"; -import { replaceEasyGsmWins } from "../lib/charset-utils"; +import { getSpokeCharCount } from "../lib/charset-utils"; const MessageLengthInfo: React.SFC<{ messageText: string }> = ({ messageText }) => { - const { charCount, msgCount, charsPerSegment } = getCharCount( - replaceEasyGsmWins(messageText) + const { charCount, msgCount, charsPerSegment } = getSpokeCharCount( + messageText ); const segmentInfo = msgCount === 1 ? "(1 segment)" : `(${msgCount} segments)`; diff --git a/src/components/ScriptEditor.tsx b/src/components/ScriptEditor.tsx index 936765305..616db1074 100644 --- a/src/components/ScriptEditor.tsx +++ b/src/components/ScriptEditor.tsx @@ -2,7 +2,6 @@ import { withApollo } from "@apollo/client/react/hoc"; import { blue, green, grey, orange, red } from "@material-ui/core/colors"; import type { CampaignVariable } from "@spoke/spoke-codegen"; import { IsValidAttachmentDocument } from "@spoke/spoke-codegen"; -import { getCharCount } from "@trt2/gsm-charset-utils"; import type { ContentBlock } from "draft-js"; import { CompositeDecorator, @@ -14,7 +13,7 @@ import { import escapeRegExp from "lodash/escapeRegExp"; import React from "react"; -import { replaceEasyGsmWins } from "../lib/charset-utils"; +import { getSpokeCharCount, replaceEasyGsmWins } from "../lib/charset-utils"; import { delimit, getAttachmentLink, getMessageType } from "../lib/scripts"; import baseTheme from "../styles/theme"; import Chip from "./Chip"; @@ -122,6 +121,7 @@ interface Props { scriptFields: string[]; campaignVariables: CampaignVariable[]; integrationSourced: boolean; + maxSmsSegmentLength: number | null; onChange: (value: string) => Promise | void; receiveFocus?: boolean; } @@ -319,7 +319,7 @@ class ScriptEditor extends React.Component { renderAttachmentWarning() { const text = this.state.editorState.getCurrentContent().getPlainText(); - const messageType = getMessageType(text); + const messageType = getMessageType(text, this.props.maxSmsSegmentLength); if (messageType === "MMS" && !this.state.validAttachment) { return (
@@ -369,8 +369,8 @@ class ScriptEditor extends React.Component { render() { const text = this.state.editorState.getCurrentContent().getPlainText(); - const info = getCharCount(replaceEasyGsmWins(text)); - const messageType = getMessageType(text); + const info = getSpokeCharCount(text); + const messageType = getMessageType(text, this.props.maxSmsSegmentLength); return (
diff --git a/src/components/forms/GSScriptField.jsx b/src/components/forms/GSScriptField.jsx index 5dab5a556..c2ceca3a8 100644 --- a/src/components/forms/GSScriptField.jsx +++ b/src/components/forms/GSScriptField.jsx @@ -116,6 +116,7 @@ class GSScriptField extends GSFormField { scriptFields={scriptFields} campaignVariables={campaignVariables} integrationSourced={integrationSourced} + maxSmsSegmentLength={orgSettings.maxSmsSegmentLength} expandable onChange={(val) => this.setState({ script: val })} /> diff --git a/src/components/forms/GSScriptOptionsField.jsx b/src/components/forms/GSScriptOptionsField.jsx index 769d53db2..3611cdc1b 100644 --- a/src/components/forms/GSScriptOptionsField.jsx +++ b/src/components/forms/GSScriptOptionsField.jsx @@ -188,6 +188,7 @@ class GSScriptOptionsField extends GSFormField { scriptFields={scriptFields} campaignVariables={campaignVariables} integrationSourced={integrationSourced} + maxSmsSegmentLength={orgSettings.maxSmsSegmentLength} receiveFocus expandable onChange={(val) => this.setState({ scriptDraft: val.trim() })} diff --git a/src/containers/Settings/components/General.jsx b/src/containers/Settings/components/General.jsx index d6c6a93da..5f55b990b 100644 --- a/src/containers/Settings/components/General.jsx +++ b/src/containers/Settings/components/General.jsx @@ -25,6 +25,7 @@ import { loadData } from "../../hoc/with-operations"; import AutosendingSettingsCard from "./AutosendingSettingsCard"; import CampaignBuilderSettingsCard from "./CampaignBuilderSettingsCard"; import EditName from "./EditName"; +import MessageSendingSettingsCard from "./MessageSendingSettingsCard"; import RejectedTextersMessageCard from "./RejectedTextersMessageCard"; import Review10DlcInfo from "./Review10DlcInfo"; import ScriptPreviewSettingsCard from "./ScriptPreviewSettingsCard"; @@ -473,6 +474,11 @@ class Settings extends React.Component { style={{ marginBottom: 20 }} /> + + {window.ENABLE_TROLLBOT && ( = ( + props +) => { + const { organizationId, style } = props; + + const getSettingsState = useGetMessageSendingSettingsQuery({ + variables: { organizationId } + }); + const settings = getSettingsState?.data?.organization?.settings; + + const [ + setMaxSmsSegmentLength, + updateState + ] = useUpdateMessageSendingSettingsMutation(); + + const working = getSettingsState.loading || updateState.loading; + const errorMsg = + getSettingsState.error?.message ?? updateState.error?.message; + + const maxSmsSegmentLength = settings?.maxSmsSegmentLength; + const showMaxSmsSegmentLength = maxSmsSegmentLength !== null; + + const setNewMaxSmsSegmentLength = async ( + newMax: number | null | undefined + ) => { + await setMaxSmsSegmentLength({ + variables: { + organizationId, + maxSmsSegmentLength: newMax + } + }); + }; + + // eslint-disable-next-line max-len + const handleChangeMaxSmsSegmentLength: React.ChangeEventHandler = async ( + event + ) => { + if (working) return; + const newMax = event.target.valueAsNumber; + await setNewMaxSmsSegmentLength(newMax); + }; + + // eslint-disable-next-line max-len + const handleToggleMmsConversion: React.ChangeEventHandler = async ( + event + ) => { + const { checked } = event.target; + const DEFAULT_MAX_SMS_SEGMENT_LENGTH = 3; + const newMax = checked ? DEFAULT_MAX_SMS_SEGMENT_LENGTH : null; + await setNewMaxSmsSegmentLength(newMax); + }; + + return ( + + + + {errorMsg && Error: {errorMsg}} +

+ Turn on this feature to automatically convert long SMS messages to + MMS. If turned on, SMS messages longer than the length you set + will be converted. For example, if you set the max length to 3, a 4 + segment SMS message will be converted to MMS. +

+

+ Messages longer than 3 segments are usually cheaper to send as MMS. + You may notice changes in deliverability when switching from SMS to + MMS messages.{" "} + + Learn more about sending MMS here + +

+ + } + /> + {showMaxSmsSegmentLength && ( + + )} +
+
+ ); +}; + +export default MessageSendingSettingsCard; diff --git a/src/lib/charset-utils.ts b/src/lib/charset-utils.ts index 3a7c6adfb..26046ff49 100644 --- a/src/lib/charset-utils.ts +++ b/src/lib/charset-utils.ts @@ -1,3 +1,5 @@ +import { getCharCount } from "@trt2/gsm-charset-utils"; + const gsmReplacements = [ ["‘", "'"], ["’", "'"], @@ -13,5 +15,8 @@ export const replaceEasyGsmWins = (text: string) => text ); +export const getSpokeCharCount = (text: string) => + getCharCount(replaceEasyGsmWins(text)); + export const replaceCurlyApostrophes = (rawText: string) => rawText.replace(/[\u2018\u2019]/g, "'"); diff --git a/src/lib/scripts.ts b/src/lib/scripts.ts index 8b56c8508..06a600a0d 100644 --- a/src/lib/scripts.ts +++ b/src/lib/scripts.ts @@ -1,9 +1,12 @@ -import type { CampaignVariable } from "@spoke/spoke-codegen"; +import type { + CampaignContact, + CampaignVariable, + User +} from "@spoke/spoke-codegen"; import escapeRegExp from "lodash/escapeRegExp"; import isNil from "lodash/isNil"; -import type { CampaignContact } from "../api/campaign-contact"; -import type { User } from "../api/user"; +import { getSpokeCharCount } from "./charset-utils"; export const delimiters = { startDelimiter: "{", @@ -37,7 +40,7 @@ const TITLE_CASE_FIELDS = [ "texterLastName" ]; -const mediaExtractor = /\[\s*(http[^\]\s]*)\s*\]/; +export const mediaExtractor = /\[\s*(http[^\]\s]*)\s*\]/; // Special first names that should not be capitalized const LOWERCASE_FIRST_NAMES = ["friend", "there"]; @@ -131,8 +134,17 @@ export const getAttachmentLink = (text: string) => { return null; }; -export const getMessageType = (text: string) => { - return mediaExtractor.test(text) ? "MMS" : "SMS"; +export const getMessageType = ( + text: string, + maxSmsSegmentLength: number | null +) => { + const { msgCount } = getSpokeCharCount(text); + if ( + mediaExtractor.test(text) || + (maxSmsSegmentLength && msgCount > maxSmsSegmentLength) + ) + return "MMS"; + return "SMS"; }; export enum ScriptTokenType { diff --git a/src/schema.graphql b/src/schema.graphql index ea14a2ca9..a11696ae1 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -482,6 +482,7 @@ input OrganizationSettingsInput { showDoNotAssignMessage: Boolean doNotAssignMessage: String defaultAutosendingControlsMode: AutosendingControlsMode + maxSmsSegmentLength: Int # Superadmin startCampaignRequiresApproval: Boolean @@ -497,6 +498,7 @@ type OrganizationSettings { confirmationClickForScriptLinks: Boolean! showDoNotAssignMessage: Boolean! doNotAssignMessage: String! + maxSmsSegmentLength: Int # Supervolunteer startCampaignRequiresApproval: Boolean diff --git a/src/server/api/lib/assemble-numbers.ts b/src/server/api/lib/assemble-numbers.ts index 3ddccbc74..3195a4748 100644 --- a/src/server/api/lib/assemble-numbers.ts +++ b/src/server/api/lib/assemble-numbers.ts @@ -1,11 +1,16 @@ +import type { Knex } from "knex"; +import type { PoolClient } from "pg"; + import { config } from "../../../config"; import { getFormattedPhoneNumber } from "../../../lib/phone-format"; +import { getMessageType } from "../../../lib/scripts"; import { stringIsAValidUrl } from "../../../lib/utils"; import logger from "../../../logger"; import { makeNumbersClient } from "../../lib/assemble-numbers"; import { r } from "../../models"; import statsd from "../../statsd"; import { errToObj } from "../../utils"; +import { getOrgFeature } from "../organization-settings"; import type { MessagingServiceRecord, RequestHandlerFactory } from "../types"; import { MessagingServiceType } from "../types"; import { symmetricDecrypt } from "./crypto"; @@ -174,8 +179,23 @@ export const sendMessage = async ( .reader("campaign_contact") .where({ id: campaignContactId }) .first("zip"); + + const { features } = await r + .reader("organization") + .where({ id: organizationId }) + .first("features") + .catch(() => ({})); + + const maxSmsSegmentLength = getOrgFeature("maxSmsSegmentLength", features); const { body, mediaUrl } = messageComponents(messageText); - const mediaUrls = mediaUrl ? [mediaUrl] : undefined; + + const mediaUrls = mediaUrl + ? [mediaUrl] + : // check if we should convert to switchboard format for empty mms + getMessageType(body, maxSmsSegmentLength) === "MMS" + ? [] + : undefined; + const messageInput: NumbersOutboundMessagePayload = { profileId, to, diff --git a/src/server/api/organization-settings.ts b/src/server/api/organization-settings.ts index 5f0f21205..7245703ad 100644 --- a/src/server/api/organization-settings.ts +++ b/src/server/api/organization-settings.ts @@ -28,6 +28,7 @@ interface IOrganizationSettings { doNotAssignMessage: string; defaultCampaignBuilderMode: CampaignBuilderMode; defaultAutosendingControlsMode: AutosendingControlsMode; + maxSmsSegmentLength: number | null; } const SETTINGS_PERMISSIONS: { @@ -45,7 +46,8 @@ const SETTINGS_PERMISSIONS: { defaultAutosendingControlsMode: UserRoleType.ADMIN, defaulTexterApprovalStatus: UserRoleType.OWNER, numbersApiKey: UserRoleType.OWNER, - trollbotWebhookUrl: UserRoleType.OWNER + trollbotWebhookUrl: UserRoleType.OWNER, + maxSmsSegmentLength: UserRoleType.TEXTER }; const SETTINGS_WRITE_PERMISSIONS: { @@ -63,7 +65,8 @@ const SETTINGS_WRITE_PERMISSIONS: { showDoNotAssignMessage: UserRoleType.OWNER, doNotAssignMessage: UserRoleType.OWNER, defaultAutosendingControlsMode: UserRoleType.OWNER, - startCampaignRequiresApproval: UserRoleType.SUPERADMIN + startCampaignRequiresApproval: UserRoleType.SUPERADMIN, + maxSmsSegmentLength: UserRoleType.OWNER }; const SETTINGS_NAMES: Partial< @@ -86,7 +89,8 @@ const SETTINGS_DEFAULTS: IOrganizationSettings = { doNotAssignMessage: "Your ability to request texts has been put on hold. Please a contact a text team leader for more information.", defaultCampaignBuilderMode: CampaignBuilderMode.Advanced, - defaultAutosendingControlsMode: AutosendingControlsMode.Detailed + defaultAutosendingControlsMode: AutosendingControlsMode.Detailed, + maxSmsSegmentLength: 3 }; const SETTINGS_TRANSFORMERS: Partial< @@ -133,13 +137,17 @@ export const getOrgFeature = ( const finalName = SETTINGS_NAMES[featureName] ?? featureName; try { const features = JSON.parse(rawFeatures); - const value = features[finalName] ?? defaultValue ?? null; + + const foundValue = features[finalName]; + const returnValue = + foundValue === undefined ? defaultValue ?? null : foundValue; + const transformer = SETTINGS_TRANSFORMERS[featureName]; - if (transformer && value) { - const result = transformer(value); + if (transformer && returnValue) { + const result = transformer(returnValue); return result as IOrganizationSettings[T]; } - return value; + return returnValue; } catch (_err) { return SETTINGS_DEFAULTS[featureName] ?? null; } @@ -183,7 +191,8 @@ export const resolvers = { "defaultCampaignBuilderMode", "showDoNotAssignMessage", "doNotAssignMessage", - "defaultAutosendingControlsMode" + "defaultAutosendingControlsMode", + "maxSmsSegmentLength" ]) } }; diff --git a/src/server/organization-settings.spec.ts b/src/server/organization-settings.spec.ts index ae9337ccc..73400714b 100644 --- a/src/server/organization-settings.spec.ts +++ b/src/server/organization-settings.spec.ts @@ -37,7 +37,8 @@ describe("get organization settings", () => { defaultAutosendingControlsMode: AutosendingControlsMode.Basic, defaulTexterApprovalStatus: RequestAutoApproveType.APPROVAL_REQUIRED, numbersApiKey: "SomethingSecret", - trollbotWebhookUrl: "https://rewired.coop/trolls" + trollbotWebhookUrl: "https://rewired.coop/trolls", + maxSmsSegmentLength: 3 }; const makeSettingsRequest = async ( @@ -68,6 +69,7 @@ describe("get organization settings", () => { defaulTexterApprovalStatus numbersApiKey trollbotWebhookUrl + maxSmsSegmentLength } } } @@ -135,6 +137,7 @@ describe("get organization settings", () => { ); expect(settings.numbersApiKey).not.toBeNull(); expect(settings.trollbotWebhookUrl).toEqual(features.trollbotWebhookUrl); + expect(settings.maxSmsSegmentLength).toEqual(features.maxSmsSegmentLength); }); it("returns the correct role required", () => { diff --git a/yarn.lock b/yarn.lock index 72c28414b..383322d19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8859,7 +8859,7 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== -bindings@^1.3.0, bindings@^1.5.0: +bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== @@ -15211,12 +15211,12 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c" integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg== -hot-shots@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/hot-shots/-/hot-shots-8.3.0.tgz#360422464e8d7c5389753723bebaa969bb6f20d2" - integrity sha512-bdhBiiIMFhn9cXYJMtDKWM7O15iSLS14O5KetoHnyYOZdFr0A3cCtHA4byzkS3E72xFbqWFg4lqJ2EN/33J7UQ== +hot-shots@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/hot-shots/-/hot-shots-10.0.0.tgz#d360f9dd252da78297aca1cb08fd84a8936739c2" + integrity sha512-uy/uGpuJk7yuyiKRfZMBNkF1GAOX5O2ifO9rDCaX9jw8fu6eW9QeWC7WRPDI+O98frW1HQgV3+xwjWsZPECIzQ== optionalDependencies: - unix-dgram "2.0.x" + unix-dgram "2.x" hpack.js@^2.1.6: version "2.1.6" @@ -20149,11 +20149,16 @@ named-placeholders@^1.1.2: dependencies: lru-cache "^4.1.3" -nan@^2.12.1, nan@^2.13.2: +nan@^2.12.1: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== +nan@^2.16.0: + version "2.17.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" + integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== + nanoclone@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" @@ -27324,13 +27329,13 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -unix-dgram@2.0.x: - version "2.0.3" - resolved "https://registry.yarnpkg.com/unix-dgram/-/unix-dgram-2.0.3.tgz#511fc1f8ed94ffe40e0dfeacdae1bf8b06533296" - integrity sha512-Bay5CkSLcdypcBCsxvHEvaG3mftzT5FlUnRToPWEAVxwYI8NI/8zSJ/Gknlp86MPhV6hBA8I8TBsETj2tssoHQ== +unix-dgram@2.x: + version "2.0.6" + resolved "https://registry.yarnpkg.com/unix-dgram/-/unix-dgram-2.0.6.tgz#6d567b0eb6d7a9504e561532b598a46e34c5968b" + integrity sha512-AURroAsb73BZ6CdAyMrTk/hYKNj3DuYYEuOaB8bYMOHGKupRNScw90Q5C71tWJc3uE7dIeXRyuwN0xLLq3vDTg== dependencies: - bindings "^1.3.0" - nan "^2.13.2" + bindings "^1.5.0" + nan "^2.16.0" unixify@^1.0.0: version "1.0.0"