mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
feat: slack-notifications
This commit is contained in:
@@ -152,6 +152,7 @@ FORCE_HTTPS=true
|
||||
# DOCS: https://docs.getoutline.com/s/hosting/doc/slack-sgMujR8J9J
|
||||
SLACK_CLIENT_ID=get_a_key_from_slack
|
||||
SLACK_CLIENT_SECRET=get_the_secret_of_above_key
|
||||
SLACK_SIGNING_SECRET=get_the_signing_secret_from_slack
|
||||
|
||||
# Google sign-in provider
|
||||
# DOCS: https://docs.getoutline.com/s/hosting/doc/google-hOuvtCmTqQ
|
||||
|
||||
@@ -13,6 +13,7 @@ GOOGLE_CLIENT_SECRET=123
|
||||
SLACK_CLIENT_ID=123
|
||||
SLACK_CLIENT_SECRET=123
|
||||
SLACK_VERIFICATION_TOKEN=test-token-123
|
||||
SLACK_SIGNING_SECRET=test-signing-secret-123
|
||||
|
||||
GITHUB_CLIENT_ID=123;
|
||||
GITHUB_CLIENT_SECRET=123;
|
||||
|
||||
+52
-11
@@ -3,6 +3,7 @@ import { computed, action, observable } from "mobx";
|
||||
import { now } from "mobx-utils";
|
||||
import { UserPreferenceDefaults } from "@shared/constants";
|
||||
import {
|
||||
NotificationChannelType,
|
||||
NotificationEventDefaults,
|
||||
type NotificationEventType,
|
||||
TeamPreference,
|
||||
@@ -206,36 +207,76 @@ class User extends ParanoidModel implements Searchable {
|
||||
* Returns the current preference for the given notification event type taking
|
||||
* into account the default system value.
|
||||
*
|
||||
* @param type The type of notification event
|
||||
* @returns The current preference
|
||||
* @param type The type of notification event.
|
||||
* @param channel Optional channel type for channel-specific check.
|
||||
* @returns The current preference.
|
||||
*/
|
||||
public subscribedToEventType = (type: NotificationEventType) =>
|
||||
this.notificationSettings[type] ?? NotificationEventDefaults[type] ?? false;
|
||||
public subscribedToEventType = (
|
||||
type: NotificationEventType,
|
||||
channel = NotificationChannelType.Email
|
||||
): boolean => {
|
||||
const setting = this.notificationSettings[type];
|
||||
const defaultValue = NotificationEventDefaults[type] ?? false;
|
||||
|
||||
if (setting === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (typeof setting === "boolean") {
|
||||
return setting;
|
||||
}
|
||||
|
||||
if (typeof setting === "object") {
|
||||
return setting[channel] ?? defaultValue;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets a preference for the users notification settings on the model and
|
||||
* saves the change to the server.
|
||||
*
|
||||
* @param type The type of notification event
|
||||
* @param value Set the preference to true/false
|
||||
* @param type The type of notification event.
|
||||
* @param value Set the preference to true/false.
|
||||
* @param channel Optional channel type for channel-specific settings.
|
||||
*/
|
||||
@action
|
||||
setNotificationEventType = async (
|
||||
eventType: NotificationEventType,
|
||||
value: boolean
|
||||
value: boolean | Record<NotificationChannelType, boolean>,
|
||||
channel?: NotificationChannelType
|
||||
) => {
|
||||
this.notificationSettings = {
|
||||
...this.notificationSettings,
|
||||
[eventType]: value,
|
||||
};
|
||||
if (channel !== undefined) {
|
||||
// Setting a specific channel preference
|
||||
const currentSetting = this.notificationSettings[eventType];
|
||||
const channelSettings =
|
||||
typeof currentSetting === "object" ? currentSetting : {};
|
||||
|
||||
this.notificationSettings = {
|
||||
...this.notificationSettings,
|
||||
[eventType]: {
|
||||
...channelSettings,
|
||||
[channel]: value,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Setting all channels or simple boolean
|
||||
this.notificationSettings = {
|
||||
...this.notificationSettings,
|
||||
[eventType]: value,
|
||||
};
|
||||
}
|
||||
|
||||
if (value) {
|
||||
await client.post(`/users.notificationsSubscribe`, {
|
||||
eventType,
|
||||
channel,
|
||||
});
|
||||
} else {
|
||||
await client.post(`/users.notificationsUnsubscribe`, {
|
||||
eventType,
|
||||
channel,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,16 +18,23 @@ import {
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { NotificationEventType } from "@shared/types";
|
||||
import {
|
||||
NotificationEventType,
|
||||
NotificationChannelType,
|
||||
IntegrationService,
|
||||
IntegrationType,
|
||||
} from "@shared/types";
|
||||
import Heading from "~/components/Heading";
|
||||
import Notice from "~/components/Notice";
|
||||
import Scene from "~/components/Scene";
|
||||
import Switch from "~/components/Switch";
|
||||
import Text from "~/components/Text";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import { client } from "~/utils/ApiClient";
|
||||
import isCloudHosted from "~/utils/isCloudHosted";
|
||||
import { settingsPath } from "~/utils/routeHelpers";
|
||||
import ChannelSelector from "./components/ChannelSelector";
|
||||
import SettingRow from "./components/SettingRow";
|
||||
|
||||
function Notifications() {
|
||||
@@ -161,11 +168,29 @@ function Notifications() {
|
||||
toast.success(t("Notifications saved"));
|
||||
}, 500);
|
||||
|
||||
const handleChange = React.useCallback(
|
||||
(eventType: NotificationEventType) => async (checked: boolean) => {
|
||||
await user.setNotificationEventType(eventType, checked);
|
||||
showSuccessMessage();
|
||||
},
|
||||
const handleChannelsChange = React.useCallback(
|
||||
(eventType: NotificationEventType) =>
|
||||
async (channels: NotificationChannelType[]) => {
|
||||
for (const channel of [
|
||||
NotificationChannelType.Email,
|
||||
NotificationChannelType.Slack,
|
||||
]) {
|
||||
const shouldEnable = channels.includes(channel);
|
||||
const currentlyEnabled = user.subscribedToEventType(
|
||||
eventType,
|
||||
channel
|
||||
);
|
||||
|
||||
if (shouldEnable !== currentlyEnabled) {
|
||||
await user.setNotificationEventType(
|
||||
eventType,
|
||||
shouldEnable,
|
||||
channel
|
||||
);
|
||||
}
|
||||
}
|
||||
showSuccessMessage();
|
||||
},
|
||||
[user, showSuccessMessage]
|
||||
);
|
||||
|
||||
@@ -206,7 +231,10 @@ function Notifications() {
|
||||
</Notice>
|
||||
)}
|
||||
<Text as="p" type="secondary">
|
||||
<Trans>Manage when and where you receive email notifications.</Trans>
|
||||
<Trans>
|
||||
Manage when and where you receive notifications. Choose to receive
|
||||
notifications via email, Slack, or both.
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<SettingRow
|
||||
@@ -223,7 +251,22 @@ function Notifications() {
|
||||
</SettingRow>
|
||||
|
||||
{options.map((option) => {
|
||||
const setting = user.subscribedToEventType(option.event);
|
||||
const emailSetting = user.subscribedToEventType(
|
||||
option.event,
|
||||
NotificationChannelType.Email
|
||||
);
|
||||
const slackSetting = user.subscribedToEventType(
|
||||
option.event,
|
||||
NotificationChannelType.Slack
|
||||
);
|
||||
|
||||
const enabledChannels: NotificationChannelType[] = [];
|
||||
if (emailSetting) {
|
||||
enabledChannels.push(NotificationChannelType.Email);
|
||||
}
|
||||
if (slackSetting) {
|
||||
enabledChannels.push(NotificationChannelType.Slack);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
@@ -238,12 +281,10 @@ function Notifications() {
|
||||
}
|
||||
compact
|
||||
>
|
||||
<Switch
|
||||
key={option.event}
|
||||
id={option.event}
|
||||
name={option.event}
|
||||
checked={!!setting}
|
||||
onChange={handleChange(option.event)}
|
||||
<ChannelSelector
|
||||
value={enabledChannels}
|
||||
onChange={handleChannelsChange(option.event)}
|
||||
slackDisabled={!hasSlackLinked}
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import * as React from "react";
|
||||
import { EmailIcon } from "outline-icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { NotificationChannelType } from "@shared/types";
|
||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import FilterOptions from "~/components/FilterOptions";
|
||||
|
||||
type Props = {
|
||||
value: NotificationChannelType[];
|
||||
onChange: (channels: NotificationChannelType[]) => void;
|
||||
slackDisabled?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A dropdown selector for managing notification channel preferences.
|
||||
* Displays enabled channels and allows toggling via a popover menu.
|
||||
*/
|
||||
function ChannelSelector({ value, onChange, slackDisabled = false }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const channels = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
key: NotificationChannelType.Email,
|
||||
label: t("Email"),
|
||||
icon: <EmailIcon size={16} />,
|
||||
},
|
||||
{
|
||||
key: NotificationChannelType.Slack,
|
||||
label: t("Slack"),
|
||||
icon: <FontAwesomeIcon icon={faSlack} size="xs" />,
|
||||
},
|
||||
],
|
||||
[t, slackDisabled]
|
||||
);
|
||||
|
||||
const handleToggle = React.useCallback(
|
||||
(channelType: NotificationChannelType) => {
|
||||
const newValue = value.includes(channelType)
|
||||
? value.filter((c) => c !== channelType)
|
||||
: [...value, channelType];
|
||||
onChange(newValue);
|
||||
},
|
||||
[value, onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterOptions
|
||||
defaultLabel={t("Select Channels")}
|
||||
options={channels}
|
||||
selectedKeys={value}
|
||||
onSelect={handleToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChannelSelector;
|
||||
@@ -0,0 +1,21 @@
|
||||
diff --git a/node_modules/@chat-adapter/shared/package.json b/node_modules/@chat-adapter/shared/package.json
|
||||
index 3015551..d624867 100644
|
||||
--- a/node_modules/@chat-adapter/shared/package.json
|
||||
+++ b/node_modules/@chat-adapter/shared/package.json
|
||||
@@ -6,7 +6,8 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
- "import": "./dist/index.js"
|
||||
+ "import": "./dist/index.js",
|
||||
+ "default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
@@ -46,4 +47,4 @@
|
||||
"build": "tsup",
|
||||
"test": "vitest run --coverage"
|
||||
}
|
||||
-}
|
||||
\ No newline at end of file
|
||||
+}
|
||||
@@ -0,0 +1,21 @@
|
||||
diff --git a/node_modules/@chat-adapter/slack/package.json b/node_modules/@chat-adapter/slack/package.json
|
||||
index 871fe33..a454831 100644
|
||||
--- a/node_modules/@chat-adapter/slack/package.json
|
||||
+++ b/node_modules/@chat-adapter/slack/package.json
|
||||
@@ -9,7 +9,8 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
- "import": "./dist/index.js"
|
||||
+ "import": "./dist/index.js",
|
||||
+ "default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@@ -53,4 +54,4 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
}
|
||||
-}
|
||||
\ No newline at end of file
|
||||
+}
|
||||
@@ -0,0 +1,33 @@
|
||||
diff --git a/node_modules/chat/package.json b/node_modules/chat/package.json
|
||||
index 89e4e44..24b552d 100644
|
||||
--- a/node_modules/chat/package.json
|
||||
+++ b/node_modules/chat/package.json
|
||||
@@ -9,15 +9,18 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
- "import": "./dist/index.js"
|
||||
+ "import": "./dist/index.js",
|
||||
+ "default": "./dist/index.js"
|
||||
},
|
||||
"./jsx-runtime": {
|
||||
"types": "./dist/jsx-runtime.d.ts",
|
||||
- "import": "./dist/jsx-runtime.js"
|
||||
+ "import": "./dist/jsx-runtime.js",
|
||||
+ "default": "./dist/jsx-runtime.js"
|
||||
},
|
||||
"./jsx-dev-runtime": {
|
||||
"types": "./dist/jsx-runtime.d.ts",
|
||||
- "import": "./dist/jsx-runtime.js"
|
||||
+ "import": "./dist/jsx-runtime.js",
|
||||
+ "default": "./dist/jsx-runtime.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@@ -68,4 +71,4 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rm -rf dist docs"
|
||||
}
|
||||
-}
|
||||
\ No newline at end of file
|
||||
+}
|
||||
@@ -254,17 +254,38 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
case IntegrationType.LinkedAccount: {
|
||||
// validation middleware ensures that code is non-null at this point
|
||||
const data = await Slack.oauthAccess(code!, SlackUtils.connectUrl());
|
||||
await Integration.create<Integration<IntegrationType.LinkedAccount>>({
|
||||
service: IntegrationService.Slack,
|
||||
type: IntegrationType.LinkedAccount,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
settings: {
|
||||
slack: {
|
||||
serviceUserId: data.user_id,
|
||||
serviceTeamId: data.team_id,
|
||||
|
||||
await sequelize.transaction(async (transaction) => {
|
||||
const authentication = await IntegrationAuthentication.create(
|
||||
{
|
||||
service: IntegrationService.Slack,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
token: data.access_token,
|
||||
scopes: data.scope.split(","),
|
||||
},
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
await Integration.create<
|
||||
Integration<IntegrationType.LinkedAccount>
|
||||
>(
|
||||
{
|
||||
service: IntegrationService.Slack,
|
||||
type: IntegrationType.LinkedAccount,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
// need to add events
|
||||
authenticationId: authentication.id,
|
||||
settings: {
|
||||
slack: {
|
||||
serviceUserId: data.user_id,
|
||||
serviceTeamId: data.team_id,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,12 @@ class SlackPluginEnvironment extends Environment {
|
||||
environment.SLACK_CLIENT_SECRET ?? environment.SLACK_SECRET
|
||||
);
|
||||
|
||||
@IsOptional()
|
||||
@CannotUseWithout("SLACK_CLIENT_ID")
|
||||
public SLACK_SIGNING_SECRET = this.toOptionalString(
|
||||
environment.SLACK_SIGNING_SECRET
|
||||
);
|
||||
|
||||
/**
|
||||
* Secret to verify webhook requests received from Slack.
|
||||
*/
|
||||
|
||||
@@ -3,9 +3,50 @@ import { InvalidRequestError } from "@server/errors";
|
||||
import fetch from "@server/utils/fetch";
|
||||
import { SlackUtils } from "../shared/SlackUtils";
|
||||
import env from "./env";
|
||||
import type { AdapterPostableMessage } from "chat";
|
||||
|
||||
const SLACK_API_URL = "https://slack.com/api";
|
||||
|
||||
/**
|
||||
* send a message to a Slack channel
|
||||
*
|
||||
* @param token - the Slack API token to authenticate the request.
|
||||
* @param channel - the channel ID or user ID to send the message to.
|
||||
* @param message - the message content to send.
|
||||
*
|
||||
* @returns the response after posting the message.
|
||||
*/
|
||||
export async function postMessage({
|
||||
token,
|
||||
channel,
|
||||
message,
|
||||
}: {
|
||||
token: string;
|
||||
channel: string;
|
||||
message: AdapterPostableMessage;
|
||||
}) {
|
||||
if (!token) {
|
||||
throw InvalidRequestError("Slack API token is required");
|
||||
}
|
||||
|
||||
try {
|
||||
const adapter = createSlackAdapter({
|
||||
botToken: token,
|
||||
signingSecret: env.SLACK_SIGNING_SECRET,
|
||||
});
|
||||
|
||||
const threadId = await adapter.openDM(channel);
|
||||
const result = await adapter.postMessage(threadId, message);
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (err.data) {
|
||||
throw InvalidRequestError(err.data.error || err.message);
|
||||
}
|
||||
throw InvalidRequestError(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a POST request to the Slack API with JSON body.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"use strict";
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
return queryInterface.sequelize.transaction(async (transaction) => {
|
||||
await queryInterface.addColumn(
|
||||
"notifications",
|
||||
"slackSentAt",
|
||||
{
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
return queryInterface.sequelize.transaction(async (transaction) => {
|
||||
await queryInterface.removeColumn("notifications", "slackSentAt", {
|
||||
transaction,
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -117,6 +117,10 @@ class Notification extends Model<
|
||||
@Column
|
||||
emailedAt?: Date | null;
|
||||
|
||||
@AllowNull
|
||||
@Column
|
||||
slackSentAt?: Date | null;
|
||||
|
||||
@AllowNull
|
||||
@Column
|
||||
viewedAt: Date | null;
|
||||
|
||||
+84
-11
@@ -40,6 +40,7 @@ import type {
|
||||
} from "@shared/types";
|
||||
import {
|
||||
CollectionPermission,
|
||||
NotificationChannelType,
|
||||
NotificationEventDefaults,
|
||||
UserRole,
|
||||
DocumentPermission,
|
||||
@@ -348,28 +349,100 @@ class User extends ParanoidModel<
|
||||
/**
|
||||
* Sets a preference for the users notification settings.
|
||||
*
|
||||
* @param type The type of notification event
|
||||
* @param value Set the preference to true/false
|
||||
* @param type The type of notification event.
|
||||
* @param value Set the preference to true/false or channel-specific settings.
|
||||
* @param channel Optional channel type for channel-specific settings.
|
||||
*/
|
||||
public setNotificationEventType = (
|
||||
type: NotificationEventType,
|
||||
value = true
|
||||
value: boolean | Record<NotificationChannelType, boolean>,
|
||||
channel?: NotificationChannelType
|
||||
) => {
|
||||
this.notificationSettings = {
|
||||
...this.notificationSettings,
|
||||
[type]: value,
|
||||
};
|
||||
if (channel !== undefined) {
|
||||
// Setting a specific channel preference
|
||||
const currentSetting = this.notificationSettings[type];
|
||||
const channelSettings =
|
||||
typeof currentSetting === "object" ? currentSetting : {};
|
||||
|
||||
this.notificationSettings = {
|
||||
...this.notificationSettings,
|
||||
[type]: {
|
||||
...channelSettings,
|
||||
[channel]: value,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Setting all channels or simple boolean
|
||||
this.notificationSettings = {
|
||||
...this.notificationSettings,
|
||||
[type]: value,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the current preference for the given notification event type taking
|
||||
* into account the default system value.
|
||||
*
|
||||
* @param type The type of notification event
|
||||
* @returns The current preference
|
||||
* @param type The type of notification event.
|
||||
* @param channel Optional channel type for channel-specific check.
|
||||
* @returns The current preference.
|
||||
*/
|
||||
public subscribedToEventType = (type: NotificationEventType) =>
|
||||
this.notificationSettings[type] ?? NotificationEventDefaults[type] ?? false;
|
||||
public subscribedToEventType = (
|
||||
type: NotificationEventType,
|
||||
channel = NotificationChannelType.Email
|
||||
): boolean => {
|
||||
const setting = this.notificationSettings[type];
|
||||
const defaultValue = NotificationEventDefaults[type] ?? false;
|
||||
|
||||
if (setting === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (typeof setting === "boolean") {
|
||||
return setting;
|
||||
}
|
||||
|
||||
if (typeof setting === "object") {
|
||||
return setting[channel] ?? defaultValue;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the user's Slack user ID if they have linked their Slack account.
|
||||
*
|
||||
* @returns The Slack user ID or null.
|
||||
*/
|
||||
public getSlackUserId = async (): Promise<string | null> => {
|
||||
const { Integration } = await import("./index");
|
||||
const { IntegrationType } = await import("@shared/types");
|
||||
const integration = await Integration.findOne({
|
||||
where: {
|
||||
userId: this.id,
|
||||
service: "slack",
|
||||
type: IntegrationType.LinkedAccount,
|
||||
},
|
||||
});
|
||||
|
||||
if (!integration || typeof integration.settings !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { settings } = integration;
|
||||
if (
|
||||
settings &&
|
||||
"slack" in settings &&
|
||||
settings.slack &&
|
||||
typeof settings.slack === "object" &&
|
||||
"serviceUserId" in settings.slack
|
||||
) {
|
||||
return settings.slack.serviceUserId as string;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* User flags are for storing information on a user record that is not visible
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import {
|
||||
IntegrationService,
|
||||
NotificationChannelType,
|
||||
NotificationEventType,
|
||||
} from "@shared/types";
|
||||
import { Notification, IntegrationAuthentication } from "@server/models";
|
||||
import type { Event, NotificationEvent } from "@server/types";
|
||||
import * as Slack from "../../../plugins/slack/server/slack";
|
||||
import BaseProcessor from "./BaseProcessor";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import { paragraph, root, strong, text, link } from "chat";
|
||||
|
||||
/**
|
||||
* Processor for sending Slack DM notifications.
|
||||
* Listens for notification.create events and sends Slack DMs to users
|
||||
* who have linked their Slack accounts and enabled Slack notifications.
|
||||
*/
|
||||
export default class SlackNotificationsProcessor extends BaseProcessor {
|
||||
static applicableEvents: Event["name"][] = ["notifications.create"];
|
||||
|
||||
async perform(event: NotificationEvent) {
|
||||
const notification = await Notification.scope([
|
||||
"withTeam",
|
||||
"withUser",
|
||||
"withActor",
|
||||
]).findByPk(event.modelId);
|
||||
|
||||
if (!notification) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (notification.user.isSuspended) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user has Slack notifications enabled for this event type
|
||||
if (
|
||||
!notification.user.subscribedToEventType(
|
||||
notification.event,
|
||||
NotificationChannelType.Slack
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slackUserId = await notification.user.getSlackUserId();
|
||||
if (!slackUserId) {
|
||||
Logger.info(
|
||||
"processor",
|
||||
`User ${notification.userId} has no linked Slack account`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const auth = await IntegrationAuthentication.findOne({
|
||||
where: {
|
||||
service: IntegrationService.Slack,
|
||||
teamId: notification.user.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!auth) {
|
||||
Logger.debug(
|
||||
"plugins",
|
||||
"No Slack integration authentication found for team",
|
||||
{
|
||||
teamId: notification.user.teamId,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const message = this.formatSlackMessage(notification);
|
||||
|
||||
await Slack.postMessage({
|
||||
token: auth?.token,
|
||||
channel: slackUserId,
|
||||
message,
|
||||
});
|
||||
|
||||
await notification.update({
|
||||
slackSentAt: new Date(),
|
||||
});
|
||||
|
||||
Logger.info(
|
||||
"processor",
|
||||
`Slack DM sent for notification ${notification.id}`
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.error(
|
||||
`Failed to send Slack DM for notification ${notification.id}`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a notification into a Slack message with rich formatting.
|
||||
*
|
||||
* @param notification - the notification to format.
|
||||
* @returns the formatted Slack message.
|
||||
*/
|
||||
private formatSlackMessage(notification: Notification) {
|
||||
const actorName = notification.actor.name;
|
||||
const teamUrl = notification.team.url;
|
||||
let textContent = "";
|
||||
let url = "";
|
||||
|
||||
switch (notification.event) {
|
||||
case NotificationEventType.PublishDocument:
|
||||
textContent = `${actorName} published a new document`;
|
||||
url = `${teamUrl}/doc/${notification.documentId}`;
|
||||
break;
|
||||
|
||||
case NotificationEventType.UpdateDocument:
|
||||
textContent = `${actorName} updated a document you're subscribed to`;
|
||||
url = `${teamUrl}/doc/${notification.documentId}`;
|
||||
break;
|
||||
|
||||
case NotificationEventType.MentionedInDocument:
|
||||
textContent = `${actorName} mentioned you in a document`;
|
||||
url = `${teamUrl}/doc/${notification.documentId}`;
|
||||
break;
|
||||
|
||||
case NotificationEventType.MentionedInComment:
|
||||
textContent = `${actorName} mentioned you in a comment`;
|
||||
url = `${teamUrl}/doc/${notification.documentId}?commentId=${notification.commentId}`;
|
||||
break;
|
||||
|
||||
case NotificationEventType.GroupMentionedInDocument:
|
||||
textContent = `${actorName} mentioned a group you're in`;
|
||||
url = `${teamUrl}/doc/${notification.documentId}`;
|
||||
break;
|
||||
|
||||
case NotificationEventType.GroupMentionedInComment:
|
||||
textContent = `${actorName} mentioned a group you're in`;
|
||||
url = `${teamUrl}/doc/${notification.documentId}?commentId=${notification.commentId}`;
|
||||
break;
|
||||
|
||||
case NotificationEventType.CreateComment:
|
||||
textContent = `${actorName} commented on a document you're subscribed to`;
|
||||
url = `${teamUrl}/doc/${notification.documentId}?commentId=${notification.commentId}`;
|
||||
break;
|
||||
|
||||
case NotificationEventType.ResolveComment:
|
||||
textContent = `${actorName} resolved a comment thread you participated in`;
|
||||
url = `${teamUrl}/doc/${notification.documentId}?commentId=${notification.commentId}`;
|
||||
break;
|
||||
|
||||
case NotificationEventType.CreateCollection:
|
||||
textContent = `A new collection was created`;
|
||||
url = `${teamUrl}/collection/${notification.collectionId}`;
|
||||
break;
|
||||
|
||||
case NotificationEventType.AddUserToDocument:
|
||||
textContent = `${actorName} shared a document with you`;
|
||||
url = `${teamUrl}/doc/${notification.documentId}`;
|
||||
break;
|
||||
|
||||
case NotificationEventType.AddUserToCollection:
|
||||
textContent = `${actorName} shared a collection with you`;
|
||||
url = `${teamUrl}/collection/${notification.collectionId}`;
|
||||
break;
|
||||
|
||||
default:
|
||||
textContent = `You have a new notification from ${actorName}`;
|
||||
url = teamUrl;
|
||||
}
|
||||
|
||||
const message = {
|
||||
ast: root([
|
||||
paragraph([
|
||||
strong([text(textContent)]),
|
||||
text("\n"),
|
||||
link(url, [text("View")]),
|
||||
]),
|
||||
]),
|
||||
};
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
NotificationBadgeType,
|
||||
NotificationChannelType,
|
||||
NotificationEventType,
|
||||
UserPreference,
|
||||
UserRole,
|
||||
|
||||
@@ -681,7 +681,7 @@ router.post(
|
||||
validate(T.UsersNotificationsSubscribeSchema),
|
||||
transaction(),
|
||||
async (ctx: APIContext<T.UsersNotificationsSubscribeReq>) => {
|
||||
const { eventType } = ctx.input.body;
|
||||
const { eventType, channel } = ctx.input.body;
|
||||
const { user } = ctx.state.auth;
|
||||
const eventTypes = eventType
|
||||
? [eventType]
|
||||
@@ -705,7 +705,7 @@ router.post(
|
||||
validate(T.UsersNotificationsUnsubscribeSchema),
|
||||
transaction(),
|
||||
async (ctx: APIContext<T.UsersNotificationsUnsubscribeReq>) => {
|
||||
const { eventType } = ctx.input.body;
|
||||
const { eventType, channel } = ctx.input.body;
|
||||
const { user } = ctx.state.auth;
|
||||
const eventTypes = eventType
|
||||
? [eventType]
|
||||
|
||||
@@ -6646,7 +6646,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/debug@npm:^4.1.8":
|
||||
"@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.8":
|
||||
version: 4.1.12
|
||||
resolution: "@types/debug@npm:4.1.12"
|
||||
dependencies:
|
||||
@@ -7041,6 +7041,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/mdast@npm:^4.0.0":
|
||||
version: 4.0.4
|
||||
resolution: "@types/mdast@npm:4.0.4"
|
||||
dependencies:
|
||||
"@types/unist": "npm:*"
|
||||
checksum: 10c0/84f403dbe582ee508fd9c7643ac781ad8597fcbfc9ccb8d4715a2c92e4545e5772cbd0dbdf18eda65789386d81b009967fdef01b24faf6640f817287f54d9c82
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/mdurl@npm:^2":
|
||||
version: 2.0.0
|
||||
resolution: "@types/mdurl@npm:2.0.0"
|
||||
@@ -7093,12 +7102,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:*, @types/node@npm:>=10.0.0":
|
||||
version: 25.0.1
|
||||
resolution: "@types/node@npm:25.0.1"
|
||||
"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=18.0.0":
|
||||
version: 25.0.3
|
||||
resolution: "@types/node@npm:25.0.3"
|
||||
dependencies:
|
||||
undici-types: "npm:~7.16.0"
|
||||
checksum: 10c0/1d5ca9f240d0cf8e43d5281c0e6ee96fb22d37dc2e5ef52c6ca71de47957a6128e124990cedf5b14c03d0250737bd78ad370d93bcf1729a75ca4e54384fdd51a
|
||||
checksum: 10c0/b7568f0d765d9469621615e2bb257c7fd1953d95e9acbdb58dffb6627a2c4150d405a4600aa1ad8a40182a94fe5f903cafd3c0a2f5132814debd0e3bfd61f835
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7339,6 +7348,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/retry@npm:0.12.0":
|
||||
version: 0.12.0
|
||||
resolution: "@types/retry@npm:0.12.0"
|
||||
checksum: 10c0/7c5c9086369826f569b83a4683661557cab1361bac0897a1cefa1a915ff739acd10ca0d62b01071046fe3f5a3f7f2aec80785fe283b75602dc6726781ea3e328
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/sanitize-filename@npm:^1.6.3":
|
||||
version: 1.6.3
|
||||
resolution: "@types/sanitize-filename@npm:1.6.3"
|
||||
@@ -7478,6 +7494,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/unist@npm:*, @types/unist@npm:^3.0.0":
|
||||
version: 3.0.3
|
||||
resolution: "@types/unist@npm:3.0.3"
|
||||
checksum: 10c0/2b1e4adcab78388e088fcc3c0ae8700f76619dbcb4741d7d201f87e2cb346bfc29a89003cfea2d76c996e1061452e14fcd737e8b25aacf949c1f2d6b2bc3dd60
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/unist@npm:^2":
|
||||
version: 2.0.11
|
||||
resolution: "@types/unist@npm:2.0.11"
|
||||
@@ -7678,6 +7701,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@workflow/serde@npm:4.1.0-beta.2":
|
||||
version: 4.1.0-beta.2
|
||||
resolution: "@workflow/serde@npm:4.1.0-beta.2"
|
||||
checksum: 10c0/c84efdb22106ab77d95873022de1b645b1ac7288d85a78e74946c11b83fb22a89e301da649aa0750e1f65ff4655a5d7bf02d27b5022aff86ef4d3d8362a97fe7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@xmldom/xmldom@npm:^0.8.6":
|
||||
version: 0.8.13
|
||||
resolution: "@xmldom/xmldom@npm:0.8.13"
|
||||
@@ -8616,6 +8646,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"character-entities@npm:^2.0.0":
|
||||
version: 2.0.2
|
||||
resolution: "character-entities@npm:2.0.2"
|
||||
checksum: 10c0/b0c645a45bcc90ff24f0e0140f4875a8436b8ef13b6bcd31ec02cfb2ca502b680362aa95386f7815bdc04b6464d48cf191210b3840d7c04241a149ede591a308
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"character-reference-invalid@npm:^1.0.0":
|
||||
version: 1.1.4
|
||||
resolution: "character-reference-invalid@npm:1.1.4"
|
||||
@@ -8623,6 +8660,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chat@npm:4.17.0, chat@npm:^4.15.0":
|
||||
version: 4.17.0
|
||||
resolution: "chat@npm:4.17.0"
|
||||
dependencies:
|
||||
"@workflow/serde": "npm:4.1.0-beta.2"
|
||||
mdast-util-to-string: "npm:^4.0.0"
|
||||
remark-gfm: "npm:^4.0.0"
|
||||
remark-parse: "npm:^11.0.0"
|
||||
remark-stringify: "npm:^11.0.0"
|
||||
remend: "npm:^1.2.1"
|
||||
unified: "npm:^11.0.5"
|
||||
checksum: 10c0/951ee54520e40711e001039147f6081751d6d52f05ebf80bee79c772edcfc2204cc14dc1a9afef2e4673e99453f93cae54879f7dfbe798d09465dbc65444cb30
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cheerio-select@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "cheerio-select@npm:2.1.0"
|
||||
@@ -9906,6 +9958,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"decode-named-character-reference@npm:^1.0.0":
|
||||
version: 1.2.0
|
||||
resolution: "decode-named-character-reference@npm:1.2.0"
|
||||
dependencies:
|
||||
character-entities: "npm:^2.0.0"
|
||||
checksum: 10c0/761a89de6b0e0a2d4b21ae99074e4cc3344dd11eb29f112e23cc5909f2e9f33c5ed20cd6b146b27fb78170bce0f3f9b3362a84b75638676a05c938c24a60f5d7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"decode-uri-component@npm:^0.2.2":
|
||||
version: 0.2.2
|
||||
resolution: "decode-uri-component@npm:0.2.2"
|
||||
@@ -10021,6 +10082,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"devlop@npm:^1.0.0, devlop@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "devlop@npm:1.1.0"
|
||||
dependencies:
|
||||
dequal: "npm:^2.0.0"
|
||||
checksum: 10c0/e0928ab8f94c59417a2b8389c45c55ce0a02d9ac7fd74ef62d01ba48060129e1d594501b77de01f3eeafc7cb00773819b0df74d96251cf20b31c5b3071f45c0e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dezalgo@npm:^1.0.4":
|
||||
version: 1.0.4
|
||||
resolution: "dezalgo@npm:1.0.4"
|
||||
@@ -10755,6 +10825,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eventemitter3@npm:^4.0.4":
|
||||
version: 4.0.7
|
||||
resolution: "eventemitter3@npm:4.0.7"
|
||||
checksum: 10c0/5f6d97cbcbac47be798e6355e3a7639a84ee1f7d9b199a07017f1d2f1e2fe236004d14fa5dfaeba661f94ea57805385e326236a6debbc7145c8877fbc0297c6b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eventemitter3@npm:^5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "eventemitter3@npm:5.0.1"
|
||||
@@ -12553,6 +12630,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-electron@npm:2.2.2":
|
||||
version: 2.2.2
|
||||
resolution: "is-electron@npm:2.2.2"
|
||||
checksum: 10c0/327bb373f7be01b16cdff3998b5ddaa87d28f576092affaa7fe0659571b3306fdd458afbf0683a66841e7999af13f46ad0e1b51647b469526cd05a4dd736438a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-extendable@npm:^0.1.0":
|
||||
version: 0.1.1
|
||||
resolution: "is-extendable@npm:0.1.1"
|
||||
@@ -12743,7 +12827,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-stream@npm:^2.0.0":
|
||||
"is-stream@npm:^2, is-stream@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "is-stream@npm:2.0.1"
|
||||
checksum: 10c0/7c284241313fc6efc329b8d7f08e16c0efeb6baab1b4cd0ba579eb78e5af1aa5da11e68559896a2067cd6c526bd29241dda4eb1225e627d5aa1a89a76d4635a5
|
||||
@@ -13911,6 +13995,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"longest-streak@npm:^3.0.0":
|
||||
version: 3.1.0
|
||||
resolution: "longest-streak@npm:3.1.0"
|
||||
checksum: 10c0/7c2f02d0454b52834d1bcedef79c557bd295ee71fdabb02d041ff3aa9da48a90b5df7c0409156dedbc4df9b65da18742652aaea4759d6ece01f08971af6a7eaa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.2.0, loose-envify@npm:^1.3.1, loose-envify@npm:^1.4.0":
|
||||
version: 1.4.0
|
||||
resolution: "loose-envify@npm:1.4.0"
|
||||
@@ -14111,6 +14202,151 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-find-and-replace@npm:^3.0.0":
|
||||
version: 3.0.2
|
||||
resolution: "mdast-util-find-and-replace@npm:3.0.2"
|
||||
dependencies:
|
||||
"@types/mdast": "npm:^4.0.0"
|
||||
escape-string-regexp: "npm:^5.0.0"
|
||||
unist-util-is: "npm:^6.0.0"
|
||||
unist-util-visit-parents: "npm:^6.0.0"
|
||||
checksum: 10c0/c8417a35605d567772ff5c1aa08363ff3010b0d60c8ea68c53cba09bf25492e3dd261560425c1756535f3b7107f62e7ff3857cdd8fb1e62d1b2cc2ea6e074ca2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-from-markdown@npm:^2.0.0":
|
||||
version: 2.0.2
|
||||
resolution: "mdast-util-from-markdown@npm:2.0.2"
|
||||
dependencies:
|
||||
"@types/mdast": "npm:^4.0.0"
|
||||
"@types/unist": "npm:^3.0.0"
|
||||
decode-named-character-reference: "npm:^1.0.0"
|
||||
devlop: "npm:^1.0.0"
|
||||
mdast-util-to-string: "npm:^4.0.0"
|
||||
micromark: "npm:^4.0.0"
|
||||
micromark-util-decode-numeric-character-reference: "npm:^2.0.0"
|
||||
micromark-util-decode-string: "npm:^2.0.0"
|
||||
micromark-util-normalize-identifier: "npm:^2.0.0"
|
||||
micromark-util-symbol: "npm:^2.0.0"
|
||||
micromark-util-types: "npm:^2.0.0"
|
||||
unist-util-stringify-position: "npm:^4.0.0"
|
||||
checksum: 10c0/76eb2bd2c6f7a0318087c73376b8af6d7561c1e16654e7667e640f391341096c56142618fd0ff62f6d39e5ab4895898b9789c84cd7cec2874359a437a0e1ff15
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-gfm-autolink-literal@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "mdast-util-gfm-autolink-literal@npm:2.0.1"
|
||||
dependencies:
|
||||
"@types/mdast": "npm:^4.0.0"
|
||||
ccount: "npm:^2.0.0"
|
||||
devlop: "npm:^1.0.0"
|
||||
mdast-util-find-and-replace: "npm:^3.0.0"
|
||||
micromark-util-character: "npm:^2.0.0"
|
||||
checksum: 10c0/963cd22bd42aebdec7bdd0a527c9494d024d1ad0739c43dc040fee35bdfb5e29c22564330a7418a72b5eab51d47a6eff32bc0255ef3ccb5cebfe8970e91b81b6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-gfm-footnote@npm:^2.0.0":
|
||||
version: 2.1.0
|
||||
resolution: "mdast-util-gfm-footnote@npm:2.1.0"
|
||||
dependencies:
|
||||
"@types/mdast": "npm:^4.0.0"
|
||||
devlop: "npm:^1.1.0"
|
||||
mdast-util-from-markdown: "npm:^2.0.0"
|
||||
mdast-util-to-markdown: "npm:^2.0.0"
|
||||
micromark-util-normalize-identifier: "npm:^2.0.0"
|
||||
checksum: 10c0/8ab965ee6be3670d76ec0e95b2ba3101fc7444eec47564943ab483d96ac17d29da2a4e6146a2a288be30c21b48c4f3938a1e54b9a46fbdd321d49a5bc0077ed0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-gfm-strikethrough@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "mdast-util-gfm-strikethrough@npm:2.0.0"
|
||||
dependencies:
|
||||
"@types/mdast": "npm:^4.0.0"
|
||||
mdast-util-from-markdown: "npm:^2.0.0"
|
||||
mdast-util-to-markdown: "npm:^2.0.0"
|
||||
checksum: 10c0/b053e93d62c7545019bd914271ea9e5667ad3b3b57d16dbf68e56fea39a7e19b4a345e781312714eb3d43fdd069ff7ee22a3ca7f6149dfa774554f19ce3ac056
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-gfm-table@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "mdast-util-gfm-table@npm:2.0.0"
|
||||
dependencies:
|
||||
"@types/mdast": "npm:^4.0.0"
|
||||
devlop: "npm:^1.0.0"
|
||||
markdown-table: "npm:^3.0.0"
|
||||
mdast-util-from-markdown: "npm:^2.0.0"
|
||||
mdast-util-to-markdown: "npm:^2.0.0"
|
||||
checksum: 10c0/128af47c503a53bd1c79f20642561e54a510ad5e2db1e418d28fefaf1294ab839e6c838e341aef5d7e404f9170b9ca3d1d89605f234efafde93ee51174a6e31e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-gfm-task-list-item@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "mdast-util-gfm-task-list-item@npm:2.0.0"
|
||||
dependencies:
|
||||
"@types/mdast": "npm:^4.0.0"
|
||||
devlop: "npm:^1.0.0"
|
||||
mdast-util-from-markdown: "npm:^2.0.0"
|
||||
mdast-util-to-markdown: "npm:^2.0.0"
|
||||
checksum: 10c0/258d725288482b636c0a376c296431390c14b4f29588675297cb6580a8598ed311fc73ebc312acfca12cc8546f07a3a285a53a3b082712e2cbf5c190d677d834
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-gfm@npm:^3.0.0":
|
||||
version: 3.1.0
|
||||
resolution: "mdast-util-gfm@npm:3.1.0"
|
||||
dependencies:
|
||||
mdast-util-from-markdown: "npm:^2.0.0"
|
||||
mdast-util-gfm-autolink-literal: "npm:^2.0.0"
|
||||
mdast-util-gfm-footnote: "npm:^2.0.0"
|
||||
mdast-util-gfm-strikethrough: "npm:^2.0.0"
|
||||
mdast-util-gfm-table: "npm:^2.0.0"
|
||||
mdast-util-gfm-task-list-item: "npm:^2.0.0"
|
||||
mdast-util-to-markdown: "npm:^2.0.0"
|
||||
checksum: 10c0/4bedcfb6a20e39901c8772f0d2bb2d7a64ae87a54c13cbd92eec062cf470fbb68c2ad754e149af5b30794e2de61c978ab1de1ace03c0c40f443ca9b9b8044f81
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-phrasing@npm:^4.0.0":
|
||||
version: 4.1.0
|
||||
resolution: "mdast-util-phrasing@npm:4.1.0"
|
||||
dependencies:
|
||||
"@types/mdast": "npm:^4.0.0"
|
||||
unist-util-is: "npm:^6.0.0"
|
||||
checksum: 10c0/bf6c31d51349aa3d74603d5e5a312f59f3f65662ed16c58017169a5fb0f84ca98578f626c5ee9e4aa3e0a81c996db8717096705521bddb4a0185f98c12c9b42f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-to-markdown@npm:^2.0.0":
|
||||
version: 2.1.2
|
||||
resolution: "mdast-util-to-markdown@npm:2.1.2"
|
||||
dependencies:
|
||||
"@types/mdast": "npm:^4.0.0"
|
||||
"@types/unist": "npm:^3.0.0"
|
||||
longest-streak: "npm:^3.0.0"
|
||||
mdast-util-phrasing: "npm:^4.0.0"
|
||||
mdast-util-to-string: "npm:^4.0.0"
|
||||
micromark-util-classify-character: "npm:^2.0.0"
|
||||
micromark-util-decode-string: "npm:^2.0.0"
|
||||
unist-util-visit: "npm:^5.0.0"
|
||||
zwitch: "npm:^2.0.0"
|
||||
checksum: 10c0/4649722a6099f12e797bd8d6469b2b43b44e526b5182862d9c7766a3431caad2c0112929c538a972f214e63c015395e5d3f54bd81d9ac1b16e6d8baaf582f749
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdast-util-to-string@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "mdast-util-to-string@npm:4.0.0"
|
||||
dependencies:
|
||||
"@types/mdast": "npm:^4.0.0"
|
||||
checksum: 10c0/2d3c1af29bf3fe9c20f552ee9685af308002488f3b04b12fa66652c9718f66f41a32f8362aa2d770c3ff464c034860b41715902ada2306bb0a055146cef064d7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdurl@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "mdurl@npm:2.0.0"
|
||||
@@ -15599,6 +15835,35 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-queue@npm:^6":
|
||||
version: 6.6.2
|
||||
resolution: "p-queue@npm:6.6.2"
|
||||
dependencies:
|
||||
eventemitter3: "npm:^4.0.4"
|
||||
p-timeout: "npm:^3.2.0"
|
||||
checksum: 10c0/5739ecf5806bbeadf8e463793d5e3004d08bb3f6177bd1a44a005da8fd81bb90f80e4633e1fb6f1dfd35ee663a5c0229abe26aebb36f547ad5a858347c7b0d3e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-retry@npm:^4":
|
||||
version: 4.6.2
|
||||
resolution: "p-retry@npm:4.6.2"
|
||||
dependencies:
|
||||
"@types/retry": "npm:0.12.0"
|
||||
retry: "npm:^0.13.1"
|
||||
checksum: 10c0/d58512f120f1590cfedb4c2e0c42cb3fa66f3cea8a4646632fcb834c56055bb7a6f138aa57b20cc236fb207c9d694e362e0b5c2b14d9b062f67e8925580c73b0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-timeout@npm:^3.2.0":
|
||||
version: 3.2.0
|
||||
resolution: "p-timeout@npm:3.2.0"
|
||||
dependencies:
|
||||
p-finally: "npm:^1.0.0"
|
||||
checksum: 10c0/524b393711a6ba8e1d48137c5924749f29c93d70b671e6db761afa784726572ca06149c715632da8f70c090073afb2af1c05730303f915604fd38ee207b70a61
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-try@npm:^2.0.0":
|
||||
version: 2.2.0
|
||||
resolution: "p-try@npm:2.2.0"
|
||||
@@ -19202,6 +19467,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"trough@npm:^2.0.0":
|
||||
version: 2.2.0
|
||||
resolution: "trough@npm:2.2.0"
|
||||
checksum: 10c0/58b671fc970e7867a48514168894396dd94e6d9d6456aca427cc299c004fe67f35ed7172a36449086b2edde10e78a71a284ec0076809add6834fb8f857ccb9b0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"truncate-utf8-bytes@npm:^1.0.0":
|
||||
version: 1.0.2
|
||||
resolution: "truncate-utf8-bytes@npm:1.0.2"
|
||||
@@ -19554,6 +19826,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unified@npm:^11.0.0, unified@npm:^11.0.5":
|
||||
version: 11.0.5
|
||||
resolution: "unified@npm:11.0.5"
|
||||
dependencies:
|
||||
"@types/unist": "npm:^3.0.0"
|
||||
bail: "npm:^2.0.0"
|
||||
devlop: "npm:^1.0.0"
|
||||
extend: "npm:^3.0.0"
|
||||
is-plain-obj: "npm:^4.0.0"
|
||||
trough: "npm:^2.0.0"
|
||||
vfile: "npm:^6.0.0"
|
||||
checksum: 10c0/53c8e685f56d11d9d458a43e0e74328a4d6386af51c8ac37a3dcabec74ce5026da21250590d4aff6733ccd7dc203116aae2b0769abc18cdf9639a54ae528dfc9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unique-filename@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "unique-filename@npm:5.0.0"
|
||||
@@ -19821,6 +20108,26 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vfile-message@npm:^4.0.0":
|
||||
version: 4.0.3
|
||||
resolution: "vfile-message@npm:4.0.3"
|
||||
dependencies:
|
||||
"@types/unist": "npm:^3.0.0"
|
||||
unist-util-stringify-position: "npm:^4.0.0"
|
||||
checksum: 10c0/33d9f219610d27987689bb14fa5573d2daa146941d1a05416dd7702c4215b23f44ed81d059e70d0e4e24f9a57d5f4dc9f18d35a993f04cf9446a7abe6d72d0c0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vfile@npm:^6.0.0":
|
||||
version: 6.0.3
|
||||
resolution: "vfile@npm:6.0.3"
|
||||
dependencies:
|
||||
"@types/unist": "npm:^3.0.0"
|
||||
vfile-message: "npm:^4.0.0"
|
||||
checksum: 10c0/e5d9eb4810623f23758cfc2205323e33552fb5972e5c2e6587babe08fe4d24859866277404fb9e2a20afb71013860d96ec806cb257536ae463c87d70022ab9ef
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vinyl-contents@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "vinyl-contents@npm:2.0.0"
|
||||
@@ -20908,3 +21215,10 @@ __metadata:
|
||||
checksum: 10c0/7ea31b558e88f9faf44f31dd185e2e1cbf51fed3081787fb96cc2534749b50c0acfc6da7f0922a7353ed092dd358c7d50c28ea96c94d04af64191bd33152eca3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zwitch@npm:^2.0.0":
|
||||
version: 2.0.4
|
||||
resolution: "zwitch@npm:2.0.4"
|
||||
checksum: 10c0/3c7830cdd3378667e058ffdb4cf2bb78ac5711214e2725900873accb23f3dfe5f9e7e5a06dcdc5f29605da976fc45c26d9a13ca334d6eea2245a15e77b8fc06e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
Reference in New Issue
Block a user