Help us understand the problem. What is going on with this article?

サイボウズOfficeのワークフローをPuppeteerでSlackに通知してみた

はじめに

この記事は 株式会社ピーアールオー(あったらいいな!を作ります) Advent Calendar 2019 の9日目の記事です。
今回は最近困っていることを解決してみようと思います。

サイボウズOfficeのワークフローにすぐ気づけない...

サイボウズOfficeでワークフローなどの申請が来た場合、すぐに気づける方法はいくつかあります。

  • ブラウザ開いてのホーム画面で確認
  • パソコン用リマインダーツールを使う(Cybozu Desktop)
  • メールを確認する

が、私は基本vscodeとslackばかり見ているので、すぐに気づかずSlackで突っ込まれることが多々あります...
なので、直ぐに気づくことができるよう、ワークフローの申請があった際にSlackに通知するツールを作ってみようと思います。

どうやって作る?

サイボウズOfficeのAPIを叩いてワークフローの状況を定期的に参照してSlackに通知しよう!
と、思ったのですが、私が調べた限りではサイボウズOfficeはAPI使えないみたいです。
(kintonegaroonはAPI使えるようです。)

なので、ヘッドレスでブラウザを操作できるPuppeteerを使って、ワークフローの状況を参照、slackに通知していきたいと思います。

処理の流れ

  1. dockerでPuppeteer用の環境構築 & 監視用スクリプト稼働
  2. サイボウズOfficeへアクセスして、ワークフローの状況を取得
  3. docker(Puppeteer)から、SlackのIncoming Webhookにリクエストを送信
  4. Incoming Webhook がSlackへ通知

dockerでPuppeteer環境を作る

dockerで開発環境を作っていきます。
まずは、dockerfile。Puppeteer Troubleshootingのdockerのコピペです。

FROM node:10-slim

# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
# installs, work.
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update \
    && apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf \
    --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*

# If running Docker >= 1.13.0 use docker run's --init arg to reap zombie processes, otherwise
# uncomment the following lines to have `dumb-init` as PID 1
# ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init
# RUN chmod +x /usr/local/bin/dumb-init
# ENTRYPOINT ["dumb-init", "--"]

# Uncomment to skip the chromium download when installing puppeteer. If you do,
# you'll need to launch puppeteer with:
#     browser.launch({executablePath: 'google-chrome-unstable'})
# ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true

# Install puppeteer so it's available in the container.
RUN npm i puppeteer \
    # Add user so we don't need --no-sandbox.
    # same layer as npm install to keep re-chowned files from using up several hundred MBs more space
    && groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
    && mkdir -p /home/pptruser/Downloads \
    && chown -R pptruser:pptruser /home/pptruser \
    && chown -R pptruser:pptruser /node_modules

# Run everything after as non-privileged user.
USER pptruser

# CMD ["google-chrome-unstable"]”

次はdocker-compose。
先ほどのdockerfileでコンテナを参照。
ワークフロー監視用のjsを共有後、homeディレクトリにコピーして実行するようにしています。
(共有ディレクトリだと上手く動かなかったので。)

参照先のサイボウズの設定なども環境変数に持たせて変更できるようにしました。
参照先はサイボウズOfficeデモサイト
監視間隔5秒で設定しています。

デモサイトなので、パスワードなし。
ログインIDはchromeデベロッパーツールでログインユーザのセレクトボックス(option値)を参照して記載しています。
Basic認証の設定も入れました。処理上は作成しますが、今回はデモサイトで認証がないので適当な値を入れます。

version: '3.3'
services:
  puppeteer:
    build:
      context: ./
      dockerfile: Dockerfile
    volumes:
      - ./checkWorkflow.js:/opt/checkWorkflow.js
    environment:
      - CYBOZU_URL=https://onlinedemo.cybozu.info/scripts/office10/ag.cgi?
      - IS_BASIC=0
      - BASIC_ID=xxxx
      - BASIC_PW=xxxx
      - LOGIN_ID=17
      - LOGIN_PW=
      - SLACK_CHANNEL=@xxxxxxx
      - SLACK_ENTRY_POINT=/services/xxxxx/xxxxx/xxxxxxxxxxxxxxxxx
      - LOOP_TIME=5000
    command: sh -c "cp /opt/checkWorkflow.js /home/pptruser/. && node /home/pptruser/checkWorkflow.js"
    tty: true

環境変数一覧

環境変数 内容
CYBOZU_URL サイボウズのURL
IS_BASIC Basic認証有無(0:なし、1:あり)
BASIC_ID Basic認証 ID
BASIC_PW Basic認証 Password
LOGIN_ID サイボウズログインID
LOGIN_PW サイボウズログインPW
SLACK_CHANNEL 検知時送信先 Slackチャンネル
SLACK_ENTRY_POINT 検知時送信先 Slack送信先URL(パス)
LOOP_TIME 監視間隔

