summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHubert Chathi <hubert@uhoreg.ca>2020-08-20 22:20:32 -0400
committerHubert Chathi <hubert@uhoreg.ca>2020-08-20 22:20:32 -0400
commitf471fc2ced03812f74eaeedec5eb395ac7c27b27 (patch)
tree5d7c90a4eb7c24eee3427d6d845d689ee748fc48
parentdoc improvements (diff)
add lower-level client, acting like the old client
-rw-r--r--lib/polyjuice/client.ex55
-rw-r--r--lib/polyjuice/client/low_level.ex174
-rw-r--r--test/polyjuice/client/low_level_test.exs174
3 files changed, 376 insertions, 27 deletions
diff --git a/lib/polyjuice/client.ex b/lib/polyjuice/client.ex
index aee93fa..81415d1 100644
--- a/lib/polyjuice/client.ex
+++ b/lib/polyjuice/client.ex
@@ -291,6 +291,33 @@ defmodule Polyjuice.Client do
)
end
+ def make_login_identifier(identifier) do
+ case identifier do
+ x when is_binary(x) ->
+ %{
+ "type" => "m.id.user",
+ "user" => identifier
+ }
+
+ {:email, address} ->
+ %{
+ "type" => "m.id.thirdparty",
+ "medium" => "email",
+ "address" => address
+ }
+
+ {:phone, country, phone} ->
+ %{
+ "type" => "m.id.phone",
+ "country" => country,
+ "phone" => phone
+ }
+
+ x when is_map(x) ->
+ identifier
+ end
+ end
+
@doc """
Log in with a password.
@@ -313,39 +340,13 @@ defmodule Polyjuice.Client do
def log_in_with_password(client, identifier, password, opts \\ [])
when (is_binary(identifier) or is_tuple(identifier) or is_map(identifier)) and
is_binary(password) and is_list(opts) do
- id =
- case identifier do
- x when is_binary(x) ->
- %{
- "type" => "m.id.user",
- "user" => identifier
- }
-
- {:email, address} ->
- %{
- "type" => "m.id.thirdparty",
- "medium" => "email",
- "address" => address
- }
-
- {:phone, country, phone} ->
- %{
- "type" => "m.id.phone",
- "country" => country,
- "phone" => phone
- }
-
- x when is_map(x) ->
- identifier
- end
-
ret =
{:ok, %{"access_token" => access_token, "user_id" => user_id, "device_id" => device_id}} =
Polyjuice.Client.API.call(
client,
%Polyjuice.Client.Endpoint.PostLogin{
type: "m.login.password",
- identifier: id,
+ identifier: make_login_identifier(identifier),
password: password,
device_id: Keyword.get(opts, :device_id),
initial_device_display_name: Keyword.get(opts, :initial_device_display_name)
diff --git a/lib/polyjuice/client/low_level.ex b/lib/polyjuice/client/low_level.ex
new file mode 100644
index 0000000..2b22c6c
--- /dev/null
+++ b/lib/polyjuice/client/low_level.ex
@@ -0,0 +1,174 @@
+# Copyright 2020 Hubert Chathi <hubert@uhoreg.ca>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+defmodule Polyjuice.Client.LowLevel do
+ @moduledoc """
+ A lower-level client than `Polyjuice.Client`.
+
+ The application is responsible for taking care of all the details. For
+ example, after logging in, the application must create a new struct using the
+ access token provided.
+ """
+
+ require Logger
+
+ @type t :: %__MODULE__{
+ base_url: String.t(),
+ access_token: String.t(),
+ user_id: String.t(),
+ device_id: String.t(),
+ storage: Polyjuice.Client.Storage.t(),
+ test: boolean
+ }
+
+ @enforce_keys [:base_url]
+ defstruct [
+ :base_url,
+ :access_token,
+ :user_id,
+ :device_id,
+ :storage,
+ test: false
+ ]
+
+ defimpl Polyjuice.Client.API do
+ def call(%{base_url: base_url, access_token: access_token, test: test}, endpoint) do
+ %Polyjuice.Client.Endpoint.HttpSpec{
+ method: method,
+ headers: headers,
+ url: url,
+ body: body,
+ auth_required: auth_required,
+ stream_response: stream_response
+ } = Polyjuice.Client.Endpoint.Proto.http_spec(endpoint, base_url)
+
+ Logger.debug("calling #{method} #{url}")
+
+ if auth_required and access_token == nil do
+ {:error, :auth_required}
+ else
+ case :hackney.request(
+ # mod_esi doesn't like POST requests to a sub-path, so change POST
+ # to PUT when running tests
+ if(method == :post and test, do: :put, else: method),
+ url,
+ if access_token do
+ [{"Authorization", "Bearer #{access_token}"} | headers]
+ else
+ headers
+ end,
+ body,
+ []
+ ) do
+ {:ok, status_code, resp_headers, client_ref} ->
+ Logger.debug("status code #{status_code}")
+
+ Polyjuice.Client.Endpoint.Proto.transform_http_result(
+ endpoint,
+ status_code,
+ resp_headers,
+ if stream_response do
+ Polyjuice.Client.hackney_response_stream(client_ref)
+ else
+ {:ok, body} = :hackney.body(client_ref)
+ body
+ end
+ )
+
+ err ->
+ # anything else is an error -- return as-is
+ err
+ end
+ end
+ end
+
+ def transaction_id(_) do
+ "#{Node.self()}_#{:erlang.system_time(:millisecond)}_#{:erlang.unique_integer()}"
+ end
+
+ def sync_child_spec(_client, _listener, _opts \\ []) do
+ # do nothing
+ end
+ end
+
+ @doc """
+ Create a client.
+
+ `opts` may contain:
+ - `access_token`: (required to make calls that require authorization) the
+ access token to use.
+ - `user_id`: (required by some endpoints) the ID of the user
+ - `device_id`: the device ID
+ - `storage`: (required by sync) the storage backend to use (see
+ `Polyjuice.Client.Storage`)
+ """
+ @spec create(base_url :: String.t(), opts :: Keyword.t()) :: t()
+ def create(base_url, opts \\ []) when is_binary(base_url) do
+ %__MODULE__{
+ base_url:
+ if(String.ends_with?(base_url, "/"), do: base_url, else: base_url <> "/")
+ |> URI.parse(),
+ access_token: Keyword.get(opts, :access_token),
+ user_id: Keyword.get(opts, :user_id),
+ device_id: Keyword.get(opts, :device_id),
+ storage: Keyword.get(opts, :storage),
+ test: Keyword.get(opts, :test, false)
+ }
+ end
+
+ @doc """
+ Log in with a password.
+
+ `identifier` may be a single string (in which case it represents a username
+ -- either just the localpart or the full MXID), a tuple of the form
+ `{:email, "email@address"}`, a tuple of the form `{:phone, "country_code",
+ "phone_number"}`, or a map that is passed directly to the login endpoint.
+
+ `opts` is a keyword list of options:
+
+ - `device_id:` (string) the device ID to use
+ - `initial_device_display_name:` (string) the display name to use for the device
+ """
+ @spec log_in_with_password(
+ client :: Polyjuice.Client.LowLevel.t(),
+ identifier :: String.t() | tuple() | map(),
+ password :: String.t(),
+ opts :: list()
+ ) :: {:ok, map()} | any
+ def log_in_with_password(client, identifier, password, opts \\ [])
+ when (is_binary(identifier) or is_tuple(identifier) or is_map(identifier)) and
+ is_binary(password) and is_list(opts) do
+ Polyjuice.Client.API.call(
+ client,
+ %Polyjuice.Client.Endpoint.PostLogin{
+ type: "m.login.password",
+ identifier: Polyjuice.Client.make_login_identifier(identifier),
+ password: password,
+ device_id: Keyword.get(opts, :device_id),
+ initial_device_display_name: Keyword.get(opts, :initial_device_display_name)
+ }
+ )
+ end
+
+ @doc """
+ Log out an existing session.
+ """
+ @spec log_out(client :: Polyjuice.Client.LowLevel.t()) :: {:ok} | any
+ def log_out(client) do
+ Polyjuice.Client.API.call(
+ client,
+ %Polyjuice.Client.Endpoint.PostLogout{}
+ )
+ end
+end
diff --git a/test/polyjuice/client/low_level_test.exs b/test/polyjuice/client/low_level_test.exs
new file mode 100644
index 0000000..a4409f3
--- /dev/null
+++ b/test/polyjuice/client/low_level_test.exs
@@ -0,0 +1,174 @@
+# Copyright 2020 Hubert Chathi <hubert@uhoreg.ca>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+defmodule Polyjuice.Client.LowLevelTest do
+ use ExUnit.Case
+
+ test "transaction_id is unique" do
+ client = Polyjuice.Client.LowLevel.create("http://localhost:8008")
+
+ # the best that we can do is test that two calls to transaction_id return
+ # different values
+ assert Polyjuice.Client.API.transaction_id(client) !=
+ Polyjuice.Client.API.transaction_id(client)
+ end
+
+ test "call" do
+ {:ok, tmpdir} = TestUtil.mktmpdir("sync-")
+
+ try do
+ tmpdir_charlist = to_charlist(tmpdir)
+
+ :inets.start()
+
+ {:ok, httpd_pid} =
+ :inets.start(
+ :httpd,
+ port: 0,
+ server_name: 'sync.test',
+ server_root: tmpdir_charlist,
+ document_root: tmpdir_charlist,
+ bind_address: {127, 0, 0, 1},
+ modules: [:mod_esi],
+ erl_script_alias: {'', [Polyjuice.ClientTest.Httpd]}
+ )
+
+ port = :httpd.info(httpd_pid) |> Keyword.fetch!(:port)
+
+ client =
+ Polyjuice.Client.LowLevel.create(
+ "http://127.0.0.1:#{port}/Elixir.Polyjuice.ClientTest.Httpd/",
+ access_token: "an_access_token",
+ user_id: "@alice:example.org",
+ test: true
+ )
+
+ # binary body
+ assert Polyjuice.Client.API.call(
+ client,
+ %DummyEndpoint{
+ http_spec: %Polyjuice.Client.Endpoint.HttpSpec{
+ method: :put,
+ url: "foo",
+ headers: [],
+ body: "foobar",
+ auth_required: false
+ }
+ }
+ ) == {:ok, %{"foo" => "bar"}}
+
+ # iolist body
+ assert Polyjuice.Client.API.call(
+ client,
+ %DummyEndpoint{
+ http_spec: %Polyjuice.Client.Endpoint.HttpSpec{
+ method: :put,
+ url: "foo",
+ headers: [],
+ body: [?f, ["oo"], ["b", [?a | "r"]]],
+ auth_required: false
+ }
+ }
+ ) == {:ok, %{"foo" => "bar"}}
+
+ :inets.stop(:httpd, httpd_pid)
+ after
+ File.rm_rf(tmpdir)
+ end
+ end
+
+ test "login" do
+ {:ok, tmpdir} = TestUtil.mktmpdir("sync-")
+
+ try do
+ tmpdir_charlist = to_charlist(tmpdir)
+
+ :inets.start()
+
+ {:ok, httpd_pid} =
+ :inets.start(
+ :httpd,
+ port: 0,
+ server_name: 'sync.test',
+ server_root: tmpdir_charlist,
+ document_root: tmpdir_charlist,
+ bind_address: {127, 0, 0, 1},
+ modules: [:mod_esi],
+ erl_script_alias: {'', [Polyjuice.ClientTest.Httpd]}
+ )
+
+ port = :httpd.info(httpd_pid) |> Keyword.fetch!(:port)
+
+ client =
+ Polyjuice.Client.LowLevel.create(
+ "http://127.0.0.1:#{port}/Elixir.Polyjuice.ClientTest.Httpd/",
+ test: true
+ )
+
+ assert Polyjuice.Client.LowLevel.log_in_with_password(
+ client,
+ "@alice:example.org",
+ "password"
+ ) ==
+ {:ok,
+ %{
+ "access_token" => "m.id.user_login",
+ "device_id" => "foo",
+ "user_id" => "@alice:example.org"
+ }}
+
+ assert Polyjuice.Client.LowLevel.log_in_with_password(
+ client,
+ {:email, "user@example.com"},
+ "password"
+ ) ==
+ {:ok,
+ %{
+ "access_token" => "m.id.thirdparty_login",
+ "device_id" => "foo",
+ "user_id" => "@alice:example.org"
+ }}
+
+ assert Polyjuice.Client.LowLevel.log_in_with_password(
+ client,
+ {:phone, "CA", "1234567890"},
+ "password"
+ ) ==
+ {:ok,
+ %{
+ "access_token" => "m.id.phone_login",
+ "device_id" => "foo",
+ "user_id" => "@alice:example.org"
+ }}
+
+ assert Polyjuice.Client.LowLevel.log_in_with_password(
+ client,
+ %{
+ "type" => "ca.uhoreg.foo"
+ },
+ "password"
+ ) ==
+ {:ok,
+ %{
+ "access_token" => "ca.uhoreg.foo_login",
+ "device_id" => "foo",
+ "user_id" => "@alice:example.org"
+ }}
+
+ :inets.stop(:httpd, httpd_pid)
+ after
+ File.rm_rf(tmpdir)
+ end
+ end
+end