Elixir&Phoenixでプロフィールサービスを作った

Elixir&Phoenixでプロフィールサービスを作った
[とりまプロフィール](https://torima-p.com)という、主にTwitterで使われることを想定したプロフィールサービスを作成しました。

![torima.png](https://qiita-image-store.s3.amazonaws.com/0/136506/72b8dfd3-2859-0f4c-2186-952cdb785bc0.png)

質問を送って回答したりも出来ます。

## 環境

* Elixir1.6
* Phoenix1.3
* MySQL5.7
* Vue.js
* GCE(f1-micro)
* Bootstrap Material Design

## プロフィール編集画面

動きのあるところはVueコンポーネントで作っています。[vuedraggable](https://github.com/SortableJS/Vue.Draggable)を入れることで既存のコードをほとんど変えずソートなども可能です。スマホでも問題なく動いていました。

各行もコンポーネントになっているので、その内部では配列的な事も気にせず単にシンプルなupdate, delete等の実装が可能です。

```html
  <div>
    <draggable v-model="currentUsersSubjects" :options="{handle: '.name'}" @end="onDragEnd">
      <profile-row
        v-for="usersSubject in currentUsersSubjects"
        :key="usersSubject.id"
        :users-subject="usersSubject"
        @on-update="onUpdate"
        @on-delete="onDelete"
      ></profile-row>
    </draggable>
    <add-subject
      :popular-subjects="popularSubjects"
      :users-subjects="currentUsersSubjects"
      :questions="questions"
      @on-create="onCreate"
    ></add-subject>
  </div>
```

## 色の設定

色を設定できるようにしていますが、material-colorsとvue-color（のSwatches）を使っています。色を自分で決めたかったので、palette属性で指定しています。（vue-color自体のソースを参考にしています）

```html
<swatches-picker v-model="currentBgColor" :palette="palette" />
```

```javascript
import material from "material-colors";
import { Swatches } from "vue-color";

const colorMap = [
  "red",
  "pink",
  "purple",
  "deepPurple",
  "indigo",
  "blue",
  "lightBlue",
  "cyan",
  "teal",
  "green",
  "lightGreen",
  "lime",
  "yellow",
  "amber",
  "orange",
  "deepOrange",
  "brown",
  "grey",
  "blueGrey",
  "black"
];
const colorLevel = [
  "900",
  "800",
  "700",
  "600",
  "500",
  "400",
  "300",
  "200",
  "100",
  "50"
];

const palette = (() => {
  let colors = [];
  colorMap.forEach(type => {
    var typeColor = [];
    if (type.toLowerCase() === "black" || type.toLowerCase() === "white") {
      typeColor = typeColor.concat(["#000000", "#FFFFFF"]);
    } else {
      colorLevel.forEach(level => {
        const color = material[type][level];
        typeColor.push(color.toUpperCase());
      });
    }
    colors.push(typeColor);
  });
  return colors;
})();
```

## Twitter API

色々作り進めていっているうちにTwitter用のライブラリを２つ使うことになってしまいました。

### 認証＆フォローユーザー取得

[ueberauth/ueberauth_twitter: Twitter Strategy for Überauth](https://github.com/ueberauth/ueberauth_twitter)

こちらのライブラリを下記目的で使用しました。

* 認証
* フォローユーザー取得
* ブロックユーザー取得

当初、ueberauthしか知らなかったのでueberauthで認証とフォローユーザー取得を使い始めてしまいました。ただ、次に紹介するライブラリはブロックユーザー取得の機能もなかったので、とりあえず差し替えずに併用することにしました。

今後リファクタリングする場合このあたりはどうするか再検討が必要そうな気がします。

#### 認証

これはもうマニュアル通りです。ただ、DBに色々保存する必要があるので下記のようにして取得しています。authがcallbackで送られてくるユーザーの情報です。

```elixir:Twitter名とトークン
    attrs = %{
      twitter_name: auth.info.nickname,
      twitter_token: to_string(auth.credentials.token),
      twitter_secret: to_string(auth.credentials.secret)
    }
```

```elixir:名前と画像
    attrs =
      attrs
      |> Map.put_new(:name, name_from_auth(auth))
      |> Map.put_new(:avatar, String.replace(auth.info.image, "http://", "https://"))
```

名前は適当にあれこれ適当に判別してます。

```elixir
  defp name_from_auth(auth) do
    if auth.info.name do
      auth.info.name
    else
      name =
        [auth.info.first_name, auth.info.last_name]
        |> Enum.filter(&(&1 != nil and &1 != ""))

      cond do
        length(name) == 0 -> auth.info.nickname
        true -> Enum.join(name, " ")
      end
    end
  end
```

#### フォローユーザー取得

これは取得する方法自体は無かったのでライブラリの中のソースを真似て作成しました。結構適当だった気がするのでコピペはしない方が良いと思います。

```elixir
  def friends_list(id, token, cursor \\ nil) do
    params = [
      {"user_id", id},
      {"count", 200},
      {"skip_status", true},
      {"include_user_entities", false}
    ]

    params = if cursor, do: params ++ [{"cursor", cursor}], else: params

    case Twitter.OAuth.get("/1.1/friends/list.json", params, token) do
      {:ok, {{'HTTP/1.1', 401, _message}, _headers, _body}} ->
        {:error, nil}

      {:ok, {{'HTTP/1.1', status_code, 'OK'}, _headers, body}} when status_code in 200..399 ->
        body = Poison.decode!(body)
        {:ok, body["users"], body["previous_cursor"], body["next_cursor"]}

      {:ok, {{'HTTP/1.1', status_code, message}, _headers, _body}} ->
        {:error, to_string(message)}
    end
  end
```

#### ブロック一覧

こちらも似たような感じです。

```elixir
  def blocks_ids(token, cursor \\ nil) do
    params = [
      {"stringify_ids", true}
    ]

    case Twitter.OAuth.get("/1.1/blocks/ids.json", params, token) do
      {:ok, {{'HTTP/1.1', 401, _message}, _headers, _body}} ->
        {:error, nil}

      {:ok, {{'HTTP/1.1', status_code, 'OK'}, _headers, body}} when status_code in 200..399 ->
        body = Poison.decode!(body)
        {:ok, body["ids"], body["previous_cursor"], body["next_cursor"]}

      {:ok, {{'HTTP/1.1', status_code, message}, _headers, _body}} ->
        {:error, to_string(message)}
    end
  end
```

### ツイート

こちらは下記を使ってます。

[parroty/extwitter: Twitter client library for elixir](https://github.com/parroty/extwitter)

非常にシンプル。ユーザーのトークンを使うのでいちいちconfigureしています。

```elixir
  def tweet(token, secret, body) do
    ex_twitter_configure(token, secret)
    ExTwitter.API.Tweets.update(body)
  end

  defp ex_twitter_configure(token, secret) do
    conf = [
      consumer_key: Application.get_env(:extwitter, :oauth)[:consumer_key],
      consumer_secret: Application.get_env(:extwitter, :oauth)[:consumer_secret],
      access_token: token,
      access_token_secret: secret
    ]

    ExTwitter.configure(:process, conf)
  end
```

### キャッシュ

[ConCache](https://github.com/sasa1977/con_cache)というのがあるのですが、メモリキャッシュなので超貧弱なサーバーだと無理そうだなと思いました。最初ConCacheを使っていましたが自前のDB保存に変更しました。

一応ConCacheのインターフェースに合わせてあるので、タプルと文字列の変換を入れています。

```elixir
  def tuple_to_string(tuple) do
    list = Tuple.to_list(tuple)

    case Poison.encode(list) do
      {:ok, str} ->
        str

      {:error, _any} ->
        ""
    end
  end

  def string_to_tuple(str) do
    case Poison.decode(str) do
      {:ok, list} ->
        List.to_tuple(list)

      {:error, _any} ->
        nil
    end
  end
```

## Bootstrap Material Design

[brunchでBootstrap Material Design4を使う](https://qiita.com/dala00/items/b5d5cb9146db36e79632)の記事の方法で導入しています。

## デプロイ

最近はどれもElixir & Phoenix & GCEなので、以前下記で書いた方法でやっています。

[Systemdを使ったPhoenixの本番デプロイ詳細例](http://alphabrend.hatenablog.com/entry/2017/12/18/212449)

## まとめ

この構成で作ったサービスもこれで４つ目になりだいぶ小慣れてきました。あまりこういった構成のものを見たことが無い方などいらっしゃれば是非見てみて下さい。

[とりまプロフィール](https://torima-p.com)