開発自体はcommandをコメントアウトして、VSリモートで作成したコンテナにアクセスして開発しました。

Puppeteerで申請状況を取得する

ログイン

docker-composeで設定した環境変数を利用してBasic認証とログインを行います。
「page.select」と「page.type」でログインフォームにログインユーザとパスワードを設定。
「document.querySelector('input[name="Submit"]').click()」でログインボタンを押します。

  const browser = await puppeteer.launch({ args: ['--no-sandbox'] });
  const page = await browser.newPage();

  // Basic認証
  if (IS_BASIC === '1')
    await page.authenticate({ username: BASIC_ID, password: BASIC_PW });

  // サイボウズアクセス
  await page.goto(CYBOZU_URL, {
    waitUntil: 'domcontentloaded'
  });

  // ログイン設定
  await page.select('select[name="_ID"]', LOGIN_ID);
  await page.type('input[name="Password"]', LOGIN_PW);

  // ログイン
  await page.evaluate(() => {
    document.querySelector('input[name="Submit"]').click();
  });

  // ログイン後画面が読み込まれるまで待機
  await page.waitForNavigation({
    timeout: 30000,
    waitUntil: 'domcontentloaded'
  });

ワークフロー取得

ワークフロー画面遷移後、受信一覧を参照します。
最新の申請の番号を取得し、チェック済み配列に格納します。
次回以降は申請済配列を参照し、新規番号がある場合は後続の処理(Slack通知)に繋げるようにしています。

 // ワークフロー画面へ
  await page.goto(CYBOZU_URL + 'page=WorkFlowRecept');

  // 受信一覧取得
  const list = await page.$$('table.dataList > tbody  > tr');

  // 申請があるか判定判定
  if (list.length <= 1) {
    await browser.close();
    console.log("nothing ")
    return;
  }

  // 最新の申請取得
  const td = await list[1].$('td')
  // 申請番号取得
  const newAppNumber = (await (await td.getProperty('textContent')).jsonValue()).replace(/\r?\n/g, '');

  // ブラウザ終了
  await browser.close();

  // チェック済みの番号か判定
  if (appNumberList.indexOf(newAppNumber) >= 0) {
    console.log("checked number : " + newAppNumber);
    return;
  } else {
    appNumberList.push(newAppNumber);
    console.log("new number : " + newAppNumber);
  }

Slackに通知する

SlackのIncoming Webhookを利用して通知します。
Incoming Webhooksについては、以下など、説明されている記事が沢山あると思いますので、割愛します。
参考: SlackのIncoming Webhooksを使い倒す

環境変数に設定したSlackのチャンネル、URLを設定してPOSTします。

 let postData = {
    channel: SLACK_CHANNEL,
    username: 'work-flow-checker',
    text: '新規申請があります!',
    icon_emoji: ':ghost:'
  };
  let postDataStr = JSON.stringify(postData);

  let options = {
    host: 'hooks.slack.com',
    // port: 80,
    path: SLACK_ENTRY_POINT,
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': Buffer.byteLength(postDataStr)
    }
  };

  let req = http.request(options, res => {
    console.log('STATUS: ' + res.statusCode);
    console.log('HEADERS: ' + JSON.stringify(res.headers));
    res.setEncoding('utf8');
    res.on('data', chunk => {
      console.log('BODY: ' + chunk);
    });
  });
  req.on('error', e => {
    console.log('problem with request: ' + e.message);
  });
  req.write(postDataStr);
  req.end();

完成

ソースが完成しました。
環境変数を変数に設定後、監視用のfunctionをsetIntervalで定期的に稼働させるようにしています。
チェック済申請番号はメモリ上に保持するので、起動毎にチェック済みの申請番号が消えますが、起動しっぱなしにするので気にしません。

ファイル全量
.
├── Dockerfile
├── checkWorkflow.js
└── docker-compose.yml
checkWorkflow.js
const puppeteer = require('puppeteer');
const http = require('https');

// 設定
const CYBOZU_URL = process.env.CYBOZU_URL
const IS_BASIC = process.env.IS_BASIC
const BASIC_ID = process.env.BASIC_ID
const BASIC_PW = process.env.BASIC_PW
const LOGIN_ID = process.env.LOGIN_ID
const LOGIN_PW = process.env.LOGIN_PW
const SLACK_CHANNEL = process.env.SLACK_CHANNEL
const SLACK_ENTRY_POINT = process.env.SLACK_ENTRY_POINT
const LOOP_TIME = process.env.LOOP_TIME // ミリ秒

// チェック済申請番号格納
var appNumberList = [];

