Post to Slack (#603)

* Migrations

* WIP: Integration model, slack perms / hooks

* So so rough it pains me. Building this new model is revealing just how much needs to be refactored

* Working connect and post

* Cleanup UI, upating documents

* Show when slack command is connected

* stash

* 💚

* Add documents.update trigger

* Authorization, tidying

* Fixed integration policy

* pick integration presenter keys
This commit is contained in:
Tom Moor
2018-04-03 20:36:25 -07:00
committed by GitHub
parent 17900c6a11
commit 44cb509ebf
38 changed files with 665 additions and 105 deletions

View File

@@ -2,7 +2,7 @@
import Router from 'koa-router';
import auth from './middlewares/authentication';
import { presentUser, presentTeam } from '../presenters';
import { Authentication, User, Team } from '../models';
import { Authentication, Integration, User, Team } from '../models';
import * as Slack from '../slack';
const router = new Router();
@@ -89,14 +89,56 @@ router.post('auth.slackCommands', auth(), async ctx => {
const user = ctx.state.user;
const endpoint = `${process.env.URL || ''}/auth/slack/commands`;
const data = await Slack.oauthAccess(code, endpoint);
const serviceId = 'slack';
await Authentication.create({
serviceId: 'slack',
const authentication = await Authentication.create({
serviceId,
userId: user.id,
teamId: user.teamId,
token: data.access_token,
scopes: data.scope.split(','),
});
await Integration.create({
serviceId,
type: 'command',
userId: user.id,
teamId: user.teamId,
authenticationId: authentication.id,
});
});
router.post('auth.slackPost', auth(), async ctx => {
const { code, collectionId } = ctx.body;
ctx.assertPresent(code, 'code is required');
const user = ctx.state.user;
const endpoint = `${process.env.URL || ''}/auth/slack/post`;
const data = await Slack.oauthAccess(code, endpoint);
const serviceId = 'slack';
const authentication = await Authentication.create({
serviceId,
userId: user.id,
teamId: user.teamId,
token: data.access_token,
scopes: data.scope.split(','),
});
await Integration.create({
serviceId,
type: 'post',
userId: user.id,
teamId: user.teamId,
authenticationId: authentication.id,
collectionId,
events: [],
settings: {
url: data.incoming_webhook.url,
channel: data.incoming_webhook.channel,
channelId: data.incoming_webhook.channel_id,
},
});
});
export default router;

View File

@@ -5,7 +5,8 @@ import auth from './middlewares/authentication';
import pagination from './middlewares/pagination';
import { presentDocument, presentRevision } from '../presenters';
import { Document, Collection, Star, View, Revision } from '../models';
import { ValidationError, InvalidRequestError } from '../errors';
import { InvalidRequestError } from '../errors';
import events from '../events';
import policy from '../policies';
const { authorize } = policy;
@@ -302,7 +303,6 @@ router.post('documents.create', auth(), async ctx => {
authorize(user, 'read', parentDocumentObj);
}
const publishedAt = publish === false ? null : new Date();
let document = await Document.create({
parentDocumentId: parentDocumentObj.id,
atlasId: collection.id,
@@ -310,20 +310,19 @@ router.post('documents.create', auth(), async ctx => {
userId: user.id,
lastModifiedById: user.id,
createdById: user.id,
publishedAt,
title,
text,
});
if (publishedAt && collection.type === 'atlas') {
await collection.addDocumentToStructure(document, index);
if (publish) {
await document.publish();
}
// reload to get all of the data needed to present (user, collection etc)
// we need to specify publishedAt to bypass default scope that only returns
// published documents
document = await Document.find({
where: { id: document.id, publishedAt },
where: { id: document.id, publishedAt: document.publishedAt },
});
ctx.body = {
@@ -332,7 +331,7 @@ router.post('documents.create', auth(), async ctx => {
});
router.post('documents.update', auth(), async ctx => {
const { id, title, text, publish, lastRevision } = ctx.body;
const { id, title, text, publish, done, lastRevision } = ctx.body;
ctx.assertPresent(id, 'id is required');
ctx.assertPresent(title || text, 'title or text is required');
@@ -346,24 +345,20 @@ router.post('documents.update', auth(), async ctx => {
}
// Update document
const previouslyPublished = !!document.publishedAt;
if (publish) document.publishedAt = new Date();
if (title) document.title = title;
if (text) document.text = text;
document.lastModifiedById = user.id;
await document.save();
const collection = document.collection;
if (collection.type === 'atlas') {
if (previouslyPublished) {
await collection.updateDocument(document);
} else if (publish) {
await collection.addDocumentToStructure(document);
if (publish) {
await document.publish();
} else {
await document.save();
if (document.publishedAt && done) {
events.add({ name: 'documents.update', model: document });
}
}
document.collection = collection;
ctx.body = {
data: await presentDocument(ctx, document),
};

View File

@@ -385,12 +385,7 @@ describe('#documents.create', async () => {
},
});
const body = await res.json();
const newDocument = await Document.findOne({
where: {
id: body.data.id,
},
});
const newDocument = await Document.findById(body.data.id);
expect(res.status).toEqual(200);
expect(newDocument.parentDocumentId).toBe(null);
expect(newDocument.collection.id).toBe(collection.id);

View File

@@ -2,6 +2,7 @@
import Router from 'koa-router';
import { AuthenticationError, InvalidRequestError } from '../errors';
import { Authentication, Document, User } from '../models';
import { presentSlackAttachment } from '../presenters';
import * as Slack from '../slack';
const router = new Router();
@@ -67,14 +68,7 @@ router.post('hooks.slack', async ctx => {
if (documents.length) {
const attachments = [];
for (const document of documents) {
attachments.push({
color: document.collection.color,
title: document.title,
title_link: `${process.env.URL}${document.getUrl()}`,
footer: document.collection.name,
text: document.getSummary(),
ts: document.getTimestamp(),
});
attachments.push(presentSlackAttachment(document));
}
ctx.body = {

View File

@@ -13,6 +13,7 @@ import views from './views';
import hooks from './hooks';
import apiKeys from './apiKeys';
import team from './team';
import integrations from './integrations';
import validation from './middlewares/validation';
import methodOverride from '../middlewares/methodOverride';
@@ -74,6 +75,7 @@ router.use('/', views.routes());
router.use('/', hooks.routes());
router.use('/', apiKeys.routes());
router.use('/', team.routes());
router.use('/', integrations.routes());
// Router is embedded in a Koa application wrapper, because koa-router does not
// allow middleware to catch any routes which were not explicitly defined.

View File

@@ -0,0 +1,48 @@
// @flow
import Router from 'koa-router';
import Integration from '../models/Integration';
import pagination from './middlewares/pagination';
import auth from './middlewares/authentication';
import { presentIntegration } from '../presenters';
import policy from '../policies';
const { authorize } = policy;
const router = new Router();
router.post('integrations.list', auth(), pagination(), async ctx => {
let { sort = 'updatedAt', direction } = ctx.body;
if (direction !== 'ASC') direction = 'DESC';
const user = ctx.state.user;
const integrations = await Integration.findAll({
where: { teamId: user.teamId },
order: [[sort, direction]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
const data = await Promise.all(
integrations.map(integration => presentIntegration(ctx, integration))
);
ctx.body = {
pagination: ctx.state.pagination,
data,
};
});
router.post('integrations.delete', auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
const integration = await Integration.findById(id);
authorize(ctx.state.user, 'delete', integration);
await integration.destroy();
ctx.body = {
success: true,
};
});
export default router;