Merge pull request #7938 from HabitRPG/send_flags_to_slack

Send flags to slack
This commit is contained in:
Blade Barringer 2016-09-02 11:32:42 -05:00 committed by GitHub
commit aa2c941e1a
6 changed files with 341 additions and 38 deletions

View file

@ -74,5 +74,9 @@
"APP_ID": "appId",
"KEY": "key",
"SECRET": "secret"
},
"SLACK": {
"FLAGGING_URL": "https://hooks.slack.com/services/id/id/id",
"FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/"
}
}

View file

@ -4,6 +4,7 @@
"version": "3.37.0",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "slackhq/node-slack-sdk#2ee794cd31326c54f38c518eef2b9d223327d939",
"accepts": "^1.3.2",
"amazon-payments": "0.0.4",
"amplitude": "^2.0.3",
@ -89,12 +90,12 @@
"superagent": "^1.8.3",
"swagger-node-express": "lefnire/swagger-node-express#habitrpg",
"universal-analytics": "~0.3.2",
"useragent": "2.1.9",
"uuid": "^2.0.1",
"validator": "^4.9.0",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"winston": "^2.1.0",
"useragent": "2.1.9"
"winston": "^2.1.0"
},
"private": true,
"engines": {

View file

@ -1,57 +1,174 @@
import winston from 'winston';
import requireAgain from 'require-again';
import logger from '../../../../../website/server/libs/logger';
import {
NotFound,
} from '../../../../../website/server/libs//errors';
/* eslint-disable global-require */
describe('logger', () => {
let pathToLoggerLib = '../../../../../website/server/libs/logger';
let infoSpy;
let errorSpy;
let logSpy;
beforeEach(() => {
infoSpy = sandbox.stub();
errorSpy = sandbox.stub();
sandbox.stub(winston, 'Logger').returns({
info: infoSpy,
error: errorSpy,
});
logSpy = sandbox.stub(winston.Logger.prototype, 'log');
});
afterEach(() => {
sandbox.restore();
});
it('info', () => {
let attachLogger = requireAgain(pathToLoggerLib);
attachLogger.info(1, 2, 3);
expect(infoSpy).to.be.calledOnce;
expect(infoSpy).to.be.calledWith(1, 2, 3);
describe('info', () => {
it('calls winston\'s info log', () => {
logger.info(1, 2, 3);
expect(logSpy).to.be.calledOnce;
expect(logSpy).to.be.calledWith('info', 1, 2, 3);
});
});
describe('error', () => {
it('with custom arguments', () => {
let attachLogger = requireAgain(pathToLoggerLib);
attachLogger.error(1, 2, 3, 4);
expect(errorSpy).to.be.calledOnce;
expect(errorSpy).to.be.calledWith(1, 2, 3, 4);
context('non-error object', () => {
it('passes through arguments if the first arg is not an error object', () => {
logger.error(1, 2, 3, 4);
expect(logSpy).to.be.calledOnce;
expect(logSpy).to.be.calledWith('error', 1, 2, 3, 4);
});
});
it('with error', () => {
let attachLogger = requireAgain(pathToLoggerLib);
let errInstance = new Error('An error.');
attachLogger.error(errInstance, {
data: 1,
}, 2, 3);
expect(errorSpy).to.be.calledOnce;
// using calledWith doesn't work
let lastCallArgs = errorSpy.lastCall.args;
context('error object', () => {
it('logs the stack and the err data', () => {
let errInstance = new Error('An error.');
logger.error(errInstance, {
data: 1,
}, 2, 3);
expect(lastCallArgs[3]).to.equal(3);
expect(lastCallArgs[2]).to.equal(2);
expect(lastCallArgs[1]).to.eql({
data: 1,
fullError: errInstance,
expect(logSpy).to.be.calledOnce;
expect(logSpy).to.be.calledWith(
'error',
errInstance.stack,
{ data: 1, fullError: errInstance },
2,
3
);
});
it('logs the stack and the err data with it\'s own fullError property', () => {
let errInstance = new Error('An error.');
let anotherError = new Error('another error');
logger.error(errInstance, {
data: 1,
fullError: anotherError,
}, 2, 3);
expect(logSpy).to.be.calledOnce;
expect(logSpy).to.be.calledWith(
'error',
errInstance.stack,
{ data: 1, fullError: anotherError },
2,
3
);
});
it('logs the error when errorData is null', () => {
let errInstance = new Error('An error.');
logger.error(errInstance, null, 2, 3);
expect(logSpy).to.be.calledOnce;
expect(logSpy).to.be.calledWith(
'error',
errInstance.stack,
null,
2,
3
);
});
it('logs the error when errorData is not an object', () => {
let errInstance = new Error('An error.');
logger.error(errInstance, true, 2, 3);
expect(logSpy).to.be.calledOnce;
expect(logSpy).to.be.calledWith(
'error',
errInstance.stack,
true,
2,
3
);
});
it('logs the error when errorData does not include isHandledError property', () => {
let errInstance = new Error('An error.');
logger.error(errInstance, { httpCode: 400 }, 2, 3);
expect(logSpy).to.be.calledOnce;
expect(logSpy).to.be.calledWith(
'error',
errInstance.stack,
{ httpCode: 400, fullError: errInstance },
2,
3
);
});
it('logs the error when errorData includes isHandledError property but is a 500 error', () => {
let errInstance = new Error('An error.');
logger.error(errInstance, {
isHandledError: true,
httpCode: 502,
}, 2, 3);
expect(logSpy).to.be.calledOnce;
expect(logSpy).to.be.calledWith(
'error',
errInstance.stack,
{ httpCode: 502, isHandledError: true, fullError: errInstance },
2,
3
);
});
it('logs a warning when errorData includes isHandledError property and is not a 500 error', () => {
let errInstance = new Error('An error.');
logger.error(errInstance, {
isHandledError: true,
httpCode: 403,
}, 2, 3);
expect(logSpy).to.be.calledOnce;
expect(logSpy).to.be.calledWith(
'warn',
errInstance.stack,
{ httpCode: 403, isHandledError: true, fullError: errInstance },
2,
3
);
});
it('logs additional data from a CustomError', () => {
let errInstance = new NotFound('An error.');
errInstance.customField = 'Some interesting data';
logger.error(errInstance, {}, 2, 3);
expect(logSpy).to.be.calledOnce;
expect(logSpy).to.be.calledWith(
'error',
errInstance.stack,
{
fullError: {
customField: 'Some interesting data',
},
},
2,
3
);
});
expect(lastCallArgs[0]).to.eql(errInstance.stack);
});
});
});

View file

@ -0,0 +1,116 @@
/* eslint-disable camelcase */
import { IncomingWebhook } from '@slack/client';
import requireAgain from 'require-again';
import slack from '../../../../../website/server/libs/slack';
import logger from '../../../../../website/server/libs/logger';
import { TAVERN_ID } from '../../../../../website/server/models/group';
import nconf from 'nconf';
describe('slack', () => {
describe('sendFlagNotification', () => {
let flagger, group, message;
beforeEach(() => {
sandbox.stub(IncomingWebhook.prototype, 'send');
flagger = {
id: 'flagger-id',
profile: {
name: 'flagger',
},
};
group = {
id: 'group-id',
privacy: 'private',
name: 'Some group',
type: 'guild',
};
message = {
id: 'chat-id',
user: 'Author',
uuid: 'author-id',
text: 'some text',
};
});
afterEach(() => {
IncomingWebhook.prototype.send.restore();
});
it('sends a slack webhook', () => {
slack.sendFlagNotification({
flagger,
group,
message,
});
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: 'flagger (flagger-id) flagged a message',
attachments: [{
fallback: 'Flag Message',
color: 'danger',
author_name: 'Author - author-id',
title: 'Flag in Some group - (private guild)',
title_link: undefined,
text: 'some text',
footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message>/),
mrkdwn_in: [
'text',
],
}],
});
});
it('includes a title link if guild is public', () => {
group.privacy = 'public';
slack.sendFlagNotification({
flagger,
group,
message,
});
expect(IncomingWebhook.prototype.send).to.be.calledWithMatch({
attachments: [sandbox.match({
title: 'Flag in Some group',
title_link: sandbox.match(/.*\/#\/options\/groups\/guilds\/group-id/),
})],
});
});
it('links to tavern', () => {
group.privacy = 'public';
group.name = 'Tavern';
group.id = TAVERN_ID;
slack.sendFlagNotification({
flagger,
group,
message,
});
expect(IncomingWebhook.prototype.send).to.be.calledWithMatch({
attachments: [sandbox.match({
title: 'Flag in Tavern',
title_link: sandbox.match(/.*\/#\/options\/groups\/tavern/),
})],
});
});
it('noops if no flagging url is provided', () => {
sandbox.stub(nconf, 'get').withArgs('SLACK:FLAGGING_URL').returns('');
sandbox.stub(logger, 'error');
let reRequiredSlack = requireAgain('../../../../../website/server/libs/slack');
expect(logger.error).to.be.calledOnce;
reRequiredSlack.sendFlagNotification({
flagger,
group,
message,
});
expect(IncomingWebhook.prototype.send).to.not.be.called;
});
});
});

View file

@ -8,6 +8,7 @@ import {
import _ from 'lodash';
import { removeFromArray } from '../../libs/collectionManipulators';
import { getUserInfo, getGroupUrl, sendTxn } from '../../libs/email';
import slack from '../../libs/slack';
import pusher from '../../libs/pusher';
import nconf from 'nconf';
import Bluebird from 'bluebird';
@ -265,6 +266,12 @@ api.flagChat = {
{name: 'GROUP_URL', content: groupUrl},
]);
slack.sendFlagNotification({
flagger: user,
group,
message,
});
res.respond(200, message);
},
};

View file

@ -0,0 +1,58 @@
/* eslint-disable camelcase */
import { IncomingWebhook } from '@slack/client';
import logger from './logger';
import { TAVERN_ID } from '../models/group';
import nconf from 'nconf';
const SLACK_FLAGGING_URL = nconf.get('SLACK:FLAGGING_URL');
const SLACK_FLAGGING_FOOTER_LINK = nconf.get('SLACK:FLAGGING_FOOTER_LINK');
const BASE_URL = nconf.get('BASE_URL');
let flagSlack;
try {
flagSlack = new IncomingWebhook(SLACK_FLAGGING_URL);
} catch (err) {
logger.error(err);
}
function sendFlagNotification ({
flagger,
group,
message,
}) {
if (!SLACK_FLAGGING_URL) {
return;
}
let titleLink;
let title = `Flag in ${group.name}`;
let text = `${flagger.profile.name} (${flagger.id}) flagged a message`;
if (group.id === TAVERN_ID) {
titleLink = `${BASE_URL}/#/options/groups/tavern`;
} else if (group.privacy === 'public') {
titleLink = `${BASE_URL}/#/options/groups/guilds/${group.id}`;
} else {
title += ` - (${group.privacy} ${group.type})`;
}
flagSlack.send({
text,
attachments: [{
fallback: 'Flag Message',
color: 'danger',
author_name: `${message.user} - ${message.uuid}`,
title,
title_link: titleLink,
text: message.text,
footer: `<${SLACK_FLAGGING_FOOTER_LINK}?groupId=${group.id}&chatId=${message.id}|Flag this message>`,
mrkdwn_in: [
'text',
],
}],
});
}
module.exports = {
sendFlagNotification,
};