Merge branch 'feat/send-webhook'

This commit is contained in:
Maël Gangloff
2024-08-17 22:37:09 +02:00
24 changed files with 1197 additions and 144 deletions

View File

@@ -8,7 +8,7 @@ import {Connector} from "../../../utils/api/connectors";
export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}: { export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}: {
watchlist: Watchlist, watchlist: Watchlist,
onUpdateWatchlist: (values: { domains: string[], emailTriggers: string[], token: string }) => Promise<void>, onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
connectors: (Connector & { id: string })[] connectors: (Connector & { id: string })[]
}) { }) {
@@ -35,7 +35,8 @@ export function UpdateWatchlistButton({watchlist, onUpdateWatchlist, connectors}
{name: 'name', value: watchlist.name}, {name: 'name', value: watchlist.name},
{name: 'connector', value: watchlist.connector?.id}, {name: 'connector', value: watchlist.connector?.id},
{name: 'domains', value: watchlist.domains.map(d => d.ldhName)}, {name: 'domains', value: watchlist.domains.map(d => d.ldhName)},
{name: 'emailTriggers', value: watchlist.triggers?.map(t => t.event)}, {name: 'triggers', value: [...new Set(watchlist.triggers?.map(t => t.event))]},
{name: 'dsn', value: watchlist.dsn}
]) ])
}}/> }}/>
</Typography.Link> </Typography.Link>

View File

