Initial code for Slack based search

This commit is contained in:
Jori Lallo
2016-08-22 23:37:01 -07:00
parent 70e46a5c05
commit 4f998bccc8
10 changed files with 131 additions and 37 deletions

View File

@ -8,6 +8,7 @@ class SlackAuthLink extends React.Component {
static propTypes = { static propTypes = {
scopes: React.PropTypes.arrayOf(React.PropTypes.string), scopes: React.PropTypes.arrayOf(React.PropTypes.string),
user: React.PropTypes.object.isRequired, user: React.PropTypes.object.isRequired,
redirectUri: React.PropTypes.string,
} }
static defaultProps = { static defaultProps = {
@ -24,7 +25,7 @@ class SlackAuthLink extends React.Component {
const params = { const params = {
client_id: SLACK_KEY, client_id: SLACK_KEY,
scope: this.props.scopes.join(' '), scope: this.props.scopes.join(' '),
redirect_uri: SLACK_REDIRECT_URI, redirect_uri: this.props.redirectUri || SLACK_REDIRECT_URI,
state: this.props.user.getOauthState(), state: this.props.user.getOauthState(),
}; };

View File

@ -71,6 +71,7 @@ render((
<Route path="/search" component={ Search } onEnter={ requireAuth } /> <Route path="/search" component={ Search } onEnter={ requireAuth } />
<Route path="/auth/slack" component={ SlackAuth } /> <Route path="/auth/slack" component={ SlackAuth } />
<Route path="/auth/slack/commands" component={ SlackAuth } apiPath="/auth.slackCommands" />
<Route path="/404" component={ Error404 } /> <Route path="/404" component={ Error404 } />
<Redirect from="*" to="/404" /> <Redirect from="*" to="/404" />

View File

@ -8,9 +8,7 @@ import Layout from 'components/Layout';
import AtlasPreview from 'components/AtlasPreview'; import AtlasPreview from 'components/AtlasPreview';
import AtlasPreviewLoading from 'components/AtlasPreviewLoading'; import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
import CenteredContent from 'components/CenteredContent'; import CenteredContent from 'components/CenteredContent';
// import DropdownMenu, { MenuItem, MoreIcon } from 'components/DropdownMenu'; import SlackAuthLink from 'components/SlackAuthLink';
// import styles from './Dashboard.scss';
@observer(['user']) @observer(['user'])
class Dashboard extends React.Component { class Dashboard extends React.Component {
@ -23,16 +21,6 @@ class Dashboard extends React.Component {
} }
render() { render() {
// const actions = (
// <Flex>
// <DropdownMenu label={ <MoreIcon /> } >
// <MenuItem onClick={ this.onClickNewAtlas }>
// Add collection
// </MenuItem>
// </DropdownMenu>
// </Flex>
// );
return ( return (
<Flex auto> <Flex auto>
<Layout> <Layout>
@ -44,6 +32,7 @@ class Dashboard extends React.Component {
return (<AtlasPreview key={ collection.id } data={ collection } />); return (<AtlasPreview key={ collection.id } data={ collection } />);
}) } }) }
</Flex> </Flex>
<SlackAuthLink scopes={ ["commands"] } redirectUri={ `${URL}/auth/slack/commands` } />
</CenteredContent> </CenteredContent>
</Layout> </Layout>
</Flex> </Flex>

View File

@ -1,16 +1,31 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { browserHistory } from 'react-router';
import { client } from 'utils/ApiClient';
@observer(['user']) @observer(['user'])
class SlackAuth extends React.Component { class SlackAuth extends React.Component {
static propTypes = { static propTypes = {
user: React.PropTypes.object.isRequired, user: React.PropTypes.object.isRequired,
location: React.PropTypes.object.isRequired, location: React.PropTypes.object.isRequired,
route: React.PropTypes.object.isRequired,
} }
componentDidMount = () => { componentDidMount = async () => {
const { code, state } = this.props.location.query; const { code, state } = this.props.location.query;
this.props.user.authWithSlack(code, state);
if (this.props.route.apiPath) {
try {
await client.post(this.props.route.apiPath, { code });
browserHistory.replace('/dashboard');
} catch (e) {
browserHistory.push('/auth-error');
return;
}
} else {
// Regular Slack authentication
this.props.user.authWithSlack(code, state);
}
} }
render() { render() {

View File

@ -15,7 +15,7 @@ router.post('auth.slack', async (ctx) => {
const body = { const body = {
client_id: process.env.SLACK_KEY, client_id: process.env.SLACK_KEY,
client_secret: process.env.SLACK_SECRET, client_secret: process.env.SLACK_SECRET,
redirect_uri: process.env.SLACK_REDIRECT_URI, redirect_uri: `${process.env.URL}/auth/slack/`,
code, code,
}; };
@ -78,4 +78,29 @@ router.post('auth.slack', async (ctx) => {
} }; } };
}); });
router.post('auth.slackCommands', async (ctx) => {
const { code } = ctx.body;
ctx.assertPresent(code, 'code is required');
const body = {
client_id: process.env.SLACK_KEY,
client_secret: process.env.SLACK_SECRET,
redirect_uri: `${process.env.URL}/auth/slack/commands`,
code,
};
let data;
try {
const response = await fetch(`https://slack.com/api/oauth.access?${querystring.stringify(body)}`);
data = await response.json();
} catch (e) {
throw httpErrors.BadRequest();
}
if (!data.ok) throw httpErrors.BadRequest(data.error);
ctx.body = { success: true };
});
export default router; export default router;

View File

@ -78,25 +78,7 @@ router.post('documents.search', auth(), async (ctx) => {
const user = await ctx.state.user; const user = await ctx.state.user;
const sql = ` const documents = await Document.searchForUser(user, query);
SELECT * FROM documents
WHERE "searchVector" @@ plainto_tsquery('english', :query) AND
"teamId" = '${user.teamId}'::uuid AND
"deletedAt" IS NULL
ORDER BY ts_rank(documents."searchVector", plainto_tsquery('english', :query))
DESC;
`;
const documents = await sequelize
.query(
sql,
{
replacements: {
query,
},
model: Document,
}
);
const data = []; const data = [];
await Promise.all(documents.map(async (document) => { await Promise.all(documents.map(async (document) => {

49
server/api/hooks.js Normal file
View File

@ -0,0 +1,49 @@
import Router from 'koa-router';
import httpErrors from 'http-errors';
import { Document, User } from '../models';
const router = new Router();
router.post('hooks.slack', async (ctx) => {
const {
token,
user_id,
text,
} = ctx.body;
ctx.assertPresent(token, 'token is required');
ctx.assertPresent(user_id, 'user_id is required');
ctx.assertPresent(text, 'text is required');
if (token !== process.env.SLACK_VERIFICATION_TOKEN) throw httpErrors.BadRequest('Invalid token');
const user = await User.find({
where: {
slackId: user_id,
},
});
if (!user) throw httpErrors.BadRequest('Invalid user');
const documents = await Document.searchForUser(user, text, {
limit: 5,
});
const results = [];
let number = 1;
for (const document of documents) {
results.push(`${number}. <${process.env.URL}${document.getUrl()}|${document.title}>`);
number += 1;
}
ctx.body = {
text: 'Search results:',
attachments: [
{
text: results.join('\n'),
color: '#3AA3E3',
},
],
};
});
export default router;

View File

@ -99,4 +99,35 @@ const Document = sequelize.define('document', {
Document.belongsTo(User); Document.belongsTo(User);
Document.searchForUser = async (user, query, options = {}) => {
const limit = options.limit || 15;
const offset = options.offset || 0;
const sql = `
SELECT * FROM documents
WHERE "searchVector" @@ plainto_tsquery('english', :query) AND
"teamId" = '${user.teamId}'::uuid AND
"deletedAt" IS NULL
ORDER BY ts_rank(documents."searchVector", plainto_tsquery('english', :query))
LIMIT :limit
OFFSET :offset
DESC;
`;
const documents = await sequelize
.query(
sql,
{
replacements: {
query,
limit,
offset,
},
model: Document,
}
);
return documents;
}
export default Document; export default Document;

View File

@ -24,7 +24,7 @@ const User = sequelize.define('user', {
}, },
async getTeam() { async getTeam() {
return this.team; return this.team;
} },
}, },
indexes: [ indexes: [
{ {

View File

@ -10,6 +10,7 @@ var definePlugin = new webpack.DefinePlugin({
__PRERELEASE__: JSON.stringify(JSON.parse(process.env.BUILD_PRERELEASE || 'false')), __PRERELEASE__: JSON.stringify(JSON.parse(process.env.BUILD_PRERELEASE || 'false')),
SLACK_REDIRECT_URI: JSON.stringify(process.env.SLACK_REDIRECT_URI), SLACK_REDIRECT_URI: JSON.stringify(process.env.SLACK_REDIRECT_URI),
SLACK_KEY: JSON.stringify(process.env.SLACK_KEY), SLACK_KEY: JSON.stringify(process.env.SLACK_KEY),
URL: JSON.stringify(process.env.URL),
}); });
module.exports = { module.exports = {