Add some rudimentary feed and post features, add decoding of messages
This commit is contained in:
parent
85961f6008
commit
74ebea218e
41 changed files with 828 additions and 159 deletions
|
@ -1,4 +1,5 @@
|
||||||
[
|
[
|
||||||
|
plugins: [Phoenix.LiveView.HTMLFormatter],
|
||||||
import_deps: [:ecto, :phoenix],
|
import_deps: [:ecto, :phoenix],
|
||||||
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
|
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
|
||||||
subdirectories: ["priv/*/migrations"]
|
subdirectories: ["priv/*/migrations"]
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
erlang 24.2.1
|
erlang 24.3.3
|
||||||
elixir 1.13.2-otp-24
|
elixir 1.13.4-otp-24
|
||||||
|
|
|
@ -8,27 +8,33 @@
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-info {
|
.alert-info {
|
||||||
color: #31708f;
|
color: #31708f;
|
||||||
background-color: #d9edf7;
|
background-color: #d9edf7;
|
||||||
border-color: #bce8f1;
|
border-color: #bce8f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-warning {
|
.alert-warning {
|
||||||
color: #8a6d3b;
|
color: #8a6d3b;
|
||||||
background-color: #fcf8e3;
|
background-color: #fcf8e3;
|
||||||
border-color: #faebcc;
|
border-color: #faebcc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-danger {
|
.alert-danger {
|
||||||
color: #a94442;
|
color: #a94442;
|
||||||
background-color: #f2dede;
|
background-color: #f2dede;
|
||||||
border-color: #ebccd1;
|
border-color: #ebccd1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert p {
|
.alert p {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert:empty {
|
.alert:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invalid-feedback {
|
.invalid-feedback {
|
||||||
color: #a94442;
|
color: #a94442;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -95,26 +101,55 @@
|
||||||
.fade-in {
|
.fade-in {
|
||||||
animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys;
|
animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-out {
|
.fade-out {
|
||||||
animation: 0.2s ease-out 0s normal forwards 1 fade-out-keys;
|
animation: 0.2s ease-out 0s normal forwards 1 fade-out-keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-in-scale-keys {
|
@keyframes fade-in-scale-keys {
|
||||||
0% { scale: 0.95; opacity: 0; }
|
0% {
|
||||||
100% { scale: 1.0; opacity: 1; }
|
scale: 0.95;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
scale: 1.0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-out-scale-keys {
|
@keyframes fade-out-scale-keys {
|
||||||
0% { scale: 1.0; opacity: 1; }
|
0% {
|
||||||
100% { scale: 0.95; opacity: 0; }
|
scale: 1.0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
scale: 0.95;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-in-keys {
|
@keyframes fade-in-keys {
|
||||||
0% { opacity: 0; }
|
0% {
|
||||||
100% { opacity: 1; }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-out-keys {
|
@keyframes fade-out-keys {
|
||||||
0% { opacity: 1; }
|
0% {
|
||||||
100% { opacity: 0; }
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea#post_text {
|
||||||
|
min-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,10 @@
|
||||||
# General application configuration
|
# General application configuration
|
||||||
import Config
|
import Config
|
||||||
|
|
||||||
|
minisome_mime_type = "text/minisome"
|
||||||
|
|
||||||
config :minisome,
|
config :minisome,
|
||||||
|
mime_type: minisome_mime_type,
|
||||||
ecto_repos: [Minisome.Storage.Repo]
|
ecto_repos: [Minisome.Storage.Repo]
|
||||||
|
|
||||||
# Configures the endpoints
|
# Configures the endpoints
|
||||||
|
@ -37,6 +40,10 @@ config :logger, :console,
|
||||||
format: "$time $metadata[$level] $message\n",
|
format: "$time $metadata[$level] $message\n",
|
||||||
metadata: [:request_id]
|
metadata: [:request_id]
|
||||||
|
|
||||||
|
config :mime, :types, %{
|
||||||
|
minisome_mime_type => ["minisome"]
|
||||||
|
}
|
||||||
|
|
||||||
# Use Jason for JSON parsing in Phoenix
|
# Use Jason for JSON parsing in Phoenix
|
||||||
config :phoenix, :json_library, Jason
|
config :phoenix, :json_library, Jason
|
||||||
|
|
||||||
|
|
|
@ -2,36 +2,9 @@ import Config
|
||||||
|
|
||||||
# Configure your database
|
# Configure your database
|
||||||
config :minisome, Minisome.Storage.Repo,
|
config :minisome, Minisome.Storage.Repo,
|
||||||
database: Path.expand("../minisome_dev.db", Path.dirname(__ENV__.file)),
|
|
||||||
pool_size: 5,
|
pool_size: 5,
|
||||||
show_sensitive_data_on_connection_error: true
|
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
|
# ## SSL Support
|
||||||
#
|
#
|
||||||
# In order to use HTTPS in development, a self-signed
|
# In order to use HTTPS in development, a self-signed
|
||||||
|
|
|
@ -1,18 +1,52 @@
|
||||||
import Config
|
import Config
|
||||||
|
import Minisome.ConfigHelpers, only: [get_env: 1, get_env: 2, get_env: 3]
|
||||||
|
|
||||||
# config/runtime.exs is executed for all environments, including
|
# config/runtime.exs is executed for all environments, including
|
||||||
# during releases. It is executed after compilation and before the
|
# during releases. It is executed after compilation and before the
|
||||||
# system starts, so it is typically used to load production configuration
|
# system starts, so it is typically used to load production configuration
|
||||||
# and secrets from environment variables or elsewhere. Do not define
|
# and secrets from environment variables or elsewhere. Do not define
|
||||||
# any compile-time configuration in here, as it won't be applied.
|
# 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
|
# 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
|
if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
|
||||||
config :minisome, Minisome.Web.Endpoint, server: true
|
config :minisome, Minisome.Web.Endpoint, server: true
|
||||||
end
|
end
|
||||||
|
|
||||||
if config_env() == :prod do
|
host = get_env("PHX_HOST", "localhost")
|
||||||
|
port = get_env("WEB_PORT", 4000, :int)
|
||||||
|
api_port = get_env("API_PORT", 33101, :int)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
config_env() == :dev ->
|
||||||
|
config :minisome, Minisome.Storage.Repo,
|
||||||
|
database:
|
||||||
|
get_env("DATABASE_PATH", Path.expand("../minisome_dev.db", Path.dirname(__ENV__.file)))
|
||||||
|
|
||||||
|
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)]}
|
||||||
|
]
|
||||||
|
|
||||||
|
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_env() == :prod ->
|
||||||
database_path =
|
database_path =
|
||||||
System.get_env("DATABASE_PATH") ||
|
System.get_env("DATABASE_PATH") ||
|
||||||
raise """
|
raise """
|
||||||
|
@ -22,7 +56,7 @@ if config_env() == :prod do
|
||||||
|
|
||||||
config :minisome, Minisome.Storage.Repo,
|
config :minisome, Minisome.Storage.Repo,
|
||||||
database: database_path,
|
database: database_path,
|
||||||
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5")
|
pool_size: get_env("POOL_SIZE", 5, :int)
|
||||||
|
|
||||||
# The secret key base is used to sign/encrypt cookies and other secrets.
|
# 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
|
# A default value is used in config/dev.exs and config/test.exs but you
|
||||||
|
@ -36,10 +70,6 @@ if config_env() == :prod do
|
||||||
You can generate one by calling: mix phx.gen.secret
|
You can generate one by calling: mix phx.gen.secret
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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.Web.Endpoint,
|
config :minisome, Minisome.Web.Endpoint,
|
||||||
url: [host: host, port: 443],
|
url: [host: host, port: 443],
|
||||||
http: [
|
http: [
|
||||||
|
|
|
@ -25,17 +25,16 @@ defmodule Minisome.API.Client.Message do
|
||||||
@doc """
|
@doc """
|
||||||
Parse a binary into a RawMessage.
|
Parse a binary into a RawMessage.
|
||||||
|
|
||||||
The binary must have the OpenSSH style signature right at the start, then the base 64 encoded
|
The binary must have the OpenSSH style signature right at the start, followed by two newlines, then the payload after that.
|
||||||
payload after that.
|
|
||||||
"""
|
"""
|
||||||
@spec parse_message(binary()) :: RawMessage.t() | nil
|
@spec parse_message(binary()) :: RawMessage.t() | nil
|
||||||
def parse_message(data) do
|
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(),
|
sig_data = sig_part <> SSH.openssh_sig_end(),
|
||||||
{:ok, signature} <- SSH.parse_signature(sig_data),
|
{:ok, signature} <- SSH.parse_signature(sig_data) do
|
||||||
{:ok, parsed_data} <- data_part |> String.trim() |> Base.decode64() do
|
|
||||||
%RawMessage{
|
%RawMessage{
|
||||||
data: parsed_data,
|
data: data_part,
|
||||||
signature: signature
|
signature: signature
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -46,10 +45,6 @@ defmodule Minisome.API.Client.Message do
|
||||||
{:split, _} ->
|
{:split, _} ->
|
||||||
Logger.warning("Unable to parse message due to missing footer")
|
Logger.warning("Unable to parse message due to missing footer")
|
||||||
nil
|
nil
|
||||||
|
|
||||||
:error ->
|
|
||||||
Logger.warning("Unable to base64 decode the message data")
|
|
||||||
nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -71,10 +66,6 @@ defmodule Minisome.API.Client.Message do
|
||||||
"""
|
"""
|
||||||
@spec format_message(RawMessage.t()) :: String.t()
|
@spec format_message(RawMessage.t()) :: String.t()
|
||||||
def format_message(%RawMessage{} = msg) do
|
def format_message(%RawMessage{} = msg) do
|
||||||
"""
|
"#{SSH.format_signature(msg.signature)}\n\n#{msg.data}"
|
||||||
#{SSH.format_signature(msg.signature)}
|
|
||||||
|
|
||||||
#{Base.encode64(msg.data)}
|
|
||||||
"""
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,9 +29,6 @@ defmodule Minisome.API.Client.MessageLibraries.Auth do
|
||||||
@impl GenericLibrary
|
@impl GenericLibrary
|
||||||
@spec decode(ProtocolMessage.t()) :: {:ok, any()} | {:error, atom()}
|
@spec decode(ProtocolMessage.t()) :: {:ok, any()} | {:error, atom()}
|
||||||
def decode(%ProtocolMessage{} = message) do
|
def decode(%ProtocolMessage{} = message) do
|
||||||
case Map.get(@message_mapping, message.type) do
|
GenericLibrary.generic_decode(@message_mapping, message)
|
||||||
nil -> {:error, :unknown_type}
|
|
||||||
module -> module.decode(message)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
34
lib/api/client/message_libraries/feed/feed.ex
Normal file
34
lib/api/client/message_libraries/feed/feed.ex
Normal file
|
@ -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
|
48
lib/api/client/message_libraries/feed/feed_message.ex
Normal file
48
lib/api/client/message_libraries/feed/feed_message.ex
Normal file
|
@ -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
|
|
@ -1,14 +1,26 @@
|
||||||
defmodule Minisome.API.Client.MessageLibraries.GenericLibrary do
|
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 library_name() :: String.t()
|
||||||
@callback encoded_version() :: Version.t()
|
@callback encoded_version() :: Version.t()
|
||||||
@callback accepted_version() :: Version.t()
|
@callback accepted_version() :: Version.t()
|
||||||
@callback decode(Minisome.API.Client.TypedMessage.t()) :: any()
|
@callback decode(ProtocolMessage.t()) :: {:ok, any()} | {:error, atom()}
|
||||||
|
|
||||||
alias Minisome.API.Client.ProtocolMessage
|
|
||||||
|
|
||||||
def dispatch(message) do
|
def dispatch(message) do
|
||||||
end
|
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()
|
@spec encode(module(), {String.t(), any()}) :: ProtocolMessage.t()
|
||||||
def encode(module, {type, payload}) do
|
def encode(module, {type, payload}) do
|
||||||
%ProtocolMessage{
|
%ProtocolMessage{
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
defmodule Minisome.API.Client.MessageLibraries.GenericMessage do
|
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 encode(any()) :: {String.t(), any()}
|
||||||
@callback decode(any()) :: {:ok, any()} | :error
|
@callback decode(any()) :: {:ok, any()} | :error
|
||||||
|
|
||||||
|
|
34
lib/api/client/message_libraries/post/post.ex
Normal file
34
lib/api/client/message_libraries/post/post.ex
Normal file
|
@ -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
|
54
lib/api/client/message_libraries/post/post_message.ex
Normal file
54
lib/api/client/message_libraries/post/post_message.ex
Normal file
|
@ -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
|
|
@ -3,14 +3,20 @@ defmodule Minisome.API.Client.ProtocolHandler do
|
||||||
Handler for protocol messages to/from raw messages.
|
Handler for protocol messages to/from raw messages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
import Minisome.Utils.WithHelper
|
import Minisome.Utils.WithHelper
|
||||||
|
|
||||||
alias Minisome.API.Client.Message
|
alias Minisome.API.Client.Message
|
||||||
alias Minisome.API.Client.ProtocolMessage
|
alias Minisome.API.Client.ProtocolMessage
|
||||||
alias Minisome.API.Client.MessageLibraries.Auth
|
alias Minisome.API.Client.MessageLibraries.Auth
|
||||||
|
alias Minisome.API.Client.MessageLibraries.Post
|
||||||
|
alias Minisome.API.Client.MessageLibraries.Feed
|
||||||
|
|
||||||
@library_mapping %{
|
@library_mapping %{
|
||||||
Auth.library_name() => Auth
|
Auth.library_name() => Auth,
|
||||||
|
Post.library_name() => Post,
|
||||||
|
Feed.library_name() => Feed
|
||||||
}
|
}
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -18,14 +24,16 @@ defmodule Minisome.API.Client.ProtocolHandler do
|
||||||
"""
|
"""
|
||||||
@spec decode_raw(Message.RawMessage.t()) :: {:ok, ProtocolMessage.t()} | {:error, any()}
|
@spec decode_raw(Message.RawMessage.t()) :: {:ok, ProtocolMessage.t()} | {:error, any()}
|
||||||
def decode_raw(%Message.RawMessage{} = raw) do
|
def decode_raw(%Message.RawMessage{} = raw) do
|
||||||
with {:ok, data} = op(:unpack, Msgpax.unpack(raw.data)),
|
with {:ok, data} <- op(:unpack, Msgpax.unpack(raw.data)),
|
||||||
%{library: library, type: type, version: version, payload: payload} <-
|
{:destructure,
|
||||||
op(:destructure, data),
|
%{"library" => library, "type" => type, "version" => version, "payload" => payload}} <-
|
||||||
|
{:destructure, data},
|
||||||
library_module <- op(:library_type, Map.get(@library_mapping, library), :permissive),
|
library_module <- op(:library_type, Map.get(@library_mapping, library), :permissive),
|
||||||
{:version, [major_version, minor_version, patch_version]}
|
{:version, [major_version, minor_version, patch_version]}
|
||||||
when is_integer(major_version) and is_integer(minor_version) and
|
when is_integer(major_version) and is_integer(minor_version) and
|
||||||
is_integer(patch_version) <- {:version, version},
|
is_integer(patch_version) <- {:version, version},
|
||||||
true <- op(:type_type, is_binary(type)) do
|
true <- op(:type_type, is_binary(type)) do
|
||||||
|
{:ok,
|
||||||
%ProtocolMessage{
|
%ProtocolMessage{
|
||||||
library: library_module,
|
library: library_module,
|
||||||
type: type,
|
type: type,
|
||||||
|
@ -35,20 +43,33 @@ defmodule Minisome.API.Client.ProtocolHandler do
|
||||||
patch: patch_version
|
patch: patch_version
|
||||||
},
|
},
|
||||||
payload: payload
|
payload: payload
|
||||||
}
|
}}
|
||||||
else
|
else
|
||||||
{:unpack, err} -> {:error, {:invalid_msgpack, err}}
|
{:unpack, err} ->
|
||||||
{:destructure, _} -> {:error, :invalid_payload}
|
Logger.warn("Unable to msgpack decode protocol message: #{inspect(err)}")
|
||||||
{:library_type, _} -> {:error, :invalid_library_type}
|
{:error, {:invalid_msgpack, err}}
|
||||||
{:version, _} -> {:error, :invalid_version}
|
|
||||||
{:type_type, _} -> {:error, :invalid_type_type}
|
{: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
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Dispatch the protocol message to its library for decoding.
|
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
|
def handle_message(%ProtocolMessage{} = message) do
|
||||||
message.library.decode(message)
|
message.library.decode(message)
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,7 +23,6 @@ defmodule Minisome.API.AuthController do
|
||||||
data = Minisome.API.Client.Message.format_message(raw)
|
data = Minisome.API.Client.Message.format_message(raw)
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("text/minisome")
|
|
||||||
|> send_resp(200, data)
|
|> send_resp(200, data)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
54
lib/api/controllers/feed_controller.ex
Normal file
54
lib/api/controllers/feed_controller.ex
Normal file
|
@ -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
|
14
lib/api/plugs/api_content_type_plug.ex
Normal file
14
lib/api/plugs/api_content_type_plug.ex
Normal file
|
@ -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
|
|
@ -5,12 +5,15 @@ defmodule Minisome.API.Router do
|
||||||
import Phoenix.Controller
|
import Phoenix.Controller
|
||||||
|
|
||||||
pipeline :api do
|
pipeline :api do
|
||||||
plug :accepts, ["text"]
|
plug :accepts, ["minisome"]
|
||||||
|
plug Minisome.API.ContentTypePlug
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", Minisome.API do
|
scope "/", Minisome.API do
|
||||||
pipe_through :api
|
pipe_through :api
|
||||||
|
|
||||||
get "/key-info", AuthController, :key_info
|
get "/key-info", AuthController, :key_info
|
||||||
|
|
||||||
|
get "/feed", FeedController, :feed
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,8 @@ defmodule Minisome.Application do
|
||||||
@impl true
|
@impl true
|
||||||
def start(_type, _args) do
|
def start(_type, _args) do
|
||||||
children = [
|
children = [
|
||||||
|
{Finch, name: Minisome.Some.HTTP},
|
||||||
|
|
||||||
# Start the Ecto repository
|
# Start the Ecto repository
|
||||||
Minisome.Storage.Repo,
|
Minisome.Storage.Repo,
|
||||||
# Start the Telemetry supervisor
|
# Start the Telemetry supervisor
|
||||||
|
@ -16,9 +18,18 @@ defmodule Minisome.Application do
|
||||||
{Phoenix.PubSub, name: Minisome.PubSub},
|
{Phoenix.PubSub, name: Minisome.PubSub},
|
||||||
# Start the Endpoint (http/https)
|
# Start the Endpoint (http/https)
|
||||||
Minisome.Web.Endpoint,
|
Minisome.Web.Endpoint,
|
||||||
Minisome.API.Endpoint
|
Minisome.API.Endpoint,
|
||||||
# Start a worker by calling: Minisome.Worker.start_link(arg)
|
# Start a worker by calling: Minisome.Worker.start_link(arg)
|
||||||
# {Minisome.Worker, 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
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||||
|
|
35
lib/config_helpers.ex
Normal file
35
lib/config_helpers.ex
Normal file
|
@ -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
|
|
@ -155,7 +155,7 @@ defmodule Minisome.Crypto.SSH do
|
||||||
|
|
||||||
"#{@openssh_sig_start}\n" <>
|
"#{@openssh_sig_start}\n" <>
|
||||||
blob_split <>
|
blob_split <>
|
||||||
"#{@openssh_sig_end}\n"
|
"#{@openssh_sig_end}"
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
86
lib/some/fetcher/fetcher.ex
Normal file
86
lib/some/fetcher/fetcher.ex
Normal file
|
@ -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
|
6
lib/some/host/api.ex
Normal file
6
lib/some/host/api.ex
Normal file
|
@ -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
|
26
lib/some/http/http.ex
Normal file
26
lib/some/http/http.ex
Normal file
|
@ -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
|
37
lib/some/post/api.ex
Normal file
37
lib/some/post/api.ex
Normal file
|
@ -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
|
9
lib/some/post/federated_post.ex
Normal file
9
lib/some/post/federated_post.ex
Normal file
|
@ -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
|
|
@ -1,6 +1,5 @@
|
||||||
defmodule Minisome.Storage.Auth.Key do
|
defmodule Minisome.Storage.Auth.Key do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Query, only: [from: 2]
|
|
||||||
import Minisome.Storage.TypedSchema
|
import Minisome.Storage.TypedSchema
|
||||||
|
|
||||||
deftypedschema "keys" do
|
deftypedschema "keys" do
|
||||||
|
|
0
lib/storage/some/federated_post.ex
Normal file
0
lib/storage/some/federated_post.ex
Normal file
10
lib/storage/some/post.ex
Normal file
10
lib/storage/some/post.ex
Normal file
|
@ -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
|
35
lib/web/components/post.ex
Normal file
35
lib/web/components/post.ex
Normal file
|
@ -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"""
|
||||||
|
<section class="post">
|
||||||
|
<header>
|
||||||
|
<p>ID: <%= @post.id %></p>
|
||||||
|
</header>
|
||||||
|
<p class="post-body">
|
||||||
|
<%= @post.text %>
|
||||||
|
</p>
|
||||||
|
<footer>
|
||||||
|
<p>Posted on <%= Calendar.strftime(@post.posted, "%c") %><%= if @post.tags in [nil, []] do "" else " with tags " <> Enum.join(@post.tags, ",") end %>.</p>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec message(%{post: Minisome.API.Client.MessageLibraries.Post.PostMessage.t()}) ::
|
||||||
|
Phoenix.LiveView.Rendered.t()
|
||||||
|
def message(assigns) do
|
||||||
|
~H"""
|
||||||
|
<section class="post">
|
||||||
|
<p class="post-body">
|
||||||
|
<%= @post.text %>
|
||||||
|
</p>
|
||||||
|
<footer>
|
||||||
|
<p>Posted on <%= Calendar.strftime(@post.posted, "%c") %><%= if @post.tags in [nil, []] do "" else " with tags " <> Enum.join(@post.tags, ",") end %>.</p>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
23
lib/web/live/feed.ex
Normal file
23
lib/web/live/feed.ex
Normal file
|
@ -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
|
3
lib/web/live/feed.html.heex
Normal file
3
lib/web/live/feed.html.heex
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<%= for post <- @posts do %>
|
||||||
|
<Minisome.Web.Components.Post.message post={post} />
|
||||||
|
<% end %>
|
37
lib/web/live/posting.ex
Normal file
37
lib/web/live/posting.ex
Normal file
|
@ -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
|
19
lib/web/live/posting.html.heex
Normal file
19
lib/web/live/posting.html.heex
Normal file
|
@ -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 %>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h4>Created new post</h4>
|
||||||
|
|
||||||
|
<Minisome.Web.Components.Post.my post={@created} />
|
||||||
|
<% end %>
|
||||||
|
</.form>
|
|
@ -1,3 +1,9 @@
|
||||||
defmodule Minisome.Web.Live.PrivateMain do
|
defmodule Minisome.Web.Live.PrivateMain do
|
||||||
use Minisome.Web, :live_view
|
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
|
end
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
Hey!
|
Hey!
|
||||||
|
|
||||||
<%= live_redirect("Key testing", to: Routes.live_path(@socket, Minisome.Web.Live.Signing)) %>
|
<ul>
|
||||||
|
<li><%= live_redirect("Feed", to: Routes.live_path(@socket, Minisome.Web.Live.Feed)) %></li>
|
||||||
|
<li><%= live_redirect("Posting", to: Routes.live_path(@socket, Minisome.Web.Live.Posting)) %></li>
|
||||||
|
<li><%= live_redirect("Key testing", to: Routes.live_path(@socket, Minisome.Web.Live.Signing)) %></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<%= for post <- @posts do %>
|
||||||
|
<Minisome.Web.Components.Post.my post={post} />
|
||||||
|
<% end %>
|
||||||
|
|
|
@ -10,10 +10,6 @@ defmodule Minisome.Web.Router do
|
||||||
plug :put_secure_browser_headers
|
plug :put_secure_browser_headers
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :api do
|
|
||||||
plug :accepts, ["json"]
|
|
||||||
end
|
|
||||||
|
|
||||||
scope "/", Minisome.Web do
|
scope "/", Minisome.Web do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
|
@ -24,11 +20,8 @@ defmodule Minisome.Web.Router do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
live "/", Live.PrivateMain
|
live "/", Live.PrivateMain
|
||||||
|
live "/feed", Live.Feed
|
||||||
live "/key-testing", Live.Signing
|
live "/key-testing", Live.Signing
|
||||||
|
live "/posting", Live.Posting
|
||||||
end
|
end
|
||||||
|
|
||||||
# Other scopes may use custom stacks.
|
|
||||||
# scope "/api", Minisome.Web do
|
|
||||||
# pipe_through :api
|
|
||||||
# end
|
|
||||||
end
|
end
|
||||||
|
|
3
mix.exs
3
mix.exs
|
@ -49,7 +49,8 @@ defmodule Minisome.MixProject do
|
||||||
{:jason, "~> 1.2"},
|
{:jason, "~> 1.2"},
|
||||||
{:plug_cowboy, "~> 2.5"},
|
{:plug_cowboy, "~> 2.5"},
|
||||||
{:msgpax, "~> 2.3"},
|
{:msgpax, "~> 2.3"},
|
||||||
{:ex_doc, "~> 0.28.0", runtime: false}
|
{:ex_doc, "~> 0.28.0", runtime: false},
|
||||||
|
{:finch, "~> 0.12.0"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
5
mix.lock
5
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"},
|
"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"},
|
"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"},
|
"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"},
|
"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"},
|
"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"},
|
"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"},
|
"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": {: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_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"},
|
"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"},
|
"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"},
|
"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_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": {: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_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"},
|
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
|
||||||
|
|
11
priv/repo/migrations/20220526070710_add_posts.exs
Normal file
11
priv/repo/migrations/20220526070710_add_posts.exs
Normal file
|
@ -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
|
Loading…
Reference in a new issue