Ruby on RailsのWebアプリで、「Mastodonでログイン」を作ってみた

#はじめに

Mastodonインスタンスの一つ、[Creatodon](https://gamelinks007.net/about)を普段は運営しています。
メインのユーザー層としては創作活動をされている方々で、絵を描かれる人や物書きが使われていますねー(かくいう私もフリーゲーム作ってます)

去年の五月に、さくらのクラウドを使ってインスタンスを建てました。

が、メディアストレージを結構使ってて辛かったり(Pawooとかと繋がっている為)、
一回ストレージ関連でインスタンスが正常に動作しなかったこともあり(cronでのキャッシュの削除が上手くいっていなかった)

そういうこともあり[Creatodon Folio](https://creatodon-folio.herokuapp.com/)という画像投稿サービスを作りました。

基本的には、インスタンスのユーザーが絵や漫画を気軽に投稿できる場所として公開していますねー。

まあ、Creatodon Folio 作った後に AWS S3 へメディアストレージを移したので作った意味があったかといわれると……。

まあ、いろいろとRailsの勉強になったのでOKだと思ってます。

今回の記事では、実際に「Mastodonでログインする機能」を実装するところを解説していきます。

#実際に作ったもの

で、実際に作ったものはこちらになります。

[Creatodon Folio](https://creatodon-folio.herokuapp.com/)

ちなみに、ソースコードは[こちら](https://github.com/S-H-GAMELINKS/CreatodonFolio)

[Railsの教科書](http://igarashikuniaki.net/rails_textbook/)を参考にさせていただきながら制作させて頂きました。

環境としては Heroku + AWS S3 を使って構築しています。
そのため、Heroku dyno の再起動の影響もなく画像を表示できていますねー。
おまけに AWS を使っているのでコストもはるかに安く済んでいます。

あと、今回の記事用に「Mastoonでのログインする機能」のみを実装した[サンプル](https://github.com/S-H-GAMELINKS/MastodonLoginSample)を作っておきました。

本記事のみで、分かりにくいようでしたらそちらを参照されると良いかと思います。

#作る手順
##制作環境
```racket:使用環境
OS:Windows 10 Pro
Ruby: 2.3.3
Rails : 5.1.4
```

上記の環境にてサンプルを制作しました。
ですので、対象はWindowsユーザーになります。ご了承くださいませ。

また既にHerokuアカウントを持っているものとして話を進めていきます。
アカウントを持っていない方は[こちら](https://qiita.com/Arashi/items/b2f2e01259238235e187)の記事を参考にされると良いと思います。


なお、このほか`Heoku CLI` を使う場面もありますので[こちら](https://devcenter.heroku.com/articles/heroku-cli#windows)より実行ファイルをダウンロードしてインストールしておいてください。


##Herokuでの下準備

まず、コマンドプロンプト又は`PowerShell`を起動し、以下のコマンドを実行して Heroku 上にアプリケーションを作成します。

```
heroku apps:create -a <アプリ名>
```

これで、Heroku での下準備は終了

##Mastodonでの下準備

自分のアカウントがあるMastoonインスタンスにて
「ユーザー設定」をクリックし、「開発」という項目をクリックします。
「新規アプリ」をクリックし、「アプリ名」を入力の上「送信」をクリックします。

その後、作成したアプリをクリックすると「クライアントキー」と「シークレットキー」を取得できます。
この二つは後々で使いますのでわかるようにメモしておきます(ただし、第三者とは絶対に共有しないでください！)

Mastoonでの下準備はこれで終了です。

##Railsでの実装

###Railsアプリケーションのひな型作成
それでは、いよいよRailsでの実装といきましょう。

まず、コマンドプロンプト又は`PowerShell`を起動し、以下のコマンドを実行して アプリケーションを作成します。

```
rails new <アプリ名>`
```

その後、下記のコマンドでディレクトリを移動します。

```
cd <アプリ名>
```

それから、以下のコマンドを実行。

```
rails g controller MastodonLogin index
```

今回はログインする機能とログインできたかが確認できればいいので、view に関しては`index` のみを作ります。

`index.html.erb` が作成された後、`config/routes.rb` に `root_path` を設定します

```ruby:
root 'mastodon_login#index'
```

次に、Gemfile を編集します。

まず、以下のように必要なgemを追記します

```ruby:
gem 'coffee-script-source', '1.8.0'

gem 'devise'

gem 'mastodon-api', require: 'mastodon'
gem 'omniauth-mastodon'
gem 'omniauth'
```

その後、`sqlite3` を下記のように開発環境でしか利用しないように設定します。

```ruby:
group :development do
  gem 'sqlite3'
end
```

また本番環境用に以下のように記述を追記しておきます。

```ruby:
group :production do
  gem 'pg'
end
```

Gemfile の編集が終わったら、`bundle update` `bundle install` の順にコマンドを実行してください。

### Devise のインストール

今回のログイン機能を実装するにあたってDevise とよばれる gem を使用します。
詳しいことは[こちら](https://qiita.com/ShinyaKato/items/a098a741a142616a753e)に書いてあるので参照してください。

以下のコマンドを実行してDviseをインストールします。

```
rails g devise:install
rails g devise:views
rails g devise user
```

上記のコマンドをサラッと説明すると

`rails g devise:install`は Devise のインストール。
`rails g devise:views` は Devise 用の view の作成
`rails g devise user`　は Devise で使用するmodelの作成

となっています。

その後、`rails db:migrate` を実行し、DBに適用します。

### Mastodonアカウントでのログイン機能の実装

まず、今のままではUserモデルの中にMastodonでのidなどを記憶できないので

```
rails g migration AddColumnsToUsers uid:string provider:string
```

上記のコマンドを実行してカラムを追加します。

次に、Mastodonのドメイン情報などを記憶するモデル：MastodonClient を以下のコマンドで作成します。

```
rails generate model MastodonClient domain:string client_id:string client_secret:string 
```

次に、`config/initializers/devise.rb`に先ほど取得したアクセスキーなどを記述していきます。

実際の記述例としては以下のようになります。

```ruby:
Devise.setup do |config|

  #(中略)

  # ==> OmniAuth
  # Add a new OmniAuth provider. Check the wiki for more information on setting
  # up on your models and hooks.
  # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'

  config.omniauth :mastodon, 'ACCESS_KEY', 'SECRET_KEY'

  config.omniauth :mastodon, scope: 'read follow', credentials: lambda { |domain, callback_url|
  client = MastodonClient.where(domain: domain).first_or_initialize(domain: domain)

  return [client.client_id, client.client_secret] unless client.new_record?

  new_client = Mastodon::REST::Client.new(base_url: "https://#{domain}").create_app('MastodonLoginSample', callback_url, 'read follow')

  client.client_id = new_client.client_id
  client.client_secret = new_client.client_secret
  client.save

  [client.client_id, client.client_secret]
}

  #(中略)

end
```

で、'ACCESS_KEY', 'SECRET_KEY' に先ほど取得したクライアントキーなどを記述します(本当はdotenvあたりを使って実際のソースコードに記述しない方が良い)

お次は、`app/controllers/users/omniauth_callbacks_controller.rb`を作成して以下のように記述します。

```ruby:
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def mastodon
    callback_from :mastodon
  end

  private

  def callback_from(provider)
    provider = provider.to_s

    @user = User.find_for_oauth(request.env['omniauth.auth'])

    if @user.persisted?
      flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: provider.capitalize)
      sign_in_and_redirect @user, event: :authentication
    else
      session["devise.#{provider}_data"] = request.env['omniauth.auth']
      redirect_to new_user_registration_url
    end
  end
end
```

このコードがないと、Mastodonで認証した後にWebアプリまで戻ってくることができませんので注意。あと、Userデータなども保存されません。

そして、`models/user.rb`に以下のように`:omniauthable`　を追記します。

```ruby:
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable, :omniauthable
```

そして、ログイン時に既にアカウントデータが作成されているかを判断するfindメソッドを記述します。
その為、最終的には以下のようになっています。

```ruby:
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable, :omniauthable

  def self.find_for_oauth(auth)
    user = User.where(uid: auth.uid, provider: auth.provider).first

    unless user
      user = User.create(
        uid:      auth.uid,
        provider: auth.provider,
        email:    User.dummy_email(auth),
        password: Devise.friendly_token[0, 20]
      )
      user.save!
    end
    current_user = user
  end

  private
  
  def self.dummy_email(auth)
    "#{auth.uid}"
  end
end
```

そして、`config/routes.rb`に

```
devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
```

を設定します。

下記のようになっていればOKです。

```ruby:
Rails.application.routes.draw do
  devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
  root 'mastodon_login#index'
end
```

あとは、`index.html.erb`を以下のように記述してログイン用のリンクとログインした後に表示される文字を設定します。

```ruby:
<h1>MastodonLogin#index</h1>
<p>Find me in app/views/mastodon_login/index.html.erb</p>

<%= link_to "Log In for Mastodon Account", user_mastodon_omniauth_authorize_path %>
<br>
<% if user_signed_in? %>
 Your Welcome !
<% end %>
```

ログインされていると、「Your Welcome !」 という文章が表示されるようになっています。
これで、とりあえずは「Mastodonでログインする機能」は実装されました。


##Herokuへのデプロイ

それでは、最後に実際の本番環境で試してみましょう。

まず、Heroku 上のアプリケーションのリポジトリとローカルにあるRailsのリポジトリを以下のコマンドで繋げます。

```
heroku git:remote -a <アプリ名>
```

で、次に以下のコマンドでHeroku上のリポジトリにpushします。

```
git add .
git commit -am "mastodon login test"
git push heroku master
```

そして、最後に以下のコマンドを実行してやれば、「Mastodonでログイン」できるようになります。

```
heroku run rails db:migrate -a <アプリ名>
```

あとは、以下のコマンドで実際のWebサプリを開いて

```
heroku run app:open -a <アプリ名>
```

「Log In for Mastodon Account」をクリックして、実際に使っているMastodonアカウントを入力するだけ


#さいごに

とりあえず、今までの手順でRailsのWebアプリケーションで「Masotodonでログインする機能」を使えるようになります。

正月休み中ずっと実装のために格闘していたので、ようやくのんびりできそうです。

#参考文献

[Heroku初心者がHello, Herokuをしてみる](https://qiita.com/Arashi/items/b2f2e01259238235e187)

[【Rails備忘録】deviseまとめ](https://qiita.com/ShinyaKato/items/a098a741a142616a753e)

[RailsなサービスでMastodonとのOauth連携を実装する](https://qiita.com/foloinfo/items/48389328da43d73ae7a0)

[Devise+OmniAuthでユーザ認証を実装する手順](https://qiita.com/kami_zh/items/94aec2d94a2b4e9a1d0b)

[mastodon-bridge](https://github.com/tootsuite/mastodon-bridge/blob/master/config/initializers/devise.rb)

またMastodonでのログイン機能実装にあたっては [kenchiki](https://github.com/kenchiki) 氏からアドバイスなどをいただきました。

この場を借りて感謝申し上げます。