ハトネコエ Web がくしゅうちょう

プログラミングやサーバー・Web制作、チームマネジメントなど得た技術のまとめ

【CircleCI 2.0】GitHub Pages へ指定ディレクトリ内だけを push するための config.yml

約2年前に上のような記事を書いたのですが、
いよいよ CircleCI ver.1 も今月をもって廃止ということで、
CircleCI 2.0 向けに書いたものを貼りつけておきます。

f:id:nekonenene:20180821041348p:plain

1. config.yml の設定

以前と同じように、結果から貼りつける形にしようと思います。
このような config.yml を、リポジトリトップディレクトリに作った .circleci ディレクトリの中に置きます。

なお、このリポジトリで使っているものです。

github.com

ソースは src ディレクトリに入っていて、 npm run build:prod で webpack を用いビルドすると、
dist ディレクトリの中に、babel や minify かましたものとか、pug をコンパイルした、HTML, JS ファイル群が入る仕組みになっています。

1-1. dist ディレクトリ以外を削除

- run:
    name: remove files and dirs which exclude `dist`
    command: /bin/ls -A | grep -v -E '^(dist$|.git)' | xargs rm -rf

まず、 dist ディレクトリ、および、 .git ディレクトリ(と、ついでに .gitignore ファイル)以外を削除しています。
そのために用いているのが、以下のコマンドです。

/bin/ls -A | grep -v -E '^(dist$|.git)' | xargs rm -rf

なぜ単純に ls でなく /bin/ls なのかと言えば、 ls 自体が ls -a --color=autoエイリアスだったりするからです。
今回、 -a オプションは有効にしたくなく、 -A オプションのみ用いたいために、 /bin/ls を利用しています。

-A オプションは、 -a オプションと似ていますが、 ... を出力しません。

ls の結果から grep で、 dist という名前、もしくは .git から始まるディレクトリ・ファイル以外を選択します。
-v オプションは「~以外」の意味合い -E オプションは正規表現を使う場合に指定します。

最後に xargs コマンドで rm -rf の引数としてディレクトリ・ファイル名を渡し削除がおこなわれています。

1-2. dist ディレクトリをトップディレクトリに

