summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.gitignore2
-rw-r--r--config/config.exs25
-rw-r--r--config/dev.exs4
-rw-r--r--config/prod.exs56
-rw-r--r--lib/lsg.ex10
-rw-r--r--lib/lsg/application.ex11
-rw-r--r--lib/lsg/icecast.ex6
-rw-r--r--lib/lsg_irc.ex27
-rw-r--r--lib/lsg_irc/admin_handler.ex32
-rw-r--r--lib/lsg_irc/base_handler.ex37
-rw-r--r--lib/lsg_irc/calc_handler.ex38
-rw-r--r--lib/lsg_irc/connection_handler.ex17
-rw-r--r--lib/lsg_irc/dice_handler.ex10
-rw-r--r--lib/lsg_irc/handler.ex24
-rw-r--r--lib/lsg_irc/kick_roulette_handler.ex29
-rw-r--r--lib/lsg_irc/last_fm_handler.ex15
-rw-r--r--lib/lsg_irc/np_handler.ex4
-rw-r--r--lib/lsg_irc/txt_handler.ex (renamed from lib/lsg_irc/text_trigger_handler.ex)175
-rw-r--r--lib/lsg_irc/user_track.ex150
-rw-r--r--lib/lsg_irc/user_track_handler.ex93
-rw-r--r--lib/lsg_irc/youtube_handler.ex4
-rw-r--r--lib/lsg_web/controllers/irc_controller.ex11
-rw-r--r--lib/lsg_web/templates/irc/index.html.eex15
-rw-r--r--lib/lsg_web/templates/irc/txt.html.eex2
-rw-r--r--lib/lsg_web/templates/irc/txts.html.eex18
-rw-r--r--lib/lsg_web/templates/layout/app.html.eex1
-rw-r--r--mix.exs9
-rw-r--r--mix.lock4
28 files changed, 670 insertions, 159 deletions
diff --git a/.gitignore b/.gitignore
index 7a59685..bf4c600 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,4 +16,4 @@ erl_crash.dump
/config/*.secret.exs
/config/secret.exs
/priv/irc.txt
-/priv/lastfm.dets
+/priv/*.dets
diff --git a/config/config.exs b/config/config.exs
index 8933545..c16b3d5 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -9,6 +9,10 @@ use Mix.Config
config :lsg,
namespace: LSG
+config :lsg, :data_path, "priv"
+
+config :lsg, :icecast_poll_interval, 600_000
+
# Configures the endpoint
config :lsg, LSGWeb.Endpoint,
url: [host: "localhost"],
@@ -27,21 +31,29 @@ config :mime, :types, %{"text/event-stream" => ["sse"]}
config :lsg, :irc,
handlers: [
+ LSG.IRC.BaseHandler,
+ LSG.IRC.AdminHandler,
LSG.IRC.BroadcastHandler,
LSG.IRC.NpHandler,
LSG.IRC.TxtHandler,
+ LSG.IRC.KickRouletteHandler,
LSG.IRC.LastFmHandler,
LSG.IRC.YouTubeHandler,
LSG.IRC.DiceHandler,
- ],
- admins: [
- # Format is {nick, user, host}. :_ for any value.
+ LSG.IRC.CalcHandler,
]
-
-config :lsg, LSG.IRC.TxtHandler, directory: "priv/irc.txt/"
+ #admins: [
+ # # Format is {nick, user, host}. :_ for any value.
+ #]
+ #irc: [
+ # host: "irc.",
+ # port: 6667,
+ # nick: "`115ans",
+ # user: "115ans",
+ # name: "https://sys.115ans.net/irc"
+ #]
config :lsg, LSG.IRC.LastFmHandler,
- dets_path: 'priv/lastfm.dets',
api_key: "x",
api_secret: "x"
@@ -51,4 +63,3 @@ config :lsg, LSG.IRC.YouTubeHandler,
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env}.exs"
-import_config "secret.exs"
diff --git a/config/dev.exs b/config/dev.exs
index 346dc12..57ab926 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -46,3 +46,7 @@ config :logger, :console, format: "[$level] $message\n"
# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20
+
+import_config "secret.exs"
+import_config "dev.secret.exs"
+
diff --git a/config/prod.exs b/config/prod.exs
index 3db7afd..4e00b42 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -1,63 +1,9 @@
use Mix.Config
-# For production, we often load configuration from external
-# sources, such as your system environment. For this reason,
-# you won't find the :http configuration below, but set inside
-# LSGWeb.Endpoint.init/2 when load_from_system_env is
-# true. Any dynamic configuration should be done there.
-#
-# Don't forget to configure the url host to something meaningful,
-# Phoenix uses this information when generating URLs.
-#
-# Finally, we also include the path to a cache manifest
-# containing the digested version of static files. This
-# manifest is generated by the mix phx.digest task
-# which you typically run after static files are built.
config :lsg, LSGWeb.Endpoint,
- url: [host: "lsg.goulag.org", port: 443],
http: [ip: {0,0,0,0}, port: 4000]
-# Do not print debug messages in production
config :logger, level: :debug
-# ## SSL Support
-#
-# To get SSL working, you will need to add the `https` key
-# to the previous section and set your `:url` port to 443:
-#
-# config :lsg, LSGWeb.Endpoint,
-# ...
-# url: [host: "example.com", port: 443],
-# https: [:inet6,
-# port: 443,
-# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
-# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
-#
-# Where those two env variables return an absolute path to
-# the key and cert in disk or a relative path inside priv,
-# for example "priv/ssl/server.key".
-#
-# We also recommend setting `force_ssl`, ensuring no data is
-# ever sent via http, always redirecting to https:
-#
-# config :lsg, LSGWeb.Endpoint,
-# force_ssl: [hsts: true]
-#
-# Check `Plug.SSL` for all available options in `force_ssl`.
-
-# ## Using releases
-#
-# If you are doing OTP releases, you need to instruct Phoenix
-# to start the server for all endpoints:
-#
-# config :phoenix, :serve_endpoints, true
-#
-# Alternatively, you can configure exactly which server to
-# start per endpoint:
-#
-# config :lsg, LSGWeb.Endpoint, server: true
-#
-
-# Finally import the config/prod.secret.exs
-# which should be versioned separately.
+import_config "secret.exs"
import_config "prod.secret.exs"
diff --git a/lib/lsg.ex b/lib/lsg.ex
index d2076f8..ab53a70 100644
--- a/lib/lsg.ex
+++ b/lib/lsg.ex
@@ -1,9 +1,7 @@
defmodule LSG do
- @moduledoc """
- LSG keeps the contexts that define your domain
- and business logic.
- Contexts are also responsible for managing your data, regardless
- if it comes from the database, an external API or others.
- """
+ def data_path do
+ Application.get_env(:lsg, :data_path)
+ end
+
end
diff --git a/lib/lsg/application.ex b/lib/lsg/application.ex
index 0df3c87..23db221 100644
--- a/lib/lsg/application.ex
+++ b/lib/lsg/application.ex
@@ -5,7 +5,6 @@ defmodule LSG.Application do
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec
- {:ok, irc_client} = ExIRC.start_link!
# Define workers and child supervisors to be supervised
children = [
@@ -16,9 +15,7 @@ defmodule LSG.Application do
worker(Registry, [[keys: :duplicate, name: LSG.BroadcastRegistry]]),
worker(LSG.IcecastAgent, []),
worker(LSG.Icecast, []),
- worker(LSG.IRC.ConnectionHandler, [irc_client]),
- worker(LSG.IRC.LoginHandler, [irc_client]),
- ] ++ irc_handlers(irc_client)
+ ] ++ LSG.IRC.application_childs
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
@@ -33,10 +30,4 @@ defmodule LSG.Application do
:ok
end
- defp irc_handlers(irc_client) do
- import Supervisor.Spec
- for handler <- Application.get_env(:lsg, :irc)[:handlers] do
- worker(handler, [irc_client])
- end
- end
end
diff --git a/lib/lsg/icecast.ex b/lib/lsg/icecast.ex
index 21a9ea2..07dd4fc 100644
--- a/lib/lsg/icecast.ex
+++ b/lib/lsg/icecast.ex
@@ -1,7 +1,6 @@
defmodule LSG.Icecast do
use GenServer
require Logger
- @interval 2_500
@hackney_pool :default
@httpoison_opts [hackney: [pool: @hackney_pool]]
@fuse __MODULE__
@@ -42,7 +41,8 @@ defmodule LSG.Icecast do
Logger.error "Icecast HTTP Error: #{inspect error}"
state
end
- :timer.send_after(@interval, :poll)
+ interval = Application.get_env(:lsg, :icecast_poll_interval, 60_000)
+ :timer.send_after(interval, :poll)
state
end
@@ -83,7 +83,7 @@ defmodule LSG.Icecast do
end
defp request(uri, method, body \\ [], headers \\ []) do
- headers = [{"user-agent", "LSG-API[lsg.goulag.org] href@random.sh"}] ++ headers
+ headers = [{"user-agent", "LSG-API[115ans.net, sys.115ans.net] href@random.sh"}] ++ headers
options = @httpoison_opts
case :ok do #:fuse.ask(@fuse, :sync) do
:ok -> run_request(method, uri, body, headers, options)
diff --git a/lib/lsg_irc.ex b/lib/lsg_irc.ex
index 751c9c9..b988e04 100644
--- a/lib/lsg_irc.ex
+++ b/lib/lsg_irc.ex
@@ -1,2 +1,29 @@
defmodule LSG.IRC do
+
+ def application_childs do
+ {:ok, irc_client} = ExIRC.start_link!
+ import Supervisor.Spec
+ [
+ worker(LSG.IRC.UserTrack.Storage, []),
+ worker(LSG.IRC.ConnectionHandler, [irc_client]),
+ worker(LSG.IRC.LoginHandler, [irc_client]),
+ worker(LSG.IRC.UserTrackHandler, [irc_client]),
+ ]
+ ++
+ for handler <- Application.get_env(:lsg, :irc)[:handlers] do
+ worker(handler, [irc_client])
+ end
+ end
+
+ def admin?(%{nick: nick, user: user, host: host}) do
+ for {n, u, h} <- Application.get_env(:lsg, :irc, [])[:admins]||[] do
+ admin_part_match?(n, nick) && admin_part_match?(u, user) && admin_part_match?(h, host)
+ end
+ |> Enum.any?
+ end
+
+ defp admin_part_match?(:_, _), do: true
+ defp admin_part_match?(a, a), do: true
+ defp admin_part_match?(_, _), do: false
+
end
diff --git a/lib/lsg_irc/admin_handler.ex b/lib/lsg_irc/admin_handler.ex
new file mode 100644
index 0000000..27d35c3
--- /dev/null
+++ b/lib/lsg_irc/admin_handler.ex
@@ -0,0 +1,32 @@
+defmodule LSG.IRC.AdminHandler do
+ @moduledoc """
+ # admin
+
+ !op
+ op; requiert admin
+ """
+
+ def irc_doc, do: nil
+
+ def start_link(client) do
+ GenServer.start_link(__MODULE__, [client])
+ end
+
+ def init([client]) do
+ ExIRC.Client.add_handler client, self
+ {:ok, client}
+ end
+
+ def handle_info({:received, "!op", sender, chan}, client) do
+ if LSG.IRC.admin?(sender) do
+ ExIRC.Client.mode(client, chan, "+o", sender.nick)
+ end
+ {:noreply, client}
+ end
+
+ def handle_info(msg, client) do
+ IO.inspect(msg)
+ {:noreply, client}
+ end
+
+end
diff --git a/lib/lsg_irc/base_handler.ex b/lib/lsg_irc/base_handler.ex
new file mode 100644
index 0000000..9145936
--- /dev/null
+++ b/lib/lsg_irc/base_handler.ex
@@ -0,0 +1,37 @@
+defmodule LSG.IRC.BaseHandler do
+
+ use LSG.IRC.Handler
+
+ def irc_doc, do: nil
+
+ def start_link(client) do
+ GenServer.start_link(__MODULE__, [client])
+ end
+
+ def init([client]) do
+ ExIRC.Client.add_handler client, self
+ {:ok, client}
+ end
+
+ def handle_info({:received, "!help", %ExIRC.SenderInfo{nick: nick}, chan}, client) do
+ url = LSGWeb.Router.Helpers.irc_url(LSGWeb.Endpoint, :index)
+ ExIRC.Client.msg(client, :privmsg, chan, "#{nick}: #{url}")
+ {:noreply, client}
+ end
+
+ def handle_info({:received, "version", %ExIRC.SenderInfo{nick: nick}}, client) do
+ {:ok, vsn} = :application.get_key(:lsg, :vsn)
+ ver = List.to_string(vsn)
+ url = LSGWeb.Router.Helpers.irc_url(LSGWeb.Endpoint, :index)
+ version = "v#{ver} ; #{url} ; source: https://git.yt/115ans/sys"
+ ExIRC.Client.msg(client, :privmsg, nick, version)
+ {:noreply, client}
+ end
+
+ def handle_info(msg, client) do
+ IO.inspect(msg)
+ {:noreply, client}
+ end
+
+end
+
diff --git a/lib/lsg_irc/calc_handler.ex b/lib/lsg_irc/calc_handler.ex
new file mode 100644
index 0000000..0f74ac9
--- /dev/null
+++ b/lib/lsg_irc/calc_handler.ex
@@ -0,0 +1,38 @@
+defmodule LSG.IRC.CalcHandler do
+ @moduledoc """
+ # calc
+
+ * **!calc `<expression>`**: évalue l'expression mathématique `<expression>`.
+ """
+
+ def irc_doc, do: @moduledoc
+
+ def start_link(client) do
+ GenServer.start_link(__MODULE__, [client])
+ end
+
+ def init([client]) do
+ ExIRC.Client.add_handler client, self
+ {:ok, client}
+ end
+
+ def handle_info({:received, "!calc "<>expr, %ExIRC.SenderInfo{nick: nick}, chan}, client) do
+ IO.inspect "HAZ CALC " <>inspect(expr)
+ result = try do
+ case Abacus.eval(expr) do
+ {:ok, result} -> result
+ error -> inspect(error)
+ end
+ rescue
+ error -> "#{error.message}"
+ end
+ ExIRC.Client.msg(client, :privmsg, chan, "#{nick}: #{expr} = #{result}")
+ {:noreply, client}
+ end
+
+ def handle_info(msg, client) do
+ {:noreply, client}
+ end
+
+end
+
diff --git a/lib/lsg_irc/connection_handler.ex b/lib/lsg_irc/connection_handler.ex
index 337fe00..8d07e58 100644
--- a/lib/lsg_irc/connection_handler.ex
+++ b/lib/lsg_irc/connection_handler.ex
@@ -1,19 +1,20 @@
defmodule LSG.IRC.ConnectionHandler do
defmodule State do
- defstruct host: "irc.quakenet.org",
- port: 6667,
- pass: "",
- nick: "`115ans",
- user: "115ans",
- name: "https://sys.115ans.net/irc",
- client: nil
+ defstruct [:host, :port, :pass, :nick, :name, :user, :client]
end
def start_link(client) do
- GenServer.start_link(__MODULE__, [%State{client: client}])
+ irc = Application.get_env(:lsg, :irc)[:irc]
+ host = irc[:host]
+ port = irc[:port]
+ nick = irc[:nick]
+ user = irc[:user]
+ name = irc[:name]
+ GenServer.start_link(__MODULE__, [%State{client: client, host: host, port: port, nick: nick, user: user, name: name}])
end
def init([state]) do
+ IO.puts inspect(state)
ExIRC.Client.add_handler state.client, self
ExIRC.Client.connect! state.client, state.host, state.port
{:ok, state}
diff --git a/lib/lsg_irc/dice_handler.ex b/lib/lsg_irc/dice_handler.ex
index b865100..b07b59b 100644
--- a/lib/lsg_irc/dice_handler.ex
+++ b/lib/lsg_irc/dice_handler.ex
@@ -4,14 +4,14 @@ defmodule LSG.IRC.DiceHandler do
@moduledoc """
# dice
- !dice [6 | faces] [1 | rolls]
- roll X times a dice of X faces.
+ * **!dice `[1 | lancés]` `[6 | faces]`**: lance une ou plusieurs fois un dé de 6 ou autre faces
"""
@default_faces 6
@default_rolls 1
@max_rolls 50
+ def short_irc_doc, do: "!dice (jeter un dé)"
defstruct client: nil, dets: nil
def irc_doc, do: @moduledoc
@@ -31,9 +31,9 @@ defmodule LSG.IRC.DiceHandler do
end
def handle_info({:received, "!dice "<>params, sender, chan}, state) do
- {faces, rolls} = case String.split(params, " ", parts: 2) do
- [faces, rolls] -> {faces, rolls}
- [faces] -> {faces, "1"}
+ {rolls, faces} = case String.split(params, " ", parts: 2) do
+ [faces, rolls] -> {rolls, faces}
+ [rolls] -> {rolls, @default_faces}
end
to_integer = fn(string, default) ->
diff --git a/lib/lsg_irc/handler.ex b/lib/lsg_irc/handler.ex
new file mode 100644
index 0000000..19c0945
--- /dev/null
+++ b/lib/lsg_irc/handler.ex
@@ -0,0 +1,24 @@
+defmodule LSG.IRC.Handler do
+ defmacro __using__(_) do
+ quote do
+ alias LSG.IRC
+ alias LSG.IRC.UserTrack
+ alias ExIRC.Client
+ require LSG.IRC.Handler
+ import LSG.IRC.Handler
+ end
+ end
+
+ def privmsg(client, {nil, %ExIRC.SenderInfo{nick: nick}}, message) do
+ privmsg(client, nick, message)
+ end
+
+ def privmsg(client, {channel, _}, message) do
+ privmsg(client, channel, message)
+ end
+
+ def privmsg(client, target, message) when is_binary(target) do
+ ExIRC.Client.msg(client, :privmsg, target, message)
+ end
+
+end
diff --git a/lib/lsg_irc/kick_roulette_handler.ex b/lib/lsg_irc/kick_roulette_handler.ex
new file mode 100644
index 0000000..7bfa90b
--- /dev/null
+++ b/lib/lsg_irc/kick_roulette_handler.ex
@@ -0,0 +1,29 @@
+defmodule LSG.IRC.KickRouletteHandler do
+ @moduledoc """
+ # kick roulette
+
+ * **!kick** (à peu près une chance sur 5)
+ """
+
+ def irc_doc, do: @moduledoc
+ def start_link(client) do
+ GenServer.start_link(__MODULE__, [client])
+ end
+
+ def init([client]) do
+ ExIRC.Client.add_handler client, self
+ {:ok, client}
+ end
+
+ def handle_info({:received, "!kick", sender, chan}, client) do
+ if 5 == :crypto.rand_uniform(1, 6) do
+ ExIRC.Client.kick(client, chan, sender.nick, "perdu")
+ end
+ {:noreply, client}
+ end
+
+ def handle_info(msg, client) do
+ {:noreply, client}
+ end
+
+end
diff --git a/lib/lsg_irc/last_fm_handler.ex b/lib/lsg_irc/last_fm_handler.ex
index ad6dae9..6ecdab9 100644
--- a/lib/lsg_irc/last_fm_handler.ex
+++ b/lib/lsg_irc/last_fm_handler.ex
@@ -4,14 +4,9 @@ defmodule LSG.IRC.LastFmHandler do
@moduledoc """
# last.fm
- !lastfm [nick|username]
- say what nick/specified username is listening on lastfm; if last.fm username is known (via +lastfm).
- !lastfmall
- say what known users (+lastfm) are listening
- +lastfm <username>
- links the nick who use the command to <username> last.fm account.
- -lastfm
- unlinks the nick's previously set last.fm username.
+ * **!lastfm `[nick|username]`**
+ * **!lastfmall**
+ * **+lastfm `<username last.fm>`, -lastfm**
"""
defstruct client: nil, dets: nil
@@ -24,7 +19,7 @@ defmodule LSG.IRC.LastFmHandler do
def init([client]) do
ExIRC.Client.add_handler(client, self())
- dets_filename = Application.get_env(:lsg, __MODULE__)[:dets_path]
+ dets_filename = (LSG.data_path() <> "/" <> "lastfm.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [])
{:ok, %__MODULE__{client: client, dets: dets}}
end
@@ -128,7 +123,7 @@ defmodule LSG.IRC.LastFmHandler do
end
defp lookup_nick(username, state) do
- case :dets.match(state.dets, {:'$1', username}) do
+ case :dets.match(state.dets, {:'$1', String.downcase(username)}) do
[[match]] -> match
[[match] | _many] -> match
_ -> username
diff --git a/lib/lsg_irc/np_handler.ex b/lib/lsg_irc/np_handler.ex
index b198cbc..5f724d4 100644
--- a/lib/lsg_irc/np_handler.ex
+++ b/lib/lsg_irc/np_handler.ex
@@ -2,10 +2,10 @@ defmodule LSG.IRC.NpHandler do
@moduledoc """
# np
- !np
- now playing on 115ans.net
+ * **!np** chanson/émission actuellement sur le stream de 115ans.net
"""
+ def short_irc_doc, do: "!np (en ce moment sur 115ans)"
def irc_doc, do: @moduledoc
def start_link(client) do
GenServer.start_link(__MODULE__, [client])
diff --git a/lib/lsg_irc/text_trigger_handler.ex b/lib/lsg_irc/txt_handler.ex
index e8331f5..f946092 100644
--- a/lib/lsg_irc/text_trigger_handler.ex
+++ b/lib/lsg_irc/txt_handler.ex
@@ -1,34 +1,38 @@
defmodule LSG.IRC.TxtHandler do
+ alias LSG.IRC.UserTrack
+
@moduledoc """
# [txt](/irc/txt)
- !txt
- statistics, file list
- +txt <file>
- create new <file>
-
- !FILE
- read a random line from the file
- !FILE <index>
- read line #<index> from the file
- !FILE <query>
- return a phrase from file who matches <query>
- +FILE <text>
- add <text> in FILE
- -FILE <index>
- remove line in FILE at index <index>
+ * **!txt**: liste des fichiers et statistiques.
+ Les fichiers avec une `*` sont vérrouillés.
+ [Voir sur le web](/irc/txt).
+
+ * **!`FICHIER`**: lis aléatoirement une ligne du fichier `FICHIER`.
+ * **!`FICHIER` `<chiffre>`**: lis la ligne `<chiffre>` du fichier `FICHIER`.
+ * **!`FILE` `<recherche>`**: recherche une ligne contenant `<recherche>` dans `FICHIER`.
+
+ * **+txt `<file`>**: crée le fichier `<file>`.
+ * **+`FICHIER` `<texte>`**: ajoute une ligne `<texte>` dans le fichier `FICHIER`.
+ * **-`FICHIER` `<chiffre>`**: supprime la ligne `<chiffre>` du fichier `FICHIER`.
+
+ * **+txtro, +txtrw**. op seulement. active/désactive le mode lecture seule.
+ * **+txtlock `<fichier>`, -txtlock `<fichier>`**. op seulement. active/désactive le verrouillage d'un fichier.
"""
+ def short_irc_doc, do: "!txt https://sys.115ans.net/irc/txt "
def irc_doc, do: @moduledoc
def start_link(client) do
GenServer.start_link(__MODULE__, [client])
end
- defstruct client: nil, triggers: %{}
+ defstruct client: nil, triggers: %{}, rw: true, locks: nil
def init([client]) do
- state = %__MODULE__{client: client}
+ dets_locks_filename = (LSG.data_path() <> "/" <> "txtlocks.dets") |> String.to_charlist
+ {:ok, locks} = :dets.open_file(dets_locks_filename, [])
+ state = %__MODULE__{client: client, locks: locks}
ExIRC.Client.add_handler(client, self())
{:ok, %__MODULE__{state | triggers: load()}}
end
@@ -37,16 +41,76 @@ defmodule LSG.IRC.TxtHandler do
{:noreply, %__MODULE__{state | triggers: load()}}
end
+ ## -- ADMIN RO/RW
+ def handle_info({:received, "+txtrw", %ExIRC.SenderInfo{nick: nick}, chan}, state = %__MODULE__{rw: false}) do
+ if UserTrack.operator?(chan, nick) do
+ say(state, chan, "txt: écriture réactivée")
+ {:noreply, %__MODULE__{state | rw: true}}
+ else
+ {:noreply, state}
+ end
+ end
+
+ def handle_info({:received, "+txtro", %ExIRC.SenderInfo{nick: nick}, chan}, state = %__MODULE__{rw: true}) do
+ if UserTrack.operator?(chan, nick) do
+ say(state, chan, "txt: écriture désactivée")
+ {:noreply, %__MODULE__{state | rw: false}}
+ else
+ {:noreply, state}
+ end
+ end
+
+ def handle_info({:received, "+txtro", _, _}, state) do
+ {:noreply, state}
+ end
+ def handle_info({:received, "+txtrw", _, _}, state) do
+ {:noreply, state}
+ end
+
+ ## -- ADMIN LOCKS
+ def handle_info({:received, "+txtlock " <> trigger, %ExIRC.SenderInfo{nick: nick}, chan}, state) do
+ with \
+ {trigger, _} <- clean_trigger(trigger),
+ true <- UserTrack.operator?(chan, nick)
+ do
+ :dets.insert(state.locks, {trigger})
+ say(state, chan, "txt: #{trigger} verrouillé")
+ end
+ {:noreply, state}
+ end
+
+ def handle_info({:received, "-txtlock " <> trigger, %ExIRC.SenderInfo{nick: nick}, chan}, state) do
+ with \
+ {trigger, _} <- clean_trigger(trigger),
+ true <- UserTrack.operator?(chan, nick),
+ true <- :dets.member(state.locks, trigger)
+ do
+ :dets.delete(state.locks, trigger)
+ say(state, chan, "txt: #{trigger} déverrouillé")
+ end
+ {:noreply, state}
+ end
+
+ # -- ADD
+
def handle_info({:received, "!txt", _, chan}, state) do
map = Enum.map(state.triggers, fn({key, data}) ->
- "#{key}: #{to_string(Enum.count(data))}"
+ locked? = case :dets.lookup(state.locks, key) do
+ [{trigger}] -> "*"
+ _ -> ""
+ end
+
+ "#{key}: #{to_string(Enum.count(data))}#{locked?}"
end)
total = Enum.reduce(state.triggers, 0, fn({_, data}, acc) ->
acc + Enum.count(data)
end)
detail = Enum.join(map, ", ")
total = ". total: #{Enum.count(state.triggers)} fichiers, #{to_string(total)} lignes. Détail: https://sys.115ans.net/irc/txt"
- (detail<>total)
+
+ ro = if !state.rw, do: " (lecture seule activée)", else: ""
+
+ (detail<>total<>ro)
|> String.codepoints
|> Enum.chunk_every(440)
|> Enum.map(&Enum.join/1)
@@ -63,29 +127,37 @@ defmodule LSG.IRC.TxtHandler do
{:noreply, state}
end
- def handle_info({:received, "+txt "<>trigger, _, chan}, state) do
- {trigger, _} = clean_trigger(trigger)
- if create_file(trigger) do
+ def handle_info({:received, "+txt "<>trigger, sender=%ExIRC.SenderInfo{nick: nick}, chan}, state) do
+ with \
+ {trigger, _} <- clean_trigger(trigger),
+ true <- can_write?(state, chan, sender, trigger),
+ :ok <- create_file(trigger)
+ do
ExIRC.Client.msg(state.client, :privmsg, chan, "#{trigger}.txt créé. Ajouter: `+#{trigger} …` ; Lire: `!#{trigger}`")
{:noreply, %__MODULE__{state | triggers: load()}}
else
- {:noreply, state}
+ _ -> {:noreply, state}
end
end
- def handle_info({:received, "+"<>trigger_and_content, _, chan}, state) do
- if idx = add(state.triggers, trigger_and_content) do
- ExIRC.Client.msg(state.client, :privmsg, chan, "ajouté. (#{idx})")
+ def handle_info({:received, "+"<>trigger_and_content, sender=%ExIRC.SenderInfo{nick: nick}, chan}, state) do
+ with \
+ {trigger, _} <- clean_trigger(trigger_and_content),
+ true <- can_write?(state, chan, sender, trigger),
+ {:ok, idx} <- add(state.triggers, trigger_and_content)
+ do
+ ExIRC.Client.msg(state.client, :privmsg, chan, "#{nick}: ajouté à #{trigger}. (#{idx})")
{:noreply, %__MODULE__{state | triggers: load()}}
else
- {:noreply, state}
+ _ -> {:noreply, state}
end
end
- def handle_info({:received, "-"<>trigger_and_id, _, chan}, state) do
+ def handle_info({:received, "-"<>trigger_and_id, sender=%ExIRC.SenderInfo{nick: nick}, chan}, state) do
with \
[trigger, id] <- String.split(trigger_and_id, " ", parts: 2),
{trigger, _} = clean_trigger(trigger),
+ true <- can_write?(state, chan, sender, trigger),
data <- Map.get(state.triggers, trigger),
{id, ""} <- Integer.parse(id),
{text, _id} <- Enum.find(data, fn({_, idx}) -> id-1 == idx end)
@@ -102,18 +174,19 @@ defmodule LSG.IRC.TxtHandler do
end
end
+
+
def handle_info(msg, state) do
{:noreply, state}
end
# Load/Reloads text files from disk
defp load() do
- dir = env()[:directory]
- Path.wildcard(dir <> "/*.txt")
+ Path.wildcard(directory() <> "/*.txt")
|> Enum.reduce(%{}, fn(path, m) ->
file = Path.basename(path)
[key, "txt"] = String.split(file, ".", parts: 2)
- data = dir <> file
+ data = directory() <> file
|> File.read!
|> String.split("\n")
|> Enum.reject(fn(line) ->
@@ -135,7 +208,7 @@ defmodule LSG.IRC.TxtHandler do
|> Enum.sort_by(fn({_, idx}) -> idx end)
|> Enum.map(fn({text, _}) -> text end)
|> Enum.join("\n")
- File.write!(env()[:directory] <> "/" <> trigger <> ".txt", data<>"\n", [])
+ File.write!(directory() <> "/" <> trigger <> ".txt", data<>"\n", [])
end
defp get_random(triggers, trigger, []) do
@@ -175,8 +248,8 @@ defmodule LSG.IRC.TxtHandler do
end
defp create_file(name) do
- File.touch!(env()[:directory] <> "/" <> name <> ".txt")
- true
+ File.touch!(directory() <> "/" <> name <> ".txt")
+ :ok
end
defp add(triggers, trigger_and_content) do
@@ -184,10 +257,13 @@ defmodule LSG.IRC.TxtHandler do
[trigger, content] ->
{trigger, _} = clean_trigger(trigger)
if Map.has_key?(triggers, trigger) do
- File.write!(env()[:directory] <> "/" <> trigger <> ".txt", content<>"\n", [:append])
- Enum.count(triggers[trigger])+1
+ File.write!(directory() <> "/" <> trigger <> ".txt", content<>"\n", [:append])
+ idx = Enum.count(triggers[trigger])+1
+ {:ok, idx}
+ else
+ :error
end
- _ -> false
+ _ -> :error
end
end
@@ -214,6 +290,29 @@ defmodule LSG.IRC.TxtHandler do
{trigger, opts}
end
- defp env(), do: Application.get_env(:lsg, __MODULE__)
+ defp directory() do
+ Application.get_env(:lsg, :data_path) <> "/irc.txt/"
+ end
+
+ defp can_write?(state = %__MODULE__{rw: rw?, locks: locks}, channel, sender, trigger) do
+ admin? = LSG.IRC.admin?(sender)
+ operator? = LSG.IRC.UserTrack.operator?(channel, sender.nick)
+ locked? = case :dets.lookup(locks, trigger) do
+ [{trigger}] -> true
+ _ -> false
+ end
+ unlocked? = if rw? == false, do: false, else: !locked?
+ can? = admin? || operator? || unlocked?
+
+ if !can? do
+ reason = if !rw?, do: "lecture seule", else: "fichier vérrouillé"
+ say(state, channel, "#{sender.nick}: permission refusée (#{reason})")
+ end
+ can?
+ end
+
+ defp say(%__MODULE__{client: client}, chan, message) do
+ ExIRC.Client.msg(client, :privmsg, chan, message)
+ end
end
diff --git a/lib/lsg_irc/user_track.ex b/lib/lsg_irc/user_track.ex
new file mode 100644
index 0000000..b67b9f6
--- /dev/null
+++ b/lib/lsg_irc/user_track.ex
@@ -0,0 +1,150 @@
+defmodule LSG.IRC.UserTrack do
+ @moduledoc """
+ User Track DB & Utilities
+ """
+
+ @ets LSG.IRC.UserTrack.Storage
+ # {uuid, nick, nicks, privilege_map}
+ # Privilege map:
+ # %{"#channel" => [:operator, :voice]
+ defmodule Storage do
+
+ def delete(id) do
+ op(fn(ets) -> :ets.delete(ets, id) end)
+ end
+
+ def insert(tuple) do
+ op(fn(ets) -> :ets.insert(ets, tuple) end)
+ end
+
+ def op(fun) do
+ GenServer.call(__MODULE__, {:op, fun})
+ end
+
+ def start_link do
+ GenServer.start_link(__MODULE__, [], [name: __MODULE__])
+ end
+
+ def init([]) do
+ ets = :ets.new(__MODULE__, [:set, :named_table, :protected, {:read_concurrency, true}])
+ {:ok, ets}
+ end
+
+ def handle_call({:op, fun}, _from, ets) do
+ returned = try do
+ {:ok, fun.(ets)}
+ rescue
+ rescued -> {:error, rescued}
+ catch
+ rescued -> {:error, rescued}
+ end
+ {:reply, returned, ets}
+ end
+ end
+
+ defmodule Id, do: use EntropyString
+
+ defmodule User do
+ defstruct [:id, :nick, :nicks, :username, :host, :realname, :privileges]
+
+ def to_tuple(u = %__MODULE__{}) do
+ {u.id || LSG.IRC.UserTrack.Id.large_id, u.nick, u.nicks || [], u.username, u.host, u.realname, u.privileges}
+ end
+
+ def from_tuple({id, nick, nicks, username, host, realname, privs}) do
+ %__MODULE__{id: id, nick: nick, nicks: nicks, username: username, realname: realname, privileges: privs}
+ end
+ end
+
+ def find_by_nick(nick) do
+ case :ets.match(@ets, {:'$1', nick, :_, :_, :_, :_, :_}) do
+ [[id]] -> lookup(id)
+ _ -> nil
+ end
+ end
+
+ def to_list, do: :ets.tab2list(@ets)
+
+ def lookup(id) do
+ case :ets.lookup(@ets, id) do
+ [] -> nil
+ [tuple] -> User.from_tuple(tuple)
+ end
+ end
+
+ def operator?(channel, nick) do
+ if user = find_by_nick(nick) do
+ privs = Map.get(user.privileges, channel, [])
+ Enum.member?(privs, :admin) || Enum.member?(privs, :operator)
+ else
+ false
+ end
+ end
+
+ def joined(c, s), do: joined(c,s,[])
+
+ def joined(channel, sender=%{nick: nick, user: uname, host: host}, privileges) do
+ privileges = if LSG.IRC.admin?(sender) do
+ privileges ++ [:admin]
+ else privileges end
+ user = if user = find_by_nick(nick) do
+ %User{user | username: uname, host: host, privileges: Map.put(user.privileges || %{}, channel, privileges)}
+ else
+ %User{nick: nick, username: uname, host: host, privileges: %{channel => privileges}}
+ end
+
+ Storage.op(fn(ets) ->
+ :ets.insert(ets, User.to_tuple(user))
+ end)
+ end
+
+ def joined(channel, nick, privileges) do
+ user = if user = find_by_nick(nick) do
+ %User{user | privileges: Map.put(user.privileges, channel, privileges)}
+ else
+ %User{nick: nick, privileges: %{channel => privileges}}
+ end
+
+ Storage.op(fn(ets) ->
+ :ets.insert(ets, User.to_tuple(user))
+ end)
+ end
+
+ def renamed(old_nick, new_nick) do
+ if user = find_by_nick(old_nick) do
+ user = %User{user | nick: new_nick, nicks: [old_nick|user.nicks]}
+ Storage.insert(User.to_tuple(user))
+ end
+ end
+
+ def change_privileges(channel, nick, {add, remove}) do
+ if user = find_by_nick(nick) do
+ privs = Map.get(user.privileges, channel)
+
+ privs = Enum.reduce(add, privs, fn(priv, acc) -> [priv|acc] end)
+ privs = Enum.reduce(remove, privs, fn(priv, acc) -> List.delete(acc, priv) end)
+
+ user = %User{user | privileges: Map.put(user.privileges, channel, privs)}
+ Storage.insert(User.to_tuple(user))
+ end
+ end
+
+ def parted(channel, nick) do
+ if user = find_by_nick(nick) do
+ privs = Map.delete(user.privileges, channel)
+ if Enum.count(privs) > 0 do
+ user = %User{user | privileges: privs}
+ Storage.insert(User.to_tuple(user))
+ else
+ Storage.delete(user.id)
+ end
+ end
+ end
+
+ def quitted(sender) do
+ if user = find_by_nick(sender.nick) do
+ Storage.delete(user.id)
+ end
+ end
+
+end
diff --git a/lib/lsg_irc/user_track_handler.ex b/lib/lsg_irc/user_track_handler.ex
new file mode 100644
index 0000000..d167af5
--- /dev/null
+++ b/lib/lsg_irc/user_track_handler.ex
@@ -0,0 +1,93 @@
+defmodule LSG.IRC.UserTrackHandler do
+ @moduledoc """
+ # User Track Handler
+
+ This handlers keeps track of users presence and privileges.
+
+ Planned API:
+
+ UserTrackHandler.operator?(%ExIRC.Sender{nick: "href", …}, "#channel") :: boolean
+
+ """
+
+ def irc_doc, do: nil
+
+ def start_link(client) do
+ GenServer.start_link(__MODULE__, [client])
+ end
+
+ defstruct client: nil, ets: nil
+
+ def init([client]) do
+ ExIRC.Client.add_handler client, self
+ {:ok, %__MODULE__{client: client}}
+ end
+
+ def handle_info({:joined, channel}, state) do
+ ExIRC.Client.who(state.client, channel)
+ {:noreply, state}
+ end
+
+ def handle_info({:who, channel, whos}, state) do
+ Enum.map(whos, fn(who = %ExIRC.Who{nick: nick, operator?: operator}) ->
+ priv = if operator, do: [:operator], else: []
+ LSG.IRC.UserTrack.joined(channel, who, priv)
+ end)
+ {:noreply, state}
+ end
+
+ def handle_info({:quit, _reason, sender}, state) do
+ LSG.IRC.UserTrack.quitted(sender)
+ {:noreply, state}
+ end
+
+ def handle_info({:joined, channel, sender}, state) do
+ LSG.IRC.UserTrack.joined(channel, sender, [])
+ {:noreply, state}
+ end
+
+ def handle_info({:kicked, nick, _by, channel, _reason}, state) do
+ parted(channel, nick)
+ {:noreply, state}
+ end
+
+ def handle_info({:parted, channel, %ExIRC.SenderInfo{nick: nick}}, state) do
+ parted(channel, nick)
+ {:noreply, state}
+ end
+
+ def handle_info({:mode, [channel, mode, nick]}, state) do
+ mode(channel, nick, mode)
+ {:noreply, state}
+ end
+
+ def handle_info({:nick_changed, old_nick, new_nick}, state) do
+ rename(old_nick, new_nick)
+ {:noreply, state}
+ end
+
+ def handle_info(msg, state) do
+ {:noreply, state}
+ end
+
+ defp parted(channel, nick) do
+ LSG.IRC.UserTrack.parted(channel, nick)
+ :ok
+ end
+
+ defp mode(channel, nick, "+o") do
+ LSG.IRC.UserTrack.change_privileges(channel, nick, {[:operator], []})
+ :ok
+ end
+
+ defp mode(channel, nick, "-o") do
+ LSG.IRC.UserTrack.change_privileges(channel, nick, {[], [:operator]})
+ :ok
+ end
+
+ defp rename(old, new) do
+ LSG.IRC.UserTrack.renamed(old, new)
+ :ok
+ end
+
+end
diff --git a/lib/lsg_irc/youtube_handler.ex b/lib/lsg_irc/youtube_handler.ex
index 769f220..e7eadfc 100644
--- a/lib/lsg_irc/youtube_handler.ex
+++ b/lib/lsg_irc/youtube_handler.ex
@@ -4,9 +4,7 @@ defmodule LSG.IRC.YouTubeHandler do
@moduledoc """
# youtube
- !youtube <recherche>
- !yt <recherche>
- cherche sur youtube (seulement le premier résultat).
+ * **!yt `<recherche>`**, !youtube `<recherche>`: retourne le premier résultat de la `<recherche>` YouTube
"""
defstruct client: nil, dets: nil
diff --git a/lib/lsg_web/controllers/irc_controller.ex b/lib/lsg_web/controllers/irc_controller.ex
index a5a68f7..bff5476 100644
--- a/lib/lsg_web/controllers/irc_controller.ex
+++ b/lib/lsg_web/controllers/irc_controller.ex
@@ -2,9 +2,11 @@ defmodule LSGWeb.IrcController do
use LSGWeb, :controller
def index(conn, _) do
+ doc = LSG.IRC.TxtHandler.irc_doc()
commands = for mod <- Application.get_env(:lsg, :irc)[:handlers] do
mod.irc_doc()
end
+ |> Enum.reject(fn(i) -> i == nil end)
render conn, "index.html", commands: commands
end
@@ -12,13 +14,16 @@ defmodule LSGWeb.IrcController do
def txt(conn, _), do: do_txt(conn, nil)
defp do_txt(conn, nil) do
- render conn, "txts.html", data: data()
+ doc = LSG.IRC.TxtHandler.irc_doc()
+ data = data()
+ lines = Enum.reduce(data, 0, fn({_, lines}, acc) -> acc + Enum.count(lines) end)
+ render conn, "txts.html", data: data, doc: doc, files: Enum.count(data), lines: lines
end
defp do_txt(conn, txt) do
data = data()
if Map.has_key?(data, txt) do
- render(conn, "txt.html", name: txt, data: data[txt])
+ render(conn, "txt.html", name: txt, data: data[txt], doc: nil)
else
conn
|> put_status(404)
@@ -26,7 +31,7 @@ defmodule LSGWeb.IrcController do
end
defp data() do
- dir = Application.get_env(:lsg, LSG.IRC.TxtHandler)[:directory]
+ dir = Application.get_env(:lsg, :data_path) <> "/irc.txt/"
Path.wildcard(dir <> "/*.txt")
|> Enum.reduce(%{}, fn(path, m) ->
path = String.split(path, "/")
diff --git a/lib/lsg_web/templates/irc/index.html.eex b/lib/lsg_web/templates/irc/index.html.eex
index 91873e6..c42d71f 100644
--- a/lib/lsg_web/templates/irc/index.html.eex
+++ b/lib/lsg_web/templates/irc/index.html.eex
@@ -1,7 +1,18 @@
-<h1>bot `115ans</h1>
+<style type="text/css">
+h1 small {
+ font-size: 14px;
+}
+</style>
+
+<h1>
+ <small><a href="https://115ans.net">115ans.net</a> &rsaquo; </small><br/>
+irc 115ans</h1>
<p>
-Si vous cherchez l'IRC c'est <a href="https://115ans.net/irc/">par là</a>.
+Serveur IRC: <a href="irc://irc.quakenet.org/lsg">irc.quakenet.org #lsg</a>.
+&nbsp;Matrix: <a href="https://matrix.random.sh/riot/#rooms/#lsg:random.sh">#lsg:random.sh</a>.
+<br />
+<a href="https://115ans.net/irc/">Webchat</a>.
<br />
<a href="/irc/stats/">Statistiques</a>.
</p>
diff --git a/lib/lsg_web/templates/irc/txt.html.eex b/lib/lsg_web/templates/irc/txt.html.eex
index 4ffde50..e060e5f 100644
--- a/lib/lsg_web/templates/irc/txt.html.eex
+++ b/lib/lsg_web/templates/irc/txt.html.eex
@@ -8,7 +8,7 @@ ol li {
</style>
<h1>
- <small><a href="/irc/txt">irc.txt</a>:</small><br/>
+ <small><a href="https://115ans.net">115ans.net</a> &rsaquo; <a href="/irc">irc</a> &rsaquo; <a href="/irc/txt">txt</a> &rsaquo;</small><br/>
<%= @name %>.txt</h1>
<ol>
diff --git a/lib/lsg_web/templates/irc/txts.html.eex b/lib/lsg_web/templates/irc/txts.html.eex
index 7c96ed9..5039c29 100644
--- a/lib/lsg_web/templates/irc/txts.html.eex
+++ b/lib/lsg_web/templates/irc/txts.html.eex
@@ -1,4 +1,18 @@
-<h1>irc.txt</h1>
+<style type="text/css">
+.help-entry h1 {
+ font-size: 18px;
+}
+.help-entry h2 {
+ font-size: 16px;
+}
+h1 small {
+ font-size: 14px;
+}
+</style>
+<h1><small><a href="https://115ans.net">115ans.net</a> &rsaquo; <a href="/irc">irc</a> &rsaquo;</small><br/>
+ txt</h1>
+
+<p><strong><%= @lines %></strong> lignes dans <strong><%= @files %></strong> fichiers.</p>
<ul>
<%= for {txt, data} <- @data do %>
@@ -6,3 +20,5 @@
<% end %>
</ul>
+<div class="help-entry"><%= @doc |> Earmark.as_html! |> raw() %></div>
+
diff --git a/lib/lsg_web/templates/layout/app.html.eex b/lib/lsg_web/templates/layout/app.html.eex
index 1c8f900..4c2ad44 100644
--- a/lib/lsg_web/templates/layout/app.html.eex
+++ b/lib/lsg_web/templates/layout/app.html.eex
@@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
+ <meta name="robots" content="noindex, nofollow, nosnippet">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="<%= static_path(@conn, "/assets/css/app.css") %>">
diff --git a/mix.exs b/mix.exs
index 866c5a6..473a399 100644
--- a/mix.exs
+++ b/mix.exs
@@ -4,7 +4,7 @@ defmodule LSG.Mixfile do
def project do
[
app: :lsg,
- version: "0.0.1",
+ version: "0.0.2",
elixir: "~> 1.4",
elixirc_paths: elixirc_paths(Mix.env),
compilers: [:phoenix, :gettext] ++ Mix.compilers,
@@ -34,9 +34,12 @@ defmodule LSG.Mixfile do
{:httpoison, "~> 1.0"},
{:jason, "~> 1.0"},
{:floki, "~> 0.19.3"},
- {:exirc, github: "bitwalker/exirc"},
+ #{:exirc, github: "hrefhref/exirc"},
+ {:exirc, path: "/usr/home/href/dev/exirc/"},
{:distillery, "~> 1.5", runtime: false},
- {:earmark, "~> 1.2"}
+ {:earmark, "~> 1.2"},
+ {:entropy_string, "~> 1.0.0"},
+ {:abacus, "~> 0.3.3"},
]
end
end
diff --git a/mix.lock b/mix.lock
index bf5e2be..0337ca4 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,8 +1,10 @@
-%{"certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"},
+%{"abacus": {:hex, :abacus, "0.3.3", "f2f11e23073f5e16af36ac425cd9fa9a338695e2f8014684239fa14a9171d5f6", [:mix], [], "hexpm"},
+ "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"},
"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"},
"distillery": {:hex, :distillery, "1.5.2", "eec18b2d37b55b0bcb670cf2bcf64228ed38ce8b046bb30a9b636a6f5a4c0080", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm"},
+ "entropy_string": {:hex, :entropy_string, "1.0.7", "61a5a989e78fd2798e35a17a98a17f81fb504e8d4ba620bcd4f19063eb782943", [:mix], [], "hexpm"},
"exirc": {:git, "https://github.com/bitwalker/exirc.git", "a9258d1058815fa0d8c3201a4caf651754621f8e", []},
"file_system": {:hex, :file_system, "0.2.4", "f0bdda195c0e46e987333e986452ec523aed21d784189144f647c43eaf307064", [:mix], [], "hexpm"},
"floki": {:hex, :floki, "0.19.3", "652d1447767f783bd6cae1d882fd2145f25db28c6841ab87659225b468cff101", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},