こんにちは、ソフトコムでBカート制作を担当しています、山田です。
僕は大学卒業後、お金を貯めて大学院に行ったのですが、その間ソフトコムではずっとバイトさせてもらっていて、今年度から正社員になりました。

多重ログインを管理したい

複数人で一つのシステムを使うときに問題になるのが、多重ログインです。
多重ログインが許可されない場合、先勝ち・後勝ちの2パターンがあります。
現実世界では先勝ちのシステムが多いです。試着室なんかは後から勝手に入ることができません。
しかしコンピュータシステムは現実と違い、ログアウトを忘れて他の人が使えなくなってしまうことがあります。
現実では試着室に入りっぱなしということはほぼあり得ないのですが、ログイン程度だとあり得るのですね。
そこで後から入ってきた人に先に入っていた人が譲るという風に実装します。これが後勝ちです。
たとえば弊社ではリモートデスクトップで接続するPCが2台あり、そこに共有のアプリなどが入っています。
リモートデスクトップは誰かがログインしている途中で別の人がログインしようとすると、前の人は接続が切れて追い出されるという、後勝ちの仕様です。
これを譲り合って使うための管理システムをSlackとFirebase Realtime Databaseで実装しました。

解決したい問題

弊社では、Skypeでリモートデスクトップを使うこと・閉じたことを宣言して、ログインの競合を避けていました。
このやり方には問題があり、ひとつはチャットを辿らないと誰が使っているのか分かりにくいこと、もうひとつは使う・閉じるのチャットを送り忘れると機能しないことです。
この問題を解決するために、Firebase Realtime Databaseでリモートデスクトップの状態を管理し、Slackからスラッシュコマンドを送るというシステムにしました。
リモートデスクトップのほかにも、同様の仕様の共有ログインアプリがあり、そちらにも応用できました。

できたもの

slackからスラッシュコマンドで「/PCNAME」(PCNAMEは弊社リモートデスクトップの名前)と送ると、対応したリモートデスクトップの使う・閉じるが登録されます。

もし誰かが使っていれば、それをこっそり教えてくれます。

また、専用ページにアクセスすれば各リモートデスクトップの使用状況が一覧で分かります。

実装

イメージ

  1. Firebase Realtime Databaseに各PCの「使用状態」「使用ユーザー」「使用開始時刻」を登録しておく。
  2. Slackのスラッシュコマンドで「ユーザーID」「使用PC」をPOST。
  3. Firebase functionsでデータベースとHTTPリクエストの内容から、データベースを更新。対応するメッセージを返す。
  4. 使用時間が30分を超えたらアラートのメッセージを送る。

コード

社内環境に関する部分があるので、かいつまんで載せます。
まずスラッシュコマンドでどのリモートデスクトップ宛てか振り分けたいので、slackAPIから送られてきたパラメータを取得します。

const slackCommand = req.body.command;
const slackUser = req.body.user_id;

予めRealtime Databaseにこんな感じのデータベースを用意しておき、

{         
    state: false,  
    user: "",      
    userName: "",       
    timestamp: ""     
}

アプリ側で呼び出して、

const db = admin.database();
const dbref = db.ref("DBNAME");

現在のデータベースの状況を確認します。

await ref.once("value", function (data) {
state = data.val().state;
pcUser = data.val().user;
pcUserName = data.val().userName;
});
  • slackから送られてきたユーザーが使用中ユーザーと同じなら、閉じる。
  • 状態が使用中だがslackから送られてきたユーザーと使用中ユーザーが違えば、使用中ユーザーを返す。
  • 状態が使用中でなければslackから送られてきたユーザーが使う。

という分岐でデータベースに情報を書き込んだりテキストを返したりします。

if (slackUser == pcUser) {
    await res.json({
        "text": `${userName}さんは${type}を閉じました`,
        "response_type": "in_channel",
    });
    await ref.update({
        state: false,
        user: "",
        userName: "",
        timestamp: ""
    });

} else if (state) {
    await res.json({
        "text": `${type}は${pcUserName}さんが使用中です`,
        "response_type": "ephemeral",
    });
} else {
    await res.json({
        "text": `${userName}さんが${type}を使います`,
        "response_type": "in_channel",
    });
    const timestamp = await Date.now();
    await ref.update({
        state: true,
        user: slackUser,
        userName: userName,
        timestamp: timestamp
    });

}

そして誰かが使い始めてから30分経ったら閉じ忘れの可能性があるので、アラートを送ります。
これはFunctionsの定期実行機能を使いました。

exports.scheduledFunction = functions.pubsub.schedule('every 30 minutes').onRun((context) => {
    const currenttime = Date.now();
    const date = new Date(currenttime);
    const hours = date.getHours();
    if (9 < hours < 20) {
        fetch("SlackURL", {
            method: "POST",
            mode: "cors",
            cache: "no-cache"
        })
    }
});

さらに拡張

SlackのスラッシュコマンドはSlackコネクトを使って参加しているワークスペース外の人は使えない仕様です。
そのため、アプリへのメンション・アプリのDMへのメッセージでも動作する仕組みに拡張しました。
スラッシュコマンドはSlackAPIの管理画面のSlash Commandから設定できますが、メンションとDMに対応するにはEvent SubscriptionのApp_mentionとmessage.imを使います。
それぞれのイベントに別のエンドポイントを設定します。
Event SubscriptionはPOST先のURLをあらかじめ認証しておかなくてはいけないので、そこで苦戦しました。
認証の際、登録したURLにchallengeというパラメータの付いたHTTPリクエストが飛ぶので、challengeのvalueを返します。

if (req.body.type == "url_verification") {
    res.send(req.body.challenge);
}

スラッシュコマンドの場合とは送られてくるデータの構造が違うので、いい感じに合わせます。

const slackText = req.body.event.text;
const channel = req.body.event.channel;
const slackUser = req.body.event.user;

こちらが返すデータもスラッシュコマンドの時と異なります。
スラッシュコマンドではjsonペイロードのtextの値がチャットに出力されますが、こちらの場合メッセージを送る必要があります。

function postText(post, postChannel) {
    const option = {
        url: 'https://slack.com/api/chat.postMessage',
        body: JSON.stringify({
            "channel": postChannel,
            "text": post
        }),
        headers: {
            "Content-type": "application/json",
            "Authorization": "TOKEN",
            "X-Slack-No-Retry": "1"
        }
    }
    request.post(option, function (error, response, body) {});
};

どうなったか

リモートデスクトップのバッティングや、「まだ使っていますか?」という確認は減ったように思います。
また当時はSlackのスラッシュコマンドについて認知度が低かったので、Slackの機能を紹介する意図もありました。
たまたま当時はSlackを共通のチャットツールとして使っていたのでこのような編成にしましたが、仕組み自体は他のアプリでも流用できるはずです。
実際に最初はFirebase Realtime DatabaseではなくGoogle Spread Sheetを使っていました。
安定性と高速化のために後から書き換えた経緯があります。
シンプルな仕組みで幅広く応用できると思いますので、皆さんもぜひお試しください!