Slack + Electron + Socket.ioを使ってニコニコ動画風にメッセージを表示する

![nicotv.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272803/38da7b97-4cdf-0daf-43fd-b0883e25c27e.gif)

## はじめに
Slack APIを使ってみたく、ただメッセージを表示するだけではつまらないので、
ニコニコ動画風にメッセージを表示するアプリケーションを作成してみました。

## 構成
* Slack：Slack Events APIを使って、メッセージ受信時にサーバーに通知
* サーバー(EC2)：Events APIを受けっとって、Electronアプリにメッセージを送信
* Electron：Socket.ioでサーバーからメッセージを受け取りニコニコ動画風に描画
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272803/ba4a7202-5d3d-35d8-35ee-169dfa5c7083.png)

## 必要なもの
* PC
* サーバー（EC2）
* Slackのアカウント

## 参考にしたサイト
* [Electronでデスクトップいっぱいに雪を降らせるアプリを作る](https://qiita.com/nyamogera/items/a2e367b44e78d04ee3a6)
* [AWS初心者でもわかる！ ブラウザ上で完結！ AWS+Slack Event APIを使ったSlackボット超入門](https://qiita.com/mido_app/items/fcbbdb2bcce3edf0d3f5)
* [Node.js + Express + Socket.ioで簡易チャットを作ってみる](https://qiita.com/riku-shiru/items/ffba3448f3aff152b6c1)

## 作り方
### サーバー（EC2）
Node.jsのExpressを使ってWebサーバーを立ち上げる。
Electronアプリとの接続用にSocket.io通信も設定する。
ここではポート3000番に起動しているので、必要に応じて変更する。

```{javascript:app.js}
const express = require('express');
const app = express();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const PORT = process.env.PORT || 3000;

const bodyParser = require('body-parser');

app.use(bodyParser.json());

io.on('connection', function (socket) {
    socket.on('message', function (msg) {
        io.emit('message', msg);
    });
});

app.post('/slack', function (req, res) {

    const { type, event } = req.body;

    if (type === 'challenge') {

    } else if (type === 'event_callback') {
        io.emit('message', event.txt);
    }

    res.status(200).json(req.body);

});

http.listen(PORT, function () {
    console.log('server listening. Port:' + PORT);
});
```

```{bash}
npm i -S http express socket.io body-parser
node app.js & #バックグラウンドで実行
```

### Slackの設定
* https://api.slack.com/apps
* Create New Apps
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272803/75bb2fbe-9c78-4320-c8f9-9400e0c424c6.png)
* App Name:任意（Nicotv)
* Development Slack Workspace:ワークスペースを指定
* [Create App]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272803/c5e6feb1-1dde-956d-a3d0-506466385712.png)
* 左下の[Event Subscriptions]から
* Request URLに先ほど設定したサーバーのURLを設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272803/eb1123a4-cccf-871c-73a0-ac113d05b719.png)
* Bot Eventsを下記のように設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272803/ca187fbc-cf30-c02c-76b5-9e59b6b12d6d.png)
* 右下の[Save Changes]
* [Install App]から手順に従いSlackにアプリをインストール

### Electronアプリの作成
超シンプルなHTMLファイル

```{html:index.html}
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Nico TV</title>
    <script src="https://xxx.xxx.com/socket.io/socket.io.js"></script>
    <script>
        window.jQuery = window.$ = require('./lib/jquery-3.3.1.min.js');
    </script>
    <link href="./css/style.css" rel="stylesheet">
</head>

<body>

    <div class="msgbox"> </div>

    <script>
        require('./renderer.js')
    </script>

</body>

</html>
```

Socket.ioに接続し、メッセージを受け取ったらニコニコ風に描画

```{javascript:renderer.js}
const colors = [
    'lime',
    'aqua',
    'yellow',
    'red',
    'fuchsia'
];

const colorsLen = colors.length;

const layerCnt = 6;

const winWidth = window.innerWidth;
const socketio = io('https://xxx.xxx.com');

const layerIds = [];

const $msgbox = $('.msgbox');

for (let i = 0; i < layerCnt; i++) {
    $('<div>', { class: 'layer' }).appendTo($msgbox);
    layerIds.push(0);
}

const $layers = $('.layer');

let msgId_ = 1;

socketio.on('message', function (msg) {

    let msgId = msgId_++;

    let minId = msgId;
    for (let layerId of layerIds) {
        if (layerId < minId) {
            minId = layerId;
        }
    }

    let index = layerIds.findIndex((id) => id === minId);

    layerIds[index] = msgId;

    const $layer = $layers.eq(index);
    const $msg = $('<div>', { class: 'msg', text: msg })
        .css('color', colors[Math.floor(Math.random() * colorsLen)])
        .appendTo($layer);

    let right = -$msg.width();

    let intervalId = setInterval(function () {

        right += 1;
        $msg.show().css('right', right);

        if (right > winWidth) {

            $msg.remove();

            if (layerIds[index] === msgId) {
                layerIds[index] = 0;
            }

            clearInterval(intervalId);

        }

    });

});
```

```{css:style.css}
html {
    height: 100%;
    width: 100%;
}

body {
    margin: 0;
    height: 100%;
    width: 100%;
    background-color: rgba(0, 0, 0, 0);
    overflow: hidden;
}

.msgbox {
    font-size: 128px;
}

.layer {
    position: relative;
    width: 100%;
    height: 128px;
    margin: 8px 0;
}

.msg {
    position: absolute;
    display: none;
    right: 0;
    white-space: nowrap;
    font-weight: 700;
}

```

Electronアプリを最大化、フレームなし、透明化、リサイズ不可、常に最前面で起動する。

```{javascript:main.js}
// Modules to control application life and create native browser window
const electron = require('electron');
const { app, BrowserWindow } = electron;

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;

function createWindow() {

    // const screen = require('screen');
    const { screen } = electron;
    let size = screen.getPrimaryDisplay().size;

    // Create the browser window.
    mainWindow = new BrowserWindow({
        left: 0,
        top: 0,
        width: size.width,
        height: size.height,
        frame: false,
        show: true,
        transparent: true,
        resizable: false,
        alwaysOnTop: true
    });

    mainWindow.setIgnoreMouseEvents(true);
    mainWindow.maximize();

    // and load the index.html of the app.
    mainWindow.loadFile('index.html');

    // Open the DevTools.
    // mainWindow.webContents.openDevTools()

    // Emitted when the window is closed.
    mainWindow.on('closed', function () {
        // Dereference the window object, usually you would store windows
        // in an array if your app supports multi windows, this is the time
        // when you should delete the corresponding element.
        mainWindow = null;
    });

}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', function () {
    // On OS X it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

app.on('activate', function () {
    // On OS X it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (mainWindow === null) {
        createWindow();
    }
});

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

```

electron-packagerを使ってExeファイルを作成する。

```{bash}
electron-packager . nicotv --platform=win32 --arch=x64 --asar
```

## 使い方
* メッセージを表示したいPCでElectronアプリを起動
* Slackで<b>Nicotv宛てにダイレクトメッセージ</b>を送ると、Electronアプリ上でメッセージが表示される


## さいごに
Slack連携のアプリケーションが意外に簡単に作れました。
SlackのBot Eventsを変更することで、チャネルの投稿をトリガーにメッセージを表示するなどということも可能です。
Electronアプリは最前面、背景透明で起動しているので、他のアプリケーションと合わせて実行することが可能です。
弊社ではこのアプリを全員が見えるところに配置した巨大なモニタに映し出して使用しています。