@@ -1,4 +1,4 @@
import {Button, Form, FormInstance, Input, Select, SelectProps, Space, Tag} from "antd"; import {Button, Form, FormInstance, Input, Select, SelectProps, Space, Tag, Typography} from "antd";
import {t} from "ttag"; import {t} from "ttag";
import {ApiOutlined, MinusCircleOutlined, PlusOutlined} from "@ant-design/icons"; import {ApiOutlined, MinusCircleOutlined, PlusOutlined} from "@ant-design/icons";
import React from "react"; import React from "react";
@@ -28,7 +28,7 @@ const formItemLayoutWithOutLabel = {
export function WatchlistForm({form, connectors, onFinish, isCreation}: { export function WatchlistForm({form, connectors, onFinish, isCreation}: {
form: FormInstance, form: FormInstance,
connectors: (Connector & { id: string })[] connectors: (Connector & { id: string })[]
onFinish: (values: { domains: string[], emailTriggers: string[], token: string }) => void onFinish: (values: { domains: string[], triggers: string[], token: string }) => void
isCreation: boolean isCreation: boolean
}) { }) {
const domainEventTranslated = domainEvent() const domainEventTranslated = domainEvent()
@@ -56,7 +56,7 @@ export function WatchlistForm({form, connectors, onFinish, isCreation}: {
{...formItemLayoutWithOutLabel} {...formItemLayoutWithOutLabel}
form={form} form={form}
onFinish={onFinish} onFinish={onFinish}
initialValues={{emailTriggers: ['last changed', 'transfer', 'expiration', 'deletion']}} initialValues={{triggers: ['last changed', 'transfer', 'expiration', 'deletion']}}
> >
<Form.Item name='token' hidden> <Form.Item name='token' hidden>
@@ -140,7 +140,7 @@ export function WatchlistForm({form, connectors, onFinish, isCreation}: {
)} )}
</Form.List> </Form.List>
<Form.Item label={t`Tracked events`} <Form.Item label={t`Tracked events`}
name='emailTriggers' name='triggers'
rules={[{required: true, message: t`At least one trigger`, type: 'array'}]} rules={[{required: true, message: t`At least one trigger`, type: 'array'}]}
labelCol={{ labelCol={{
xs: {span: 24}, xs: {span: 24},
@@ -186,7 +186,58 @@ export function WatchlistForm({form, connectors, onFinish, isCreation}: {
}))} }))}
/> />
</Form.Item> </Form.Item>
<Form.Item> <Form.List
name="dsn">
{(fields, {add, remove}, {errors}) => (
<>
{fields.map((field, index) => (
<Form.Item
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
label={index === 0 ? t`DSN` : ''}
required={true}
key={field.key}
>
<Form.Item
{...field}
validateTrigger={['onChange', 'onBlur']}
rules={[{
required: true,
message: t`Required`
}, {
pattern: /:\/\//,
message: t`This DSN does not appear to be valid`
}]}
noStyle
>
<Input placeholder={t`slack://TOKEN@default?channel=CHANNEL`} style={{width: '60%'}} autoComplete='off'/>
</Form.Item>
{fields.length > 1 ? (
<MinusCircleOutlined
className="dynamic-delete-button"
onClick={() => remove(field.name)}
/>
) : null}
</Form.Item>
))}
<Form.Item help={
<Typography.Link href='https://symfony.com/doc/current/notifier.html#chat-channel'>
{t`Check out this link to the Symfony documentation to help you build the DSN`}
</Typography.Link>}
>
<Button
type="dashed"
onClick={() => add()}
style={{width: '60%'}}
icon={<PlusOutlined/>}
>
{t`Add a Webhook`}
</Button>
<Form.ErrorList errors={errors}/>
</Form.Item>
</>
)}
</Form.List>
<Form.Item style={{marginTop: '10px'}}>
<Space> <Space>
<Button type="primary" htmlType="submit"> <Button type="primary" htmlType="submit">
{isCreation ? t`Create` : t`Update`} {isCreation ? t`Create` : t`Update`}

View File

@@ -14,7 +14,7 @@ import {ViewDiagramWatchlistButton} from "./diagram/ViewDiagramWatchlistButton";
export function WatchlistsList({watchlists, onDelete, onUpdateWatchlist, connectors}: { export function WatchlistsList({watchlists, onDelete, onUpdateWatchlist, connectors}: {
watchlists: Watchlist[], watchlists: Watchlist[],
onDelete: () => void, onDelete: () => void,
onUpdateWatchlist: (values: { domains: string[], emailTriggers: string[], token: string }) => Promise<void>, onUpdateWatchlist: (values: { domains: string[], triggers: string[], token: string }) => Promise<void>,
connectors: (Connector & { id: string })[] connectors: (Connector & { id: string })[]
}) { }) {
const sm = useBreakpoint('sm') const sm = useBreakpoint('sm')

View File

@@ -14,6 +14,7 @@ export type Watchlist = {
token: string, token: string,
domains: { ldhName: string }[], domains: { ldhName: string }[],
triggers?: { event: EventAction, action: string }[], triggers?: { event: EventAction, action: string }[],
dsn?: string[]
connector?: { connector?: {
id: string id: string
provider: string provider: string
@@ -22,6 +23,33 @@ export type Watchlist = {
createdAt: string createdAt: string
} }
type FormValuesType = {
name?: string
domains: string[],
triggers: string[]
connector?: string,
dsn?: string[]
}
const getRequestDataFromForm = (values: FormValuesType) => {
const domainsURI = values.domains.map(d => '/api/domains/' + d.toLowerCase())
let triggers = values.triggers.map(t => ({event: t, action: 'email'}))
if (values.dsn !== undefined) {
triggers = [...triggers, ...values.triggers.map(t => ({
event: t,
action: 'chat'
}))]
}
return {
name: values.name,
domains: domainsURI,
triggers,
connector: values.connector !== undefined ? ('/api/connectors/' + values.connector) : undefined,
dsn: values.dsn
}
}
export default function WatchlistPage() { export default function WatchlistPage() {
const [form] = Form.useForm() const [form] = Form.useForm()
@@ -29,19 +57,9 @@ export default function WatchlistPage() {
const [watchlists, setWatchlists] = useState<Watchlist[] | null>() const [watchlists, setWatchlists] = useState<Watchlist[] | null>()
const [connectors, setConnectors] = useState<(Connector & { id: string })[] | null>() const [connectors, setConnectors] = useState<(Connector & { id: string })[] | null>()
const onCreateWatchlist = (values: { const onCreateWatchlist = (values: FormValuesType) => {
name?: string
domains: string[], postWatchlist(getRequestDataFromForm(values)).then((w) => {
emailTriggers: string[]
connector?: string
}) => {
const domainsURI = values.domains.map(d => '/api/domains/' + d.toLowerCase())
postWatchlist({
name: values.name,
domains: domainsURI,
triggers: values.emailTriggers.map(t => ({event: t, action: 'email'})),
connector: values.connector !== undefined ? ('/api/connectors/' + values.connector) : undefined
}).then((w) => {
form.resetFields() form.resetFields()
refreshWatchlists() refreshWatchlists()
messageApi.success(t`Watchlist created !`) messageApi.success(t`Watchlist created !`)
@@ -50,28 +68,16 @@ export default function WatchlistPage() {
}) })
} }
const onUpdateWatchlist = async (values: { const onUpdateWatchlist = async (values: FormValuesType & { token: string }) => putWatchlist({
token: string
name?: string
domains: string[],
emailTriggers: string[]
connector?: string
}) => {
const domainsURI = values.domains.map(d => '/api/domains/' + d.toLowerCase())
return putWatchlist({
token: values.token, token: values.token,
name: values.name, ...getRequestDataFromForm(values)
domains: domainsURI, }
triggers: values.emailTriggers.map(t => ({event: t, action: 'email'})), ).then((w) => {
connector: values.connector !== undefined ? ('/api/connectors/' + values.connector) : undefined refreshWatchlists()
}).then((w) => { messageApi.success(t`Watchlist updated !`)
refreshWatchlists() }).catch((e: AxiosError) => {
messageApi.success(t`Watchlist updated !`) throw showErrorAPI(e, messageApi)
}).catch((e: AxiosError) => { })
throw showErrorAPI(e, messageApi)
})
}
const refreshWatchlists = () => getWatchlists().then(w => { const refreshWatchlists = () => getWatchlists().then(w => {
setWatchlists(w['hydra:member']) setWatchlists(w['hydra:member'])

View File

@@ -69,6 +69,7 @@ export interface WatchlistRequest {
domains: string[], domains: string[],
triggers: { event: EventAction, action: TriggerAction }[], triggers: { event: EventAction, action: TriggerAction }[],
connector?: string connector?: string
dsn?: string[]
} }
export interface Watchlist { export interface Watchlist {
@@ -78,6 +79,7 @@ export interface Watchlist {
triggers: { event: EventAction, action: TriggerAction }[], triggers: { event: EventAction, action: TriggerAction }[],
connector?: string connector?: string
createdAt: string createdAt: string
dsn?: string[]
} }
export interface InstanceConfig { export interface InstanceConfig {

View File

@@ -41,16 +41,20 @@
"symfony/asset": "7.1.*", "symfony/asset": "7.1.*",
"symfony/asset-mapper": "7.1.*", "symfony/asset-mapper": "7.1.*",
"symfony/console": "7.1.*", "symfony/console": "7.1.*",
"symfony/discord-notifier": "7.1.*",
"symfony/doctrine-messenger": "7.1.*", "symfony/doctrine-messenger": "7.1.*",
"symfony/dotenv": "7.1.*", "symfony/dotenv": "7.1.*",
"symfony/expression-language": "7.1.*", "symfony/expression-language": "7.1.*",
"symfony/flex": "^2", "symfony/flex": "^2",
"symfony/form": "7.1.*", "symfony/form": "7.1.*",
"symfony/framework-bundle": "7.1.*", "symfony/framework-bundle": "7.1.*",
"symfony/google-chat-notifier": "7.1.*",
"symfony/http-client": "7.1.*", "symfony/http-client": "7.1.*",
"symfony/intl": "7.1.*", "symfony/intl": "7.1.*",
"symfony/lock": "7.1.*", "symfony/lock": "7.1.*",
"symfony/mailer": "7.1.*", "symfony/mailer": "7.1.*",
"symfony/mattermost-notifier": "7.1.*",
"symfony/microsoft-teams-notifier": "7.1.*",
"symfony/mime": "7.1.*", "symfony/mime": "7.1.*",
"symfony/monolog-bundle": "^3.0", "symfony/monolog-bundle": "^3.0",
"symfony/notifier": "7.1.*", "symfony/notifier": "7.1.*",
@@ -58,12 +62,15 @@
"symfony/property-access": "7.1.*", "symfony/property-access": "7.1.*",
"symfony/property-info": "7.1.*", "symfony/property-info": "7.1.*",
"symfony/rate-limiter": "7.1.*", "symfony/rate-limiter": "7.1.*",
"symfony/rocket-chat-notifier": "7.1.*",
"symfony/runtime": "7.1.*", "symfony/runtime": "7.1.*",
"symfony/scheduler": "7.1.*", "symfony/scheduler": "7.1.*",
"symfony/security-bundle": "7.1.*", "symfony/security-bundle": "7.1.*",
"symfony/serializer": "7.1.*", "symfony/serializer": "7.1.*",
"symfony/slack-notifier": "7.1.*",
"symfony/stimulus-bundle": "^2.18", "symfony/stimulus-bundle": "^2.18",
"symfony/string": "7.1.*", "symfony/string": "7.1.*",
"symfony/telegram-notifier": "7.1.*",
"symfony/translation": "7.1.*", "symfony/translation": "7.1.*",
"symfony/twig-bundle": "7.1.*", "symfony/twig-bundle": "7.1.*",
"symfony/uid": "7.1.*", "symfony/uid": "7.1.*",
@@ -72,6 +79,7 @@
"symfony/web-link": "7.1.*", "symfony/web-link": "7.1.*",
"symfony/webpack-encore-bundle": "^2.1", "symfony/webpack-encore-bundle": "^2.1",
"symfony/yaml": "7.1.*", "symfony/yaml": "7.1.*",
"symfony/zulip-notifier": "7.1.*",
"symfonycasts/verify-email-bundle": "*", "symfonycasts/verify-email-bundle": "*",
"twig/extra-bundle": "^2.12|^3.0", "twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0" "twig/twig": "^2.12|^3.0"

547
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "bab584811b8175e404608e6738549f52", "content-hash": "f64fa606b60efd34dccdee3abcdad8b2",
"packages": [ "packages": [
{ {
"name": "api-platform/core", "name": "api-platform/core",
@@ -4325,6 +4325,74 @@
], ],
"time": "2024-04-18T09:32:20+00:00" "time": "2024-04-18T09:32:20+00:00"
}, },
{
"name": "symfony/discord-notifier",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/discord-notifier.git",
"reference": "f3d8368ca5ff80c1268a851f925e1f0c07997a8e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/discord-notifier/zipball/f3d8368ca5ff80c1268a851f925e1f0c07997a8e",
"reference": "f3d8368ca5ff80c1268a851f925e1f0c07997a8e",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/http-client": "^6.4|^7.0",
"symfony/notifier": "^6.4|^7.0",
"symfony/polyfill-mbstring": "^1.0"
},
"type": "symfony-notifier-bridge",
"autoload": {
"psr-4": {
"Symfony\\Component\\Notifier\\Bridge\\Discord\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Discord Notifier Bridge",
"homepage": "https://symfony.com",
"keywords": [
"discord",
"notifier"
],
"support": {
"source": "https://github.com/symfony/discord-notifier/tree/v7.1.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T14:57:53+00:00"
},
{ {
"name": "symfony/doctrine-bridge", "name": "symfony/doctrine-bridge",
"version": "v7.1.2", "version": "v7.1.2",
@@ -5313,6 +5381,75 @@
], ],
"time": "2024-06-28T08:00:31+00:00" "time": "2024-06-28T08:00:31+00:00"
}, },
{
"name": "symfony/google-chat-notifier",
"version": "v7.1.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/google-chat-notifier.git",
"reference": "1e92b6c89b2182ba26554861dc261c530c98000f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/google-chat-notifier/zipball/1e92b6c89b2182ba26554861dc261c530c98000f",
"reference": "1e92b6c89b2182ba26554861dc261c530c98000f",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/http-client": "^6.4|^7.0",
"symfony/notifier": "^6.4|^7.0"
},
"type": "symfony-notifier-bridge",
"autoload": {
"psr-4": {
"Symfony\\Component\\Notifier\\Bridge\\GoogleChat\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Google Chat Notifier Bridge",
"homepage": "https://symfony.com",
"keywords": [
"Google-Chat",
"chat",
"google",
"notifier"
],
"support": {
"source": "https://github.com/symfony/google-chat-notifier/tree/v7.1.2"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-06-25T19:55:06+00:00"
},
{ {
"name": "symfony/http-client", "name": "symfony/http-client",
"version": "v7.1.2", "version": "v7.1.2",
@@ -5920,6 +6057,73 @@
], ],
"time": "2024-06-28T08:00:31+00:00" "time": "2024-06-28T08:00:31+00:00"
}, },
{
"name": "symfony/mattermost-notifier",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/mattermost-notifier.git",
"reference": "c5ff6774682ab3504a77bbe01f8c1275b4bf48e9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mattermost-notifier/zipball/c5ff6774682ab3504a77bbe01f8c1275b4bf48e9",
"reference": "c5ff6774682ab3504a77bbe01f8c1275b4bf48e9",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/http-client": "^6.4|^7.0",
"symfony/notifier": "^6.4|^7.0"
},
"type": "symfony-notifier-bridge",
"autoload": {
"psr-4": {
"Symfony\\Component\\Notifier\\Bridge\\Mattermost\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Emanuele Panzeri",
"email": "thepanz@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Mattermost Notifier Bridge",
"homepage": "https://symfony.com",
"keywords": [
"Mattermost",
"notifier"
],
"support": {
"source": "https://github.com/symfony/mattermost-notifier/tree/v7.1.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T14:57:53+00:00"
},
{ {
"name": "symfony/messenger", "name": "symfony/messenger",
"version": "v7.1.2", "version": "v7.1.2",
@@ -6006,6 +6210,78 @@
], ],
"time": "2024-06-28T08:00:31+00:00" "time": "2024-06-28T08:00:31+00:00"
}, },
{
"name": "symfony/microsoft-teams-notifier",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/microsoft-teams-notifier.git",
"reference": "546b0368928b5849d08728b7daf5d22a07a052b3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/microsoft-teams-notifier/zipball/546b0368928b5849d08728b7daf5d22a07a052b3",
"reference": "546b0368928b5849d08728b7daf5d22a07a052b3",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/http-client": "^6.4|^7.0",
"symfony/notifier": "^6.4|^7.0"
},
"type": "symfony-notifier-bridge",
"autoload": {
"psr-4": {
"Symfony\\Component\\Notifier\\Bridge\\MicrosoftTeams\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Edouard Lescot",
"email": "edouard.lescot@gmail.com"
},
{
"name": "Oskar Stark",
"email": "oskarstark@googlemail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Microsoft Teams Notifier Bridge",
"homepage": "https://symfony.com",
"keywords": [
"chat",
"microsoft-teams",
"notifier"
],
"support": {
"source": "https://github.com/symfony/microsoft-teams-notifier/tree/v7.1.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T14:57:53+00:00"
},
{ {
"name": "symfony/mime", "name": "symfony/mime",
"version": "v7.1.2", "version": "v7.1.2",
@@ -7319,6 +7595,73 @@
], ],
"time": "2024-05-31T14:57:53+00:00" "time": "2024-05-31T14:57:53+00:00"
}, },
{
"name": "symfony/rocket-chat-notifier",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/rocket-chat-notifier.git",
"reference": "b17bff59107b51753e3e347c5194dc304019daf7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/rocket-chat-notifier/zipball/b17bff59107b51753e3e347c5194dc304019daf7",
"reference": "b17bff59107b51753e3e347c5194dc304019daf7",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/http-client": "^6.4|^7.0",
"symfony/notifier": "^6.4|^7.0"
},
"type": "symfony-notifier-bridge",
"autoload": {
"psr-4": {
"Symfony\\Component\\Notifier\\Bridge\\RocketChat\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jeroen Spee",
"homepage": "https://github.com/Jeroeny"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony RocketChat Notifier Bridge",
"homepage": "https://symfony.com",
"keywords": [
"notifier",
"rocketchat"
],
"support": {
"source": "https://github.com/symfony/rocket-chat-notifier/tree/v7.1.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T14:57:53+00:00"
},
{ {
"name": "symfony/routing", "name": "symfony/routing",
"version": "v7.1.1", "version": "v7.1.1",
@@ -8087,6 +8430,73 @@
], ],
"time": "2024-04-18T09:32:20+00:00" "time": "2024-04-18T09:32:20+00:00"
}, },
{
"name": "symfony/slack-notifier",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/slack-notifier.git",
"reference": "452a17e3935192e6a9a5b16f0443911d67e456af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/slack-notifier/zipball/452a17e3935192e6a9a5b16f0443911d67e456af",
"reference": "452a17e3935192e6a9a5b16f0443911d67e456af",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/http-client": "^6.4|^7.0",
"symfony/notifier": "^6.4|^7.0"
},
"type": "symfony-notifier-bridge",
"autoload": {
"psr-4": {
"Symfony\\Component\\Notifier\\Bridge\\Slack\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Slack Notifier Bridge",
"homepage": "https://symfony.com",
"keywords": [
"notifier",
"slack"
],
"support": {
"source": "https://github.com/symfony/slack-notifier/tree/v7.1.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T14:57:53+00:00"
},
{ {
"name": "symfony/stimulus-bundle", "name": "symfony/stimulus-bundle",
"version": "v2.18.1", "version": "v2.18.1",
@@ -8305,6 +8715,74 @@
], ],
"time": "2024-06-28T09:27:18+00:00" "time": "2024-06-28T09:27:18+00:00"
}, },
{
"name": "symfony/telegram-notifier",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/telegram-notifier.git",
"reference": "521e77470d5b07306c1001c2d1d1bc88474a8035"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/telegram-notifier/zipball/521e77470d5b07306c1001c2d1d1bc88474a8035",
"reference": "521e77470d5b07306c1001c2d1d1bc88474a8035",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/http-client": "^6.4|^7.0",
"symfony/mime": "^6.4|^7.0",
"symfony/notifier": "^6.4|^7.0"
},
"type": "symfony-notifier-bridge",
"autoload": {
"psr-4": {
"Symfony\\Component\\Notifier\\Bridge\\Telegram\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Telegram Notifier Bridge",
"homepage": "https://symfony.com",
"keywords": [
"notifier",
"telegram"
],
"support": {
"source": "https://github.com/symfony/telegram-notifier/tree/v7.1.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T14:57:53+00:00"
},
{ {
"name": "symfony/translation", "name": "symfony/translation",
"version": "v7.1.1", "version": "v7.1.1",
@@ -9404,6 +9882,73 @@
], ],
"time": "2024-05-31T14:57:53+00:00" "time": "2024-05-31T14:57:53+00:00"
}, },
{
"name": "symfony/zulip-notifier",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/zulip-notifier.git",
"reference": "48b3e1ac791d8eac7ee268108865b36de3be5ed2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/zulip-notifier/zipball/48b3e1ac791d8eac7ee268108865b36de3be5ed2",
"reference": "48b3e1ac791d8eac7ee268108865b36de3be5ed2",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/http-client": "^6.4|^7.0",
"symfony/notifier": "^6.4|^7.0"
},
"type": "symfony-notifier-bridge",
"autoload": {
"psr-4": {
"Symfony\\Component\\Notifier\\Bridge\\Zulip\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mohammad Emran Hasan",
"email": "phpfour@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Zulip Notifier Bridge",
"homepage": "https://symfony.com",
"keywords": [
"notifier",
"zulip"
],
"support": {
"source": "https://github.com/symfony/zulip-notifier/tree/v7.1.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T14:57:53+00:00"
},
{ {
"name": "symfonycasts/verify-email-bundle", "name": "symfonycasts/verify-email-bundle",
"version": "v1.17.0", "version": "v1.17.0",

View File

@@ -1,6 +1,14 @@
framework: framework:
notifier: notifier:
chatter_transports: chatter_transports:
zulip: '%env(ZULIP_DSN)%'
telegram: '%env(TELEGRAM_DSN)%'
slack: '%env(SLACK_DSN)%'
rocketchat: '%env(ROCKETCHAT_DSN)%'
microsoftteams: '%env(MICROSOFT_TEAMS_DSN)%'
mattermost: '%env(MATTERMOST_DSN)%'
googlechat: '%env(GOOGLE_CHAT_DSN)%'
discord: '%env(DISCORD_DSN)%'
texter_transports: texter_transports:
channel_policy: channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo # use chat/slack, chat/telegram, sms/twilio or sms/nexmo

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240816185909 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE watch_list ADD webhook_dsn TEXT DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN watch_list.webhook_dsn IS \'(DC2Type:simple_array)\'');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE watch_list DROP webhook_dsn');
}
}

View File

@@ -19,6 +19,10 @@
"@babel/preset-env": "^7.16.0", "@babel/preset-env": "^7.16.0",
"@babel/preset-react": "^7.24.7", "@babel/preset-react": "^7.24.7",
"@fontsource/noto-color-emoji": "^5.0.27", "@fontsource/noto-color-emoji": "^5.0.27",
"@hotwired/stimulus": "^3.0.0",
"@hotwired/turbo": "^7.1.1 || ^8.0",
"@symfony/stimulus-bridge": "^3.2.0",
"@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/assets",
"@symfony/webpack-encore": "^4.0.0", "@symfony/webpack-encore": "^4.0.0",
"@types/axios": "^0.14.0", "@types/axios": "^0.14.0",
"@types/dagre": "^0.7.52", "@types/dagre": "^0.7.52",

View File

@@ -5,4 +5,5 @@ namespace App\Config;
enum TriggerAction: string enum TriggerAction: string
{ {
case SendEmail = 'email'; case SendEmail = 'email';
case SendChat = 'chat';
} }

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Config;
use Symfony\Component\Notifier\Bridge\Discord\DiscordTransportFactory;
use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatTransportFactory;
use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory;
use Symfony\Component\Notifier\Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory;
use Symfony\Component\Notifier\Bridge\RocketChat\RocketChatTransportFactory;
use Symfony\Component\Notifier\Bridge\Slack\SlackTransportFactory;
use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransportFactory;
use Symfony\Component\Notifier\Bridge\Zulip\ZulipTransportFactory;
enum WebhookScheme: string
{
case DISCORD = 'discord';
case GOOGLE_CHAT = 'googlechat';
case MATTERMOST = 'mattermost';
case MICROSOFT_TEAMS = 'microsoftteams';
case ROCKET_CHAT = 'rocketchat';
case SLACK = 'slack';
case TELEGRAM = 'telegram';
case ZULIP = 'zulip';
public function getChatTransportFactory(): string
{
return match ($this) {
WebhookScheme::DISCORD => DiscordTransportFactory::class,
WebhookScheme::GOOGLE_CHAT => GoogleChatTransportFactory::class,
WebhookScheme::MATTERMOST => MattermostTransportFactory::class,
WebhookScheme::MICROSOFT_TEAMS => MicrosoftTeamsTransportFactory::class,
WebhookScheme::ROCKET_CHAT => RocketChatTransportFactory::class,
WebhookScheme::SLACK => SlackTransportFactory::class,
WebhookScheme::TELEGRAM => TelegramTransportFactory::class,
WebhookScheme::ZULIP => ZulipTransportFactory::class
};
}
}

View File

@@ -2,11 +2,13 @@
namespace App\Controller; namespace App\Controller;
use App\Config\WebhookScheme;
use App\Entity\Domain; use App\Entity\Domain;
use App\Entity\DomainEntity; use App\Entity\DomainEntity;
use App\Entity\DomainEvent; use App\Entity\DomainEvent;
use App\Entity\User; use App\Entity\User;
use App\Entity\WatchList; use App\Entity\WatchList;
use App\Notifier\TestChatNotification;
use App\Repository\WatchListRepository; use App\Repository\WatchListRepository;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@@ -32,6 +34,10 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Notifier\Exception\TransportExceptionInterface;
use Symfony\Component\Notifier\Transport\AbstractTransportFactory;
use Symfony\Component\Notifier\Transport\Dsn;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\SerializerInterface;
@@ -45,6 +51,29 @@ class WatchListController extends AbstractController
) { ) {
} }
/**
* @throws TransportExceptionInterface
*/
private function verifyWebhookDSN(WatchList $watchList): void
{
if (null !== $watchList->getWebhookDsn()) {
foreach ($watchList->getWebhookDsn() as $dsnString) {
$dsn = new Dsn($dsnString);
$scheme = $dsn->getScheme();
$webhookScheme = WebhookScheme::tryFrom($scheme);
if (null === $webhookScheme) {
throw new BadRequestHttpException("The DSN scheme ($scheme) is not supported");
}
$transportFactoryClass = $webhookScheme->getChatTransportFactory();
/** @var AbstractTransportFactory $transportFactory */
$transportFactory = new $transportFactoryClass();
$transportFactory->create($dsn)->send((new TestChatNotification())->asChatMessage());
}
}
}
/** /**
* @throws \Exception * @throws \Exception
*/ */
@@ -65,6 +94,8 @@ class WatchListController extends AbstractController
$user = $this->getUser(); $user = $this->getUser();
$watchList->setUser($user); $watchList->setUser($user);
$this->verifyWebhookDSN($watchList);
/* /*
* In the limited version, we do not want a user to be able to register the same domain more than once in their watchlists. * In the limited version, we do not want a user to be able to register the same domain more than once in their watchlists.
* This policy guarantees the equal probability of obtaining a domain name if it is requested by several users. * This policy guarantees the equal probability of obtaining a domain name if it is requested by several users.
@@ -151,6 +182,8 @@ class WatchListController extends AbstractController
$user = $this->getUser(); $user = $this->getUser();
$watchList->setUser($user); $watchList->setUser($user);
$this->verifyWebhookDSN($watchList);
if ($this->getParameter('limited_features')) { if ($this->getParameter('limited_features')) {
if ($watchList->getDomains()->count() > (int) $this->getParameter('limit_max_watchlist_domains')) { if ($watchList->getDomains()->count() > (int) $this->getParameter('limit_max_watchlist_domains')) {
$this->logger->notice('User {username} tried to update a Watchlist. The maximum number of domains has been reached for this Watchlist', [ $this->logger->notice('User {username} tried to update a Watchlist. The maximum number of domains has been reached for this Watchlist', [

View File

@@ -12,6 +12,7 @@ use App\Controller\WatchListController;
use App\Repository\WatchListRepository; use App\Repository\WatchListRepository;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups; use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Attribute\SerializedName; use Symfony\Component\Serializer\Attribute\SerializedName;
@@ -118,6 +119,11 @@ class WatchList
#[Groups(['watchlist:list', 'watchlist:item'])] #[Groups(['watchlist:list', 'watchlist:item'])]
private ?\DateTimeImmutable $createdAt = null; private ?\DateTimeImmutable $createdAt = null;
#[SerializedName('dsn')]
#[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)]
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
private ?array $webhookDsn = null;
public function __construct() public function __construct()
{ {
$this->token = Uuid::v4(); $this->token = Uuid::v4();
@@ -237,4 +243,16 @@ class WatchList
return $this; return $this;
} }
public function getWebhookDsn(): ?array
{
return $this->webhookDsn;
}
public function setWebhookDsn(?array $webhookDsn): static
{
$this->webhookDsn = $webhookDsn;
return $this;
}
} }

View File

@@ -4,43 +4,51 @@ namespace App\MessageHandler;
use App\Config\Connector\ConnectorInterface; use App\Config\Connector\ConnectorInterface;
use App\Config\TriggerAction; use App\Config\TriggerAction;
use App\Entity\Connector; use App\Config\WebhookScheme;
use App\Entity\Domain; use App\Entity\Domain;
use App\Entity\DomainEvent; use App\Entity\DomainEvent;
use App\Entity\User;
use App\Entity\WatchList; use App\Entity\WatchList;
use App\Entity\WatchListTrigger; use App\Entity\WatchListTrigger;
use App\Message\ProcessDomainTrigger; use App\Message\ProcessDomainTrigger;
use App\Notifier\DomainOrderErrorNotification;
use App\Notifier\DomainOrderNotification;
use App\Notifier\DomainUpdateNotification;
use App\Repository\DomainRepository; use App\Repository\DomainRepository;
use App\Repository\WatchListRepository; use App\Repository\WatchListRepository;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Exception\ExceptionInterface;
use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email; use Symfony\Component\Notifier\Recipient\Recipient;
use Symfony\Component\Notifier\Transport\AbstractTransportFactory;
use Symfony\Component\Notifier\Transport\Dsn;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
#[AsMessageHandler] #[AsMessageHandler]
final readonly class ProcessDomainTriggerHandler final readonly class ProcessDomainTriggerHandler
{ {
private Address $sender;
public function __construct( public function __construct(
private string $mailerSenderEmail, string $mailerSenderEmail,
private string $mailerSenderName, string $mailerSenderName,
private MailerInterface $mailer,
private WatchListRepository $watchListRepository, private WatchListRepository $watchListRepository,
private DomainRepository $domainRepository, private DomainRepository $domainRepository,
private KernelInterface $kernel, private KernelInterface $kernel,
private LoggerInterface $logger, private LoggerInterface $logger,
private HttpClientInterface $client private HttpClientInterface $client,
private MailerInterface $mailer
) { ) {
$this->sender = new Address($mailerSenderEmail, $mailerSenderName);
} }
/** /**
* @throws TransportExceptionInterface * @throws TransportExceptionInterface
* @throws \Exception * @throws \Exception
* @throws ExceptionInterface
*/ */
public function __invoke(ProcessDomainTrigger $message): void public function __invoke(ProcessDomainTrigger $message): void
{ {
@@ -70,12 +78,16 @@ final readonly class ProcessDomainTriggerHandler
$connectorProvider->orderDomain($domain, $this->kernel->isDebug()); $connectorProvider->orderDomain($domain, $this->kernel->isDebug());
$this->sendEmailDomainOrdered($domain, $connector, $watchList->getUser()); $email = (new DomainOrderNotification($this->sender, $domain, $connector))
->asEmailMessage(new Recipient($watchList->getUser()->getEmail()));
$this->mailer->send($email->getMessage());
} catch (\Throwable) { } catch (\Throwable) {
$this->logger->warning('Unable to complete purchase. An error message is sent to user {username}.', [ $this->logger->warning('Unable to complete purchase. An error message is sent to user {username}.', [
'username' => $watchList->getUser()->getUserIdentifier(), 'username' => $watchList->getUser()->getUserIdentifier(),
]); ]);
$this->sendEmailDomainOrderError($domain, $watchList->getUser()); $email = (new DomainOrderErrorNotification($this->sender, $domain))
->asEmailMessage(new Recipient($watchList->getUser()->getEmail()));
$this->mailer->send($email->getMessage());
} }
} }
@@ -91,67 +103,29 @@ final readonly class ProcessDomainTriggerHandler
'ldhName' => $message->ldhName, 'ldhName' => $message->ldhName,
'username' => $watchList->getUser()->getUserIdentifier(), 'username' => $watchList->getUser()->getUserIdentifier(),
]); ]);
$recipient = new Recipient($watchList->getUser()->getEmail());
$notification = new DomainUpdateNotification($this->sender, $event);
if (TriggerAction::SendEmail == $watchListTrigger->getAction()) { if (TriggerAction::SendEmail == $watchListTrigger->getAction()) {
$this->sendEmailDomainUpdated($event, $watchList->getUser()); $this->mailer->send($notification->asEmailMessage($recipient)->getMessage());
} elseif (TriggerAction::SendChat == $watchListTrigger->getAction()) {
if (null !== $watchList->getWebhookDsn()) {
foreach ($watchList->getWebhookDsn() as $dsnString) {
$dsn = new Dsn($dsnString);
$scheme = $dsn->getScheme();
$webhookScheme = WebhookScheme::tryFrom($scheme);
if (null !== $webhookScheme) {
$transportFactoryClass = $webhookScheme->getChatTransportFactory();
/** @var AbstractTransportFactory $transportFactory */
$transportFactory = new $transportFactoryClass();
$transportFactory->create($dsn)->send($notification->asChatMessage());
}
}
}
} }
} }
} }
} }
/**
* @throws TransportExceptionInterface
*/
private function sendEmailDomainOrdered(Domain $domain, Connector $connector, User $user): void
{
$email = (new TemplatedEmail())
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
->to($user->getEmail())
->priority(Email::PRIORITY_HIGHEST)
->subject('A domain name has been ordered')
->htmlTemplate('emails/success/domain_ordered.html.twig')
->locale('en')
->context([
'domain' => $domain,
'provider' => $connector->getProvider()->value,
]);
$this->mailer->send($email);
}
/**
* @throws TransportExceptionInterface
*/
private function sendEmailDomainOrderError(Domain $domain, User $user): void
{
$email = (new TemplatedEmail())
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
->to($user->getEmail())
->subject('An error occurred while ordering a domain name')
->htmlTemplate('emails/errors/domain_order.html.twig')
->locale('en')
->context([
'domain' => $domain,
]);
$this->mailer->send($email);
}
/**
* @throws TransportExceptionInterface
*/
private function sendEmailDomainUpdated(DomainEvent $domainEvent, User $user): void
{
$email = (new TemplatedEmail())
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
->to($user->getEmail())
->priority(Email::PRIORITY_HIGHEST)
->subject('A domain name has been changed')
->htmlTemplate('emails/success/domain_updated.html.twig')
->locale('en')
->context([
'event' => $domainEvent,
]);
$this->mailer->send($email);
}
} }

