この記事は
Applibot Advent Calendar 2021 の記事になります。
前日は@yuucuさんの「テキストファイルに対してSQLを発行できるツール「q」& vimから使う「vimq.vim」の紹介」という記事でした。
はじめに
こんにちは、(株)Applibotでバックエンドエンジニアを担当している小川です。
昨年のアドカレでは社内勉強会についての記事を書きましたが、
社内でエンジニア間での交流を推進する横軸組織の一員としても活動しています。
年末年始といえば忘年会や新年会…
コミュニケーション上大人数で開催したいが昨今のコロナ禍によって直接集まることは減り、
Zoomなどでのオンラインでのコンテンツに苦労することも増えました。
ですが、
コミュニケーションを大事に、満足度と効果の高いコンテンツを作って活用したい気持ちがあり、
ApplibotではZoomでのアプリボットワイドショーや、忘年会を開催し続けています。
コメントが画面上に流れる仕組み等も社内で開発されていて活用されています。
今回はZoom上でのエンジニア忘年会のために作った、
「 Zoomのコメントを使って、自由回答のクイズを集計する仕組み 」についてご紹介します。
概要
まず前置きとして触れておきますが、内容自体はとても単純で、難しい事はしていません。
メイン業務はプロジェクトにアサインされて開発を行っているので、納期がそこまで避けなかったりするので合間に作れるものになりました。
言語はあまり深く考えず、さくっと書ける Go を使っています。
また、Zoomであれば投票機能もあるので工夫すれば使えるかもしれませんが、
今回の要素的には自由回答にしたかったことと、コメントが流れて欲しかったこと、会社側のアカウント事情などにより投票機能がそもそも使えなかったりしたので自前で用意することにしました。
視聴者には正解を用意したクイズ・質問にコメントで気楽に回答してもらい、それを集計します。
大雑把な集計の流れは以下の通りです。
- クイズ・質問にZoomコメントで回答してもらう
- クイズごとに回答した開始・終了時間をメモしておく
- 集計時にZoom上でコメントを出力(手作業、3クリックくらい)
- メモしておいた回答時間と、ダンプしたコメントを入力にプログラムを実行
- データベースに名前ごとにポイントが集計される
以上です。これだけ見ても内容が想像できてしまうかもしれません。
自前で集計するので、色々出来ると考え、
今回はクイズが複数あり、クイズごとに早く正解を回答した人がより高いポイントを貰え、最後に累計ポイントが高い順に出力する形式 にも対応できるようにしました。
実装
簡単にちょっとややこしかった部分だけ説明します。
Zoomのコメント出力
自動で出力する方法等もありましたが、権限やそもそもZoom会議自体が終わらないと保存出来ない様だったので、
何度でもいつでも出力できる手動出力としました。
コメントの上にあるボタンからすぐ出力できます。
出力ファイルは指定したフォルダに一定のファイル名でtxt出力されるため、予めプログラムにはファイルの指定をしておきます。
ただ出力されたファイルは若干厄介で、スペースと改行で区切られた独自のフォーマットになっています。
コメントの抽出
自前で作るとしたら、恐らく一番面倒かと思われるのはコメントの抽出なので参考に載せておきます。
Zoomの出力に対しては色々やり方はあると思いますが、今回はなるべくシンプルに 正規表現で抽出しました。
var rep, _ = regexp.Compile(`\s*(.*)\t\s開始\s(.*)\s:\s(.*)$`)
(Zoom側のコメントフォーマットがアップデートで変わる事があるらしく、忘年会直前にフォーマットが突然変わって一瞬焦ったりもしました。)
今回複数のクイズを時間で区切って取りまとめたかったため、configとしてStartDatetimeとEndDatetimeを持ち、 時間ごとに集計しています。
Zoomのコメントはいつ出力しても会議を抜けない限り積み重なっていくため、開始・終了時間だけ控えておけば冪等性があります。
何かしらミスをしても、再集計可能です。
// getComments 指定の問題のコメントを取得
func getComments(config *Config, strs []string) []*Comment {
startTime, err := time.Parse(timelayout, config.Start)
if err != nil {
panic("start_timeのパースに失敗" + config.Start)
}
endTime, err := time.Parse(timelayout, config.End)
if err != nil {
panic("end_timeのパースに失敗" + config.End)
}
// 期間内対象のコメント
comments := make([]*Comment, 0, len(strs))
for i, str := range strs {
if str == "" || i < config.SkipNumber {
continue
}
// 正規表現で分解
results := rep.FindAllStringSubmatch(str, -1)
if results == nil {
// 改行などあった場合はパースできないのでSkip
continue
}
t, err := time.Parse(timelayout, results[0][1])
if err != nil {
fmt.Println("warn: timeのパースに失敗" + results[0][1])
continue
}
// 対象の時間のコメントのみ格納
// startTime <= logTime <= endTime
if !t.Before(startTime) && !t.After(endTime) {
comments = append(comments, &Comment{
Time: t,
Name: results[0][2],
Comment: results[0][3],
})
}
}
return comments
}
type Comment struct {
Time time.Time
Name string
Comment string
}
オプションや重みの設定
単純に設問ごとにConfigを持って、早押しではない問題や、複数の回答例などを持てるようにしています。
type Config struct {
Start string // 開始時間
End string // 終了時間
IsFast bool // 早押しか
Point int // 早押しでない場合の得点
Answer []string // 回答例
}
// Weight 早押し用 正解が早いほどPointを高く重み付け
type Weight struct {
Start int
End int
Point int
}
点数計算と保存
コメントは上記の通り抽出出来るので、Configに沿って点数を計算し、DB(MySQL)に結果を保存します。
クエリでSumを取ったり、Orderを取ったりが楽なので安直にDBを選びました。
実行時にクイズ番号だけでも全てのクイズ集計も実行できるようにし、
冪等性のため、そのクイズ番号でデータをTrancate、Deleteしてから集計出力させます。
keyは単純にユーザ名とし、クイズごとに点数を保存しました。
CREATE TABLE user_point (
name VARCHAR(32) NOT NULL,
quiz_no INT NOT NULL,
point INT NOT NULL,
PRIMARY KEY (name, quiz_no)
) DEFAULT CHARACTER SET utf8mb4;
終わりに
泥臭い内容ですがやっている事は単純ですし、機能を付け足したりもある程度柔軟にできるので、案外忘年会程度であれば十分運用可能なものになった気がします。
当日リアルタイムで集計するため、複数コメント時の点数計算や、異常系などどんな状況やコメントが来てもトラブらない実装などはある程度色々試して入れてたりします。
細々したところは使いやすいように工夫したり、要件に合わせて修正すれば良さそうです。
出力としてはテーブルとして吐き出すだけなので、これからZoomでの結果の見せ方を華やかにするための追加など今後していけたら良いかもしれません。
他にもコンテンツを盛り上げる工夫やアイデアがあれば、是非教えてください!