ID: <%= @post.id %>
++ <%= @post.text %> +
+ +diff --git a/.formatter.exs b/.formatter.exs
index 8a6391c..490a6cc 100644
--- a/.formatter.exs
+++ b/.formatter.exs
@@ -1,4 +1,5 @@
[
+ plugins: [Phoenix.LiveView.HTMLFormatter],
import_deps: [:ecto, :phoenix],
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
subdirectories: ["priv/*/migrations"]
diff --git a/.tool-versions b/.tool-versions
index 705b79d..6b61424 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,2 +1,2 @@
-erlang 24.2.1
-elixir 1.13.2-otp-24
+erlang 24.3.3
+elixir 1.13.4-otp-24
diff --git a/assets/css/app.css b/assets/css/app.css
index 19c2e51..33211db 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -8,27 +8,33 @@
border: 1px solid transparent;
border-radius: 4px;
}
+
.alert-info {
color: #31708f;
background-color: #d9edf7;
border-color: #bce8f1;
}
+
.alert-warning {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc;
}
+
.alert-danger {
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
+
.alert p {
margin-bottom: 0;
}
+
.alert:empty {
display: none;
}
+
.invalid-feedback {
color: #a94442;
display: block;
@@ -46,12 +52,12 @@
transition: opacity 1s ease-out;
}
-.phx-loading{
+.phx-loading {
cursor: wait;
}
.phx-modal {
- opacity: 1!important;
+ opacity: 1 !important;
position: fixed;
z-index: 1;
left: 0;
@@ -59,7 +65,7 @@
width: 100%;
height: 100%;
overflow: auto;
- background-color: rgba(0,0,0,0.4);
+ background-color: rgba(0, 0, 0, 0.4);
}
.phx-modal-content {
@@ -95,26 +101,55 @@
.fade-in {
animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys;
}
+
.fade-out {
animation: 0.2s ease-out 0s normal forwards 1 fade-out-keys;
}
-@keyframes fade-in-scale-keys{
- 0% { scale: 0.95; opacity: 0; }
- 100% { scale: 1.0; opacity: 1; }
+@keyframes fade-in-scale-keys {
+ 0% {
+ scale: 0.95;
+ opacity: 0;
+ }
+
+ 100% {
+ scale: 1.0;
+ opacity: 1;
+ }
}
-@keyframes fade-out-scale-keys{
- 0% { scale: 1.0; opacity: 1; }
- 100% { scale: 0.95; opacity: 0; }
+@keyframes fade-out-scale-keys {
+ 0% {
+ scale: 1.0;
+ opacity: 1;
+ }
+
+ 100% {
+ scale: 0.95;
+ opacity: 0;
+ }
}
-@keyframes fade-in-keys{
- 0% { opacity: 0; }
- 100% { opacity: 1; }
+@keyframes fade-in-keys {
+ 0% {
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 1;
+ }
}
-@keyframes fade-out-keys{
- 0% { opacity: 1; }
- 100% { opacity: 0; }
+@keyframes fade-out-keys {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ }
+}
+
+textarea#post_text {
+ min-height: 300px;
}
diff --git a/config/config.exs b/config/config.exs
index 38ec232..ab013cd 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -7,7 +7,10 @@
# General application configuration
import Config
+minisome_mime_type = "text/minisome"
+
config :minisome,
+ mime_type: minisome_mime_type,
ecto_repos: [Minisome.Storage.Repo]
# Configures the endpoints
@@ -37,6 +40,10 @@ config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
+config :mime, :types, %{
+ minisome_mime_type => ["minisome"]
+}
+
# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason
diff --git a/config/dev.exs b/config/dev.exs
index 5e23fbb..92fa281 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -2,36 +2,9 @@ import Config
# Configure your database
config :minisome, Minisome.Storage.Repo,
- database: Path.expand("../minisome_dev.db", Path.dirname(__ENV__.file)),
pool_size: 5,
show_sensitive_data_on_connection_error: true
-# For development, we disable any cache and enable
-# debugging and code reloading.
-#
-# The watchers configuration can be used to run external
-# watchers to your application. For example, we use it
-# with esbuild to bundle .js and .css sources.
-config :minisome, Minisome.Web.Endpoint,
- # Binding to loopback ipv4 address prevents access from other machines.
- # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
- http: [ip: {127, 0, 0, 1}, port: 4000],
- check_origin: false,
- code_reloader: true,
- debug_errors: true,
- secret_key_base: "MR3TEt40ApczckU3+IRzAi91t5iNhbiRt4l0ChL30DdADwAwtdfa++i+NX0ezfc1",
- watchers: [
- # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
- esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
- ]
-
-config :minisome, Minisome.API.Endpoint,
- http: [ip: {127, 0, 0, 1}, port: 33101],
- check_origin: false,
- code_reloader: true,
- debug_errors: true,
- secret_key_base: "MR3TEt40ApczckU3+IRzAi91t5iNhbiRt4l0ChL30DdADwAwtdfa++i+NX0ezfc1"
-
# ## SSL Support
#
# In order to use HTTPS in development, a self-signed
diff --git a/config/runtime.exs b/config/runtime.exs
index 20026ee..d3a2f27 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -1,76 +1,106 @@
import Config
+import Minisome.ConfigHelpers, only: [get_env: 1, get_env: 2, get_env: 3]
# config/runtime.exs is executed for all environments, including
# during releases. It is executed after compilation and before the
# system starts, so it is typically used to load production configuration
# and secrets from environment variables or elsewhere. Do not define
# any compile-time configuration in here, as it won't be applied.
-# The block below contains prod specific runtime configuration.
+
+config :minisome,
+ # How often to fetch feeds of other hosts
+ update_delay: get_env("UPDATE_DELAY", 10, :int)
# Start the phoenix server if environment is set and running in a release
if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
config :minisome, Minisome.Web.Endpoint, server: true
end
-if config_env() == :prod do
- database_path =
- System.get_env("DATABASE_PATH") ||
- raise """
- environment variable DATABASE_PATH is missing.
- For example: /etc/minisome/minisome.db
- """
+host = get_env("PHX_HOST", "localhost")
+port = get_env("WEB_PORT", 4000, :int)
+api_port = get_env("API_PORT", 33101, :int)
- config :minisome, Minisome.Storage.Repo,
- database: database_path,
- pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5")
+cond do
+ config_env() == :dev ->
+ config :minisome, Minisome.Storage.Repo,
+ database:
+ get_env("DATABASE_PATH", Path.expand("../minisome_dev.db", Path.dirname(__ENV__.file)))
- # The secret key base is used to sign/encrypt cookies and other secrets.
- # A default value is used in config/dev.exs and config/test.exs but you
- # want to use a different value for prod and you most likely don't want
- # to check this value into version control, so we use an environment
- # variable instead.
- secret_key_base =
- System.get_env("SECRET_KEY_BASE") ||
- raise """
- environment variable SECRET_KEY_BASE is missing.
- You can generate one by calling: mix phx.gen.secret
- """
+ config :minisome, Minisome.Web.Endpoint,
+ # Binding to loopback ipv4 address prevents access from other machines.
+ # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
+ http: [ip: {127, 0, 0, 1}, port: port],
+ check_origin: false,
+ code_reloader: true,
+ debug_errors: true,
+ secret_key_base: "MR3TEt40ApczckU3+IRzAi91t5iNhbiRt4l0ChL30DdADwAwtdfa++i+NX0ezfc1",
+ watchers: [
+ # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
+ esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
+ ]
- host = System.get_env("PHX_HOST") || "example.com"
- port = String.to_integer(System.get_env("WEB_PORT") || "4000")
- api_port = String.to_integer(System.get_env("API_PORT") || "33101")
+ config :minisome, Minisome.API.Endpoint,
+ http: [ip: {127, 0, 0, 1}, port: api_port],
+ check_origin: false,
+ code_reloader: true,
+ debug_errors: true,
+ secret_key_base: "MR3TEt40ApczckU3+IRzAi91t5iNhbiRt4l0ChL30DdADwAwtdfa++i+NX0ezfc1"
- config :minisome, Minisome.Web.Endpoint,
- url: [host: host, port: 443],
- http: [
- # Enable IPv6 and bind on all interfaces.
- # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
- # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
- # for details about using IPv6 vs IPv4 and loopback vs public addresses.
- ip: {0, 0, 0, 0, 0, 0, 0, 0},
- port: port
- ],
- secret_key_base: secret_key_base
+ config_env() == :prod ->
+ database_path =
+ System.get_env("DATABASE_PATH") ||
+ raise """
+ environment variable DATABASE_PATH is missing.
+ For example: /etc/minisome/minisome.db
+ """
- config :minisome, Minisome.API.Endpoint,
- url: [host: host, port: api_port],
- http: [
- # Enable IPv6 and bind on all interfaces.
- # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
- # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
- # for details about using IPv6 vs IPv4 and loopback vs public addresses.
- ip: {0, 0, 0, 0, 0, 0, 0, 0},
- port: api_port
- ],
- secret_key_base: secret_key_base
+ config :minisome, Minisome.Storage.Repo,
+ database: database_path,
+ pool_size: get_env("POOL_SIZE", 5, :int)
- # ## Using releases
- #
- # If you are doing OTP releases, you need to instruct Phoenix
- # to start each relevant endpoint:
- #
- # config :minisome, Minisome.Web.Endpoint, server: true
- #
- # Then you can assemble a release by calling `mix release`.
- # See `mix help release` for more information.
+ # The secret key base is used to sign/encrypt cookies and other secrets.
+ # A default value is used in config/dev.exs and config/test.exs but you
+ # want to use a different value for prod and you most likely don't want
+ # to check this value into version control, so we use an environment
+ # variable instead.
+ secret_key_base =
+ System.get_env("SECRET_KEY_BASE") ||
+ raise """
+ environment variable SECRET_KEY_BASE is missing.
+ You can generate one by calling: mix phx.gen.secret
+ """
+
+ config :minisome, Minisome.Web.Endpoint,
+ url: [host: host, port: 443],
+ http: [
+ # Enable IPv6 and bind on all interfaces.
+ # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
+ # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
+ # for details about using IPv6 vs IPv4 and loopback vs public addresses.
+ ip: {0, 0, 0, 0, 0, 0, 0, 0},
+ port: port
+ ],
+ secret_key_base: secret_key_base
+
+ config :minisome, Minisome.API.Endpoint,
+ url: [host: host, port: api_port],
+ http: [
+ # Enable IPv6 and bind on all interfaces.
+ # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
+ # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
+ # for details about using IPv6 vs IPv4 and loopback vs public addresses.
+ ip: {0, 0, 0, 0, 0, 0, 0, 0},
+ port: api_port
+ ],
+ secret_key_base: secret_key_base
+
+ # ## Using releases
+ #
+ # If you are doing OTP releases, you need to instruct Phoenix
+ # to start each relevant endpoint:
+ #
+ # config :minisome, Minisome.Web.Endpoint, server: true
+ #
+ # Then you can assemble a release by calling `mix release`.
+ # See `mix help release` for more information.
end
diff --git a/lib/api/client/message.ex b/lib/api/client/message.ex
index a5e61bd..299b1bb 100644
--- a/lib/api/client/message.ex
+++ b/lib/api/client/message.ex
@@ -25,17 +25,16 @@ defmodule Minisome.API.Client.Message do
@doc """
Parse a binary into a RawMessage.
- The binary must have the OpenSSH style signature right at the start, then the base 64 encoded
- payload after that.
+ The binary must have the OpenSSH style signature right at the start, followed by two newlines, then the payload after that.
"""
@spec parse_message(binary()) :: RawMessage.t() | nil
def parse_message(data) do
- with {:split, [sig_part, data_part]} <- {:split, :binary.split(data, SSH.openssh_sig_end())},
+ with {:split, [sig_part, data_part]} <-
+ {:split, :binary.split(data, "#{SSH.openssh_sig_end()}\n\n")},
sig_data = sig_part <> SSH.openssh_sig_end(),
- {:ok, signature} <- SSH.parse_signature(sig_data),
- {:ok, parsed_data} <- data_part |> String.trim() |> Base.decode64() do
+ {:ok, signature} <- SSH.parse_signature(sig_data) do
%RawMessage{
- data: parsed_data,
+ data: data_part,
signature: signature
}
else
@@ -46,10 +45,6 @@ defmodule Minisome.API.Client.Message do
{:split, _} ->
Logger.warning("Unable to parse message due to missing footer")
nil
-
- :error ->
- Logger.warning("Unable to base64 decode the message data")
- nil
end
end
@@ -71,10 +66,6 @@ defmodule Minisome.API.Client.Message do
"""
@spec format_message(RawMessage.t()) :: String.t()
def format_message(%RawMessage{} = msg) do
- """
- #{SSH.format_signature(msg.signature)}
-
- #{Base.encode64(msg.data)}
- """
+ "#{SSH.format_signature(msg.signature)}\n\n#{msg.data}"
end
end
diff --git a/lib/api/client/message_libraries/auth/auth.ex b/lib/api/client/message_libraries/auth/auth.ex
index b7fc5ae..9a07adb 100644
--- a/lib/api/client/message_libraries/auth/auth.ex
+++ b/lib/api/client/message_libraries/auth/auth.ex
@@ -29,9 +29,6 @@ defmodule Minisome.API.Client.MessageLibraries.Auth do
@impl GenericLibrary
@spec decode(ProtocolMessage.t()) :: {:ok, any()} | {:error, atom()}
def decode(%ProtocolMessage{} = message) do
- case Map.get(@message_mapping, message.type) do
- nil -> {:error, :unknown_type}
- module -> module.decode(message)
- end
+ GenericLibrary.generic_decode(@message_mapping, message)
end
end
diff --git a/lib/api/client/message_libraries/feed/feed.ex b/lib/api/client/message_libraries/feed/feed.ex
new file mode 100644
index 0000000..1e4e760
--- /dev/null
+++ b/lib/api/client/message_libraries/feed/feed.ex
@@ -0,0 +1,34 @@
+defmodule Minisome.API.Client.MessageLibraries.Feed do
+ alias Minisome.API.Client.MessageLibraries.GenericLibrary
+ alias Minisome.API.Client.ProtocolMessage
+ alias Minisome.API.Client.MessageLibraries.Feed.FeedMessage
+
+ @behaviour GenericLibrary
+
+ @message_mapping %{
+ FeedMessage.message_type() => FeedMessage
+ }
+
+ @impl GenericLibrary
+ @spec library_name() :: String.t()
+ def library_name(), do: "feed"
+
+ @impl GenericLibrary
+ @spec encoded_version() :: Version.t()
+ def encoded_version(),
+ do: %Version{
+ major: 1,
+ minor: 0,
+ patch: 0
+ }
+
+ @impl GenericLibrary
+ @spec accepted_version() :: Version.t()
+ def accepted_version(), do: encoded_version()
+
+ @impl GenericLibrary
+ @spec decode(ProtocolMessage.t()) :: {:ok, any()} | {:error, atom()}
+ def decode(%ProtocolMessage{} = message) do
+ GenericLibrary.generic_decode(@message_mapping, message)
+ end
+end
diff --git a/lib/api/client/message_libraries/feed/feed_message.ex b/lib/api/client/message_libraries/feed/feed_message.ex
new file mode 100644
index 0000000..111c4cb
--- /dev/null
+++ b/lib/api/client/message_libraries/feed/feed_message.ex
@@ -0,0 +1,48 @@
+defmodule Minisome.API.Client.MessageLibraries.Feed.FeedMessage do
+ import Minisome.Utils.TypedStruct
+
+ alias Minisome.API.Client.MessageLibraries.GenericMessage
+
+ @behaviour GenericMessage
+
+ deftypedstruct(%{
+ contents: [Minisome.API.Client.Message.RawMessage.t()]
+ })
+
+ defmodule Decoded do
+ deftypedstruct(%{
+ contents: [struct()]
+ })
+ end
+
+ @impl GenericMessage
+ @spec message_type() :: String.t()
+ def message_type(), do: "feed"
+
+ @impl GenericMessage
+ @spec encode(t()) :: {String.t(), map()}
+ def encode(%__MODULE__{} = message) do
+ GenericMessage.encode(__MODULE__, %{
+ "contents" =>
+ for content <- message.contents do
+ Minisome.API.Client.Message.format_message(content)
+ end
+ })
+ end
+
+ @impl GenericMessage
+ @spec decode(any()) :: {:ok, t()} | :error
+ def decode(payload) do
+ with true <- is_map(payload),
+ {:ok, contents} when is_list(contents) <- Map.fetch(payload, "contents"),
+ raw_contents <- Enum.map(contents, &Minisome.API.Client.Message.parse_message/1),
+ true <- Enum.all?(raw_contents, &(!is_nil(&1))) do
+ {:ok,
+ %__MODULE__{
+ contents: raw_contents
+ }}
+ else
+ _ -> :error
+ end
+ end
+end
diff --git a/lib/api/client/message_libraries/generic_library.ex b/lib/api/client/message_libraries/generic_library.ex
index a9de157..6b0ef84 100644
--- a/lib/api/client/message_libraries/generic_library.ex
+++ b/lib/api/client/message_libraries/generic_library.ex
@@ -1,14 +1,26 @@
defmodule Minisome.API.Client.MessageLibraries.GenericLibrary do
+ alias Minisome.API.Client.ProtocolMessage
+
+ @type message_mapping() :: %{
+ optional(Minisome.API.Client.MessageLibraries.GenericMessage.type()) => module()
+ }
+
@callback library_name() :: String.t()
@callback encoded_version() :: Version.t()
@callback accepted_version() :: Version.t()
- @callback decode(Minisome.API.Client.TypedMessage.t()) :: any()
-
- alias Minisome.API.Client.ProtocolMessage
+ @callback decode(ProtocolMessage.t()) :: {:ok, any()} | {:error, atom()}
def dispatch(message) do
end
+ @spec generic_decode(message_mapping(), ProtocolMessage.t()) :: {:ok, any()} | {:error, atom()}
+ def generic_decode(message_mapping, %ProtocolMessage{} = message) do
+ case Map.get(message_mapping, message.type) do
+ nil -> {:error, :unknown_type}
+ module -> module.decode(message.payload)
+ end
+ end
+
@spec encode(module(), {String.t(), any()}) :: ProtocolMessage.t()
def encode(module, {type, payload}) do
%ProtocolMessage{
diff --git a/lib/api/client/message_libraries/generic_message.ex b/lib/api/client/message_libraries/generic_message.ex
index 64f8b87..e390513 100644
--- a/lib/api/client/message_libraries/generic_message.ex
+++ b/lib/api/client/message_libraries/generic_message.ex
@@ -1,5 +1,7 @@
defmodule Minisome.API.Client.MessageLibraries.GenericMessage do
- @callback message_type() :: String.t()
+ @type type() :: String.t()
+
+ @callback message_type() :: type()
@callback encode(any()) :: {String.t(), any()}
@callback decode(any()) :: {:ok, any()} | :error
diff --git a/lib/api/client/message_libraries/post/post.ex b/lib/api/client/message_libraries/post/post.ex
new file mode 100644
index 0000000..f64da5c
--- /dev/null
+++ b/lib/api/client/message_libraries/post/post.ex
@@ -0,0 +1,34 @@
+defmodule Minisome.API.Client.MessageLibraries.Post do
+ alias Minisome.API.Client.MessageLibraries.GenericLibrary
+ alias Minisome.API.Client.ProtocolMessage
+ alias Minisome.API.Client.MessageLibraries.Post.PostMessage
+
+ @behaviour GenericLibrary
+
+ @message_mapping %{
+ PostMessage.message_type() => PostMessage
+ }
+
+ @impl GenericLibrary
+ @spec library_name() :: String.t()
+ def library_name(), do: "post"
+
+ @impl GenericLibrary
+ @spec encoded_version() :: Version.t()
+ def encoded_version(),
+ do: %Version{
+ major: 1,
+ minor: 0,
+ patch: 0
+ }
+
+ @impl GenericLibrary
+ @spec accepted_version() :: Version.t()
+ def accepted_version(), do: encoded_version()
+
+ @impl GenericLibrary
+ @spec decode(ProtocolMessage.t()) :: {:ok, any()} | {:error, atom()}
+ def decode(%ProtocolMessage{} = message) do
+ GenericLibrary.generic_decode(@message_mapping, message)
+ end
+end
diff --git a/lib/api/client/message_libraries/post/post_message.ex b/lib/api/client/message_libraries/post/post_message.ex
new file mode 100644
index 0000000..d5637af
--- /dev/null
+++ b/lib/api/client/message_libraries/post/post_message.ex
@@ -0,0 +1,54 @@
+defmodule Minisome.API.Client.MessageLibraries.Post.PostMessage do
+ import Minisome.Utils.TypedStruct
+
+ alias Minisome.API.Client.MessageLibraries.GenericMessage
+
+ @behaviour GenericMessage
+
+ deftypedstruct(%{
+ id: String.t(),
+ text: String.t(),
+ tags: [String.t()],
+ posted: DateTime.t()
+ })
+
+ @impl GenericMessage
+ @spec message_type() :: String.t()
+ def message_type(), do: "post"
+
+ @impl GenericMessage
+ @spec encode(t()) :: {String.t(), map()}
+ def encode(%__MODULE__{} = message) do
+ GenericMessage.encode(__MODULE__, %{
+ "id" => message.id,
+ "text" => message.text,
+ "tags" => message.tags,
+ "posted" => message.posted
+ })
+ end
+
+ @impl GenericMessage
+ @spec decode(any()) :: {:ok, t()} | :error
+ def decode(payload) do
+ with true <- is_map(payload),
+ {:ok, text} <- Map.fetch(payload, "text"),
+ {:ok, tags} <- Map.fetch(payload, "tags"),
+ {:ok, posted} <- Map.fetch(payload, "posted"),
+ {:ok, id} <- Map.fetch(payload, "id"),
+ true <- String.valid?(text),
+ true <- is_struct(posted, DateTime),
+ true <- is_list(tags),
+ true <- String.valid?(id),
+ true <- Enum.all?(tags, &String.valid?/1) do
+ {:ok,
+ %__MODULE__{
+ id: id,
+ text: text,
+ tags: tags,
+ posted: posted
+ }}
+ else
+ _ -> :error
+ end
+ end
+end
diff --git a/lib/api/client/protocol_handler.ex b/lib/api/client/protocol_handler.ex
index be17b76..11e790b 100644
--- a/lib/api/client/protocol_handler.ex
+++ b/lib/api/client/protocol_handler.ex
@@ -3,14 +3,20 @@ defmodule Minisome.API.Client.ProtocolHandler do
Handler for protocol messages to/from raw messages.
"""
+ require Logger
+
import Minisome.Utils.WithHelper
alias Minisome.API.Client.Message
alias Minisome.API.Client.ProtocolMessage
alias Minisome.API.Client.MessageLibraries.Auth
+ alias Minisome.API.Client.MessageLibraries.Post
+ alias Minisome.API.Client.MessageLibraries.Feed
@library_mapping %{
- Auth.library_name() => Auth
+ Auth.library_name() => Auth,
+ Post.library_name() => Post,
+ Feed.library_name() => Feed
}
@doc """
@@ -18,37 +24,52 @@ defmodule Minisome.API.Client.ProtocolHandler do
"""
@spec decode_raw(Message.RawMessage.t()) :: {:ok, ProtocolMessage.t()} | {:error, any()}
def decode_raw(%Message.RawMessage{} = raw) do
- with {:ok, data} = op(:unpack, Msgpax.unpack(raw.data)),
- %{library: library, type: type, version: version, payload: payload} <-
- op(:destructure, data),
+ with {:ok, data} <- op(:unpack, Msgpax.unpack(raw.data)),
+ {:destructure,
+ %{"library" => library, "type" => type, "version" => version, "payload" => payload}} <-
+ {:destructure, data},
library_module <- op(:library_type, Map.get(@library_mapping, library), :permissive),
{:version, [major_version, minor_version, patch_version]}
when is_integer(major_version) and is_integer(minor_version) and
is_integer(patch_version) <- {:version, version},
true <- op(:type_type, is_binary(type)) do
- %ProtocolMessage{
- library: library_module,
- type: type,
- version: %Version{
- major: major_version,
- minor: minor_version,
- patch: patch_version
- },
- payload: payload
- }
+ {:ok,
+ %ProtocolMessage{
+ library: library_module,
+ type: type,
+ version: %Version{
+ major: major_version,
+ minor: minor_version,
+ patch: patch_version
+ },
+ payload: payload
+ }}
else
- {:unpack, err} -> {:error, {:invalid_msgpack, err}}
- {:destructure, _} -> {:error, :invalid_payload}
- {:library_type, _} -> {:error, :invalid_library_type}
- {:version, _} -> {:error, :invalid_version}
- {:type_type, _} -> {:error, :invalid_type_type}
+ {:unpack, err} ->
+ Logger.warn("Unable to msgpack decode protocol message: #{inspect(err)}")
+ {:error, {:invalid_msgpack, err}}
+
+ {:destructure, payload} ->
+ Logger.warn("Invalid protocol message payload: #{inspect(payload)}")
+ {:error, :invalid_payload}
+
+ {:library_type, type} ->
+ Logger.warn("Invalid protocol message library: #{inspect(type)}")
+ {:error, :invalid_library_type}
+
+ {:version, version} ->
+ Logger.warn("Invalid protocol message version: #{inspect(version)}")
+ {:error, :invalid_version}
+
+ {:type_type, _} ->
+ {:error, :invalid_type_type}
end
end
@doc """
Dispatch the protocol message to its library for decoding.
"""
- @spec handle_message(ProtocolMessage.t()) :: any()
+ @spec handle_message(ProtocolMessage.t()) :: {:ok, any()} | {:error, atom()}
def handle_message(%ProtocolMessage{} = message) do
message.library.decode(message)
end
diff --git a/lib/api/controllers/auth_controller.ex b/lib/api/controllers/auth_controller.ex
index e747d84..3d5b862 100644
--- a/lib/api/controllers/auth_controller.ex
+++ b/lib/api/controllers/auth_controller.ex
@@ -23,7 +23,6 @@ defmodule Minisome.API.AuthController do
data = Minisome.API.Client.Message.format_message(raw)
conn
- |> put_resp_content_type("text/minisome")
|> send_resp(200, data)
end
end
diff --git a/lib/api/controllers/feed_controller.ex b/lib/api/controllers/feed_controller.ex
new file mode 100644
index 0000000..6687685
--- /dev/null
+++ b/lib/api/controllers/feed_controller.ex
@@ -0,0 +1,54 @@
+defmodule Minisome.API.FeedController do
+ use Phoenix.Controller, namespace: Minisome.API
+ import Plug.Conn
+
+ alias Minisome.API.Client.MessageLibraries.Post, as: PostLibrary
+ alias Minisome.API.Client.MessageLibraries.Post.PostMessage
+ alias Minisome.API.Client.MessageLibraries.Feed, as: FeedLibrary
+ alias Minisome.API.Client.MessageLibraries.Feed.FeedMessage
+ alias Minisome.Some.Post.API, as: PostAPI
+
+ @spec feed(Plug.Conn.t(), Plug.Conn.params()) :: Plug.Conn.t()
+ def feed(conn, _params) do
+ keys = Minisome.Storage.Auth.MyKey.get_active_keys()
+
+ posts = PostAPI.all()
+
+ post_messages =
+ for post <- posts do
+ %PostMessage{
+ id: post.id,
+ text: post.text,
+ tags: post.tags,
+ posted: post.posted
+ }
+ |> form_post_message(keys)
+ end
+
+ message = %FeedMessage{
+ contents: post_messages
+ }
+
+ payload = FeedMessage.encode(message)
+
+ protocol_message =
+ Minisome.API.Client.MessageLibraries.GenericLibrary.encode(FeedLibrary, payload)
+
+ {:ok, packed} = Minisome.API.Client.ProtocolHandler.encode_message(protocol_message)
+ raw = Minisome.API.Client.Message.encode_message(packed, keys |> List.first() |> elem(0))
+ data = Minisome.API.Client.Message.format_message(raw)
+
+ conn
+ |> send_resp(200, data)
+ end
+
+ defp form_post_message(post, keys) do
+ payload = PostMessage.encode(post)
+
+ protocol_message =
+ Minisome.API.Client.MessageLibraries.GenericLibrary.encode(PostLibrary, payload)
+
+ {:ok, packed} = Minisome.API.Client.ProtocolHandler.encode_message(protocol_message)
+ Minisome.API.Client.Message.encode_message(packed, keys |> List.first() |> elem(0))
+ end
+end
diff --git a/lib/api/plugs/api_content_type_plug.ex b/lib/api/plugs/api_content_type_plug.ex
new file mode 100644
index 0000000..0ece081
--- /dev/null
+++ b/lib/api/plugs/api_content_type_plug.ex
@@ -0,0 +1,14 @@
+defmodule Minisome.API.ContentTypePlug do
+ @behaviour Plug
+
+ @minisome_content_type Application.compile_env!(:minisome, :mime_type)
+
+ @impl Plug
+ def init(opts), do: opts
+
+ @impl Plug
+ @spec call(Plug.Conn.t(), any) :: Plug.Conn.t()
+ def call(conn, _opts) do
+ Plug.Conn.put_resp_content_type(conn, @minisome_content_type)
+ end
+end
diff --git a/lib/api/router.ex b/lib/api/router.ex
index f5205a9..1f6d065 100644
--- a/lib/api/router.ex
+++ b/lib/api/router.ex
@@ -5,12 +5,15 @@ defmodule Minisome.API.Router do
import Phoenix.Controller
pipeline :api do
- plug :accepts, ["text"]
+ plug :accepts, ["minisome"]
+ plug Minisome.API.ContentTypePlug
end
scope "/", Minisome.API do
pipe_through :api
get "/key-info", AuthController, :key_info
+
+ get "/feed", FeedController, :feed
end
end
diff --git a/lib/application.ex b/lib/application.ex
index 986cd1e..2767064 100644
--- a/lib/application.ex
+++ b/lib/application.ex
@@ -8,6 +8,8 @@ defmodule Minisome.Application do
@impl true
def start(_type, _args) do
children = [
+ {Finch, name: Minisome.Some.HTTP},
+
# Start the Ecto repository
Minisome.Storage.Repo,
# Start the Telemetry supervisor
@@ -16,9 +18,18 @@ defmodule Minisome.Application do
{Phoenix.PubSub, name: Minisome.PubSub},
# Start the Endpoint (http/https)
Minisome.Web.Endpoint,
- Minisome.API.Endpoint
+ Minisome.API.Endpoint,
# Start a worker by calling: Minisome.Worker.start_link(arg)
# {Minisome.Worker, arg}
+
+ Supervisor.child_spec({Phoenix.PubSub, name: Minisome.Some.Fetcher.PubSub},
+ id: Minisome.Some.Fetcher.PubSub
+ ),
+ {Minisome.Some.Fetcher,
+ %Minisome.Some.Fetcher.Options{
+ hosts_getter: &Minisome.Some.Host.API.all/0,
+ fetch_delay: Application.get_env(:minisome, :update_delay) * 1_000
+ }}
]
# See https://hexdocs.pm/elixir/Supervisor.html
diff --git a/lib/config_helpers.ex b/lib/config_helpers.ex
new file mode 100644
index 0000000..14215ac
--- /dev/null
+++ b/lib/config_helpers.ex
@@ -0,0 +1,35 @@
+defmodule Minisome.ConfigHelpers do
+ @type config_type :: :str | :int | :bool | :json
+
+ @doc """
+ Get value from environment variable, converting it to the given type if needed.
+
+ If no default value is given, or `:no_default` is given as the default, an error is raised if the variable is not
+ set.
+ """
+ @spec get_env(String.t(), :no_default | any(), config_type()) :: any()
+ def get_env(var, default \\ :no_default, type \\ :str)
+
+ def get_env(var, :no_default, type) do
+ System.fetch_env!(var)
+ |> get_with_type(type)
+ end
+
+ def get_env(var, default, type) do
+ with {:ok, val} <- System.fetch_env(var) do
+ get_with_type(val, type)
+ else
+ :error -> default
+ end
+ end
+
+ @spec get_with_type(String.t(), config_type()) :: any()
+ defp get_with_type(val, type)
+
+ defp get_with_type(val, :str), do: val
+ defp get_with_type(val, :int), do: String.to_integer(val)
+ defp get_with_type("true", :bool), do: true
+ defp get_with_type("false", :bool), do: false
+ defp get_with_type(val, :json), do: Jason.decode!(val)
+ defp get_with_type(val, type), do: raise("Cannot convert to #{inspect(type)}: #{inspect(val)}")
+end
diff --git a/lib/crypto/ssh.ex b/lib/crypto/ssh.ex
index cd90215..237d746 100644
--- a/lib/crypto/ssh.ex
+++ b/lib/crypto/ssh.ex
@@ -155,7 +155,7 @@ defmodule Minisome.Crypto.SSH do
"#{@openssh_sig_start}\n" <>
blob_split <>
- "#{@openssh_sig_end}\n"
+ "#{@openssh_sig_end}"
end
@doc """
diff --git a/lib/some/fetcher/fetcher.ex b/lib/some/fetcher/fetcher.ex
new file mode 100644
index 0000000..ba73607
--- /dev/null
+++ b/lib/some/fetcher/fetcher.ex
@@ -0,0 +1,86 @@
+defmodule Minisome.Some.Fetcher do
+ import Minisome.Utils.TypedStruct
+
+ alias Minisome.Storage.Auth.Host
+
+ use GenServer
+
+ defmodule Options do
+ deftypedstruct(%{
+ name: {GenServer.name(), Minisome.Some.Fetcher},
+ fetch_delay: pos_integer(),
+ hosts_getter: (() -> [Host.t()])
+ })
+ end
+
+ defmodule State do
+ deftypedstruct(%{
+ fetch_delay: pos_integer(),
+ hosts: [Host.t()],
+ timer_ref: :timer.tref()
+ })
+ end
+
+ @spec start_link(Options.t()) :: GenServer.on_start()
+ def start_link(%Options{} = opts) do
+ GenServer.start_link(__MODULE__, opts, name: opts.name)
+ end
+
+ @impl GenServer
+ @spec init(Options.t()) :: {:ok, State.t()}
+ def init(opts) do
+ {:ok, tref} = :timer.send_after(opts.fetch_delay, self(), :fetch)
+
+ hosts = opts.hosts_getter.()
+
+ {:ok,
+ %State{
+ fetch_delay: opts.fetch_delay,
+ hosts: hosts,
+ timer_ref: tref
+ }}
+ end
+
+ @impl GenServer
+ @spec handle_info(any(), State.t()) :: {:noreply, State.t()}
+ def handle_info(msg, state)
+
+ def handle_info(:fetch, state) do
+ feeds =
+ for host <- state.hosts do
+ Minisome.Some.HTTP.get(host, "/feed")
+ end
+ |> Enum.reject(&(&1 == :error))
+ |> Enum.map(fn {:ok, msg} -> decode_feed(msg) end)
+ |> Enum.reject(&(&1 == :error))
+
+ Phoenix.PubSub.broadcast!(Minisome.Some.Fetcher.PubSub, "feed", {:feed, feeds})
+
+ {:ok, tref} = :timer.send_after(state.fetch_delay, self(), :fetch)
+ {:noreply, %State{state | timer_ref: tref}}
+ end
+
+ @spec decode_feed(Minisome.API.Client.MessageLibraries.Feed.FeedMessage.t()) ::
+ Minisome.API.Client.MessageLibraries.Feed.FeedMessage.Decoded.t() | :error
+ defp decode_feed(feed) do
+ raw_contents = feed.contents
+
+ with protocol_messages <-
+ Enum.map(raw_contents, &Minisome.API.Client.ProtocolHandler.decode_raw/1),
+ true <-
+ Enum.all?(protocol_messages, fn
+ {:ok, _} -> true
+ _ -> false
+ end),
+ decoded_messages <-
+ Enum.map(protocol_messages, fn {:ok, msg} ->
+ Minisome.API.Client.ProtocolHandler.handle_message(msg)
+ end) do
+ %Minisome.API.Client.MessageLibraries.Feed.FeedMessage.Decoded{
+ contents: decoded_messages |> Enum.reject(&(&1 == :error)) |> Enum.map(&elem(&1, 1))
+ }
+ else
+ _ -> :error
+ end
+ end
+end
diff --git a/lib/some/host/api.ex b/lib/some/host/api.ex
new file mode 100644
index 0000000..e3797d4
--- /dev/null
+++ b/lib/some/host/api.ex
@@ -0,0 +1,6 @@
+defmodule Minisome.Some.Host.API do
+ @spec all(Ecto.Repo.t()) :: [Minisome.Storage.Auth.Host.t()]
+ def all(repo \\ Minisome.Storage.Repo) do
+ repo.all(Minisome.Storage.Auth.Host)
+ end
+end
diff --git a/lib/some/http/http.ex b/lib/some/http/http.ex
new file mode 100644
index 0000000..fd30464
--- /dev/null
+++ b/lib/some/http/http.ex
@@ -0,0 +1,26 @@
+defmodule Minisome.Some.HTTP do
+ alias Minisome.Storage.Auth.Host
+ alias Minisome.API.Client.Message.RawMessage
+
+ @spec get(Host.t(), String.t()) :: {:ok, RawMessage.t()} | :error
+ def get(host, path) do
+ req = build_req(host, path)
+
+ with {:ok, resp} <- Finch.request(req, __MODULE__),
+ msg when msg != nil <- Minisome.API.Client.Message.parse_message(resp.body),
+ {:ok, proto_msg} <- Minisome.API.Client.ProtocolHandler.decode_raw(msg),
+ {:ok, handled_msg} <-
+ Minisome.API.Client.ProtocolHandler.handle_message(proto_msg) do
+ {:ok, handled_msg}
+ else
+ _ -> :error
+ end
+ end
+
+ @spec build_req(Host.t(), String.t()) :: Finch.Request.t()
+ def build_req(host, path) do
+ url = URI.merge(URI.new!("http://#{host.hostname}:#{host.port}"), %URI{path: path})
+
+ Finch.build(:get, url, [{"accept", Application.get_env(:minisome, :mime_type)}])
+ end
+end
diff --git a/lib/some/post/api.ex b/lib/some/post/api.ex
new file mode 100644
index 0000000..13a5ccc
--- /dev/null
+++ b/lib/some/post/api.ex
@@ -0,0 +1,37 @@
+defmodule Minisome.Some.Post.API do
+ alias Ecto.Changeset
+ alias Minisome.Storage.Repo
+ alias Minisome.Storage.Some.Post
+
+ @spec create_changeset(map()) :: Changeset.t()
+ def create_changeset(params \\ %{}) do
+ %Post{}
+ |> Changeset.cast(params, [:text, :tags])
+ |> Changeset.validate_required([:text])
+ |> Changeset.validate_length(:text, min: 1)
+ |> Changeset.validate_change(:tags, fn
+ _, val when is_list(val) ->
+ failed = Enum.reject(val, &String.valid?/1)
+
+ if failed != [] do
+ [tags: "Tags must be strings."]
+ else
+ []
+ end
+
+ _, _val ->
+ [tags: "Tags need to be a list."]
+ end)
+ |> Changeset.put_change(:posted, DateTime.utc_now() |> DateTime.truncate(:second))
+ end
+
+ @spec create(Changeset.t(), Ecto.Repo.t()) :: {:ok, Post.t()} | {:error, Changeset.t()}
+ def create(changeset, repo \\ Repo) do
+ repo.insert(changeset)
+ end
+
+ @spec all(Ecto.Repo.t()) :: [Post.t()]
+ def all(repo \\ Repo) do
+ repo.all(Post)
+ end
+end
diff --git a/lib/some/post/federated_post.ex b/lib/some/post/federated_post.ex
new file mode 100644
index 0000000..30f85b4
--- /dev/null
+++ b/lib/some/post/federated_post.ex
@@ -0,0 +1,9 @@
+defmodule Minisome.Some.Post.FederatedPost do
+ import Minisome.Utils.TypedStruct
+
+ deftypedstruct(%{
+ post: Minisome.API.Client.MessageLibraries.Post.PostMessage.t(),
+ original: Minisome.API.Client.Message.RawMessage.t(),
+ path: [Minisome.Storage.Auth.Host.t()]
+ })
+end
diff --git a/lib/storage/auth/key.ex b/lib/storage/auth/key.ex
index e16e7d3..7379040 100644
--- a/lib/storage/auth/key.ex
+++ b/lib/storage/auth/key.ex
@@ -1,6 +1,5 @@
defmodule Minisome.Storage.Auth.Key do
use Ecto.Schema
- import Ecto.Query, only: [from: 2]
import Minisome.Storage.TypedSchema
deftypedschema "keys" do
diff --git a/lib/storage/some/federated_post.ex b/lib/storage/some/federated_post.ex
new file mode 100644
index 0000000..e69de29
diff --git a/lib/storage/some/post.ex b/lib/storage/some/post.ex
new file mode 100644
index 0000000..a5e1aaa
--- /dev/null
+++ b/lib/storage/some/post.ex
@@ -0,0 +1,10 @@
+defmodule Minisome.Storage.Some.Post do
+ use Ecto.Schema
+ import Minisome.Storage.TypedSchema
+
+ deftypedschema "posts" do
+ field(:posted, :utc_datetime, DateTime.t())
+ field(:text, :string, String.t())
+ field(:tags, {:array, :string}, [String.t()])
+ end
+end
diff --git a/lib/web/components/post.ex b/lib/web/components/post.ex
new file mode 100644
index 0000000..c326e74
--- /dev/null
+++ b/lib/web/components/post.ex
@@ -0,0 +1,35 @@
+defmodule Minisome.Web.Components.Post do
+ import Phoenix.LiveView.Helpers, only: [sigil_H: 2]
+
+ @spec my(%{post: Minisome.Storage.Some.Post.t()}) :: Phoenix.LiveView.Rendered.t()
+ def my(assigns) do
+ ~H"""
+ ID: <%= @post.id %>
+ <%= @post.text %>
+
+ <%= @post.text %>
+