- run:
    name: move `dist` dir to current dir
    command: mv ./dist/* ./

mv コマンドで dist ディレクトリの中身を移動しています。

1-3. gh-pages ブランチにデプロイ

- run:
    name: deploy to gh-pages
    command: |
      git config --global user.name  "CircleCI"
      git config --global user.email "circleci@gh-pages.com"
      git add .
      git commit -m "Publish [ci skip]"
      git checkout -B gh-pages
      git push -u origin gh-pages --force

前回の記事で説明したことですので、説明は省きます。

今回は簡潔に build job ひとつにデプロイまでまとめていますが、
Circle CI 2.0Workflows 機能を用いて、 build, test, deploy を別ジョブとして定義すると、より CircleCI 2.0 らしい config.yml になるでしょう。

2. git push のための権限

前回の記事でも書いていますが、大事なことなのでこちらでも。

CircleCI から git push をおこなうためには設定が必要です。

ダッシュボードのページから、リポジトリ名の右についている歯車アイコンをクリックし……

f:id:nekonenene:20180821050636p:plain

PERMISSIONS の中の 「Checkout SSH keys」をクリックし……

f:id:nekonenene:20180821050751p:plain

Add user key 欄の「Authorize With GitHubボタンをクリックし……

f:id:nekonenene:20180821051038p:plain

オーサライズが終わるとボタンが変化するので、「Create and add <username> user key」ボタンをクリックし、 user key を作成します。

f:id:nekonenene:20180821051302p:plain

3. 以上です!

というわけで、以上で Circle CI 2.0 に対応させた、
dist ディレクトリのみを gh-pages ブランチにアップロードさせる方法でした。

JavaScript で動くWebアプリを作った場合なんかは、
「クローンして npm install してから npm start して自分で動かしてみてね!」よりは、
「このページにアクセスして試してみてね!」の方が気軽なので、このようなデプロイ方法を使ってみるといいと思います! では!!

Wi-Fiの接続先が変化した際にシェルスクリプトを走らせる(Mac OSX)

すごく稀なユースケースだと思うんですが、
例えば研究室の無線LANに接続しているときだけプロキシ設定を有効にし、それ以外のときは無効にしたい場合を考えます。

通常であれば、研究室のWi-Fiに繋いだら

export http_proxy="http://www-proxy.waseda.jp:8080"
export HTTPS_PROXY="http://www-proxy.waseda.jp:8080"

環境変数の設定をして、家に戻ったら

unset http_proxy HTTPS_PROXY

環境変数をリセットさせる。

このようなことを何度も繰り返します。
でも、間違いなくめんどうです!

そこで、接続先の無線LANが変化した(異なるSSIDのネットワークに接続した)際に、プロキシ設定を更新することを考えます。

1. シェルの hook を考える

通常、特定のシェルスクリプトを何度も走らせたい場合、cronを用いることが一般的だと思いますが、
今回は、特定のイベントがおこなわれた際に発火する hook を使ってみます。

シェルの hook については、こちらのサイトが詳しく説明してくれていました。
大変ありがたかったです。

シェルでコマンドの実行前後をフックする - Hibariya ( http://note.hibariya.org/articles/20170219/shell-postexec.html )

hook の種類は少ないですが、
そこは if 文でがんばるとして、
preexec という、シェルに入力したコマンドが実行される直前に発火されるというこの hook を使ってみましょう。

2. 接続中の SSID を取得する

ここのサイトで知りました。

https://stackoverflow.com/questions/4481005/get-wireless-ssid-through-shell-script-on-mac-os-x

/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I | awk '/ SSID/ {print substr($0, index($0, $2))}'

これを実行すると、たしかに現在接続中のSSID名称が出てきます。すごい。

SSID取得の方法がわかったので、この値を環境変数として記録しておいて、
次回呼び出したときに、前回のSSIDと変わっているのかを確認すれば良さそうです。

SSIDが変更されたかの条件分岐を最初におこなうことで、
コマンド実行ごとに発火されるこの hook によるシェルへの負荷を、できるだけ少なくしてあげることが出来ます。

3. できあがるスクリプト

上の 1. , 2. を参考にすると、例えば以下のようなスクリプトが出来上がります。

zshbashに対応させました。(bashの方は検証が足りていないので、もしかすると上手く動かないかも…)

# Check if SSID is changed, and update proxies
function check_ssid() {
  local -r LABO_PROXY="http://www-proxy.waseda.jp:8080"
  
  local previous_ssid="$CURRENT_SSID"
  local current_ssid=$(/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I | awk '/ SSID/ {print substr($0, index($0, $2))}')

  export CURRENT_SSID="$current_ssid"

  if [ "$previous_ssid" != "$current_ssid" ] ; then
    case "$current_ssid" in
      "ssid_of_labo") # SSID of your labo's network
        export http_proxy="$LABO_PROXY"
        export HTTPS_PROXY="$LABO_PROXY"
        ;;
      *)
        unset http_proxy HTTPS_PROXY
        ;;
    esac
  fi
}

function add_shell_hook() {
  local shell_pid=$$

  if ps -p $shell_pid | grep -qs "zsh" ; then
    autoload -Uz add-zsh-hook
    add-zsh-hook preexec check_ssid
  elif ps -p $shell_pid | grep -qs "bash" ; then
    local -r BASH_PREEXEC_PATH="${HOME}/.bash-preexec.sh"
    if [ ! -e $BASH_PREEXEC_PATH ]; then
      curl -s https://raw.githubusercontent.com/rcaloras/bash-preexec/master/bash-preexec.sh -o $BASH_PREEXEC_PATH
    fi

    source $BASH_PREEXEC_PATH
    preexec() { check_ssid; }
  else
    echo "Error: This script supports only zsh and bash." 1>&2
  fi
}

add_shell_hook

これを ~/.zshrc (もしくは ~/.bashrc )に直書きするか、
もしくはシェルスクリプトに記述して、 ~/.zshrc ( ~/.bashrc ) から source コマンドで呼び出すようにすれば、hook が登録され、
SSID が変わるごとにプロキシ設定が更新されるようになります。

もちろん、 LABO_PROXY"ssid_of_labo" の値は適当に変えてくださいね。

4. 注意点

私のハマったところとして、
このシェルスクリプト内で export CURRENT_SSID="$current_ssid" と export している箇所がありますが、
単純なシェルスクリプトとして実行させていたときは CURRENT_SSID環境変数として保存されず悩んでいました。

【追記あり】シェルスクリプト内でexportした環境変数は実行後でも使えるのか - by shigemk2 ( https://www.shigemk2.com/entry/linux_shell_export_path )

これによると、

シェルスクリプトを実行する際には、実行するための新たな環境ができるので、環境変数はプロンプトの環境に引き継がれません

ということでとても合点がいきました。
スクリプトを使って現在の shell に環境変数を読み込ませるためには、
.zshrc に直接書くなり、 source コマンドで呼び出すなりしないといけないのですね。

5. まとめ

以上、shell の hook 機能と、
SSIDを取得できることを利用したプチハックでした。

これの延長線上で、いろいろ便利なことができそうな気がします。

以下に、より細かい設定を加えたバージョンの gist を貼っておきます。ご参考にどうぞ。

DiscordのbotをRubyで簡単に作成したのでコード晒します

同僚と松屋で食べてたら盛り上がって、 Discord の部屋を立てました。
Slackと違ってアプリ連携できないよな〜と思ってたら bot は作れると知ったので「よっしゃ! 作るしかない!」Ruby で作りました。

1. 完成形

こんなものができました。

f:id:nekonenene:20180719041905p:plain

f:id:nekonenene:20180719042000p:plain

2. 必要なもの

  • Ruby (今回は v2.4.2 および v2.5.1 で動作確認しました)
  • discordrb (Discord bot 用のRubyライブラリ)
  • VPSなどのサーバー、もしくは常時稼働させているご自宅のPC
  • それなりの権限を持っているDiscordサーバー

VPSに関しては、GCPの無料枠でGCEを使うのがいいかもしれません。
botなのでメモリもディスク容量も必要ありませんので。

参考 : GCP(Google Cloud Platform)での無料GCE(Google Compute Engine)インスタンス作成

3. 各プログラミング言語におけるライブラリ

Discord の bot は便利なことにいろんな言語で作れます。
ライブラリが充実しているので、得意な言語で書くことが出来ます。

今回は Ruby での作り方を書きますが、各プログラミング言語におけるライブラリと参考になる記事を紹介します。
以下で書いたもの以外にも、PHP, Rust, Swift, Haskell などのライブラリもあるようです。(参考 : https://discordapi.com/unofficial/libs.html

3-1. Python

気のせいかもしれないですが、Pythonbot を作っている記事を多く見かけます。
言語処理などライブラリも充実しているので、あれこれ作るには向いているのでしょうね。

ライブラリ
記事

3-2. Node.js (JavaScript)

Pythonに次いで記事が多いように感じました。
Glitchで無料で動かす手法、とても良さそうです。

ライブラリ
記事

3-3. Java

上手くやればKotlinでも書けるそうなのでチャレンジしてみてはいかがでしょう。

ライブラリ
記事

3-4. C#

C# はWebのイメージがないのでライブラリが2つもあることに驚きました。
よく考えるとゲーム制作者とC#はフレンズさんなので、Discordに関わる人が書きやすい言語である可能性がありますね。

ライブラリ
記事

3-5. Go (golang)

最近人気のgolangことGo言語ですが、なぜか日本語記事が少ないです。
ただ、こういう文字列処理を多く扱うWebアプリケーションはGo言語より他の高級言語が向いているとも言えるでしょうね。

ライブラリ
記事

4. コード公開

作ったコードがこちらです!
解説しやすいよう、実際よりかはいくらか削ってあります。(特に munou_message メソッドは長いのでガッツリ削りました)

また、API Keyなどの秘密情報は環境変数としました。
実際に使う際は、 export HOGEHOGE_KEY='secret_value_is_here' のようにコンソールに打って値を設定するなり、
コードに値を直接記入するなり、 dotenv を活用するなりしてください。

5. 解説

はじめに断っておくと、例外処理はけっこう甘いです。
例外を吐くと bot が何も返さないという結果が起こるだけで、プロセスが落ちたりするわけではないのでゆるくやってます。
ですが、本来はメソッドの仕様上 nil が返ってきてはいけない箇所があったりします。

28〜30行目: CommandBot クラスの作成

@bot = Discordrb::Commands::CommandBot.new(client_id: BOT_CLIENT_ID, token: BOT_TOKEN, prefix: "/")

BOT_CLIENT_ID, BOT_TOKEN に基づいて CommandBot クラスを作成します。
この client id および bot token の値についてはこちらの記事を参考にしてみてください。
今回の制作にあたってとても参考になったサイト様です。

DiscordのBotを作ってみよう! | 東京工業大学デジタル創作同好会traP

ここで、 client id と bot token の組み合わせが誤っていると、 @bot.run のときの接続で
「Authentication failed.」のエラーがコンソールに出力されます。

今回 "/" を指定している prefix は、
コマンドの先頭につける文字です。スラッシュ / だとDiscord側で用意されているコマンドとかぶるおそれがあるので、
!: などを使用するのが安心かもしれません。

68〜76行目: お天気案内( message, command メソッド)

### 天気 ###
@bot.message(contains: /天気は?$/) do |event|
  return if event.message.mentions.count > 0
  event.respond(weather_message)
end

@bot.command :weather do |event|
  event.respond(weather_message)
end

下の @bot.command から説明します。

先に説明した prefix の話はこの command メソッドが関連します。
prefix では "/" を指定しておりましたので、ユーザーがチャット欄に /weather と打つと反応してくれます。

f:id:nekonenene:20180719033130p:plain

つまり、prefixを ! としているのであれば、ユーザーは /weather でなく !weather と打たなければ bot が反応してくれないわけです。

次に @bot.message の方を説明します。

message メソッドは、ユーザーが何の気なしに発言したメッセージに勝手に反応します。
contains: /天気は?$/正規表現で拾うようにしています。

なお、この場合は contains: /天気は?$/ というように正規表現せずとも end_with: "天気は?" と定義しても同じになります。
オプションについて詳しくはこちらをご覧ください。 https://www.rubydoc.info/gems/discordrb/Discordrb/EventContainer#message-instance_method

これだけだと 、あとで説明するメンションにおいても、「天気は?」が含まれるメッセージに反応するようにしていますので、 「@kizuna-bot 明日の天気は?」という質問に 2回botが反応してしまう現象が発生してしまいます。ですので、

return if event.message.mentions.count > 0

を入れて、誰かへのメンションであるメッセージに関しては無視するようにしています。

本当はここで nil を返すのは良くなくて、
本来は Discordrb::Events::MessageEvent などの Event クラスが返り値となるべき箇所ですので、
https://github.com/meew0/discordrb/blob/8b93549e9bf45accd0c49401830ef3af87364071/lib/discordrb/bot.rb#L1227 でエラーが起こり discordrb 側で例外処理されています。

42〜55行目: メンションへの反応( mention メソッド)

### メンションに反応 ###
@bot.mention do |event|
  mention_users = event.message.mentions
  message = event.content

  # 不要な文字列を除去
  message.delete!("\s")
  mention_users.each{ |user|
    message.slice!("<@#{user.id}>")
  }

  reply = munou_message(message: message, event: event)
  event.respond(reply) unless reply.nil?
end

mention メソッドはこのbotに対するリプライを誰かが飛ばしたときに反応するものです。

あとの munou_message メソッドで扱いやすいよう、
「@kizuna-bot 明日の天気は?」のメンション部分を削って「明日の天気は?」を message 変数に代入するよう調整しています。

メンションは、Discord内の処理としては <@ユーザーID> という文字列の形式をとっていますので、その形を slice! メソッドを使用することで削り取っています。
なお、「#general」などのチャンネルリンクも同様で <#チャンネルID> という文字列を内部的には保持しています。

整形したら、 message と event を渡してあげて、
248行目以降の munou_message メソッドで、返すメッセージを選んでくれています。

この munou_message メソッドの語源は「人工無脳」からです。
決められたメッセージに反応するbotを俗に人工無脳と呼びます。
(Siriは人工知能なんかじゃなく人工無脳なんじゃないか、と言われたりもしますね)

294〜295行目など: そしてスタートだ!

kizuna_bot = KizunaBot.new
kizuna_bot.start

KizunaBotクラスを作り start メソッドを呼び出しています。

クラスが作られた時点で initialize メソッドが走り CommandBot クラスが作成され、
start メソッドにより、コンソールにbotの invite URL を表示させたあと、
settings メソッドで先ほどのコマンドやメンションなどの際のbotの反応を定義させ、
最後に run メソッドを呼び出すことで WebSocket による通信を開始しています。
参考 : https://github.com/meew0/discordrb/blob/8b93549e9bf45accd0c49401830ef3af87364071/lib/discordrb/gateway.rb#L504-L529

6. コンソールからの実行

この kizuna_bot_for_blog.rb の実行方法について説明します。

RubyをインストールしてあるPCで

gem install discordrb

と打ち、依存ライブラリをまずインストールします。

kizuna_bot_for_blog.rb をダウンロードして適当な場所に置いたあと、

BOT_CLIENT_ID = ENV["BOT_CLIENT_ID"].freeze
BOT_TOKEN = ENV["BOT_TOKEN"].freeze

の部分に従って、環境変数の設定をするか、もしくはこの部分のコードを直接書き換えます。

コンソールで環境変数の設定をするなら

export BOT_CLIENT_ID="1234567890"; export BOT_TOKEN="secret.token"

のように打ち込みます。
RSS2JSON_API_KEY, RECRUIT_API_KEY については、各サイトでAPI Keyを取得してくる必要があるのでここでは説明を省きます)

最後に、コンソールで kizuna_bot_for_blog.rb があるディレクトリにて

ruby kizuna_bot_for_blog.rb

と実行するだけです。けっこうシンプルです。

7. Heroku での実行

クレジットカードを持っているなら 2. で書いたとおり、GCPの無料枠でGCEを使うのが一番気軽な選択肢かと思いますが、 「24時間起動していたいけどクレジットカードも持ってないしパソコンを立ち上げっぱなしもつらい〜」という方は Heroku を利用する手段があります。

無料プランだと月に550時間(クレジットカード登録をすると1000時間になる)しか動かせないので、
使われる頻度の高いbotだと厳しいですが、botがあまり使われないようであれば、
Herokuは、30分使用されないと勝手にスリープ状態に入り、スリープ期間中は使用時間としてカウントされませんので、1ヶ月でなんとか550時間を超えないで済むかもしれません。

この550時間は、動かしているプロセスの合計時間としての制限ですので、
複数の bot を動かしたい場合は簡単に制限を超えてしまうので注意しましょう。

Heroku で実行するには Gemfile , Gemfile.lockProcfile が必要になります。
説明すると長くなりますので、詳細はいろいろググってみてください。
(ぱっとググってみた感じだと、あまり網羅的な記事が見つかりませんでした……)

github.com

今回は私のほうで Heroku で実行できるようなファイルセットにしたものを用意しました。

README に使い方を記していますので、使ってみたい方はお読みください。

8. おしまい

というわけで、大変長くなりましたが、Ruby で Discord の bot を作る方法でした。
初期セットアップの部分の記事はいくらか見ましたので(お世話になりました、ありがとうございます)、
この記事ではもう一歩踏み込んだ実用としての部分のコードをお見せしました。

Ruby は便利メソッドが大変多いので、botを手軽に作りたいときに選ぶプログラミング言語としてはなかなか向いているのではないでしょうか。

ところで今回の KizunaBot 、Kizuna という名前で Kizuna AI ちゃんの方を連想されたかたが多そうですが、
VOICEROID であり VOCALOID でもある 紲星あかり (きずなあかり)ちゃんの名前からとっていました。

かわいいですよね。

Kizuna AI + ミライアカリ みたいな名前してるので、人と話してると VTuber の名前だと勘違いされがちです。

発売が最近なのでまだ知名度ないですが、素直な(声の)子なのでがんばってほしいです。

VOICEROID2 紲星あかり|ダウンロード版

VOICEROID2 紲星あかり|ダウンロード版

かわいかろう?

いや、でも VOICEROID で言うと私はきりたんの方が欲しいんですよ。
あのダウナーで、かつリアルなしゃべり。きりたそ〜欲しい〜。

VOICEROID+ 東北きりたん EX ダウンロード版|ダウンロード版

VOICEROID+ 東北きりたん EX ダウンロード版|ダウンロード版

以上です。