diff --git a/lib/lsg_web.ex b/lib/lsg_web.ex index eb0cdc5..3d9ab9a 100644 --- a/lib/lsg_web.ex +++ b/lib/lsg_web.ex @@ -1,93 +1,99 @@ defmodule LSGWeb 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 LSGWeb, :controller use LSGWeb, :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: LSGWeb import Plug.Conn import LSGWeb.Router.Helpers import LSGWeb.Gettext + alias LSGWeb.Router.Helpers, as: Routes end end def view do quote do use Phoenix.View, root: "lib/lsg_web/templates", namespace: LSGWeb # 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 LSGWeb.Router.Helpers import LSGWeb.ErrorHelpers import LSGWeb.Gettext import Phoenix.LiveView.Helpers + + alias LSGWeb.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 LSGWeb.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 diff --git a/lib/lsg_web/context_plug.ex b/lib/lsg_web/context_plug.ex index 7896ace..29eab28 100644 --- a/lib/lsg_web/context_plug.ex +++ b/lib/lsg_web/context_plug.ex @@ -1,81 +1,91 @@ defmodule LSGWeb.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 + end + end + def call(conn, opts) do account = with \ - {:account, account_id} when is_binary(account_id) <- {:account, get_session(conn, :account)}, + {: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 LSGWeb.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(LSGWeb.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/lsg_web/controllers/irc_controller.ex b/lib/lsg_web/controllers/irc_controller.ex index 32007c2..d518481 100644 --- a/lib/lsg_web/controllers/irc_controller.ex +++ b/lib/lsg_web/controllers/irc_controller.ex @@ -1,100 +1,101 @@ defmodule LSGWeb.IrcController do use LSGWeb, :controller plug LSGWeb.ContextPlug def index(conn, params) do network = Map.get(params, "network") - channel = if c = Map.get(params, "channel"), do: LSGWeb.reformat_chan(c) + channel = if c = Map.get(params, "chan"), do: LSGWeb.reformat_chan(c) commands = for mod <- Enum.uniq([IRC.Account.AccountPlugin] ++ IRC.Plugin.enabled()) do if is_atom(mod) do identifier = Module.split(mod) |> List.last |> String.replace("Plugin", "") |> Macro.underscore {identifier, mod.irc_doc()} end end |> Enum.filter(& &1) |> Enum.filter(fn({_, doc}) -> doc end) members = cond do - network -> IRC.Membership.expanded_members_or_friends(conn.assigns.account, network, channel) - true -> IRC.Membership.of_account(conn.assigns.account) + 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 = LSG.IRC.TxtPlugin.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}/#{LSGWeb.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(:lsg, :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/lsg_web/controllers/open_id_controller.ex b/lib/lsg_web/controllers/open_id_controller.ex new file mode 100644 index 0000000..d5af318 --- /dev/null +++ b/lib/lsg_web/controllers/open_id_controller.ex @@ -0,0 +1,64 @@ +defmodule LSGWeb.OpenIdController do + use LSGWeb, :controller + plug LSGWeb.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(:lsg, :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(LSGWeb.Endpoint, :callback) + ]) + end +end diff --git a/lib/lsg_web/live/chat_live.ex b/lib/lsg_web/live/chat_live.ex index d736d72..e7a44db 100644 --- a/lib/lsg_web/live/chat_live.ex +++ b/lib/lsg_web/live/chat_live.ex @@ -1,97 +1,100 @@ defmodule LSGWeb.ChatLive do use Phoenix.LiveView use Phoenix.HTML require Logger def mount(%{"network" => network, "chan" => chan}, %{"account" => account_id}, socket) do chan = LSGWeb.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, _} = LSG.IRC.BufferPlugin.select_buffer(connection.network, chan) + backlog = case LSG.IRC.BufferPlugin.select_buffer(connection.network, chan) do + {backlog, _} -> 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, Enum.reverse(backlog)) + |> 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)) |> assign(:backlog, socket.assigns.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)) |> assign(:backlog, socket.assigns.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)) |> assign(:backlog, socket.assigns.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)) |> assign(:backlog, socket.assigns.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 socket = socket |> assign(:backlog, socket.assigns.backlog ++ [message]) {:noreply, socket} end def handle_info(info, socket) do Logger.debug("Unhandled info: #{inspect info}") {:noreply, socket} end end diff --git a/lib/lsg_web/router.ex b/lib/lsg_web/router.ex index e872cef..eba71dc 100644 --- a/lib/lsg_web/router.ex +++ b/lib/lsg_web/router.ex @@ -1,75 +1,81 @@ defmodule LSGWeb.Router do use LSGWeb, :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, {LSGWeb.LayoutView, :root} end pipeline :api do plug :accepts, ["json", "sse"] end pipeline :matrix_app_service do plug :accepts, ["json"] plug LSG.Matrix.Plug.Auth plug LSG.Matrix.Plug.SetConfig end scope "/api", LSGWeb do pipe_through :api get "/irc-auth.sse", IrcAuthSseController, :sse post "/sms/callback/Ovh", SmsController, :ovh_callback, as: :sms end scope "/", LSGWeb 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 "/-/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 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/lsg_web/templates/alcoolog/auth.html.eex b/lib/lsg_web/templates/alcoolog/auth.html.eex index af6db53..6e5cedc 100644 --- a/lib/lsg_web/templates/alcoolog/auth.html.eex +++ b/lib/lsg_web/templates/alcoolog/auth.html.eex @@ -1,39 +1,43 @@ -
<%= if @bot, do: "Send this to #{@bot} on #{@network}:", else: "Find your bot nickname and send:" %>
/msg <%= @bot || "the-bot-nickname" %> web
... then come back to this address.
Légende:
entre < >
: argument obligatoire,
entre [ ]
: argument optionel; [1 | ]
: argument optionel avec valeur par défaut.
source: git.yt/115ans/sys
diff --git a/lib/lsg_web/templates/layout/app.html.eex b/lib/lsg_web/templates/layout/app.html.eex index bca1555..b918ca5 100644 --- a/lib/lsg_web/templates/layout/app.html.eex +++ b/lib/lsg_web/templates/layout/app.html.eex @@ -1,126 +1,126 @@<%= @error %>
diff --git a/lib/lsg_web/templates/page/user.html.eex b/lib/lsg_web/templates/page/user.html.eex index 56fc485..6eeb39d 100644 --- a/lib/lsg_web/templates/page/user.html.eex +++ b/lib/lsg_web/templates/page/user.html.eex @@ -1,40 +1,43 @@