View File

@@ -3,33 +3,36 @@
namespace App\MessageHandler; namespace App\MessageHandler;
use App\Entity\Domain; use App\Entity\Domain;
use App\Entity\User;
use App\Entity\WatchList; use App\Entity\WatchList;
use App\Message\ProcessDomainTrigger; use App\Message\ProcessDomainTrigger;
use App\Message\ProcessWatchListTrigger; use App\Message\ProcessWatchListTrigger;
use App\Notifier\DomainUpdateErrorNotification;
use App\Repository\WatchListRepository; use App\Repository\WatchListRepository;
use App\Service\RDAPService; use App\Service\RDAPService;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Exception\ExceptionInterface; use Symfony\Component\Messenger\Exception\ExceptionInterface;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Address;
use Symfony\Component\Notifier\Recipient\Recipient;
#[AsMessageHandler] #[AsMessageHandler]
final readonly class ProcessWatchListTriggerHandler final readonly class ProcessWatchListTriggerHandler
{ {
private Address $sender;
public function __construct( public function __construct(
private RDAPService $RDAPService, private RDAPService $RDAPService,
private MailerInterface $mailer, private MailerInterface $mailer,
private string $mailerSenderEmail, string $mailerSenderEmail,
private string $mailerSenderName, string $mailerSenderName,
private MessageBusInterface $bus, private MessageBusInterface $bus,
private WatchListRepository $watchListRepository, private WatchListRepository $watchListRepository,
private LoggerInterface $logger private LoggerInterface $logger
) { ) {
$this->sender = new Address($mailerSenderEmail, $mailerSenderName);
} }
/** /**
@@ -63,28 +66,12 @@ final readonly class ProcessWatchListTriggerHandler
'username' => $watchList->getUser()->getUserIdentifier(), 'username' => $watchList->getUser()->getUserIdentifier(),
'error' => $e, 'error' => $e,
]); ]);
$this->sendEmailDomainUpdateError($domain, $watchList->getUser()); $email = (new DomainUpdateErrorNotification($this->sender, $domain))
->asEmailMessage(new Recipient($watchList->getUser()->getEmail()));
$this->mailer->send($email->getMessage());
} }
$this->bus->dispatch(new ProcessDomainTrigger($watchList->getToken(), $domain->getLdhName(), $updatedAt)); $this->bus->dispatch(new ProcessDomainTrigger($watchList->getToken(), $domain->getLdhName(), $updatedAt));
} }
} }
/**
* @throws TransportExceptionInterface
*/
private function sendEmailDomainUpdateError(Domain $domain, User $user): void
{
$email = (new TemplatedEmail())
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
->to($user->getEmail())
->subject('An error occurred while updating a domain name')
->htmlTemplate('emails/errors/domain_update.html.twig')
->locale('en')
->context([
'domain' => $domain,
]);
$this->mailer->send($email);
}
} }

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Notifier;
use App\Entity\Domain;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mime\Address;
use Symfony\Component\Notifier\Message\ChatMessage;
use Symfony\Component\Notifier\Message\EmailMessage;
use Symfony\Component\Notifier\Notification\ChatNotificationInterface;
use Symfony\Component\Notifier\Notification\EmailNotificationInterface;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
use Symfony\Component\Notifier\Recipient\RecipientInterface;
class DomainOrderErrorNotification extends Notification implements ChatNotificationInterface, EmailNotificationInterface
{
public function __construct(
private readonly Address $sender,
private readonly Domain $domain
) {
parent::__construct();
}
public function asChatMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?ChatMessage
{
$ldhName = $this->domain->getLdhName();
$this->subject("Error: Domain Order $ldhName")
->content("Domain name $ldhName tried to be purchased. The attempt failed.")
->importance(Notification::IMPORTANCE_HIGH);
return ChatMessage::fromNotification($this);
}
public function asEmailMessage(EmailRecipientInterface $recipient, ?string $transport = null): EmailMessage
{
return new EmailMessage((new TemplatedEmail())
->from($this->sender)
->to($recipient->getEmail())
->subject('An error occurred while ordering a domain name')
->htmlTemplate('emails/errors/domain_order.html.twig')
->locale('en')
->context([
'domain' => $this->domain,
]));
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Notifier;
use App\Entity\Connector;
use App\Entity\Domain;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Component\Notifier\Message\ChatMessage;
use Symfony\Component\Notifier\Message\EmailMessage;
use Symfony\Component\Notifier\Notification\ChatNotificationInterface;
use Symfony\Component\Notifier\Notification\EmailNotificationInterface;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
use Symfony\Component\Notifier\Recipient\RecipientInterface;
class DomainOrderNotification extends Notification implements ChatNotificationInterface, EmailNotificationInterface
{
public function __construct(
private readonly Address $sender,
private readonly Domain $domain,
private readonly Connector $connector
) {
parent::__construct();
}
public function asChatMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?ChatMessage
{
$ldhName = $this->domain->getLdhName();
$this
->subject("Success: Domain Ordered $ldhName!")
->content("Domain name $ldhName has just been purchased. The API provider did not return an error.")
->importance(Notification::IMPORTANCE_HIGH);
return ChatMessage::fromNotification($this);
}
public function asEmailMessage(EmailRecipientInterface $recipient, ?string $transport = null): EmailMessage
{
return new EmailMessage((new TemplatedEmail())
->from($this->sender)
->to($recipient->getEmail())
->priority(Email::PRIORITY_HIGHEST)
->subject('A domain name has been ordered')
->htmlTemplate('emails/success/domain_ordered.html.twig')
->locale('en')
->context([
'domain' => $this->domain,
'provider' => $this->connector->getProvider()->value,
]));
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Notifier;
use App\Entity\Domain;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mime\Address;
use Symfony\Component\Notifier\Message\ChatMessage;
use Symfony\Component\Notifier\Message\EmailMessage;
use Symfony\Component\Notifier\Notification\ChatNotificationInterface;
use Symfony\Component\Notifier\Notification\EmailNotificationInterface;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
use Symfony\Component\Notifier\Recipient\RecipientInterface;
class DomainUpdateErrorNotification extends Notification implements ChatNotificationInterface, EmailNotificationInterface
{
public function __construct(
private readonly Address $sender,
private readonly Domain $domain
) {
parent::__construct();
}
public function asChatMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?ChatMessage
{
$ldhName = $this->domain->getLdhName();
$this->subject("Error: Domain Update $ldhName")
->content("Domain name $ldhName tried to be updated. The attempt failed.")
->importance(Notification::IMPORTANCE_MEDIUM);
return ChatMessage::fromNotification($this);
}
public function asEmailMessage(EmailRecipientInterface $recipient, ?string $transport = null): EmailMessage
{
return new EmailMessage((new TemplatedEmail())
->from($this->sender)
->to($recipient->getEmail())
->subject('An error occurred while updating a domain name')
->htmlTemplate('emails/errors/domain_update.html.twig')
->locale('en')
->context([
'domain' => $this->domain,
]));
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Notifier;
use App\Entity\DomainEvent;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Component\Notifier\Message\ChatMessage;
use Symfony\Component\Notifier\Message\EmailMessage;
use Symfony\Component\Notifier\Notification\ChatNotificationInterface;
use Symfony\Component\Notifier\Notification\EmailNotificationInterface;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
use Symfony\Component\Notifier\Recipient\RecipientInterface;
class DomainUpdateNotification extends Notification implements ChatNotificationInterface, EmailNotificationInterface
{
public function __construct(
private readonly Address $sender,
private readonly DomainEvent $domainEvent
) {
parent::__construct();
}
public function asChatMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?ChatMessage
{
$ldhName = $this->domainEvent->getDomain()->getLdhName();
$action = $this->domainEvent->getAction();
$this->subject("Domain changed $ldhName ($action)")
->content("Domain name $ldhName information has been updated ($action).")
->importance(Notification::IMPORTANCE_HIGH);
return ChatMessage::fromNotification($this);
}
public function asEmailMessage(EmailRecipientInterface $recipient, ?string $transport = null): EmailMessage
{
return new EmailMessage((new TemplatedEmail())
->from($this->sender)
->to($recipient->getEmail())
->priority(Email::PRIORITY_HIGHEST)
->subject('A domain name has been changed')
->htmlTemplate('emails/success/domain_updated.html.twig')
->locale('en')
->context([
'event' => $this->domainEvent,
]));
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Notifier;
use Symfony\Component\Notifier\Message\ChatMessage;
use Symfony\Component\Notifier\Notification\ChatNotificationInterface;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\Recipient\RecipientInterface;
class TestChatNotification extends Notification implements ChatNotificationInterface
{
public function asChatMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?ChatMessage
{
$this
->subject('Test notification')
->content('This is a test message. If you can read me, this Webhook is configured correctly')
->importance(Notification::IMPORTANCE_LOW);
return ChatMessage::fromNotification($this);
}
}

View File

@@ -153,6 +153,15 @@
"config/packages/debug.yaml" "config/packages/debug.yaml"
] ]
}, },
"symfony/discord-notifier": {
"version": "7.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.2",
"ref": "b97655f9a2fb8fc04d9f4081e0b5599d897b7f6e"
}
},
"symfony/flex": { "symfony/flex": {
"version": "2.4", "version": "2.4",
"recipe": { "recipe": {
@@ -184,6 +193,15 @@
"src/Kernel.php" "src/Kernel.php"
] ]
}, },
"symfony/google-chat-notifier": {
"version": "7.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.3",
"ref": "5954a3403bf1cdc557e2b71d3854cc81ba7d37cb"
}
},
"symfony/lock": { "symfony/lock": {
"version": "7.1", "version": "7.1",
"recipe": { "recipe": {
@@ -217,6 +235,15 @@
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
} }
}, },
"symfony/mattermost-notifier": {
"version": "7.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.1",
"ref": "60df16a0ff39e942f6579e624d5630fc6b4e0cc1"
}
},
"symfony/messenger": { "symfony/messenger": {
"version": "7.1", "version": "7.1",
"recipe": { "recipe": {
@@ -229,6 +256,15 @@
"config/packages/messenger.yaml" "config/packages/messenger.yaml"
] ]
}, },
"symfony/microsoft-teams-notifier": {
"version": "7.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.3",
"ref": "e4e1704f0b11573aaededc00640492c915de0bbe"
}
},
"symfony/monolog-bundle": { "symfony/monolog-bundle": {
"version": "3.10", "version": "3.10",
"recipe": { "recipe": {
@@ -268,6 +304,15 @@
"tests/bootstrap.php" "tests/bootstrap.php"
] ]
}, },
"symfony/rocket-chat-notifier": {
"version": "7.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.1",
"ref": "5c91d24b503de5cc0d0eb880fc95afa1bef8b6f4"
}
},
"symfony/routing": { "symfony/routing": {
"version": "7.1", "version": "7.1",
"recipe": { "recipe": {
@@ -294,6 +339,15 @@
"config/routes/security.yaml" "config/routes/security.yaml"
] ]
}, },
"symfony/slack-notifier": {
"version": "7.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.2",
"ref": "8fb9603326990013efbe6d71c2dd78ada316808a"
}
},
"symfony/stimulus-bundle": { "symfony/stimulus-bundle": {
"version": "2.18", "version": "2.18",
"recipe": { "recipe": {
@@ -308,6 +362,15 @@
"assets/controllers/hello_controller.js" "assets/controllers/hello_controller.js"
] ]
}, },
"symfony/telegram-notifier": {
"version": "7.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.0",
"ref": "6cecb59a0e96c9e1cee469f2b82fa920101a68e8"
}
},
"symfony/translation": { "symfony/translation": {
"version": "7.1", "version": "7.1",
"recipe": { "recipe": {
@@ -387,6 +450,15 @@
"webpack.config.js" "webpack.config.js"
] ]
}, },
"symfony/zulip-notifier": {
"version": "7.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.2",
"ref": "f420901c554baf7cde79a2a0bbf9b37ab1a650aa"
}
},
"symfonycasts/verify-email-bundle": { "symfonycasts/verify-email-bundle": {
"version": "v1.17.0" "version": "v1.17.0"
}, },

