diff --git a/lib/fruitbot/command.ex b/lib/fruitbot/command.ex new file mode 100644 index 0000000..8befcaf --- /dev/null +++ b/lib/fruitbot/command.ex @@ -0,0 +1,3 @@ +defmodule Fruitbot.Command do + defstruct aliases: [], handler: nil +end diff --git a/lib/fruitbot/command_handler.ex b/lib/fruitbot/command_handler.ex new file mode 100644 index 0000000..8489046 --- /dev/null +++ b/lib/fruitbot/command_handler.ex @@ -0,0 +1,31 @@ +defmodule Fruitbot.CommandHandler do + import Fruitbot.Commands, only: [all_commands: 0] + + def handle_command(payload) do + IO.inspect(payload) + [command | tail] = String.split(payload) + query = Enum.join(tail) + # how to pass query???? + + case find_command(command) do + nil -> + IO.puts("Command not found: #{command}") + {:error, :bad_command} + c -> execute_command(c, query) + end + end + + defp find_command(message) do + IO.puts(message) + Enum.find(all_commands(), fn command -> + IO.puts(command.aliases) + c = message |> String.split("!") |> List.last() + IO.puts(c) + Enum.any?(command.aliases, &(&1 == c)) + end) + end + + defp execute_command(%Fruitbot.Command{handler: handler}, query) do + handler.(query) + end +end diff --git a/lib/fruitbot/commands.ex b/lib/fruitbot/commands.ex index 1d3b906..e31ecf5 100644 --- a/lib/fruitbot/commands.ex +++ b/lib/fruitbot/commands.ex @@ -1,282 +1,145 @@ defmodule Fruitbot.Commands do - def handle_message(payload) do - IO.inspect(payload) - [command | tail] = String.split(payload) - query = Enum.join(tail) - - case command do - "!anysong" -> - # pull a random link from #anysong channel in discord - channel_id = 925_232_059_058_880_522 - {:ok, msgs} = Nostrum.Api.get_channel_messages(channel_id, 200) - - urls = - msgs - |> Enum.map(fn m -> m.content end) - |> Enum.filter(fn m -> String.contains?(m, "https") end) - |> Enum.map(fn s -> List.flatten(Regex.scan(~r/https\S+/iu, s)) end) - |> Enum.reject(fn each -> Enum.empty?(each) end) - - msg = List.first(Enum.random(urls)) - {:ok, msg} - - "!vr" -> - msg = - "Join the party in VR here! https://hubs.mozilla.com/MsvfAkH/terrific-satisfied-area" - - {:ok, msg} - - "!discord" -> - msg = "https://discord.gg/kM3bW7SM" - {:ok, msg} - - "!donate" -> - msg = - "Enjoying the stream? The best way to support is with a monthly donation on Patreon. Learn more at https://datafruits.fm/support." - - {:ok, msg} - - "!advice" -> - :random.seed(:erlang.now) - advices = [ - "Don't live like me Brendon. Don't get a tattoo of a cheese cow.", - "Next thing you know, you're in the circus, touring, making good money.", - "I've got trademark products all over my body because I was drunk one night. Don't live like me.", - "Your honor might I suggest a spanking on his tush tush?", - "Quarter for the bus, quarter for the bus. The end. Hey Brendon, the end.", - "What are you looking at?", - "It's Spaghetti Time!", - "Next time that thing comes near me, I'm gonna eat it. I'm serious!", - ] - msg = Enum.random(advices) - {:ok, msg} - - "!sorry" -> - # shell to mplayer - System.cmd("play", ["./sfx/onion_salad_dressing.mp3"]) - - msg = - "Must have been the onion salad dressing. Right, Brendon? :sorrymusthavebeentheonionsaladdressing:" - - {:ok, msg} - - "!thisisamazing" -> - # shell to mplayer - System.cmd("play", ["./sfx/thisisamazing.mp3"]) - msg = "It's just a website" - {:ok, msg} - - "!gohackyourself" -> - # shell to mplayer - System.cmd("play", ["./sfx/go_hack_yourself.wav"]) - msg = "go hack yourself" - {:ok, msg} - - "!pewpew" -> - # shell to mplayer - System.cmd("play", ["./sfx/PEWPEW.wav"]) - msg = "pewpew" - {:ok, msg} - - "!bass" -> - # shell to mplayer - System.cmd("play", ["./sfx/bass.mp3"]) - msg = "BASS" - {:ok, msg} - - "!scream" -> - # shell to mplayer - System.cmd("play", ["./sfx/somebody_scream.wav"]) - msg = "c'mon ethel let's get outta here" - {:ok, msg} - - "!internet" -> - # shell to mplayer - System.cmd("play", ["./sfx/internet.wav"]) - msg = "https://www.youtube.com/watch?v=ip34OUo3IS0" - {:ok, msg} - - "!penith" -> - # shell to mplayer - System.cmd("play", ["./sfx/penith.wav"]) - msg = ":dizzy:" - {:ok, msg} - - "!ballin" -> - # shell to mplayer - System.cmd("play", ["./sfx/ballin.wav"]) - msg = ":lain_dad:" - {:ok, msg} - - "!duck" -> - # shell to mplayer - System.cmd("play", ["./sfx/duck_rotate.wav"]) - msg = ":duckle:" - {:ok, msg} - - "!fries" -> - # shell to mplayer - System.cmd("mplayer", ["./sfx/greasy_fries.ogg"]) - msg = ":greasyhotdogs:" - {:ok, msg} - - "!hotdogs" -> - # shell to mplayer - System.cmd("mplayer", ["./sfx/greasy_hotd.ogg"]) - msg = ":greasyhotdogs:" - {:ok, msg} - - "!bug" -> - # shell to mplayer - System.cmd("mplayer", ["./sfx/bug.mp3"]) - msg = "FIX THAT BUG" - {:ok, msg} - - "!gj" -> - # shell to mplayer - System.cmd("mplayer", ["./sfx/gj.mp3"]) - msg = ":goodbeverage:" - {:ok, msg} - - "!false" -> - # shell to mplayer - System.cmd("mplayer", ["./sfx/false.mp3"]) - msg = "it never happened" - {:ok, msg} - - "!totalfabrication" -> - # shell to mplayer - System.cmd("mplayer", ["./sfx/total_fabrication.mp3"]) - msg = "it's a total fabrication" - {:ok, msg} + defmodule Handlers do + def say_commands(_query) do + commands = Enum.join(Enum.map(Fruitbot.Commands.all_commands(), fn %Fruitbot.Command{aliases: [first | _rest]} -> "!" <> first end), " ") + {:ok, commands} + end - "!boost" -> - # shell to mplayer - System.cmd("mplayer", ["./sfx/boostyrdesktoplifestyle.mp3"]) - msg = ":marty:" - {:ok, msg} + def say_anysong(_query) do + # pull a random link from #anysong channel in discord + channel_id = 925_232_059_058_880_522 + {:ok, msgs} = Nostrum.Api.get_channel_messages(channel_id, 200) - "!computers" -> - # shell to mplayer - System.cmd("mplayer", ["./sfx/computers.mp3"]) - msg = "I hope we all learned about computers" - {:ok, msg} + urls = + msgs + |> Enum.map(fn m -> m.content end) + |> Enum.filter(fn m -> String.contains?(m, "https") end) + |> Enum.map(fn s -> List.flatten(Regex.scan(~r/https\S+/iu, s)) end) + |> Enum.reject(fn each -> Enum.empty?(each) end) - "!done" -> - # shell to mplayer - System.cmd("mplayer", ["./sfx/done.mp3"]) - msg = "and yr done" - {:ok, msg} + msg = List.first(Enum.random(urls)) + {:ok, msg} + end - "!onionrings" -> - # shell to mplayer - System.cmd("mplayer", ["./sfx/greasy_onion_rings.ogg"]) - msg = ":greasyhotdogs:" - {:ok, msg} + def say_discord(_query) do + msg = "https://discord.gg/kM3bW7SM" + {:ok, msg} + end - "!awake" -> - # shell to mplayer - System.cmd("mplayer", ["./sfx/alive_alert_awake.mp3"]) - msg = ":alive_alert_awake:" - {:ok, msg} + def say_donate(_query) do + msg = + "Enjoying the stream? The best way to support is with a monthly donation on Patreon. Learn more at https://datafruits.fm/support." - "!next" -> - next_show = Fruitbot.StreampusherApi.next_show() - {:ok, next_show} + {:ok, msg} + end - "!latest" -> - # return latest archive - latest = Fruitbot.StreampusherApi.latest_archive() - {:ok, latest} + def say_advice(_query) do + {:ok, model} = Markov.load("./coach_model", sanitize_tokens: true, store_log: [:train]) + :ok = Markov.configure(model, shift_probabilities: true) - "!wiki" -> - wiki_link = Fruitbot.StreampusherApi.wiki_search(query) - {:ok, wiki_link} + {:ok, msg} = Markov.generate_text(model) + Markov.unload(model) + {:ok, msg} + end - "!tag" -> - result = Fruitbot.StreampusherApi.tag_search(query) - {:ok, result} + def say_next(_query) do + next_show = Fruitbot.StreampusherApi.next_show() + {:ok, next_show} + end - "!datafruiter" -> - { url, username } = Fruitbot.StreampusherApi.user_search(query) - message = "Datafruit found: #{url}" - {:ok, message} + def say_np(_query) do + current_show = Fruitbot.StreampusherApi.current_show() + # return current show if exists or current archive playing + {:ok, current_show} + end - "!bigup" -> - query_stripped = String.replace_prefix(query, "@", "") - lookup = :ets.lookup(:user_bigups, query_stripped) - case lookup do - [] -> - :ets.insert(:user_bigups, {query_stripped, 1}) - [{user, count}] -> - :ets.insert(:user_bigups, {query_stripped, count + 1}) - end - [{user, count}] = :ets.lookup(:user_bigups, query_stripped) - message = ":airhorn: big up #{user} :airhorn: 88888888888888888+++++++++ #{user} has #{count} bigups" - { :ok, message } + def say_latest(_query) do + # return latest archive + latest = Fruitbot.StreampusherApi.latest_archive() + {:ok, latest} + end - # wait this is already a command... - # "!hotdogs" -> - # { :ok, message } + def say_wiki(query) do + wiki_link = Fruitbot.StreampusherApi.wiki_search(query) + {:ok, wiki_link} + end - "!hack" -> - message = "hack the planet https://github.com/datafruits" - { :ok, message } + def say_tag(query) do + result = Fruitbot.StreampusherApi.tag_search(query) + {:ok, result} + end - "!github" -> - message = "hack the planet https://github.com/datafruits" - { :ok, message } + def say_shrimpo(query) do + result = Fruitbot.StreampusherApi.current_shrimpos() + {:ok, result} + end - "!sfx" -> - # can we pull the list of sfx automatically somehow? - list = """ - !sorry - !thisisamazing - !gohackyourself - !pewpew - !bass - !scream - !internet - !penith - !ballin - !duck - !fries - !hotdogs - !onionrings - !gj - !bug - !computers - !done - !false - !totalfabrication - !boost - """ + def say_datafruiter(query) do + result = Fruitbot.StreampusherApi.user_search(query) + case result do + { url, username } -> + message = "Datafruit found: #{url}" + {:ok, message} + { :error, error_message } -> + message = error_message + {:ok, message} + end + end - {:ok, list} + def say_bigup(query) do + query_stripped = String.replace_prefix(query, "@", "") + lookup = :ets.lookup(:user_bigups, query_stripped) + case lookup do + [] -> + :ets.insert(:user_bigups, {query_stripped, 1}) + [{user, count}] -> + :ets.insert(:user_bigups, {query_stripped, count + 1}) + end + [{user, count}] = :ets.lookup(:user_bigups, query_stripped) + message = ":airhorn: big up #{user} :airhorn: 88888888888888888+++++++++ #{user} has #{count} bigups" + { :ok, message } + end + def say_hack(_query) do + message = "hack the planet https://github.com/datafruits" + { :ok, message } + end - "!commands" -> - # can we pull the list of commands automatically somehow? - list = """ - !vr - !donate - !advice - !sorry - !thisisamazing - !gohackyourself - !next - !wiki - !tag - !datafruiter - !commands - """ + def say_help(_query) do + message = "type !commands to see list of commands or check out the !wiki for more info" + { :ok, message } + end - {:ok, list} + def say_label(_query) do + message = "check out the datafruits label releases here https://datafruits.bandcamp.com/" + { :ok, message } + end - _ -> - IO.puts("unhandled command: #{command}") - # Elixir prefers two-element tuples esp. for :ok and :error - {:error, :bad_command} + def say_coc(_query) do + message = "all fruits must abide by the code of conduct https://datafruits.fm/coc" + { :ok, message } end end + + @commands [ + %Fruitbot.Command{aliases: ["commands"], handler: &Handlers.say_commands/1}, + %Fruitbot.Command{aliases: ["anysong"], handler: &Handlers.say_anysong/1}, + %Fruitbot.Command{aliases: ["discord"], handler: &Handlers.say_discord/1}, + %Fruitbot.Command{aliases: ["donate", "patreon", "subscribe"], handler: &Handlers.say_donate/1}, + %Fruitbot.Command{aliases: ["advice"], handler: &Handlers.say_advice/1}, + %Fruitbot.Command{aliases: ["next"], handler: &Handlers.say_next/1}, + %Fruitbot.Command{aliases: ["np", "now"], handler: &Handlers.say_np/1}, + %Fruitbot.Command{aliases: ["latest"], handler: &Handlers.say_latest/1}, + %Fruitbot.Command{aliases: ["wiki"], handler: &Handlers.say_wiki/1}, + %Fruitbot.Command{aliases: ["tag"], handler: &Handlers.say_tag/1}, + %Fruitbot.Command{aliases: ["shrimpo", "shrimpos"], handler: &Handlers.say_shrimpo/1}, + %Fruitbot.Command{aliases: ["datafruiter", "datafruit"], handler: &Handlers.say_datafruiter/1}, + %Fruitbot.Command{aliases: ["bigup", "bigups"], handler: &Handlers.say_bigup/1}, + %Fruitbot.Command{aliases: ["hack", "github"], handler: &Handlers.say_hack/1}, + %Fruitbot.Command{aliases: ["help"], handler: &Handlers.say_help/1}, + %Fruitbot.Command{aliases: ["label", "bandcamp"], handler: &Handlers.say_label/1}, + %Fruitbot.Command{aliases: ["coc", "conduct"], handler: &Handlers.say_coc/1}, + ] + + def all_commands(), do: @commands end diff --git a/lib/fruitbot/countdown.ex b/lib/fruitbot/countdown.ex new file mode 100644 index 0000000..7cd5882 --- /dev/null +++ b/lib/fruitbot/countdown.ex @@ -0,0 +1,13 @@ +defmodule Fruitbot.Countdown do + def time_left(end_at) do + {:ok, now} = DateTime.now("Etc/UTC") + {:ok, then, 0} = DateTime.from_iso8601(end_at) + seconds_diff = DateTime.diff(then, now) + + days = div(seconds_diff, 86400) + hours = div(rem(seconds_diff, 86400), 3600) + minutes = div(rem(seconds_diff, 3600), 60) + + %{ days: days, hours: hours, minutes: minutes } + end +end diff --git a/lib/fruitbot/streampusher_api.ex b/lib/fruitbot/streampusher_api.ex index 6711ec6..451c422 100644 --- a/lib/fruitbot/streampusher_api.ex +++ b/lib/fruitbot/streampusher_api.ex @@ -14,7 +14,8 @@ defmodule Fruitbot.StreampusherApi do { url, username } %HTTPoison.Response{status_code: 404} -> - "#{query} not found" + error_message = "#{query} not found" + { :error, error_message } _ -> "Whoops must have eaten a bad fruit" @@ -75,6 +76,78 @@ defmodule Fruitbot.StreampusherApi do end end + @spec current_shrimpos() :: String.t() + def current_shrimpos do + response = + case HTTPoison.get!("https://datafruits.streampusher.com/api/shrimpos.json") do + %HTTPoison.Response{status_code: 200, body: body} -> + Jason.decode!(body) + + # not sure it seems the API doesn't return a 404 when search result not found, just an empty list + %HTTPoison.Response{status_code: 404} -> + "Not found" + + _ -> + "Whoops must have eaten a bad fruit" + + # %HTTPoison.Error{reason: reason} -> + # IO.inspect(reason) + end + data = response["data"] + # get list of shrimpos + # + # current + # + # voting + current_shrimpos = Enum.filter(response["data"], fn s -> Kernel.get_in(s, ["attributes", "status"]) == "running" end) + voting_shrimpos = Enum.filter(response["data"], fn s -> Kernel.get_in(s, ["attributes", "status"]) == "voting" end) + current_shrimpos = for shrimpo <- current_shrimpos do + # calculate time left + end_at = Kernel.get_in(shrimpo, ["attributes", "end_at"]) + countdown = Fruitbot.Countdown.time_left end_at + # + # calculate URL + slug = Kernel.get_in(shrimpo, ["attributes", "slug"]) + url = "https://datafruits.fm/shrimpos/#{slug}" + %{title: shrimpo["attributes"]["title"], url: url, countdown: countdown, end_at: end_at, image: shrimpo["attributes"]["cover_art_url"]} + end + shrimpo_strings = Enum.map(current_shrimpos, fn shrimpo -> + "#{shrimpo[:title]} ends in #{shrimpo[:countdown][:days]} days, #{shrimpo[:countdown][:hours]} hours, #{shrimpo[:countdown][:minutes]} minutes (#{shrimpo[:end_at]})! :link: #{shrimpo[:url]} #{shrimpo[:image]}" + end) + "Current Shrimpos: \n #{Enum.join(shrimpo_strings, "\n")}" + end + + @spec current_show() :: String.t() + def current_show do + response = + case HTTPoison.get!("https://datafruits.streampusher.com/scheduled_shows/current.json") do + %HTTPoison.Response{status_code: 200, body: body} -> + Jason.decode!(body) + + # not sure it seems the API doesn't return a 404 when search result not found, just an empty list + %HTTPoison.Response{status_code: 404} -> + "Not found" + + _ -> + "Whoops must have eaten a bad fruit" + + # %HTTPoison.Error{reason: reason} -> + # IO.inspect(reason) + end + + data = response["data"] + title = Kernel.get_in(data, ["attributes", "title"]) + host = Kernel.get_in(data, ["attributes", "hosted_by"]) + description = Kernel.get_in(data, ["attributes", "description"]) + + slug = Kernel.get_in(data, ["attributes", "slug"]) + show_series_slug = Kernel.get_in(data, ["attributes", "show_series_slug"]) + url = "https://datafruits.fm/shows/#{show_series_slug}/episodes/#{slug}" + image_url = Kernel.get_in(data, ["attributes", "thumb_image_url"]) + + "Current show is #{title}, hosted by #{host}! Description: #{description}. :link: #{url} #{image_url}" + end + @spec next_show() :: String.t() def next_show do response = @@ -95,12 +168,7 @@ defmodule Fruitbot.StreampusherApi do data = response["data"] start = Kernel.get_in(data, ["attributes", "start"]) - {:ok, now} = DateTime.now("Etc/UTC") - {:ok, then, 0} = DateTime.from_iso8601(start) - IO.puts now - IO.puts then - countdown = DateTime.diff(then, now) |> Kernel./(60) |> Kernel.trunc() - IO.puts countdown + countdown = Fruitbot.Countdown.time_left start title = Kernel.get_in(data, ["attributes", "title"]) host = Kernel.get_in(data, ["attributes", "hosted_by"]) @@ -109,7 +177,7 @@ defmodule Fruitbot.StreampusherApi do show_series_slug = Kernel.get_in(data, ["attributes", "show_series_slug"]) url = "https://datafruits.fm/shows/#{show_series_slug}/episodes/#{slug}" image_url = Kernel.get_in(data, ["attributes", "thumb_image_url"]) - "Next show is #{title}, hosted by #{host}! Beginning in #{countdown} minutes. Description: #{description}. :link: #{url} #{image_url}" + "Next show is #{title}, hosted by #{host}! Beginning in #{countdown[:days]} days, #{countdown[:hours]} hours, #{countdown[:minutes]} minutes . Description: #{description}. :link: #{url} #{image_url}" end @spec latest_archive() :: String.t() diff --git a/lib/fruitbot/supervisor.ex b/lib/fruitbot/supervisor.ex index 7a499f6..d092064 100644 --- a/lib/fruitbot/supervisor.ex +++ b/lib/fruitbot/supervisor.ex @@ -23,7 +23,7 @@ defmodule Fruitbot.Supervisor do Plug.Cowboy.child_spec(scheme: :http, plug: Fruitbot.Router, options: [port: get_port()]), {Fruitbot.Worker, uri: System.get_env("CHAT_URL")}, {Fruitbot.NostrumConsumer, name: Fruitbot.NostrumConsumer}, - {TMI.Supervisor, bot_config} + # {TMI.Supervisor, bot_config} ] Supervisor.init(children, strategy: :one_for_one) diff --git a/lib/fruitbot/worker.ex b/lib/fruitbot/worker.ex index 1c9f8f0..1180050 100644 --- a/lib/fruitbot/worker.ex +++ b/lib/fruitbot/worker.ex @@ -1,6 +1,6 @@ defmodule Fruitbot.Worker do use Slipstream, - restart: :temporary + restart: :permanent require Logger @@ -77,15 +77,23 @@ defmodule Fruitbot.Worker do if Map.has_key?(message, "body") do IO.puts("message body: #{message["body"]}") - - case Fruitbot.Commands.handle_message(message["body"]) do - {:ok, message} -> - send_message(socket, message) - - {:error, :bad_command} -> - # noop - IO.puts("Coach doesn't understand this command. Try another!") - :ignore + IO.puts("message role: #{message["role"]}") + + # case Fruitbot.Commands.handle_message(message["body"]) do + # ignore bots + if(message["role"] != "bot") do + case Fruitbot.CommandHandler.handle_command(message["body"]) do + {:ok, message} -> + send_message(socket, message) + + {:error, :bad_command} -> + {:ok, model} = Markov.load("./coach_model", sanitize_tokens: true, store_log: [:train]) + :ok = Markov.train(model, message["body"]) + Markov.unload(model) + # noop + IO.puts("Coach doesn't understand this command. Try another!") + :ignore + end end end @@ -95,9 +103,12 @@ defmodule Fruitbot.Worker do # handle disconnects @impl Slipstream def handle_disconnect(_reason, socket) do + IO.puts "DISCONNECTED ------------------ disconnected from phoenix" case reconnect(socket) do {:ok, socket} -> {:ok, socket} - {:error, reason} -> {:stop, reason, socket} + {:error, reason} -> + IO.puts "ERROR -------------------------------------- error reconnecting: #{reason}" + {:stop, reason, socket} end end diff --git a/mix.exs b/mix.exs index fccdf2a..74feff7 100644 --- a/mix.exs +++ b/mix.exs @@ -30,7 +30,8 @@ defmodule Fruitbot.MixProject do {:httpoison, "~> 2.0"}, {:tmi, git: "https://github.com/mcfiredrill/tmi.ex" }, {:castore, "~> 1.0"}, - {:persistent_ets, "~> 0.1.0"} + {:persistent_ets, "~> 0.1.0"}, + {:markov, "~> 4.0"} ] end end diff --git a/mix.lock b/mix.lock index 6440743..f90d24c 100644 --- a/mix.lock +++ b/mix.lock @@ -3,13 +3,16 @@ "castore": {:hex, :castore, "1.0.2", "0c6292ecf3e3f20b7c88408f00096337c4bfd99bd46cc2fe63413ddbe45b3573", [:mix], [], "hexpm", "40b2dd2836199203df8500e4a270f10fc006cc95adc8a319e148dc3077391d96"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "chacha20": {:hex, :chacha20, "1.0.4", "0359d8f9a32269271044c1b471d5cf69660c362a7c61a98f73a05ef0b5d9eb9e", [:mix], [], "hexpm", "2027f5d321ae9903f1f0da7f51b0635ad6b8819bc7fe397837930a2011bc2349"}, + "complex": {:hex, :complex, "0.5.0", "af2d2331ff6170b61bb738695e481b27a66780e18763e066ee2cd863d0b1dd92", [:mix], [], "hexpm", "2683bd3c184466cfb94fad74cbfddfaa94b860e27ad4ca1bffe3bff169d91ef1"}, "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"}, + "elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"}, "equivalex": {:hex, :equivalex, "1.0.3", "170d9a82ae066e0020dfe1cf7811381669565922eb3359f6c91d7e9a1124ff74", [:mix], [], "hexpm", "46fa311adb855117d36e461b9c0ad2598f72110ad17ad73d7533c78020e045fc"}, "exirc": {:hex, :exirc, "2.0.0", "6b3b8da6d56096a2a613f3688ef05ed52d235a95566e889e885409c307b218b6", [:mix], [], "hexpm", "aba949c7e60ce4bbf1954d78ede33825ffe2fa01e840edd843ddc8f3775e7390"}, + "exla": {:hex, :exla, "0.7.1", "790493288cf4441abed98df0c4e98da15a2e3a7fa27cd2a1f74ec0693952c579", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:nx, "~> 0.7.1", [hex: :nx, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xla, "~> 0.6.0", [hex: :xla, repo: "hexpm", optional: false]}], "hexpm", "ec9c1698a9a17b859d79f9b3c1d75c370335580cdd0353db9c2017f86155e2ec"}, "forecastle": {:hex, :forecastle, "0.1.2", "f8dab08962c7a33010ebd39182513129f17b8814aa16fa453ddd536040882daf", [:mix], [], "hexpm", "8efaeb2e7d0fa24c605605e42562e2dbb0ffd11dc1dd99ef77d78884536ce501"}, "gen_stage": {:hex, :gen_stage, "1.1.2", "b1656cd4ba431ed02c5656fe10cb5423820847113a07218da68eae5d6a260c23", [:mix], [], "hexpm", "9e39af23140f704e2b07a3e29d8f05fd21c2aaf4088ff43cb82be4b9e3148d02"}, "gun": {:hex, :gun, "2.0.1", "160a9a5394800fcba41bc7e6d421295cf9a7894c2252c0678244948e3336ad73", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "a10bc8d6096b9502205022334f719cc9a08d9adcfbfc0dbee9ef31b56274a20b"}, @@ -19,13 +22,17 @@ "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "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"}, + "libring": {:hex, :libring, "1.6.0", "d5dca4bcb1765f862ab59f175b403e356dec493f565670e0bacc4b35e109ce0d", [:mix], [], "hexpm", "5e91ece396af4bce99953d49ee0b02f698cd38326d93cd068361038167484319"}, + "markov": {:hex, :markov, "4.1.3", "632bd48864c5d2a24f7dd5debdbb0e7417e24484c1d2452a58ab97bb30d5aea5", [:mix], [{:exla, "~> 0.3", [hex: :exla, repo: "hexpm", optional: false]}, {:nx, "~> 0.3", [hex: :nx, repo: "hexpm", optional: false]}, {:sidx, "~> 0.1.5", [hex: :sidx, repo: "hexpm", optional: false]}], "hexpm", "202ba48f085615fa0c04b6e88ac43c862efcb42d5f9792f2eefe0b91fe7de260"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"}, "mint_web_socket": {:hex, :mint_web_socket, "1.0.3", "aab42fff792a74649916236d0b01f560a0b3f03ca5dea693c230d1c44736b50e", [:mix], [{:mint, "~> 1.4 and >= 1.4.1", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "ca3810ca44cc8532e3dce499cc17f958596695d226bb578b2fbb88c09b5954b0"}, "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, + "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, "nostrum": {:git, "git@github.com:Kraigie/nostrum.git", "1ec397fda41d4dd345aaeba471b88c8ccded920f", [branch: "master"]}, + "nx": {:hex, :nx, "0.7.1", "5f6376e3d18408116e8a84b8f4ac851fb07dfe61764a5410ebf0b5dcb69c1b7e", [:mix], [{:complex, "~> 0.5", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e3ddd6a3f2a9bac79c67b3933368c25bb5ec814a883fc68aba8fd8a236751777"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "persistent_ets": {:hex, :persistent_ets, "0.1.0", "a9ea1d6e41094441bb7fd3ea6a5717b66de82343bc6a2899e678a5bad46e495b", [:mix], [], "hexpm", "ab0be0f5aa6963f3d6fc712fc37341e75ca7beef46cf46ced2b66af908c9b55f"}, "phoenix_client": {:hex, :phoenix_client, "0.11.1", "b64de69050a3a435438a2545d6f3afcd5fa105b74110bb5212364e2f3ce96a46", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:websocket_client, "~> 1.3", [hex: :websocket_client, repo: "hexpm", optional: false]}], "hexpm", "07edd8a87725b0158abe35711aeaf857e8f8c3c17727725b9324533d5abdf880"}, @@ -35,10 +42,12 @@ "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"}, + "sidx": {:hex, :sidx, "0.1.5", "ed5dfa6284b5c12ebc5d11ff6c17842441abdce0b1e54c4077edc10cc3527e31", [:mix], [{:libring, "~> 1.6", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm", "551c0f0f3a5548abc51b94fb95b91a676bcdfe49faa497b69bb203b47a3fcbbd"}, "slipstream": {:hex, :slipstream, "1.0.4", "982490aca172abc3cdd643cbd891b11c2eed8a6952daa5b2d0b7a6b6ca104590", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mint_web_socket, "~> 1.0 or ~> 0.2", [hex: :mint_web_socket, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0 or ~> 0.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0 or ~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1089f8062ee56a18e052f7771e3a76065bd2c65e2d5c896f7c2e961410b41403"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, "tmi": {:git, "https://github.com/mcfiredrill/tmi.ex", "9c8ab08e7974bad35efbbd7e7a574f968eae087d", []}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websocket_client": {:hex, :websocket_client, "1.5.0", "e825f23c51a867681a222148ed5200cc4a12e4fb5ff0b0b35963e916e2b5766b", [:rebar3], [], "hexpm", "2b9b201cc5c82b9d4e6966ad8e605832eab8f4ddb39f57ac62f34cb208b68de9"}, + "xla": {:hex, :xla, "0.6.0", "67bb7695efa4a23b06211dc212de6a72af1ad5a9e17325e05e0a87e4c241feb8", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "dd074daf942312c6da87c7ed61b62fb1a075bced157f1cc4d47af2d7c9f44fb7"}, }