From 74ebea218eaf63e59fd4d085a91ef695fcd47510 Mon Sep 17 00:00:00 2001 From: Mikko Ahlroth Date: Sun, 12 Jun 2022 20:17:51 +0300 Subject: [PATCH] Add some rudimentary feed and post features, add decoding of messages --- .formatter.exs | 1 + .tool-versions | 4 +- assets/css/app.css | 65 ++++++-- config/config.exs | 7 + config/dev.exs | 27 ---- config/runtime.exs | 142 +++++++++++------- lib/api/client/message.ex | 21 +-- lib/api/client/message_libraries/auth/auth.ex | 5 +- lib/api/client/message_libraries/feed/feed.ex | 34 +++++ .../message_libraries/feed/feed_message.ex | 48 ++++++ .../message_libraries/generic_library.ex | 18 ++- .../message_libraries/generic_message.ex | 4 +- lib/api/client/message_libraries/post/post.ex | 34 +++++ .../message_libraries/post/post_message.ex | 54 +++++++ lib/api/client/protocol_handler.ex | 61 +++++--- lib/api/controllers/auth_controller.ex | 1 - lib/api/controllers/feed_controller.ex | 54 +++++++ lib/api/plugs/api_content_type_plug.ex | 14 ++ lib/api/router.ex | 5 +- lib/application.ex | 13 +- lib/config_helpers.ex | 35 +++++ lib/crypto/ssh.ex | 2 +- lib/some/fetcher/fetcher.ex | 86 +++++++++++ lib/some/host/api.ex | 6 + lib/some/http/http.ex | 26 ++++ lib/some/post/api.ex | 37 +++++ lib/some/post/federated_post.ex | 9 ++ lib/storage/auth/key.ex | 1 - lib/storage/some/federated_post.ex | 0 lib/storage/some/post.ex | 10 ++ lib/web/components/post.ex | 35 +++++ lib/web/live/feed.ex | 23 +++ lib/web/live/feed.html.heex | 3 + lib/web/live/posting.ex | 37 +++++ lib/web/live/posting.html.heex | 19 +++ lib/web/live/private_main.ex | 6 + lib/web/live/private_main.html.heex | 10 +- lib/web/router.ex | 11 +- mix.exs | 3 +- mix.lock | 5 + .../migrations/20220526070710_add_posts.exs | 11 ++ 41 files changed, 828 insertions(+), 159 deletions(-) create mode 100644 lib/api/client/message_libraries/feed/feed.ex create mode 100644 lib/api/client/message_libraries/feed/feed_message.ex create mode 100644 lib/api/client/message_libraries/post/post.ex create mode 100644 lib/api/client/message_libraries/post/post_message.ex create mode 100644 lib/api/controllers/feed_controller.ex create mode 100644 lib/api/plugs/api_content_type_plug.ex create mode 100644 lib/config_helpers.ex create mode 100644 lib/some/fetcher/fetcher.ex create mode 100644 lib/some/host/api.ex create mode 100644 lib/some/http/http.ex create mode 100644 lib/some/post/api.ex create mode 100644 lib/some/post/federated_post.ex create mode 100644 lib/storage/some/federated_post.ex create mode 100644 lib/storage/some/post.ex create mode 100644 lib/web/components/post.ex create mode 100644 lib/web/live/feed.ex create mode 100644 lib/web/live/feed.html.heex create mode 100644 lib/web/live/posting.ex create mode 100644 lib/web/live/posting.html.heex create mode 100644 priv/repo/migrations/20220526070710_add_posts.exs 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 %> +

+
+

Posted on <%= Calendar.strftime(@post.posted, "%c") %><%= if @post.tags in [nil, []] do "" else " with tags " <> Enum.join(@post.tags, ",") end %>.

+
+
+ """ + end + + @spec message(%{post: Minisome.API.Client.MessageLibraries.Post.PostMessage.t()}) :: + Phoenix.LiveView.Rendered.t() + def message(assigns) do + ~H""" +
+

+ <%= @post.text %> +

+
+

Posted on <%= Calendar.strftime(@post.posted, "%c") %><%= if @post.tags in [nil, []] do "" else " with tags " <> Enum.join(@post.tags, ",") end %>.

