diff options
author | Jordan Bracco <href@random.sh> | 2022-12-20 00:21:54 +0000 |
---|---|---|
committer | Jordan Bracco <href@random.sh> | 2022-12-20 19:29:41 +0100 |
commit | 2d83df8b32bff7f0028923bb5b64dc0b55f20d03 (patch) | |
tree | 1207e67b5b15f540963db05e7be89f3ca950e724 /lib/irc | |
parent | Nola 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.ex | 451 | ||||
-rw-r--r-- | lib/irc/connection.ex | 521 | ||||
-rw-r--r-- | lib/irc/conns.ex | 3 | ||||
-rw-r--r-- | lib/irc/irc.ex | 79 | ||||
-rw-r--r-- | lib/irc/membership.ex | 129 | ||||
-rw-r--r-- | lib/irc/plugin/temp_ref.ex | 95 | ||||
-rw-r--r-- | lib/irc/plugin_supervisor.ex | 99 | ||||
-rw-r--r-- | lib/irc/puppet_connection.ex | 238 | ||||
-rw-r--r-- | lib/irc/user_track.ex | 329 |
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 |