Commit 407d202f authored by Pierre de Lacroix's avatar Pierre de Lacroix
Browse files

Merge branch 'choose_endpoint' into 'master'

add endpoint handled by the libraryCloses #23

See merge request !12
parents 59da3d1a 8a05e93e
Pipeline #223699650 passed with stages
in 5 minutes and 35 seconds
......@@ -26,8 +26,8 @@ See [documentation](https://hexdocs.pm/matrix_app_service).
* [x] User query
* [x] Room query
* [x] Transaction push
* [x] Allow usage of own Phoenix endpoint
* [ ] Third party indications
* [ ] Allow usage of own Phoenix endpoint
* [ ] Bridge functionalities: relations between local and remote rooms
* [ ] Bridge functionalities: handling of remote sessions
* [ ] Conveniences for building bots
......@@ -23,6 +23,9 @@ config :logger, :console,
# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason
config :matrix_app_service,
internal_supervisor: true
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
......@@ -10,7 +10,10 @@ config :matrix_app_service, MatrixAppServiceWeb.Endpoint,
config :logger, level: :warn
config :matrix_app_service,
internal_supervisor: true,
transaction_adapter: MatrixAppService.TestTransactionAdapter,
room_adapter: MatrixAppService.TestRoomAdapter,
user_adapter: MatrixAppService.TestUserAdapter,
homeserver_token: "homeserver token"
homeserver_token: "homeserver token",
access_token: "homeserver token",
base_url: "http://homeserver"
......@@ -9,23 +9,13 @@ defmodule MatrixAppService do
```
defp deps do
[...]
# ...,
{:matrix_app_service, "~> 0.1.0"}
end
```
## Usage
### Inject routes
In your Phoenix Router:
```
require MatrixAppServiceWeb.Router
MatrixAppServiceWeb.Router.routes()
```
### Write adapters
Create one or multiple modules that implement the following modules:
......@@ -59,10 +49,66 @@ defmodule MatrixAppService do
end
```
### Write configuration
### Configure
#### Option 1: Using the :matrix_app_service supervision tree
Configuration:
```
config :matrix_app_service,
internal_supervisor: true,
transaction_adapter: App.Matrix.Transaction,
room_adapter: App.Matrix.Room,
user_adapter: App.Matrix.User,
path: "/matrix"
base_url: "http://synapse:8008",
access_token: "access token",
homeserver_token: "homeserver token"
```
#### Option 2: Using the :matrix_app_service endpoint in your own supervision tree
In your application module:
```
children = [
# ...,
{MatrixAppServiceWeb.Endpoint, app_service_config()}
]
# ...
defp app_service_config(), do: Application.get_env(:app, :app_service)
```
Configuration:
```
config :app, :app_service,
transaction_adapter: App.Matrix.Transaction,
room_adapter: App.Matrix.Room,
user_adapter: App.Matrix.User,
path: "/matrix"
base_url: "http://synapse:8008",
access_token: "access token",
homeserver_token: "homeserver token"
```
#### Option 3: Using your own endpoint
In your Phoenix Router:
```
use MatrixAppServiceWeb.Routes
MatrixAppServiceWeb.Routes.routes(Application.get_env(:app, :app_service))
```
Configuration:
```
config :app, :app_service,
transaction_adapter: App.Matrix.Transaction,
room_adapter: App.Matrix.Room,
user_adapter: App.Matrix.User,
......
......@@ -18,9 +18,10 @@ defmodule MatrixAppService.Application do
]
children =
if Mix.env() == :test do
if start_endpoint?() do
[
MatrixAppServiceWeb.TestEndpoint
# MatrixAppServiceWeb.Endpoint
{MatrixAppServiceWeb.Endpoint, endpoint_config()}
| children
]
else
......@@ -36,10 +37,26 @@ defmodule MatrixAppService.Application do
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
def config_change(changed, _new, removed) do
if Application.get_env(:matrix_app_service, :standalone, false) do
if start_endpoint?() do
MatrixAppServiceWeb.Endpoint.config_change(changed, removed)
end
:ok
end
def start_endpoint?() do
Application.get_env(:matrix_app_service, :internal_supervisor, false)
end
def endpoint_config() do
[
transaction_adapter: Application.fetch_env!(:matrix_app_service, :transaction_adapter),
room_adapter: Application.fetch_env!(:matrix_app_service, :room_adapter),
user_adapter: Application.fetch_env!(:matrix_app_service, :user_adapter),
homeserver_token: Application.fetch_env!(:matrix_app_service, :homeserver_token),
access_token: Application.fetch_env!(:matrix_app_service, :access_token),
base_url: Application.fetch_env!(:matrix_app_service, :base_url),
path: Application.get_env(:matrix_app_service, :path, "/")
]
end
end
......@@ -24,10 +24,21 @@ defmodule MatrixAppService.Client do
Polyjuice.Client.LowLevel.t()
def client(opts \\ []) do
base_url =
Keyword.get(opts, :base_url, Application.fetch_env!(:matrix_app_service, :base_url))
Keyword.get(opts, :base_url) ||
(MatrixAppService.Application.start_endpoint?() &&
MatrixAppServiceWeb.Endpoint.config(:base_url)) ||
Application.get_env(:matrix_app_service, :app_service)[:base_url] ||
raise "MatrixAppService: config key base_url missing"
access_token =
Keyword.get(opts, :access_token) ||
(MatrixAppService.Application.start_endpoint?() &&
MatrixAppServiceWeb.Endpoint.config(:access_token)) ||
Application.get_env(:matrix_app_service, :app_service)[:access_token] ||
raise "MatrixAppService: config key access_token missing"
default_opts = [
access_token: Application.fetch_env!(:matrix_app_service, :access_token),
access_token: access_token,
device_id: "APP_SERVICE"
]
......@@ -82,12 +93,12 @@ defmodule MatrixAppService.Client do
Arguments:
1. `opts`: a keyword list that can contain these keys:
* `:inhibit_login`: true
* `:device_id`: device ID, defaults to `"APP_SERVICE"`
* `:initial_device_display_name`: device name, defaults to
`"ApplicationService"`
* `:kind`: kind of account to register, defaults to `"user"`, can also be
`"guest"`
* `:inhibit_login`: true
* `:device_id`: device ID, defaults to `"APP_SERVICE"`
* `:initial_device_display_name`: device name, defaults to
`"ApplicationService"`
* `:kind`: kind of account to register, defaults to `"user"`, can also be
`"guest"`
2. `client_options`: see `client/1`
"""
@spec register(Polyjuice.Client.LowLevel.register_opts(), client_options()) ::
......
......@@ -17,20 +17,20 @@ defmodule MatrixAppServiceWeb.AuthPlug do
@doc false
@impl Plug
def call(%Plug.Conn{params: %{"access_token" => hs_token}} = conn, _opts) do
config_hs_token = Application.fetch_env!(:matrix_app_service, :homeserver_token)
with ^config_hs_token <- hs_token do
conn
else
_ ->
Logger.warn("Received invalid homeserver token")
respond_error(conn, 403)
end
def call(%Plug.Conn{params: %{"access_token" => access_token}} = conn, homeserver_token)
when access_token == homeserver_token do
conn
end
def call(%Plug.Conn{params: %{"access_token" => _access_token}} = conn, _homeserver_token) do
Logger.warn("Received invalid homeserver token")
respond_error(conn, 403)
end
def call(conn, _opts) do
Logger.warn("No homeserver token provided")
respond_error(conn, 401)
end
......
......@@ -8,7 +8,10 @@ defmodule MatrixAppServiceWeb.V1.RoomController do
https://matrix.org/docs/spec/application_service/r0.1.2#get-matrix-app-v1-rooms-roomalias
"""
def query(conn, %{"room_alias" => room_alias}) do
adapter = Application.fetch_env!(:matrix_app_service, :room_adapter)
adapter =
conn.private[:room_adapter] ||
MatrixAppServiceWeb.Endpoint.config(:room_adapter) ||
raise "MatrixAppService: config key room_adapter missing"
with :ok <- adapter.query_alias(room_alias) do
conn
......
......@@ -19,20 +19,21 @@ defmodule MatrixAppServiceWeb.V1.TransactionController do
# "user_id" => "@alice:matrix.imago.local"}],
# "txn_id" => "269"}
defp create_event(%{
"age" => age,
"content" => content,
"event_id" => event_id,
"origin_server_ts" => origin_server_ts,
"room_id" => room_id,
"sender" => sender,
"state_key" => state_key,
"type" => type,
"unsigned" => unsigned,
"user_id" => user_id
}) do
adapter = Application.fetch_env!(:matrix_app_service, :transaction_adapter)
defp create_event(
%{
"age" => age,
"content" => content,
"event_id" => event_id,
"origin_server_ts" => origin_server_ts,
"room_id" => room_id,
"sender" => sender,
"state_key" => state_key,
"type" => type,
"unsigned" => unsigned,
"user_id" => user_id
},
adapter
) do
event = %MatrixAppService.Event{
age: age,
content: content,
......@@ -49,19 +50,20 @@ defmodule MatrixAppServiceWeb.V1.TransactionController do
adapter.new_event(event)
end
defp create_event(%{
"age" => age,
"content" => content,
"event_id" => event_id,
"origin_server_ts" => origin_server_ts,
"room_id" => room_id,
"sender" => sender,
"type" => type,
"unsigned" => unsigned,
"user_id" => user_id
}) do
adapter = Application.fetch_env!(:matrix_app_service, :transaction_adapter)
defp create_event(
%{
"age" => age,
"content" => content,
"event_id" => event_id,
"origin_server_ts" => origin_server_ts,
"room_id" => room_id,
"sender" => sender,
"type" => type,
"unsigned" => unsigned,
"user_id" => user_id
},
adapter
) do
event = %MatrixAppService.Event{
age: age,
content: content,
......@@ -82,7 +84,13 @@ defmodule MatrixAppServiceWeb.V1.TransactionController do
https://matrix.org/docs/spec/application_service/r0.1.2#put-matrix-app-v1-transactions-txnid
"""
def push(conn, %{"events" => events}) do
Enum.each(events, &create_event(&1))
adapter =
conn.private[:transaction_adapter] ||
MatrixAppServiceWeb.Endpoint.config(:transaction_adapter) ||
raise "MatrixAppService: config key room_adapter missing"
Enum.each(events, &create_event(&1, adapter))
send_resp(conn, 200, "{}")
end
end
......@@ -8,7 +8,10 @@ defmodule MatrixAppServiceWeb.V1.UserController do
https://matrix.org/docs/spec/application_service/r0.1.2#get-matrix-app-v1-users-userid
"""
def query(conn, %{"user_id" => user_id}) do
adapter = Application.fetch_env!(:matrix_app_service, :user_adapter)
adapter =
conn.private[:user_adapter] ||
MatrixAppServiceWeb.Endpoint.config(:user_adapter) ||
raise "MatrixAppService: config key user_adapter missing"
with :ok <- adapter.query_user(user_id) do
conn
......
defmodule MatrixAppServiceWeb.TestEndpoint do
defmodule MatrixAppServiceWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :matrix_app_service
require Logger
def init(:supervisor, config) do
Logger.error(inspect(config))
{:ok, config}
end
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
@session_options [
store: :cookie,
key: "_matrix_app_service_key",
signing_salt: "zE7AHynD"
]
# @session_options [
# store: :cookie,
# key: "_matrix_app_service_key",
# signing_salt: "zE7AHynD"
# ]
plug Plug.RequestId
# plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
......@@ -20,6 +26,6 @@ defmodule MatrixAppServiceWeb.TestEndpoint do
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug MatrixAppServiceWeb.TestRouter
# plug Plug.Session, @session_options
plug MatrixAppServiceWeb.Router
end
defmodule MatrixAppServiceWeb.Router do
@moduledoc """
Provides the Matrix Application Service API routes.
use Phoenix.Router
use MatrixAppServiceWeb.Routes
https://matrix.org/docs/spec/application_service/r0.1.2
"""
import Plug.Conn
import Phoenix.Controller
@doc """
This macro injects the API routes in a Phoenix router.
"""
defmacro routes() do
quote do
pipeline :matrix_api do
plug :accepts, ["json"]
plug MatrixAppServiceWeb.AuthPlug
end
path = Application.compile_env(:matrix_app_service, :path, "/")
scope path, MatrixAppServiceWeb.V1, as: :matrix do
pipe_through :matrix_api
put "/transactions/:txn_id", TransactionController, :push
get "/users/:user_id", UserController, :query
get "/rooms/:room_alias", RoomController, :query
get "/thirdparty/protocol/:protocol", ThirdPartyController, :query_protocol
get "/thirdparty/user/:protocol", ThirdPartyController, :query_users
get "/thirdparty/location/:protocol", ThirdPartyController, :query_locations
get "/thirdparty/location", ThirdPartyController, :query_location_by_alias
get "/thirdparty/user", ThirdPartyController, :query_user_by_id
end
end
end
# if MatrixAppService.Application.start_endpoint?() do
MatrixAppServiceWeb.Routes.routes(:no_config)
# end
end
defmodule MatrixAppServiceWeb.Routes do
@moduledoc """
Provides the Matrix Application Service API routes.
https://matrix.org/docs/spec/application_service/r0.1.2
"""
@doc """
"""
defmacro __using__(_env) do
quote do
require unquote(__MODULE__)
@matrix_app_services 0
end
end
@doc """
This macro injects the API routes in a Phoenix router.
"""
# defmacro routes(opts \\ [])
defmacro routes(:no_config) do
quote do
pipeline :matrix_app_service do
plug :accepts, ["json"]
end
scope "/", MatrixAppServiceWeb.V1, as: :matrix do
pipe_through :matrix_app_service
put "/transactions/:txn_id", TransactionController, :push
get "/users/:user_id", UserController, :query
get "/rooms/:room_alias", RoomController, :query
get "/thirdparty/protocol/:protocol", ThirdPartyController, :query_protocol
get "/thirdparty/user/:protocol", ThirdPartyController, :query_users
get "/thirdparty/location/:protocol", ThirdPartyController, :query_locations
get "/thirdparty/location", ThirdPartyController, :query_location_by_alias
get "/thirdparty/user", ThirdPartyController, :query_user_by_id
end
end
end
defmacro routes(opts) do
quote bind_quoted: [opts: opts] do
path = Keyword.get(opts, :path, "/")
namespace = Keyword.get(opts, :namespace, :matrix)
homeserver_token = Keyword.fetch!(opts, :homeserver_token)
pipeline_name = String.to_atom("matrix_api_#{@matrix_app_services}")
pipeline pipeline_name do
plug :accepts, ["json"]
plug MatrixAppServiceWeb.SetConfigPlug, opts
plug MatrixAppServiceWeb.AuthPlug, homeserver_token
end
scope path, MatrixAppServiceWeb.V1, as: namespace do
pipe_through pipeline_name
put "/transactions/:txn_id", TransactionController, :push
get "/users/:user_id", UserController, :query
get "/rooms/:room_alias", RoomController, :query
get "/thirdparty/protocol/:protocol", ThirdPartyController, :query_protocol
get "/thirdparty/user/:protocol", ThirdPartyController, :query_users
get "/thirdparty/location/:protocol", ThirdPartyController, :query_locations
get "/thirdparty/location", ThirdPartyController, :query_location_by_alias
get "/thirdparty/user", ThirdPartyController, :query_user_by_id
end
@matrix_app_services @matrix_app_services + 1
end
end
end
defmodule MatrixAppServiceWeb.SetConfigPlug do
@moduledoc """
"""
@behaviour Plug
import Plug.Conn
require Logger
@doc false
@impl Plug
def init(opts) do
opts
end
@doc false
@impl Plug
def call(conn, opts) do
conn
|> put_private(:transaction_adapter, Keyword.fetch!(opts, :transaction_adapter))
|> put_private(:room_adapter, Keyword.fetch!(opts, :room_adapter))
|> put_private(:user_adapter, Keyword.fetch!(opts, :user_adapter))
|> put_private(:homeserver_token, Keyword.fetch!(opts, :homeserver_token))
|> put_private(:access_token, Keyword.fetch!(opts, :access_token))
|> put_private(:base_url, Keyword.fetch!(opts, :base_url))
|> put_private(:path, Keyword.fetch!(opts, :path))
end
end
......@@ -5,19 +5,18 @@ defmodule MatrixAppServiceWeb.AuthPlugTest do
import ExUnit.CaptureLog
test "call with correct acces token returns conn unchanged" do
Application.put_env(:matrix_app_service, :homeserver_token, "test_token")
conn = conn(:get, "/users/2", %{"access_token" => "test_token"})
conn =
conn(:get, "/users/2", %{
"access_token" => "correct token"
})
assert MatrixAppServiceWeb.AuthPlug.call(conn, nil) == conn
assert MatrixAppServiceWeb.AuthPlug.call(conn, "correct token") == conn
end
test "call with incorrect access token halts with error 403" do
Application.put_env(:matrix_app_service, :homeserver_token, "test_token")
conn =
conn(:get, "/users/2", %{"access_token" => "incorrect_token"})
|> MatrixAppServiceWeb.AuthPlug.call(nil)
conn(:get, "/users/2", %{"access_token" => "incorrect token"})
|> MatrixAppServiceWeb.AuthPlug.call("correct token")
assert conn.status == 403
assert conn.private[:phoenix_template] == "403.json"
......@@ -26,17 +25,16 @@ defmodule MatrixAppServiceWeb.AuthPlugTest do
end
test "call with incorrect access token gets logged" do
Application.put_env(:matrix_app_service, :homeserver_token, "test_token")
conn = conn(:get, "/users/2", %{"access_token" => "incorrect_token"})
conn = conn(:get, "/users/2", %{"access_token" => "incorrect token"})
assert capture_log(fn -> MatrixAppServiceWeb.AuthPlug.call(conn, nil) end) =~
assert capture_log(fn -> MatrixAppServiceWeb.AuthPlug.call(conn, "correct token") end) =~
"Received invalid homeserver token"
end
test "call without access token halts with error 401" do
conn =
conn(:get, "/users/2")
|> MatrixAppServiceWeb.AuthPlug.call(nil)
|> MatrixAppServiceWeb.AuthPlug.call("correct token")
assert conn.status == 401
assert conn.private[:phoenix_template] == "401.json"
......@@ -47,7 +45,7 @@ defmodule MatrixAppServiceWeb.AuthPlugTest do
test "call without access token gets logged" do
conn = conn(:get, "user/3")
assert capture_log(fn -> MatrixAppServiceWeb.AuthPlug.call(conn, nil) end) =~
assert capture_log(fn -> MatrixAppServiceWeb.AuthPlug.call(conn, "correct token") end) =~