mirror of
https://codeberg.org/godmaire/sergei.git
synced 2024-09-19 15:58:26 +00:00
Rewrite Sergei in Elixir
This commit is contained in:
parent
62958752a6
commit
b8e7c0f87b
15 changed files with 412 additions and 85 deletions
4
.formatter.exs
Normal file
4
.formatter.exs
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
21
.gitignore
vendored
21
.gitignore
vendored
|
@ -1 +1,22 @@
|
|||
# The directory Mix will write compiled artifacts to.
|
||||
/_build/
|
||||
|
||||
# If you run "mix test --cover", coverage assets end up here.
|
||||
/cover/
|
||||
|
||||
# The directory Mix downloads your dependencies sources to.
|
||||
/deps/
|
||||
|
||||
# Where third-party dependencies like ExDoc output generated docs.
|
||||
/doc/
|
||||
|
||||
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||
erl_crash.dump
|
||||
|
||||
# Also ignore archive artifacts (built via "mix archive.build").
|
||||
*.ez
|
||||
|
||||
# Ignore package tarball (built via "mix hex.build").
|
||||
sergei-*.tar
|
||||
|
||||
.env
|
||||
|
|
33
Dockerfile
33
Dockerfile
|
@ -1,14 +1,27 @@
|
|||
FROM python:3
|
||||
FROM elixir:alpine AS build_stage
|
||||
|
||||
# Install dependencies
|
||||
RUN apt update
|
||||
RUN apt install -y ffmpeg
|
||||
RUN pip3 install discord.py python-dotenv pynacl
|
||||
# Config
|
||||
ENV MIX_ENV prod
|
||||
WORKDIR /opt/build
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY . .
|
||||
# Dependendies
|
||||
COPY mix.* ./
|
||||
COPY config ./config
|
||||
|
||||
RUN curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
|
||||
RUN chmod a+rx /usr/local/bin/yt-dlp
|
||||
RUN mix local.hex --force && \
|
||||
mix local.rebar --force && \
|
||||
mix deps.get --only prod && \
|
||||
mix deps.compile
|
||||
|
||||
CMD ["python3", "main.py"]
|
||||
# Build project
|
||||
COPY lib ./lib
|
||||
RUN mix release sergei
|
||||
|
||||
FROM elixir:alpine
|
||||
|
||||
WORKDIR /opt/sergei
|
||||
RUN apk add ffmpeg yt-dlp
|
||||
COPY --from=build_stage /opt/build/_build/prod/rel/sergei /opt/sergei
|
||||
|
||||
ENTRYPOINT ["/opt/sergei/bin/sergei"]
|
||||
CMD ["start"]
|
||||
|
|
10
Justfile
Normal file
10
Justfile
Normal file
|
@ -0,0 +1,10 @@
|
|||
set dotenv-load
|
||||
|
||||
deploy:
|
||||
docker build -t godmaire/sergei:latest
|
||||
docker tag godmaire/sergei:latest registry.digitalocean.com/godmaire/sergei:latest
|
||||
docker push registry.digitalocean.com/godmaire/sergei:latest
|
||||
|
||||
run:
|
||||
mix deps.get
|
||||
DISCORD_TOK=$DISCORD_TOK mix run --no-halt
|
4
config/config.exs
Normal file
4
config/config.exs
Normal file
|
@ -0,0 +1,4 @@
|
|||
import Config
|
||||
|
||||
config :nostrum,
|
||||
youtubedl: "/usr/bin/yt-dlp"
|
4
config/runtime.exs
Normal file
4
config/runtime.exs
Normal file
|
@ -0,0 +1,4 @@
|
|||
import Config
|
||||
|
||||
config :nostrum,
|
||||
token: System.get_env("DISCORD_TOK")
|
5
lib/sergei.ex
Normal file
5
lib/sergei.ex
Normal file
|
@ -0,0 +1,5 @@
|
|||
defmodule Sergei do
|
||||
@moduledoc """
|
||||
Sergei is poggers.
|
||||
"""
|
||||
end
|
24
lib/sergei/application.ex
Normal file
24
lib/sergei/application.ex
Normal file
|
@ -0,0 +1,24 @@
|
|||
defmodule Sergei.Application do
|
||||
@moduledoc false
|
||||
use Application
|
||||
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
# Start the Consumer
|
||||
Sergei.Consumer,
|
||||
Sergei.Player,
|
||||
Sergei.VoiceStateCache,
|
||||
{Plug.Cowboy, scheme: :http, plug: Server, options: [port: 8080]}
|
||||
]
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
# for other strategies and supported options
|
||||
opts = [
|
||||
strategy: :one_for_one,
|
||||
name: Sergei.Supervisor
|
||||
]
|
||||
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
end
|
102
lib/sergei/consumer.ex
Normal file
102
lib/sergei/consumer.ex
Normal file
|
@ -0,0 +1,102 @@
|
|||
defmodule Sergei.Consumer do
|
||||
use Nostrum.Consumer
|
||||
|
||||
alias Nostrum.Api
|
||||
|
||||
# Translate params to list of maps
|
||||
opt = fn type, name, desc, opts ->
|
||||
%{type: type, name: name, description: desc}
|
||||
|> Map.merge(Enum.into(opts, %{}))
|
||||
end
|
||||
|
||||
@play_opts [
|
||||
opt.(3, "url", "URL of the audio to play", required: true)
|
||||
]
|
||||
|
||||
@slash_commands [
|
||||
{"ping", "Pong", []},
|
||||
{"play", "Play some tunes", @play_opts}
|
||||
]
|
||||
|
||||
def start_link do
|
||||
Consumer.start_link(__MODULE__)
|
||||
end
|
||||
|
||||
# Bulk overwrite commands per guild. While this is efficient enough for now, moving
|
||||
# to global commands in the future is probably a good idea.
|
||||
@spec register_commands!(integer) :: :ok
|
||||
def register_commands!(guild_id) do
|
||||
commands =
|
||||
Enum.map(@slash_commands, fn {name, description, options} ->
|
||||
%{
|
||||
name: name,
|
||||
description: description,
|
||||
options: options
|
||||
}
|
||||
end)
|
||||
|
||||
case Api.bulk_overwrite_guild_application_commands(guild_id, commands) do
|
||||
{:ok, _res} -> :ok
|
||||
{:error, err} -> raise err
|
||||
end
|
||||
end
|
||||
|
||||
# Initialization of the Discord Client
|
||||
def handle_event({:READY, %{guilds: guilds} = _event, _ws_state}) do
|
||||
Api.update_status(:online, "some tunes", 0)
|
||||
|
||||
guilds
|
||||
|> Enum.map(fn guild -> guild.id end)
|
||||
|> Enum.each(®ister_commands!/1)
|
||||
end
|
||||
|
||||
def handle_event({:VOICE_STATE_UPDATE, state, _ws_state}) do
|
||||
Sergei.VoiceStateCache.update_state(state)
|
||||
end
|
||||
|
||||
# Handle interactions
|
||||
def handle_event({:INTERACTION_CREATE, interaction, _ws_state}) do
|
||||
response =
|
||||
case do_command(interaction) do
|
||||
{:ok, msg} ->
|
||||
%{type: 4, data: %{content: msg, flags: 2 ** 6}}
|
||||
|
||||
{:error, msg} ->
|
||||
%{type: 4, data: %{content: msg, flags: 2 ** 6}}
|
||||
end
|
||||
|
||||
Api.create_interaction_response!(interaction, response)
|
||||
end
|
||||
|
||||
# Ignore other events
|
||||
def handle_event(_event) do
|
||||
:noop
|
||||
end
|
||||
|
||||
# /ping
|
||||
def do_command(%{data: %{name: "ping"}}) do
|
||||
{:ok, "Pong"}
|
||||
end
|
||||
|
||||
# /play <url>
|
||||
def do_command(
|
||||
%{
|
||||
guild_id: guild_id,
|
||||
member: %{user: %{id: invoker_id}},
|
||||
data: %{name: "play", options: opts}
|
||||
} = _interaction
|
||||
) do
|
||||
[%{name: "url", value: url}] = opts
|
||||
|
||||
case Sergei.VoiceStateCache.get_state(invoker_id) do
|
||||
%{guild_id: id} = _res when guild_id != id ->
|
||||
{:error, "You're not connected to a voice channel in this server."}
|
||||
|
||||
%{channel_id: channel_id} = _res ->
|
||||
Sergei.Player.play(guild_id, channel_id, url)
|
||||
|
||||
nil ->
|
||||
{:error, "You are not in a voice channel."}
|
||||
end
|
||||
end
|
||||
end
|
87
lib/sergei/player.ex
Normal file
87
lib/sergei/player.ex
Normal file
|
@ -0,0 +1,87 @@
|
|||
defmodule Sergei.Player do
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
alias Nostrum.Voice
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(state) do
|
||||
Process.send_after(self(), :tick, 100)
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
# Client
|
||||
def play(guild_id, channel_id, url) do
|
||||
GenServer.call(__MODULE__, {:play, guild_id, channel_id, url})
|
||||
end
|
||||
|
||||
# Server
|
||||
@impl true
|
||||
def handle_info(:tick, state) do
|
||||
state
|
||||
|> Enum.each(fn {guild_id, %{url: url, paused: paused}} = _state ->
|
||||
if not Voice.playing?(guild_id) and not paused do
|
||||
Voice.play(guild_id, url, :ytdl)
|
||||
end
|
||||
end)
|
||||
|
||||
Process.send_after(self(), :tick, 100)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:play, guild_id, channel_id, url}, _from, state) do
|
||||
res = play_music(guild_id, channel_id, url)
|
||||
|
||||
state =
|
||||
Map.put(state, guild_id, %{
|
||||
url: url,
|
||||
paused: false
|
||||
})
|
||||
|
||||
{:reply, res, state}
|
||||
end
|
||||
|
||||
def play_music(guild_id, channel_id, url) do
|
||||
cond do
|
||||
Voice.get_channel_id(guild_id) != channel_id ->
|
||||
Logger.debug("Changing channels...")
|
||||
|
||||
Voice.leave_channel(guild_id)
|
||||
Voice.join_channel(guild_id, channel_id)
|
||||
|
||||
# Wait for the client to finish joining the voice channel
|
||||
# TODO: check the channel ID in a loop instead
|
||||
Process.sleep(500)
|
||||
|
||||
play_music(guild_id, channel_id, url)
|
||||
|
||||
Voice.playing?(guild_id) ->
|
||||
Logger.debug("Stopping playback...")
|
||||
|
||||
Voice.stop(guild_id)
|
||||
play_music(guild_id, channel_id, url)
|
||||
|
||||
Voice.ready?(guild_id) ->
|
||||
case Voice.play(guild_id, url, :ytdl) do
|
||||
:ok ->
|
||||
Logger.info("Playing #{url}")
|
||||
{:ok, "Playing music..."}
|
||||
|
||||
{:error, res} ->
|
||||
Logger.error("Could not play music: #{res}")
|
||||
{:error, "Fuck..."}
|
||||
end
|
||||
|
||||
true ->
|
||||
Logger.debug("Waiting...")
|
||||
Process.sleep(500)
|
||||
play_music(guild_id, channel_id, url)
|
||||
end
|
||||
end
|
||||
end
|
48
lib/sergei/voice_cache.ex
Normal file
48
lib/sergei/voice_cache.ex
Normal file
|
@ -0,0 +1,48 @@
|
|||
defmodule Sergei.VoiceStateCache do
|
||||
use GenServer
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(cache) do
|
||||
{:ok, cache}
|
||||
end
|
||||
|
||||
# Client
|
||||
@spec update_state(Nostrum.Struct.Event.VoiceState.t()) :: :ok
|
||||
def update_state(state) do
|
||||
GenServer.cast(__MODULE__, {:update, state})
|
||||
end
|
||||
|
||||
@spec get_state(non_neg_integer()) :: %{guild_id: integer(), channel_id: integer()} | nil
|
||||
def get_state(user_id) do
|
||||
GenServer.call(__MODULE__, {:get, user_id})
|
||||
end
|
||||
|
||||
# Server
|
||||
@impl true
|
||||
def handle_cast({:update, state}, cache) do
|
||||
%{
|
||||
guild_id: guild_id,
|
||||
channel_id: channel_id,
|
||||
member: %{
|
||||
user: %{
|
||||
id: user_id
|
||||
}
|
||||
}
|
||||
} = state
|
||||
|
||||
entry =
|
||||
Map.new()
|
||||
|> Map.put(user_id, %{guild_id: guild_id, channel_id: channel_id})
|
||||
|
||||
{:noreply, Map.merge(cache, entry)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:get, user_id}, _from, cache) do
|
||||
{:reply, Map.get(cache, user_id), cache}
|
||||
end
|
||||
end
|
20
lib/server.ex
Normal file
20
lib/server.ex
Normal file
|
@ -0,0 +1,20 @@
|
|||
defmodule Server do
|
||||
use Plug.Router
|
||||
|
||||
plug(:match)
|
||||
plug(:dispatch)
|
||||
|
||||
get "/" do
|
||||
conn
|
||||
|> send_resp(200, "Ok")
|
||||
end
|
||||
|
||||
match _ do
|
||||
conn
|
||||
|> send_resp(404, "Page not found")
|
||||
end
|
||||
|
||||
def start_link(_) do
|
||||
Plug.Adapters.Cowboy.http(Server, [])
|
||||
end
|
||||
end
|
75
main.py
75
main.py
|
@ -1,75 +0,0 @@
|
|||
import os
|
||||
import subprocess
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from functools import partial
|
||||
|
||||
import discord
|
||||
|
||||
class Client(discord.Client):
|
||||
def __init__(self):
|
||||
intents = discord.Intents().default()
|
||||
intents.members = True
|
||||
super().__init__(intents=intents)
|
||||
|
||||
|
||||
async def on_ready(self):
|
||||
print("Logged on as {}".format(self.user))
|
||||
|
||||
|
||||
async def on_message(self, message: discord.Message):
|
||||
if message.author == self.user:
|
||||
return
|
||||
|
||||
if message.channel.type is not discord.ChannelType.private:
|
||||
return
|
||||
|
||||
url = message.content
|
||||
v_id = url.split("=")[1]
|
||||
user = message.channel.recipient.id
|
||||
|
||||
# Download file if it doesn't exist already
|
||||
if not os.path.exists(v_id):
|
||||
subprocess.run(["yt-dlp", "-f", "ba/b", "-o", v_id, url])
|
||||
|
||||
for chan in self.get_all_channels():
|
||||
# Check if Channel is Voice Channel
|
||||
if type(chan) is not discord.VoiceChannel:
|
||||
continue
|
||||
|
||||
# Join the voice channel the user who DM'd Sergei is in
|
||||
if user not in [i.id for i in chan.members]:
|
||||
continue
|
||||
|
||||
# Try to create a new voice client; grab existing one if available
|
||||
try:
|
||||
v_client: discord.VoiceClient = await chan.connect()
|
||||
except discord.errors.ClientException:
|
||||
for client in self.voice_clients:
|
||||
if client.channel.id != chan.id:
|
||||
continue
|
||||
|
||||
v_client = client
|
||||
await v_client.move_to(chan)
|
||||
v_client.stop()
|
||||
|
||||
self.play(v_client, v_id)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def play(vc: discord.VoiceClient, v_id: str, err=None):
|
||||
if err is not None:
|
||||
print(err)
|
||||
return
|
||||
|
||||
if vc.is_playing():
|
||||
return
|
||||
|
||||
audio = discord.FFmpegOpusAudio(v_id)
|
||||
vc.play(audio, after=partial(Client.play, vc, v_id))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
load_dotenv()
|
||||
Client().run(os.getenv("DISCORD_TOK"))
|
||||
|
37
mix.exs
Normal file
37
mix.exs
Normal file
|
@ -0,0 +1,37 @@
|
|||
defmodule Sergei.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :sergei,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.14",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps(),
|
||||
releases: [
|
||||
sergei: [
|
||||
include_executables_for: [:unix]
|
||||
]
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help compile.app" to learn about applications.
|
||||
def application do
|
||||
[
|
||||
mod: {Sergei.Application, []},
|
||||
extra_applications: [:logger]
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help deps" to learn about dependencies.
|
||||
defp deps do
|
||||
[
|
||||
# {:dep_from_hexpm, "~> 0.3.0"},
|
||||
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||
{:plug_cowboy, "~> 2.0"},
|
||||
{:cowlib, "~> 2.11", hex: :remedy_cowlib, override: true},
|
||||
{:nostrum, "~> 0.6"}
|
||||
]
|
||||
end
|
||||
end
|
23
mix.lock
Normal file
23
mix.lock
Normal file
|
@ -0,0 +1,23 @@
|
|||
%{
|
||||
"certifi": {:hex, :certifi, "2.10.0", "a4ab316320bfca83bd0b57fd022d091555d42a30eefabb38063887158294773a", [:rebar3], [], "hexpm", "e87a9dd6e7fe9c5804887850d4cdbcd83db4da7a27f928174f11e4e06fb7902e"},
|
||||
"chacha20": {:hex, :chacha20, "1.0.4", "0359d8f9a32269271044c1b471d5cf69660c362a7c61a98f73a05ef0b5d9eb9e", [:mix], [], "hexpm", "2027f5d321ae9903f1f0da7f51b0635ad6b8819bc7fe397837930a2011bc2349"},
|
||||
"cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||
"cowlib": {:hex, :remedy_cowlib, "2.11.1", "7abb4d0779a7d1c655f7642dc0bd0af754951e95005dfa01b500c68fe35a5961", [:rebar3], [], "hexpm", "0b613dc308e080cb6134285f1b1b55c3873e101652e70c70010fc6651c91b130"},
|
||||
"curve25519": {:hex, :curve25519, "1.0.5", "f801179424e4012049fcfcfcda74ac04f65d0ffceeb80e7ef1d3352deb09f5bb", [:mix], [], "hexpm", "0fba3ad55bf1154d4d5fc3ae5fb91b912b77b13f0def6ccb3a5d58168ff4192d"},
|
||||
"ed25519": {:hex, :ed25519, "1.4.1", "479fb83c3e31987c9cad780e6aeb8f2015fb5a482618cdf2a825c9aff809afc4", [:mix], [], "hexpm", "0dacb84f3faa3d8148e81019ca35f9d8dcee13232c32c9db5c2fb8ff48c80ec7"},
|
||||
"equivalex": {:hex, :equivalex, "1.0.3", "170d9a82ae066e0020dfe1cf7811381669565922eb3359f6c91d7e9a1124ff74", [:mix], [], "hexpm", "46fa311adb855117d36e461b9c0ad2598f72110ad17ad73d7533c78020e045fc"},
|
||||
"gen_stage": {:hex, :gen_stage, "1.2.0", "ee49244b57803f54bdab08a60a927e1b4e5bb5d635c52eca0f376a0673af3f8c", [:mix], [], "hexpm", "c3e40992c72e74d9c4eda16d7515bf32c9e7b634e827ab11091fff3290f7b503"},
|
||||
"gun": {:hex, :remedy_gun, "2.0.1", "0f0caed812ed9e4da4f144df2d5bf73b0a99481d395ecde990a3791decf321c6", [:rebar3], [{:cowlib, "~> 2.11.1", [hex: :remedy_cowlib, repo: "hexpm", optional: false]}], "hexpm", "b6685a85fbd12b757f86809be1b3d88fcef365b77605cd5aa34db003294c446e"},
|
||||
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
|
||||
"kcl": {:hex, :kcl, "1.4.2", "8b73a55a14899dc172fcb05a13a754ac171c8165c14f65043382d567922f44ab", [:mix], [{:curve25519, ">= 1.0.4", [hex: :curve25519, repo: "hexpm", optional: false]}, {:ed25519, "~> 1.3", [hex: :ed25519, repo: "hexpm", optional: false]}, {:poly1305, "~> 1.0", [hex: :poly1305, repo: "hexpm", optional: false]}, {:salsa20, "~> 1.0", [hex: :salsa20, repo: "hexpm", optional: false]}], "hexpm", "9f083dd3844d902df6834b258564a82b21a15eb9f6acdc98e8df0c10feeabf05"},
|
||||
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
|
||||
"nostrum": {:hex, :nostrum, "0.6.1", "aaad13e8e8ce8ace1f218685c6824972e7ea4f979e5ba3242a98f22bda52e605", [:mix], [{:certifi, "~> 2.8", [hex: :certifi, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.11 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:gun, "== 2.0.1", [hex: :remedy_gun, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:kcl, "~> 1.4", [hex: :kcl, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "27925a47e413766664928fbeb38c9887a7fd23066cdba831d5fd8241072bf76a"},
|
||||
"plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
|
||||
"poly1305": {:hex, :poly1305, "1.0.4", "7cdc8961a0a6e00a764835918cdb8ade868044026df8ef5d718708ea6cc06611", [:mix], [{:chacha20, "~> 1.0", [hex: :chacha20, repo: "hexpm", optional: false]}, {:equivalex, "~> 1.0", [hex: :equivalex, repo: "hexpm", optional: false]}], "hexpm", "e14e684661a5195e149b3139db4a1693579d4659d65bba115a307529c47dbc3b"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"salsa20": {:hex, :salsa20, "1.0.4", "404cbea1fa8e68a41bcc834c0a2571ac175580fec01cc38cc70c0fb9ffc87e9b", [:mix], [], "hexpm", "745ddcd8cfa563ddb0fd61e7ce48d5146279a2cf7834e1da8441b369fdc58ac6"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
}
|
Loading…
Reference in a new issue