+
+
+ """ + end +end diff --git a/lib/web/live/feed.ex b/lib/web/live/feed.ex new file mode 100644 index 0000000..5b7c5c3 --- /dev/null +++ b/lib/web/live/feed.ex @@ -0,0 +1,23 @@ +defmodule Minisome.Web.Live.Feed do + use Minisome.Web, :live_view + + @impl Phoenix.LiveView + def mount(_params, _session, socket) do + :ok = Phoenix.PubSub.subscribe(Minisome.Some.Fetcher.PubSub, "feed") + + {:ok, assign(socket, posts: [])} + end + + @impl Phoenix.LiveView + def handle_info(msg, socket) + + def handle_info({:feed, feeds}, socket) do + posts = + for feed <- feeds, reduce: [] do + acc -> Enum.concat(feed.contents, acc) + end + |> Enum.sort_by(& &1.posted, {:desc, DateTime}) + + {:noreply, assign(socket, posts: posts)} + end +end diff --git a/lib/web/live/feed.html.heex b/lib/web/live/feed.html.heex new file mode 100644 index 0000000..c669d9c --- /dev/null +++ b/lib/web/live/feed.html.heex @@ -0,0 +1,3 @@ +<%= for post <- @posts do %> + +<% end %> diff --git a/lib/web/live/posting.ex b/lib/web/live/posting.ex new file mode 100644 index 0000000..e6ec957 --- /dev/null +++ b/lib/web/live/posting.ex @@ -0,0 +1,37 @@ +defmodule Minisome.Web.Live.Posting do + use Minisome.Web, :live_view + + alias Minisome.Some.Post.API, as: PostAPI + + @impl Phoenix.LiveView + def mount(_params, _session, socket) do + {:ok, + assign( + socket, + cset: PostAPI.create_changeset(), + created: nil + )} + end + + @impl Phoenix.LiveView + def handle_event(event, params, socket) + + def handle_event("change", params, socket) do + {:noreply, assign(socket, cset: PostAPI.create_changeset(Map.get(params, "post", %{})))} + end + + def handle_event("submit", params, socket) do + cset = PostAPI.create_changeset(Map.get(params, "post", %{})) + + socket = + case PostAPI.create(cset) do + {:ok, post} -> + assign(socket, created: post) + + {:error, error_cset} -> + assign(socket, cset: error_cset) + end + + {:noreply, socket} + end +end diff --git a/lib/web/live/posting.html.heex b/lib/web/live/posting.html.heex new file mode 100644 index 0000000..ad2fb03 --- /dev/null +++ b/lib/web/live/posting.html.heex @@ -0,0 +1,19 @@ +<.form let={f} for={@cset} phx-change="change" phx-submit="submit"> + <%= label(f, :text, "Post text") %> + <%= textarea(f, :text) %> + <%= error_tag(f, :text) %> + + <%= label(f, :tags, "Post tags") %> + <%= textarea(f, :tags) %> + <%= error_tag(f, :tags) %> + + <%= submit("Submit") %> + + <%= if @created do %> +
+ +

Created new post

+ + + <% end %> + diff --git a/lib/web/live/private_main.ex b/lib/web/live/private_main.ex index 90643d1..02d4c29 100644 --- a/lib/web/live/private_main.ex +++ b/lib/web/live/private_main.ex @@ -1,3 +1,9 @@ defmodule Minisome.Web.Live.PrivateMain do use Minisome.Web, :live_view + + @impl Phoenix.LiveView + def mount(_params, _session, socket) do + posts = Minisome.Some.Post.API.all() + {:ok, assign(socket, posts: posts)} + end end diff --git a/lib/web/live/private_main.html.heex b/lib/web/live/private_main.html.heex index 9a66efb..48ccc92 100644 --- a/lib/web/live/private_main.html.heex +++ b/lib/web/live/private_main.html.heex @@ -1,3 +1,11 @@ Hey! -<%= live_redirect("Key testing", to: Routes.live_path(@socket, Minisome.Web.Live.Signing)) %> +
    +
  • <%= live_redirect("Feed", to: Routes.live_path(@socket, Minisome.Web.Live.Feed)) %>
  • +
  • <%= live_redirect("Posting", to: Routes.live_path(@socket, Minisome.Web.Live.Posting)) %>
  • +
  • <%= live_redirect("Key testing", to: Routes.live_path(@socket, Minisome.Web.Live.Signing)) %>
  • +
+ +<%= for post <- @posts do %> + +<% end %> diff --git a/lib/web/router.ex b/lib/web/router.ex index e6f17e0..42990da 100644 --- a/lib/web/router.ex +++ b/lib/web/router.ex @@ -10,10 +10,6 @@ defmodule Minisome.Web.Router do plug :put_secure_browser_headers end - pipeline :api do - plug :accepts, ["json"] - end - scope "/", Minisome.Web do pipe_through :browser @@ -24,11 +20,8 @@ defmodule Minisome.Web.Router do pipe_through :browser live "/", Live.PrivateMain + live "/feed", Live.Feed live "/key-testing", Live.Signing + live "/posting", Live.Posting end - - # Other scopes may use custom stacks. - # scope "/api", Minisome.Web do - # pipe_through :api - # end end diff --git a/mix.exs b/mix.exs index 30ae6b9..a3f0a58 100644 --- a/mix.exs +++ b/mix.exs @@ -49,7 +49,8 @@ defmodule Minisome.MixProject do {:jason, "~> 1.2"}, {:plug_cowboy, "~> 2.5"}, {:msgpax, "~> 2.3"}, - {:ex_doc, "~> 0.28.0", runtime: false} + {:ex_doc, "~> 0.28.0", runtime: false}, + {:finch, "~> 0.12.0"} ] end diff --git a/mix.lock b/mix.lock index dbc933b..608634f 100644 --- a/mix.lock +++ b/mix.lock @@ -15,16 +15,21 @@ "ex_doc": {:hex, :ex_doc, "0.28.0", "7eaf526dd8c80ae8c04d52ac8801594426ae322b52a6156cd038f30bafa8226f", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e55cdadf69a5d1f4cfd8477122ebac5e1fadd433a8c1022dafc5025e48db0131"}, "exqlite": {:hex, :exqlite, "0.9.3", "57c80e742584dc4486d717681956d4152c7d03fb34ddbfb269844b504824528d", [:make, :mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "5108f84bcc91fd7ae5b1b247e2be3860e449de5f8383ccaa1454278ffa1fc509"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "finch": {:hex, :finch, "0.12.0", "6bbb3e0bb62dd91cd1217d9682a30f5bfc9b0b74950bf10a0b4d4399c2076892", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "320da3f32459e7dcb77f4271b4f2445ba6c5d32cc3c7cca8e2cff599e24be5a6"}, "floki": {:hex, :floki, "0.32.0", "f915dc15258bc997d49be1f5ef7d3992f8834d6f5695270acad17b41f5bcc8e2", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "1c5a91cae1fd8931c26a4826b5e2372c284813904c8bacb468b5de39c7ececbd"}, "gettext": {:hex, :gettext, "0.19.1", "564953fd21f29358e68b91634799d9d26989f8d039d7512622efb3c3b1c97892", [:mix], [], "hexpm", "10c656c0912b8299adba9b061c06947511e3f109ab0d18b44a866a4498e77222"}, + "hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, + "mint": {:hex, :mint, "1.4.1", "49b3b6ea35a9a38836d2ad745251b01ca9ec062f7cb66f546bf22e6699137126", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "cd261766e61011a9079cccf8fa9d826e7a397c24fbedf0e11b49312bea629b58"}, "msgpax": {:hex, :msgpax, "2.3.0", "14f52ad249a3f77b5e2d59f6143e6c18a6e74f34666989e22bac0a465f9835cc", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "65c36846a62ed5615baf7d7d47babb6541313a6c0b6d2ff19354bd518f52df7e"}, + "nimble_options": {:hex, :nimble_options, "0.4.0", "c89babbab52221a24b8d1ff9e7d838be70f0d871be823165c94dd3418eea728f", [:mix], [], "hexpm", "e6701c1af326a11eea9634a3b1c62b475339ace9456c1a23ec3bc9a847bca02d"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.2", "b99ca56bbce410e9d5ee4f9155a212e942e224e259c7ebbf8f2c86ac21d4fa3c", [:mix], [], "hexpm", "98d51bd64d5f6a2a9c6bb7586ee8129e27dfaab1140b5a4753f24dac0ba27d2f"}, + "nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"}, "phoenix": {:hex, :phoenix, "1.6.6", "281c8ce8dccc9f60607346b72cdfc597c3dde134dd9df28dff08282f0b751754", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "807bd646e64cd9dc83db016199715faba72758e6db1de0707eef0a2da4924364"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, diff --git a/priv/repo/migrations/20220526070710_add_posts.exs b/priv/repo/migrations/20220526070710_add_posts.exs new file mode 100644 index 0000000..4825c8d --- /dev/null +++ b/priv/repo/migrations/20220526070710_add_posts.exs @@ -0,0 +1,11 @@ +defmodule Minisome.Storage.Repo.Migrations.AddPosts do + use Ecto.Migration + + def change do + create table(:posts) do + add(:posted, :utc_datetime, null: false) + add(:text, :text, null: false) + add(:tags, {:array, :string}, null: false, default: []) + end + end +end