diff --git a/config/config.exs b/config/config.exs index 8a1faa6..e1bbf1f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,58 +1,59 @@ # This file is responsible for configuring your application # and its dependencies with the aid of the Mix.Config module. # # This configuration file is loaded before any dependency and # is restricted to this project. use Mix.Config config :logger, level: :debug config :logger, :console, format: "$date $time [$level$levelpad] $metadata$message\n", metadata: :all config :phoenix, :json_library, Jason # General application configuration config :lsg, namespace: LSG config :lsg, :data_path, "priv" -config :lsg, :irc, - name: "ircbot" +config :lsg, :brand, + name: "Nola", + source_url: "https://phab.random.sh/source/Bot/" config :ex_aws, region: "us-east-1", host: "s3.wasabisys.com", s3: [ host: "s3.wasabisys.com", region: "us-east-1", scheme: "https://" ] # Configures the endpoint config :lsg, LSGWeb.Endpoint, url: [host: "localhost"], secret_key_base: "cAFb7x2p/D7PdV8/C6Os18uygoD0FVQh3efNEFc5+5L529q3dofZtZye/BG12MRZ", render_errors: [view: LSGWeb.ErrorView, accepts: ~w(html json)], server: true, live_view: [signing_salt: "CHANGE_ME_FFS"], pubsub: [name: LSG.PubSub, adapter: Phoenix.PubSub.PG2] config :mime, :types, %{"text/event-stream" => ["sse"]} config :lsg, LSG.IRC.LastFmHandler, api_key: "x", api_secret: "x" config :lsg, LSG.IRC.YouTubeHandler, api_key: "x", invidious: "yewtu.be" config :mnesia, dir: '.mnesia/#{Mix.env}/#{node()}' # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env}.exs" diff --git a/lib/irc/irc.ex b/lib/irc/irc.ex index fbad6e1..d9fdfb5 100644 --- a/lib/irc/irc.ex +++ b/lib/irc/irc.ex @@ -1,79 +1,79 @@ defmodule IRC do defmodule Message do @derive {Poison.Encoder, except: [:replyfun]} defstruct [:id, :text, {:transport, :irc}, :network, :account, :sender, :channel, :trigger, :replyfun, :at, {:meta, %{}} ] end defmodule Trigger do @derive Poison.Encoder defstruct [:type, :trigger, :args] end def send_message_as(account, network, channel, text, force_puppet \\ false) do connection = IRC.Connection.get_network(network) if connection && (force_puppet || IRC.PuppetConnection.whereis(account, connection)) do IRC.PuppetConnection.start_and_send_message(account, connection, channel, text) else user = IRC.UserTrack.find_by_account(network, account) nick = if(user, do: user.nick, else: account.name) IRC.Connection.broadcast_message(network, channel, "<#{nick}> #{text}") end end def register(key) do case Registry.register(IRC.PubSub, key, []) do {:ok, _} -> :ok error -> error end end def admin?(%Message{sender: sender}), do: admin?(sender) def admin?(%{nick: nick, user: user, host: host}) do - for {n, u, h} <- Application.get_env(:lsg, :irc, [])[:admins]||[] do + for {n, u, h} <- LSG.IRC.env(:admins, []) do admin_part_match?(n, nick) && admin_part_match?(u, user) && admin_part_match?(h, host) end |> Enum.any? end defp admin_part_match?(:_, _), do: true defp admin_part_match?(a, a), do: true defp admin_part_match?(_, _), do: false @max_chars 440 def splitlong(string, max_chars \\ 440) def splitlong(string, max_chars) when is_list(string) do Enum.map(string, fn(s) -> splitlong(s, max_chars) end) |> List.flatten() end def splitlong(string, max_chars) do string |> String.codepoints |> Enum.chunk_every(max_chars) |> Enum.map(&Enum.join/1) end def splitlong_with_prefix(string, prefix, max_chars \\ 440) do prefix = "#{prefix} " max_chars = max_chars - (length(String.codepoints(prefix))) string |> String.codepoints |> Enum.chunk_every(max_chars) |> Enum.map(fn(line) -> prefix <> Enum.join(line) end) end end diff --git a/lib/lsg/lsg.ex b/lib/lsg/lsg.ex index 25f127f..b5da5e0 100644 --- a/lib/lsg/lsg.ex +++ b/lib/lsg/lsg.ex @@ -1,19 +1,30 @@ defmodule LSG do - def source_url() do - "https://git.random.sh/ircbot.git" - end + @default_brand [ + name: "Nola, + source_url: "https://phab.random.sh/source/Bot/", + owner: "Ashamed owner", + owner_email: "contact@my.nola.bot" + ] + + def env(), do: Application.get_env(:lsg) + def env(key, default \\ nil), do: Application.get_env(:lsg, key, default) + + def brand(), do: env(:brand, @default_brand) + def brand(key), do: Keyword.get(brand(), key) + def name(), do: brand(:name) + def source_url(), do: brand(:source_url) def data_path(suffix) do Path.join(data_path(), suffix) end def data_path do Application.get_env(:lsg, :data_path) end def version do Application.spec(:lsg)[:vsn] end end diff --git a/lib/lsg_irc/logger_plugin.ex b/lib/lsg_irc/logger_plugin.ex index e5307bc..de601a6 100644 --- a/lib/lsg_irc/logger_plugin.ex +++ b/lib/lsg_irc/logger_plugin.ex @@ -1,69 +1,70 @@ defmodule LSG.IRC.LoggerPlugin do require Logger @couch_db "bot-logs" def irc_doc(), do: nil def start_link() do GenServer.start_link(__MODULE__, [], name: __MODULE__) end def init([]) do regopts = [plugin: __MODULE__] {:ok, _} = Registry.register(IRC.PubSub, "triggers", regopts) {:ok, _} = Registry.register(IRC.PubSub, "messages", regopts) + {:ok, _} = Registry.register(IRC.PubSub, "irc:outputs", regopts) {:ok, _} = Registry.register(IRC.PubSub, "messages:private", regopts) {:ok, nil} end def handle_info({:irc, :trigger, _, m}, state) do {:noreply, log(m, state)} end def handle_info({:irc, :text, m}, state) do {:noreply, log(m, state)} end def handle_info(info, state) do Logger.debug("logger_plugin: unhandled info: #{info}") {:noreply, state} end def log(entry, state) do case Couch.post(@couch_db, format_to_db(entry)) do {:ok, id, _rev} -> Logger.debug("logger_plugin: saved: #{inspect id}") state error -> Logger.error("logger_plugin: save failed: #{inspect error}") end rescue e -> Logger.error("logger_plugin: rescued processing for #{inspect entry}: #{inspect e}") Logger.error(Exception.format(:error, e, __STACKTRACE__)) state catch e, b -> Logger.error("logger_plugin: catched processing for #{inspect entry}: #{inspect e}") Logger.error(Exception.format(e, b, __STACKTRACE__)) state end def format_to_db(msg = %IRC.Message{id: id}) do msg |> Poison.encode!() |> Map.drop("id") %{"_id" => id || FlakeId.get(), "type" => "irc.message/v1", "object" => msg} end def format_to_db(anything) do %{"_id" => FlakeId.get(), "type" => "object", "object" => anything} end end diff --git a/lib/lsg_irc/lsg_irc.ex b/lib/lsg_irc/lsg_irc.ex index c2782ad..a50abed 100644 --- a/lib/lsg_irc/lsg_irc.ex +++ b/lib/lsg_irc/lsg_irc.ex @@ -1,37 +1,34 @@ defmodule LSG.IRC do require Logger + def env(), do: LSG.env(:irc) + def env(key, default \\ nil), do: Keyword.get(env(), key, default) def application_childs do - env = Application.get_env(:lsg, :irc) import Supervisor.Spec IRC.Connection.setup() IRC.Plugin.setup() - # Probably just needed for migration - #for plugin <- Application.get_env(:lsg, :irc)[:plugins], do: IRC.Plugin.declare(plugin) - [ worker(Registry, [[keys: :duplicate, name: IRC.ConnectionPubSub]], id: :registr_irc_conn), worker(Registry, [[keys: :duplicate, name: IRC.PubSub]], id: :registry_irc), worker(IRC.Membership, []), worker(IRC.Account, []), worker(IRC.UserTrack.Storage, []), worker(IRC.Account.AccountPlugin, []), supervisor(IRC.Plugin.Supervisor, [], [name: IRC.Plugin.Supervisor]), supervisor(IRC.Connection.Supervisor, [], [name: IRC.Connection.Supervisor]), supervisor(IRC.PuppetConnection.Supervisor, [], [name: IRC.PuppetConnection.Supervisor]), ] end + # Start plugins first to let them get on connection events. def after_start() do - # Start plugins first to let them get on connection events. - Logger.debug("IRC.after_start - initializing plugins") + Logger.info("Starting plugins") IRC.Plugin.start_all() - Logger.debug("IRC.after_start - initializing connections") + Logger.info("Starting connections") IRC.Connection.start_all() - Logger.debug("IRC.after_start - ok") end end diff --git a/lib/lsg_web/controllers/page_controller.ex b/lib/lsg_web/controllers/page_controller.ex index a87cf1d..94c9c70 100644 --- a/lib/lsg_web/controllers/page_controller.ex +++ b/lib/lsg_web/controllers/page_controller.ex @@ -1,53 +1,53 @@ defmodule LSGWeb.PageController do use LSGWeb, :controller plug LSGWeb.ContextPlug when action not in [:token] plug LSGWeb.ContextPlug, [restrict: :public] when action in [:token] def token(conn, %{"token" => token}) do with \ {:ok, account, perks} <- LSG.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 <- Application.get_env(:lsg, :irc)[:handlers] do + bot_helps = for mod <- LSG.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/lsg_web/views/layout_view.ex b/lib/lsg_web/views/layout_view.ex index 41c5341..720281d 100644 --- a/lib/lsg_web/views/layout_view.ex +++ b/lib/lsg_web/views/layout_view.ex @@ -1,81 +1,81 @@ defmodule LSGWeb.LayoutView do use LSGWeb, :view def liquid_markdown(conn, text) do context_path = cond do conn.assigns[:chan] -> "/#{conn.assigns[:network]}/#{LSGWeb.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(Application.get_env(:lsg, :irc), :name, "ircbot") + true -> Keyword.get(LSG.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/mix.exs b/mix.exs index 427aeeb..1778206 100644 --- a/mix.exs +++ b/mix.exs @@ -1,98 +1,98 @@ -defmodule LSG.Mixfile do +defmodule Nola.Mixfile do use Mix.Project def project do [ - app: :lsg, - version: version("0.2.6"), + app: :nola, + version: version("0.2.7"), elixir: "~> 1.4", elixirc_paths: elixirc_paths(Mix.env), compilers: [:phoenix, :gettext] ++ Mix.compilers, start_permanent: Mix.env == :prod, deps: deps() ] end def application do [ mod: {LSG.Application, []}, extra_applications: [:logger, :runtime_tools] ] end defp elixirc_paths(:test), do: ["lib", "test/support"] defp elixirc_paths(_), do: ["lib"] defp aliases do [ "assets.deploy": ["make -C assets", "phx.digest"] ] end defp deps do [ {:phoenix, "~> 1.6.0-rc.0", override: true}, {:phoenix_pubsub, "~> 2.0"}, {:phoenix_live_reload, "~> 1.0", only: :dev}, {:phoenix_html, "~> 3.0"}, {:phoenix_live_view, "~> 0.16.0"}, {:phoenix_live_dashboard, "~> 0.5"}, {:telemetry, "~> 1.0.0", override: true}, {:telemetry_metrics, "~> 0.6"}, {:telemetry_poller, "~> 0.5"}, {:plug_cowboy, "~> 2.0"}, {:cowlib, "~> 2.9.1", override: true}, {:plug, "~> 1.7"}, {:gettext, "~> 0.11"}, {:httpoison, "~> 1.8", override: true}, {:jason, "~> 1.0"}, {:poison, "~> 4.0", override: true}, {:floki, "~> 0.19.3"}, {:ecto, "~> 3.4"}, {:exirc, git: "https://git.random.sh/ircbot/exirc.git", branch: "fix-who-nick"}, {:distillery, "~> 2.0"}, {:earmark, "~> 1.2"}, {:oauther, "~> 1.1"}, {:extwitter, "~> 0.14.0"}, {:entropy_string, "~> 1.0.0"}, {:abacus, "~> 0.3.3"}, {:ex_chain, github: "eljojo/ex_chain"}, {:timex, "~> 3.6"}, {:muontrap, "~> 0.5.1"}, {:tzdata, "~> 1.0"}, {:nimble_csv, "~> 0.7.0"}, {:backoff, git: "https://github.com/ferd/backoff", branch: "master"}, {:telegram, git: "https://github.com/hrefhref/telegram.git", branch: "master"}, {:ex_aws, "~> 2.0"}, {:ex_aws_s3, "~> 2.0"}, {:gen_magic, git: "https://github.com/hrefhref/gen_magic", branch: "develop"}, {:liquex, "~> 0.3"}, {:html_entities, "0.4.0", override: true}, {:file_size, "~> 3.0"}, {:ex2ms, "~> 1.0"}, {:polyjuice_client, git: "https://git.random.sh/ircbot/polyjuice_client.git", branch: "master", override: true}, {:matrix_app_service, git: "https://git.random.sh/ircbot/matrix_app_service.ex.git", branch: "master"}, {:sentry, "~> 8.0.5"}, {:logger_json, "~> 4.3"}, {:oauth2, "~> 2.0"}, {:powerdnsex, git: "https://git.random.sh/ircbot/powerdnsex.git", branch: "master"}, {:pfx, "~> 0.7.0"}, {:flake_id, "~> 0.1.0"} ] end defp version(v) do {describe, 0} = System.cmd("git", ~w(describe --dirty --broken --all --tags --long)) [_, rest] = String.split(describe, "/") info = String.trim(rest) env = cond do Mix.env() == :prod -> "" true -> "." <> to_string(Mix.env()) end #build_date_tag = Timex.format!(DateTime.utc_now(), ".build%y%m%d@%H%M", :strftime) build_date_tag = ".build" v <> "+" <> info <> env <> build_date_tag end end