Skip to content

Latest commit

 

History

History
181 lines (148 loc) · 6.15 KB

who-is-that.asc

File metadata and controls

181 lines (148 loc) · 6.15 KB

用户授权了吗?

在我的设计里,所有的消息,是一定都要检查该用户是否已授权的。

这种场景非常适合 Plug 来处理,这里我们在 twitter_controller.ex 文件里新增一个 function plug。

lib/tweet_bot_web/controllers/twitter_controller.ex
  alias TweetBot.Accounts
  plug(:find_user)

  defp find_user(conn, _) do
    %{"message" => %{"from" => %{"id" => from_id}}} = conn.params

    case Accounts.get_user_by_from_id(from_id) do
      user when not is_nil(user) ->
        assign(conn, :current_user, user.from_id)

      nil ->
        token =
          ExTwitter.request_token(
            URI.encode_www_form(
              Routes.auth_url(conn, :callback) <> "?from_id=#{from_id}"
            )
          )

        {:ok, authenticate_url} = ExTwitter.authenticate_url(token.oauth_token)

        sendMessage(
          from_id,
          "请点击链接登录您的 Twitter 账号进行授权:<a href='" <> authenticate_url <> "'>登录 Twitter</a>",
          parse_mode: "HTML"
        )

        conn |> halt()
    end
  end

现在会报 Accounts.get_user_by_from_id 未找到的错误,因为我们还没有定义它。

打开 accounts.ex 文件,添加方法:

lib/tweet_bot/accounts/accounts.ex
  def get_user_by_from_id(from_id) do
    Repo.get_by(User, from_id: from_id)
  end

不过还是会报错:

[debug] ** (Ecto.Query.CastError) deps/ecto/lib/ecto/repo/queryable.ex:357: value 48885097 in where cannot be cast to type :string in query:

这是因为我们在定义 User 时,from_id 是一个字符串,而 conn.params 中解析出的却是数值。我们可以粗暴一点,直接做类型转换:

Repo.get_by(User, from_id: Integer.to_string(from_id))

但长远来说,这只是个 workaround,不是真正的解决办法。

下面我们将通过 migration 调整 Userfrom_id 的类型。

Migration

创建一个 migration:

$ mix ecto.gen.migration alter_users
* creating priv/repo/migrations/20181203125626_alter_users.exs

打开新建的文件,修改内容如下:

priv/repo/migrations/20181203125626_alter_users.exs
+defmodule TweetBot.Repo.Migrations.AlterUsers do
+  use Ecto.Migration
+
+  def change do
+    alter table(:users) do
+      modify(:from_id, :integer)
+    end
+  end
+end

运行 mix ecto.migrate

$ mix ecto.migrate
[info] == Running TweetBot.Repo.Migrations.AlterUsers.change/0 forward
[info] alter table users
** (Postgrex.Error) ERROR 42804 (datatype_mismatch): column "from_id" cannot be cast automatically to type integer

报错了。我怀疑是不是因为数据库中已经有数据导致的,就删库重试:

$ mix ecto.drop
$ mix ecto.create
$ mix ecto.migrate

仍然报错。Google 扫了一圈没找到答案,只好到 elixir forum 提问,好了,有回复:

  def change do
    execute(
      "alter table users alter column from_id type integer using (from_id::integer)",
      "alter table users alter column from_id type character varying(255)"
    )
  end

重新运行 mix ecto.migrate,成功。

此外还要调整下 user.ex 文件:

     field :access_token, :string
-    field :from_id, :string
+    field :from_id, :integer
     field :access_token_secret, :string

因为我们调整了 :from_id 的类型,可以预计,mix test 一定会报错。

不过修复起来也很简单,打开 accounts_test.exs 文件,将 from_id 从字符串改为数值:

test/tweet_bot/accounts/accounts_test.exs
-    @valid_attrs %{access_token: "some access_token", from_id: "some from_id"}
-    @update_attrs %{access_token: "some updated access_token", from_id: "some updated from_id"}
+    @valid_attrs %{access_token: "some access_token", from_id: 1}
+    @update_attrs %{access_token: "some updated access_token", from_id: 2}
     @invalid_attrs %{access_token: nil, from_id: nil}

     def user_fixture(attrs \\ %{}) do
@@ -32,7 +32,7 @@ defmodule TweetBot.AccountsTest do
     test "create_user/1 with valid data creates a user" do
       assert {:ok, %User{} = user} = Accounts.create_user(@valid_attrs)
       assert user.access_token == "some access_token"
-      assert user.from_id == "some from_id"
+      assert user.from_id == 1
     end

     test "create_user/1 with invalid data returns error changeset" do
@@ -44,7 +44,7 @@ defmodule TweetBot.AccountsTest do
       assert {:ok, user} = Accounts.update_user(user, @update_attrs)
       assert %User{} = user
       assert user.access_token == "some updated access_token"
-      assert user.from_id == "some updated from_id"
+      assert user.from_id == 2
     end

再运行 mix test,悉数通过。

优化代码

在添加上述 Plug 后,我们可以对 twitter_controller.ex 中的 index 动作做进一步优化:

lib/tweet_bot_web/controllers/twitter_controller.ex
   plug :find_user

-  def index(conn, %{"message" => %{"from" => %{"id" => from_id}, "text" => text}}) do
-    case text do
-      "/start" ->
-        token = ExTwitter.request_token(URI.encode_www_form(Routers.auth_url(conn, :callback) <> "?from_id=#{from_id}"))
-        {:ok, authenticate_url} = ExTwitter.authenticate_url(token.oauth_token)
-        sendMessage(from_id, "请点击链接登录您的 Twitter 账号进行授权:<a href='" <> authenticate_url <> "'>登录 Twitter</a>", parse_mode: "HTML")
-      _ -> sendMessage(from_id, "你好")
-    end
+  def index(conn, %{"message" => %{"text" => "/start"}}) do
+    sendMessage(conn.assigns.current_user, "已授权,请直接发送消息")
+    json(conn, %{})
+  end
+
+  def index(conn, _) do
     json(conn, %{})
   end

是了,这里展示的正是模式匹配的优美。我们可以在一个 controller 文件里写多个同名 index 动作,每个动作处理不同的参数 - 不必在一个巨大的 index 动作中又是 if else 又是 case do 了。