async function checkWorkflow() {
  const browser = await puppeteer.launch({ args: ['--no-sandbox'] });
  const page = await browser.newPage();

  // Basic認証
  if (IS_BASIC === '1')
    await page.authenticate({ username: BASIC_ID, password: BASIC_PW });

  // サイボウズアクセス
  await page.goto(CYBOZU_URL, {
    waitUntil: 'domcontentloaded'
  });

  // ログイン設定
  await page.select('select[name="_ID"]', LOGIN_ID);
  await page.type('input[name="Password"]', LOGIN_PW);

  // ログイン
  await page.evaluate(() => {
    document.querySelector('input[name="Submit"]').click();
  });

  // ログイン後画面が読み込まれるまで待機
  await page.waitForNavigation({
    timeout: 30000,
    waitUntil: 'domcontentloaded'
  });

  // ワークフロー画面へ
  await page.goto(CYBOZU_URL + 'page=WorkFlowRecept');

  // 受信一覧取得
  const list = await page.$$('table.dataList > tbody  > tr');

  // 申請があるか判定判定
  if (list.length <= 1) {
    await browser.close();
    console.log("nothing ")
    return;
  }

  // 最新の申請取得
  const td = await list[1].$('td')
  // 申請番号取得
  const newAppNumber = (await (await td.getProperty('textContent')).jsonValue()).replace(/\r?\n/g, '');

  // ブラウザ終了
  await browser.close();

  // チェック済みの番号か判定
  if (appNumberList.indexOf(newAppNumber) >= 0) {
    console.log("checked number : " + newAppNumber);
    return;
  } else {
    appNumberList.push(newAppNumber);
    console.log("new number : " + newAppNumber);
  }

  // SlackへのPOST用データ作成
  let postData = {
    channel: SLACK_CHANNEL,
    username: 'work-flow-checker',
    text: '新規申請があります!',
    icon_emoji: ':ghost:'
  };
  let postDataStr = JSON.stringify(postData);

  // POST設定
  let options = {
    host: 'hooks.slack.com',
    // port: 80,
    path: SLACK_ENTRY_POINT,
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': Buffer.byteLength(postDataStr)
    }
  };

  // POST
  let req = http.request(options, res => {
    console.log('STATUS: ' + res.statusCode);
    console.log('HEADERS: ' + JSON.stringify(res.headers));
    res.setEncoding('utf8');
    res.on('data', chunk => {
      console.log('BODY: ' + chunk);
    });
  });
  req.on('error', e => {
    console.log('problem with request: ' + e.message);
  });
  req.write(postDataStr);
  req.end();
};

setInterval(function () {
  checkWorkflow();
}, LOOP_TIME);

動かしてみる

それでは動かしていきます!

デモサイトの申請状況

ワークフロー(受信一覧)_-_サイボウズ_Office.png

docker起動

 docker-compose up
Starting cybozu-puppeteer_puppeteer_1 ... done
Attaching to cybozu-puppeteer_puppeteer_1
puppeteer_1  | new number : 11 
puppeteer_1  | STATUS: 200
puppeteer_1  | HEADERS: {"content-type":"text/html","transfer-encoding":"chunked","connection":"close","date":"Sun, 08 Dec 2019 21:07:31 GMT","server":"Apache","vary":"Accept-Encoding","strict-transport-security":"max-age=31536000; includeSubDomains; preload","referrer-policy":"no-referrer","x-frame-options":"SAMEORIGIN","access-control-allow-origin":"*","x-via":"haproxy-www-ow8r","x-cache":"Miss from cloudfront","via":"1.1 89e14ce757792ac369341dc84fa01d52.cloudfront.net (CloudFront)","x-amz-cf-pop":"NRT57-C2","x-amz-cf-id":"p0yzt6kUDK3Nk1tuxr7ojy1tWrIgTG8j1F8m2Euu0pIPnxj9kV-gug=="}
puppeteer_1  | BODY: ok
puppeteer_1  | checked number : 11 

new numberとして、11番の申請が検知されました。
Slackの方にも通知がきました。

Slack___Slackbot___PRO-D-Sol_と_「サイボウズOfficeのワークフローをPuppeteerでSlackに通知してみた」を編集_-_Qiita.png

デモサイトで追加申請をあげてみます。
新規申請が追加されることで、再度Slackに通知が来るはずです。

申請追加
ワークフロー(受信一覧)_-_サイボウズ_Office.png

Slackを確認。追加で申請がきました!

Slack___Slackbot___PRO-D-Sol_と_「サイボウズOfficeのワークフローをPuppeteerでSlackに通知してみた」を編集_-_Qiita.png

感想

普段サイボウズの通知見逃しが多いので作ってみました。
エラーハンドリングなどあまり考慮せずバッと作ったので、エラー沢山あるかもしれません...
実際に使ってみて、少しずつ直して行く予定です。これで、申請漏れなどなくなることを祈ってます!

今回作成したソース

よろしければ使ってみてください。
https://github.com/hiro-kane/cybozu-check-tool

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした