summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorJordan Bracco <href@random.sh>2025-06-25 18:07:03 +0200
committerJordan Bracco <href@random.sh>2025-06-25 18:07:03 +0200
commit93847380b97657fba826d29f3bbed491aca887fa (patch)
treef47f4fc0969c697017f169b87397616c3ad2929a /lib
parentupdate otp (diff)
conn: use cacerts
Diffstat (limited to 'lib')
-rw-r--r--lib/irc/connection.ex382
1 files changed, 264 insertions, 118 deletions
diff --git a/lib/irc/connection.ex b/lib/irc/connection.ex
index 5ad3e9e..61617a9 100644
--- a/lib/irc/connection.ex
+++ b/lib/irc/connection.ex
@@ -32,18 +32,18 @@ defmodule Nola.Irc.Connection do
def irc_doc, do: nil
@min_backoff :timer.seconds(5)
- @max_backoff :timer.seconds(2*60)
+ @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: []
+ 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
@@ -54,7 +54,12 @@ defmodule Nola.Irc.Connection do
end
def start_child(%Nola.Irc.Connection{} = conn) do
- spec = %{id: conn.id, start: {Nola.Irc.Connection, :start_link, [conn]}, restart: :transient}
+ spec = %{
+ id: conn.id,
+ start: {Nola.Irc.Connection, :start_link, [conn]},
+ restart: :transient
+ }
+
DynamicSupervisor.start_child(__MODULE__, spec)
end
@@ -68,7 +73,6 @@ defmodule Nola.Irc.Connection do
end
end
-
def changeset(params) do
import Ecto.Changeset
@@ -79,11 +83,23 @@ defmodule Nola.Irc.Connection do
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}
+ {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}
+ %__MODULE__{
+ id: id,
+ network: network,
+ host: host,
+ port: port,
+ nick: nick,
+ user: user,
+ name: name,
+ pass: pass,
+ tls: tls,
+ channels: channels
+ }
end
## -- MANAGER API
@@ -102,7 +118,7 @@ defmodule Nola.Irc.Connection do
end
def connections() do
- :dets.foldl(fn(object, acc) -> [from_tuple(object) | acc] end, [], dets())
+ :dets.foldl(fn object, acc -> [from_tuple(object) | acc] end, [], dets())
end
def start_all() do
@@ -110,23 +126,29 @@ defmodule Nola.Irc.Connection do
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)
+ 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)
+ 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}}}],
- [:"$_"]}
+ 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
@@ -147,7 +169,9 @@ defmodule Nola.Irc.Connection do
case :global.whereis_name(id) do
pid when is_pid(pid) ->
GenServer.stop(pid, :normal)
- _ -> :error
+
+ _ ->
+ :error
end
end
@@ -160,7 +184,9 @@ defmodule Nola.Irc.Connection do
:dets.insert(dets(), to_tuple(conn))
Nola.Irc.Connection.Supervisor.start_child(conn)
end
- error -> error
+
+ error ->
+ error
end
end
@@ -175,11 +201,12 @@ defmodule Nola.Irc.Connection do
def broadcast_message(net, chan, message, meta \\ []) do
dispatch("conn", {:broadcast, net, chan, message}, meta, Nola.Irc.ConnectionPubSub)
end
- #def broadcast_message(list, message, meta \\ []) when is_list(list) do
+
+ # def broadcast_message(list, message, meta \\ []) when is_list(list) do
# for {net, chan} <- list do
# broadcast_message(net, chan, message, meta)
# end
- #send
+ # send
def privmsg(channel, line) do
GenServer.cast(__MODULE__, {:privmsg, channel, line})
@@ -187,13 +214,22 @@ defmodule Nola.Irc.Connection do
def init([conn]) do
Logger.metadata(conn: conn.id)
- backoff = :backoff.init(@min_backoff, @max_backoff)
- |> :backoff.type(:jitter)
- {:ok, %{client: nil,
- ignore_channel_for_a_while: conn.network == "snoonet",
- backoff: backoff,
- channels: Map.new,
- conn: conn, connected_server: nil, connected_port: nil, network: conn.network}, {:continue, :connect}}
+
+ backoff =
+ :backoff.init(@min_backoff, @max_backoff)
+ |> :backoff.type(:jitter)
+
+ {:ok,
+ %{
+ client: nil,
+ ignore_channel_for_a_while: conn.network == "snoonet",
+ backoff: backoff,
+ channels: Map.new(),
+ conn: conn,
+ connected_server: nil,
+ connected_port: nil,
+ network: conn.network
+ }, {:continue, :connect}}
end
@triggers %{
@@ -214,31 +250,41 @@ defmodule Nola.Irc.Connection do
}
def handle_continue(:connect, state) do
- client_opts = []
- |> Keyword.put(:network, state.conn.network)
+ client_opts =
+ []
+ |> Keyword.put(:network, state.conn.network)
+
{:ok, _} = Registry.register(Nola.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}]
+ 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}, {:cacerts, :certifi.cacerts()}]
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])
+
+ conn_result =
+ apply(ExIRC.Client, conn_fun, [client, to_charlist(state.conn.host), state.conn.port, opts])
+
+ Logger.warn(
+ "Connecting to #{state.conn.host}:#{state.conn.port} using #{conn_fun} => #{inspect(conn_result)}"
+ )
{: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")
+ Logger.info("#{inspect(self())} Disconnected -- reconnecting in #{inspect(delay)}ms")
Process.send_after(self(), :connect, delay)
- {:noreply, %{state | channels: Map.new, backoff: backoff}}
+ {:noreply, %{state | channels: Map.new(), backoff: backoff}}
end
def handle_info(:connect, state) do
@@ -249,13 +295,29 @@ defmodule Nola.Irc.Connection 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}")
+ 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 | channels: Map.new, backoff: backoff, connected_server: server, connected_port: port}}
+
+ ExIRC.Client.logon(
+ state.client,
+ state.conn.pass || "",
+ state.conn.nick,
+ state.conn.user,
+ state.conn.name
+ )
+
+ {:noreply,
+ %{
+ state
+ | channels: Map.new(),
+ backoff: backoff,
+ connected_server: server,
+ connected_port: port
+ }}
end
# Logon successful
@@ -269,9 +331,11 @@ defmodule Nola.Irc.Connection do
# ISUP
def handle_info({:isup, network}, state) when is_binary(network) do
Nola.UserTrack.clear_network(state.network)
+
if network != state.network do
Logger.warn("Possibly misconfigured network: #{network} != #{state.network}")
end
+
{:noreply, state}
end
@@ -283,49 +347,88 @@ defmodule Nola.Irc.Connection do
# Received something in a channel
def handle_info({:received, text, sender, chan}, state) do
- channel_state = Map.get(state.channels, chan, Map.new)
- if state.ignore_channel_for_a_while && DateTime.diff(DateTime.utc_now(), Map.get(channel_state, :joined)) < 30 do
+ channel_state = Map.get(state.channels, chan, Map.new())
+
+ if state.ignore_channel_for_a_while &&
+ DateTime.diff(DateTime.utc_now(), Map.get(channel_state, :joined)) < 30 do
:ignore_channel
else
- user = if user = Nola.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 = Nola.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
+ user =
+ if user = Nola.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 = Nola.UserTrack.joined(chan, sender, [])
+ # Rewho everything in case of need ? We shouldn't not know that user..
+ ExIRC.Client.who(state.client, chan)
+ 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}}")
+ # Rewho everything in case of need ? We shouldn't not know that user..
+ ExIRC.Client.who(state.client, chan)
+
+ 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
+ reply_fun = fn text -> irc_reply(state, {chan, sender}, text) end
account = Nola.Account.lookup(sender)
- message = %Nola.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 Nola.UserTrack.messaged(message) do
- :ok -> message
- {:ok, message} -> message
- end
+
+ message = %Nola.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 Nola.UserTrack.messaged(message) do
+ :ok -> message
+ {:ok, message} -> message
+ end
+
publish(message, ["#{message.network}/#{chan}:messages"])
end
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
+ reply_fun = fn text -> irc_reply(state, {sender.nick, sender}, text) end
account = Nola.Account.lookup(sender)
- message = %Nola.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 Nola.UserTrack.messaged(message) do
- :ok -> message
- {:ok, message} -> message
- end
+
+ message = %Nola.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 Nola.UserTrack.messaged(message) do
+ :ok -> message
+ {:ok, message} -> message
+ end
+
publish(message, ["messages:private", "#{message.network}/#{account.id}:messages"])
{:noreply, state}
end
@@ -334,38 +437,45 @@ defmodule Nola.Irc.Connection do
def handle_info({:broadcast, net, account = %Nola.Account{}, message}, state) do
if net == state.conn.network do
user = Nola.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
- channels = Map.put(state.channels, channel, %{joined: DateTime.utc_now})
+ channels = Map.put(state.channels, channel, %{joined: DateTime.utc_now()})
ExIRC.Client.who(state.client, channel)
{:noreply, %{state | channels: channels}}
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.
- Nola.UserTrack.joined(channel, who, priv, false)
- account = Nola.Account.lookup(who)
- if account do
- {:account, who.network, channel, who.nick, account.id}
- end
- end)
- |> Enum.filter(fn(x) -> x end)
+ 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.
+ Nola.UserTrack.joined(channel, who, priv, false)
+ account = Nola.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
@@ -378,9 +488,11 @@ defmodule Nola.Irc.Connection do
def handle_info({:joined, channel, sender}, state) do
Nola.UserTrack.joined(channel, sender, [])
account = Nola.Account.lookup(sender)
+
if account do
dispatch("account", {:account, sender.network, channel, sender.nick, account.id})
end
+
{:noreply, state}
end
@@ -405,7 +517,7 @@ defmodule Nola.Irc.Connection do
end
def handle_info(unhandled, client) do
- Logger.debug("unhandled: #{inspect unhandled}")
+ Logger.debug("unhandled: #{inspect(unhandled)}")
{:noreply, client}
end
@@ -416,34 +528,50 @@ defmodule Nola.Irc.Connection do
end
def publish(m = %Nola.Message{trigger: t = %Nola.Trigger{trigger: trigger}}, keys) do
- dispatch(["triggers", "#{m.network}/#{m.channel}:triggers", "trigger:"<>trigger], {:irc, :trigger, trigger, m}, Enum.into(m.meta, []))
+ dispatch(
+ ["triggers", "#{m.network}/#{m.channel}:triggers", "trigger:" <> trigger],
+ {:irc, :trigger, trigger, m},
+ Enum.into(m.meta, [])
+ )
end
def publish_event(net, event = %{type: _}) when is_binary(net) do
- event = event
- |> Map.put(:at, NaiveDateTime.utc_now())
- |> Map.put(:network, net)
+ 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)
+ 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, meta \\ [], sub \\ Nola.PubSub)
- def dispatch(key, content, meta, sub) when is_binary(key), do: dispatch([key], content, meta, sub)
+ def dispatch(key, content, meta, sub) when is_binary(key),
+ do: dispatch([key], content, meta, sub)
+
def dispatch(keys, content, meta, sub) when is_list(keys) do
- Logger.debug("dispatching: #{inspect keys} #{inspect content}")
+ Logger.debug("dispatching: #{inspect(keys)} #{inspect(content)}")
should_dispatch_data = should_dispatch_data(content)
origin = Keyword.get(meta, :origin, :no_origin)
+
for key <- keys do
- spawn(fn() ->
+ spawn(fn ->
Registry.dispatch(sub, key, fn h ->
- for {pid, reg} <- h, do: if(should_dispatch?(key, origin, should_dispatch_data, reg), do: send(pid, content))
+ for {pid, reg} <- h,
+ do:
+ if(should_dispatch?(key, origin, should_dispatch_data, reg),
+ do: send(pid, content)
+ )
end)
end)
end
@@ -452,21 +580,25 @@ defmodule Nola.Irc.Connection do
defp should_dispatch_data({:irc, :trigger, _, msg}) do
should_dispatch_data(msg)
end
+
defp should_dispatch_data({:irc, :text, msg}) do
should_dispatch_data(msg)
end
+
defp should_dispatch_data(%Nola.Message{network: network, channel: channel}) do
Application.get_env(:nola, :channel_pubsub_plugin_ignore_list, %{})
|> Map.get("#{network}/#{channel}", [])
end
+
defp should_dispatch_data(_) do
[]
end
def should_dispatch?(_, _, [], _), do: true
+
def should_dispatch?(key, origin, data, reg) do
if plugin = Keyword.get(reg, :plugin) do
- #plugin != origin && !Enum.member?(data, plugin)
+ # plugin != origin && !Enum.member?(data, plugin)
!Enum.member?(data, plugin)
else
true
@@ -480,7 +612,7 @@ defmodule Nola.Irc.Connection do
def triggers, do: @triggers
for {trigger, name} <- @triggers do
- def extract_trigger(unquote(trigger)<>text) do
+ def extract_trigger(unquote(trigger) <> text) do
text = String.strip(text)
[trigger | args] = String.split(text, " ")
%Nola.Trigger{type: unquote(name), trigger: String.downcase(trigger), args: args}
@@ -495,15 +627,30 @@ defmodule Nola.Irc.Connection do
# 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 = Nola.Irc.Message.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, %Nola.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
+ defp irc_reply(state = %{client: client, network: network}, {target, _}, text)
+ when is_binary(text) or is_list(text) do
+ lines =
+ Nola.Irc.Message.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,
+ %Nola.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
@@ -548,12 +695,11 @@ defmodule Nola.Irc.Connection do
end
defp track_mode(network, channel, nick, mode) do
- Logger.warn("Unhandled track_mode: #{inspect {nick, mode}}")
+ Logger.warn("Unhandled track_mode: #{inspect({nick, mode})}")
:ok
end
defp server(%{conn: %{host: host, port: port}}) do
host <> ":" <> to_string(port)
end
-
end