RailsなサービスでMastodonとのOauth連携を実装する

皆さんtootしてますか！
取り敢えず技術系の方々が次々と寄ってたかってる感はありますが、Twitterと違って文字数制限が500文字までと言うところが弊社で現在運営してます http://drip.ink と相性がいいなぁ、引用投稿垂れ流したいなぁ、と言うことで連携投稿を実装してみることにしました。
あとインスタンス立てる系の投稿はたくさんあるみたいですが、連携の情報はなさそうなので書くことにしました。
とは言えgemの恩恵でほぼ設定だけなのでむちゃ楽です。

## gemを導入する

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

## MastodonClientモデルの作成

Mastodonは他のSNSと異なり様々なドメインが存在しますので、利用者の情報以外にもMastodonClientの情報を保持する必要があります。

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

## Initializerにomniauthの情報を追加

devise利用の場合は `initializers/devise.rb` に、そうでなければ `initializers/omniauth.rb` にでも追記します。
サンプルでは `client.create_app('Sample Application', callback_url, 'read write')` に `read wirte` の権限は記述されていませんが、書かないと怒られます。　`follow`なども取れますが、今回は利用しなかったので取得しませんでした。`email`は今のところ取得できないみたいです。

```
Rails.application.config.middleware.use OmniAuth::Builder do
  ...
  provider :mastodon, scope: 'read write', credentials: lambda { |domain, callback_url|
    Rails.logger.info "Requested credentials for #{domain} with callback URL #{callback_url}"

    existing = MastodonClient.find_by(domain: domain)
    return [existing.client_id, existing.client_secret] unless existing.nil?

    client = Mastodon::REST::Client.new(base_url: "https://#{domain}")
    app = client.create_app('Sample Application', callback_url, 'read write')

    MastodonClient.create!(domain: domain, client_id: app.client_id, client_secret: app.client_secret)

    [app.client_id, app.client_secret]
  }

end
```

この状態で `/auth/mastodon` にアクセスすると、ドメインを含めたアカウント名を求められます

![スクリーンショット 2017-04-20 19.30.47.png](https://qiita-image-store.s3.amazonaws.com/0/1946/29620e58-acc2-ddbd-ec4a-ea889b748dc0.png)

後はログインを押せば、インスタンスが初回連携時であればローカルにインスタンスの情報が保存されます。
ここらへん自分で作らないといけないのかな…とか思っていたのでめちゃくちゃ楽でした。

あとは取得したデータを元に、他のSNSと同様にアカウント情報などを登録してユーザと紐付ければOKです。ここら辺はtwitterやfacebookと同じです。
サンプルとして、rspecで利用してる構造を貼り付けておきます。プロフィール（多分noteのパラメータ）なども取得可能ですが、今回は使わなかったので省いています。

```

    OmniAuth.config.mock_auth[:mastodon] = OmniAuth::AuthHash.new({
      'info' => {
        'name' => 'account_name',
        'nickname' => 'account_name',
        'image' => 'http://drip.ink/images/ogimage.png',
      },
      'uid' => 'account_name@example.com',
      'provider' => 'mastodon',
      'credentials' => {
        'token' => 'token',
        'expires' => false
      },
      'extra' => {
        'raw_info' => {
          'display_name' => 'Firstname Lastname',
          'id' => '1',
          'username' => 'account_name'
        }
      }
    })
```

## 各種APIを叩いて、欲しいデータを取ったり投げたりする

クライアントの取得は、authから取得したuidの後ろ半分がドメインですので、ドメイン名とtokenを利用して行います。

```
_, domain = uid.split('@')
client = Mastodon::REST::Client.new(base_url: "https://#{domain}", bearer_token: token)
```

以下はサービスによってしたいことがマチマチだと思いますが、dripで利用した物をサンプルとして書いておきます。

### avatarを取得する

```
client.verify_credentials.avatar
```

### ステータスを投稿する

```
client.create_status('toot! :elephant:')
```

こんな感じで無事、他のSNS連携と同じようにMastodon連携と投稿が出来ました。

参考
* [tootsuite/mastodon-bridge: Tool to help you find your Twitter friends on Mastodon. Also an example of Mastodon API usage](https://github.com/tootsuite/mastodon-bridge)

## 余談

Mastodonの、インスタンス単位で運営体制が自由に操作出来る思想は素敵だなと個人的にとても期待感を持ってます。
ソーシャルグラフではなくインタレストグラフの考え方だと思うのですが、個人的にそれが欲しくてdripを作っている側面も強いので、一時的な流行に留まらず成長して欲しいと思っています。

運営者の意向次第で例えばBotは完全禁止のpureなつぶやきオンリーな空間を作ることも、逆にBotしかいない世界を作ったりも出来ますし、読んでいる記事をひたすら垂れ流すような空間を作ることも出来ます（dripのインスタンス作って何かやりたい）。
「何をしても良い」と言うのはある意味で余り自由ではなく、制約を設けることでより自由になる、と言うのがMastodonの思想ではないでしょうか。
気付かないうちに情報の泡に取り込まれているよりも、自分のいる場所とその嗜好性が明確になった上で、色々な人やインスタンスと繋がっていくとみんなハッピーになれる気がします。
この辺りはまた深く掘り下げてエントリーでも書きたい所ですが、まぁそういう事はみなさん勝手にやるでしょうから、取り敢えずTwitterだけでなく、Mastodon内のトラフィックにdripのデータを流していこうかと思うのでした。

宣伝：
http://drip.ink は引用文によるソーシャルブックマークサービスです。読んでいる記事から残しておきたい一文をコンパクトに保存・整理・共有、そして他ユーザの保存した情報から有用な記事が発見できます。
今回実装したMastodon連携の他、TwitterとFacebook連携で共有したり、[自分の投稿をRssに吐き出してevernoteに書き出したり](http://hokoxjouhou.blog105.fc2.com/blog-entry-865.html)、と言ったことが出来ます。
使ってね :elephant: