社内slackにVIPチャンネルを作った話

[ABEJA Advent Calendar](https://qiita.com/advent-calendar/2019/abeja)の1日目です。

# はじめに

昨年はABEJA Platformに関するAdvent Calendarでしたが、今年はプラットフォームに限らず幅広い技術を扱おう、ということで縛りを作らずに様々な技術を紹介していきます。

さて、皆さん、社内でのコミュニケーションツールは何をお使いでしょうか。色々なツールがあると思いますが、Slackを使っている所が多いのではないかと思います。Slackはとても良いツールなのですが、使いこなす会社側にその運用ルールが委ねられています。中でも、DMやプライベートチャンネルでの秘密の会話による情報格差などが発生することが問題になり、オープンチャンネルに限定している会社も多いのではないでしょうか。しかしながら、オープンに会話をすれば、皆が平等かつ平和に会話ができるか？というと、全くそんなことはありません。オープンにすると下記のような問題が発生します

### 問題1：心理的安全性が確保されない

様々な技術力、ポジションの人がひしめく社内において、HTMLって何？なんて聞いた日には、こいつマジ技術ないわ、みたいに思われてしまうのではないでしょうか。その結果、自身のブランドを守るために、知ったかぶりをしつつ、後ろで一生懸命検索をする、そんなことはないでしょうか。また、大多数に向けて責任を持って発言する必要があり、いい天気ですねばりの当たり障りのない会話になってしまうと思います。

### 問題２：会話の参加者の立ち位置によるパワーバランス

社内の議論の中で、急にスーパー偉い人が参加してきて、「これがいいのでは？」なんて言ったら、多くの人が従ってしまうのではないでしょうか（弊社では、そんなことはない）。もちろんポジションだけではなく技術面での強さによっても発生します。僕もきっとJeff Deanに「この方法が良いんじゃね？」なんて言われたら反論できない。

### 問題３：素の自分を出せない

自身の社内におけるイメージというものがあるから、オブラートに包みまくった会話しかできず、あれ？これをいいたかったんだっけ？みたいに変わってしまったりすることがありますよね。

# Slackに匿名チャンネルを開設してみた

上記を解決するべく、下記のような匿名チャンネルを作りました。Slackコマンドで"/2ch hogehoge"と入れると、匿名でhogehogeと投稿されます。チャンネル内で打つとtypingって出て匿名性が失われるので、別のチャンネルでコマンドを打てば完全に匿名化出来るぞ。なお、セキュリティとかは全く考えてないので、その辺はよしなにやって下さい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146529/8d74ea97-6476-9959-7adb-e23822e292be.png)

### 懸念ポイント

匿名チャンネルを作ると治安が悪くなるのでは？という懸念があるかと思います。これについては面白いもので、今の所**全く荒れない**で現状運用できています。理由としてはいくつか考えられますが、

- 社内の管理人（僕）がその気になれば発言者を特定できる
- 相手が社内のメンバーなので、むやみに荒らしたくはない
- 荒らしたら負け

などでしょうか。とにかく平和です。

# 作ってみよう！

## 全体の仕組み

仕組み自体は非常にシンプルでコマンドからメッセージが送られると、Lambdaの関数が実行され、関数内でpostMessage APIでメッセージを投稿するというものです。この際にLambdaの関数自体は何もメッセージを返さない事で匿名の投稿が実現できます。

## botの作成

