summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorHubert Chathi <hubert@uhoreg.ca>2020-08-15 04:19:50 -0400
committerHubert Chathi <hubert@uhoreg.ca>2020-08-15 04:28:16 -0400
commit988bf8af92c6f253ae729a044178a19c76648a71 (patch)
tree04df4a99c2a26a66fd7927a42a25163a6b685171 /lib
parents/Any/any/ in typespecs (diff)
allow body to be iolist or a filename, and add media upload/download
Diffstat (limited to 'lib')
-rw-r--r--lib/polyjuice/client.ex31
-rw-r--r--lib/polyjuice/client/endpoint.ex13
-rw-r--r--lib/polyjuice/client/endpoint/get_media_download.ex133
-rw-r--r--lib/polyjuice/client/endpoint/post_join.ex2
-rw-r--r--lib/polyjuice/client/endpoint/post_login.ex2
-rw-r--r--lib/polyjuice/client/endpoint/post_media_upload.ex75
-rw-r--r--lib/polyjuice/client/endpoint/post_user_filter.ex2
-rw-r--r--lib/polyjuice/client/endpoint/put_rooms_send.ex2
-rw-r--r--lib/polyjuice/client/endpoint/put_rooms_state.ex2
-rw-r--r--lib/polyjuice/client/media.ex99
-rw-r--r--lib/polyjuice/client/sync.ex3
11 files changed, 348 insertions, 16 deletions
diff --git a/lib/polyjuice/client.ex b/lib/polyjuice/client.ex
index dfe8775..0d3bc75 100644
--- a/lib/polyjuice/client.ex
+++ b/lib/polyjuice/client.ex
@@ -64,6 +64,8 @@ defmodule Polyjuice.Client do
def prefix_r0, do: "_matrix/client/r0"
@doc "The unstable client URL prefix"
def prefix_unstable, do: "_matrix/client/unstable"
+ @doc "The r0 media URL prefix"
+ def prefix_media_r0, do: "_matrix/media/r0"
defprotocol API do
@moduledoc """
@@ -122,10 +124,11 @@ defmodule Polyjuice.Client do
headers: headers,
url: url,
body: body,
- auth_required: auth_required
+ auth_required: auth_required,
+ stream_response: stream_response
} = Polyjuice.Client.Endpoint.Proto.http_spec(endpoint, base_url)
- Logger.debug("calling #{url}")
+ Logger.debug("calling #{method} #{url}")
if auth_required and access_token == nil do
{:error, :auth_required}
@@ -141,16 +144,21 @@ defmodule Polyjuice.Client do
headers
end,
body,
- [:with_body]
+ []
) do
- {:ok, status_code, resp_headers, body} ->
+ {: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,
- body
+ if stream_response do
+ Polyjuice.Client.hackney_response_stream(client_ref)
+ else
+ {:ok, body} = :hackney.body(client_ref)
+ body
+ end
)
err ->
@@ -169,6 +177,19 @@ defmodule Polyjuice.Client do
end
end
+ @doc false
+ def hackney_response_stream(client_ref) do
+ Stream.unfold(
+ client_ref,
+ fn client_ref ->
+ case :hackney.stream_body(client_ref) do
+ {:ok, data} -> {data, client_ref}
+ _ -> nil
+ end
+ end
+ )
+ end
+
@doc """
Synchronize messages from the server.
diff --git a/lib/polyjuice/client/endpoint.ex b/lib/polyjuice/client/endpoint.ex
index 41c0a86..b344743 100644
--- a/lib/polyjuice/client/endpoint.ex
+++ b/lib/polyjuice/client/endpoint.ex
@@ -22,16 +22,18 @@ defmodule Polyjuice.Client.Endpoint do
- `method` is the HTTP verb
- `headers` is a list of the HTTP headers
- `url` is the URL to call
- - `body` is the HTTP body (if any)
- - `transform` is a function to transform the result (status code, headers, content) to a return value
+ - `body` is the HTTP body (if any) as a binary, or `{:file, filename}`
+ - `transform` is a function to transform the result (status code, headers,
+ content) to a return value
- `auth_required` indicates whether the end point requires authentication
"""
@type t :: %__MODULE__{
method: atom,
headers: [{String.t(), String.t()}],
url: String.t(),
- body: String.t(),
- auth_required: true | false
+ body: iodata() | {:file, String.t()} | {Enumerable.t(), integer},
+ auth_required: boolean,
+ stream_response: boolean
}
@enforce_keys [:method, :headers, :url]
defstruct [
@@ -39,7 +41,8 @@ defmodule Polyjuice.Client.Endpoint do
:headers,
:url,
body: "",
- auth_required: true
+ auth_required: true,
+ stream_response: false
]
end
diff --git a/lib/polyjuice/client/endpoint/get_media_download.ex b/lib/polyjuice/client/endpoint/get_media_download.ex
new file mode 100644
index 0000000..25250f9
--- /dev/null
+++ b/lib/polyjuice/client/endpoint/get_media_download.ex
@@ -0,0 +1,133 @@
+# 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.Endpoint.GetMediaDownload do
+ @moduledoc """
+ Download from the media repository.
+
+ https://matrix.org/docs/spec/client_server/latest#get-matrix-media-r0-download-servername-mediaid
+ https://matrix.org/docs/spec/client_server/latest#get-matrix-media-r0-download-servername-mediaid-filename
+ """
+
+ @type t :: %__MODULE__{
+ url: String.t() | URI.t() | {String.t(), String.t()},
+ allow_remote: boolean,
+ filename: String.t() | nil
+ }
+
+ @enforce_keys [:url]
+ defstruct [
+ :url,
+ :filename,
+ allow_remote: true
+ ]
+
+ defimpl Polyjuice.Client.Endpoint.Proto do
+ def http_spec(
+ %{
+ url: mxc_url,
+ allow_remote: allow_remote,
+ filename: filename
+ },
+ base_url
+ ) do
+ e = &URI.encode_www_form/1
+
+ {server_name, media_id} =
+ case mxc_url do
+ _ when is_binary(mxc_url) ->
+ %{
+ scheme: "mxc",
+ host: host,
+ path: path
+ } = URI.parse(mxc_url)
+
+ {host, String.trim_leading(path, "/")}
+
+ %URI{
+ scheme: "mxc",
+ host: host,
+ path: path
+ } ->
+ {host, String.trim_leading(path, "/")}
+
+ {_, _} ->
+ mxc_url
+ end
+
+ filename_part = if is_binary(filename), do: "/" <> filename, else: ""
+
+ url = %{
+ URI.merge(
+ base_url,
+ "#{Polyjuice.Client.prefix_media_r0()}/download/#{e.(server_name)}/#{e.(media_id)}#{
+ filename_part
+ }"
+ )
+ | query: if(allow_remote, do: nil, else: "allow_remote=false")
+ }
+
+ %Polyjuice.Client.Endpoint.HttpSpec{
+ method: :get,
+ headers: [
+ {"Accept", "*/*"}
+ ],
+ url: to_string(url),
+ stream_response: true
+ }
+ end
+
+ def transform_http_result(req, status_code, resp_headers, body) do
+ if status_code == 200 do
+ {_, content_type} =
+ Enum.find(
+ resp_headers,
+ {nil, "application/octet-stream"},
+ fn {name, _} -> String.downcase(name, :ascii) == "content-type" end
+ )
+
+ filename =
+ Enum.find_value(
+ resp_headers,
+ fn {name, value} ->
+ if String.downcase(name, :ascii) == "content-disposition" do
+ Regex.split(~r";\s*", value)
+ |> Enum.find_value(fn directive ->
+ # FIXME: also handle "filename*"
+ with [d_name, quoted_name] <- String.split(directive, "=", parts: 2),
+ "filename" <- String.downcase(d_name) do
+ case Regex.run(~r/"(.*)"/, quoted_name) do
+ [_, name] -> Regex.replace(~r/\\(.)/, name, "\\1")
+ nil -> quoted_name
+ end
+ # do some basic sanitizing
+ |> (&Regex.replace(~r/\0/, &1, "")).()
+ |> Path.basename()
+ |> (&if(&1 == "", do: nil, else: &1)).()
+ else
+ _ ->
+ nil
+ end
+ end)
+ end
+ end
+ )
+
+ {:ok, filename, content_type, body}
+ else
+ Polyjuice.Client.Endpoint.parse_response(req, status_code, resp_headers, Enum.join(body))
+ end
+ end
+ end
+end
diff --git a/lib/polyjuice/client/endpoint/post_join.ex b/lib/polyjuice/client/endpoint/post_join.ex
index 00d3669..1ddc922 100644
--- a/lib/polyjuice/client/endpoint/post_join.ex
+++ b/lib/polyjuice/client/endpoint/post_join.ex
@@ -44,7 +44,7 @@ defmodule Polyjuice.Client.Endpoint.PostJoin do
e = &URI.encode_www_form/1
body =
- Jason.encode!(
+ Jason.encode_to_iodata!(
if third_party_signed do
%{"third_party_signed" => third_party_signed}
else
diff --git a/lib/polyjuice/client/endpoint/post_login.ex b/lib/polyjuice/client/endpoint/post_login.ex
index ce093f0..0414cfc 100644
--- a/lib/polyjuice/client/endpoint/post_login.ex
+++ b/lib/polyjuice/client/endpoint/post_login.ex
@@ -65,7 +65,7 @@ defmodule Polyjuice.Client.Endpoint.PostLogin do
]
|> Enum.concat()
|> Map.new()
- |> Jason.encode!()
+ |> Jason.encode_to_iodata!()
%Polyjuice.Client.Endpoint.HttpSpec{
method: :post,
diff --git a/lib/polyjuice/client/endpoint/post_media_upload.ex b/lib/polyjuice/client/endpoint/post_media_upload.ex
new file mode 100644
index 0000000..27fca1a
--- /dev/null
+++ b/lib/polyjuice/client/endpoint/post_media_upload.ex
@@ -0,0 +1,75 @@
+# 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.Endpoint.PostMediaUpload do
+ @moduledoc """
+ Upload to the media repository.
+
+ https://matrix.org/docs/spec/client_server/latest#post-matrix-media-r0-upload
+ """
+
+ @type t :: %__MODULE__{
+ filename: String.t(),
+ data: binary | {:file, String.t()},
+ mimetype: String.t()
+ }
+
+ @enforce_keys [:filename, :data, :mimetype]
+ defstruct [
+ :filename,
+ :data,
+ mimetype: "application/octet-stream"
+ ]
+
+ defimpl Polyjuice.Client.Endpoint.Proto do
+ def http_spec(
+ %{
+ filename: filename,
+ data: data,
+ mimetype: mimetype
+ },
+ base_url
+ ) do
+ e = &URI.encode_www_form/1
+
+ url = %{
+ URI.merge(
+ base_url,
+ "#{Polyjuice.Client.prefix_media_r0()}/upload"
+ )
+ | query: "filename=#{e.(filename)}"
+ }
+
+ %Polyjuice.Client.Endpoint.HttpSpec{
+ method: :post,
+ headers: [
+ {"Accept", "application/json"},
+ {"Content-Type", mimetype}
+ ],
+ url: to_string(url),
+ body: data
+ }
+ end
+
+ def transform_http_result(req, status_code, resp_headers, body) do
+ Polyjuice.Client.Endpoint.parse_response(req, status_code, resp_headers, body)
+ end
+ end
+
+ defimpl Polyjuice.Client.Endpoint.BodyParser do
+ def parse(_req, parsed) do
+ {:ok, Map.get(parsed, "content_uri")}
+ end
+ end
+end
diff --git a/lib/polyjuice/client/endpoint/post_user_filter.ex b/lib/polyjuice/client/endpoint/post_user_filter.ex
index ceab001..22605aa 100644
--- a/lib/polyjuice/client/endpoint/post_user_filter.ex
+++ b/lib/polyjuice/client/endpoint/post_user_filter.ex
@@ -39,7 +39,7 @@ defmodule Polyjuice.Client.Endpoint.PostUserFilter do
base_url
) do
e = &URI.encode_www_form/1
- body = Jason.encode!(filter)
+ body = Jason.encode_to_iodata!(filter)
%Polyjuice.Client.Endpoint.HttpSpec{
method: :post,
diff --git a/lib/polyjuice/client/endpoint/put_rooms_send.ex b/lib/polyjuice/client/endpoint/put_rooms_send.ex
index 855f346..1bb2ee8 100644
--- a/lib/polyjuice/client/endpoint/put_rooms_send.ex
+++ b/lib/polyjuice/client/endpoint/put_rooms_send.ex
@@ -45,7 +45,7 @@ defmodule Polyjuice.Client.Endpoint.PutRoomsSend do
base_url
) do
e = &URI.encode_www_form/1
- body = Jason.encode!(message)
+ body = Jason.encode_to_iodata!(message)
%Polyjuice.Client.Endpoint.HttpSpec{
method: :put,
diff --git a/lib/polyjuice/client/endpoint/put_rooms_state.ex b/lib/polyjuice/client/endpoint/put_rooms_state.ex
index 74407bf..ca87c42 100644
--- a/lib/polyjuice/client/endpoint/put_rooms_state.ex
+++ b/lib/polyjuice/client/endpoint/put_rooms_state.ex
@@ -45,7 +45,7 @@ defmodule Polyjuice.Client.Endpoint.PutRoomsState do
base_url
) do
e = &URI.encode_www_form/1
- body = Jason.encode!(content)
+ body = Jason.encode_to_iodata!(content)
%Polyjuice.Client.Endpoint.HttpSpec{
method: :put,
diff --git a/lib/polyjuice/client/media.ex b/lib/polyjuice/client/media.ex
new file mode 100644
index 0000000..2a37025
--- /dev/null
+++ b/lib/polyjuice/client/media.ex
@@ -0,0 +1,99 @@
+# 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.Media do
+ @moduledoc """
+ Media-related functions.
+ """
+ require Logger
+
+ @doc """
+ Upload a file to the media repository.
+
+ `data` may be either a binary, indicating the file contents, or a tuple
+ `{:file, path}`, where `path` is a path to the file to be uploaded.
+
+ `opts` is a keyword list of options. Recognized options are:
+ - `filename:` the filename to use for the uploaded file. This is required
+ when `data` is a binary. If not specified, and `data` is of the form
+ `{:file, path}`, then the filename defaults to the basename of the path.
+ - `mimetype:` the mimetype to use for the uploaded file. Defaults to
+ `application/octet-stream`.
+ """
+ @spec upload(
+ client_api :: Polyjuice.Client.API.t(),
+ data :: binary | {:file, String.t()},
+ opts :: Keyword.t()
+ ) :: {:ok, String.t()} | any
+ def upload(client_api, data, opts \\ []) do
+ filename =
+ Keyword.get_lazy(opts, :filename, fn ->
+ case data do
+ {:file, name} ->
+ Path.basename(name)
+ end
+ end)
+
+ mimetype = Keyword.get(opts, :mimetype, "application/octet-stream")
+
+ Polyjuice.Client.API.call(
+ client_api,
+ %Polyjuice.Client.Endpoint.PostMediaUpload{
+ filename: filename,
+ data: data,
+ mimetype: mimetype
+ }
+ )
+ end
+
+ @doc """
+ Download a file from the media repository.
+
+ `url` may be either a binary or a `URI` (giving an `mxc://` URI to download),
+ or a `{server_name, media_id}` tuple. `filename` is an (optional) filename to
+ request that the server use, and `allow_remote` indicates whether the server
+ should fetch media from remote servers if necessary (defaults to true).
+
+ If successful, returns a tuple of the form `{:ok, filename, content_type, body}`,
+ where `body` is a `Stream` such that `Enum.join(body)` is the file contents.
+ """
+ @spec download(
+ client_api :: Polyjuice.Client.API.t(),
+ url :: String.t() | URI.t() | {String.t(), String.t()},
+ filename :: String.t(),
+ allow_remote :: boolean
+ ) :: {:ok, String.t(), String.t(), Stream.t()} | any
+ def download(client_api, url, filename \\ nil, allow_remote \\ true) do
+ Polyjuice.Client.API.call(
+ client_api,
+ %Polyjuice.Client.Endpoint.GetMediaDownload{
+ url: url,
+ allow_remote:
+ cond do
+ is_boolean(filename) -> filename
+ is_boolean(allow_remote) -> allow_remote
+ end,
+ filename:
+ cond do
+ is_boolean(filename) -> nil
+ true -> filename
+ end
+ }
+ )
+ end
+
+ # 13.8.2.4 GET /_matrix/media/r0/thumbnail/{serverName}/{mediaId}
+ # 13.8.2.5 GET /_matrix/media/r0/preview_url
+ # 13.8.2.6 GET /_matrix/media/r0/config
+end
diff --git a/lib/polyjuice/client/sync.ex b/lib/polyjuice/client/sync.ex
index d56099b..c898a6e 100644
--- a/lib/polyjuice/client/sync.ex
+++ b/lib/polyjuice/client/sync.ex
@@ -165,7 +165,8 @@ defmodule Polyjuice.Client.Sync do
case :hackney.send_request(
state.conn_ref,
- {if(state.test, do: :put, else: :post), path, headers, Jason.encode!(state.set_filter)}
+ {if(state.test, do: :put, else: :post), path, headers,
+ Jason.encode_to_iodata!(state.set_filter)}
) do
{:ok, status_code, _resp_headers, client_ref} ->
case status_code do