View File

@@ -23,6 +23,7 @@ msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:156 #: assets/components/tracking/connector/ConnectorForm.tsx:156
#: assets/components/tracking/connector/ConnectorForm.tsx:165 #: assets/components/tracking/connector/ConnectorForm.tsx:165
#: assets/components/tracking/watchlist/WatchlistForm.tsx:109 #: assets/components/tracking/watchlist/WatchlistForm.tsx:109
#: assets/components/tracking/watchlist/WatchlistForm.tsx:205
msgid "Required" msgid "Required"
msgstr "" msgstr ""
@@ -203,12 +204,12 @@ msgid ""
msgstr "" msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:176 #: assets/components/tracking/connector/ConnectorForm.tsx:176
#: assets/components/tracking/watchlist/WatchlistForm.tsx:192 #: assets/components/tracking/watchlist/WatchlistForm.tsx:243
msgid "Create" msgid "Create"
msgstr "" msgstr ""
#: assets/components/tracking/connector/ConnectorForm.tsx:179 #: assets/components/tracking/connector/ConnectorForm.tsx:179
#: assets/components/tracking/watchlist/WatchlistForm.tsx:195 #: assets/components/tracking/watchlist/WatchlistForm.tsx:246
msgid "Reset" msgid "Reset"
msgstr "" msgstr ""
@@ -284,7 +285,27 @@ msgid ""
"that may be available soon." "that may be available soon."
msgstr "" msgstr ""
#: assets/components/tracking/watchlist/WatchlistForm.tsx:192 #: assets/components/tracking/watchlist/WatchlistForm.tsx:196
msgid "DSN"
msgstr ""
#: assets/components/tracking/watchlist/WatchlistForm.tsx:208
msgid "This DSN does not appear to be valid"
msgstr ""
#: assets/components/tracking/watchlist/WatchlistForm.tsx:212
msgid "slack://TOKEN@default?channel=CHANNEL"
msgstr ""
#: assets/components/tracking/watchlist/WatchlistForm.tsx:224
msgid "Check out this link to the Symfony documentation to help you build the DSN"
msgstr ""
#: assets/components/tracking/watchlist/WatchlistForm.tsx:233
msgid "Add a Webhook"
msgstr ""
#: assets/components/tracking/watchlist/WatchlistForm.tsx:243
msgid "Update" msgid "Update"
msgstr "" msgstr ""
@@ -292,11 +313,11 @@ msgstr ""
msgid "Edit the Watchlist" msgid "Edit the Watchlist"
msgstr "" msgstr ""
#: assets/components/tracking/watchlist/UpdateWatchlistButton.tsx:43 #: assets/components/tracking/watchlist/UpdateWatchlistButton.tsx:44
msgid "Update a Watchlist" msgid "Update a Watchlist"
msgstr "" msgstr ""
#: assets/components/tracking/watchlist/UpdateWatchlistButton.tsx:53 #: assets/components/tracking/watchlist/UpdateWatchlistButton.tsx:54
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
@@ -526,15 +547,15 @@ msgstr ""
msgid "Create a Connector" msgid "Create a Connector"
msgstr "" msgstr ""
#: assets/pages/tracking/WatchlistPage.tsx:47 #: assets/pages/tracking/WatchlistPage.tsx:65
msgid "Watchlist created !" msgid "Watchlist created !"
msgstr "" msgstr ""
#: assets/pages/tracking/WatchlistPage.tsx:70 #: assets/pages/tracking/WatchlistPage.tsx:77
msgid "Watchlist updated !" msgid "Watchlist updated !"
msgstr "" msgstr ""
#: assets/pages/tracking/WatchlistPage.tsx:96 #: assets/pages/tracking/WatchlistPage.tsx:102
msgid "Create a Watchlist" msgid "Create a Watchlist"
msgstr "" msgstr ""

