Skip to content
Draft
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
3af7d47
scrapbox: dummy commit for draft pull request
pizzacat83 Jan 15, 2020
72aec15
scrapbox: implement mute of notifications
pizzacat83 Jan 15, 2020
6b48a20
scrapbox: implement maskAttachment
pizzacat83 Jan 15, 2020
1bf5b2c
scrapbox: type return value of maskAttachment
pizzacat83 Jan 15, 2020
c7dd4dd
scrapbox: implement isMuted
pizzacat83 Jan 15, 2020
8a2141e
scrapbox: don't show images
pizzacat83 Jan 15, 2020
5ca1d05
scrapbox: [WIP] add test for mute
pizzacat83 Jan 15, 2020
b6c6f6b
bcr: [Test Not Passing] mock axios
pizzacat83 Jan 17, 2020
4b93a9a
scrapbox: fix tests
pizzacat83 Jan 19, 2020
e5a0d0e
scrapbox: fix tests
pizzacat83 Jan 19, 2020
1444120
scrapbox: update .env.sample
pizzacat83 Jan 19, 2020
3c092fd
scrapbox: use (decode|encode)URIComponent
pizzacat83 Jan 27, 2020
2528f13
scrapbox: change URL of webhook
pizzacat83 Jan 27, 2020
13895cf
scrapbox: handle errors
pizzacat83 Jan 27, 2020
5ee0278
lib/fastify: add constructor of fastify for unit test
pizzacat83 Feb 22, 2020
fbe0c7e
scrapbox: 1 API request per notification
pizzacat83 Feb 23, 2020
b8d06ef
lib/fastify: add test for fastifyDevConstructor
pizzacat83 Feb 23, 2020
9b6a7fe
lib/scrapbox: add some functions
pizzacat83 Feb 24, 2020
70e3e86
add package eslint-plugin-jest
pizzacat83 Feb 25, 2020
6457b81
Merge remote-tracking branch 'origin/master' into scrapbox-mute-some-…
pizzacat83 Feb 25, 2020
3ac7c6b
lib/scrapbox: add scrapbox wrapper class
pizzacat83 Mar 3, 2020
6943983
lib/scrapbox: add tests for Page & fix bugs
pizzacat83 Mar 3, 2020
db48f9f
lib/scrapbox: add tests for fetchScrapboxUrl
pizzacat83 Mar 3, 2020
1c9178d
lib/scrapbox: add tests for getPageUrlRegExp
pizzacat83 Mar 3, 2020
f3aab4e
scrapbox: use lib/scrapbox
pizzacat83 Mar 3, 2020
c257e5c
welcome: use lib/scrapbox
pizzacat83 Mar 3, 2020
2d5045d
scrapbox: mock lib/scrapbox in tests
pizzacat83 Mar 3, 2020
a4c5590
scrapbox: implement splitAttachments
pizzacat83 Mar 4, 2020
1244809
lib/scrapbox: cache calls of getPageUrlRegExp
pizzacat83 Mar 4, 2020
d3b9f5a
scrapbox: add helper class for tests
pizzacat83 Mar 4, 2020
c83d003
scrapbox: reimplement mute
pizzacat83 Mar 4, 2020
5f6b6ad
lib/scrapbox: fix unintended nullable
pizzacat83 Mar 4, 2020
cbc5f2f
scrapbox: fix eslintrc
pizzacat83 Mar 4, 2020
93aadce
scrapbox: lint
pizzacat83 Mar 4, 2020
8ed67dc
lib/scrapbox: getPageUrlRegExp -> pageUrlRegExp and isPageOfProject
pizzacat83 Mar 4, 2020
6e16349
scrapbox: add doc
pizzacat83 Mar 4, 2020
2cead8b
scrapbox: split files
pizzacat83 Mar 5, 2020
a20913e
Merge remote-tracking branch 'origin/master' into scrapbox-mute-some-…
pizzacat83 Mar 5, 2020
f435b6d
scrapbox/mute: add text: '' in image attachments
pizzacat83 Mar 5, 2020
2e717b0
lib/scrapbox: refactoring
pizzacat83 Mar 9, 2020
90e2ba8
scrapbox/mute: add comments
pizzacat83 Mar 9, 2020
c2977de
scrapbox: delete unnecessary mock
pizzacat83 Mar 9, 2020
17642d4
scrapbox: remove dummy data not used in test
pizzacat83 Mar 9, 2020
2c20353
scrapbox/mute: better variable name
pizzacat83 Mar 9, 2020
c3f17a0
small refactoring
pizzacat83 Mar 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ SWARM_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
CHANNEL_SANDBOX=CXXXXXXXX
CHANNEL_ESOLANG=CXXXXXXXX
CHANNEL_PROCON=CXXXXXXXX
CHANNEL_SCRAPBOX=CXXXXXXXX
CHANNEL_RANDOM=CXXXXXXXX
USER_TSGBOT=UXXXXXXXX
GITHUB_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
GOOGLE_APPLICATION_CREDENTIALS=google_application_credentials.json
FIREBASE_ENDPOINT=https://hakata-shi.firebaseio.com
ACCUWEATHER_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
SCRAPBOX_SID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
SCRAPBOX_PROJECT_NAME=xxxxxxxxx
GITHUB_WEBHOOK_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
SIGNING_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
PORT=21864
Expand Down
8 changes: 7 additions & 1 deletion scrapbox/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
"parser": "@typescript-eslint/parser",
"rules": {
"no-console": "off",
"camelcase": "off"
"camelcase": "off",
"valid-jsdoc": [
"error", {
"requireParamType": false,
"requireReturnType": false
}
]
}
}
104 changes: 95 additions & 9 deletions scrapbox/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
jest.mock('axios');

