aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.env.example2
-rw-r--r--.gitignore2
-rw-r--r--README.md25
-rw-r--r--config/config.exs34
-rw-r--r--config/runtime.exs15
-rw-r--r--lib/application.ex6
-rw-r--r--lib/command/addmeme.ex105
-rw-r--r--lib/command/command.ex57
-rw-r--r--lib/command/meme.ex84
-rw-r--r--lib/command/util.ex64
-rw-r--r--lib/consumer.ex50
-rw-r--r--lib/db/schema.ex55
-rw-r--r--lib/meme.ex42
-rw-r--r--lib/message.ex46
-rw-r--r--mix.exs16
-rw-r--r--mix.lock3
16 files changed, 411 insertions, 195 deletions
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..90dcdc8
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,2 @@
+DISCORD_TOKEN=NTM4MjA0NTYyMTU1MjQxNDgz.GHroNu.1j6C5W5ZrBDBsNcWyPJLAOtpCe0tJr3rPcj0iY
+DATABASE_URL=postgres://your_user:your_pass@server_ip/database_name
diff --git a/.gitignore b/.gitignore
index ffbe771..850d13f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,3 +26,5 @@ thulani-*.tar
/tmp/
/priv/
+
+/.env
diff --git a/README.md b/README.md
index 02c4078..4b4119f 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,28 @@
-# Thulani
+# thulani
-**TODO: Add description**
+Discord bot that stores / posts random memes and plays audio in voice chat.
-## Installation
+[Formerly written in Rust](https://pub.npry.dev/thulani). <!-- TODO: update branch !-->
-If [available in Hex](https://hex.pm/docs/publish), the package can be installed
-by adding `thulani` to your list of dependencies in `mix.exs`:
+## Installation
```elixir
def deps do
[
- {:thulani, "~> 0.1.0"}
+ {:thulani, "~> 0.1"}
]
end
```
-Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
-and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
-be found at <https://hexdocs.pm/thulani>.
+## Configuration
+
+```elixir
+config :thulani,
+ prefix: "!thulani "
+
+# optional: retrieved from DATABASE_URL env var if not configured
+config :thulani, Thulani.Repo, "psql:///user:pass@dbhost/db"
+# optional: retrieved from DISCORD_TOKEN env var if not configured
+config :nostrum, token: "my_discord_token"
+```
diff --git a/config/config.exs b/config/config.exs
index cfd4263..206f883 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1,7 +1,6 @@
import Config
config :nostrum,
- token: "NTM4MjA0NTYyMTU1MjQxNDgz.GHroNu.1j6C5W5ZrBDBsNcWyPJLAOtpCe0tJr3rPcj0iY",
gateway_intents: [
:guild_messages,
:guild_message_reactions,
@@ -17,7 +16,32 @@ config :logger, :console,
format: "[$level] $message ($metadata)\n",
metadata: [:module]
-config :thulani, Thulani.Repo,
- database: "memes",
- username: "thulani",
- hostname: "localhost"
+if config_env() != :prod && is_nil(Application.get_env(:thulani, :prefix)) do
+ config :thulani,
+ prefix: [
+ "!thulani ",
+ "!thulando ",
+ "!thulani madondo ",
+ "!thulan ",
+ "!thulando madondo "
+ ],
+ restricted: [
+ "!todd ",
+ "!toddlani ",
+ "!toddbert "
+ ]
+end
+
+common_prefixes = Application.get_env(:thulani, :prefix, ["!thulani "])
+restricted_prefixes = Application.get_env(:thulani, :restricted, [])
+
+wrap_list = fn x ->
+ if is_list(x) do
+ x
+ else
+ [x]
+ end
+end
+
+config :nosedrum,
+ prefix: wrap_list.(common_prefixes) ++ wrap_list.(restricted_prefixes)
diff --git a/config/runtime.exs b/config/runtime.exs
index becde76..0204d77 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -1 +1,16 @@
import Config
+import Dotenv
+
+if config_env() != :prod do
+ Dotenv.load!()
+end
+
+if is_nil(Application.get_env(:thulani, Thulani.Repo)) do
+ db_url = System.get_env("DATABASE_URL")
+ config :thulani, Thulani.Repo, url: db_url
+end
+
+if is_nil(Application.get_env(:nostrum, :token)) do
+ discord_token = System.get_env("DISCORD_TOKEN")
+ config :nostrum, token: discord_token
+end
diff --git a/lib/application.ex b/lib/application.ex
index f469b0d..958ab7d 100644
--- a/lib/application.ex
+++ b/lib/application.ex
@@ -4,11 +4,11 @@ defmodule Thulani do
use Application
@children [
- Thulani.DiscordConsumer,
{Phoenix.PubSub, name: :thulani_pubsub},
- Thulani.Command,
{Registry, keys: :unique, name: Thulani.Audio.Registry},
- Thulani.Repo
+ Thulani.Repo,
+ Thulani.DiscordConsumer,
+ Nosedrum.TextCommand.Storage.ETS
]
def start(_typ, _arg) do
diff --git a/lib/command/addmeme.ex b/lib/command/addmeme.ex
new file mode 100644
index 0000000..a4df2f3
--- /dev/null
+++ b/lib/command/addmeme.ex
@@ -0,0 +1,105 @@
+require Logger
+
+alias Nosedrum.TextCommand.Predicates
+alias Nostrum.Struct.Message
+
+alias Thulani.Command.Util
+alias Thulani.Schema.Meme
+alias Thulani.Schema.Image
+
+defmodule Thulani.Command.AddMeme do
+ @behaviour Nosedrum.TextCommand
+
+ @impl true
+ def aliases, do: ["addmeme"]
+
+ @impl true
+ # TODO
+ def usage, do: ["addmeme <title:str>"]
+
+ @impl true
+ def predicates, do: [&Predicates.guild_only/1]
+
+ @impl true
+ def description, do: "add a meme to the db"
+
+ @impl true
+ def command(
+ msg,
+ [title, content]
+ ) do
+ result =
+ Thulani.Repo.transaction(fn ->
+ %{
+ content: content,
+ title: title,
+ metadata: Util.metadata(msg)
+ }
+ |> Map.merge(media(msg))
+ |> dbg
+ |> Meme.insert_changeset()
+ |> Thulani.Repo.insert()
+ end)
+
+ case result do
+ {:ok, _meme} ->
+ Logger.debug("inserted meme ok")
+ msg |> Util.react!("👌")
+
+ {:error, changeset} ->
+ Logger.error("updating")
+
+ Ecto.Changeset.traverse_errors(
+ changeset,
+ fn {msg, opts} ->
+ dbg({msg, opts})
+ end
+ )
+ end
+
+ {:ok}
+ end
+
+ defp media(
+ %Message{
+ attachments: [
+ %Message.Attachment{
+ url: url,
+ filename: filename
+ }
+ ]
+ } = msg
+ ) do
+ data = Req.get!(url).body
+ hash = :crypto.hash(:sha, data)
+
+ existing_image = image_with_hash(hash) |> Thulani.Repo.one()
+
+ if is_nil(existing_image) do
+ Logger.debug("no existing image")
+
+ %{
+ image: %{
+ filename: filename,
+ data: data,
+ data_hash: hash,
+ metadata: Util.metadata(msg)
+ }
+ }
+ else
+ Logger.debug("found existing image", existing_image: existing_image)
+
+ %{
+ image_id: existing_image.id
+ }
+ end
+ end
+
+ defp media(_invalid), do: %{}
+
+ defp image_with_hash(hash) when is_binary(hash) do
+ import Ecto.Query
+
+ from(i in Image, where: i.data_hash == ^hash, limit: 1)
+ end
+end
diff --git a/lib/command/command.ex b/lib/command/command.ex
deleted file mode 100644
index 07be96c..0000000
--- a/lib/command/command.ex
+++ /dev/null
@@ -1,57 +0,0 @@
-alias Phoenix.PubSub
-alias Nostrum.Api
-
-defmodule Thulani.Command do
- use GenServer
- use Thulani.Command.Util
- require Logger
-
- def start_link(arg) do
- GenServer.start_link(__MODULE__, arg)
- end
-
- @impl true
- def init(_) do
- PubSub.subscribe(:thulani_pubsub, "cmd")
- {:ok, %{}}
- end
-
- @impl true
- def handle_info(Util.command("meme", "", _msg), state) do
- {:noreply, state}
- end
-
- @impl true
- def handle_info(Util.command("meme", rest, _msg), state) do
- case rest do
- "" -> Thulani.Meme
- end
-
- {:noreply, state}
- end
-
- def handle_info(Util.command("http", _rest, _msg), _state) do
- end
-
- def handle_info(Util.command("roll", rest, msg), state) do
- Logger.info("hi")
-
- case rest do
- " " <> expr ->
- case Thulani.Calc.eval(expr) do
- {:ok, val} -> Api.create_message!(msg.channel_id, "#{val}")
- {:error, err} -> Logger.error("BAD: #{err}")
- end
-
- _ ->
- Logger.error("nothing else in the message")
- end
-
- {:noreply, state}
- end
-
- def handle_info(_msg, state) do
- Logger.debug("unhandled message")
- {:noreply, state}
- end
-end
diff --git a/lib/command/meme.ex b/lib/command/meme.ex
index 5be4582..50e8769 100644
--- a/lib/command/meme.ex
+++ b/lib/command/meme.ex
@@ -1,56 +1,58 @@
-alias Nostrum.Struct.Message
+alias Nosedrum.TextCommand.Predicates
+alias Thulani.Command.Util
+alias Thulani.Schema.Meme
+alias Thulani.Schema.Image
+# alias Thulani.Schema.Audio
defmodule Thulani.Command.Meme do
- use GenServer
+ @behaviour Nosedrum.TextCommand
- @type audio_permission :: :unrestrict | boolean
+ @impl true
+ def aliases, do: ["meme"]
- defmodule Request do
- alias Thulani.Command.Meme
+ @impl true
+ def usage, do: ["meme <title:str>"]
- @type t ::
- %__MODULE__{
- name: :random,
- audio: Meme.audio_permission(),
- msg: Message.t()
- }
- | %__MODULE__{
- name: String.t(),
- audio: nil,
- msg: Message.t()
- }
+ @impl true
+ def predicates, do: [&Predicates.guild_only/1]
- @enforce_keys [:msg]
- defstruct name: :random, audio: :unrestrict, msg: nil
- end
+ @impl true
+ def description, do: "post a random meme"
- @spec random_meme(Message.t()) :: GenServer.cast()
- def random_meme(msg) do
- GenServer.cast(__MODULE__, %Request{msg: msg})
- end
+ @impl true
+ def command(msg, args) do
+ {:ok, meme} =
+ Thulani.Repo.transaction(fn ->
+ get(args)
+ |> Thulani.Repo.one()
+ |> Thulani.Repo.preload([:image, :audio, :metadata])
+ end)
- @spec meme(Message.t(), String.t()) :: GenServer.cast()
- @spec meme(Message.t(), String.t(), audio_permission) :: GenServer.cast()
- def meme(msg, name, audio \\ :unrestrict) do
- GenServer.cast(__MODULE__, %Request{name: name, msg: msg, audio: audio})
+ meme = post(meme)
+ msg |> Util.reply(meme)
end
- def start_link(arg) do
- GenServer.start_link(__MODULE__, arg)
- end
+ defp get([]), do: Thulani.Meme.random(Thulani.Meme.any())
+ defp get([name]), do: Thulani.Meme.by_name(name)
- @impl true
- def init(_) do
- {:ok, %{}}
+ @spec post(Meme.t()) :: Nostrum.Api.options()
+ defp post(%Meme{
+ content: content,
+ image: image
+ }) do
+ [content: content] ++ post_image(image)
end
- @impl true
- def handle_cast(%Request{name: :random, msg: _msg, audio: _audio}, state) do
- {:noreply, state}
- end
+ defp post_image(%Image{
+ filename: filename,
+ data: data
+ }),
+ do: [
+ file: %{
+ name: filename,
+ body: data
+ }
+ ]
- @impl true
- def handle_cast(%Request{name: _name, msg: _msg}, state) do
- {:noreply, state}
- end
+ defp post_image(_x), do: []
end
diff --git a/lib/command/util.ex b/lib/command/util.ex
index 8f2018f..d47dd59 100644
--- a/lib/command/util.ex
+++ b/lib/command/util.ex
@@ -1,18 +1,60 @@
defmodule Thulani.Command.Util do
- alias Nostrum.Snowflake
- @type qualified_channel :: {Snowflake.t(), Snowflake.t()}
+ alias Nostrum.Struct.Message
+ alias Nostrum.Struct.User
+ alias Nostrum.Struct.Emoji
+ alias Nostrum.Struct.Channel
+ alias Nostrum.Api
- defmacro __using__(_opts) do
- mod = __MODULE__
+ @spec reply(Message.t(), Api.options() | String.t()) ::
+ Api.create_message(Channel.id(), Message.t())
+ def reply(
+ %Message{
+ channel_id: channel_id
+ },
+ msg
+ )
+ when is_bitstring(msg) do
+ Api.create_message(channel_id, content: msg)
+ end
- quote do
- alias unquote(mod)
- end
+ def reply(
+ %Message{
+ channel_id: channel_id
+ },
+ opts
+ ) do
+ Api.create_message(channel_id, opts)
end
- defmacro command(word, rest, msg) do
- quote do
- {:cmd, " #{unquote(word)}" <> unquote(rest), unquote(msg)}
- end
+ @spec reply!(Message.t(), Api.options() | String.t()) :: nil
+ def reply!(msg, opts), do: {:ok} = reply(msg, opts)
+
+ @spec react(Message.t(), Api.emoji()) ::
+ Api.create_reaction(Channel.id(), Message.id(), Emoji.t())
+ def react(
+ %Message{
+ id: id,
+ channel_id: channel_id
+ },
+ reaction
+ ) do
+ Api.create_reaction(channel_id, id, reaction)
+ end
+
+ @spec react!(Message.t(), Api.emoji()) ::
+ Api.create_reaction(Channel.id(), Message.id(), Emoji.t())
+ def react!(msg, opts), do: {:ok} = react(msg, opts)
+
+ @spec metadata(Message.t()) :: Map.t()
+ def metadata(%Message{
+ author: %User{
+ id: user_id
+ },
+ timestamp: timestamp
+ }) do
+ %{
+ created_by: user_id,
+ created: timestamp
+ }
end
end
diff --git a/lib/consumer.ex b/lib/consumer.ex
index a594846..406ddeb 100644
--- a/lib/consumer.ex
+++ b/lib/consumer.ex
@@ -1,26 +1,56 @@
require Logger
-alias Nostrum.Struct.Message
-alias Phoenix.PubSub
-
defmodule Thulani.DiscordConsumer do
use Nostrum.Consumer
@spec handle_event(Nostrum.Consumer.message_create()) :: nil
+
def handle_event({
:MESSAGE_CREATE,
- message = %Message{},
+ msg,
_ws_state
}) do
- case Thulani.Message.validate(message) do
- {:ok, command} ->
- Logger.debug(%{command: command})
- PubSub.broadcast!(:thulani_pubsub, "cmd", {:cmd, command, message})
+ unless msg.author.bot do
+ case Nosedrum.TextCommand.Invoker.Split.handle_message(msg) do
+ {:error, {:unknown_subcommand, name, :known, known}} ->
+ Logger.error("unknown subcommand", name: name, known: known)
+
+ {:error, other} ->
+ Logger.error("unknown error", data: other)
- :ignore ->
- nil
+ {:ok, data} ->
+ Logger.info("handled command ok", data: data)
+
+ other ->
+ Logger.warning("unhandled textcommand case", result: other)
+ end
end
end
+ def handle_event({
+ :READY,
+ data,
+ _ws_state
+ }) do
+ Logger.info("bot ready", data: data)
+
+ commands()
+ |> Stream.flat_map(fn command_mod ->
+ apply(command_mod, :aliases, [])
+ |> Stream.zip(Stream.cycle([command_mod]))
+ end)
+ |> Stream.each(fn {name, mod} ->
+ :ok = Nosedrum.TextCommand.Storage.ETS.add_command([name], mod)
+ Logger.info("registered command", command_name: name, mod: mod)
+ end)
+ |> Stream.run()
+ end
+
def handle_event(_event), do: :noop
+
+ defp commands,
+ do: [
+ Thulani.Command.Meme,
+ Thulani.Command.AddMeme
+ ]
end
diff --git a/lib/db/schema.ex b/lib/db/schema.ex
index 860fc15..ec6f4e5 100644
--- a/lib/db/schema.ex
+++ b/lib/db/schema.ex
@@ -1,5 +1,6 @@
defmodule Thulani.Schema do
alias Thulani.Schema
+ import Ecto.Changeset
defmodule Meme do
use Ecto.Schema
@@ -10,10 +11,24 @@ defmodule Thulani.Schema do
field(:title, :string)
field(:content, :string)
- has_one(:image, Image)
- has_one(:audio, Audio)
+ belongs_to(:image, Schema.Image)
+ belongs_to(:audio, Schema.Audio)
- belongs_to(:metadata, Metadata)
+ belongs_to(:metadata, Schema.Metadata)
+ end
+
+ def insert_changeset(params \\ %{}) do
+ changeset(%Meme{}, params)
+ end
+
+ def changeset(meme, params \\ %{}) do
+ meme
+ |> cast(params, [:title, :content, :image_id, :audio_id])
+ |> validate_required([:title])
+ |> cast_assoc(:image)
+ |> cast_assoc(:audio)
+ |> cast_assoc(:metadata, required: true)
+ |> unique_constraint(:title, name: :text_memes_title_key)
end
end
@@ -23,7 +38,7 @@ defmodule Thulani.Schema do
schema "invocation_records" do
field(:user_id, :integer)
field(:message_id, :integer)
- field(:time, :utc_datetime)
+ field(:time, :utc_datetime_usec)
field(:random, :boolean)
belongs_to(:meme, Schema.Meme)
@@ -38,9 +53,15 @@ defmodule Thulani.Schema do
has_one(:audio, Schema.Audio)
has_one(:image, Schema.Image)
- field(:created, :utc_datetime)
+ field(:created, :utc_datetime_usec)
field(:created_by, :integer)
end
+
+ def changeset(meta, params \\ %{}) do
+ meta
+ |> cast(params, [:created, :created_by])
+ |> validate_required([:created, :created_by])
+ end
end
defmodule Audio do
@@ -48,11 +69,19 @@ defmodule Thulani.Schema do
schema "audio" do
belongs_to(:metadata, Schema.Metadata)
- belongs_to(:meme, Schema.Meme)
+ has_many(:memes, Schema.Meme)
field(:data, :binary)
field(:data_hash, :binary)
end
+
+ def changeset(image, params \\ %{}) do
+ image
+ |> cast(params, [:data, :data_hash])
+ |> cast_assoc(:metadata, required: true)
+ |> validate_required([:data, :data_hash])
+ |> unique_constraint(:data_hash, name: :image_hash)
+ end
end
defmodule Image do
@@ -60,12 +89,20 @@ defmodule Thulani.Schema do
schema "images" do
belongs_to(:metadata, Schema.Metadata)
- belongs_to(:meme, Schema.Meme)
+ has_many(:memes, Schema.Meme)
field(:data, :binary)
field(:data_hash, :binary)
field(:filename, :string)
end
+
+ def changeset(image, params \\ %{}) do
+ image
+ |> cast(params, [:data, :data_hash, :filename])
+ |> cast_assoc(:metadata, required: true)
+ |> validate_required([:data, :data_hash, :filename])
+ |> unique_constraint(:data_hash, name: :image_hash)
+ end
end
defmodule Tombstone do
@@ -75,9 +112,9 @@ defmodule Thulani.Schema do
field(:meme_id, :integer)
field(:deleted_by, :integer)
- field(:deleted_at, :utc_datetime)
+ field(:deleted_at, :utc_datetime_usec)
- has_one(:metadata, Schema.Metadata)
+ belongs_to(:metadata, Schema.Metadata)
end
end
end
diff --git a/lib/meme.ex b/lib/meme.ex
new file mode 100644
index 0000000..36048f8
--- /dev/null
+++ b/lib/meme.ex
@@ -0,0 +1,42 @@
+import Ecto.Query
+
+alias Thulani.Schema.Meme
+
+defmodule Thulani.Meme do
+ @type kind :: :audio | :text | :image
+
+ def any, do: [:audio, :text, :image] |> MapSet.new()
+ def silent, do: [:text, :image] |> MapSet.new()
+
+ @spec random(MapSet.t(kind) | kind) :: {:ok, Meme} | {:err, term()}
+ def random(allowed_kinds \\ any())
+
+ def random(allowed_kinds) when is_struct(allowed_kinds) do
+ from(m in Meme,
+ where: ^random_predicates(allowed_kinds),
+ order_by: fragment("random()"),
+ limit: 1
+ )
+ end
+
+ def random(kind) when is_atom(kind) do
+ [kind] |> MapSet.new() |> random
+ end
+
+ defp random_predicates(allowed_kinds) do
+ not_permitted = any() |> MapSet.difference(allowed_kinds)
+
+ not_permitted
+ |> Enum.map(fn x -> require_nil(x) end)
+ |> Enum.reduce(true, fn acc, x -> dynamic([], ^acc and ^x) end)
+ end
+
+ defp require_nil(:audio), do: dynamic([m], is_nil(m.audio_id))
+ defp require_nil(:image), do: dynamic([m], is_nil(m.image_id))
+ defp require_nil(:text), do: dynamic([m], is_nil(m.content))
+
+ @spec by_name(String.t()) :: Meme
+ def by_name(name) when is_bitstring(name) do
+ from(m in Meme, where: m.title == ^name)
+ end
+end
diff --git a/lib/message.ex b/lib/message.ex
deleted file mode 100644
index 1f3f08b..0000000
--- a/lib/message.ex
+++ /dev/null
@@ -1,46 +0,0 @@
-require Logger
-
-alias Nostrum.Struct.Message
-alias Nostrum.Struct.User
-alias Nostrum.Api
-
-defmodule Thulani.Message do
- @prefix Application.compile_env(:thulani, :prefix, "!thulani")
-
- @spec validate(Message.t()) :: {:ok, String.t()} | :ignore
- def validate(%Message{
- content: @prefix <> rest,
- guild_id: guild_id,
- author: %User{
- bot: nil
- }
- })
- when guild_id != nil do
- {:ok, rest}
- end
-
- def validate(%Message{
- guild_id: guild_id,
- author: %User{
- username: username,
- bot: bot
- }
- })
- when guild_id != nil do
- {:ok, guild} = Api.get_guild(guild_id)
-
- Logger.debug(%{
- ignored: true,
- guild: guild.name,
- author: "#{username}# (bot: #{bot})"
- })
-
- :ignore
- end
-
- def validate(msg) do
- Logger.debug(%{validate: "unmatched validate", msg: msg})
-
- :ignore
- end
-end
diff --git a/mix.exs b/mix.exs
index 1a801e5..a2af63c 100644
--- a/mix.exs
+++ b/mix.exs
@@ -14,14 +14,22 @@ defmodule Thulani.MixProject do
def application do
[
mod: {Thulani, []},
- extra_applications: [:logger, :crypto]
+ extra_applications: [:logger, :crypto] ++ debug_applications()
]
end
+ defp debug_applications() do
+ if Mix.env() != :prod do
+ [:observer, :runtime_tools, :wx, :debugger]
+ else
+ []
+ end
+ end
+
defp deps do
[
- {:nostrum, "~> 0.10"},
- {:nosedrum, "~> 0.5"},
+ {:nostrum, "~> 0.10", override: true},
+ {:nosedrum, github: "jchristgit/nosedrum"},
{:witchcraft, "~> 1.0"},
{:quark, "~> 2.3"},
{:exceptional, "~> 2.1"},
@@ -31,6 +39,8 @@ defmodule Thulani.MixProject do
{:rustler, "~> 0.34"},
{:ecto_sql, "~> 3.11"},
{:postgrex, "~> 0.14"},
+ {:dotenv, "~> 3.0"},
+ {:req, "~> 0.5"},
{:credo, "~> 1.0", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.1", only: :dev, runtime: false}
]
diff --git a/mix.lock b/mix.lock
index 115d022..5af7dee 100644
--- a/mix.lock
+++ b/mix.lock
@@ -11,6 +11,7 @@
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
+ "dotenv": {:hex, :dotenv, "3.1.0", "d5a76bb17dc28acfb5236655bbe5776a1ffbdc8d3589fc992de0882b3ae4bc10", [:mix], [], "hexpm", "01bed84d21bedd8739aebad16489a3ce12d19c2d59af87377da65ebb361980d3"},
"ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"},
"ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"},
"ed25519": {:hex, :ed25519, "1.4.1", "479fb83c3e31987c9cad780e6aeb8f2015fb5a482618cdf2a825c9aff809afc4", [:mix], [], "hexpm", "0dacb84f3faa3d8148e81019ca35f9d8dcee13232c32c9db5c2fb8ff48c80ec7"},
@@ -29,7 +30,7 @@
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
- "nosedrum": {:hex, :nosedrum, "0.5.0", "fccf1e6bc430f3464be56d9a9afb49d83ddcc12b9ef4abbcc48f1896ba4b019a", [:mix], [{:nostrum, "~> 0.7", [hex: :nostrum, repo: "hexpm", optional: false]}], "hexpm", "41e2963724d6dfe9b9469bda92a59236e21a2c3fda56048048c8dbdeddb7b81d"},
+ "nosedrum": {:git, "https://github.com/jchristgit/nosedrum.git", "da5e329b581380bb9f36812e705c972a375a582c", []},
"nostrum": {:hex, :nostrum, "0.10.0", "c54395970ee630cf577472f13baad1fb8eb02b7cd48868bc62b8a8a153d62cde", [:mix], [{:castle, "~> 0.3.0", [hex: :castle, repo: "hexpm", optional: false]}, {:certifi, "~> 2.13", [hex: :certifi, repo: "hexpm", optional: false]}, {:ezstd, "~> 1.1", [hex: :ezstd, repo: "hexpm", optional: true]}, {:gun, "~> 2.0", [hex: :gun, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "dd507bcd7c9bd7a5ec6ee69f9207f8101603caaedd14b204d3be3842d95be948"},
"ok": {:hex, :ok, "2.3.0", "0a3d513ec9038504dc5359d44e14fc14ef59179e625563a1a144199cdc3a6d30", [:mix], [], "hexpm", "f0347b3f8f115bf347c704184b33cf084f2943771273f2b98a3707a5fa43c4d5"},
"operator": {:hex, :operator, "0.2.1", "4572312bbd3e63a5c237bf15c3a7670d568e3651ea744289130780006e70e5f5", [:mix], [], "hexpm", "1990cc6dc651d7fff04636eef06fc64e6bc1da83a1da890c08ca3432e17e267a"},