Add some rudimentary feed and post features, add decoding of messages

This commit is contained in:
Mikko Ahlroth 2022-06-12 20:17:51 +03:00
parent 85961f6008
commit 74ebea218e
41 changed files with 828 additions and 159 deletions

View file

@ -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"]

View file

@ -1,2 +1,2 @@
erlang 24.2.1 erlang 24.3.3
elixir 1.13.2-otp-24 elixir 1.13.4-otp-24

View file

@ -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;
@ -46,12 +52,12 @@
transition: opacity 1s ease-out; transition: opacity 1s ease-out;
} }
.phx-loading{ .phx-loading {
cursor: wait; cursor: wait;
} }
.phx-modal { .phx-modal {
opacity: 1!important; opacity: 1 !important;
position: fixed; position: fixed;
z-index: 1; z-index: 1;
left: 0; left: 0;
@ -59,7 +65,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: auto; overflow: auto;
background-color: rgba(0,0,0,0.4); background-color: rgba(0, 0, 0, 0.4);
} }
.phx-modal-content { .phx-modal-content {
@ -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;
} }

View file

@ -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

View file

@ -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

View file

@ -1,76 +1,106 @@
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")
database_path = port = get_env("WEB_PORT", 4000, :int)
System.get_env("DATABASE_PATH") || api_port = get_env("API_PORT", 33101, :int)
raise """
environment variable DATABASE_PATH is missing.
For example: /etc/minisome/minisome.db
"""
config :minisome, Minisome.Storage.Repo, cond do
database: database_path, config_env() == :dev ->
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5") 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. config :minisome, Minisome.Web.Endpoint,
# A default value is used in config/dev.exs and config/test.exs but you # Binding to loopback ipv4 address prevents access from other machines.
# want to use a different value for prod and you most likely don't want # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
# to check this value into version control, so we use an environment http: [ip: {127, 0, 0, 1}, port: port],
# variable instead. check_origin: false,
secret_key_base = code_reloader: true,
System.get_env("SECRET_KEY_BASE") || debug_errors: true,
raise """ secret_key_base: "MR3TEt40ApczckU3+IRzAi91t5iNhbiRt4l0ChL30DdADwAwtdfa++i+NX0ezfc1",
environment variable SECRET_KEY_BASE is missing. watchers: [
You can generate one by calling: mix phx.gen.secret # 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" config :minisome, Minisome.API.Endpoint,
port = String.to_integer(System.get_env("WEB_PORT") || "4000") http: [ip: {127, 0, 0, 1}, port: api_port],
api_port = String.to_integer(System.get_env("API_PORT") || "33101") check_origin: false,
code_reloader: true,
debug_errors: true,
secret_key_base: "MR3TEt40ApczckU3+IRzAi91t5iNhbiRt4l0ChL30DdADwAwtdfa++i+NX0ezfc1"
config :minisome, Minisome.Web.Endpoint, config_env() == :prod ->
url: [host: host, port: 443], database_path =
http: [ System.get_env("DATABASE_PATH") ||
# Enable IPv6 and bind on all interfaces. raise """
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. environment variable DATABASE_PATH is missing.
# See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html For example: /etc/minisome/minisome.db
# 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, config :minisome, Minisome.Storage.Repo,
url: [host: host, port: api_port], database: database_path,
http: [ pool_size: get_env("POOL_SIZE", 5, :int)
# 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 # 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
# If you are doing OTP releases, you need to instruct Phoenix # want to use a different value for prod and you most likely don't want
# to start each relevant endpoint: # to check this value into version control, so we use an environment
# # variable instead.
# config :minisome, Minisome.Web.Endpoint, server: true secret_key_base =
# System.get_env("SECRET_KEY_BASE") ||
# Then you can assemble a release by calling `mix release`. raise """
# See `mix help release` for more information. 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 end

View file

@ -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

View file

@ -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

View 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

View 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

View file

@ -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{

View file

@ -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

View 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

View 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

View file

@ -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,37 +24,52 @@ 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
%ProtocolMessage{ {:ok,
library: library_module, %ProtocolMessage{
type: type, library: library_module,
version: %Version{ type: type,
major: major_version, version: %Version{
minor: minor_version, major: major_version,
patch: patch_version minor: minor_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

View file

@ -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

View 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

View 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

View file

@ -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

View file

@ -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
View 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

View file

@ -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 """

View 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
View 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
View 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
View 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

View 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

View file

@ -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

View file

10
lib/storage/some/post.ex Normal file
View 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

View 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
View 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

View 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
View 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

View 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>

View file

@ -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

View file

@ -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 %>

View file

@ -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

View file

@ -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

View file

@ -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"},

View 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