View File

@@ -1504,6 +1504,21 @@
resolved "https://registry.yarnpkg.com/@fontsource/noto-color-emoji/-/noto-color-emoji-5.0.27.tgz#61e40657bea980553bde8fd2d566104bd2859ad6" resolved "https://registry.yarnpkg.com/@fontsource/noto-color-emoji/-/noto-color-emoji-5.0.27.tgz#61e40657bea980553bde8fd2d566104bd2859ad6"
integrity sha512-gsIMN5o8qoRLrA+XyDNKKnMNpTbwpNbINisJqirLOLXl83arOV2sHRnNOkVht2gzUcImUxEUBGektp56G3Vj9w== integrity sha512-gsIMN5o8qoRLrA+XyDNKKnMNpTbwpNbINisJqirLOLXl83arOV2sHRnNOkVht2gzUcImUxEUBGektp56G3Vj9w==
"@hotwired/stimulus-webpack-helpers@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@hotwired/stimulus-webpack-helpers/-/stimulus-webpack-helpers-1.0.1.tgz#4cd74487adeca576c9865ac2b9fe5cb20cef16dd"
integrity sha512-wa/zupVG0eWxRYJjC1IiPBdt3Lruv0RqGN+/DTMmUWUyMAEB27KXmVY6a8YpUVTM7QwVuaLNGW4EqDgrS2upXQ==
"@hotwired/stimulus@^3.0.0":
version "3.2.2"
resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.2.2.tgz#071aab59c600fed95b97939e605ff261a4251608"
integrity sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==
"@hotwired/turbo@^7.1.1 || ^8.0":
version "8.0.5"
resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.5.tgz#abae6dad018a891e4286e87fa0959217e3866d5a"
integrity sha512-TdZDA7fxVQ2ZycygvpnzjGPmFq4sO/E2QVg+2em/sJ3YTSsIWVEis8HmWlumz+c9DjWcUkcCuB+muF08TInpAQ==
"@jest/schemas@^29.6.3": "@jest/schemas@^29.6.3":
version "29.6.3" version "29.6.3"
resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03"
@@ -1670,6 +1685,20 @@
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e"
integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==
"@symfony/stimulus-bridge@^3.2.0":
version "3.2.2"
resolved "https://registry.yarnpkg.com/@symfony/stimulus-bridge/-/stimulus-bridge-3.2.2.tgz#afc1918f82d78cb2b6e299285c54094aa7f53696"
integrity sha512-kIaUEGPXW7g14zsHkIvQWw8cmfCdXSqsEQx18fuHPBb+R0h8nYPyY+e9uVtTuHlE2wHwAjrJoc6YBBK4a7CpKA==
dependencies:
"@hotwired/stimulus-webpack-helpers" "^1.0.1"
"@types/webpack-env" "^1.16.4"
acorn "^8.0.5"
loader-utils "^2.0.0"
schema-utils "^3.0.0"
"@symfony/ux-turbo@file:vendor/symfony/ux-turbo/assets":
version "0.1.0"
"@symfony/webpack-encore@^4.0.0": "@symfony/webpack-encore@^4.0.0":
version "4.6.1" version "4.6.1"
resolved "https://registry.yarnpkg.com/@symfony/webpack-encore/-/webpack-encore-4.6.1.tgz#a3ced0baf1b02feb6d1a564906aff0e479b07259" resolved "https://registry.yarnpkg.com/@symfony/webpack-encore/-/webpack-encore-4.6.1.tgz#a3ced0baf1b02feb6d1a564906aff0e479b07259"
@@ -1993,6 +2022,11 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/webpack-env@^1.16.4":
version "1.18.5"
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.18.5.tgz#eccda0b04fe024bed505881e2e532f9c119169bf"
integrity sha512-wz7kjjRRj8/Lty4B+Kr0LN6Ypc/3SymeCCGSbaXp2leH0ZVg/PriNiOwNj4bD4uphI7A8NXS4b6Gl373sfO5mA==
"@types/ws@^8.5.5": "@types/ws@^8.5.5":
version "8.5.11" version "8.5.11"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.11.tgz#90ad17b3df7719ce3e6bc32f83ff954d38656508" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.11.tgz#90ad17b3df7719ce3e6bc32f83ff954d38656508"
@@ -2202,7 +2236,7 @@ acorn-import-attributes@^1.9.5:
resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef"
integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==
acorn@^8.7.1, acorn@^8.8.2: acorn@^8.0.5, acorn@^8.7.1, acorn@^8.8.2:
version "8.12.1" version "8.12.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
@@ -6020,7 +6054,7 @@ scheduler@^0.23.2:
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
schema-utils@^3.1.1, schema-utils@^3.2.0: schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe"
integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==