Update language on /outline help text
Update Slack hooks to use zod validation closes #5768
This commit is contained in:
@@ -271,6 +271,7 @@ describe("#hooks.interactive", () => {
|
|||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
});
|
});
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
|
type: "message_action",
|
||||||
token: env.SLACK_VERIFICATION_TOKEN,
|
token: env.SLACK_VERIFICATION_TOKEN,
|
||||||
user: {
|
user: {
|
||||||
id: user.authentications[0].providerId,
|
id: user.authentications[0].providerId,
|
||||||
@@ -300,6 +301,7 @@ describe("#hooks.interactive", () => {
|
|||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
});
|
});
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
|
type: "message_action",
|
||||||
token: env.SLACK_VERIFICATION_TOKEN,
|
token: env.SLACK_VERIFICATION_TOKEN,
|
||||||
user: {
|
user: {
|
||||||
id: "unknown-slack-user-id",
|
id: "unknown-slack-user-id",
|
||||||
@@ -324,6 +326,7 @@ describe("#hooks.interactive", () => {
|
|||||||
it("should error if incorrect verification token", async () => {
|
it("should error if incorrect verification token", async () => {
|
||||||
const { user } = await seed();
|
const { user } = await seed();
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
|
type: "message_action",
|
||||||
token: "wrong-verification-token",
|
token: "wrong-verification-token",
|
||||||
user: {
|
user: {
|
||||||
id: user.authentications[0].providerId,
|
id: user.authentications[0].providerId,
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ import { t } from "i18next";
|
|||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
import escapeRegExp from "lodash/escapeRegExp";
|
import escapeRegExp from "lodash/escapeRegExp";
|
||||||
import { Op } from "sequelize";
|
import { Op } from "sequelize";
|
||||||
|
import { z } from "zod";
|
||||||
import { IntegrationService } from "@shared/types";
|
import { IntegrationService } from "@shared/types";
|
||||||
import env from "@server/env";
|
import env from "@server/env";
|
||||||
import { AuthenticationError, InvalidRequestError } from "@server/errors";
|
import {
|
||||||
|
AuthenticationError,
|
||||||
|
InvalidRequestError,
|
||||||
|
ValidationError,
|
||||||
|
} from "@server/errors";
|
||||||
import Logger from "@server/logging/Logger";
|
import Logger from "@server/logging/Logger";
|
||||||
|
import validate from "@server/middlewares/validate";
|
||||||
import {
|
import {
|
||||||
UserAuthentication,
|
UserAuthentication,
|
||||||
AuthenticationProvider,
|
AuthenticationProvider,
|
||||||
@@ -20,9 +26,9 @@ import SearchHelper from "@server/models/helpers/SearchHelper";
|
|||||||
import { APIContext } from "@server/types";
|
import { APIContext } from "@server/types";
|
||||||
import { safeEqual } from "@server/utils/crypto";
|
import { safeEqual } from "@server/utils/crypto";
|
||||||
import { opts } from "@server/utils/i18n";
|
import { opts } from "@server/utils/i18n";
|
||||||
import { assertPresent } from "@server/validation";
|
|
||||||
import presentMessageAttachment from "../presenters/messageAttachment";
|
import presentMessageAttachment from "../presenters/messageAttachment";
|
||||||
import * as Slack from "../slack";
|
import * as Slack from "../slack";
|
||||||
|
import * as T from "./schema";
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
@@ -39,342 +45,366 @@ function verifySlackToken(token: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// triggered by a user posting a getoutline.com link in Slack
|
// triggered by a user posting a getoutline.com link in Slack
|
||||||
router.post("hooks.unfurl", async (ctx: APIContext) => {
|
router.post(
|
||||||
const { challenge, token, event } = ctx.request.body;
|
"hooks.unfurl",
|
||||||
|
validate(T.HooksUnfurlSchema),
|
||||||
// See URL verification handshake documentation on this page:
|
async (ctx: APIContext<T.HooksUnfurlReq>) => {
|
||||||
// https://api.slack.com/apis/connections/events-api
|
// See URL verification handshake documentation on this page:
|
||||||
if (challenge) {
|
// https://api.slack.com/apis/connections/events-api
|
||||||
ctx.body = {
|
if ("challenge" in ctx.input.body) {
|
||||||
challenge,
|
ctx.body = {
|
||||||
};
|
challenge: ctx.input.body.challenge,
|
||||||
return;
|
};
|
||||||
}
|
return;
|
||||||
|
|
||||||
assertPresent(token, "token is required");
|
|
||||||
verifySlackToken(token);
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
where: {
|
|
||||||
providerId: event.user,
|
|
||||||
},
|
|
||||||
model: UserAuthentication,
|
|
||||||
as: "authentications",
|
|
||||||
required: true,
|
|
||||||
separate: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
if (!user) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auth = await IntegrationAuthentication.findOne({
|
|
||||||
where: {
|
|
||||||
service: IntegrationService.Slack,
|
|
||||||
teamId: user.teamId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!auth) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// get content for unfurled links
|
|
||||||
const unfurls = {};
|
|
||||||
|
|
||||||
for (const link of event.links) {
|
|
||||||
const id = link.url.slice(link.url.lastIndexOf("/") + 1);
|
|
||||||
const doc = await Document.findByPk(id);
|
|
||||||
if (!doc || doc.teamId !== user.teamId) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
unfurls[link.url] = {
|
|
||||||
title: doc.title,
|
|
||||||
text: doc.getSummary(),
|
|
||||||
color: doc.collection?.color,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await Slack.post("chat.unfurl", {
|
const { token, event } = ctx.input.body;
|
||||||
token: auth.token,
|
verifySlackToken(token);
|
||||||
channel: event.channel,
|
|
||||||
ts: event.message_ts,
|
|
||||||
unfurls,
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.body = {
|
const user = await User.findOne({
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// triggered by interactions with actions, dialogs, message buttons in Slack
|
|
||||||
router.post("hooks.interactive", async (ctx: APIContext) => {
|
|
||||||
const { payload } = ctx.request.body;
|
|
||||||
assertPresent(payload, "payload is required");
|
|
||||||
|
|
||||||
const data = JSON.parse(payload);
|
|
||||||
const { callback_id, token } = data;
|
|
||||||
|
|
||||||
assertPresent(token, "token is required");
|
|
||||||
assertPresent(callback_id, "callback_id is required");
|
|
||||||
verifySlackToken(token);
|
|
||||||
|
|
||||||
// we find the document based on the users teamId to ensure access
|
|
||||||
const document = await Document.scope("withCollection").findByPk(
|
|
||||||
data.callback_id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw InvalidRequestError("Invalid callback_id");
|
|
||||||
}
|
|
||||||
|
|
||||||
const team = await Team.findByPk(document.teamId, { rejectOnEmpty: true });
|
|
||||||
|
|
||||||
// respond with a public message that will be posted in the original channel
|
|
||||||
ctx.body = {
|
|
||||||
response_type: "in_channel",
|
|
||||||
replace_original: false,
|
|
||||||
attachments: [
|
|
||||||
presentMessageAttachment(
|
|
||||||
document,
|
|
||||||
team,
|
|
||||||
document.collection,
|
|
||||||
document.getSummary()
|
|
||||||
),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// triggered by the /outline command in Slack
|
|
||||||
router.post("hooks.slack", async (ctx: APIContext) => {
|
|
||||||
const { token, team_id, user_id, text = "" } = ctx.request.body;
|
|
||||||
assertPresent(token, "token is required");
|
|
||||||
assertPresent(team_id, "team_id is required");
|
|
||||||
assertPresent(user_id, "user_id is required");
|
|
||||||
verifySlackToken(token);
|
|
||||||
|
|
||||||
let user, team;
|
|
||||||
|
|
||||||
// attempt to find the corresponding team for this request based on the team_id
|
|
||||||
team = await Team.findOne({
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
where: {
|
|
||||||
name: "slack",
|
|
||||||
providerId: team_id,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
as: "authenticationProviders",
|
|
||||||
model: AuthenticationProvider,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (team) {
|
|
||||||
const authentication = await UserAuthentication.findOne({
|
|
||||||
where: {
|
|
||||||
providerId: user_id,
|
|
||||||
},
|
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
where: {
|
where: {
|
||||||
teamId: team.id,
|
providerId: event.user,
|
||||||
},
|
},
|
||||||
model: User,
|
model: UserAuthentication,
|
||||||
as: "user",
|
as: "authentications",
|
||||||
|
required: true,
|
||||||
|
separate: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auth = await IntegrationAuthentication.findOne({
|
||||||
|
where: {
|
||||||
|
service: IntegrationService.Slack,
|
||||||
|
teamId: user.teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!auth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// get content for unfurled links
|
||||||
|
const unfurls = {};
|
||||||
|
|
||||||
|
for (const link of event.links) {
|
||||||
|
const id = link.url.slice(link.url.lastIndexOf("/") + 1);
|
||||||
|
const doc = await Document.findByPk(id);
|
||||||
|
if (!doc || doc.teamId !== user.teamId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
unfurls[link.url] = {
|
||||||
|
title: doc.title,
|
||||||
|
text: doc.getSummary(),
|
||||||
|
color: doc.collection?.color,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await Slack.post("chat.unfurl", {
|
||||||
|
token: auth.token,
|
||||||
|
channel: event.channel,
|
||||||
|
ts: event.message_ts,
|
||||||
|
unfurls,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// triggered by interactions with actions, dialogs, message buttons in Slack
|
||||||
|
router.post(
|
||||||
|
"hooks.interactive",
|
||||||
|
validate(T.HooksInteractiveSchema),
|
||||||
|
async (ctx: APIContext<T.HooksInteractiveReq>) => {
|
||||||
|
const { payload } = ctx.input.body;
|
||||||
|
let callback_id, token;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// https://api.slack.com/interactivity/handling#payloads
|
||||||
|
const data = JSON.parse(payload);
|
||||||
|
|
||||||
|
const parsed = z
|
||||||
|
.object({
|
||||||
|
type: z.string(),
|
||||||
|
callback_id: z.string(),
|
||||||
|
token: z.string(),
|
||||||
|
})
|
||||||
|
.parse(data);
|
||||||
|
|
||||||
|
callback_id = parsed.callback_id;
|
||||||
|
token = parsed.token;
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error("Failed to parse Slack interactive payload", err, {
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
throw ValidationError("Invalid payload");
|
||||||
|
}
|
||||||
|
|
||||||
|
verifySlackToken(token);
|
||||||
|
|
||||||
|
// we find the document based on the users teamId to ensure access
|
||||||
|
const document = await Document.scope("withCollection").findByPk(
|
||||||
|
callback_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
throw InvalidRequestError("Invalid callback_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
const team = await Team.findByPk(document.teamId, { rejectOnEmpty: true });
|
||||||
|
|
||||||
|
// respond with a public message that will be posted in the original channel
|
||||||
|
ctx.body = {
|
||||||
|
response_type: "in_channel",
|
||||||
|
replace_original: false,
|
||||||
|
attachments: [
|
||||||
|
presentMessageAttachment(
|
||||||
|
document,
|
||||||
|
team,
|
||||||
|
document.collection,
|
||||||
|
document.getSummary()
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// triggered by the /outline command in Slack
|
||||||
|
router.post(
|
||||||
|
"hooks.slack",
|
||||||
|
validate(T.HooksSlackCommandSchema),
|
||||||
|
async (ctx: APIContext<T.HooksSlackCommandReq>) => {
|
||||||
|
const { token, team_id, user_id, text } = ctx.input.body;
|
||||||
|
verifySlackToken(token);
|
||||||
|
|
||||||
|
let user, team;
|
||||||
|
|
||||||
|
// attempt to find the corresponding team for this request based on the team_id
|
||||||
|
team = await Team.findOne({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
name: "slack",
|
||||||
|
providerId: team_id,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
as: "authenticationProviders",
|
||||||
|
model: AuthenticationProvider,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (authentication) {
|
if (team) {
|
||||||
user = authentication.user;
|
const authentication = await UserAuthentication.findOne({
|
||||||
}
|
where: {
|
||||||
} else {
|
providerId: user_id,
|
||||||
// If we couldn't find a team it's still possible that the request is from
|
|
||||||
// a team that authenticated with a different service, but connected Slack
|
|
||||||
// via integration
|
|
||||||
const integration = await Integration.findOne({
|
|
||||||
where: {
|
|
||||||
service: IntegrationService.Slack,
|
|
||||||
settings: {
|
|
||||||
serviceTeamId: team_id,
|
|
||||||
},
|
},
|
||||||
},
|
include: [
|
||||||
include: [
|
{
|
||||||
{
|
|
||||||
model: Team,
|
|
||||||
as: "team",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (integration) {
|
|
||||||
team = integration.team;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle "help" command or no input
|
|
||||||
if (text.trim() === "help" || !text.trim()) {
|
|
||||||
ctx.body = {
|
|
||||||
response_type: "ephemeral",
|
|
||||||
text: t("How to use {{ command }}", {
|
|
||||||
command: "/outline",
|
|
||||||
...opts(user),
|
|
||||||
}),
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
text: t(
|
|
||||||
"To search your knowledgebase use {{ command }}. \nYou’ve already learned how to get help with {{ command2 }}.",
|
|
||||||
{
|
|
||||||
command: `/outline keyword`,
|
|
||||||
command2: `/outline help`,
|
|
||||||
...opts(user),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should be super rare, how does someone end up being able to make a valid
|
|
||||||
// request from Slack that connects to no teams in Outline.
|
|
||||||
if (!team) {
|
|
||||||
ctx.body = {
|
|
||||||
response_type: "ephemeral",
|
|
||||||
text: t(
|
|
||||||
`Sorry, we couldn’t find an integration for your team. Head to your {{ appName }} settings to set one up.`,
|
|
||||||
{
|
|
||||||
...opts(user),
|
|
||||||
appName: env.APP_NAME,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to find the user by matching the email address if it is confirmed on
|
|
||||||
// Slack's side. It's always trusted on our side as it is only updatable
|
|
||||||
// through the authentication provider.
|
|
||||||
if (!user) {
|
|
||||||
const auth = await IntegrationAuthentication.findOne({
|
|
||||||
where: {
|
|
||||||
scopes: { [Op.contains]: ["identity.email"] },
|
|
||||||
service: IntegrationService.Slack,
|
|
||||||
teamId: team.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (auth) {
|
|
||||||
try {
|
|
||||||
const response = await Slack.request("users.info", {
|
|
||||||
token: auth.token,
|
|
||||||
user: user_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.user.is_email_confirmed && response.user.profile.email) {
|
|
||||||
user = await User.findOne({
|
|
||||||
where: {
|
where: {
|
||||||
email: response.user.profile.email,
|
|
||||||
teamId: team.id,
|
teamId: team.id,
|
||||||
},
|
},
|
||||||
});
|
model: User,
|
||||||
}
|
as: "user",
|
||||||
} catch (err) {
|
required: true,
|
||||||
// Old connections do not have the correct permissions to access user info
|
},
|
||||||
// so errors here are expected.
|
],
|
||||||
Logger.info(
|
});
|
||||||
"utils",
|
|
||||||
"Failed requesting users.info from Slack, the Slack integration should be reconnected.",
|
if (authentication) {
|
||||||
|
user = authentication.user;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If we couldn't find a team it's still possible that the request is from
|
||||||
|
// a team that authenticated with a different service, but connected Slack
|
||||||
|
// via integration
|
||||||
|
const integration = await Integration.findOne({
|
||||||
|
where: {
|
||||||
|
service: IntegrationService.Slack,
|
||||||
|
settings: {
|
||||||
|
serviceTeamId: team_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: [
|
||||||
{
|
{
|
||||||
teamId: auth.teamId,
|
model: Team,
|
||||||
}
|
as: "team",
|
||||||
);
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (integration) {
|
||||||
|
team = integration.team;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
// Handle "help" command or no input
|
||||||
limit: 5,
|
if (text.trim() === "help" || !text.trim()) {
|
||||||
};
|
ctx.body = {
|
||||||
|
response_type: "ephemeral",
|
||||||
// If we were able to map the request to a user then we can use their permissions
|
text: t("How to use {{ command }}", {
|
||||||
// to load more documents based on the collections they have access to. Otherwise
|
command: "/outline",
|
||||||
// just a generic search against team-visible documents is allowed.
|
...opts(user),
|
||||||
const { results, totalCount } = user
|
}),
|
||||||
? await SearchHelper.searchForUser(user, text, options)
|
attachments: [
|
||||||
: await SearchHelper.searchForTeam(team, text, options);
|
{
|
||||||
|
text: t(
|
||||||
void SearchQuery.create({
|
"To search your knowledgebase use {{ command }}. \nType {{ command2 }} help to display this help text.",
|
||||||
userId: user ? user.id : null,
|
{
|
||||||
teamId: team.id,
|
command: `/outline keyword`,
|
||||||
source: "slack",
|
command2: `/outline help`,
|
||||||
query: text,
|
...opts(user),
|
||||||
results: totalCount,
|
}
|
||||||
}).catch((err) => {
|
),
|
||||||
Logger.error("Failed to create search query", err);
|
},
|
||||||
});
|
],
|
||||||
|
};
|
||||||
const haventSignedIn = t(
|
return;
|
||||||
`It looks like you haven’t signed in to {{ appName }} yet, so results may be limited`,
|
|
||||||
{
|
|
||||||
...opts(user),
|
|
||||||
appName: env.APP_NAME,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Map search results to the format expected by the Slack API
|
|
||||||
if (results.length) {
|
|
||||||
const attachments = [];
|
|
||||||
|
|
||||||
for (const result of results) {
|
|
||||||
const queryIsInTitle = !!result.document.title
|
|
||||||
.toLowerCase()
|
|
||||||
.match(escapeRegExp(text.toLowerCase()));
|
|
||||||
attachments.push(
|
|
||||||
presentMessageAttachment(
|
|
||||||
result.document,
|
|
||||||
team,
|
|
||||||
result.document.collection,
|
|
||||||
queryIsInTitle ? undefined : result.context,
|
|
||||||
env.SLACK_MESSAGE_ACTIONS
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
name: "post",
|
|
||||||
text: t("Post to Channel", opts(user)),
|
|
||||||
type: "button",
|
|
||||||
value: result.document.id,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: undefined
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = {
|
// This should be super rare, how does someone end up being able to make a valid
|
||||||
text: user
|
// request from Slack that connects to no teams in Outline.
|
||||||
? t(`This is what we found for "{{ term }}"`, {
|
if (!team) {
|
||||||
|
ctx.body = {
|
||||||
|
response_type: "ephemeral",
|
||||||
|
text: t(
|
||||||
|
`Sorry, we couldn’t find an integration for your team. Head to your {{ appName }} settings to set one up.`,
|
||||||
|
{
|
||||||
...opts(user),
|
...opts(user),
|
||||||
term: text,
|
appName: env.APP_NAME,
|
||||||
})
|
}
|
||||||
: t(`This is what we found for "{{ term }}"`, {
|
),
|
||||||
term: text,
|
};
|
||||||
}) + ` (${haventSignedIn})…`,
|
return;
|
||||||
attachments,
|
}
|
||||||
};
|
|
||||||
} else {
|
// Try to find the user by matching the email address if it is confirmed on
|
||||||
ctx.body = {
|
// Slack's side. It's always trusted on our side as it is only updatable
|
||||||
text: user
|
// through the authentication provider.
|
||||||
? t(`No results for "{{ term }}"`, {
|
if (!user) {
|
||||||
...opts(user),
|
const auth = await IntegrationAuthentication.findOne({
|
||||||
term: text,
|
where: {
|
||||||
})
|
scopes: { [Op.contains]: ["identity.email"] },
|
||||||
: t(`No results for "{{ term }}"`, { term: text }) +
|
service: IntegrationService.Slack,
|
||||||
` (${haventSignedIn})…`,
|
teamId: team.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (auth) {
|
||||||
|
try {
|
||||||
|
const response = await Slack.request("users.info", {
|
||||||
|
token: auth.token,
|
||||||
|
user: user_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.user.is_email_confirmed && response.user.profile.email) {
|
||||||
|
user = await User.findOne({
|
||||||
|
where: {
|
||||||
|
email: response.user.profile.email,
|
||||||
|
teamId: team.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Old connections do not have the correct permissions to access user info
|
||||||
|
// so errors here are expected.
|
||||||
|
Logger.info(
|
||||||
|
"utils",
|
||||||
|
"Failed requesting users.info from Slack, the Slack integration should be reconnected.",
|
||||||
|
{
|
||||||
|
teamId: auth.teamId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
limit: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If we were able to map the request to a user then we can use their permissions
|
||||||
|
// to load more documents based on the collections they have access to. Otherwise
|
||||||
|
// just a generic search against team-visible documents is allowed.
|
||||||
|
const { results, totalCount } = user
|
||||||
|
? await SearchHelper.searchForUser(user, text, options)
|
||||||
|
: await SearchHelper.searchForTeam(team, text, options);
|
||||||
|
|
||||||
|
void SearchQuery.create({
|
||||||
|
userId: user ? user.id : null,
|
||||||
|
teamId: team.id,
|
||||||
|
source: "slack",
|
||||||
|
query: text,
|
||||||
|
results: totalCount,
|
||||||
|
}).catch((err) => {
|
||||||
|
Logger.error("Failed to create search query", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
const haventSignedIn = t(
|
||||||
|
`It looks like you haven’t signed in to {{ appName }} yet, so results may be limited`,
|
||||||
|
{
|
||||||
|
...opts(user),
|
||||||
|
appName: env.APP_NAME,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Map search results to the format expected by the Slack API
|
||||||
|
if (results.length) {
|
||||||
|
const attachments = [];
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
const queryIsInTitle = !!result.document.title
|
||||||
|
.toLowerCase()
|
||||||
|
.match(escapeRegExp(text.toLowerCase()));
|
||||||
|
attachments.push(
|
||||||
|
presentMessageAttachment(
|
||||||
|
result.document,
|
||||||
|
team,
|
||||||
|
result.document.collection,
|
||||||
|
queryIsInTitle ? undefined : result.context,
|
||||||
|
env.SLACK_MESSAGE_ACTIONS
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: "post",
|
||||||
|
text: t("Post to Channel", opts(user)),
|
||||||
|
type: "button",
|
||||||
|
value: result.document.id,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
text: user
|
||||||
|
? t(`This is what we found for "{{ term }}"`, {
|
||||||
|
...opts(user),
|
||||||
|
term: text,
|
||||||
|
})
|
||||||
|
: t(`This is what we found for "{{ term }}"`, {
|
||||||
|
...opts(user),
|
||||||
|
term: text,
|
||||||
|
}) + ` (${haventSignedIn})…`,
|
||||||
|
attachments,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
ctx.body = {
|
||||||
|
text: user
|
||||||
|
? t(`No results for "{{ term }}"`, {
|
||||||
|
...opts(user),
|
||||||
|
term: text,
|
||||||
|
})
|
||||||
|
: t(`No results for "{{ term }}"`, { ...opts(user), term: text }) +
|
||||||
|
` (${haventSignedIn})…`,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
44
plugins/slack/server/api/schema.ts
Normal file
44
plugins/slack/server/api/schema.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const HooksUnfurlSchema = z.object({
|
||||||
|
body: z
|
||||||
|
.object({
|
||||||
|
challenge: z.string(),
|
||||||
|
})
|
||||||
|
.or(
|
||||||
|
z.object({
|
||||||
|
token: z.string(),
|
||||||
|
event: z.object({
|
||||||
|
channel: z.string(),
|
||||||
|
message_ts: z.string(),
|
||||||
|
links: z.array(
|
||||||
|
z.object({
|
||||||
|
url: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
user: z.string(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type HooksUnfurlReq = z.infer<typeof HooksUnfurlSchema>;
|
||||||
|
|
||||||
|
export const HooksSlackCommandSchema = z.object({
|
||||||
|
body: z.object({
|
||||||
|
token: z.string(),
|
||||||
|
team_id: z.string(),
|
||||||
|
user_id: z.string(),
|
||||||
|
text: z.string().optional().default(""),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type HooksSlackCommandReq = z.infer<typeof HooksSlackCommandSchema>;
|
||||||
|
|
||||||
|
export const HooksInteractiveSchema = z.object({
|
||||||
|
body: z.object({
|
||||||
|
payload: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type HooksInteractiveReq = z.infer<typeof HooksInteractiveSchema>;
|
||||||
@@ -906,7 +906,7 @@
|
|||||||
"Connect": "Connect",
|
"Connect": "Connect",
|
||||||
"The Slack integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.": "The Slack integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.",
|
"The Slack integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.": "The Slack integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.",
|
||||||
"How to use {{ command }}": "How to use {{ command }}",
|
"How to use {{ command }}": "How to use {{ command }}",
|
||||||
"To search your knowledgebase use {{ command }}. \nYou’ve already learned how to get help with {{ command2 }}.": "To search your knowledgebase use {{ command }}. \nYou’ve already learned how to get help with {{ command2 }}.",
|
"To search your knowledgebase use {{ command }}. \nType {{ command2 }} help to display this help text.": "To search your knowledgebase use {{ command }}. \nType {{ command2 }} help to display this help text.",
|
||||||
"Sorry, we couldn’t find an integration for your team. Head to your {{ appName }} settings to set one up.": "Sorry, we couldn’t find an integration for your team. Head to your {{ appName }} settings to set one up.",
|
"Sorry, we couldn’t find an integration for your team. Head to your {{ appName }} settings to set one up.": "Sorry, we couldn’t find an integration for your team. Head to your {{ appName }} settings to set one up.",
|
||||||
"It looks like you haven’t signed in to {{ appName }} yet, so results may be limited": "It looks like you haven’t signed in to {{ appName }} yet, so results may be limited",
|
"It looks like you haven’t signed in to {{ appName }} yet, so results may be limited": "It looks like you haven’t signed in to {{ appName }} yet, so results may be limited",
|
||||||
"Post to Channel": "Post to Channel",
|
"Post to Channel": "Post to Channel",
|
||||||
|
|||||||
Reference in New Issue
Block a user