diff options
| author | Nathan Perry <np@nathanperry.dev> | 2024-08-08 08:45:15 -0400 |
|---|---|---|
| committer | Nathan Perry <np@nathanperry.dev> | 2024-08-08 08:45:15 -0400 |
| commit | 7ccdb63fb7f4008b7d785c687b4f55b6b4291483 (patch) | |
| tree | b1c342020429e1b4acf4d8d1e84fde26e4a4d3f8 | |
| parent | 5b639185cca5a2d1dc18c99699065e8a5623dbf1 (diff) | |
adding and posting images working
| -rw-r--r-- | .env.example | 2 | ||||
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | README.md | 25 | ||||
| -rw-r--r-- | config/config.exs | 34 | ||||
| -rw-r--r-- | config/runtime.exs | 15 | ||||
| -rw-r--r-- | lib/application.ex | 6 | ||||
| -rw-r--r-- | lib/command/addmeme.ex | 105 | ||||
| -rw-r--r-- | lib/command/command.ex | 57 | ||||
| -rw-r--r-- | lib/command/meme.ex | 84 | ||||
| -rw-r--r-- | lib/command/util.ex | 64 | ||||
| -rw-r--r-- | lib/consumer.ex | 50 | ||||
| -rw-r--r-- | lib/db/schema.ex | 55 | ||||
| -rw-r--r-- | lib/meme.ex | 42 | ||||
| -rw-r--r-- | lib/message.ex | 46 | ||||
| -rw-r--r-- | mix.exs | 16 | ||||
| -rw-r--r-- | mix.lock | 3 |
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 @@ -26,3 +26,5 @@ thulani-*.tar /tmp/ /priv/ + +/.env @@ -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 @@ -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} ] @@ -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"}, |
