Initial code for Slack based search
This commit is contained in:
@ -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(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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" />
|
||||||
|
@ -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>
|
||||||
|
@ -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() {
|
||||||
|
@ -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;
|
||||||
|
@ -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
49
server/api/hooks.js
Normal 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;
|
@ -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;
|
||||||
|
@ -24,7 +24,7 @@ const User = sequelize.define('user', {
|
|||||||
},
|
},
|
||||||
async getTeam() {
|
async getTeam() {
|
||||||
return this.team;
|
return this.team;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
indexes: [
|
indexes: [
|
||||||
{
|
{
|
||||||
|
@ -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 = {
|
||||||
|
Reference in New Issue
Block a user