Spreadsheetsに書き込むDiscordBotをRubyで書いてDockerで動かす

# はじめに

この記事は[Hamee Advent Calendar 2020](https://qiita.com/advent-calendar/2020/hamee)の17日目の記事です。

私は趣味で某ヤバイ☆ゲームを遊んでおり、毎月月末にプレイヤー同士が30人でチームを組みランキングを競うイベントをメインに楽しんでいます。
ランキング上位のチームはDiscord上であれやこれや連絡を取りながらプレイするのですが、チャットでしかやりとりをしないので30人もプレイヤーがいると情報をまとめきれずイベントをスムーズに進めることができないことがあります。
それを解決するためにDiscordのチャット内容からGoogle Spreadsheetsに書き込むBotを作成しました。が、今月から使わなくなったためこの記事で供養します。

# 準備
DiscordBotの開発には、Botのtokenが必要です。
以前に記事を書いたのでこちらを参考にしてください（今回はRubyで動かします）。

- [discordのbotをpythonで動かしてみる](https://qiita.com/TanigUhey/items/2f1e7132934fdb17a9ef)

Spreadsheetに書き込むために、SpreadsheetsAPIのcredentialも必要です。
こちらの記事の**[Google Sheets API の設定]**を参考にしてください。

- [Google Sheets API を使ってスプレッドシートを操作](https://japan.appeon.com/technical/techblog/technicalblog019/)

Dockerコンテナ上で動作させますが、Dockerについては割愛します。

# 中身

2日くらいで作ったためイケてない箇所が多々あります。

用意したファイルと構成はこんな感じです。

```:tree
.
├── Dockerfile
├── Gemfile
├── Gemfile.lock
└── source
    ├── .env
    ├── bot.rb
    └── credentials.json
```

## スプレッドシート

シートは以下のようなものを用意して、発言した人の行のC列に書き込みをするbotとします

||A|B|C|
|---|---|---|---|
|1|252398827|Aさん||
|2|261223322|Bさん||
|3|348710907|Cさん||

## Dockerfile

```dockerfile:Dockerfile
FROM ruby:2.6.5

ENV APP_HOME /home/source

RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME
ADD Gemfile Gemfile
ADD Gemfile.lock Gemfile.lock
ADD ./source ./

RUN bundle install

CMD ["bundle", "exec", "ruby", "bot.rb"]
```

このDockerfileをbuildして、runします

## Gemfile
```ruby:Gemfile
source 'https://rubygems.org'

gem 'dotenv'
gem 'discordrb'
gem 'google_drive'
```

* dotenv
  * DiscordBotのトークンなどを.envファイルで管理するために使用します
* discordrb
  * Ruby版のDiscordAPIのライブラリです
  * [リファレンス](https://www.rubydoc.info/github/meew0/discordrb)
* google_drive
  * Spreadsheets APIのために使用します
  * 公式のライブラリはgoogle-api-clientというライブラリなのですがこちらを使うことにしました
  * [GitHub](https://github.com/gimite/google-drive-ruby)

## credentials.json

前述の準備で用意したものをそのまま配置します。

## bot.rb

全体を通してエラーに関しては何も考えていません。

### Spreadsheets部分

```ruby:bot.rb
require "google/apis/sheets_v4"
require "googleauth/stores/file_token_store"
require "fileutils"

Dotenv.load

class Spreadsheet
  OOB_URI = 'urn:ietf:wg:oauth:2.0:oob'.freeze
  APPLICATION_NAME = 'MyBot'.freeze
  CREDENTIALS_PATH = 'credentials.json'.freeze
  TOKEN_PATH = 'token.yaml'.freeze
  SCOPE = Google::Apis::SheetsV4::AUTH_SPREADSHEETS
  ID = ENV['SHEAT_ID']

  attr_accessor :service

  def initialize
    @service = Google::Apis::SheetsV4::SheetsService.new
    @service.client_options.application_name = APPLICATION_NAME
    @service.authorization = authorize
  end

  def authorize
    client_id = Google::Auth::ClientId.from_file CREDENTIALS_PATH
    token_store = Google::Auth::Stores::FileTokenStore.new file: TOKEN_PATH
    authorizer = Google::Auth::UserAuthorizer.new client_id, SCOPE, token_store
    user_id = "default"
    credentials = authorizer.get_credentials user_id
    if credentials.nil?
      url = authorizer.get_authorization_url base_url: OOB_URI
      puts "Open the following URL in the browser and enter the " \
           "resulting code after authorization:\n" + url
      code = gets
      credentials = authorizer.get_and_store_credentials_from_code(
        user_id: user_id, code: code, base_url: OOB_URI
      )
    end
    credentials
  end

  def read(range)
    response = service.get_spreadsheet_values ID, range
    response.values
  end
  
  def write(range, value)
    data = Google::Apis::SheetsV4::ValueRange.new
    data.major_dimension = 'ROWS'
    data.range = range
    data.values = [[value]]
    options = {
      value_input_option: 'RAW'
    }

    response_write = service.update_spreadsheet_value ID, range, data, options
    response_write.updated_cells
  end
end
```

* authorize
  * どこかからコピペしてきたものです
  * 初回実行時にのみ認証作業を求められるので、表示されたURLにアクセスし表示されたコードを入力してください
  * 認証が済むとtoken.yamlが生成されるので2回目以降は作業の必要はありません

### Discordbot部分

```ruby:bot.rb
require 'discordrb'
require 'dotenv'

Dotenv.load

class MyBot
  attr_accessor :bot, :spreadsheet

  def initialize
    @bot = Discordrb::Commands::CommandBot.new client_id: ENV['CLIENT_ID'], token: ENV['BOT_TOKEN']
    @spreadsheet = Spreadsheet.new
  end

  def start
    settings

    @bot.run
  end
  
  def get_user_row(user_id)
    a = @spreadsheet.read('A:A')
    (a.find_index [user_id.to_s]) + 1
  end

  def settings
    @bot.message contains: /^[1-3]$/ do |event|
      num = event.content
      user_row = get_user_row event.user.id
      @spreadsheet.write "C#{user_row}", num
    end
  end

end

bot = MyBot.new
bot.start
```

* initialize
  * CommandBotというクラスを使います
  * 今回は使いませんが第三引数にprefixを設定すると、特定の文字から始まるメッセージにのみ反応させることができます

* start
  * bot.runを実行するとbotが起動します

* setting
  * 上記コードでは、どのチャンネルか区別なく1〜3のみの文字の発言に反応するようにしています
  * メッセージを発言した人のIDとスプレッドシート上のIDを照らし合わせて、書き込みを行なっています

## 詰まりポイント

実はさきほどのDockerfileには不備があり、いざ起動してみると以下のようなエラーが表示されます。

```
/usr/local/bundle/gems/ffi-1.13.1/lib/ffi/library.rb:145:in `block in ffi_lib': Could not open library 'sodium': sodium: cannot open shared object file: No such file or directory. (LoadError)
Could not open library 'libsodium.so': libsodium.so: cannot open shared object file: No such file or directory
```

libsodiumというライブラリが足りないようなのですが、いざググってみても何者かあまりわからない。

結局[discordrbのREADME](https://github.com/discordrb/discordrb)にちゃんと依存ファイルとして書いてあって、それを見逃していただけでした。
discordrbについて解説している他ブログなどではあまり明示していることがなかったため、基礎部分だけコピペで作ろうとすると詰まるという例でした。
Dockerfileに以下の記述を追加して解決。

```dockerfile:Dockerfile
RUN apt-get update -y && apt-get install -y libsodium-dev
```

# おわりに

11月の月末は上記のBotが稼働してくれました。
今月はいろいろ理由があり、Botをnode.jsで作り直して機能を拡張する予定で、現在開発中です。
個人的にはRubyでBotを開発できて満足していますし、Botが稼働している様子は我が子が頑張っている感があって良いですね。