summaryrefslogtreecommitdiff
path: root/lib/irc
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/irc
parentNola rename, the end. pt 6. Refs T77. (diff)
Nola rename: The Big Move, Refs T77
Diffstat (limited to 'lib/irc')
-rw-r--r--lib/irc/account.ex451
-rw-r--r--lib/irc/connection.ex521
-rw-r--r--lib/irc/conns.ex3
-rw-r--r--lib/irc/irc.ex79
-rw-r--r--lib/irc/membership.ex129
-rw-r--r--lib/irc/plugin/temp_ref.ex95
-rw-r--r--lib/irc/plugin_supervisor.ex99
-rw-r--r--lib/irc/puppet_connection.ex238
-rw-r--r--lib/irc/user_track.ex329
9 files changed, 0 insertions, 1944 deletions
diff --git a/lib/irc/account.ex b/lib/irc/account.ex
deleted file mode 100644
index 45680f8..0000000
--- a/lib/irc/account.ex
+++ /dev/null
@@ -1,451 +0,0 @@
-defmodule IRC.Account do
- alias IRC.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(["#IRC.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)
- IRC.Membership.merge_account(old_id, new_id)
- IRC.UserTrack.merge_account(old_id, new_id)
- IRC.Connection.dispatch("account", {:account_change, old_id, new_id})
- 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 = IRC.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
- 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 = %IRC.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 = IRC.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(IRC.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
- id = EntropyString.large_id()
- :dets.insert(file("db"), {id, nick, EntropyString.token()})
- get(id)
- end
-
- 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 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
-
- defmodule AccountPlugin do
- @moduledoc """
- # Account
-
- * **account** Get current account id and token
- * **auth `<account-id>` `<token>`** Authenticate and link the current nickname to an account
- * **auth** list authentications methods
- * **whoami** list currently authenticated users
- * **web** get a one-time login link to web
- * **enable-telegram** Link a Telegram account
- * **enable-sms** Link a SMS number
- * **enable-untappd** Link a Untappd account
- * **set-name** set account name
- * **setusermeta puppet-nick `<nick>`** Set puppet IRC nickname
- """
-
- def irc_doc, do: @moduledoc
- def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
- def init(_) do
- {:ok, _} = Registry.register(IRC.PubSub, "messages:private", [])
- {:ok, nil}
- end
-
- def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "help"}}, state) do
- text = [
- "account: show current account and auth token",
- "auth: show authentications methods",
- "whoami: list authenticated users",
- "set-name <name>: set account name",
- "web: login to web",
- "enable-sms | disable-sms: enable/change or disable sms",
- "enable-telegram: link/change telegram",
- "enable-untappd: link untappd account",
- "getmeta: show meta datas",
- "setusermeta: set user meta",
- ]
- m.replyfun.(text)
- {:noreply, state}
- end
-
- def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "auth"}}, state) do
- spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}]
- predicates = :dets.select(IRC.Account.file("predicates"), spec)
- text = for {net, {key, value}} <- predicates, do: "#{net}: #{to_string(key)}: #{value}"
- m.replyfun.(text)
- {:noreply, state}
- end
-
- def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "whoami"}}, state) do
- users = for user <- IRC.UserTrack.find_by_account(m.account) do
- chans = Enum.map(user.privileges, fn({chan, _}) -> chan end)
- |> Enum.join(" ")
- "#{user.network} - #{user.nick}!#{user.username}@#{user.host} - #{chans}"
- end
- m.replyfun.(users)
- {:noreply, state}
- end
-
- def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "account"}}, state) do
- account = IRC.Account.lookup(m.sender)
- text = ["Account Id: #{account.id}",
- "Authenticate to this account from another network: \"auth #{account.id} #{account.token}\" to the other bot!"]
- m.replyfun.(text)
- {:noreply, state}
- end
-
- def handle_info({:irc, :text, m = %IRC.Message{sender: sender, text: "auth"<>_}}, state) do
- #account = IRC.Account.lookup(m.sender)
- case String.split(m.text, " ") do
- ["auth", id, token] ->
- join_account(m, id, token)
- _ ->
- m.replyfun.("Invalid parameters")
- end
- {:noreply, state}
- end
-
- def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "set-name "<>name}}, state) do
- IRC.Account.update_account_name(account, name)
- m.replyfun.("Name changed: #{name}")
- {:noreply, state}
- end
-
- def handle_info({:irc, :text, m = %IRC.Message{text: "disable-sms"}}, state) do
- if IRC.Account.get_meta(m.account, "sms-number") do
- IRC.Account.delete_meta(m.account, "sms-number")
- m.replfyun.("SMS disabled.")
- else
- m.replyfun.("SMS already disabled.")
- end
- {:noreply, state}
- end
-
- def handle_info({:irc, :text, m = %IRC.Message{text: "web"}}, state) do
- auth_url = Untappd.auth_url()
- login_url = Nola.AuthToken.new_url(m.account.id, nil)
- m.replyfun.("-> " <> login_url)
- {:noreply, state}
- end
-
- def handle_info({:irc, :text, m = %IRC.Message{text: "enable-sms"}}, state) do
- code = String.downcase(EntropyString.small_id())
- IRC.Account.put_meta(m.account, "sms-validation-code", code)
- IRC.Account.put_meta(m.account, "sms-validation-target", m.network)
- number = Nola.IRC.SmsPlugin.my_number()
- text = "To enable or change your number for SMS messaging, please send:"
- <> " \"enable #{code}\" to #{number}"
- m.replyfun.(text)
- {:noreply, state}
- end
-
- def handle_info({:irc, :text, m = %IRC.Message{text: "enable-telegram"}}, state) do
- code = String.downcase(EntropyString.small_id())
- IRC.Account.delete_meta(m.account, "telegram-id")
- IRC.Account.put_meta(m.account, "telegram-validation-code", code)
- IRC.Account.put_meta(m.account, "telegram-validation-target", m.network)
- text = "To enable or change your number for telegram messaging, please open #{Nola.Telegram.my_path()} and send:"
- <> " \"/enable #{code}\""
- m.replyfun.(text)
- {:noreply, state}
- end
-
- def handle_info({:irc, :text, m = %IRC.Message{text: "enable-untappd"}}, state) do
- auth_url = Untappd.auth_url()
- login_url = Nola.AuthToken.new_url(m.account.id, {:external_redirect, auth_url})
- m.replyfun.(["To link your Untappd account, open this URL:", login_url])
- {:noreply, state}
- end
-
- def handle_info({:irc, :text, m = %IRC.Message{text: "getmeta"<>_}}, state) do
- result = case String.split(m.text, " ") do
- ["getmeta"] ->
- for {k, v} <- IRC.Account.get_all_meta(m.account) do
- case k do
- "u:"<>key -> "(user) #{key}: #{v}"
- key -> "#{key}: #{v}"
- end
- end
- ["getmeta", key] ->
- value = IRC.Account.get_meta(m.account, key)
- text = if value do
- "#{key}: #{value}"
- else
- "#{key} is not defined"
- end
- _ ->
- "usage: getmeta [key]"
- end
- m.replyfun.(result)
- {:noreply, state}
- end
-
- def handle_info({:irc, :text, m = %IRC.Message{text: "setusermet"<>_}}, state) do
- result = case String.split(m.text, " ") do
- ["setusermeta", key, value] ->
- IRC.Account.put_user_meta(m.account, key, value)
- "ok"
- _ ->
- "usage: setusermeta <key> <value>"
- end
- m.replyfun.(result)
- {:noreply, state}
- end
-
- def handle_info(_, state) do
- {:noreply, state}
- end
-
- defp join_account(m, id, token) do
- old_account = IRC.Account.lookup(m.sender)
- new_account = IRC.Account.get(id)
- if new_account && token == new_account.token do
- case IRC.Account.merge_account(old_account.id, new_account.id) do
- :ok ->
- if old_account.id == new_account.id do
- m.replyfun.("Already authenticated, but hello")
- else
- m.replyfun.("Accounts merged!")
- end
- _ -> m.replyfun.("Something failed :(")
- end
- else
- m.replyfun.("Invalid token")
- end
- end
-
- end
-
-end
diff --git a/lib/irc/connection.ex b/lib/irc/connection.ex
deleted file mode 100644
index 86d8279..0000000
--- a/lib/irc/connection.ex
+++ /dev/null
@@ -1,521 +0,0 @@
-defmodule IRC.Connection do
- require Logger
- use Ecto.Schema
-
- @moduledoc """
- # IRC Connection
-
- Provides a nicer abstraction over ExIRC's handlers.
-
- ## Start connections
-
- ```
- IRC.Connection.start_link(host: "irc.random.sh", port: 6697, nick: "pouetbot", channels: ["#dev"])
-
- ## PubSub topics
-
- * `account` -- accounts change
- * {:account_change, old_account_id, new_account_id} # Sent when account merged
- * {:accounts, [{:account, network, channel, nick, account_id}] # Sent on bot join
- * {:account, network, nick, account_id} # Sent on user join
- * `message` -- aill messages (without triggers)
- * `message:private` -- all messages without a channel
- * `message:#CHANNEL` -- all messages within `#CHANNEL`
- * `triggers` -- all triggers
- * `trigger:TRIGGER` -- any message with a trigger `TRIGGER`
-
- ## Replying to %IRC.Message{}
-
- Each `IRC.Message` comes with a dedicated `replyfun`, to which you only have to pass either:
-
- """
- def irc_doc, do: nil
-
- @min_backoff :timer.seconds(5)
- @max_backoff :timer.seconds(2*60)
-
- embedded_schema do
- field :network, :string
- field :host, :string
- field :port, :integer
- field :nick, :string
- field :user, :string
- field :name, :string
- field :pass, :string
- field :tls, :boolean, default: false
- field :channels, {:array, :string}, default: []
- end
-
- defmodule Supervisor do
- use DynamicSupervisor
-
- def start_link() do
- DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
- end
-
- def start_child(%IRC.Connection{} = conn) do
- spec = %{id: conn.id, start: {IRC.Connection, :start_link, [conn]}, restart: :transient}
- DynamicSupervisor.start_child(__MODULE__, spec)
- end
-
- @impl true
- def init(_init_arg) do
- DynamicSupervisor.init(
- strategy: :one_for_one,
- max_restarts: 10,
- max_seconds: 1
- )
- end
- end
-
-
- def changeset(params) do
- import Ecto.Changeset
-
- %__MODULE__{id: EntropyString.large_id()}
- |> cast(params, [:network, :host, :port, :nick, :user, :name, :pass, :channels, :tls])
- |> validate_required([:host, :port, :nick, :user, :name])
- |> apply_action(:insert)
- end
-
- def to_tuple(%__MODULE__{} = conn) do
- {conn.id, conn.network, conn.host, conn.port, conn.nick, conn.user, conn.name, conn.pass, conn.tls, conn.channels, nil}
- end
-
- def from_tuple({id, network, host, port, nick, user, name, pass, tls, channels, _}) do
- %__MODULE__{id: id, network: network, host: host, port: port, nick: nick, user: user, name: name, pass: pass, tls: tls, channels: channels}
- end
-
- ## -- MANAGER API
-
- def setup() do
- :dets.open_file(dets(), [])
- end
-
- def dets(), do: to_charlist(Nola.data_path("/connections.dets"))
-
- def lookup(id) do
- case :dets.lookup(dets(), id) do
- [object | _] -> from_tuple(object)
- _ -> nil
- end
- end
-
- def connections() do
- :dets.foldl(fn(object, acc) -> [from_tuple(object) | acc] end, [], dets())
- end
-
- def start_all() do
- for conn <- connections(), do: {conn, IRC.Connection.Supervisor.start_child(conn)}
- end
-
- def get_network(network, channel \\ nil) do
- spec = [{{:_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_},
- [{:==, :"$1", {:const, network}}], [:"$_"]}]
- results = Enum.map(:dets.select(dets(), spec), fn(object) -> from_tuple(object) end)
- if channel do
- Enum.find(results, fn(conn) -> Enum.member?(conn.channels, channel) end)
- else
- List.first(results)
- end
- end
-
- def get_host_nick(host, port, nick) do
- spec = [{{:_, :_, :"$1", :"$2", :"$3", :_, :_, :_, :_, :_, :_},
- [{:andalso,
- {:andalso, {:==, :"$1", {:const, host}}, {:==, :"$2", {:const, port}}},
- {:==, :"$3", {:const, nick}}}],
- [:"$_"]}
- ]
- case :dets.select(dets(), spec) do
- [object] -> from_tuple(object)
- [] -> nil
- end
- end
-
- def delete_connection(%__MODULE__{id: id} = conn) do
- :dets.delete(dets(), id)
- stop_connection(conn)
- :ok
- end
-
- def start_connection(%__MODULE__{} = conn) do
- IRC.Connection.Supervisor.start_child(conn)
- end
-
- def stop_connection(%__MODULE__{id: id}) do
- case :global.whereis_name(id) do
- pid when is_pid(pid) ->
- GenServer.stop(pid, :normal)
- _ -> :error
- end
- end
-
- def add_connection(opts) do
- case changeset(opts) do
- {:ok, conn} ->
- if existing = get_host_nick(conn.host, conn.port, conn.nick) do
- {:error, {:existing, conn}}
- else
- :dets.insert(dets(), to_tuple(conn))
- IRC.Connection.Supervisor.start_child(conn)
- end
- error -> error
- end
- end
-
- def update_connection(connection) do
- :dets.insert(dets(), to_tuple(connection))
- end
-
- def start_link(conn) do
- GenServer.start_link(__MODULE__, [conn], name: {:global, conn.id})
- end
-
- def broadcast_message(net, chan, message) do
- dispatch("conn", {:broadcast, net, chan, message}, IRC.ConnectionPubSub)
- end
- def broadcast_message(list, message) when is_list(list) do
- for {net, chan} <- list do
- broadcast_message(net, chan, message)
- end
- end
-
- def privmsg(channel, line) do
- GenServer.cast(__MODULE__, {:privmsg, channel, line})
- end
-
- def init([conn]) do
- Logger.metadata(conn: conn.id)
- backoff = :backoff.init(@min_backoff, @max_backoff)
- |> :backoff.type(:jitter)
- {:ok, %{client: nil, backoff: backoff, conn: conn, connected_server: nil, connected_port: nil, network: conn.network}, {:continue, :connect}}
- end
-
- @triggers %{
- "!" => :bang,
- "+" => :plus,
- "-" => :minus,
- "?" => :query,
- "." => :dot,
- "~" => :tilde,
- "@" => :at,
- "++" => :plus_plus,
- "--" => :minus_minus,
- "!!" => :bang_bang,
- "??" => :query_query,
- ".." => :dot_dot,
- "~~" => :tilde_tilde,
- "@@" => :at_at
- }
-
- def handle_continue(:connect, state) do
- client_opts = []
- |> Keyword.put(:network, state.conn.network)
- {:ok, _} = Registry.register(IRC.ConnectionPubSub, "conn", [])
- client = if state.client && Process.alive?(state.client) do
- Logger.info("Reconnecting client")
- state.client
- else
- Logger.info("Connecting")
- {:ok, client} = ExIRC.Client.start_link(debug: false)
- ExIRC.Client.add_handler(client, self())
- client
- end
-
- opts = [{:nodelay, true}]
- conn_fun = if state.conn.tls, do: :connect_ssl!, else: :connect!
- apply(ExIRC.Client, conn_fun, [client, to_charlist(state.conn.host), state.conn.port, opts])
-
- {:noreply, %{state | client: client}}
- end
-
- def handle_info(:disconnected, state) do
- {delay, backoff} = :backoff.fail(state.backoff)
- Logger.info("#{inspect(self())} Disconnected -- reconnecting in #{inspect delay}ms")
- Process.send_after(self(), :connect, delay)
- {:noreply, %{state | backoff: backoff}}
- end
-
- def handle_info(:connect, state) do
- {:noreply, state, {:continue, :connect}}
- end
-
- def handle_cast({:privmsg, channel, line}, state) do
- irc_reply(state, {channel, nil}, line)
- {:noreply, state}
- end
-
- # Connection successful
- def handle_info({:connected, server, port}, state) do
- Logger.info("#{inspect(self())} Connected to #{inspect(server)}:#{port} #{inspect state}")
- {_, backoff} = :backoff.succeed(state.backoff)
- ExIRC.Client.logon(state.client, state.conn.pass || "", state.conn.nick, state.conn.user, state.conn.name)
- {:noreply, %{state | backoff: backoff, connected_server: server, connected_port: port}}
- end
-
- # Logon successful
- def handle_info(:logged_in, state) do
- Logger.info("#{inspect(self())} Logged in")
- {_, backoff} = :backoff.succeed(state.backoff)
- Enum.map(state.conn.channels, &ExIRC.Client.join(state.client, &1))
- {:noreply, %{state | backoff: backoff}}
- end
-
- # ISUP
- def handle_info({:isup, network}, state) when is_binary(network) do
- IRC.UserTrack.clear_network(state.network)
- if network != state.network do
- Logger.warn("Possibly misconfigured network: #{network} != #{state.network}")
- end
- {:noreply, state}
- end
-
- # Been kicked
- def handle_info({:kicked, _sender, chan, _reason}, state) do
- ExIRC.Client.join(state.client, chan)
- {:noreply, state}
- end
-
- # Received something in a channel
- def handle_info({:received, text, sender, chan}, state) do
- user = if user = IRC.UserTrack.find_by_nick(state.network, sender.nick) do
- user
- else
- Logger.error("Could not lookup user for message: #{inspect {state.network, chan, sender.nick}}")
- user = IRC.UserTrack.joined(chan, sender, [])
- ExIRC.Client.who(state.client, chan) # Rewho everything in case of need ? We shouldn't not know that user..
- user
- end
- if !user do
- ExIRC.Client.who(state.client, chan) # Rewho everything in case of need ? We shouldn't not know that user..
- Logger.error("Could not lookup user nor create it for message: #{inspect {state.network, chan, sender.nick}}")
- else
- if !Map.get(user.options, :puppet) do
- reply_fun = fn(text) -> irc_reply(state, {chan, sender}, text) end
- account = IRC.Account.lookup(sender)
- message = %IRC.Message{id: FlakeId.get(), transport: :irc, at: NaiveDateTime.utc_now(), text: text, network: state.network,
- account: account, sender: sender, channel: chan, replyfun: reply_fun,
- trigger: extract_trigger(text)}
- message = case IRC.UserTrack.messaged(message) do
- :ok -> message
- {:ok, message} -> message
- end
- publish(message, ["#{message.network}/#{chan}:messages"])
- end
- end
- {:noreply, state}
- end
-
- # Received a private message
- def handle_info({:received, text, sender}, state) do
- reply_fun = fn(text) -> irc_reply(state, {sender.nick, sender}, text) end
- account = IRC.Account.lookup(sender)
- message = %IRC.Message{id: FlakeId.get(), transport: :irc, text: text, network: state.network, at: NaiveDateTime.utc_now(),
- account: account, sender: sender, replyfun: reply_fun, trigger: extract_trigger(text)}
- message = case IRC.UserTrack.messaged(message) do
- :ok -> message
- {:ok, message} -> message
- end
- publish(message, ["messages:private", "#{message.network}/#{account.id}:messages"])
- {:noreply, state}
- end
-
- ## -- Broadcast
- def handle_info({:broadcast, net, account = %IRC.Account{}, message}, state) do
- if net == state.conn.network do
- user = IRC.UserTrack.find_by_account(net, account)
- if user do
- irc_reply(state, {user.nick, nil}, message)
- end
- end
- {:noreply, state}
- end
- def handle_info({:broadcast, net, chan, message}, state) do
- if net == state.conn.network && Enum.member?(state.conn.channels, chan) do
- irc_reply(state, {chan, nil}, message)
- end
- {:noreply, state}
- end
-
- ## -- UserTrack
-
- def handle_info({:joined, channel}, state) do
- ExIRC.Client.who(state.client, channel)
- {:noreply, state}
- end
-
- def handle_info({:who, channel, whos}, state) do
- accounts = Enum.map(whos, fn(who = %ExIRC.Who{nick: nick, operator?: operator}) ->
- priv = if operator, do: [:operator], else: []
- # Don't touch -- on WHO the bot joined, not the users.
- IRC.UserTrack.joined(channel, who, priv, false)
- account = IRC.Account.lookup(who)
- if account do
- {:account, who.network, channel, who.nick, account.id}
- end
- end)
- |> Enum.filter(fn(x) -> x end)
- dispatch("account", {:accounts, accounts})
- {:noreply, state}
- end
-
- def handle_info({:quit, reason, sender}, state) do
- IRC.UserTrack.quitted(sender, reason)
- {:noreply, state}
- end
-
- def handle_info({:joined, channel, sender}, state) do
- IRC.UserTrack.joined(channel, sender, [])
- account = IRC.Account.lookup(sender)
- if account do
- dispatch("account", {:account, sender.network, channel, sender.nick, account.id})
- end
- {:noreply, state}
- end
-
- def handle_info({:kicked, nick, _by, channel, _reason}, state) do
- IRC.UserTrack.parted(state.network, channel, nick)
- {:noreply, state}
- end
-
- def handle_info({:parted, channel, %ExIRC.SenderInfo{nick: nick}}, state) do
- IRC.UserTrack.parted(state.network, channel, nick)
- {:noreply, state}
- end
-
- def handle_info({:mode, [channel, mode, nick]}, state) do
- track_mode(state.network, channel, nick, mode)
- {:noreply, state}
- end
-
- def handle_info({:nick_changed, old_nick, new_nick}, state) do
- IRC.UserTrack.renamed(state.network, old_nick, new_nick)
- {:noreply, state}
- end
-
- def handle_info(unhandled, client) do
- Logger.debug("unhandled: #{inspect unhandled}")
- {:noreply, client}
- end
-
- def publish(pub), do: publish(pub, [])
-
- def publish(m = %IRC.Message{trigger: nil}, keys) do
- dispatch(["messages"] ++ keys, {:irc, :text, m})
- end
-
- def publish(m = %IRC.Message{trigger: t = %IRC.Trigger{trigger: trigger}}, keys) do
- dispatch(["triggers", "#{m.network}/#{m.channel}:triggers", "trigger:"<>trigger], {:irc, :trigger, trigger, m})
- end
-
- def publish_event(net, event = %{type: _}) when is_binary(net) do
- event = event
- |> Map.put(:at, NaiveDateTime.utc_now())
- |> Map.put(:network, net)
- dispatch("#{net}:events", {:irc, :event, event})
- end
- def publish_event({net, chan}, event = %{type: type}) do
- event = event
- |> Map.put(:at, NaiveDateTime.utc_now())
- |> Map.put(:network, net)
- |> Map.put(:channel, chan)
- dispatch("#{net}/#{chan}:events", {:irc, :event, event})
- end
-
- def dispatch(keys, content, sub \\ IRC.PubSub)
-
- def dispatch(key, content, sub) when is_binary(key), do: dispatch([key], content, sub)
- def dispatch(keys, content, sub) when is_list(keys) do
- Logger.debug("dispatch #{inspect keys} = #{inspect content}")
- for key <- keys do
- spawn(fn() -> Registry.dispatch(sub, key, fn h ->
- for {pid, _} <- h, do: send(pid, content)
- end) end)
- end
- end
-
- #
- # Triggers
- #
-
- def triggers, do: @triggers
-
- for {trigger, name} <- @triggers do
- def extract_trigger(unquote(trigger)<>text) do
- text = String.strip(text)
- [trigger | args] = String.split(text, " ")
- %IRC.Trigger{type: unquote(name), trigger: String.downcase(trigger), args: args}
- end
- end
-
- def extract_trigger(_), do: nil
-
- #
- # IRC Replies
- #
-
- # irc_reply(ExIRC.Client pid, {channel or nick, ExIRC.Sender}, binary | replies
- # replies :: {:kick, reason} | {:kick, nick, reason} | {:mode, mode, nick}
- defp irc_reply(state = %{client: client, network: network}, {target, _}, text) when is_binary(text) or is_list(text) do
- lines = IRC.splitlong(text)
- |> Enum.map(fn(x) -> if(is_list(x), do: x, else: String.split(x, "\n")) end)
- |> List.flatten()
- outputs = for line <- lines do
- ExIRC.Client.msg(client, :privmsg, target, line)
- {:irc, :out, %IRC.Message{id: FlakeId.get(), transport: :irc, network: network,
- channel: target, text: line, sender: %ExIRC.SenderInfo{nick: state.conn.nick}, at: NaiveDateTime.utc_now(), meta: %{self: true}}}
- end
- for f <- outputs, do: dispatch(["irc:outputs", "#{network}/#{target}:outputs"], f)
- end
-
- defp irc_reply(%{client: client}, {target, %{nick: nick}}, {:kick, reason}) do
- ExIRC.Client.kick(client, target, nick, reason)
- end
-
- defp irc_reply(%{client: client}, {target, _}, {:kick, nick, reason}) do
- ExIRC.Client.kick(client, target, nick, reason)
- end
-
- defp irc_reply(%{client: client}, {target, %{nick: nick}}, {:mode, mode}) do
- ExIRC.Client.mode(%{client: client}, target, mode, nick)
- end
-
- defp irc_reply(%{client: client}, target, {:mode, mode, nick}) do
- ExIRC.Client.mode(client, target, mode, nick)
- end
-
- defp irc_reply(%{client: client}, target, {:channel_mode, mode}) do
- ExIRC.Client.mode(client, target, mode)
- end
-
- defp track_mode(network, channel, nick, "+o") do
- IRC.UserTrack.change_privileges(network, channel, nick, {[:operator], []})
- :ok
- end
-
- defp track_mode(network, channel, nick, "-o") do
- IRC.UserTrack.change_privileges(network, channel, nick, {[], [:operator]})
- :ok
- end
-
- defp track_mode(network, channel, nick, "+v") do
- IRC.UserTrack.change_privileges(network, channel, nick, {[:voice], []})
- :ok
- end
-
- defp track_mode(network, channel, nick, "-v") do
- IRC.UserTrack.change_privileges(network, channel, nick, {[], [:voice]})
- :ok
- end
-
- defp track_mode(network, channel, nick, mode) do
- Logger.warn("Unhandled track_mode: #{inspect {nick, mode}}")
- :ok
- end
-
- defp server(%{conn: %{host: host, port: port}}) do
- host <> ":" <> to_string(port)
- end
-
-end
diff --git a/lib/irc/conns.ex b/lib/irc/conns.ex
deleted file mode 100644
index b0e5d3c..0000000
--- a/lib/irc/conns.ex
+++ /dev/null
@@ -1,3 +0,0 @@
-defmodule IRC.Conns do
-
-end
diff --git a/lib/irc/irc.ex b/lib/irc/irc.ex
deleted file mode 100644
index 71d6d93..0000000
--- a/lib/irc/irc.ex
+++ /dev/null
@@ -1,79 +0,0 @@
-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} <- 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
-
- @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/irc/membership.ex b/lib/irc/membership.ex
deleted file mode 100644
index b727dfd..0000000
--- a/lib/irc/membership.ex
+++ /dev/null
@@ -1,129 +0,0 @@
-defmodule IRC.Membership do
- @moduledoc """
- Memberships (users in channels)
- """
-
- # Key: {account, net, channel}
- # Format: {key, last_seen}
-
- defp dets() do
- to_charlist(Nola.data_path <> "/memberships.dets")
- end
-
- def start_link() do
- GenServer.start_link(__MODULE__, [], [name: __MODULE__])
- end
-
- def init(_) do
- dets = :dets.open_file(dets(), [])
- {:ok, dets}
- end
-
- def of_account(%IRC.Account{id: id}) do
- spec = [{{{:"$1", :"$2", :"$3"}, :_}, [{:==, :"$1", {:const, id}}], [{{:"$2", :"$3"}}]}]
- :dets.select(dets(), spec)
- end
-
- def merge_account(old_id, new_id) do
- #iex(37)> :ets.fun2ms(fn({{old_id, _, _}, _}=obj) when old_id == "42" -> obj end)
- spec = [{{{:"$1", :_, :_}, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
- Util.ets_mutate_select_each(:dets, dets(), spec, fn(table, obj = {{_old, net, chan}, ts}) ->
- :dets.delete_object(table, obj)
- :dets.insert(table, {{new_id, net, chan}, ts})
- end)
- end
-
- def touch(%IRC.Account{id: id}, network, channel) do
- :dets.insert(dets(), {{id, network, channel}, NaiveDateTime.utc_now()})
- end
- def touch(account_id, network, channel) do
- if account = IRC.Account.get(account_id) do
- touch(account, network, channel)
- end
- end
-
- def notify_channels(account, minutes \\ 30, last_active \\ true) do
- not_before = NaiveDateTime.add(NaiveDateTime.utc_now(), (minutes*-60), :second)
- spec = [{{{:"$1", :_, :_}, :_}, [{:==, :"$1", {:const, account.id}}], [:"$_"]}]
- memberships = :dets.select(dets(), spec)
- |> Enum.sort_by(fn({_, ts}) -> ts end, {:desc, NaiveDateTime})
- active_memberships = Enum.filter(memberships, fn({_, ts}) -> NaiveDateTime.compare(ts, not_before) == :gt end)
- cond do
- active_memberships == [] && last_active ->
- case memberships do
- [{{_, net, chan}, _}|_] -> [{net, chan}]
- _ -> []
- end
- active_memberships == [] ->
- []
- true ->
- Enum.map(active_memberships, fn({{_, net, chan}, _}) -> {net,chan} end)
- end
- end
-
- def members_or_friends(account, _network, nil) do
- friends(account)
- end
-
- def members_or_friends(_, network, channel) do
- members(network, channel)
- end
-
- def expanded_members_or_friends(account, network, channel) do
- expand(network, members_or_friends(account, network, channel))
- end
-
- def expanded_members(network, channel) do
- expand(network, members(network, channel))
- end
-
- def members(network, channel) do
- #iex(19)> :ets.fun2ms(fn({{id, net, chan}, ts}) when net == network and chan == channel and ts > min_seen -> id end)
- limit = 0 # NaiveDateTime.add(NaiveDateTime.utc_now, 30*((24*-60)*60), :second)
- spec = [
- {{{:"$1", :"$2", :"$3"}, :"$4"},
- [
- {:andalso,
- {:andalso, {:==, :"$2", {:const, network}}, {:==, :"$3", {:const, channel}}},
- {:>, :"$4", {:const, limit}}}
- ], [:"$1"]}
- ]
- :dets.select(dets(), spec)
- end
-
- def friends(account = %IRC.Account{id: id}) do
- for({net, chan} <- of_account(account), do: members(net, chan))
- |> List.flatten()
- |> Enum.uniq()
- end
-
- def handle_info(_, dets) do
- {:noreply, dets}
- end
-
- def handle_cast(_, dets) do
- {:noreply, dets}
- end
-
- def handle_call(_, _, dets) do
- {:noreply, dets}
- end
-
- def terminate(_, dets) do
- :dets.sync(dets)
- :dets.close(dets)
- end
-
- defp expand(network, list) do
- for id <- list do
- if account = IRC.Account.get(id) do
- user = IRC.UserTrack.find_by_account(network, account)
- nick = if(user, do: user.nick, else: account.name)
- {account, user, nick}
- end
- end
- |> Enum.filter(fn(x) -> x end)
- end
-
-
-end
diff --git a/lib/irc/plugin/temp_ref.ex b/lib/irc/plugin/temp_ref.ex
deleted file mode 100644
index 923fa1a..0000000
--- a/lib/irc/plugin/temp_ref.ex
+++ /dev/null
@@ -1,95 +0,0 @@
-defmodule Irc.Plugin.TempRef do
- @moduledoc """
- This module allows to easily implement local temporary simple references for easy access from IRC.
-
- For example, your plugin output could be acted on, and instead of giving the burden for the user to
- write or copy that uuid, you could give them a small alphanumeric reference to use instead.
-
- You can configure how many and for how long the references are kept.
-
- ## Usage
-
- `import Irc.Plugin.TempRef`
-
- ```elixir
- defmodule Irc.MyPlugin do
- defstruct [:temprefs]
-
- def init(_) do
- # …
- {:ok, %__MODULE__{temprefs: new_temp_refs()}
- end
- end
- ```
- """
-
- defstruct [:refs, :max, :expire, :build_fun, :build_increase_fun, :build_options]
-
- defmodule SimpleAlphaNumericBuilder do
- def build(options) do
- length = Keyword.get(options, :length, 3)
- for _ <- 1..length, into: "", do: <<Enum.random('bcdfghjkmpqtrvwxy2346789')>>
- end
-
- def increase(options) do
- Keyword.put(options, :length, Keyword.get(options, :length, 3) + 1)
- end
- end
-
- def new_temp_refs(options \\ []) do
- %__MODULE__{
- refs: Keyword.get(options, :init_refs, []),
- max: Keyword.get(options, :max, []),
- expire: Keyword.get(options, :expire, :infinity),
- build_fun: Keyword.get(options, :build_fun, &__MODULE__.SimpleAlphaNumericBuilder.build/1),
- build_increase_fun: Keyword.get(options, :build_increase_fun, &__MODULE__.SimpleAlphaNumericBuilder.increase/1),
- build_options: Keyword.get(options, :build_options, [length: 3])
- }
- end
-
- def janitor_refs(state = %__MODULE__{}) do
- if length(state.refs) > state.max do
- %__MODULE__{refs: state.refs |> Enum.reverse() |> tl() |> Enum.reverse()}
- else
- state
- end
- end
-
- def put_temp_ref(data, state = %__MODULE__{}) do
- state = janitor_refs(state)
- key = new_nonexisting_key(state)
- if key do
- ref = {key, DateTime.utc_now(), data}
- {key, %__MODULE__{state | refs: [ref | state.refs]}}
- else
- {nil, state}
- end
- end
-
- def lookup_temp_ref(key, state, default \\ nil) do
- case List.keyfind(state.refs, key, 0) do
- {_, _, data} -> data
- _ -> default
- end
- end
-
- defp new_nonexisting_key(state, i) when i > 50 do
- nil
- end
-
- defp new_nonexisting_key(state = %__MODULE__{refs: refs}, i \\ 1) do
- build_options = if rem(i, 5) == 0 do
- state.build_increase_fun.(state.build_options)
- else
- state.build_options
- end
-
- key = state.build_fun.(state.build_options)
- if !List.keymember?(refs, key, 0) do
- key
- else
- new_nonexisting_key(state, i + 1)
- end
- end
-
-end
diff --git a/lib/irc/plugin_supervisor.ex b/lib/irc/plugin_supervisor.ex
deleted file mode 100644
index a65ad09..0000000
--- a/lib/irc/plugin_supervisor.ex
+++ /dev/null
@@ -1,99 +0,0 @@
-defmodule IRC.Plugin do
- require Logger
-
- 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: {IRC.Plugin,module}, start: {IRC.Plugin, :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
- for mod <- enabled(), do: {mod, IRC.Plugin.Supervisor.start_child(mod)}
- end
-
- def declare(module) do
- case get(module) do
- :disabled -> :dets.insert(dets(), {module, true, nil})
- _ -> nil
- end
- end
-
- def start(module, opts \\ []) do
- IRC.Plugin.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("Plugin: #{to_string(module)} ignored start: #{to_string(error)}")
- :ignore
- end
- end
-
-end
diff --git a/lib/irc/puppet_connection.ex b/lib/irc/puppet_connection.ex
deleted file mode 100644
index 91a26b3..0000000
--- a/lib/irc/puppet_connection.ex
+++ /dev/null
@@ -1,238 +0,0 @@
-defmodule IRC.PuppetConnection do
- require Logger
- @min_backoff :timer.seconds(5)
- @max_backoff :timer.seconds(2*60)
- @max_idle :timer.hours(12)
- @env Mix.env
-
- defmodule Supervisor do
- use DynamicSupervisor
-
- def start_link() do
- DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
- end
-
- def start_child(%IRC.Account{id: account_id}, %IRC.Connection{id: connection_id}) do
- spec = %{id: {account_id, connection_id}, start: {IRC.PuppetConnection, :start_link, [account_id, connection_id]}, restart: :transient}
- DynamicSupervisor.start_child(__MODULE__, spec)
- end
-
- @impl true
- def init(_init_arg) do
- DynamicSupervisor.init(
- strategy: :one_for_one,
- max_restarts: 10,
- max_seconds: 1
- )
- end
- end
-
- def whereis(account = %IRC.Account{id: account_id}, connection = %IRC.Connection{id: connection_id}) do
- {:global, name} = name(account_id, connection_id)
- case :global.whereis_name(name) do
- :undefined -> nil
- pid -> pid
- end
- end
-
- def send_message(account = %IRC.Account{id: account_id}, connection = %IRC.Connection{id: connection_id}, channel, text) do
- GenServer.cast(name(account_id, connection_id), {:send_message, self(), channel, text})
- end
-
- def start_and_send_message(account = %IRC.Account{id: account_id}, connection = %IRC.Connection{id: connection_id}, channel, text) do
- {:global, name} = name(account_id, connection_id)
- pid = whereis(account, connection)
- pid = if !pid do
- case IRC.PuppetConnection.Supervisor.start_child(account, connection) do
- {:ok, pid} -> pid
- {:error, {:already_started, pid}} -> pid
- end
- else
- pid
- end
- GenServer.cast(pid, {:send_message, self(), channel, text})
- end
-
- def start(account = %IRC.Account{}, connection = %IRC.Connection{}) do
- IRC.PuppetConnection.Supervisor.start_child(account, connection)
- end
-
- def start_link(account_id, connection_id) do
- GenServer.start_link(__MODULE__, [account_id, connection_id], name: name(account_id, connection_id))
- end
-
- def name(account_id, connection_id) do
- {:global, {PuppetConnection, account_id, connection_id}}
- end
-
- def init([account_id, connection_id]) do
- account = %IRC.Account{} = IRC.Account.get(account_id)
- connection = %IRC.Connection{} = IRC.Connection.lookup(connection_id)
- Logger.metadata(puppet_conn: account.id <> "@" <> connection.id)
- backoff = :backoff.init(@min_backoff, @max_backoff)
- |> :backoff.type(:jitter)
- idle = :erlang.send_after(@max_idle, self, :idle)
- {:ok, %{client: nil, backoff: backoff, idle: idle, connected: false, buffer: [], channels: [], connection_id: connection_id, account_id: account_id, connected_server: nil, connected_port: nil, network: connection.network}, {:continue, :connect}}
- end
-
- def handle_continue(:connect, state) do
- #ipv6 = if @env == :prod do
- # subnet = Nola.Subnet.assign(state.account_id)
- # IRC.Account.put_meta(IRC.Account.get(state.account_id), "subnet", subnet)
- # ip = Pfx.host(subnet, 1)
- # {:ok, ipv6} = :inet_parse.ipv6_address(to_charlist(ip))
- # System.cmd("add-ip6", [ip])
- # ipv6
- #end
-
- conn = IRC.Connection.lookup(state.connection_id)
- client_opts = []
- |> Keyword.put(:network, conn.network)
- client = if state.client && Process.alive?(state.client) do
- Logger.info("Reconnecting client")
- state.client
- else
- Logger.info("Connecting")
- {:ok, client} = ExIRC.Client.start_link(debug: false)
- ExIRC.Client.add_handler(client, self())
- client
- end
-
- base_opts = [
- {:nodelay, true}
- ]
-
- #{ip, opts} = case {ipv6, :inet_res.resolve(to_charlist(conn.host), :in, :aaaa)} do
- # {ipv6, {:ok, {:dns_rec, _dns_header, _query, rrs = [{:dns_rr, _, _, _, _, _, _, _, _, _} | _], _, _}}} ->
- # ip = rrs
- # |> Enum.map(fn({:dns_rr, _, :aaaa, :in, _, _, ipv6, _, _, _}) -> ipv6 end)
- # |> Enum.shuffle()
- # |> List.first()
-
- # opts = [
- # :inet6,
- # {:ifaddr, ipv6}
- # ]
- # {ip, opts}
- # _ ->
- {ip, opts} = {to_charlist(conn.host), []}
- #end
-
- conn_fun = if conn.tls, do: :connect_ssl!, else: :connect!
- apply(ExIRC.Client, conn_fun, [client, ip, conn.port, base_opts ++ opts])
-
- {:noreply, %{state | client: client}}
- end
-
- def handle_continue(:connected, state) do
- state = Enum.reduce(Enum.reverse(state.buffer), state, fn(b, state) ->
- {:noreply, state} = handle_cast(b, state)
- state
- end)
- {:noreply, %{state | buffer: []}}
- end
-
- def handle_cast(cast = {:send_message, _pid, _channel, _text}, state = %{connected: false, buffer: buffer}) do
- {:noreply, %{state | buffer: [cast | buffer]}}
- end
-
- def handle_cast({:send_message, pid, channel, text}, state = %{connected: true}) do
- channels = if !Enum.member?(state.channels, channel) do
- ExIRC.Client.join(state.client, channel)
- [channel | state.channels]
- else
- state.channels
- end
- ExIRC.Client.msg(state.client, :privmsg, channel, text)
-
- meta = %{puppet: true, from: pid}
- account = IRC.Account.get(state.account_id)
- nick = make_nick(state)
- sender = %ExIRC.SenderInfo{network: state.network, nick: suffix_nick(nick), user: nick, host: "puppet."}
- reply_fun = fn(text) ->
- IRC.Connection.broadcast_message(state.network, channel, text)
- end
- message = %IRC.Message{id: FlakeId.get(), at: NaiveDateTime.utc_now(), text: text, network: state.network, account: account, sender: sender, channel: channel, replyfun: reply_fun, trigger: IRC.Connection.extract_trigger(text), meta: meta}
- message = case IRC.UserTrack.messaged(message) do
- :ok -> message
- {:ok, message} -> message
- end
- IRC.Connection.publish(message, ["#{message.network}/#{channel}:messages"])
-
- idle = if length(state.buffer) == 0 do
- :erlang.cancel_timer(state.idle)
- :erlang.send_after(@max_idle, self(), :idle)
- else
- state.idle
- end
-
- {:noreply, %{state | idle: idle, channels: channels}}
- end
-
- def handle_info(:idle, state) do
- ExIRC.Client.quit(state.client, "Puppet was idle for too long")
- ExIRC.Client.stop!(state.client)
- {:stop, :normal, state}
- end
-
- def handle_info(:disconnected, state) do
- {delay, backoff} = :backoff.fail(state.backoff)
- Logger.info("#{inspect(self())} Disconnected -- reconnecting in #{inspect delay}ms")
- Process.send_after(self(), :connect, delay)
- {:noreply, %{state | connected: false, backoff: backoff}}
- end
-
- def handle_info(:connect, state) do
- {:noreply, state, {:continue, :connect}}
- end
-
- # Connection successful
- def handle_info({:connected, server, port}, state) do
- Logger.info("#{inspect(self())} Connected to #{inspect(server)}:#{port} #{inspect state}")
- {_, backoff} = :backoff.succeed(state.backoff)
- base_nick = make_nick(state)
- ExIRC.Client.logon(state.client, "", suffix_nick(base_nick), base_nick, "#{base_nick}'s puppet")
- {:noreply, %{state | backoff: backoff, connected_server: server, connected_port: port}}
- end
-
- # Logon successful
- def handle_info(:logged_in, state) do
- Logger.info("#{inspect(self())} Logged in")
- {_, backoff} = :backoff.succeed(state.backoff)
- # Create an UserTrack entry for the client so it's authenticated to the right account_id already.
- IRC.UserTrack.connected(state.network, suffix_nick(make_nick(state)), make_nick(state), "puppet.", state.account_id, %{puppet: true})
- {:noreply, %{state | backoff: backoff}}
- end
-
- # ISUP
- def handle_info({:isup, network}, state) do
- {:noreply, %{state | network: network, connected: true}, {:continue, :connected}}
- end
-
- # Been kicked
- def handle_info({:kicked, _sender, chan, _reason}, state) do
- {:noreply, %{state | channels: state.channels -- [chan]}}
- end
-
- def handle_info(_info, state) do
- {:noreply, state}
- end
-
- def make_nick(state) do
- account = IRC.Account.get(state.account_id)
- user = IRC.UserTrack.find_by_account(state.network, account)
- base_nick = if(user, do: user.nick, else: account.name)
- clean_nick = case String.split(base_nick, ":", parts: 2) do
- ["@"<>nick, _] -> nick
- [nick] -> nick
- end
- clean_nick
- end
-
- if Mix.env == :dev do
- def suffix_nick(nick), do: "#{nick}[d]"
- else
- def suffix_nick(nick), do: "#{nick}[p]"
- end
-
-end
diff --git a/lib/irc/user_track.ex b/lib/irc/user_track.ex
deleted file mode 100644
index 1efa523..0000000
--- a/lib/irc/user_track.ex
+++ /dev/null
@@ -1,329 +0,0 @@
-defmodule IRC.UserTrack do
- @moduledoc """
- User Track DB & Utilities
- """
-
- @ets IRC.UserTrack.Storage
- # {uuid, network, nick, nicks, privilege_map}
- # Privilege map:
- # %{"#channel" => [:operator, :voice]
- defmodule Storage do
-
- def delete(id) do
- op(fn(ets) -> :ets.delete(ets, id) end)
- end
-
- def insert(tuple) do
- op(fn(ets) -> :ets.insert(ets, tuple) end)
- end
-
- def clear_network(network) do
- op(fn(ets) ->
- spec = [
- {{:_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_, :_},
- [
- {:==, :"$1", {:const, network}}
- ], [:"$_"]}
- ]
- :ets.match_delete(ets, spec)
- end)
- end
-
- def op(fun) do
- GenServer.call(__MODULE__, {:op, fun})
- end
-
- def start_link do
- GenServer.start_link(__MODULE__, [], [name: __MODULE__])
- end
-
- def init([]) do
- ets = :ets.new(__MODULE__, [:set, :named_table, :protected, {:read_concurrency, true}])
- {:ok, ets}
- end
-
- def handle_call({:op, fun}, _from, ets) do
- returned = try do
- {:ok, fun.(ets)}
- rescue
- rescued -> {:error, rescued}
- catch
- rescued -> {:error, rescued}
- end
- {:reply, returned, ets}
- end
-
- def terminate(_reason, ets) do
- :ok
- end
- end
-
- defmodule Id, do: use EntropyString
-
- defmodule User do
- defstruct [:id, :account, :network, :nick, {:nicks, []}, :username, :host, :realname, {:privileges, %{}}, {:last_active, %{}}, {:options, %{}}]
-
- def to_tuple(u = %__MODULE__{}) do
- {u.id || IRC.UserTrack.Id.large_id, u.network, u.account, String.downcase(u.nick), u.nick, u.nicks || [], u.username, u.host, u.realname, u.privileges, u.last_active, u.options}
- end
-
- #tuple size: 11
- def from_tuple({id, network, account, _downcased_nick, nick, nicks, username, host, realname, privs, last_active, opts}) do
- struct = %__MODULE__{id: id, account: account, network: network, nick: nick, nicks: nicks, username: username, host: host, realname: realname, privileges: privs, last_active: last_active, options: opts}
- end
- end
-
- def find_by_account(%IRC.Account{id: id}) do
- #iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
- spec = [
- {{:_, :_, :"$2", :_, :_, :_, :_, :_, :_, :_, :_, :_},
- [
- {:==, :"$2", {:const, id}}
- ], [:"$_"]}
- ]
- results = :ets.select(@ets, spec)
- |> Enum.filter(& &1)
- for obj <- results, do: User.from_tuple(obj)
- end
-
- def find_by_account(network, nil) do
- nil
- end
-
- def find_by_account(network, %IRC.Account{id: id}) do
- #iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
- spec = [
- {{:_, :"$1", :"$2", :_, :_, :_, :_, :_, :_, :_, :_, :_},
- [
- {:andalso, {:==, :"$1", {:const, network}},
- {:==, :"$2", {:const, id}}}
- ], [:"$_"]}
- ]
- case :ets.select(@ets, spec) do
- results = [_r | _] ->
- result = results
- |> Enum.reject(fn({_, net, _, _, _, _, _, _, _, _, actives, opts}) -> network != "matrix" && net == "matrix" end)
- |> Enum.reject(fn({_, net, _, _, _, _, _, _, _, _, actives, opts}) -> network != "telegram" && net == "telegram" end)
- |> Enum.reject(fn({_, _, _, _, _, _, _, _, _, _, actives, opts}) -> network not in ["matrix", "telegram"] && Map.get(opts, :puppet) end)
- |> Enum.sort_by(fn({_, _, _, _, _, _, _, _, _, _, actives, _}) ->
- Map.get(actives, nil)
- end, {:desc, NaiveDateTime})
- |> List.first
-
- if result, do: User.from_tuple(result)
- _ -> nil
- end
- end
-
- def clear_network(network) do
- Storage.clear_network(network)
- end
-
-
- def merge_account(old_id, new_id) do
- #iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
- spec = [
- {{:_, :_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_},
- [
- {:==, :"$1", {:const, old_id}}
- ], [:"$_"]}
- ]
- Enum.each(:ets.select(@ets, spec), fn({id, net, _, downcased_nick, nick, nicks, username, host, realname, privs, active, opts}) ->
- Storage.op(fn(ets) ->
- :ets.insert(@ets, {id, net, new_id, downcased_nick, nick, nicks, username, host, realname, privs, active, opts})
- end)
- end)
- end
-
- def find_by_nick(%ExIRC.Who{network: network, nick: nick}) do
- find_by_nick(network, nick)
- end
-
-
- def find_by_nick(%ExIRC.SenderInfo{network: network, nick: nick}) do
- find_by_nick(network, nick)
- end
-
- def find_by_nick(network, nick) do
- case :ets.match(@ets, {:"$1", network, :_, String.downcase(nick), :_, :_, :_, :_, :_, :_, :_, :_}) do
- [[id] | _] -> lookup(id)
- _ ->
- nil
- end
- end
-
- def to_list, do: :ets.tab2list(@ets)
-
- def lookup(id) do
- case :ets.lookup(@ets, id) do
- [] -> nil
- [tuple] -> User.from_tuple(tuple)
- end
- end
-
- def operator?(network, channel, nick) do
- if user = find_by_nick(network, nick) do
- privs = Map.get(user.privileges, channel, [])
- Enum.member?(privs, :admin) || Enum.member?(privs, :operator)
- else
- false
- end
- end
-
- def channel(network, channel) do
- Enum.filter(to_list(), fn({_, network, _, _, _, _, _, _, _, channels, _, _}) ->
- Map.get(channels, channel)
- end)
- end
-
- # TODO
- def connected(network, nick, user, host, account_id, opts \\ %{}) do
- if account = IRC.Account.get(account_id) do
- user = if user = find_by_nick(network, nick) do
- user
- else
- user = %User{id: IRC.UserTrack.Id.large_id, account: account_id, network: network, nick: nick, username: user, host: host, privileges: %{}, options: opts}
- Storage.op(fn(ets) ->
- :ets.insert(ets, User.to_tuple(user))
- end)
- user
- end
-
- IRC.Connection.publish_event(network, %{type: :connect, user_id: user.id, account_id: user.account})
- :ok
- else
- :error
- end
- end
-
- def joined(c, s), do: joined(c,s,[])
-
- def joined(channel, sender=%{nick: nick, user: uname, host: host}, privileges, touch \\ true) do
- privileges = if IRC.admin?(sender) do
- privileges ++ [:admin]
- else privileges end
- user = if user = find_by_nick(sender.network, nick) do
- %User{user | username: uname, host: host, privileges: Map.put(user.privileges || %{}, channel, privileges)}
- else
- user = %User{id: IRC.UserTrack.Id.large_id, network: sender.network, nick: nick, username: uname, host: host, privileges: %{channel => privileges}}
-
- account = IRC.Account.lookup(user).id
- user = %User{user | account: account}
- end
- user = touch_struct(user, channel)
-
- if touch && user.account do
- IRC.Membership.touch(user.account, sender.network, channel)
- end
-
- Storage.op(fn(ets) ->
- :ets.insert(ets, User.to_tuple(user))
- end)
-
- IRC.Connection.publish_event({sender.network, channel}, %{type: :join, user_id: user.id, account_id: user.account})
-
- user
- end
-
- #def joined(network, channel, nick, privileges) do
- # user = if user = find_by_nick(network, nick) do
- # %User{user | privileges: Map.put(user.privileges, channel, privileges)}
- # else
- # %User{nick: nick, privileges: %{channel => privileges}}
- # end
- #
- # Storage.op(fn(ets) ->
- # :ets.insert(ets, User.to_tuple(user))
- # end)
- #end
-
- def messaged(%IRC.Message{network: network, account: account, channel: chan, sender: %{nick: nick}} = m) do
- {user, account} = if user = find_by_nick(network, nick) do
- {touch_struct(user, chan), account || IRC.Account.lookup(user)}
- else
- user = %User{network: network, nick: nick, privileges: %{}}
- account = IRC.Account.lookup(user)
- {%User{user | account: account.id}, account}
- end
- Storage.insert(User.to_tuple(user))
- if chan, do: IRC.Membership.touch(account, network, chan)
- if !m.account do
- {:ok, %IRC.Message{m | account: account}}
- else
- :ok
- end
- end
-
- def renamed(network, old_nick, new_nick) do
- if user = find_by_nick(network, old_nick) do
- old_account = IRC.Account.lookup(user)
- user = %User{user | nick: new_nick, nicks: [old_nick|user.nicks]}
- account = IRC.Account.lookup(user, false) || old_account
- user = %User{user | nick: new_nick, account: account.id, nicks: [old_nick|user.nicks]}
- Storage.insert(User.to_tuple(user))
- channels = for {channel, _} <- user.privileges, do: channel
- IRC.Connection.publish_event(network, %{type: :nick, user_id: user.id, account_id: account.id, nick: new_nick, old_nick: old_nick})
- end
- end
-
- def change_privileges(network, channel, nick, {add, remove}) do
- if user = find_by_nick(network, nick) do
- privs = Map.get(user.privileges, channel)
-
- privs = Enum.reduce(add, privs, fn(priv, acc) -> [priv|acc] end)
- privs = Enum.reduce(remove, privs, fn(priv, acc) -> List.delete(acc, priv) end)
-
- user = %User{user | privileges: Map.put(user.privileges, channel, privs)}
- Storage.insert(User.to_tuple(user))
- IRC.Connection.publish_event({network, channel}, %{type: :privileges, user_id: user.id, account_id: user.account, added: add, removed: remove})
- end
- end
-
- # XXX: Reason
- def parted(channel, %{network: network, nick: nick}) do
- parted(network, channel, nick)
- end
-
- def parted(network, channel, nick) do
- if user = find_by_nick(network, nick) do
- if user.account do
- IRC.Membership.touch(user.account, network, channel)
- end
-
- privs = Map.delete(user.privileges, channel)
- lasts = Map.delete(user.last_active, channel)
- if Enum.count(privs) > 0 do
- user = %User{user | privileges: privs}
- Storage.insert(User.to_tuple(user))
- IRC.Connection.publish_event({network, channel}, %{type: :part, user_id: user.id, account_id: user.account, reason: nil})
- else
- IRC.Connection.publish_event(network, %{type: :quit, user_id: user.id, account_id: user.account, reason: "Left all known channels"})
- Storage.delete(user.id)
- end
- end
- end
-
- def quitted(sender, reason) do
- if user = find_by_nick(sender.network, sender.nick) do
- if user.account do
- for {channel, _} <- user.privileges do
- IRC.Membership.touch(user.account, sender.network, channel)
- end
- IRC.Connection.publish_event(sender.network, %{type: :quit, user_id: user.id, account_id: user.account, reason: reason})
- end
- Storage.delete(user.id)
- end
- end
-
- defp touch_struct(user = %User{last_active: last_active}, channel) do
- now = NaiveDateTime.utc_now()
- last_active = last_active
- |> Map.put(channel, now)
- |> Map.put(nil, now)
- %User{user | last_active: last_active}
- end
-
- defp userchans(%{privileges: privileges}) do
- for({chan, _} <- privileges, do: chan)
- end
-end