import scrapbox from './index';
// @ts-ignore
import Slack from '../lib/slackMock.js';
import axios from 'axios';
import qs from 'querystring';
import { escapeRegExp } from 'lodash';
import fastifyConstructor from 'fastify';
import {MessageAttachment} from '@slack/client';


// @ts-ignore
axios.response = {data: {title: 'hoge', descriptions: ['fuga', 'piyo']}};

let slack: Slack = null;

beforeEach(async () => {
slack = new Slack();
process.env.CHANNEL_SANDBOX = slack.fakeChannel;
await scrapbox(slack);
});
const projectName = 'PROJECTNAME';
process.env.SCRAPBOX_PROJECT_NAME = projectName;
import scrapbox from './index';
import {server} from './index';

describe('scrapbox', () => {
beforeEach(async () => {
slack = new Slack();
process.env.CHANNEL_SANDBOX = slack.fakeChannel;
await scrapbox(slack);
});

it('respond to slack hook of scrapbox unfurling', async () => {
const done = new Promise((resolve) => {
// @ts-ignore
axios.mockImplementation(({url, data}: {url: string, data: any}) => {
if (url === 'https://slack.com/api/chat.unfurl') {
const parsed = qs.parse(data);
const unfurls = JSON.parse(Array.isArray(parsed.unfurls) ? parsed.unfurls[0] : parsed.unfurls);
expect(unfurls['https://scrapbox.io/tsg/hoge']).toBeTruthy();
expect(unfurls['https://scrapbox.io/tsg/hoge'].text).toBe('fuga\npiyo');
expect(unfurls[`https://scrapbox.io/${projectName}/hoge`]).toBeTruthy();
expect(unfurls[`https://scrapbox.io/${projectName}/hoge`].text).toBe('fuga\npiyo');
resolve();
return Promise.resolve({data: {ok: true}});
}
Expand All @@ -44,11 +52,89 @@ describe('scrapbox', () => {
links: [
{
domain: 'scrapbox.io',
url: 'https://scrapbox.io/tsg/hoge',
url: `https://scrapbox.io/${projectName}/hoge`,
},
],
});

return done;
});
});

describe('scrapbox', () => {
it('mutes pages with ##ミュート tag', async () => {
const fakeChannel = 'CSCRAPBOX';
process.env.CHANNEL_SCRAPBOX = fakeChannel;
const fastify = fastifyConstructor();
// eslint-disable-next-line array-plural/array-plural
const attachments_req: (MessageAttachment & any)[] = [
{
title: 'page 1',
title_link: `https://scrapbox.io/${projectName}/page_1#c632c886dc3061e3b85cabbd`,
text: 'hoge',
rawText: 'hoge',
mrkdwn_in: ['text'],
author_name: 'Alice',
image_url: 'https://example.com/hoge1.png',
thumb_url: 'https://example.com/fuga1.png',
},
{
title: 'page 2',
title_link: `https://scrapbox.io/${projectName}/page_2#aaf8924806eb538413c07c43`,
text: 'hoge',
rawText: 'hoge',
mrkdwn_in: ['text'],
author_name: 'Bob',
image_url: 'https://example.com/hoge2.png',
thumb_url: 'https://example.com/fuga2.png',
},
];
// @ts-ignore
axios.get.mockImplementation((url: string) => {
if (url.match(new RegExp(`${escapeRegExp(`https://scrapbox.io/api/pages/${projectName}/page_1`)}(?:#.*)?`))) {
return {data: {title: 'page 1', links: ['page 3', '##ミュート']}};
} else if (url.match(new RegExp(`${escapeRegExp(`https://scrapbox.io/api/pages/${projectName}/page_2`)}(?:#.*)?`))) {
return {data: {title: 'page 2', links: ['page 4']}};
}
throw Error('axios-mock: unexpected URL');
});

slack = {chat: {
postMessage: jest.fn(),
}};
fastify.register(server({webClient: slack} as any));
const args = {
text: `New lines on <https://scrapbox.io/${projectName}|${projectName}>`,
mrkdwn: true,
username: 'Scrapbox',
attachments: attachments_req,
};
const {payload, statusCode} = await fastify.inject({
method: 'POST',
url: '/scrapbox',
payload: args,
});
if (statusCode !== 200) {
throw JSON.parse(payload);
}
expect(slack.chat.postMessage.mock.calls.length).toBe(1);
const {channel, text, attachments: attachments_res}: {channel: string; text: string; attachments: MessageAttachment[]} = slack.chat.postMessage.mock.calls[0][0];
expect(channel).toBe(fakeChannel);
expect(text).toBe(args.text);
const unchanged = ['title', 'title_link', 'mrkdwn_in', 'author_name'] as const;
for (const i of [0, 1]) {
for (const key of unchanged) {
expect(attachments_res[i][key]).toEqual(attachments_req[i][key]);
}
}
const nulled = ['image_url', 'thumb_url'] as const;
for (const key of nulled) {
expect(attachments_res[0][key]).toBeNull();
// eslint-disable-next-line array-plural/array-plural
expect(attachments_res[1][key]).toEqual(attachments_req[1][key]);
}
expect(attachments_res[0].text).toContain('ミュート');
// eslint-disable-next-line array-plural/array-plural
expect(attachments_res[1].text).toBe(attachments_req[1].text);
});
});
93 changes: 82 additions & 11 deletions scrapbox/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import axios from 'axios';
// @ts-ignore
import logger from '../lib/logger.js';
import {LinkUnfurls} from '@slack/client';
import qs from 'querystring';
import plugin from 'fastify-plugin';
import { escapeRegExp } from 'lodash';
import {WebClient, RTMClient, LinkUnfurls, MessageAttachment} from '@slack/client';

const getScrapboxUrl = (pageName: string) => `https://scrapbox.io/api/pages/tsg/${pageName}`;
const projectName = process.env.SCRAPBOX_PROJECT_NAME;
const scrapboxUrlRegexp = new RegExp(`^https?${escapeRegExp(`://scrapbox.io/${projectName}/`)}(?<pageTitle>.+)$`);
const getScrapboxUrl = (pageName: string) => `https://scrapbox.io/api/pages/${projectName}/${pageName}`;
const getScrapboxUrlFromPageUrl = (url: string): string => {
let pageName = url.replace(scrapboxUrlRegexp, '$<pageTitle>');
try {
if (decodeURI(pageName) === pageName) {
pageName = encodeURI(pageName);
}
} catch {}
return getScrapboxUrl(pageName);
};

import {WebClient, RTMClient} from '@slack/client';

interface SlackInterface {
rtmClient: RTMClient,
Expand All @@ -22,16 +34,10 @@ export default async ({rtmClient: rtm, webClient: slack, eventClient: event}: Sl
const unfurls: LinkUnfurls = {};
for (const link of links) {
const {url} = link;
if (!(/^https?:\/\/scrapbox.io\/tsg\/.+/).test(url)) {
if (!scrapboxUrlRegexp.test(url)) {
continue;
}
let pageName = url.replace(/^https?:\/\/scrapbox.io\/tsg\/(.+)$/, '$1');
try {
if (decodeURI(pageName) === pageName) {
pageName = encodeURI(pageName);
}
} catch {}
const scrapboxUrl = getScrapboxUrl(pageName);
const scrapboxUrl = getScrapboxUrlFromPageUrl(url);
const response = await axios.get(scrapboxUrl, {headers: {Cookie: `connect.sid=${process.env.SCRAPBOX_SID}`}});
const {data} = response;

Expand Down Expand Up @@ -73,3 +79,68 @@ export default async ({rtmClient: rtm, webClient: slack, eventClient: event}: Sl
}
});
};


interface SlackIncomingWebhookRequest {
text: string;
mrkdwn?: boolean;
username?: string;
attachments: MessageAttachment[];
}

/**
* ミュートしたいattachmentに対し,隠したい情報を消して返す
*
* @param attachment ミュートするattachment
* @return ミュート済みのattachment
*/
const maskAttachment = (attachment: MessageAttachment): MessageAttachment => {
const dummyText = 'この記事の更新通知はミュートされています。';
return {
...attachment,
text: dummyText,
fallback: dummyText,
image_url: null,
thumb_url: null,
};
};

/**
* 指定したURLの記事がミュート対象かどうかを判定する
*
* @param url Scrapbox記事のURL
* @return ミュート対象ならtrue, 対象外ならfalse
*/
const isMuted = async (url: string): Promise<boolean> => {
if (!scrapboxUrlRegexp.test(url)) {
// this url is not a scrapbox page
return false;
}
const muteTag = '##ミュート';
const infoUrl = getScrapboxUrlFromPageUrl(url);
const pageInfo = await axios.get(infoUrl, {headers: {Cookie: `connect.sid=${process.env.SCRAPBOX_SID}`}});
return pageInfo.data.links.indexOf(muteTag) !== -1; // if found, the page is muted
};

/**
* Scrapboxからの更新通知 (Incoming Webhook形式) を受け取り,ミュート処理をしてSlackに投稿する
*/
// eslint-disable-next-line node/no-unsupported-features, node/no-unsupported-features/es-syntax
export const server = ({webClient: slack}: SlackInterface) => plugin((fastify, opts, next) => {
fastify.post<unknown, unknown, unknown, SlackIncomingWebhookRequest>('/scrapbox', async (req) => {
await slack.chat.postMessage(
{
channel: process.env.CHANNEL_SCRAPBOX,
icon_emoji: ':scrapbox:',
...req.body,
attachments: await Promise.all(req.body.attachments.map(
async (attachment) => await isMuted(attachment.title_link) ? maskAttachment(attachment) : attachment
)),
}
);
return '';
});

next();
});