diff options
author | Jordan Bracco <href@random.sh> | 2022-12-20 02:19:42 +0000 |
---|---|---|
committer | Jordan Bracco <href@random.sh> | 2022-12-20 19:29:41 +0100 |
commit | 9958e90eb5eb5a2cc171c40860745e95a96bd429 (patch) | |
tree | b49cdb1d0041b9c0a81a14950d38c0203896f527 /lib/web | |
parent | Rename to Nola (diff) |
Actually do not prefix folders with nola_ refs T77
Diffstat (limited to 'lib/web')
44 files changed, 2391 insertions, 0 deletions
diff --git a/lib/web/channels/user_socket.ex b/lib/web/channels/user_socket.ex new file mode 100644 index 0000000..eadd4e0 --- /dev/null +++ b/lib/web/channels/user_socket.ex @@ -0,0 +1,37 @@ +defmodule NolaWeb.UserSocket do + use Phoenix.Socket + + ## Channels + # channel "room:*", NolaWeb.RoomChannel + + ## Transports + #transport :websocket, Phoenix.Transports.WebSocket + # transport :longpoll, Phoenix.Transports.LongPoll + + # Socket params are passed from the client and can + # be used to verify and authenticate a user. After + # verification, you can put default assigns into + # the socket that will be set for all channels, ie + # + # {:ok, assign(socket, :user_id, verified_user_id)} + # + # To deny connection, return `:error`. + # + # See `Phoenix.Token` documentation for examples in + # performing token verification on connect. + def connect(_params, socket) do + {:ok, socket} + end + + # Socket id's are topics that allow you to identify all sockets for a given user: + # + # def id(socket), do: "user_socket:#{socket.assigns.user_id}" + # + # Would allow you to broadcast a "disconnect" event and terminate + # all active sockets and channels for a given user: + # + # NolaWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) + # + # Returning `nil` makes this socket anonymous. + def id(_socket), do: nil +end diff --git a/lib/web/components/component.ex b/lib/web/components/component.ex new file mode 100644 index 0000000..fff8263 --- /dev/null +++ b/lib/web/components/component.ex @@ -0,0 +1,44 @@ +defmodule NolaWeb.Component do + use Phoenix.Component + + @date_time_default_format "%F %H:%M" + @date_time_formats %{"time-24-with-seconds" => "%H:%M:%S"} + def naive_date_time_utc(assigns = %{at: nil}) do + "" + end + + def naive_date_time_utc(assigns = %{format: format}) do + assigns = assign(assigns, :format, Map.get(@date_time_formats, format, format)) + ~H""" + <time class="component" + id={"time-#{:erlang.phash2(@datetime)}"} + phx-hook="NaiveDateTimeUTC" + data-time-format={get_luxon_format(@format)} + datetime={NaiveDateTime.to_iso8601(@datetime)}> + <%= Timex.format!(@datetime, @format, :strftime) %> + </time> + """ + end + def naive_date_time_utc(assigns) do + naive_date_time_utc(assign(assigns, :format, "%F %H:%M")) + end + def get_luxon_format("%H:%M:%S"), do: "TIME_24_WITH_SECONDS" + + def nick(assigns = %{self: false}) do + ~H""" + <span class="nickname" data-account-id={@account_id} data-user-id={@user_id}> + <%= @nick %> + </span> + """ + end + + def nick(assigns = %{self: true}) do + ~H""" + <span class="nickname self" data-account-id={@account_id} data-user-id={@user_id}> + You + </span> + """ + end + + +end diff --git a/lib/web/components/event_component.ex b/lib/web/components/event_component.ex new file mode 100644 index 0000000..8af3c67 --- /dev/null +++ b/lib/web/components/event_component.ex @@ -0,0 +1,43 @@ +defmodule NolaWeb.EventComponent do + use Phoenix.Component + + def content(assigns = %{event: %{type: :day_changed}}) do + ~H""" + Day changed: + <span class="reason"><%= Date.to_string(@date) %></span> + """ + end + + def content(assigns = %{event: %{type: :quit}}) do + ~H""" + <NolaWeb.Component.nick self={@self} nick={@user.nick} user_id={@user.id} account_id={@user.account} /> + has quit: + <span class="reason"><%= @reason %></span> + """ + end + + def content(assigns = %{event: %{type: :part}}) do + ~H""" + <NolaWeb.Component.nick self={@self} nick={@user.nick} user_id={@user.id} account_id={@user.account} /> + has left: + <span class="reason"><%= @reason %></span> + """ + end + + def content(assigns = %{event: %{type: :nick}}) do + ~H""" + <span class="old-nick"><%= @old_nick %></span> + is now known as + <NolaWeb.Component.nick self={@self} nick={@user.nick} user_id={@user.id} account_id={@user.account} /> + """ + end + + def content(assigns = %{event: %{type: :join}}) do + ~H""" + <NolaWeb.Component.nick self={@self} nick={@user.nick} user_id={@user.id} account_id={@user.account} /> + joined + """ + end + + +end diff --git a/lib/web/components/message_component.ex b/lib/web/components/message_component.ex new file mode 100644 index 0000000..5d0386b --- /dev/null +++ b/lib/web/components/message_component.ex @@ -0,0 +1,12 @@ +defmodule NolaWeb.MessageComponent do + use Phoenix.Component + + def content(assigns) do + ~H""" + <NolaWeb.Component.naive_date_time_utc datetime={@message.at} format="time-24-with-seconds" /> + <div class="inline-block font-bold flex-none cursor-default"><%= @message.sender.nick %></div> + <div class="inline-block flex-grow cursor-default"><%= @text %></div> + """ + end + +end diff --git a/lib/web/context_plug.ex b/lib/web/context_plug.ex new file mode 100644 index 0000000..ebededa --- /dev/null +++ b/lib/web/context_plug.ex @@ -0,0 +1,92 @@ +defmodule NolaWeb.ContextPlug do + import Plug.Conn + import Phoenix.Controller + + def init(opts \\ []) do + opts || [] + end + + def get_account(conn) do + cond do + get_session(conn, :account) -> get_session(conn, :account) + get_session(conn, :oidc_id) -> if account = IRC.Account.find_meta_account("identity-id", get_session(conn, :oidc_id)), do: account.id + true -> nil + end + end + + def call(conn, opts) do + account = with \ + {:account, account_id} when is_binary(account_id) <- {:account, get_account(conn)}, + {:account, account} when not is_nil(account) <- {:account, IRC.Account.get(account_id)} + do + account + else + _ -> nil + end + + network = Map.get(conn.params, "network") + network = if network == "-", do: nil, else: network + + oidc_account = IRC.Account.find_meta_account("identity-id", get_session(conn, :oidc_id)) + + conns = IRC.Connection.get_network(network) + chan = if c = Map.get(conn.params, "chan") do + NolaWeb.reformat_chan(c) + end + chan_conn = IRC.Connection.get_network(network, chan) + + memberships = if account do + IRC.Membership.of_account(account) + end + + auth_required = cond do + Keyword.get(opts, :restrict) == :public -> false + account == nil -> true + network == nil -> false + Keyword.get(opts, :restrict) == :logged_in -> false + network && chan -> + !Enum.member?(memberships, {network, chan}) + network -> + !Enum.any?(memberships, fn({n, _}) -> n == network end) + end + + bot = cond do + network && chan && chan_conn -> chan_conn.nick + network && conns -> conns.nick + true -> nil + end + + + cond do + account && auth_required -> + conn + |> put_status(404) + |> text("Page not found") + |> halt() + auth_required -> + conn + |> put_status(403) + |> render(NolaWeb.AlcoologView, "auth.html", bot: bot, no_header: true, network: network) + |> halt() + (network && !conns) -> + conn + |> put_status(404) + |> text("Page not found") + |> halt() + (chan && !chan_conn) -> + conn + |> put_status(404) + |> text("Page not found") + |> halt() + true -> + conn = conn + |> assign(:network, network) + |> assign(:chan, chan) + |> assign(:bot, bot) + |> assign(:account, account) + |> assign(:oidc_account, oidc_account) + |> assign(:memberships, memberships) + end + end + +end diff --git a/lib/web/controllers/alcoolog_controller.ex b/lib/web/controllers/alcoolog_controller.ex new file mode 100644 index 0000000..3c812c5 --- /dev/null +++ b/lib/web/controllers/alcoolog_controller.ex @@ -0,0 +1,323 @@ +defmodule NolaWeb.AlcoologController do + use NolaWeb, :controller + require Logger + + plug NolaWeb.ContextPlug when action not in [:token] + plug NolaWeb.ContextPlug, [restrict: :public] when action in [:token] + + def token(conn, %{"token" => token}) do + case Nola.Token.lookup(token) do + {:ok, {:alcoolog, :index, network, channel}} -> index(conn, nil, network, channel) + err -> + Logger.debug("AlcoologControler: token #{inspect err} invalid") + conn + |> put_status(404) + |> text("Page not found") + end + end + + def nick(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do + profile_account = IRC.Account.find_always_by_nick(network, nick, nick) + days = String.to_integer(Map.get(params, "days", "180")) + friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id) + if friend? do + stats = Nola.Plugins.Alcoolog.get_full_statistics(profile_account.id) + history = for {{nick, ts}, points, active, cl, deg, type, descr, meta} <- Nola.Plugins.Alcoolog.nick_history(profile_account) do + %{ + at: ts |> DateTime.from_unix!(:millisecond), + points: points, + active: active, + cl: cl, + deg: deg, + type: type, + description: descr, + meta: meta + } + end + history = Enum.sort(history, &(DateTime.compare(&1.at, &2.at) != :lt)) + |> IO.inspect() + conn + |> assign(:title, "alcoolog #{nick}") + |> render("user.html", network: network, profile: profile_account, days: days, nick: nick, history: history, stats: stats) + else + conn + |> put_status(404) + |> text("Page not found") + end + end + + def nick_stats_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do + profile_account = IRC.Account.find_always_by_nick(network, nick, nick) + friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id) + if friend? do + stats = Nola.Plugins.Alcoolog.get_full_statistics(profile_account.id) + + conn + |> put_resp_content_type("application/json") + |> text(Jason.encode!(stats)) + else + conn + |> put_status(404) + |> json([]) + end + end + + def nick_gls_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do + profile_account = IRC.Account.find_always_by_nick(network, nick, nick) + friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id) + count = String.to_integer(Map.get(params, "days", "180")) + if friend? do + data = Nola.Plugins.Alcoolog.user_over_time_gl(profile_account, count) + delay = count*((24 * 60)*60) + now = DateTime.utc_now() + start_date = DateTime.utc_now() + |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase) + |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase) + |> DateTime.to_date() + |> Date.to_erl() + filled = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl)) + |> Enum.to_list + |> Enum.map(&(:calendar.gregorian_days_to_date(&1))) + |> Enum.map(&Date.from_erl!(&1)) + |> Enum.map(fn(date) -> + %{date: date, gls: Map.get(data, date, 0)} + end) + |> Enum.sort(&(Date.compare(&1.date, &2.date) != :gt)) + + conn + |> put_resp_content_type("application/json") + |> text(Jason.encode!(filled)) + else + conn + |> put_status(404) + |> json([]) + end + end + + + + def nick_volumes_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do + profile_account = IRC.Account.find_always_by_nick(network, nick, nick) + friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id) + count = String.to_integer(Map.get(params, "days", "180")) + if friend? do + data = Nola.Plugins.Alcoolog.user_over_time(profile_account, count) + delay = count*((24 * 60)*60) + now = DateTime.utc_now() + start_date = DateTime.utc_now() + |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase) + |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase) + |> DateTime.to_date() + |> Date.to_erl() + filled = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl)) + |> Enum.to_list + |> Enum.map(&(:calendar.gregorian_days_to_date(&1))) + |> Enum.map(&Date.from_erl!(&1)) + |> Enum.map(fn(date) -> + %{date: date, volumes: Map.get(data, date, 0)} + end) + |> Enum.sort(&(Date.compare(&1.date, &2.date) != :gt)) + + conn + |> put_resp_content_type("application/json") + |> text(Jason.encode!(filled)) + else + conn + |> put_status(404) + |> json([]) + end + end + + def nick_log_json(conn = %{assigns: %{account: account}}, %{"network" => network, "nick" => nick}) do + profile_account = IRC.Account.find_always_by_nick(network, nick, nick) + friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id) + if friend? do + history = for {{nick, ts}, points, active, cl, deg, type, descr, meta} <- Nola.Plugins.Alcoolog.nick_history(profile_account) do + %{ + at: ts |> DateTime.from_unix!(:millisecond) |> DateTime.to_iso8601(), + points: points, + active: active, + cl: cl, + deg: deg, + type: type, + description: descr, + meta: meta + } + end + last = List.last(history) + {_, active} = Nola.Plugins.Alcoolog.user_stats(profile_account) + last = %{last | active: active, at: DateTime.utc_now() |> DateTime.to_iso8601()} + history = history ++ [last] + + conn + |> put_resp_content_type("application/json") + |> text(Jason.encode!(history)) + else + conn + |> put_status(404) + |> json([]) + end + end + + def nick_history_json(conn = %{assigns: %{account: account}}, %{"network" => network, "nick" => nick}) do + profile_account = IRC.Account.find_always_by_nick(network, nick, nick) + friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id) + if friend? do + history = for {_, date, value} <- Nola.Plugs.AlcoologAnnouncer.log(profile_account) do + %{date: DateTime.to_iso8601(date), value: value} + end + conn + |> put_resp_content_type("application/json") + |> text(Jason.encode!(history)) + else + conn + |> put_status(404) + |> json([]) + end + end + + def index(conn = %{assigns: %{account: account}}, %{"network" => network, "chan" => channel}) do + index(conn, account, network, NolaWeb.reformat_chan(channel)) + end + + def index(conn = %{assigns: %{account: account}}, _) do + index(conn, account, nil, nil) + end + + #def index(conn, params) do + # network = Map.get(params, "network") + # chan = if c = Map.get(params, "chan") do + # NolaWeb.reformat_chan(c) + # end + # irc_conn = if network do + # IRC.Connection.get_network(network, chan) + # end + # bot = if(irc_conn, do: irc_conn.nick)# + # + # conn + # |> put_status(403) + # |> render("auth.html", network: network, channel: chan, irc_conn: conn, bot: bot) + #end + + def index(conn, account, network, channel) do + aday = ((24 * 60)*60) + now = DateTime.utc_now() + before7 = now + |> DateTime.add(-(7*aday), :second) + |> DateTime.to_unix(:millisecond) + before15 = now + |> DateTime.add(-(15*aday), :second) + |> DateTime.to_unix(:millisecond) + before31 = now + |> DateTime.add(-(31*aday), :second) + |> DateTime.to_unix(:millisecond) + #match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end) + match = [ + {{{:_, :"$1"}, :_, :_, :_, :_, :_, :_, :_}, + [ + {:>, :"$1", {:const, before15}}, + ], [:"$_"]} + ] + + # tuple ets: {{nick, date}, volumes, current, nom, commentaire} + members = IRC.Membership.expanded_members_or_friends(account, network, channel) + members_ids = Enum.map(members, fn({account, _, nick}) -> account.id end) + member_names = Enum.reduce(members, %{}, fn({account, _, nick}, acc) -> Map.put(acc, account.id, nick) end) + drinks = :ets.select(Nola.Plugins.Alcoolog.ETS, match) + |> Enum.filter(fn({{account, _}, _vol, _cur, _cl, _deg, _name, _cmt, _meta}) -> Enum.member?(members_ids, account) end) + |> Enum.map(fn({{account, _}, _, _, _, _, _, _, _} = object) -> {object, Map.get(member_names, account)} end) + |> Enum.sort_by(fn({{{_, ts}, _, _, _, _, _, _, _}, _}) -> ts end, &>/2) + + stats = Nola.Plugins.Alcoolog.get_channel_statistics(account, network, channel) + + top = Enum.reduce(drinks, %{}, fn({{{account_id, _}, vol, _, _, _, _, _, _}, _}, acc) -> + nick = Map.get(member_names, account_id) + all = Map.get(acc, nick, 0) + Map.put(acc, nick, all + vol) + end) + |> Enum.sort_by(fn({_nick, count}) -> count end, &>/2) + # {date, single_peak} + # + conn + |> assign(:title, "alcoolog") + |> render("index.html", network: network, channel: channel, drinks: drinks, top: top, stats: stats) + end + + def index_gls_json(conn = %{assigns: %{account: account}}, %{"network" => network, "chan" => channel}) do + count = 30 + channel = NolaWeb.reformat_chan(channel) + members = IRC.Membership.expanded_members_or_friends(account, network, channel) + members_ids = Enum.map(members, fn({account, _, nick}) -> account.id end) + member_names = Enum.reduce(members, %{}, fn({account, _, nick}, acc) -> Map.put(acc, account.id, nick) end) + delay = count*((24 * 60)*60) + now = DateTime.utc_now() + start_date = DateTime.utc_now() + |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase) + |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase) + |> DateTime.to_date() + |> Date.to_erl() + filled = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl)) + |> Enum.to_list + |> Enum.map(&(:calendar.gregorian_days_to_date(&1))) + |> Enum.map(&Date.from_erl!(&1)) + |> Enum.map(fn(date) -> + {date, (for {a, _, _} <- members, into: Map.new, do: {Map.get(member_names, a.id, a.id), 0})} + end) + |> Enum.into(Map.new) + + gls = Enum.reduce(members, filled, fn({account, _, _}, gls) -> + Enum.reduce(Nola.Plugins.Alcoolog.user_over_time_gl(account, count), gls, fn({date, gl}, gls) -> + u = Map.get(gls, date, %{}) + |> Map.put(Map.get(member_names, account.id, account.id), gl) + Map.put(gls, date, u) + end) + end) + + dates = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl)) + |> Enum.to_list + |> Enum.map(&(:calendar.gregorian_days_to_date(&1))) + |> Enum.map(&Date.from_erl!(&1)) + + filled2 = Enum.map(member_names, fn({_, name}) -> + history = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl)) + |> Enum.to_list + |> Enum.map(&(:calendar.gregorian_days_to_date(&1))) + |> Enum.map(&Date.from_erl!(&1)) + |> Enum.map(fn(date) -> + get_in(gls, [date, name]) #%{date: date, gl: get_in(gls, [date, name])} + end) + if Enum.all?(history, fn(x) -> x == 0 end) do + nil + else + %{name: name, history: history} + end + end) + |> Enum.filter(fn(x) -> x end) + + conn + |> put_resp_content_type("application/json") + |> text(Jason.encode!(%{labels: dates, data: filled2})) + end + + def minisync(conn, %{"user_id" => user_id, "key" => key, "value" => value}) do + account = IRC.Account.get(user_id) + if account do + ds = Nola.Plugins.Alcoolog.data_state() + meta = Nola.Plugins.Alcoolog.get_user_meta(ds, account.id) + case Float.parse(value) do + {val, _} -> + new_meta = Map.put(meta, String.to_existing_atom(key), val) + Nola.Plugins.Alcoolog.put_user_meta(ds, account.id, new_meta) + _ -> + conn + |> put_status(:unprocessable_entity) + |> text("invalid value") + end + else + conn + |> put_status(:not_found) + |> text("not found") + end + end + +end diff --git a/lib/web/controllers/gpt_controller.ex b/lib/web/controllers/gpt_controller.ex new file mode 100644 index 0000000..810a875 --- /dev/null +++ b/lib/web/controllers/gpt_controller.ex @@ -0,0 +1,33 @@ +defmodule NolaWeb.GptController do + use NolaWeb, :controller + require Logger + + plug NolaWeb.ContextPlug + + def result(conn, params = %{"id" => result_id}) do + case Nola.Plugins.Gpt.get_result(result_id) do + {:ok, result} -> + network = Map.get(params, "network") + channel = if c = Map.get(params, "chan"), do: NolaWeb.reformat_chan(c) + render(conn, "result.html", network: network, channel: channel, result: result) + {:error, :not_found} -> + conn + |> put_status(404) + |> text("Page not found") + end + end + + def prompt(conn, params = %{"id" => prompt_id}) do + case Nola.Plugins.Gpt.get_prompt(prompt_id) do + {:ok, prompt} -> + network = Map.get(params, "network") + channel = if c = Map.get(params, "chan"), do: NolaWeb.reformat_chan(c) + render(conn, "prompt.html", network: network, channel: channel, prompt: prompt) + {:error, :not_found} -> + conn + |> put_status(404) + |> text("Page not found") + end + end + +end diff --git a/lib/web/controllers/icecast_see_controller.ex b/lib/web/controllers/icecast_see_controller.ex new file mode 100644 index 0000000..877ad4e --- /dev/null +++ b/lib/web/controllers/icecast_see_controller.ex @@ -0,0 +1,41 @@ +defmodule NolaWeb.IcecastSseController do + use NolaWeb, :controller + require Logger + + @ping_interval 20_000 + + def sse(conn, _params) do + conn + |> put_resp_header("X-Accel-Buffering", "no") + |> put_resp_header("content-type", "text/event-stream") + |> send_chunked(200) + |> subscribe + |> send_sse_message("ping", "ping") + |> send_sse_message("icecast", Nola.IcecastAgent.get) + |> sse_loop + end + + def subscribe(conn) do + :timer.send_interval(@ping_interval, {:event, :ping}) + {:ok, _} = Registry.register(Nola.BroadcastRegistry, "icecast", []) + conn + end + + def sse_loop(conn) do + {type, event} = receive do + {:event, :ping} -> {"ping", "ping"} + {:icecast, stats} -> {"icecast", stats} + end + + conn + |> send_sse_message(type, event) + |> sse_loop() + end + + defp send_sse_message(conn, type, data) do + json = Jason.encode!(%{type => data}) + {:ok, conn} = chunk(conn, "event: #{type}\ndata: #{json}\n\n") + conn + end + +end diff --git a/lib/web/controllers/irc_auth_sse_controller.ex b/lib/web/controllers/irc_auth_sse_controller.ex new file mode 100644 index 0000000..62ee2b5 --- /dev/null +++ b/lib/web/controllers/irc_auth_sse_controller.ex @@ -0,0 +1,66 @@ +defmodule NolaWeb.IrcAuthSseController do + use NolaWeb, :controller + require Logger + + @ping_interval 20_000 + @expire_delay :timer.minutes(3) + + def sse(conn, params) do + perks = if uri = Map.get(params, "redirect_to") do + {:redirect, uri} + else + nil + end + token = String.downcase(EntropyString.random_string(65)) + conn + |> assign(:token, token) + |> assign(:perks, perks) + |> put_resp_header("X-Accel-Buffering", "no") + |> put_resp_header("content-type", "text/event-stream") + |> send_chunked(200) + |> subscribe() + |> send_sse_message("token", token) + |> sse_loop + end + + def subscribe(conn) do + :timer.send_interval(@ping_interval, {:event, :ping}) + :timer.send_after(@expire_delay, {:event, :expire}) + {:ok, _} = Registry.register(IRC.PubSub, "messages:private", []) + conn + end + + def sse_loop(conn) do + {type, event, exit} = receive do + {:event, :ping} -> {"ping", "ping", false} + {:event, :expire} -> {"expire", "expire", true} + {:irc, :text, %{account: account, text: token} = m} -> + if String.downcase(String.trim(token)) == conn.assigns.token do + path = Nola.AuthToken.new_path(account.id, conn.assigns.perks) + m.replyfun.("ok!") + {"authenticated", path, true} + else + {nil, nil, false} + end + _ -> {nil, nil, false} + end + + conn = if type do + send_sse_message(conn, type, event) + else + conn + end + + if exit do + conn + else + sse_loop(conn) + end + end + + defp send_sse_message(conn, type, data) do + {:ok, conn} = chunk(conn, "event: #{type}\ndata: #{data}\n\n") + conn + end + +end diff --git a/lib/web/controllers/irc_controller.ex b/lib/web/controllers/irc_controller.ex new file mode 100644 index 0000000..d6114e6 --- /dev/null +++ b/lib/web/controllers/irc_controller.ex @@ -0,0 +1,101 @@ +defmodule NolaWeb.IrcController do + use NolaWeb, :controller + + plug NolaWeb.ContextPlug + + def index(conn, params) do + network = Map.get(params, "network") + channel = if c = Map.get(params, "chan"), do: NolaWeb.reformat_chan(c) + commands = for mod <- Enum.uniq([Nola.Plugins.Account] ++ Nola.Plugins.enabled()) do + if is_atom(mod) do + identifier = Module.split(mod) |> List.last |> Macro.underscore + {identifier, mod.irc_doc()} + end + end + |> Enum.filter(& &1) + |> Enum.filter(fn({_, doc}) -> doc end) + members = cond do + network && channel -> Enum.map(IRC.UserTrack.channel(network, channel), fn(tuple) -> IRC.UserTrack.User.from_tuple(tuple) end) + true -> + IRC.Membership.of_account(conn.assigns.account) + end + render conn, "index.html", network: network, commands: commands, channel: channel, members: members + end + + def txt(conn, %{"name" => name}) do + if String.contains?(name, ".txt") do + name = String.replace(name, ".txt", "") + data = data() + if Map.has_key?(data, name) do + lines = Enum.join(data[name], "\n") + text(conn, lines) + else + conn + |> put_status(404) + |> text("Not found") + end + else + do_txt(conn, name) + end + end + def txt(conn, _), do: do_txt(conn, nil) + + + defp do_txt(conn, nil) do + doc = Nola.IRC.Txt.irc_doc() + data = data() + main = Enum.filter(data, fn({trigger, _}) -> !String.contains?(trigger, ".") end) |> Enum.into(Map.new) + system = Enum.filter(data, fn({trigger, _}) -> String.contains?(trigger, ".") end) |> Enum.into(Map.new) + lines = Enum.reduce(main, 0, fn({_, lines}, acc) -> acc + Enum.count(lines) end) + conn + |> assign(:title, "txt") + |> render("txts.html", data: main, doc: doc, files: Enum.count(main), lines: lines, system: system) + end + + defp do_txt(conn, txt) do + data = data() + base_url = cond do + conn.assigns[:chan] -> "/#{conn.assigns.network}/#{NolaWeb.format_chan(conn.assigns.chan)}" + true -> "/-" + end + if lines = Map.get(data, txt) do + lines = Enum.map(lines, fn(line) -> + line + |> String.split("\\\\") + |> Enum.intersperse(Phoenix.HTML.Tag.tag(:br)) + end) + conn + |> assign(:breadcrumbs, [{"txt", "#{base_url}/txt"}]) + |> assign(:title, "#{txt}.txt") + |> render("txt.html", name: txt, data: lines, doc: nil) + else + conn + |> put_status(404) + |> text("Not found") + end + end + + defp data() do + dir = Application.get_env(:nola, :data_path) <> "/irc.txt/" + Path.wildcard(dir <> "/*.txt") + |> Enum.reduce(%{}, fn(path, m) -> + path = String.split(path, "/") + file = List.last(path) + key = String.replace(file, ".txt", "") + data = dir <> file + |> File.read! + |> String.split("\n") + |> Enum.reject(fn(line) -> + cond do + line == "" -> true + !line -> true + true -> false + end + end) + Map.put(m, key, data) + end) + |> Enum.sort + |> Enum.into(Map.new) + end + +end diff --git a/lib/web/controllers/network_controller.ex b/lib/web/controllers/network_controller.ex new file mode 100644 index 0000000..800294f --- /dev/null +++ b/lib/web/controllers/network_controller.ex @@ -0,0 +1,11 @@ +defmodule NolaWeb.NetworkController do + use NolaWeb, :controller + plug NolaWeb.ContextPlug + + def index(conn, %{"network" => network}) do + conn + |> assign(:title, network) + |> render("index.html") + end + +end diff --git a/lib/web/controllers/open_id_controller.ex b/lib/web/controllers/open_id_controller.ex new file mode 100644 index 0000000..d3fef5d --- /dev/null +++ b/lib/web/controllers/open_id_controller.ex @@ -0,0 +1,64 @@ +defmodule NolaWeb.OpenIdController do + use NolaWeb, :controller + plug NolaWeb.ContextPlug, restrict: :public + require Logger + + def login(conn, _) do + url = OAuth2.Client.authorize_url!(new_client(), scope: "openid", state: Base.url_encode64(:crypto.strong_rand_bytes(32), padding: false)) + redirect(conn, external: url) + end + + def callback(conn, %{"error" => error_code, "error_description" => error}) do + Logger.warn("OpenId error: #{error_code} #{error}") + render(conn, "error.html", error: error) + end + + def callback(conn, %{"code" => code, "state" => state}) do + with \ + client = %{token: %OAuth2.AccessToken{access_token: json}} = OAuth2.Client.get_token!(new_client(), state: state, code: code), + {:ok, %{"access_token" => token}} <- Jason.decode(json), + client = %OAuth2.Client{client | token: %OAuth2.AccessToken{access_token: token}}, + {:ok, %OAuth2.Response{body: body}} <- OAuth2.Client.get(client, "/userinfo"), + {:ok, %{"sub" => id, "preferred_username" => username}} <- Jason.decode(body) + do + if account = conn.assigns.account do + if !IRC.Account.get_meta(account, "identity-id") do # XXX: And oidc id not linked yet + IRC.Account.put_meta(account, "identity-id", id) + end + IRC.Account.put_meta(account, "identity-username", username) + conn + else + conn + end + + conn + |> put_session(:oidc_id, id) + |> put_flash(:info, "Logged in!") + |> redirect(to: Routes.path(conn, "/")) + else + {:error, %OAuth2.Response{status_code: 401}} -> + Logger.error("OpenID: Unauthorized token") + render(conn, "error.html", error: "The token is invalid.") + {:error, %OAuth2.Error{reason: reason}} -> + Logger.error("Error: #{inspect reason}") + render(conn, "error.html", error: reason) + end + end + + def callback(conn, _params) do + render(conn, "error.html", error: "Unspecified error.") + end + + defp new_client() do + config = Application.get_env(:nola, :oidc) + OAuth2.Client.new([ + strategy: OAuth2.Strategy.AuthCode, + client_id: config[:client_id], + client_secret: config[:client_secret], + site: config[:base_url], + authorize_url: config[:authorize_url], + token_url: config[:token_url], + redirect_uri: Routes.open_id_url(NolaWeb.Endpoint, :callback) + ]) + end +end diff --git a/lib/web/controllers/page_controller.ex b/lib/web/controllers/page_controller.ex new file mode 100644 index 0000000..2ac4d0a --- /dev/null +++ b/lib/web/controllers/page_controller.ex @@ -0,0 +1,53 @@ +defmodule NolaWeb.PageController do + use NolaWeb, :controller + + plug NolaWeb.ContextPlug when action not in [:token] + plug NolaWeb.ContextPlug, [restrict: :public] when action in [:token] + + def token(conn, %{"token" => token}) do + with \ + {:ok, account, perks} <- Nola.AuthToken.lookup(token) + do + IO.puts("Authenticated account #{inspect account}") + conn = put_session(conn, :account, account) + case perks do + nil -> redirect(conn, to: "/") + {:redirect, path} -> redirect(conn, to: path) + {:external_redirect, url} -> redirect(conn, external: url) + end + else + z -> + IO.inspect(z) + text(conn, "Error: invalid or expired token") + end + end + + def index(conn = %{assigns: %{account: account}}, _) do + memberships = IRC.Membership.of_account(account) + users = IRC.UserTrack.find_by_account(account) + metas = IRC.Account.get_all_meta(account) + predicates = IRC.Account.get_predicates(account) + conn + |> assign(:title, account.name) + |> render("user.html", users: users, memberships: memberships, metas: metas, predicates: predicates) + end + + def irc(conn, _) do + bot_helps = for mod <- Nola.IRC.env(:handlers) do + mod.irc_doc() + end + render conn, "irc.html", bot_helps: bot_helps + end + + def authenticate(conn, _) do + with \ + {:account, account_id} when is_binary(account_id) <- {:account, get_session(conn, :account)}, + {:account, account} when not is_nil(account) <- {:account, IRC.Account.get(account_id)} + do + assign(conn, :account, account) + else + _ -> conn + end + end + +end diff --git a/lib/web/controllers/sms_controller.ex b/lib/web/controllers/sms_controller.ex new file mode 100644 index 0000000..0fffa23 --- /dev/null +++ b/lib/web/controllers/sms_controller.ex @@ -0,0 +1,10 @@ +defmodule NolaWeb.SmsController do + use NolaWeb, :controller + require Logger + + def ovh_callback(conn, %{"senderid" => from, "message" => message}) do + spawn(fn() -> Nola.Plugins.Sms.incoming(from, String.trim(message)) end) + text(conn, "") + end + +end diff --git a/lib/web/controllers/untappd_controller.ex b/lib/web/controllers/untappd_controller.ex new file mode 100644 index 0000000..d3a540d --- /dev/null +++ b/lib/web/controllers/untappd_controller.ex @@ -0,0 +1,18 @@ +defmodule NolaWeb.UntappdController do + use NolaWeb, :controller + + def callback(conn, %{"code" => code}) do + with \ + {:account, account_id} when is_binary(account_id) <- {:account, get_session(conn, :account)}, + {:account, account} when not is_nil(account) <- {:account, IRC.Account.get(account_id)}, + {:ok, auth_token} <- Untappd.auth_callback(code) + do + IRC.Account.put_meta(account, "untappd-token", auth_token) + text(conn, "OK!") + else + {:account, _} -> text(conn, "Error: account not found") + :error -> text(conn, "Error: untappd authentication failed") + end + end + +end diff --git a/lib/web/endpoint.ex b/lib/web/endpoint.ex new file mode 100644 index 0000000..a401f54 --- /dev/null +++ b/lib/web/endpoint.ex @@ -0,0 +1,62 @@ +defmodule NolaWeb.Endpoint do + use Sentry.PlugCapture + use Phoenix.Endpoint, otp_app: :nola + + # Serve at "/" the static files from "priv/static" directory. + # + # You should set gzip to true if you are running phoenix.digest + # when deploying your static files in production. + plug Plug.Static, + at: "/", from: :nola, gzip: false, + only: ~w(assets css js fonts images favicon.ico robots.txt) + + # Code reloading can be explicitly enabled under the + # :code_reloader configuration of your endpoint. + if 42==43 && code_reloading? do + socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket + plug Phoenix.LiveReloader + plug Phoenix.CodeReloader + end + + plug Plug.RequestId + plug Plug.Logger + + plug Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + pass: ["*/*"], + json_decoder: Jason + + plug Sentry.PlugContext + plug Plug.MethodOverride + plug Plug.Head + + @session_options [store: :cookie, + key: "_nola_key", + signing_salt: "+p7K3wrj"] + + + socket "/live", Phoenix.LiveView.Socket, + websocket: [connect_info: [session: @session_options]] + + # The session will be stored in the cookie and signed, + # this means its contents can be read but not tampered with. + # Set :encryption_salt if you would also like to encrypt it. + plug Plug.Session, @session_options + + plug NolaWeb.Router + + @doc """ + Callback invoked for dynamically configuring the endpoint. + + It receives the endpoint configuration and checks if + configuration should be loaded from the system environment. + """ + def init(_key, config) do + if config[:load_from_system_env] do + port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" + {:ok, Keyword.put(config, :http, [:inet6, port: port])} + else + {:ok, config} + end + end +end diff --git a/lib/web/gettext.ex b/lib/web/gettext.ex new file mode 100644 index 0000000..a43cb0d --- /dev/null +++ b/lib/web/gettext.ex @@ -0,0 +1,24 @@ +defmodule NolaWeb.Gettext do + @moduledoc """ + A module providing Internationalization with a gettext-based API. + + By using [Gettext](https://hexdocs.pm/gettext), + your module gains a set of macros for translations, for example: + + import NolaWeb.Gettext + + # Simple translation + gettext "Here is the string to translate" + + # Plural translation + ngettext "Here is the string to translate", + "Here are the strings to translate", + 3 + + # Domain-based translation + dgettext "errors", "Here is the error message to translate" + + See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. + """ + use Gettext, otp_app: :nola +end diff --git a/lib/web/live/chat_live.ex b/lib/web/live/chat_live.ex new file mode 100644 index 0000000..678ea36 --- /dev/null +++ b/lib/web/live/chat_live.ex @@ -0,0 +1,120 @@ +defmodule NolaWeb.ChatLive do + use Phoenix.LiveView + use Phoenix.HTML + require Logger + + def mount(%{"network" => network, "chan" => chan}, %{"account" => account_id}, socket) do + chan = NolaWeb.reformat_chan(chan) + connection = IRC.Connection.get_network(network, chan) + account = IRC.Account.get(account_id) + membership = IRC.Membership.of_account(IRC.Account.get("DRgpD4fLf8PDJMLp8Dtb")) + if account && connection && Enum.member?(membership, {connection.network, chan}) do + {:ok, _} = Registry.register(IRC.PubSub, "#{connection.network}:events", plugin: __MODULE__) + for t <- ["messages", "triggers", "outputs", "events"] do + {:ok, _} = Registry.register(IRC.PubSub, "#{connection.network}/#{chan}:#{t}", plugin: __MODULE__) + end + + IRC.PuppetConnection.start(account, connection) + + users = IRC.UserTrack.channel(connection.network, chan) + |> Enum.map(fn(tuple) -> IRC.UserTrack.User.from_tuple(tuple) end) + |> Enum.reduce(Map.new, fn(user = %{id: id}, acc) -> + Map.put(acc, id, user) + end) + + backlog = case Nola.IRC.Buffer.select_buffer(connection.network, chan) do + {backlog, _} -> + {backlog, _} = Enum.reduce(backlog, {backlog, nil}, &reduce_contextual_event/2) + Enum.reverse(backlog) + _ -> [] + end + + socket = socket + |> assign(:connection_id, connection.id) + |> assign(:network, connection.network) + |> assign(:chan, chan) + |> assign(:title, "live") + |> assign(:channel, chan) + |> assign(:account_id, account.id) + |> assign(:backlog, backlog) + |> assign(:users, users) + |> assign(:counter, 0) + + {:ok, socket} + else + {:ok, redirect(socket, to: "/")} + end + end + + def handle_event("send", %{"message" => %{"text" => text}}, socket) do + account = IRC.Account.get(socket.assigns.account_id) + IRC.send_message_as(account, socket.assigns.network, socket.assigns.channel, text, true) + {:noreply, assign(socket, :counter, socket.assigns.counter + 1)} + end + + def handle_info({:irc, :event, event = %{type: :join, user_id: id}}, socket) do + if user = IRC.UserTrack.lookup(id) do + socket = socket + |> assign(:users, Map.put(socket.assigns.users, id, user)) + |> append_to_backlog(event) + {:noreply, socket} + else + {:noreply, socket} + end + end + + def handle_info({:irc, :event, event = %{type: :nick, user_id: id, nick: nick}}, socket) do + socket = socket + |> assign(:users, update_in(socket.assigns.users, [id, :nick], nick)) + |> append_to_backlog(event) + {:noreply, socket} + end + + def handle_info({:irc, :event, event = %{type: :quit, user_id: id}}, socket) do + socket = socket + |> assign(:users, Map.delete(socket.assigns.users, id)) + |> append_to_backlog(event) + {:noreply, socket} + end + + def handle_info({:irc, :event, event = %{type: :part, user_id: id}}, socket) do + socket = socket + |> assign(:users, Map.delete(socket.assigns.users, id)) + |> append_to_backlog(event) + {:noreply, socket} + end + + def handle_info({:irc, :trigger, _, message}, socket) do + handle_info({:irc, nil, message}, socket) + end + + def handle_info({:irc, :text, message}, socket) do + IO.inspect({:live_message, message}) + socket = socket + |> append_to_backlog(message) + {:noreply, socket} + end + + def handle_info(info, socket) do + Logger.debug("Unhandled info: #{inspect info}") + {:noreply, socket} + end + + defp append_to_backlog(socket, line) do + {add, _} = reduce_contextual_event(line, {[], List.last(socket.assigns.backlog)}) + assign(socket, :backlog, socket.assigns.backlog ++ add) + end + + defp reduce_contextual_event(line, {acc, nil}) do + {[line | acc], line} + end + defp reduce_contextual_event(line, {acc, last}) do + if NaiveDateTime.to_date(last.at) != NaiveDateTime.to_date(line.at) do + {[%{type: :day_changed, date: NaiveDateTime.to_date(line.at), at: nil}, line | acc], line} + else + {[line | acc], line} + end + + end + +end diff --git a/lib/web/live/chat_live.html.heex b/lib/web/live/chat_live.html.heex new file mode 100644 index 0000000..29cd6a1 --- /dev/null +++ b/lib/web/live/chat_live.html.heex @@ -0,0 +1,91 @@ +<div class="chat" data-turbo="false"> + + <div class="py-4 px-4 bg-gradient-to-b from-black to-gray-900"> + <div class="grid grid-cols-2"> + <h1 class="text-gray-50 tracking-tight font-extrabold text-xl"> + <%= @network %> + <span class="font-bold"><%= @chan %></span> + </h1> + <div class="text-right"> + <a href="/" class="text-gray-400"><%= @account_id %></a> + </div> + </div> + </div> + + <div class="body"> + + <div class="log"> + <p class="disconnected text-center text-6xl tracking-tight font-extrabold text-red-800 w-full my-24 mx-auto overflow-y-auto"> + Disconnected <span class="text-mono">:'(</span> + </p> + <p class="phx-errored text-center text-6xl tracking-tight font-extrabold text-red-800 w-full my-24 mx-auto overflow-y-auto"> + Oh no error <span class="text-mono">>:(</span> + </p> + + <ul class="pt-4 pl-4"> + <%= for message <- @backlog do %> + <%= if is_map(message) && Map.get(message, :__struct__) == IRC.Message do %> + <li class="flex gap-2 place-items-center message" + data-account-id={message.account.id}> + <NolaWeb.MessageComponent.content + message={message} + self={message.account.id == @account_id} + text={message.text} + /> + </li> + <% end %> + + <%= if is_binary(message) do %> + <li class="notice"><%= message %></li> + <% end %> + + <%= if is_map(message) && Map.get(message, :type) do %> + <li class="flex gap-2 place-items-center event"> + <NolaWeb.Component.naive_date_time_utc datetime={message.at} format="time-24-with-seconds" /> + <span class="inline-block font-bold flex-none cursor-default text-gray-700">* * *</span> + <span class="inline-block flex-grow cursor-default text-gray-700"> + <NolaWeb.EventComponent.content event={message} + self={@users[message.user_id] && @users[message.user_id].account == @account_id} + user={@users[message.user_id]} + /> + </span> + </li> + <% end %> + <% end %> + </ul> + </div> + + <aside> + <%= for {_, user} <- @users do %> + <details class="user dropdown"> + <summary><%= user.nick %></summary> + <div class="content"> + <h3 class="text-xl font-bold"><%= user.nick %></h3> + + <ul class="mt-4 space-y-2"> + <li class="">User: <span class="font-bold"><%= user.username %></span></li> + <li class="">Name: <%= user.realname || user.nick %></li> + <li class="">Host: <span class="font-mono"><%= user.host %></span></li> + </ul> + + <div class="mt-4 font-xs text-gray-300 text-center"> + UID: <%= user.id %> + <br /> + AID: <%= user.account %> + </div> + </div> + </details> + <% end %> + </aside> + + </div> + + <.form let={f} id={"form-#{@counter}"} for={:message} phx-submit="send" class="w-full px-4 pt-4"> + <div> + <div class="mt-1 flex rounded-md shadow-sm border border-gray-300"> + <%= text_input f, :text, class: "focus:ring-indigo-500 focus:border-indigo-500 block w-full border rounded-md pl-4 sm:text-sm border-gray-300", autofocus: true, 'phx-hook': "AutoFocus", autocomplete: "off", placeholder: "Don't be shy, say something…" %> + <%= submit content_tag(:span, "Send"), class: "-ml-px relative inline-flex items-center space-x-2 px-4 py-2 border border-gray-300 text-sm font-medium rounded-r-md text-gray-700 bg-gray-50 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"%> + </div> + </div> + </.form> +</div> diff --git a/lib/web/router.ex b/lib/web/router.ex new file mode 100644 index 0000000..5658fda --- /dev/null +++ b/lib/web/router.ex @@ -0,0 +1,85 @@ +defmodule NolaWeb.Router do + use NolaWeb, :router + + pipeline :browser do + plug :accepts, ["html", "txt"] + plug :fetch_session + plug :fetch_flash + plug :fetch_live_flash + plug :protect_from_forgery + plug :put_secure_browser_headers + plug :put_root_layout, {NolaWeb.LayoutView, :root} + end + + pipeline :api do + plug :accepts, ["json", "sse"] + end + + pipeline :matrix_app_service do + plug :accepts, ["json"] + plug Nola.Matrix.Plug.Auth + plug Nola.Matrix.Plug.SetConfig + end + + scope "/api", NolaWeb do + pipe_through :api + get "/irc-auth.sse", IrcAuthSseController, :sse + post "/sms/callback/Ovh", SmsController, :ovh_callback, as: :sms + end + + scope "/", NolaWeb do + pipe_through :browser + get "/", PageController, :index + + get "/login/irc/:token", PageController, :token, as: :login + get "/login/oidc", OpenIdController, :login + get "/login/oidc/callback", OpenIdController, :callback + + get "/api/untappd/callback", UntappdController, :callback, as: :untappd_callback + + get "/-", IrcController, :index + get "/-/txt", IrcController, :txt + get "/-/txt/:name", IrcController, :txt + get "/-/gpt/prompt/:id", GptController, :task + get "/-/gpt/result/:id", GptController, :result + + get "/-/alcoolog", AlcoologController, :index + get "/-/alcoolog/~/:account_name", AlcoologController, :index + get "/:network", NetworkController, :index + get "/:network/~:nick/alcoolog", AlcoologController, :nick + get "/:network/~:nick/alcoolog/log.json", AlcoologController, :nick_log_json + get "/:network/~:nick/alcoolog/gls.json", AlcoologController, :nick_gls_json + get "/:network/~:nick/alcoolog/volumes.json", AlcoologController, :nick_volumes_json + get "/:network/~:nick/alcoolog/history.json", AlcoologController, :nick_history_json + get "/:network/~:nick/alcoolog/stats.json", AlcoologController, :nick_stats_json + get "/:network/:chan/alcoolog", AlcoologController, :index + get "/:network/:chan/alcoolog/gls.json", AlcoologController, :index_gls_json + get "/:network/:chan/gpt/prompt/:id", GptController, :task + get "/:network/:chan/gpt/result/:id", GptController, :result + put "/api/alcoolog/minisync/:user_id/meta/:key", AlcoologController, :minisync_put_meta + + get "/:network/:chan", IrcController, :index + live "/:network/:chan/live", ChatLive + get "/:network/:chan/txt", IrcController, :txt + get "/:network/:chan/txt/:name", IrcController, :txt + get "/:network/:channel/preums", IrcController, :preums + get "/:network/:chan/alcoolog/t/:token", AlcoologController, :token + end + + scope "/_matrix/:appservice", MatrixAppServiceWeb.V1, as: :matrix do + pipe_through :matrix_app_service + + put "/transactions/:txn_id", TransactionController, :push + + get "/users/:user_id", UserController, :query + get "/rooms/*room_alias", RoomController, :query + + get "/thirdparty/protocol/:protocol", ThirdPartyController, :query_protocol + get "/thirdparty/user/:protocol", ThirdPartyController, :query_users + get "/thirdparty/location/:protocol", ThirdPartyController, :query_locations + get "/thirdparty/location", ThirdPartyController, :query_location_by_alias + get "/thirdparty/user", ThirdPartyController, :query_user_by_id + end + + +end diff --git a/lib/web/templates/alcoolog/auth.html.eex b/lib/web/templates/alcoolog/auth.html.eex new file mode 100644 index 0000000..6e5cedc --- /dev/null +++ b/lib/web/templates/alcoolog/auth.html.eex @@ -0,0 +1,43 @@ +<div class="grid grid-cols-2"> + <h1 class="text-2xl font-bold">authentication</h1> + <div class="text-right"> + <%= link("connect using random.sh", to: "/login/oidc", class: "inline-block font-medium underline") %> + </div> +</div> + +<div id="authenticator" class="mt-12 h-32 place-self-end justify-self-start"> + <p> + <%= if @bot, do: "Send this to #{@bot} on #{@network}:", else: "Find your bot nickname and send:" %><br /><br /> + <strong>/msg <%= @bot || "the-bot-nickname" %> web</strong> + <br /><br /> + ... then come back to this address. + </p> +</div> + +<script type="text/javascript"> + var authSse = new EventSource("/api/irc-auth.sse?redirect_to=" + window.location.pathname); + authSse.addEventListener("token", function(event) { + html = "<%= if @bot, do: "Send this to #{@bot} on #{@network}:", else: "Find your bot nickname and send:" %><br /><br /><strong class='text-xl font-mono ml-12'>/msg <%= @bot || "<the-bot-nickname>" %> "+event.data+"</strong>"; + elem = document.getElementById("authenticator"); + elem.innerHTML = html; + }); + authSse.addEventListener("authenticated", function(event) { + elem = document.getElementById("authenticator"); + elem.innerHTML = "<strong>success, redirecting...</strong>"; + window.keepInner = true; + window.location.pathname = event.data; + }); + authSse.addEventListener("expire", function(event) { + elem = document.getElementById("authenticator"); + elem.innerHTML = "<strong>authentication expired</strong>"; + window.keepInner = true; + }); + authSse.onerror = function() { + authSse.close(); + if (!window.keepInner) { + elem = document.getElementById("authenticator"); + elem.innerHTML = "<strong>server closed connection :(</strong>"; + } + }; + +</script> diff --git a/lib/web/templates/alcoolog/index.html.eex b/lib/web/templates/alcoolog/index.html.eex new file mode 100644 index 0000000..5a5423a --- /dev/null +++ b/lib/web/templates/alcoolog/index.html.eex @@ -0,0 +1,205 @@ +<style type="text/css"> +ol li { + margin-bottom: 5px +} +</style> + +<%= if @stats == [] do %> + <div class="rounded-md bg-red-50 p-4"> + <div class="flex"> + <div class="flex-shrink-0"> + <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /> + </svg> + </div> + <div class="ml-3"> + <h3 class="text-sm leading-5 font-medium text-red-800"> + CATASTROPHE! Personne n'a bu!!!! + </h3> + </div> + </div> + </div> +<% end %> + +<ul class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> + <%= for {nick, status} <- @stats do %> + <li class="col-span-1 bg-white rounded-lg shadow"> + <div class="w-full flex items-center justify-between p-6 space-x-6"> + <div class="flex-1 truncate"> + <div class="flex items-center space-x-3"> + <h3 class="text-gray-900 text-base leading-5 font-semibold truncate"><%= link nick, to: alcoolog_path(@conn, :nick, @network, nick) %></h3> + <% rising_class = if status.rising, do: "teal", else: "red" %> + <span class="flex-shrink-0 inline-block px-2 py-0.5 text-<%= rising_class %>-800 text-sm leading-4 font-medium bg-<%= rising_class %>-100 rounded-full"> + <%= status.trend_symbol %> <%= Float.round(status.active, 4) %> g/l + </span> + </div> + <p class="mt-1 text-gray-700 text-sm leading-5 truncate"> + <span class="text-base"><%= status.last_cl %>cl @ <%= status.last_deg %>°</span> + <%= if status.last_descr && status.last_descr != "" do %> + <br /><%= status.last_descr %> + <% end %> + <br/><small><%= NolaWeb.LayoutView.format_time(status.last_at) %></small> + </p> + + <p class="mt-1 text-gray-500 text-sm leading-5 truncate"> + <br /> + — sobre dans: <%= status.sober_in_s %><br /> + <%= if status.since do %> + — depuis: <%= status.since_s %><br /> + <% end %> + <small> + — 15m: <%= status.active15m %> g/l - 30m: <%= status.active30m %> g/l - 1h: <%= status.active1h %> g/l<br /> + — aujourd'hui: <%= status.daily_volumes %> points, <%= status.daily_gl %> g/l + </small> + </p> + + + </div> + </div> + </li> + <% end %> +</ul> + +<%= if @stats == %{} do %> + <div class="rounded-md bg-red-50 p-4"> + <div class="flex"> + <div class="flex-shrink-0"> + <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /> + </svg> + </div> + <div class="ml-3"> + <h3 class="text-sm leading-5 font-medium text-red-800"> + ENCORE PIRE! Aucune boisson enregistrée! + </h3> + </div> + </div> + </div> +<% else %> + +<canvas id="myChart" class="w-full" height="200"></canvas> + + <h2 class="leading-8 m-4 font-semibold text-2xl">Classement <span class="font-medium text-gray-600 text-lg">15 jours</span></h2> + +<ul class="grid grid-cols-1 gap-6 sm:grid-cols-5 lg:grid-cols-5"> + <%= for {{nick, count}, rank} <- Enum.with_index(@top) do %> + <% rank = rank + 1 %> + <% trophy = rank <= 3 %> + <% {colour, text} = case rank do +1 -> {"yellow-500", "font-semibold text-base"} +2 -> {"gray-500", "font-medium text-base"} +3 -> {"orange-300", "font-medium text-base"} +_ -> {"gray-300", ""} + end %> + <li class="col-span-1 bg-white rounded-lg shadow text-center <%= text %>"> + <%= if trophy do %> + <span class="text-<%= colour %>"> + <svg style="width: 1.125em; height: 1.5em; display: inline-block;" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="trophy" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" data-fa-i2svg=""><path fill="currentColor" d="M552 64H448V24c0-13.3-10.7-24-24-24H152c-13.3 0-24 10.7-24 24v40H24C10.7 64 0 74.7 0 88v56c0 35.7 22.5 72.4 61.9 100.7 31.5 22.7 69.8 37.1 110 41.7C203.3 338.5 240 360 240 360v72h-48c-35.3 0-64 20.7-64 56v12c0 6.6 5.4 12 12 12h296c6.6 0 12-5.4 12-12v-12c0-35.3-28.7-56-64-56h-48v-72s36.7-21.5 68.1-73.6c40.3-4.6 78.6-19 110-41.7 39.3-28.3 61.9-65 61.9-100.7V88c0-13.3-10.7-24-24-24zM99.3 192.8C74.9 175.2 64 155.6 64 144v-16h64.2c1 32.6 5.8 61.2 12.8 86.2-15.1-5.2-29.2-12.4-41.7-21.4zM512 144c0 16.1-17.7 36.1-35.3 48.8-12.5 9-26.7 16.2-41.8 21.4 7-25 11.8-53.6 12.8-86.2H512v16z"></path></svg> + </span> + <% end %> + #<%= rank %>: <%= link nick, to: alcoolog_path(@conn, :nick, @network, nick) %> + <br /> + <span class="text-xs"><%= Float.round(count, 4) %></span> + </li> + <% end %> +</ul> + + <h2 class="leading-8 m-4 font-semibold text-2xl">Historique</h2> +<div class="flex flex-col"> + <div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8"> + <div class="align-middle inline-block min-w-full shadow overflow-hidden sm:rounded-lg border-b border-gray-200"> + <table class="min-w-full divide-y divide-gray-200"> + <thead> + <tr> + <th class="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"> + date + </th> + <th class="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"> + nick + </th> + <th class="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"> + + </th> + <th class="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"> + + </th> + </tr> + </thead> + <tbody> + <%= for {{{{account, date}, points, _active, cl, deg, nom, comment, _meta}, nick}, index} <- Enum.with_index(@drinks) do %> + <% class = if(Integer.is_even(index), do: "bg-gray-50", else: "bg-white") %> + <% date = DateTime.from_unix!(date, :millisecond) %> + <tr class="<%= class %>"> + <td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 font-medium text-gray-900"> + <%= NolaWeb.LayoutView.format_time(date, false) %> + </td> + <td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 font-medium text-gray-900"> + <%= link nick, to: alcoolog_path(@conn, :nick, @network, nick) %> + </td> + <td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500"><%= cl %>cl <%= deg %>°</td> + <td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500"><%= comment||"" %></td> + </tr> + <% end %> + </tbody> + </table> + </div> + </div> +</div> +<% end %> + +<%= if @conn.assigns.account && (@network || @channel) do %> + <%= link("alcoolog global", to: alcoolog_path(@conn, :index)) %> +<% end %> + +<link href='/css/metricsgraphics.css' rel='stylesheet' type='text/css'> +<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.css" integrity="sha512-SUJFImtiT87gVCOXl3aGC00zfDl6ggYAw5+oheJvRJ8KBXZrr/TMISSdVJ5bBarbQDRC2pR5Kto3xTR0kpZInA==" crossorigin="anonymous" /> +<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js" integrity="sha512-vBmx0N/uQOXznm/Nbkp7h0P1RfLSj0HQrFSzV8m7rOGyj30fYAOKHYvCNez+yM8IrfnW0TCodDEjRqf6fodf/Q==" crossorigin="anonymous"></script> +<script src="/js/jquery3.1.0.min.js"></script> +<script src="/js/d3.v4.min.js"></script> +<script src="/js/metricsgraphics.min.js"></script> + <script type="text/javascript"> + (function() { + // oui s trè moch :( :( :( +var ctx = document.getElementById('myChart').getContext('2d'); + d3.json('<%= alcoolog_path(@conn, :index_gls_json, @network, NolaWeb.format_chan(@channel)) %>', function(data) { + + var dynamicColors = function() { + var r = Math.floor(Math.random() * 255); + var g = Math.floor(Math.random() * 255); + var b = Math.floor(Math.random() * 255); + return "rgb(" + r + "," + g + "," + b + ")"; + }; + + labels = data.labels; + datasets = $.map($.makeArray(data.data), function(set) { + return {label: set.name, data: set.history, fill: false, borderColor: dynamicColors()} + }); + + +var myChart = new Chart(ctx, { + type: 'line', + data: { + labels: labels, + datasets: datasets + }, + options: { + scales: { + xAxes: [{ + type: 'time', + time: { + unit: 'day' + } + }], + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + } +}); +}); + + +})(); +</script> diff --git a/lib/web/templates/alcoolog/user.html.eex b/lib/web/templates/alcoolog/user.html.eex new file mode 100644 index 0000000..d7f716b --- /dev/null +++ b/lib/web/templates/alcoolog/user.html.eex @@ -0,0 +1,170 @@ +<%= if @stats.active > 0 do %> + <h3 class="text-gray-900 text-xl leading-5 font-semibold truncate"> + <% rising_class = if @stats.rising, do: "teal", else: "red" %> + <span class="flex-shrink-0 inline-block px-3 py-3 text-<%= rising_class %>-800 text-2xl leading-4 font-medium bg-<%= rising_class %>-100 rounded-full"> + <%= @stats.trend_symbol %> <%= Float.round(@stats.active, 4) %> g/l + </span> + + <span class="px-3"><%= @stats.last_cl %>cl @ <%= @stats.last_deg %>°</span> + + <span class="px-3 text-lg text-gray-700"><%= if @stats.last_descr && @stats.last_descr != "" do %> + <%= @stats.last_descr %> + <% end %> + </span> + + <span class="px-3 text-gray-500 text-sm font-thin"><%= NolaWeb.LayoutView.format_time(@stats.last_at) %></span> + + </h3> + + <p class="text-gray-500 font-thin text-lg"> + a commencé il y a <span class="text-gray-700 font-semibold"><%= @stats.since_s %></span> + — + sobre dans <span class="text-gray-700 font-semibold"><%= @stats.sober_in_s %></span> + </p> +<% else %> + <h3 class="text-gray-900 text-xl leading-5 font-semibold truncate"> + est sobre! + + <p class="text-gray-500 font-thin text-lg"> + dernier verre + <span class="px-3"><%= @stats.last_cl %>cl @ <%= @stats.last_deg %>°</span> + + <span class="px-3 text-lg text-gray-700"><%= if @stats.last_descr && @stats.last_descr != "" do %> + <%= @stats.last_descr %> + <% end %> + </span> + + <span class="px-3 text-gray-500 text-sm font-thin"><%= NolaWeb.LayoutView.format_time(@stats.last_at) %></span> + </h3> +<% end %> + +<canvas id="myChart" class="w-full" height="200"></canvas> +<canvas id="myChartGl" class="w-full" height="200"></canvas> + + +<h2 class="leading-8 m-4 font-semibold text-2xl">Historique</h2> +<div class="flex flex-col"> + <div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8"> + <div class="align-middle inline-block min-w-full shadow overflow-hidden sm:rounded-lg border-b border-gray-200"> + <table class="min-w-full divide-y divide-gray-200"> + <thead> + <tr> + <th class="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"> + date + </th> + <th class="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"> + + </th> + <th class="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"> + + </th> + </tr> + </thead> + <tbody> + <%= for {%{at: date, cl: cl, deg: deg, description: comment}, index} <- Enum.with_index(@history) do %> + <% class = if(Integer.is_even(index), do: "bg-gray-50", else: "bg-white") %> + <tr class="<%= class %>"> + <td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 font-medium text-gray-900"> + <%= NolaWeb.LayoutView.format_time(date, false) %> + </td> + <td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500"><%= cl %>cl <%= deg %>°</td> + <td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500"><%= comment||"" %></td> + </tr> + <% end %> + </tbody> + </table> + </div> + </div> +</div> + +<div id="metrics-gl" class="w-full"></div> + +<link href='/css/metricsgraphics.css' rel='stylesheet' type='text/css'> +<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.css" integrity="sha512-SUJFImtiT87gVCOXl3aGC00zfDl6ggYAw5+oheJvRJ8KBXZrr/TMISSdVJ5bBarbQDRC2pR5Kto3xTR0kpZInA==" crossorigin="anonymous" /> +<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js" integrity="sha512-vBmx0N/uQOXznm/Nbkp7h0P1RfLSj0HQrFSzV8m7rOGyj30fYAOKHYvCNez+yM8IrfnW0TCodDEjRqf6fodf/Q==" crossorigin="anonymous"></script> +<script src="/js/jquery3.1.0.min.js"></script> +<script src="/js/d3.v4.min.js"></script> +<script src="/js/metricsgraphics.min.js"></script> + <script type="text/javascript"> + (function() { +var ctx = document.getElementById('myChart').getContext('2d'); + d3.json('<%= alcoolog_path(@conn, :nick_volumes_json, @network, @nick, days: @days) %>', function(data) { +var myChart = new Chart(ctx, { + type: 'bar', + data: { + labels: data.map(function(data) { return new Date(data.date) }), + datasets: [{ + label: 'Volumes d\'alcool', + data: data.map(function(data) { return data.volumes }), + borderWidth: 1 + }] + }, + options: { + scales: { + xAxes: [{ + type: 'time', + time: { + unit: 'day' + } + }], + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + } +}); +}); + + + + +var ctxgl = document.getElementById('myChartGl').getContext('2d'); + d3.json('<%= alcoolog_path(@conn, :nick_gls_json, @network, @nick, days: @days) %>', function(data) { +var myChartgl = new Chart(ctxgl, { + type: 'bar', + data: { + labels: data.map(function(data) { return new Date(data.date) }), + datasets: [{ + label: 'g/l ingérés', + data: data.map(function(data) { return data.gls }), + borderWidth: 1 + }] + }, + options: { + scales: { + xAxes: [{ + type: 'time', + time: { + unit: 'day' + } + }], + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + } +}); +}); + + + d3.json('<%= alcoolog_path(@conn, :nick_log_json, @network, @nick, days: @days) %>', function(data) { + data = data.map(function(d) { + date = new Date(d.at); + return {"date": date, "active": d.active}; + }); + MG.data_graphic({ + title: "g/l, All Time", + data: data, + width: 1000, + height: 400, + target: document.getElementById('metrics-gl'), + x_accessor: 'date', + y_accessor: 'active', + }); + }); +})(); +</script> diff --git a/lib/web/templates/irc/index.html.eex b/lib/web/templates/irc/index.html.eex new file mode 100644 index 0000000..182624d --- /dev/null +++ b/lib/web/templates/irc/index.html.eex @@ -0,0 +1,44 @@ +<div class="hidden sm:block"> + <nav class="flex flex-wrap space-x-4"> + <%= link("live", to: NolaWeb.Router.Helpers.live_path(NolaWeb.Endpoint, NolaWeb.ChatLive, @network, NolaWeb.format_chan(@chan)), class: "py-4 font-medium leading-5 rounded-md text-gray-500 hover:text-gray-700 focus:outline-none focus:text-indigo-600 focus:bg-indigo-50", 'data-turbo': false) %> + <% list = for {identifier, _} <- @commands do %> + <% name = String.replace(identifier, "_", " ") %> + <%= link(name, to: "##{identifier}", class: "py-4 font-medium leading-5 rounded-md text-gray-500 hover:text-gray-700 focus:outline-none focus:text-indigo-600 focus:bg-indigo-50") %> + <% end %> + <%= list %> + </nav> +</div> + +<%= if @members != [] do %> + <ul class="flex flew-wrap space-x-4 mt-12"> + <%= for user <- @members do %><li><%= user.nick %></li><% end %> + </ul> +<% end %> + +<div class="irchelps space-y-6 mt-12"> + <%= for {identifier, help} <- @commands do %> + <%= if help do %> + <div class="bg-white border-b border-gray-200" id="<%= identifier %>"> + <div class="prose w-auto max-w-full"><%= NolaWeb.LayoutView.liquid_markdown(@conn, help) %></div> + </div> + <% end %> + <% end %> +</div> + +<br/><br/><br/> + +<p> +<small> + <strong>Légende:</strong><br /> + entre <code>< ></code>: argument obligatoire,<br /> + entre <code>[ ]</code>: argument optionel; <code>[1 | ]</code>: argument optionel avec valeur par défaut. +</small> +</p> + +<br/><br/><br/> + +<p> +<small> + running beautte version <%= Nola.version() %> — <a href="<%= Nola.source_url() %>">git</a> +</small> +</p> diff --git a/lib/web/templates/irc/txt.html.eex b/lib/web/templates/irc/txt.html.eex new file mode 100644 index 0000000..fd4ea00 --- /dev/null +++ b/lib/web/templates/irc/txt.html.eex @@ -0,0 +1,27 @@ +<style type="text/css"> +:target { + background-color: #ffa; + font-weight: bold; +} +ol > li > a { + display: none; +} +ol > li:hover > a { + display: inline-block; + color: gray; +} +</style> + +<ol class="prose space-y-3 w-auto max-w-full list-outside list-decimal m-4 leading-loose"> + <%= for {txt, id} <- Enum.with_index(@data) do %> + <li class="text-lg" id="q<%= id+1 %>"> + + <%= txt %> + <a href="#q<%= id+1 %>" class="text-xs text-gray-500 space-x-3">#</a> + </li> + <% end %> +</ol> + +<p> + <a href="/-/txt/<%= @name %>.txt" class="text-xs text-gray-500">télécharger au format texte</a> +</p> diff --git a/lib/web/templates/irc/txts.html.eex b/lib/web/templates/irc/txts.html.eex new file mode 100644 index 0000000..aff0c5d --- /dev/null +++ b/lib/web/templates/irc/txts.html.eex @@ -0,0 +1,49 @@ +<div> + <h2 class="text-gray-500 text-xs font-medium uppercase tracking-wide"> + <strong><%= @lines %></strong> lignes dans <strong><%= @files %></strong> fichiers + <a href="#help" class="ml-4 font-bold">Aide</a> + <a href="#system" class="text-gray-300 ml-4">Fichiers système</a> + </h2> + <ul class="mt-3 grid grid-cols-1 gap-5 sm:gap-6 sm:grid-cols-4 lg:grid-cols-6"> + <%= for {txt, data} <- @data do %> + <% base_url = cond do + @conn.assigns[:chan] -> "/#{@conn.assigns.network}/#{NolaWeb.format_chan(@conn.assigns.chan)}" + true -> "/-" + end %> + <li class="col-span-1 flex shadow-sm rounded-m"> + <div class="flex-1 flex items-center justify-between border-l border-t border-r border-b border-gray-200 bg-white rounded-md truncate"> + <div class="flex-1 px-4 py-2 text-sm leading-5 truncate"> + <a href="<%= base_url %>/txt/<%= txt %>" class="text-gray-900 text-lg font-medium hover:text-gray-600 transition ease-in-out duration-150"><%= txt %></a> + <p class="text-gray-500"><%= Enum.count(data) %> lignes</p> + </div> + </div> + </li> + <% end %> + </ul> +</div> + +<div class="prose mt-12" id="help""><%= NolaWeb.LayoutView.liquid_markdown(@conn, @doc) %></div> + +<div class="mt-24"> + <h2 class="text-gray-500 text-xs font-medium uppercase tracking-wide"> + Fichiers système + </h2> + + <ul class="mt-3 grid grid-cols-1 gap-5 sm:gap-6 sm:grid-cols-4 lg:grid-cols-6"> + <%= for {txt, data} <- @system do %> + <% base_url = cond do + @conn.assigns[:chan] -> "/#{@conn.assigns.network}/#{NolaWeb.format_chan(@conn.assigns.chan)}" + true -> "/-" + end %> + <li class="col-span-1 flex shadow-sm rounded-m"> + <div class="flex-1 flex items-center justify-between border-l border-t border-r border-b border-gray-200 bg-white rounded-md truncate"> + <div class="flex-1 px-4 py-2 text-sm leading-5 truncate"> + <a href="<%= base_url %>/txt/<%= txt %>" class="text-gray-900 text-lg font-medium hover:text-gray-600 transition ease-in-out duration-150"><%= txt %></a> + <p class="text-gray-500"><%= Enum.count(data) %> lignes</p> + </div> + </div> + </li> + <% end %> + </ul> + +</div> diff --git a/lib/web/templates/layout/app.html.eex b/lib/web/templates/layout/app.html.eex new file mode 100644 index 0000000..c774369 --- /dev/null +++ b/lib/web/templates/layout/app.html.eex @@ -0,0 +1,126 @@ +<div> + <div class="bg-gray-800 pb-32"> + <nav class="bg-gray-800"> + <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> + <div class="border-b border-gray-700"> + <div class="flex items-center justify-between h-16 px-4 sm:px-0"> + <div class="flex items-center"> + <div class="flex-shrink-0"> + <img class="h-8 w-8" src="https://tailwindui.com/img/logos/workflow-mark-on-dark.svg" alt="Workflow logo"> + </div> + <div class="hidden md:block"> + <div class="ml-10 flex items-baseline"> + <a href="/" class="px-3 py-2 rounded-md text-sm font-medium text-white bg-gray-900 focus:outline-none focus:text-white focus:bg-gray-700"><%= Nola.name() %></a> + <!--<a href="#" class="ml-4 px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700 focus:outline-none focus:text-white focus:bg-gray-700">Team</a> + <a href="#" class="ml-4 px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700 focus:outline-none focus:text-white focus:bg-gray-700">Projects</a> + <a href="#" class="ml-4 px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700 focus:outline-none focus:text-white focus:bg-gray-700">Calendar</a> + <a href="#" class="ml-4 px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700 focus:outline-none focus:text-white focus:bg-gray-700">Reports</a>--> + </div> + </div> + </div> + <div class="hidden md:block"> + <div class="ml-4 flex items-center md:ml-6"> + <!--<button class="p-1 border-2 border-transparent text-gray-400 rounded-full hover:text-white focus:outline-none focus:text-white focus:bg-gray-700" aria-label="Notifications"> + <svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" /> + </svg> + </button>--> + <!-- Profile dropdown --> + <div class="ml-3 relative"> + <div> + <button class="max-w-xs flex items-center text-sm rounded-full text-white focus:outline-none focus:shadow-solid" id="user-menu" aria-label="User menu" aria-haspopup="true"> + <!--<img class="h-8 w-8 rounded-full" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="">--> + ~<%= if @conn.assigns[:account], do: @conn.assigns.account.name %> + + </button> + </div> + <!-- + Profile dropdown panel, show/hide based on dropdown state. + + Entering: "transition ease-out duration-100" + From: "transform opacity-0 scale-95" + To: "transform opacity-100 scale-100" + Leaving: "transition ease-in duration-75" + From: "transform opacity-100 scale-100" + To: "transform opacity-0 scale-95" + --> + <!--<div class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg"> + <div class="py-1 rounded-md bg-white shadow-xs"> + <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Your Profile</a> + <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Settings</a> + <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Sign out</a> + </div> + </div>--> + </div> + </div> + </div> + <div class="-mr-2 flex md:hidden"> + <!-- Mobile menu button --> + <button class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:bg-gray-700 focus:text-white"> + <!-- Menu open: "hidden", Menu closed: "block" --> + <svg class="block h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> + </svg> + <!-- Menu open: "block", Menu closed: "hidden" --> + <svg class="hidden h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> + </svg> + </button> + </div> + </div> + </div> + </div> + + <!-- + Mobile menu, toggle classes based on menu state. + + Open: "block", closed: "hidden" + --> + <div class="hidden border-b border-gray-700 md:hidden"> + <div class="px-2 py-3 sm:px-3"> + <a href="#" class="block px-3 py-2 rounded-md text-base font-medium text-white bg-gray-900 focus:outline-none focus:text-white focus:bg-gray-700">Dashboard</a> + <a href="#" class="mt-1 block px-3 py-2 rounded-md text-base font-medium text-gray-300 hover:text-white hover:bg-gray-700 focus:outline-none focus:text-white focus:bg-gray-700">Team</a> + <a href="#" class="mt-1 block px-3 py-2 rounded-md text-base font-medium text-gray-300 hover:text-white hover:bg-gray-700 focus:outline-none focus:text-white focus:bg-gray-700">Projects</a> + <a href="#" class="mt-1 block px-3 py-2 rounded-md text-base font-medium text-gray-300 hover:text-white hover:bg-gray-700 focus:outline-none focus:text-white focus:bg-gray-700">Calendar</a> + <a href="#" class="mt-1 block px-3 py-2 rounded-md text-base font-medium text-gray-300 hover:text-white hover:bg-gray-700 focus:outline-none focus:text-white focus:bg-gray-700">Reports</a> + </div> + <div class="pt-4 pb-3 border-t border-gray-700"> + <div class="flex items-center px-5"> + <div class="flex-shrink-0"> + <img class="h-10 w-10 rounded-full" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt=""> + </div> + <div class="ml-3"> + <div class="text-base font-medium leading-none text-white">Tom Cook</div> + <div class="mt-1 text-sm font-medium leading-none text-gray-400">tom@example.com</div> + </div> + </div> + <div class="mt-3 px-2" role="menu" aria-orientation="vertical" aria-labelledby="user-menu"> + <a href="#" class="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:text-white focus:bg-gray-700" role="menuitem">Your Profile</a> + <a href="#" class="mt-1 block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:text-white focus:bg-gray-700" role="menuitem">Settings</a> + <a href="#" class="mt-1 block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:text-white focus:bg-gray-700" role="menuitem">Sign out</a> + </div> + </div> + </div> + </nav> + <header class="py-10"> + <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> + <h1 class="text-3xl leading-9 font-bold text-white"> + <%= if n = @conn.assigns[:network] do %><a href="/<%= n %>"><%= n %></a> › <% end %> + <%= if c = @conn.assigns[:chan] do %><a href="/<%= @conn.assigns.network %>/<%= NolaWeb.format_chan(c) %>"><%= c %></a> › <% end %> + <%= for({name, href} <- Enum.uniq(@conn.assigns[:breadcrumbs]||[]), do: [link(name, to: href), raw(" › ")]) %> + <%= @conn.assigns[:title] %> + </h1> + </div> + </header> + </div> + + <main class="-mt-32 h-full"> + <div class="max-w-7xl h-full mx-auto pb-12 px-4 sm:px-6 lg:px-8"> + <!-- Replace with your content --> + <div class="bg-white h-full rounded-lg shadow px-5 py-6 sm:px-6"> + <%= @inner_content %> + </div> + <!-- /End replace --> + </div> + </main> +</div> diff --git a/lib/web/templates/layout/root.html.leex b/lib/web/templates/layout/root.html.leex new file mode 100644 index 0000000..6a48506 --- /dev/null +++ b/lib/web/templates/layout/root.html.leex @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <%= page_title(@conn) %> + <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"> + <meta name="robots" content="noindex, noarchive, nofollow, nosnippet" /> + <title><%= Map.get(assigns, :title, "") %></title> + <link rel="stylesheet" href="<%= static_path(@conn, "/assets/site.css") %>"> + <%= csrf_meta_tag() %> + <script src="<%= static_path(@conn, "/assets/site.js") %>" defer></script> + </head> + <body> + <%= @inner_content %> + </body> +</html> diff --git a/lib/web/templates/network/index.html.eex b/lib/web/templates/network/index.html.eex new file mode 100644 index 0000000..fc024dd --- /dev/null +++ b/lib/web/templates/network/index.html.eex @@ -0,0 +1 @@ +pouet diff --git a/lib/web/templates/open_id/error.html.eex b/lib/web/templates/open_id/error.html.eex new file mode 100644 index 0000000..d1b35b9 --- /dev/null +++ b/lib/web/templates/open_id/error.html.eex @@ -0,0 +1,3 @@ +<h1 class="text-xl font-bold text-red-800">OpenID authentication error</h1> + +<p class="mt-12 text-base prose"><%= @error %></p> diff --git a/lib/web/templates/page/api.html.eex b/lib/web/templates/page/api.html.eex new file mode 100644 index 0000000..03dfa6b --- /dev/null +++ b/lib/web/templates/page/api.html.eex @@ -0,0 +1,35 @@ +<h1>sys.115ans.net/api</h1> + +<h2>Icecast Status</h2> + +<h3>GET /api/icecast.json</h3> + +<p> + Content-Type: <code>application/json</code> +</p> + +<pre><code> +{ + "np": String, + "genre": null | String, + "live": false | true +} +</pre></code> + +<h3>GET /api/icecast.sse</h3> +<p> + Content-Type: <code>text/event-stream</code> +</p> + +<p> + Stream of: +</p> + + <ul> + <li><strong>icecast</strong> events (same format as <code>/api/icecast.json</code>)</li> + <li><strong>ping</strong> events (to keep-alive connection. You can safely ignore them)</li> + </ul> +<p> + On client connection, the server sends the latest <code>icecast</code> status known. +</p> + diff --git a/lib/web/templates/page/index.html.eex b/lib/web/templates/page/index.html.eex new file mode 100644 index 0000000..1b8519a --- /dev/null +++ b/lib/web/templates/page/index.html.eex @@ -0,0 +1 @@ +<p>vOv</p> diff --git a/lib/web/templates/page/irc.html.eex b/lib/web/templates/page/irc.html.eex new file mode 100644 index 0000000..f6598ee --- /dev/null +++ b/lib/web/templates/page/irc.html.eex @@ -0,0 +1,19 @@ +<h1>bot `115ans</h1> + +<p>Si vous cherchez l'IRC c'est <a href="https://115ans.net/irc/">par là</a>.</p> + +<style type="text/css"> +.help-entry h1 { + font-size: 18px; +} +.help-entry h2 { + font-size: 16px; +} +</style> + +<div class="irchelps"> + <%= for help <- @bot_helps do %> + <div class="help-entry"><%= help |> Earmark.as_html! |> raw() %></div> + <% end %> +</div> + diff --git a/lib/web/templates/page/user.html.eex b/lib/web/templates/page/user.html.eex new file mode 100644 index 0000000..de9f718 --- /dev/null +++ b/lib/web/templates/page/user.html.eex @@ -0,0 +1,43 @@ +<div class="prose prose-lg"> +<ul> + <li><%= link("Help", to: "/-") %></li> + <%= unless List.keyfind(@metas, "identity-id", 0) do %> + <li><%= link("Connect with random.sh", to: Routes.open_id_path(@conn, :login)) %></li> + <% end %> +</ul> + +<h2>channels</h2> +<ul> + <%= for {net, channel} <- @memberships do %> + <li> + <% url = NolaWeb.Router.Helpers.irc_path(NolaWeb.Endpoint, :index, net, NolaWeb.format_chan(channel)) %> + <%= link([net, ": ", content_tag(:strong, channel)], to: url) %> + </li> + <% end %> +</ul> + +<h2>connections</h2> +<ul> + <%= for user <- @users do %> + <li> + <strong><%= user.network %></strong>: <strong><%= user.nick %></strong>!<%= user.username %>@<%= user.host %> <i><%= user.realname %></i><br /> + <%= Enum.join(Enum.intersperse(Enum.map(user.privileges, fn({c, _}) -> c end), ", ")) %> + </li> + <% end %> +</ul> + +<h2>account</h2> +<ul> + <li>account-id: <%= @conn.assigns.account.id %></li> + <%= for {k, v} <- @metas do %> + <li><%= k %>: <%= to_string(v) %></li> + <% end %> +</ul> + +<strong>irc auths:</strong> +<ul> + <%= for {net, {predicate, v}} <- @predicates do %> + <li><%= net %>: <%= to_string(predicate) %>, <%= v %></li> + <% end %> +</ul> +</div> diff --git a/lib/web/templates/page/widget.html.eex b/lib/web/templates/page/widget.html.eex new file mode 100644 index 0000000..65853b3 --- /dev/null +++ b/lib/web/templates/page/widget.html.eex @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<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/widget.css") %>"> +<script src="<%= static_path(@conn, "/assets/js/soundmanager2-jsmin.js") %>"></script> +<script src="<%= static_path(@conn, "/assets/js/widget.js") %>"></script> +</head> +<body> + <div id="player" class="<%= if @icecast.live, do: "live", else: "autodj" %>"> + <div id="state">▶</div> + <div id="titles"> + <div id="genre"><%= @icecast.genre %></div> + <div id="np"><%= @icecast.np %></div> + </div> + </div> +</body> +</html> diff --git a/lib/web/views/alcoolog_view.ex b/lib/web/views/alcoolog_view.ex new file mode 100644 index 0000000..ad52472 --- /dev/null +++ b/lib/web/views/alcoolog_view.ex @@ -0,0 +1,6 @@ +defmodule NolaWeb.AlcoologView do + use NolaWeb, :view + require Integer + +end + diff --git a/lib/web/views/error_helpers.ex b/lib/web/views/error_helpers.ex new file mode 100644 index 0000000..25214bd --- /dev/null +++ b/lib/web/views/error_helpers.ex @@ -0,0 +1,40 @@ +defmodule NolaWeb.ErrorHelpers do + @moduledoc """ + Conveniences for translating and building error messages. + """ + + use Phoenix.HTML + + @doc """ + Generates tag for inlined form input errors. + """ + def error_tag(form, field) do + Enum.map(Keyword.get_values(form.errors, field), fn (error) -> + content_tag :span, translate_error(error), class: "help-block" + end) + end + + @doc """ + Translates an error message using gettext. + """ + def translate_error({msg, opts}) do + # Because error messages were defined within Ecto, we must + # call the Gettext module passing our Gettext backend. We + # also use the "errors" domain as translations are placed + # in the errors.po file. + # Ecto will pass the :count keyword if the error message is + # meant to be pluralized. + # On your own code and templates, depending on whether you + # need the message to be pluralized or not, this could be + # written simply as: + # + # dngettext "errors", "1 file", "%{count} files", count + # dgettext "errors", "is invalid" + # + if count = opts[:count] do + Gettext.dngettext(NolaWeb.Gettext, "errors", msg, msg, count, opts) + else + Gettext.dgettext(NolaWeb.Gettext, "errors", msg, opts) + end + end +end diff --git a/lib/web/views/error_view.ex b/lib/web/views/error_view.ex new file mode 100644 index 0000000..5cad939 --- /dev/null +++ b/lib/web/views/error_view.ex @@ -0,0 +1,17 @@ +defmodule NolaWeb.ErrorView do + use NolaWeb, :view + + def render("404.html", _assigns) do + "Page not found" + end + + def render("500.html", _assigns) do + "Internal server error" + end + + # In case no render clause matches or no + # template is found, let's render it as 500 + def template_not_found(_template, assigns) do + render "500.html", assigns + end +end diff --git a/lib/web/views/irc_view.ex b/lib/web/views/irc_view.ex new file mode 100644 index 0000000..331d91f --- /dev/null +++ b/lib/web/views/irc_view.ex @@ -0,0 +1,3 @@ +defmodule NolaWeb.IrcView do + use NolaWeb, :view +end diff --git a/lib/web/views/layout_view.ex b/lib/web/views/layout_view.ex new file mode 100644 index 0000000..2bffc6f --- /dev/null +++ b/lib/web/views/layout_view.ex @@ -0,0 +1,81 @@ +defmodule NolaWeb.LayoutView do + use NolaWeb, :view + + def liquid_markdown(conn, text) do + context_path = cond do + conn.assigns[:chan] -> "/#{conn.assigns[:network]}/#{NolaWeb.format_chan(conn.assigns[:chan])}" + conn.assigns[:network] -> "/#{conn.assigns[:network]}/-" + true -> "/-" + end + + {:ok, ast} = Liquex.parse(text) + context = Liquex.Context.new(%{ + "context_path" => context_path + }) + {content, _} = Liquex.render(ast, context) + content + |> to_string() + |> Earmark.as_html!() + |> raw() + end + + def page_title(conn) do + target = cond do + conn.assigns[:chan] -> + "#{conn.assigns.chan} @ #{conn.assigns.network}" + conn.assigns[:network] -> conn.assigns.network + true -> Keyword.get(Nola.name()) + end + + breadcrumb_title = Enum.map(Map.get(conn.assigns, :breadcrumbs)||[], fn({title, _href}) -> title end) + + title = [conn.assigns[:title], breadcrumb_title, target] + |> List.flatten() + |> Enum.uniq() + |> Enum.filter(fn(x) -> x end) + |> Enum.intersperse(" / ") + |> Enum.join() + + content_tag(:title, title) + end + + def format_time(date, with_relative \\ true) do + alias Timex.Format.DateTime.Formatters + alias Timex.Timezone + date = if is_integer(date) do + date + |> DateTime.from_unix!(:millisecond) + |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase) + else + date + |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase) + end + + now = DateTime.now!("Europe/Paris", Tzdata.TimeZoneDatabase) + + now_week = Timex.iso_week(now) + date_week = Timex.iso_week(date) + + {y, w} = now_week + now_last_week = {y, w-1} + now_last_roll = 7-Timex.days_to_beginning_of_week(now) + + date_date = DateTime.to_date(date) + now_date = DateTime.to_date(date) + + format = cond do + date.year != now.year -> "{D}/{M}/{YYYY} {h24}:{m}" + date_date == now_date -> "{h24}:{m}" + (now_week == date_week) || (date_week == now_last_week && (Date.day_of_week(date) >= now_last_roll)) -> "{WDfull} {h24}:{m}" + (now.year == date.year && now.month == date.month) -> "{WDfull} {D} {h24}:{m}" + true -> "{WDfull} {D} {M} {h24}:{m}" + end + + {:ok, relative} = Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr") + {:ok, full} = Formatters.Default.lformat(date, "{WDfull} {D} {YYYY} {h24}:{m}", "fr") #"{h24}:{m} {WDfull} {D}", "fr") + {:ok, detail} = Formatters.Default.lformat(date, format, "fr") #"{h24}:{m} {WDfull} {D}", "fr") + + content_tag(:time, if(with_relative, do: relative, else: detail), [title: full]) + end + +end diff --git a/lib/web/views/network_view.ex b/lib/web/views/network_view.ex new file mode 100644 index 0000000..7a24db1 --- /dev/null +++ b/lib/web/views/network_view.ex @@ -0,0 +1,4 @@ +defmodule NolaWeb.NetworkView do + use NolaWeb, :view + +end diff --git a/lib/web/views/open_id_view.ex b/lib/web/views/open_id_view.ex new file mode 100644 index 0000000..bd8089b --- /dev/null +++ b/lib/web/views/open_id_view.ex @@ -0,0 +1,4 @@ +defmodule NolaWeb.OpenIdView do + use NolaWeb, :view + +end diff --git a/lib/web/views/page_view.ex b/lib/web/views/page_view.ex new file mode 100644 index 0000000..1bfaadd --- /dev/null +++ b/lib/web/views/page_view.ex @@ -0,0 +1,3 @@ +defmodule NolaWeb.PageView do + use NolaWeb, :view +end diff --git a/lib/web/web.ex b/lib/web/web.ex new file mode 100644 index 0000000..906e961 --- /dev/null +++ b/lib/web/web.ex @@ -0,0 +1,99 @@ +defmodule NolaWeb do + @moduledoc """ + The entrypoint for defining your web interface, such + as controllers, views, channels and so on. + + This can be used in your application as: + + use NolaWeb, :controller + use NolaWeb, :view + + The definitions below will be executed for every view, + controller, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. Instead, define any helper function in modules + and import those modules here. + """ + + def format_chan("##") do + "♯♯" + end + + def format_chan("#") do + "♯" + end + + def format_chan("#"<>chan) do + chan + end + + def format_chan(chan = "!"<>_), do: chan + + def reformat_chan("♯") do + "#" + end + def reformat_chan("♯♯") do + "##" + end + def reformat_chan(chan = "!"<>_), do: chan + + def reformat_chan(chan) do + "#"<>chan + end + + def controller do + quote do + use Phoenix.Controller, namespace: NolaWeb + import Plug.Conn + import NolaWeb.Router.Helpers + import NolaWeb.Gettext + alias NolaWeb.Router.Helpers, as: Routes + end + end + + def view do + quote do + use Phoenix.View, root: "lib/nola_web/templates", + namespace: NolaWeb + + # Import convenience functions from controllers + import Phoenix.Controller, only: [get_flash: 2, view_module: 1] + + # Use all HTML functionality (forms, tags, etc) + use Phoenix.HTML + + import NolaWeb.Router.Helpers + import NolaWeb.ErrorHelpers + import NolaWeb.Gettext + + import Phoenix.LiveView.Helpers + + alias NolaWeb.Router.Helpers, as: Routes + end + end + + def router do + quote do + use Phoenix.Router + import Plug.Conn + import Phoenix.Controller + import Phoenix.LiveView.Router + end + end + + def channel do + quote do + use Phoenix.Channel + import NolaWeb.Gettext + end + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end |