[Slack API](https://api.slack.com/)のサイトからBotを作っていきましょう！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146529/d468f81c-5c91-749c-492a-235cae52ee48.png)

Slack Bot自体の詳細の作り方は色々な所に解説があるので、ここでは割愛しますが、

 - `Add features and functionality`
    - `Slash Commands`
        - Command: /2ch
        - Request URL: 後ほど作るLambdaのAPI Gateway
    - `Permissions`
        - `Scopes`
            - chat:write:bot: On
            - commands: On

のように設定します。 

## Lambdaの準備

こちらも、色々な所に解説が乗っているので、Lambda+API Gatewayの使い方は割愛します。Slackとの接続も、POSTメソッドを受け付けるようにするだけですので、特に特殊なことはしません。唯一の注意点としては、BotからSlack APIを叩くために、`requests`モジュールを使えるようにしたいです。Lambda Layerを使って`requests`入りのレイヤーを作っておきましょう。

## 最もシンプルなバージョン

はい、ドーン。

```python

import os
import json
import urllib.parse
import requests

def lambda_handler(event, context):
    qs_d = urllib.parse.parse_qs(event['body'])
    
    access_token = os.environ['ACCESSTOKEN']
    channel_id = os.environ['CHANNEL_ID']
    
    headers = {
        'Authorization': 'Bearer {}'.format(access_token),
        'Content-Type': 'application/json; charset=utf-8'
    }
    
    text = qs_d['text'][0]

    data = {
        "channel": channel_id,
        "text": text,
        "username": '以下、名無しにかわりましてVIPがお送りします'
    }
        
    json_data = json.dumps(data).encode("utf-8")

    ret = requests.post('https://slack.com/api/chat.postMessage', 
                        data=json_data, headers=headers)
    status_code = ret.status_code
    body = ret.content.decode('utf-8')
    
    return {
        'statusCode': status_code,
    }
```

`body`にクエリ文字列（google検索した時にURLの後ろについているやつ）に、各種情報が埋め込まれているので、urllibを使ってパースしましょう。`text`フィールドに本文が入っています。`ACCESSTOKEN`にはBotのトークン情報を、`CHANNEL_ID`には[チャンネルID](https://qiita.com/YumaInaura/items/0c4f4adb33eb21032c08)を入れます。あとは、SlackAPIの`chat.postMessage`を叩くだけ。まずは匿名チャットができたぞ。

### 結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146529/8b41e462-c496-57b0-271f-2e45fe9db369.png)

## メッセージに番号とIDを付けよう

さて、これだけでは前のメッセージとつながってしまい、読みづらいですし、特定のメッセージへのレスポンスが難しいです。それに2chといえば、やっぱ番号がないとね。

しかしながらLambdaはステートを持たないため、書き込み番号を持つことが出来ません。そこで今回は、
[Systems Manager パラメータストア](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-parameter-store.html)を利用してみました。

使い方としては、下記のように`calc_count`を呼び出すと、`counter`キーで保持された書き込み番号が読み込まれ、インクリメントして保存し直します。これを名前に入れましょう。

```python
ssm = boto3.client('ssm')

def get_param(key: str) -> str:
    global ssm
    try:
        return ssm.get_parameter(Name=key)['Parameter']['Value']
    except ssm.exceptions.ParameterNotFound:
        return None

def set_param(key: str, value: str):
    global ssm
    ssm.put_parameter(Name=key, Value=value, Type='String', Overwrite=True)
    
def calc_count():
    key = 'counter'
    count = int(get_param(key) or 0)
    if count > 1000:
        count = 0
    count += 1
    set_param(key, str(count))
    return count
```

さらに、誰が発言したかが分からないと会話が難しいということで、2chの仕様に従い、日毎にIDを振りましょう。IDは、Slackのユーザ名と日付からハッシュ値を作成するだけ。

```python
user_id = qs_d['user_id'][0]
hashed_id = hashlib.md5((str(datetime.date.today()) + user_id).encode()).hexdigest()[:9]
```

最後にメッセージを作成します。

```python
message_id = calc_count()

title_template = "{message_id} 名前：以下、VIPがお送りします ：{date} ID:{hashed_id}"
username = title_template.format(message_id=message_id, date=datetime.datetime.today(), hashed_id=hashed_id)
```

### 結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146529/1c822499-e9bc-ebde-efb6-fc3158884787.png)

## コテハン＆トリップ

次にやはり必要なのはコテハンとトリップでしょう(下図)。詳細についてはググって下さい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146529/a964ffc7-d17d-8805-bc80-7d9caaef6c0b.png)

2chであれば、名前の欄に「コテハン#トリップ」と入れれば良かったけど、ここはSlack、とりあえず、コメントを[コテハン#トリップ]で開始すると、コテハンが作られる事にしよう。苦手な正規表現で頑張ってみる。下記のようにすれば、上記がパースできそう（もっと良い方法ないかな）。

```python
ptn = '(\[([^#]+)(\#([a-zA-Z0-9]+))?\])?(.*)'
ret = re.match(ptn, text, flags=(re.MULTILINE | re.DOTALL))
    
username = ret.group(2)
password = ret.group(4)
main_text = ret.group(5)
```

さらに、トリップ偽装する輩がいないとも限らないので、対策も入れて下記のように名前を実装します。

```python
if username is not None and password is not None:
    hashed_name = hashlib.md5(password.encode()).hexdigest()[:8]
    default_name = username[:8] + '◆' + hashed_name
    hashed_id = '????????'
elif username is not None:
    default_name = username[:16].replace('◆', '◇')
    
username = title_template.format(message_id=message_id, 
                                 name=default_name, 
                                 date=now_str,
                                 hashed_id=hashed_id)
```

### 結果

こんな感じにコテハンとトリップが入れられて・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146529/9383ce0d-da74-4385-331c-b325568856f7.png)

