summaryrefslogtreecommitdiff
path: root/lib/nola
diff options
context:
space:
mode:
authorJordan Bracco <href@random.sh>2022-12-20 00:21:54 +0000
committerJordan Bracco <href@random.sh>2022-12-20 19:29:41 +0100
commit2d83df8b32bff7f0028923bb5b64dc0b55f20d03 (patch)
tree1207e67b5b15f540963db05e7be89f3ca950e724 /lib/nola
parentNola rename, the end. pt 6. Refs T77. (diff)
Nola rename: The Big Move, Refs T77
Diffstat (limited to 'lib/nola')
-rw-r--r--lib/nola/application.ex56
-rw-r--r--lib/nola/auth_token.ex59
-rw-r--r--lib/nola/icecast.ex117
-rw-r--r--lib/nola/icecast_agent.ex17
-rw-r--r--lib/nola/nola.ex30
-rw-r--r--lib/nola/subnet.ex84
-rw-r--r--lib/nola/token.ex38
7 files changed, 401 insertions, 0 deletions
diff --git a/lib/nola/application.ex b/lib/nola/application.ex
new file mode 100644
index 0000000..4f3d1da
--- /dev/null
+++ b/lib/nola/application.ex
@@ -0,0 +1,56 @@
+defmodule Nola.Application do
+ use Application
+
+ # See https://hexdocs.pm/elixir/Application.html
+ # for more information on OTP Applications
+ def start(_type, _args) do
+ import Supervisor.Spec
+
+ Logger.add_backend(Sentry.LoggerBackend)
+ :ok = Nola.Matrix.setup()
+ :ok = Nola.TelegramRoom.setup()
+
+ # Define workers and child supervisors to be supervised
+ children = [
+ # Start the endpoint when the application starts
+ supervisor(NolaWeb.Endpoint, []),
+ # Start your own worker by calling: Nola.Worker.start_link(arg1, arg2, arg3)
+ # worker(Nola.Worker, [arg1, arg2, arg3]),
+ worker(Registry, [[keys: :duplicate, name: Nola.BroadcastRegistry]], id: :registry_broadcast),
+ worker(Nola.IcecastAgent, []),
+ worker(Nola.Token, []),
+ worker(Nola.AuthToken, []),
+ Nola.Subnet,
+ {GenMagic.Pool, [name: Nola.GenMagic, pool_size: 2]},
+ #worker(Nola.Icecast, []),
+ ] ++ Nola.IRC.application_childs
+ ++ Nola.Matrix.application_childs
+
+ # See https://hexdocs.pm/elixir/Supervisor.html
+ # for other strategies and supported options
+ opts = [strategy: :one_for_one, name: Nola.Supervisor]
+ sup = Supervisor.start_link(children, opts)
+ start_telegram()
+ spawn_link(fn() -> Nola.IRC.after_start() end)
+ spawn_link(fn() -> Nola.Matrix.after_start() end)
+ spawn_link(fn() -> Nola.TelegramRoom.after_start() end)
+ sup
+ end
+
+ # Tell Phoenix to update the endpoint configuration
+ # whenever the application is updated.
+ def config_change(changed, _new, removed) do
+ NolaWeb.Endpoint.config_change(changed, removed)
+ :ok
+ end
+
+ defp start_telegram() do
+ token = Keyword.get(Application.get_env(:nola, :telegram, []), :key)
+ options = [
+ username: Keyword.get(Application.get_env(:nola, :telegram, []), :nick, "beauttebot"),
+ purge: false
+ ]
+ telegram = Telegram.Bot.ChatBot.Supervisor.start_link({Nola.Telegram, token, options})
+ end
+
+end
diff --git a/lib/nola/auth_token.ex b/lib/nola/auth_token.ex
new file mode 100644
index 0000000..d125ea4
--- /dev/null
+++ b/lib/nola/auth_token.ex
@@ -0,0 +1,59 @@
+defmodule Nola.AuthToken do
+ use GenServer
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, [], [name: __MODULE__])
+ end
+
+ def lookup(id) do
+ GenServer.call(__MODULE__, {:lookup, id})
+ end
+
+ def new_path(account, perks \\ nil) do
+ case new(account, perks) do
+ {:ok, id} ->
+ NolaWeb.Router.Helpers.login_path(NolaWeb.Endpoint, :token, id)
+ error ->
+ error
+ end
+ end
+
+ def new_url(account, perks \\ nil) do
+ case new(account, perks) do
+ {:ok, id} ->
+ NolaWeb.Router.Helpers.login_url(NolaWeb.Endpoint, :token, id)
+ error ->
+ error
+ end
+ end
+
+ def new(account, perks \\ nil) do
+ GenServer.call(__MODULE__, {:new, account, perks})
+ end
+
+ def init(_) do
+ {:ok, Map.new}
+ end
+
+ def handle_call({:lookup, id}, _, state) do
+ IO.inspect(state)
+ with \
+ {account, date, perks} <- Map.get(state, id),
+ d when d > 0 <- DateTime.diff(date, DateTime.utc_now())
+ do
+ {:reply, {:ok, account, perks}, Map.delete(state, id)}
+ else
+ x ->
+ IO.inspect(x)
+ {:reply, {:error, :invalid_token}, state}
+ end
+ end
+
+ def handle_call({:new, account, perks}, _, state) do
+ id = IRC.UserTrack.Id.token()
+ expire = DateTime.utc_now()
+ |> DateTime.add(15*60, :second)
+ {:reply, {:ok, id}, Map.put(state, id, {account, expire, perks})}
+ end
+
+end
diff --git a/lib/nola/icecast.ex b/lib/nola/icecast.ex
new file mode 100644
index 0000000..5a53192
--- /dev/null
+++ b/lib/nola/icecast.ex
@@ -0,0 +1,117 @@
+defmodule Nola.Icecast do
+ use GenServer
+ require Logger
+ @hackney_pool :default
+ @httpoison_opts [hackney: [pool: @hackney_pool]]
+ @fuse __MODULE__
+
+ def start_link, do: GenServer.start_link(__MODULE__, [], [])
+
+ def init(_) do
+ GenServer.cast(self(), :poll)
+ {:ok, nil}
+ end
+
+ def handle_cast(:poll, state) do
+ state = poll(state)
+ {:noreply, state}
+ end
+
+ def handle_info(:poll, state) do
+ state = poll(state)
+ {:noreply, state}
+ end
+
+ defp poll(state) do
+ state = case request(base_url(), :get) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ #update_json_stats(Jason.decode(body))
+ stats = update_stats(body)
+ if state != stats do
+ Logger.info "Icecast Update: " <> inspect(stats)
+ Nola.IcecastAgent.update(stats)
+ Registry.dispatch(Nola.BroadcastRegistry, "icecast", fn ws ->
+ for {pid, _} <- ws, do: send(pid, {:icecast, stats})
+ end)
+ stats
+ else
+ state
+ end
+ error ->
+ Logger.error "Icecast HTTP Error: #{inspect error}"
+ state
+ end
+ interval = Application.get_env(:nola, :icecast_poll_interval, 60_000)
+ :timer.send_after(interval, :poll)
+ state
+ end
+
+ defp update_stats(html) do
+ raw = Floki.find(html, "div.roundbox")
+ |> Enum.map(fn(html) ->
+ html = Floki.raw_html(html)
+ [{"h3", _, ["Mount Point /"<>mount]}] = Floki.find(html, "h3.mount")
+ stats = Floki.find(html, "tr")
+ |> Enum.map(fn({"tr", _, tds}) ->
+ [{"td", _, keys}, {"td", _, values}] = tds
+ key = List.first(keys)
+ value = List.first(values)
+ {key, value}
+ end)
+ |> Enum.into(Map.new)
+ {mount, stats}
+ end)
+ |> Enum.into(Map.new)
+
+ live? = if Map.get(raw["live"], "Content Type:", false), do: true, else: false
+ np = if live? do
+ raw["live"]["Currently playing:"]
+ else
+ raw["autodj"]["Currently playing:"]
+ end
+
+ genre = raw["live"]["Genre:"] || nil
+ %{np: np || "", live: live? || false, genre: genre}
+ end
+
+ defp update_json_stats({:ok, body}) do
+ Logger.debug "JSON STATS: #{inspect body}"
+ end
+
+ defp update_json_stats(error) do
+ Logger.error "Failed to decode JSON Stats: #{inspect error}"
+ end
+
+ defp request(uri, method, body \\ [], headers \\ []) do
+ headers = [{"user-agent", "Nola-API[115ans.net, sys.115ans.net] href@random.sh"}] ++ headers
+ options = @httpoison_opts
+ case :ok do #:fuse.ask(@fuse, :sync) do
+ :ok -> run_request(method, uri, body, headers, options)
+ :blown -> :blown
+ end
+ end
+
+ # This is to work around hackney's behaviour of returning `{:error, :closed}` when a pool connection has been closed
+ # (keep-alive expired). We just retry the request immediatly up to five times.
+ defp run_request(method, uri, body, headers, options), do: run_request(method, uri, body, headers, options, 0)
+ defp run_request(method, uri, body, headers, options, retries) when retries < 4 do
+ case HTTPoison.request(method, uri, body, headers, options) do
+ {:error, :closed} -> run_request(method, uri, body, headers, options, retries + 1)
+ other -> other
+ end
+ end
+ defp run_request(method, uri, body, headers, options, _exceeded_retries), do: {:error, :unavailable}
+
+ #
+ # -- URIs
+ #
+
+ defp stats_json_url do
+ base_url() <> "/status-json.xsl"
+ end
+
+ defp base_url do
+ "http://91.121.59.45:8089"
+ end
+
+end
diff --git a/lib/nola/icecast_agent.ex b/lib/nola/icecast_agent.ex
new file mode 100644
index 0000000..8a3a72b
--- /dev/null
+++ b/lib/nola/icecast_agent.ex
@@ -0,0 +1,17 @@
+defmodule Nola.IcecastAgent do
+ use Agent
+
+ def start_link() do
+ Agent.start_link(fn -> nil end, name: __MODULE__)
+ end
+
+ def update(stats) do
+ Agent.update(__MODULE__, fn(_old) -> stats end)
+ end
+
+ def get do
+ Agent.get(__MODULE__, fn(stats) -> stats end)
+ end
+
+end
+
diff --git a/lib/nola/nola.ex b/lib/nola/nola.ex
new file mode 100644
index 0000000..0acb76e
--- /dev/null
+++ b/lib/nola/nola.ex
@@ -0,0 +1,30 @@
+defmodule Nola do
+
+ @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(:nola)
+ def env(key, default \\ nil), do: Application.get_env(:nola, 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(:nola, :data_path)
+ end
+
+ def version do
+ Application.spec(:nola)[:vsn]
+ end
+
+end
diff --git a/lib/nola/subnet.ex b/lib/nola/subnet.ex
new file mode 100644
index 0000000..ac9d8e6
--- /dev/null
+++ b/lib/nola/subnet.ex
@@ -0,0 +1,84 @@
+defmodule Nola.Subnet do
+ use Agent
+
+ def start_link(_) do
+ Agent.start_link(&setup/0, name: __MODULE__)
+ end
+
+ def assignations() do
+ :dets.select(dets(), [{{:"$1", :"$2"}, [is_binary: :"$2"], [{{:"$1", :"$2"}}]}])
+ end
+
+ def find_subnet_for(binary) when is_binary(binary) do
+ case :dets.select(dets(), [{{:"$1", :"$2"}, [{:==, :"$2", binary}], [{{:"$1", :"$2"}}]}]) do
+ [{subnet, _}] -> subnet
+ _ -> nil
+ end
+ end
+
+ def assign(binary) when is_binary(binary) do
+ result = if subnet = find_subnet_for(binary) do
+ {:ok, subnet}
+ else
+ Agent.get_and_update(__MODULE__, fn(dets) ->
+ {subnet, _} = available_select(dets)
+ :dets.insert(dets, {subnet, binary})
+ :dets.sync(dets)
+ {{:new, subnet}, dets}
+ end)
+ end
+
+ case result do
+ {:new, subnet} ->
+ ip = Pfx.host(subnet, 1)
+ set_reverse(binary, ip)
+ subnet
+ {:ok, subnet} ->
+ subnet
+ end
+ end
+
+ def set_reverse(name, ip, value \\ nil)
+
+ def set_reverse(name, ip, nil) do
+ set_reverse(name, ip, "#{name}.users.goulag.org")
+ end
+
+ def set_reverse(_, ip, value) do
+ ptr_zone = "3.0.0.2.d.f.0.a.2.ip6.arpa"
+ ip_fqdn = Pfx.dns_ptr(ip)
+ ip_local = String.replace(ip_fqdn, ".#{ptr_zone}", "")
+ rev? = String.ends_with?(value, ".users.goulag.org")
+ if rev? do
+ {:ok, rev_zone} = PowerDNSex.show_zone("users.goulag.org")
+ rev_update? = Enum.any?(rev_zone.rrsets, fn(rr) -> rr.name == "#{ip_fqdn}." end)
+ record = %{name: "#{value}.", type: "AAAA", ttl: 8600, records: [%{content: ip, disabled: false}]}
+ if(rev_update?, do: PowerDNSex.update_record(rev_zone, record), else: PowerDNSex.create_record(rev_zone, record))
+ end
+ {:ok, zone} = PowerDNSex.show_zone(ptr_zone)
+ update? = Enum.any?(zone.rrsets, fn(rr) -> rr.name == "#{ip_fqdn}." end)
+ record = %{name: "#{ip_fqdn}.", type: "PTR", ttl: 3600, records: [%{content: "#{value}.", disabled: false}]}
+ pdns = if(update?, do: PowerDNSex.update_record(zone, record), else: PowerDNSex.create_record(zone, record))
+ :ok
+ end
+
+ @doc false
+ def dets() do
+ (Nola.data_path() <> "/subnets.dets") |> String.to_charlist()
+ end
+
+ @doc false
+ def setup() do
+ {:ok, dets} = :dets.open_file(dets(), [])
+ dets
+ end
+
+ defp available_select(dets) do
+ spec = [{{:"$1", :"$2"}, [is_integer: :"$2"], [{{:"$1", :"$2"}}]}]
+ {subnets, _} = :dets.select(dets, spec, 20)
+ subnet = subnets
+ |> Enum.sort_by(fn({_, last}) -> last end)
+ |> List.first()
+ end
+
+end
diff --git a/lib/nola/token.ex b/lib/nola/token.ex
new file mode 100644
index 0000000..563ac72
--- /dev/null
+++ b/lib/nola/token.ex
@@ -0,0 +1,38 @@
+defmodule Nola.Token do
+ use GenServer
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, [], [name: __MODULE__])
+ end
+
+ def lookup(id) do
+ with \
+ [{_, cred, date}] <- :ets.lookup(__MODULE__.ETS, id),
+ IO.inspect("cred: #{inspect cred} valid for #{inspect date} now #{inspect DateTime.utc_now()}"),
+ d when d > 0 <- DateTime.diff(date, DateTime.utc_now())
+ do
+ {:ok, cred}
+ else
+ err -> {:error, err}
+ end
+ end
+
+ def new(cred) do
+ GenServer.call(__MODULE__, {:new, cred})
+ end
+
+ def init(_) do
+ ets = :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}])
+ {:ok, ets}
+ end
+
+ def handle_call({:new, cred}, _, ets) do
+ id = IRC.UserTrack.Id.large_id()
+ expire = DateTime.utc_now()
+ |> DateTime.add(15*60, :second)
+ obj = {id, cred, expire}
+ :ets.insert(ets, obj)
+ {:reply, {:ok, id}, ets}
+ end
+
+end