Adds 'post to channel' functionality. (#901)

* Adds 'post to channel' functionality. Closes #613

* Add specs
Update Slack integration marketing page

* Fix specs

* 💚
This commit is contained in:
Tom Moor 2019-02-19 22:42:13 -08:00 committed by GitHub
parent e283d15d7e
commit 19fc99944a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 138 additions and 9 deletions

View File

@ -28,6 +28,7 @@ GOOGLE_ALLOWED_DOMAINS=
# Third party credentials (optional)
SLACK_VERIFICATION_TOKEN=PLxk6OlXXXXXVj3YYYY
SLACK_APP_ID=A0XXXXXXX
SLACK_MESSAGE_ACTIONS=true
GOOGLE_ANALYTICS_ID=
BUGSNAG_KEY=
GITHUB_ACCESS_TOKEN=

View File

@ -7,6 +7,7 @@ import { presentSlackAttachment } from '../presenters';
import * as Slack from '../slack';
const router = new Router();
// triggered by a user posting a getoutline.com link in Slack
router.post('hooks.unfurl', async ctx => {
const { challenge, token, event } = ctx.body;
if (challenge) return (ctx.body = ctx.body.challenge);
@ -46,6 +47,50 @@ router.post('hooks.unfurl', async ctx => {
});
});
// triggered by interactions with actions, dialogs, message buttons in Slack
router.post('hooks.interactive', async ctx => {
const { payload } = ctx.body;
ctx.assertPresent(payload, 'payload is required');
const data = JSON.parse(payload);
const { callback_id, token } = data;
ctx.assertPresent(token, 'token is required');
ctx.assertPresent(callback_id, 'callback_id is required');
if (token !== process.env.SLACK_VERIFICATION_TOKEN)
throw new AuthenticationError('Invalid verification token');
const user = await User.find({
where: { service: 'slack', serviceId: data.user.id },
});
if (!user) {
ctx.body = {
text: 'Sorry, we couldnt find your user on this team in Outline.',
response_type: 'ephemeral',
replace_original: false,
};
return;
}
// we find the document based on the users teamId to ensure access
const document = await Document.find({
where: { id: data.callback_id, teamId: user.teamId },
});
if (!document) throw new InvalidRequestError('Invalid document');
const team = await Team.findById(user.teamId);
// respond with a public message that will be posted in the original channel
ctx.body = {
response_type: 'in_channel',
replace_original: false,
attachments: [
presentSlackAttachment(document, team, document.getSummary()),
],
};
});
// triggered by the /outline command in Slack
router.post('hooks.slack', async ctx => {
const { token, user_id, text } = ctx.body;
ctx.assertPresent(token, 'token is required');
@ -53,7 +98,7 @@ router.post('hooks.slack', async ctx => {
ctx.assertPresent(text, 'text is required');
if (token !== process.env.SLACK_VERIFICATION_TOKEN)
throw new AuthenticationError('Invalid token');
throw new AuthenticationError('Invalid verification token');
const user = await User.find({
where: {
@ -61,11 +106,14 @@ router.post('hooks.slack', async ctx => {
serviceId: user_id,
},
});
if (!user) throw new InvalidRequestError('Invalid user');
if (!user) {
ctx.body = {
text: 'Sorry, we couldnt find your user on this team in Outline.',
};
return;
}
const team = await Team.findById(user.teamId);
const results = await Document.searchForUser(user, text, {
limit: 5,
});
@ -81,7 +129,17 @@ router.post('hooks.slack', async ctx => {
presentSlackAttachment(
result.document,
team,
queryIsInTitle ? undefined : result.context
queryIsInTitle ? undefined : result.context,
process.env.SLACK_MESSAGE_ACTIONS
? [
{
name: 'post',
text: 'Post to Channel',
type: 'button',
value: result.document.id,
},
]
: undefined
)
);
}

View File

@ -128,7 +128,7 @@ describe('#hooks.slack', async () => {
);
});
it('should error if unknown user', async () => {
it('should respond with error if unknown user', async () => {
const res = await server.post('/api/hooks.slack', {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
@ -136,7 +136,10 @@ describe('#hooks.slack', async () => {
text: 'Welcome',
},
});
expect(res.status).toEqual(400);
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.text.includes('Sorry')).toEqual(true);
expect(body.attachments).toEqual(undefined);
});
it('should error if incorrect verification token', async () => {
@ -152,3 +155,56 @@ describe('#hooks.slack', async () => {
expect(res.status).toEqual(401);
});
});
describe('#hooks.interactive', async () => {
it('should respond with replacement message', async () => {
const user = await buildUser();
const document = await buildDocument({
title: 'This title contains a search term',
userId: user.id,
teamId: user.teamId,
});
const payload = JSON.stringify({
token: process.env.SLACK_VERIFICATION_TOKEN,
user: { id: user.serviceId, name: user.name },
callback_id: document.id,
});
const res = await server.post('/api/hooks.interactive', {
body: { payload },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.response_type).toEqual('in_channel');
expect(body.attachments.length).toEqual(1);
expect(body.attachments[0].title).toEqual(document.title);
});
it('should respond with error if unknown user', async () => {
const payload = JSON.stringify({
token: process.env.SLACK_VERIFICATION_TOKEN,
user: { id: 'not-a-user-id', name: 'unknown' },
callback_id: 'doesnt-matter',
});
const res = await server.post('/api/hooks.interactive', {
body: { payload },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.text.includes('Sorry')).toEqual(true);
expect(body.attachments).toEqual(undefined);
});
it('should error if incorrect verification token', async () => {
const { user } = await seed();
const payload = JSON.stringify({
token: 'wrong-verification-token',
user: { id: user.serviceId, name: user.name },
callback_id: 'doesnt-matter',
});
const res = await server.post('/api/hooks.interactive', {
body: { payload },
});
expect(res.status).toEqual(401);
});
});

View File

@ -7,7 +7,7 @@ Sign In with Slack means your team doesn't have to worry about invites, password
## Search your Knowledgebase
Optionally [Connect to Slack](https://www.getoutline.com/settings/integrations/slack) to enable the `/outline` slack command. Once enabled team members can easily search your wiki from within Slack by typing `/outline search term`.
Optionally [Connect to Slack](https://www.getoutline.com/settings/integrations/slack) to enable the `/outline` slack command. Once enabled, team members can easily search your wiki from within Slack by typing `/outline search term`, and post results directly back to the Slack channel.
![Slack Search Integration](/images/screenshots/slack-search.png)

View File

@ -1,7 +1,19 @@
// @flow
import { Document, Team } from '../models';
function present(document: Document, team: Team, context?: string) {
type Action = {
type: string,
text: string,
name: string,
value: string,
};
function present(
document: Document,
team: Team,
context?: string,
actions?: Action[]
) {
// the context contains <b> tags around search terms, we convert them here
// to the markdown format that slack expects to receive.
const text = context
@ -13,8 +25,10 @@ function present(document: Document, team: Team, context?: string) {
title: document.title,
title_link: `${team.url}${document.url}`,
footer: document.collection.name,
callback_id: document.id,
text,
ts: document.getTimestamp(),
actions,
};
}