こんな感じで偽装対策がされています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146529/0622e9a9-14b4-b9fe-4f7b-431880f2cdd1.png)

## fusianasan

やはり、2chといえばフシアナさんでしょう。これで多くのユーザを嵌め込みましょう。作り方は簡単。ユーザ名がfusianasanだったら、`user_id`を表示してやります。

```python
if username == 'fusianasan':
    default_name = user_id
```

### 結果

さすがにIDは公開出来ないけど・・・。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146529/6be4e150-0440-6cf0-ea16-fe858a3133b8.png)

## ヘルプ機能

やはり使い方が分からなくなることも多く、管理人としてヘルプを作ることにしました。helpと入力すると・・・。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146529/16028f4a-80df-69be-b83f-534a7815598a.png)

↑こんな感じになる。裏サイトへ飛ぼうとする人がいたらIDがバレる仕組みです。なお、１ヶ月運用しているけど誰も裏サイトに飛んだ人はいない。

## その他

色々やっているけど、

- 書き込んだユーザーのログ情報はCloudWatchに一応保存している
    - DynamoDBに入れようかと思ったけど、こんなんで課金されても勿体ないのでやっていない
- 自動デプロイ
    - GithubにコードをプッシュするとApexで自動的にデプロイされるぞ、便利！
  
# 運用中の事故？

## ログが保存されない！

Apexに切り替えた時に、設定をミスったのでCloudWatchにログが保存されなかったようで、急に2chになった。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146529/46fc23d7-2b0b-3e5b-7d9f-0cef73400c6c.png)

## 攻撃される

社内で遊びでサービスを作ると必ず何かを仕掛けてくる奴がいる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146529/4b02c0ba-20e9-73a3-341c-92fda96c59a0.png)

・・・というわけでお陰様で平和に治安維持されています。

# 他社比較

世の中には似たものを考える人がいるようで、[匿名チャンネル](https://github.com/saitota/SlackAnonymousChannel)はあるようでした。このチャンネルではメッセージをチャンネルに入れると、削除されてbotから再投稿される仕組みのようです。しかしながら、typingが見えてしまう、一瞬書き込みが見えるなど、我々のような匿名性を担保することは出来ません。

![image.png](https://user-images.githubusercontent.com/1152469/37695513-52bd3158-2d13-11e8-8626-dea07e1dd439.gif)

ということで、機能を比較すると下記のようになります！圧倒的ではないか我軍は。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146529/d868ce6d-558b-e6a9-160f-9fdda448e9b1.png)

# おわりに

アドベントカレンダー遅れて申し訳ありませんでした。かくなる上は半年ROMります。