diff --git a/config/config.exs b/config/config.exs index ba4426d..9a3633e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,94 +1,89 @@ # This file is responsible for configuring your application # and its dependencies with the aid of the Mix.Config module. # # This configuration file is loaded before any dependency and # is restricted to this project. use Mix.Config config :logger, level: :debug config :logger, :console, format: "$date $time [$level$levelpad] $metadata$message\n", metadata: :all # General application configuration config :lsg, namespace: LSG config :lsg, :data_path, "priv" config :lsg, :icecast_poll_interval, 600_000 config :ex_aws, region: "us-east-1", host: "s3.wasabisys.com", s3: [ host: "s3.wasabisys.com", region: "us-east-1", scheme: "https://" ] # Configures the endpoint config :lsg, LSGWeb.Endpoint, url: [host: "localhost"], secret_key_base: "cAFb7x2p/D7PdV8/C6Os18uygoD0FVQh3efNEFc5+5L529q3dofZtZye/BG12MRZ", render_errors: [view: LSGWeb.ErrorView, accepts: ~w(html json)], server: true, pubsub: [name: LSG.PubSub, adapter: Phoenix.PubSub.PG2] -# Configures Elixir's Logger -config :logger, :console, - format: "$time $metadata[$level] $message\n", - metadata: [:request_id] - config :mime, :types, %{"text/event-stream" => ["sse"]} # THIS IS NOT USED ANYMORE! # XXX: Make sure it is not used anymore and remove.. config :lsg, :irc, name: "irc bot", handlers: [ LSG.IRC.AdminHandler, #LSG.IRC.BroadcastHandler, #LSG.IRC.NpHandler, ], plugins: [ LSG.IRC.BasePlugin, LSG.IRC.TxtPlugin, LSG.IRC.CalcPlugin, LSG.IRC.DicePlugin, LSG.IRC.YouTubePlugin, LSG.IRC.WikipediaPlugin, LSG.IRC.KickRoulettePlugin, LSG.IRC.AlcoolismePlugin, LSG.IRC.QuatreCentVingtPlugin, LSG.IRC.LastFmPlugin, LSG.IRC.LinkPlugin, LSG.IRC.OutlinePlugin, LSG.IRC.StocksPlugin ] #admins: [ # # Format is {nick, user, host}. :_ for any value. #] #irc: [ # host: "irc.", # port: 6667, # nick: "`115ans", # user: "115ans", # name: "https://sys.115ans.net/irc" #] config :lsg, LSG.IRC.LastFmHandler, api_key: "x", api_secret: "x" config :lsg, LSG.IRC.YouTubeHandler, api_key: "x", invidious: "yewtu.be" config :mnesia, dir: '.mnesia/#{Mix.env}/#{node()}' # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env}.exs" diff --git a/lib/irc/puppet_connection.ex b/lib/irc/puppet_connection.ex new file mode 100644 index 0000000..88b4f8a --- /dev/null +++ b/lib/irc/puppet_connection.ex @@ -0,0 +1,157 @@ +defmodule IRC.PuppetConnection do + require Logger + @min_backoff :timer.seconds(5) + @max_backoff :timer.seconds(2*60) + @max_idle :timer.minutes(30) + + 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 send_message(account = %IRC.Account{id: account_id}, connection = %IRC.Connection{id: connection_id}, channel, text) do + pid = case IRC.PuppetConnection.Supervisor.start_child(account, connection) do + {:ok, pid} -> pid + {:error, {:already_started, pid}} -> pid + end + GenServer.cast(pid, {:send_message, channel, text}) + 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 + 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 + if conn.tls do + ExIRC.Client.connect_ssl!(client, conn.host, conn.port, [])#[{:ifaddr, {45,150,150,33}}]) + else + ExIRC.Client.connect!(client, conn.host, conn.port, [])#[{:ifaddr, {45,150,150,33}}]) + end + {:noreply, %{state | client: client}} + end + + def handle_continue(:connected, state) do + state = Enum.reduce(state.buffer, state, fn(b, state) -> + {:noreply, state} = handle_cast(b, state) + state + end) + {:noreply, %{state | buffer: []}} + end + + def handle_cast(cast = {:send_message, channel, text}, state = %{connected: false, buffer: buffer}) do + {:noreply, %{state | buffer: [cast | buffer]}} + end + + def handle_cast({:send_message, 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) + + 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 is 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 #{server}:#{port} #{inspect state}") + {_, backoff} = :backoff.succeed(state.backoff) + 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) + nick = "#{base_nick}[p]" + ExIRC.Client.logon(state.client, "", 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) + {: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 + +end diff --git a/lib/lsg/telegram_room.ex b/lib/lsg/telegram_room.ex index 8c228e1..f973c58 100644 --- a/lib/lsg/telegram_room.ex +++ b/lib/lsg/telegram_room.ex @@ -1,55 +1,53 @@ defmodule LSG.TelegramRoom do require Logger @behaviour Telegram.ChatBot alias Telegram.Api @impl Telegram.ChatBot def init(id) do token = Keyword.get(Application.get_env(:lsg, :telegram, []), :key) {:ok, chat} = Api.request(token, "getChat", chat_id: id) Logger.debug("Starting ChatBot for room #{id} \"#{chat["title"]}\"") [net, chan] = String.split(chat["title"], "/", parts: 2) case IRC.Connection.get_network(net, chan) do %IRC.Connection{} -> :global.register_name({__MODULE__, net, chan}, self()) {:ok, _} = Registry.register(IRC.PubSub, "#{net}/#{chan}:message", plugin: __MODULE__) {:ok, _} = Registry.register(IRC.PubSub, "#{net}/#{chan}:triggers", plugin: __MODULE__) err -> Logger.warn("Did not found telegram match for #{id} \"#{chat["title"]}\"") end {:ok, %{id: id, net: net, chan: chan}} end def handle_update(%{"message" => %{"from" => %{"id" => user_id}, "text" => text}}, _token, state) do account = IRC.Account.find_meta_account("telegram-id", user_id) - user = IRC.UserTrack.find_by_account(state.net, account) - nick = if(user, do: user.nick, else: account.name) - prefix = "<#{nick}> " - IRC.Connection.broadcast_message(state.net, state.chan, "#{prefix}#{text}") + connection = IRC.Connection.get_network(state.net) + IRC.PuppetConnection.send_message(account, connection, state.chan, text) {:ok, state} end def handle_update(update, token, state) do {:ok, state} end def handle_info({:irc, _, %IRC.Message{sender: %{nick: nick}, text: text}}, state) do LSG.Telegram.send_message(state.id, "<#{nick}> #{text}") {:ok, state} end def handle_info({:raw, lines}, state) when is_list(lines) do formatted = for l <- lines, into: <<>>, do: l <> "\n" LSG.Telegram.send_message(state.id, formatted) {:ok, state} end def handle_info({:raw, line}, state) do handle_info({:raw, [line]}, state) end def handle_info(info, state) do {:ok, state} end end diff --git a/lib/lsg_irc.ex b/lib/lsg_irc.ex index ba0828a..c2782ad 100644 --- a/lib/lsg_irc.ex +++ b/lib/lsg_irc.ex @@ -1,36 +1,37 @@ defmodule LSG.IRC do require Logger def application_childs do env = Application.get_env(:lsg, :irc) import Supervisor.Spec IRC.Connection.setup() IRC.Plugin.setup() # Probably just needed for migration #for plugin <- Application.get_env(:lsg, :irc)[:plugins], do: IRC.Plugin.declare(plugin) [ worker(Registry, [[keys: :duplicate, name: IRC.ConnectionPubSub]], id: :registr_irc_conn), worker(Registry, [[keys: :duplicate, name: IRC.PubSub]], id: :registry_irc), worker(IRC.Membership, []), worker(IRC.Account, []), worker(IRC.UserTrack.Storage, []), worker(IRC.Account.AccountPlugin, []), supervisor(IRC.Plugin.Supervisor, [], [name: IRC.Plugin.Supervisor]), supervisor(IRC.Connection.Supervisor, [], [name: IRC.Connection.Supervisor]), + supervisor(IRC.PuppetConnection.Supervisor, [], [name: IRC.PuppetConnection.Supervisor]), ] end def after_start() do # Start plugins first to let them get on connection events. Logger.debug("IRC.after_start - initializing plugins") IRC.Plugin.start_all() Logger.debug("IRC.after_start - initializing connections") IRC.Connection.start_all() Logger.debug("IRC.after_start - ok") end end diff --git a/lib/lsg_irc/say_plugin.ex b/lib/lsg_irc/say_plugin.ex index 690d0a6..c385e77 100644 --- a/lib/lsg_irc/say_plugin.ex +++ b/lib/lsg_irc/say_plugin.ex @@ -1,72 +1,74 @@ defmodule LSG.IRC.SayPlugin do def irc_doc do """ # say Say something... * **!say `` ``** say something on `channel` * **!asay `` ``** same but anonymously You must be a member of the channel. """ end def start_link() do GenServer.start_link(__MODULE__, [], name: __MODULE__) end def init([]) do regopts = [type: __MODULE__] {:ok, _} = Registry.register(IRC.PubSub, "trigger:say", regopts) {:ok, _} = Registry.register(IRC.PubSub, "trigger:asay", regopts) {:ok, _} = Registry.register(IRC.PubSub, "message:private", regopts) {:ok, nil} end def handle_info({:irc, :trigger, "say", m = %{trigger: %{type: :bang, args: [target | text]}}}, state) do text = Enum.join(text, " ") say_for(m.account, target, text, true) {:noreply, state} end def handle_info({:irc, :trigger, "asay", m = %{trigger: %{type: :bang, args: [target | text]}}}, state) do text = Enum.join(text, " ") say_for(m.account, target, text, false) {:noreply, state} end def handle_info({:irc, :text, m = %{text: "say "<>rest}}, state) do case String.split(rest, " ", parts: 2) do [target, text] -> say_for(m.account, target, text, true) _ -> nil end {:noreply, state} end def handle_info({:irc, :text, m = %{text: "asay "<>rest}}, state) do case String.split(rest, " ", parts: 2) do [target, text] -> say_for(m.account, target, text, false) _ -> nil end {:noreply, state} end def handle_info(_, state) do {:noreply, state} end defp say_for(account, target, text, with_nick?) do for {net, chan} <- IRC.Membership.of_account(account) do chan2 = String.replace(chan, "#", "") if (target == "#{net}/#{chan}" || target == "#{net}/#{chan2}" || target == chan || target == chan2) do - user = IRC.UserTrack.find_by_account(net, account) - nick = if(user, do: user.nick, else: account.name) - prefix = if(with_nick?, do: "<#{nick}> ", else: "") - IRC.Connection.broadcast_message(net, chan, "#{prefix}#{text}") + if with_nick? do + connection = IRC.Connection.get_network(net) + IRC.PuppetConnection.send_message(account, connection, chan, text) + else + IRC.Connection.broadcast_message(net, chan, text) + end end end end end