ABEJA Advent Calendarの1日目です。
はじめに
昨年はABEJA Platformに関するAdvent Calendarでしたが、今年はプラットフォームに限らず幅広い技術を扱おう、ということで縛りを作らずに様々な技術を紹介していきます。
さて、皆さん、社内でのコミュニケーションツールは何をお使いでしょうか。色々なツールがあると思いますが、Slackを使っている所が多いのではないかと思います。Slackはとても良いツールなのですが、使いこなす会社側にその運用ルールが委ねられています。中でも、DMやプライベートチャンネルでの秘密の会話による情報格差などが発生することが問題になり、オープンチャンネルに限定している会社も多いのではないでしょうか。しかしながら、オープンに会話をすれば、皆が平等かつ平和に会話ができるか?というと、全くそんなことはありません。オープンにすると下記のような問題が発生します
問題1:心理的安全性が確保されない
様々な技術力、ポジションの人がひしめく社内において、HTMLって何?なんて聞いた日には、こいつマジ技術ないわ、みたいに思われてしまうのではないでしょうか。その結果、自身のブランドを守るために、知ったかぶりをしつつ、後ろで一生懸命検索をする、そんなことはないでしょうか。また、大多数に向けて責任を持って発言する必要があり、いい天気ですねばりの当たり障りのない会話になってしまうと思います。
問題2:会話の参加者の立ち位置によるパワーバランス
社内の議論の中で、急にスーパー偉い人が参加してきて、「これがいいのでは?」なんて言ったら、多くの人が従ってしまうのではないでしょうか(弊社では、そんなことはない)。もちろんポジションだけではなく技術面での強さによっても発生します。僕もきっとJeff Deanに「この方法が良いんじゃね?」なんて言われたら反論できない。
問題3:素の自分を出せない
自身の社内におけるイメージというものがあるから、オブラートに包みまくった会話しかできず、あれ?これをいいたかったんだっけ?みたいに変わってしまったりすることがありますよね。
Slackに匿名チャンネルを開設してみた
上記を解決するべく、下記のような匿名チャンネルを作りました。Slackコマンドで"/2ch hogehoge"と入れると、匿名でhogehogeと投稿されます。チャンネル内で打つとtypingって出て匿名性が失われるので、別のチャンネルでコマンドを打てば完全に匿名化出来るぞ。なお、セキュリティとかは全く考えてないので、その辺はよしなにやって下さい。
懸念ポイント
匿名チャンネルを作ると治安が悪くなるのでは?という懸念があるかと思います。これについては面白いもので、今の所全く荒れないで現状運用できています。理由としてはいくつか考えられますが、
- 社内の管理人(僕)がその気になれば発言者を特定できる
- 相手が社内のメンバーなので、むやみに荒らしたくはない
- 荒らしたら負け
などでしょうか。とにかく平和です。
作ってみよう!
全体の仕組み
仕組み自体は非常にシンプルでコマンドからメッセージが送られると、Lambdaの関数が実行され、関数内でpostMessage APIでメッセージを投稿するというものです。この際にLambdaの関数自体は何もメッセージを返さない事で匿名の投稿が実現できます。
botの作成
Slack APIのサイトからBotを作っていきましょう!
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
入りのレイヤーを作っておきましょう。
最もシンプルなバージョン
はい、ドーン。
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を入れます。あとは、SlackAPIのchat.postMessage
を叩くだけ。まずは匿名チャットができたぞ。
結果
メッセージに番号とIDを付けよう
さて、これだけでは前のメッセージとつながってしまい、読みづらいですし、特定のメッセージへのレスポンスが難しいです。それに2chといえば、やっぱ番号がないとね。
しかしながらLambdaはステートを持たないため、書き込み番号を持つことが出来ません。そこで今回は、
Systems Manager パラメータストアを利用してみました。
使い方としては、下記のようにcalc_count
を呼び出すと、counter
キーで保持された書き込み番号が読み込まれ、インクリメントして保存し直します。これを名前に入れましょう。
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のユーザ名と日付からハッシュ値を作成するだけ。
user_id = qs_d['user_id'][0]
hashed_id = hashlib.md5((str(datetime.date.today()) + user_id).encode()).hexdigest()[:9]
最後にメッセージを作成します。
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)
結果
コテハン&トリップ
次にやはり必要なのはコテハンとトリップでしょう(下図)。詳細についてはググって下さい。
2chであれば、名前の欄に「コテハン#トリップ」と入れれば良かったけど、ここはSlack、とりあえず、コメントを[コテハン#トリップ]で開始すると、コテハンが作られる事にしよう。苦手な正規表現で頑張ってみる。下記のようにすれば、上記がパースできそう(もっと良い方法ないかな)。
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)
さらに、トリップ偽装する輩がいないとも限らないので、対策も入れて下記のように名前を実装します。
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)
結果
こんな感じにコテハンとトリップが入れられて・・・
こんな感じで偽装対策がされています。
fusianasan
やはり、2chといえばフシアナさんでしょう。これで多くのユーザを嵌め込みましょう。作り方は簡単。ユーザ名がfusianasanだったら、user_id
を表示してやります。
if username == 'fusianasan':
default_name = user_id
結果
さすがにIDは公開出来ないけど・・・。
ヘルプ機能
やはり使い方が分からなくなることも多く、管理人としてヘルプを作ることにしました。helpと入力すると・・・。
↑こんな感じになる。裏サイトへ飛ぼうとする人がいたらIDがバレる仕組みです。なお、1ヶ月運用しているけど誰も裏サイトに飛んだ人はいない。
その他
色々やっているけど、
- 書き込んだユーザーのログ情報はCloudWatchに一応保存している
- DynamoDBに入れようかと思ったけど、こんなんで課金されても勿体ないのでやっていない
- 自動デプロイ
- GithubにコードをプッシュするとApexで自動的にデプロイされるぞ、便利!
運用中の事故?
ログが保存されない!
Apexに切り替えた時に、設定をミスったのでCloudWatchにログが保存されなかったようで、急に2chになった。
攻撃される
社内で遊びでサービスを作ると必ず何かを仕掛けてくる奴がいる。
・・・というわけでお陰様で平和に治安維持されています。
他社比較
世の中には似たものを考える人がいるようで、匿名チャンネルはあるようでした。このチャンネルではメッセージをチャンネルに入れると、削除されてbotから再投稿される仕組みのようです。しかしながら、typingが見えてしまう、一瞬書き込みが見えるなど、我々のような匿名性を担保することは出来ません。
ということで、機能を比較すると下記のようになります!圧倒的ではないか我軍は。
おわりに
アドベントカレンダー遅れて申し訳ありませんでした。かくなる上は半年ROMります。