Page MenuHomePhabricator

No OneTemporary

diff --git a/lib/irc.ex b/lib/irc.ex
index 8d04d50..1592955 100644
--- a/lib/irc.ex
+++ b/lib/irc.ex
@@ -1,49 +1,49 @@
defmodule Nola.Irc do
require Logger
- def env(), do: Nola.env(:irc)
+ def env(), do: Nola.env(:irc, [])
def env(key, default \\ nil), do: Keyword.get(env(), key, default)
def send_message_as(account, network, channel, text, force_puppet \\ false) do
connection = Nola.Irc.Connection.get_network(network)
if connection && (force_puppet || Nola.Irc.PuppetConnection.whereis(account, connection)) do
Nola.Irc.PuppetConnection.start_and_send_message(account, connection, channel, text)
else
user = Nola.UserTrack.find_by_account(network, account)
nick = if(user, do: user.nick, else: account.name)
Nola.Irc.Connection.broadcast_message(network, channel, "<#{nick}> #{text}")
end
end
def admin?(%Nola.Message{sender: sender}), do: admin?(sender)
def admin?(%{nick: nick, user: user, host: host}) do
for {n, u, h} <- Nola.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
def application_childs do
import Supervisor.Spec
Nola.Irc.Connection.setup()
[
worker(Registry, [[keys: :duplicate, name: Nola.Irc.ConnectionPubSub]], id: :registr_irc_conn),
supervisor(Nola.Irc.Connection.Supervisor, [], [name: Nola.Irc.Connection.Supervisor]),
supervisor(Nola.Irc.PuppetConnection.Supervisor, [], [name: Nola.Irc.PuppetConnection.Supervisor]),
]
end
# Start plugins first to let them get on connection events.
def after_start() do
Logger.info("Starting connections")
Nola.Irc.Connection.start_all()
end
end
diff --git a/lib/nola/account.ex b/lib/nola/account.ex
index 47e46b8..70e9e40 100644
--- a/lib/nola/account.ex
+++ b/lib/nola/account.ex
@@ -1,263 +1,263 @@
defmodule Nola.Account do
alias Nola.UserTrack.User
@moduledoc """
Account registry....
Maps a network predicate:
* `{net, {:nick, nickname}}`
* `{net, {:account, account}}`
* `{net, {:mask, user@host}}`
to an unique identifier, that can be shared over multiple networks.
If a predicate cannot be found for an existing account, a new account will be made in the database.
To link two existing accounts from different network onto a different one, a merge operation is provided.
"""
# FIXME: Ensure uniqueness of name?
@derive {Poison.Encoder, except: [:token]}
defstruct [:id, :name, :token]
@type t :: %__MODULE__{id: id(), name: String.t()}
@type id :: String.t()
defimpl Inspect, for: __MODULE__ do
import Inspect.Algebra
def inspect(%{id: id, name: name}, opts) do
concat(["#Nola.Account[", id, " ", name, "]"])
end
end
def file(base) do
to_charlist(Nola.data_path() <> "/account_#{base}.dets")
end
defp from_struct(%__MODULE__{id: id, name: name, token: token}) do
{id, name, token}
end
defp from_tuple({id, name, token}) do
%__MODULE__{id: id, name: name, token: token}
end
def start_link() do
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
end
def init(_) do
{:ok, accounts} = :dets.open_file(file("db"), [])
{:ok, meta} = :dets.open_file(file("meta"), [])
{:ok, predicates} = :dets.open_file(file("predicates"), [{:type, :set}])
{:ok, %{accounts: accounts, meta: meta, predicates: predicates}}
end
def get(id) do
case :dets.lookup(file("db"), id) do
[account] -> from_tuple(account)
_ -> nil
end
end
def get_by_name(name) do
spec = [{{:_, :"$1", :_}, [{:==, :"$1", {:const, name}}], [:"$_"]}]
case :dets.select(file("db"), spec) do
[account] -> from_tuple(account)
_ -> nil
end
end
def get_meta(%__MODULE__{id: id}, key, default \\ nil) do
case :dets.lookup(file("meta"), {id, key}) do
[{_, value}] -> (value || default)
_ -> default
end
end
@spec find_meta_accounts(String.t()) :: [{account :: t(), value :: String.t()}, ...]
@doc "Find all accounts that have a meta of `key`."
def find_meta_accounts(key) do
spec = [{{{:"$1", :"$2"}, :"$3"}, [{:==, :"$2", {:const, key}}], [{{:"$1", :"$3"}}]}]
for {id, val} <- :dets.select(file("meta"), spec), do: {get(id), val}
end
@doc "Find an account given a specific meta `key` and `value`."
@spec find_meta_account(String.t(), String.t()) :: t() | nil
def find_meta_account(key, value) do
#spec = [{{{:"$1", :"$2"}, :"$3"}, [:andalso, {:==, :"$2", {:const, key}}, {:==, :"$3", {:const, value}}], [:"$1"]}]
spec = [{{{:"$1", :"$2"}, :"$3"}, [{:andalso, {:==, :"$2", {:const, key}}, {:==, {:const, value}, :"$3"}}], [:"$1"]}]
case :dets.select(file("meta"), spec) do
[id] -> get(id)
_ -> nil
end
end
def get_all_meta(%__MODULE__{id: id}) do
spec = [{{{:"$1", :"$2"}, :"$3"}, [{:==, :"$1", {:const, id}}], [{{:"$2", :"$3"}}]}]
:dets.select(file("meta"), spec)
end
def put_user_meta(account = %__MODULE__{}, key, value) do
put_meta(account, "u:"<>key, value)
end
def put_meta(%__MODULE__{id: id}, key, value) do
:dets.insert(file("meta"), {{id, key}, value})
end
def delete_meta(%__MODULE__{id: id}, key) do
:dets.delete(file("meta"), {id, key})
end
def all_accounts() do
:dets.traverse(file("db"), fn(obj) -> {:continue, from_tuple(obj)} end)
end
def all_predicates() do
:dets.traverse(file("predicates"), fn(obj) -> {:continue, obj} end)
end
def all_meta() do
:dets.traverse(file("meta"), fn(obj) -> {:continue, obj} end)
end
def merge_account(old_id, new_id) do
if old_id != new_id do
spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, old_id}}], [:"$1"]}]
predicates = :dets.select(file("predicates"), spec)
for pred <- predicates, do: :ok = :dets.insert(file("predicates"), {pred, new_id})
spec = [{{{:"$1", :"$2"}, :"$3"}, [{:==, :"$1", {:const, old_id}}], [{{:"$2", :"$3"}}]}]
metas = :dets.select(file("meta"), spec)
for {k,v} <- metas do
:dets.delete(file("meta"), {{old_id, k}})
:ok = :dets.insert(file("meta"), {{new_id, k}, v})
end
:dets.delete(file("db"), old_id)
Nola.Membership.merge_account(old_id, new_id)
Nola.UserTrack.merge_account(old_id, new_id)
Nola.Irc.Connection.dispatch("account", {:account_change, old_id, new_id})
Nola.Irc.Connection.dispatch("conn", {:account_change, old_id, new_id})
end
:ok
end
@doc "Find an account by a logged in user"
def find_by_nick(network, nick) do
do_lookup(%ExIRC.SenderInfo{nick: nick, network: network}, false)
end
@doc "Always find an account by nickname, even if offline. Uses predicates and then account name."
def find_always_by_nick(network, chan, nick) do
with \
nil <- find_by_nick(network, nick),
nil <- do_lookup(%User{network: network, nick: nick}, false),
nil <- get_by_name(nick)
do
nil
else
%__MODULE__{} = account ->
memberships = Nola.Membership.of_account(account)
if Enum.any?(memberships, fn({net, ch}) -> (net == network) or (chan && chan == ch) end) do
account
else
nil
end
end
end
def find(something) do
do_lookup(something, false)
end
def lookup(something, make_default \\ true) do
account = do_lookup(something, make_default)
if account && Map.get(something, :nick) do
Nola.Irc.Connection.dispatch("account", {:account_auth, Map.get(something, :nick), account.id})
end
account
end
def handle_info(_, state) do
{:noreply, state}
end
def handle_cast(_, state) do
{:noreply, state}
end
def handle_call(_, _, state) do
{:noreply, state}
end
def terminate(_, state) do
for {_, dets} <- state do
:dets.sync(dets)
:dets.close(dets)
end
end
defp do_lookup(message = %Nola.Message{account: account_id}, make_default) when is_binary(account_id) do
get(account_id)
end
defp do_lookup(sender = %ExIRC.Who{}, make_default) do
if user = Nola.UserTrack.find_by_nick(sender) do
lookup(user, make_default)
else
#FIXME this will never work with continued lookup by other methods as Who isn't compatible
lookup_by_nick(sender, :dets.lookup(file("predicates"), {sender.network,{:nick, sender.nick}}), make_default)
end
end
defp do_lookup(sender = %ExIRC.SenderInfo{}, make_default) do
lookup(Nola.UserTrack.find_by_nick(sender), make_default)
end
defp do_lookup(user = %User{account: id}, make_default) when is_binary(id) do
get(id)
end
defp do_lookup(user = %User{network: server, nick: nick}, make_default) do
lookup_by_nick(user, :dets.lookup(file("predicates"), {server,{:nick, nick}}), make_default)
end
defp do_lookup(nil, _) do
nil
end
defp lookup_by_nick(_, [{_, id}], _make_default) do
get(id)
end
defp lookup_by_nick(user, _, make_default) do
#authenticate_by_host(user)
if make_default, do: new_account(user), else: nil
end
- def new_account(nick) do
+ def new_account(%{nick: nick, network: server}) do
id = EntropyString.large_id()
:dets.insert(file("db"), {id, nick, EntropyString.token()})
+ :dets.insert(file("predicates"), {{server, {:nick, nick}}, id})
get(id)
end
- def new_account(%{nick: nick, network: server}) do
+ def new_account(nick) when is_binary(nick) do
id = EntropyString.large_id()
:dets.insert(file("db"), {id, nick, EntropyString.token()})
- :dets.insert(file("predicates"), {{server, {:nick, nick}}, id})
get(id)
end
def update_account_name(account = %__MODULE__{id: id}, name) do
account = %__MODULE__{account | name: name}
:dets.insert(file("db"), from_struct(account))
get(id)
end
def get_predicates(%__MODULE__{} = account) do
spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}]
:dets.select(file("predicates"), spec)
end
end
diff --git a/lib/nola/plugins.ex b/lib/nola/plugins.ex
index b0c3ce3..7872cd6 100644
--- a/lib/nola/plugins.ex
+++ b/lib/nola/plugins.ex
@@ -1,100 +1,136 @@
defmodule Nola.Plugins do
require Logger
+ @builtins [
+ Nola.Plugins.Account,
+ Nola.Plugins.Alcoolog,
+ Nola.Plugins.AlcoologAnnouncer,
+ Nola.Plugins.Base,
+ Nola.Plugins.Boursorama,
+ Nola.Plugins.Buffer,
+ Nola.Plugins.Calc,
+ Nola.Plugins.Coronavirus,
+ Nola.Plugins.Correction,
+ Nola.Plugins.Dice,
+ Nola.Plugins.Finance,
+ Nola.Plugins.Gpt,
+ Nola.Plugins.KickRoulette,
+ Nola.Plugins.LastFm,
+ Nola.Plugins.Link,
+ Nola.PLugins.Logger,
+ Nola.Plugins.Preums,
+ Nola.Plugins.QuatreCentVingt,
+ Nola.Plugins.RadioFrance,
+ Nola.Plugins.Say,
+ Nola.Plugins.Script,
+ Nola.Plugins.Seen,
+ Nola.Plugins.Sms,
+ Nola.Plugins.Tell,
+ Nola.Plugins.Txt,
+ Nola.Plugins.Untappd,
+ Nola.Plugins.UserMention,
+ Nola.Plugins.WolframAlpha,
+ Nola.Plugins.YouTube,
+ ]
+
defmodule Supervisor do
use DynamicSupervisor
require Logger
def start_link() do
DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
end
def start_child(module, opts \\ []) do
Logger.info("Starting #{module}")
- spec = %{id: {__MODULE__,module}, start: {__MODULE__, :start_link, [module, opts]}, name: module, restart: :transient}
+ spec = %{id: {Nola.Plugins,module}, start: {Nola.Plugins, :start_link, [module, opts]}, name: module, restart: :transient}
case DynamicSupervisor.start_child(__MODULE__, spec) do
{:ok, _} = res -> res
:ignore ->
Logger.warn("Ignored #{module}")
:ignore
{:error,_} = res ->
Logger.error("Could not start #{module}: #{inspect(res, pretty: true)}")
res
end
end
@impl true
def init(_init_arg) do
DynamicSupervisor.init(
strategy: :one_for_one,
max_restarts: 10,
max_seconds: 1
)
end
end
def dets(), do: to_charlist(Nola.data_path("/plugins.dets"))
def setup() do
:dets.open_file(dets(), [])
end
def enabled() do
:dets.foldl(fn
{name, true, _}, acc -> [name | acc]
_, acc -> acc
end, [], dets())
end
def start_all() do
Logger.info("starting plugins.")
for mod <- enabled(), do: {mod, __MODULE__.Supervisor.start_child(mod)}
end
def declare(module) do
case get(module) do
:disabled -> :dets.insert(dets(), {module, true, nil})
_ -> nil
end
end
+ def declare_all_builtins do
+ for b <- @builtins, do: declare(b)
+ end
+
def start(module, opts \\ []) do
__MODULE__.Supervisor.start_child(module)
end
@doc "Enables a plugin"
def enable(name), do: switch(name, true)
@doc "Disables a plugin"
def disable(name), do: switch(name, false)
@doc "Enables or disables a plugin"
def switch(name, value) when is_boolean(value) do
last = case get(name) do
{:ok, last} -> last
_ -> nil
end
:dets.insert(dets(), {name, value, last})
end
@spec get(module()) :: {:ok, last_start :: nil | non_neg_integer()} | :disabled
def get(name) do
case :dets.lookup(dets(), name) do
[{name, enabled, last_start}] -> {:ok, enabled, last_start}
_ -> :disabled
end
end
def start_link(module, options \\ []) do
with {:disabled, {_, true, last}} <- {:disabled, get(module)},
{:throttled, false} <- {:throttled, false}
do
module.start_link()
else
{error, _} ->
Logger.info("#{__MODULE__}: #{to_string(module)} ignored start: #{to_string(error)}")
:ignore
end
end
end
diff --git a/lib/plugins/base.ex b/lib/plugins/base.ex
index 0f2c7e5..1baf066 100644
--- a/lib/plugins/base.ex
+++ b/lib/plugins/base.ex
@@ -1,132 +1,136 @@
defmodule Nola.Plugins.Base do
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(Nola.PubSub, "trigger:version", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "trigger:help", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "trigger:liquidrender", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "trigger:plugin", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "trigger:plugins", regopts)
{:ok, nil}
end
def handle_info({:irc, :trigger, "plugins", msg = %{trigger: %{type: :bang, args: []}}}, _) do
enabled_string = Nola.Plugins.enabled()
- |> Enum.map(fn(mod) ->
- mod
- |> Macro.underscore()
- |> String.split("/", parts: :infinity)
- |> List.last()
- |> Enum.sort()
+ |> Enum.map(fn(string_or_module) ->
+ case string_or_module do
+ string when is_binary(string) -> string
+ module when is_atom(module) ->
+ module
+ |> Macro.underscore()
+ |> String.split("/", parts: :infinity)
+ |> List.last()
+ end
end)
+ |> Enum.sort()
|> Enum.join(", ")
msg.replyfun.("Enabled plugins: #{enabled_string}")
{:noreply, nil}
end
def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :query, args: [plugin]}} = m}, _) do
module = Module.concat([Nola.Plugins, Macro.camelize(plugin)])
with true <- Code.ensure_loaded?(module),
pid when is_pid(pid) <- GenServer.whereis(module)
do
m.replyfun.("loaded, active: #{inspect(pid)}")
else
false -> m.replyfun.("not loaded")
nil ->
msg = case Nola.Plugins.get(module) do
:disabled -> "disabled"
{_, false, _} -> "disabled"
_ -> "not active"
end
m.replyfun.(msg)
end
{:noreply, nil}
end
def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :plus, args: [plugin]}} = m}, _) do
module = Module.concat([Nola.Plugins, Macro.camelize(plugin)])
with true <- Code.ensure_loaded?(module),
Nola.Plugins.switch(module, true),
{:ok, pid} <- Nola.Plugins.start(module)
do
m.replyfun.("started: #{inspect(pid)}")
else
false -> m.replyfun.("not loaded")
:ignore -> m.replyfun.("disabled or throttled")
{:error, _} -> m.replyfun.("start error")
end
{:noreply, nil}
end
def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :tilde, args: [plugin]}} = m}, _) do
module = Module.concat([Nola.Plugins, Macro.camelize(plugin)])
with true <- Code.ensure_loaded?(module),
pid when is_pid(pid) <- GenServer.whereis(module),
:ok <- GenServer.stop(pid),
{:ok, pid} <- Nola.Plugins.start(module)
do
m.replyfun.("restarted: #{inspect(pid)}")
else
false -> m.replyfun.("not loaded")
nil -> m.replyfun.("not active")
end
{:noreply, nil}
end
def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :minus, args: [plugin]}} = m}, _) do
module = Module.concat([Nola.Plugins, Macro.camelize(plugin)])
with true <- Code.ensure_loaded?(module),
pid when is_pid(pid) <- GenServer.whereis(module),
:ok <- GenServer.stop(pid)
do
Nola.Plugins.switch(module, false)
m.replyfun.("stopped: #{inspect(pid)}")
else
false -> m.replyfun.("not loaded")
nil -> m.replyfun.("not active")
end
{:noreply, nil}
end
def handle_info({:irc, :trigger, "liquidrender", m = %{trigger: %{args: args}}}, _) do
template = Enum.join(args, " ")
m.replyfun.(Tmpl.render(template, m))
{:noreply, nil}
end
def handle_info({:irc, :trigger, "help", m = %{trigger: %{type: :bang}}}, _) do
url = NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :index, m.network, NolaWeb.format_chan(m.channel))
m.replyfun.("-> #{url}")
{:noreply, nil}
end
def handle_info({:irc, :trigger, "version", message = %{trigger: %{type: :bang}}}, _) do
{:ok, vsn} = :application.get_key(:nola, :vsn)
ver = List.to_string(vsn)
url = NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :index)
elixir_ver = Application.started_applications() |> List.keyfind(:elixir, 0) |> elem(2) |> to_string()
otp_ver = :erlang.system_info(:system_version) |> to_string() |> String.trim()
system = :erlang.system_info(:system_architecture) |> to_string()
brand = Nola.brand(:name)
owner = "#{Nola.brand(:owner)} <#{Nola.brand(:owner_email)}>"
message.replyfun.([
<<"🤖 I am a robot running", 2, "#{brand}, version #{ver}", 2, " — source: #{Nola.source_url()}">>,
"🦾 Elixir #{elixir_ver} #{otp_ver} on #{system}",
"👷‍♀️ Owner: h#{owner}",
"🌍 Web interface: #{url}"
])
{:noreply, nil}
end
def handle_info(msg, _) do
{:noreply, nil}
end
end

File Metadata

Mime Type
text/x-diff
Expires
Mon, Apr 28, 1:24 PM (1 d, 1 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
38902
Default Alt Text
(18 KB)

Event Timeline