diff options
Diffstat (limited to '')
28 files changed, 670 insertions, 159 deletions
@@ -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" @@ -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> › </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>. + 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> › <a href="/irc">irc</a> › <a href="/irc/txt">txt</a> ›</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> › <a href="/irc">irc</a> ›</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") %>"> @@ -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 @@ -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"}, |