From e9d056ce119969e27217343284b94f964eb99c58 Mon Sep 17 00:00:00 2001 From: href Date: Sat, 11 Sep 2021 00:31:07 +0200 Subject: reorg --- lib/irc.ex | 76 -------------- lib/irc/irc.ex | 76 ++++++++++++++ lib/lsg.ex | 15 --- lib/lsg/lsg.ex | 15 +++ lib/lsg/telegram.ex | 232 ------------------------------------------- lib/lsg/telegram_room.ex | 130 ------------------------ lib/lsg_irc.ex | 37 ------- lib/lsg_irc/lsg_irc.ex | 37 +++++++ lib/lsg_telegram/room.ex | 130 ++++++++++++++++++++++++ lib/lsg_telegram/telegram.ex | 232 +++++++++++++++++++++++++++++++++++++++++++ lib/lsg_web.ex | 99 ------------------ lib/lsg_web/lsg_web.ex | 99 ++++++++++++++++++ 12 files changed, 589 insertions(+), 589 deletions(-) delete mode 100644 lib/irc.ex create mode 100644 lib/irc/irc.ex delete mode 100644 lib/lsg.ex create mode 100644 lib/lsg/lsg.ex delete mode 100644 lib/lsg/telegram.ex delete mode 100644 lib/lsg/telegram_room.ex delete mode 100644 lib/lsg_irc.ex create mode 100644 lib/lsg_irc/lsg_irc.ex create mode 100644 lib/lsg_telegram/room.ex create mode 100644 lib/lsg_telegram/telegram.ex delete mode 100644 lib/lsg_web.ex create mode 100644 lib/lsg_web/lsg_web.ex (limited to 'lib') diff --git a/lib/irc.ex b/lib/irc.ex deleted file mode 100644 index e063244..0000000 --- a/lib/irc.ex +++ /dev/null @@ -1,76 +0,0 @@ -defmodule IRC do - - defmodule Message do - defstruct [:text, - {:transport, :irc}, - :network, - :account, - :sender, - :channel, - :trigger, - :replyfun, - :at, - {:meta, %{}} - ] - end - defmodule Trigger do - 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} <- Application.get_env(:lsg, :irc, [])[: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/irc.ex b/lib/irc/irc.ex new file mode 100644 index 0000000..e063244 --- /dev/null +++ b/lib/irc/irc.ex @@ -0,0 +1,76 @@ +defmodule IRC do + + defmodule Message do + defstruct [:text, + {:transport, :irc}, + :network, + :account, + :sender, + :channel, + :trigger, + :replyfun, + :at, + {:meta, %{}} + ] + end + defmodule Trigger do + 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} <- Application.get_env(:lsg, :irc, [])[: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/lsg.ex b/lib/lsg.ex deleted file mode 100644 index dc0f641..0000000 --- a/lib/lsg.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule LSG do - - def data_path(suffix) do - Path.join(data_path(), suffix) - end - - def data_path do - Application.get_env(:lsg, :data_path) - end - - def version do - Application.spec(:lsg)[:vsn] - end - -end diff --git a/lib/lsg/lsg.ex b/lib/lsg/lsg.ex new file mode 100644 index 0000000..dc0f641 --- /dev/null +++ b/lib/lsg/lsg.ex @@ -0,0 +1,15 @@ +defmodule LSG do + + def data_path(suffix) do + Path.join(data_path(), suffix) + end + + def data_path do + Application.get_env(:lsg, :data_path) + end + + def version do + Application.spec(:lsg)[:vsn] + end + +end diff --git a/lib/lsg/telegram.ex b/lib/lsg/telegram.ex deleted file mode 100644 index 63940dc..0000000 --- a/lib/lsg/telegram.ex +++ /dev/null @@ -1,232 +0,0 @@ -defmodule LSG.Telegram do - require Logger - @behaviour Telegram.ChatBot - - def my_path() do - "https://t.me/beauttebot" - end - - def send_message(id, text, md2 \\ false) do - md = if md2, do: "MarkdownV2", else: "Markdown" - token = Keyword.get(Application.get_env(:lsg, :telegram, []), :key) - Telegram.Bot.ChatBot.Chat.Session.Supervisor.start_child(LSG.Telegram, id) - Telegram.Api.request(token, "sendMessage", chat_id: id, text: text, parse_mode: "Markdown") - end - - @impl Telegram.ChatBot - def init(chat_id) when chat_id < 0 do - {:ok, state} = LSG.TelegramRoom.init(chat_id) - {:ok, %{room_state: state}} - end - def init(chat_id) do - Logger.info("Telegram session starting: #{chat_id}") - account = IRC.Account.find_meta_account("telegram-id", chat_id) - account_id = if account, do: account.id - {:ok, %{account: account_id}} - end - - @impl Telegram.ChatBot - def handle_update(update, token, %{room_state: room_state}) do - {:ok, room_state} = LSG.TelegramRoom.handle_update(update, token, room_state) - {:ok, %{room_state: room_state}} - end - - def handle_update(%{"message" => m = %{"chat" => %{"type" => "private"}, "text" => text = "/start"<>_}}, _token, state) do - text = "*Welcome to beautte!*\n\nQuery the bot on IRC and say \"enable-telegram\" to continue." - send_message(m["chat"]["id"], text) - {:ok, %{account: nil}} - end - - def handle_update(%{"message" => m = %{"chat" => %{"type" => "private"}, "text" => text = "/enable"<>_}}, _token, state) do - key = case String.split(text, " ") do - ["/enable", key | _] -> key - _ -> "nil" - end - - #Handled message "1247435154:AAGnSSCnySn0RuVxy_SUcDEoOX_rbF6vdq0" %{"message" => - # %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"}, - # "date" => 1591027272, "entities" => - # [%{"length" => 7, "offset" => 0, "type" => "bot_command"}], - # "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"}, - # "message_id" => 11, "text" => "/enable salope"}, "update_id" => 764148578} - account = IRC.Account.find_meta_account("telegram-validation-code", String.downcase(key)) - text = if account do - net = IRC.Account.get_meta(account, "telegram-validation-target") - IRC.Account.put_meta(account, "telegram-id", m["chat"]["id"]) - IRC.Account.put_meta(account, "telegram-username", m["chat"]["username"]) - IRC.Account.put_meta(account, "telegram-username", m["chat"]["username"]) - IRC.Account.delete_meta(account, "telegram-validation-code") - IRC.Account.delete_meta(account, "telegram-validation-target") - IRC.Connection.broadcast_message(net, account, "Telegram #{m["chat"]["username"]} account added!") - "Yay! Linked to account **#{account.name}**." - else - "Token invalid" - end - send_message(m["chat"]["id"], text) - {:ok, %{account: account.id}} - end - - #[debug] Unhandled update: %{"message" => - # %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"}, - # "date" => 1591096015, - # "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"}, - # "message_id" => 29, - # "photo" => [ - # %{"file_id" => "AgACAgQAAxkBAAMdXtYyz4RQqLcpOlR6xKK3w3NayHAAAnCzMRuL4bFSgl_cTXMl4m5G_T0kXQADAQADAgADbQADZVMBAAEaBA", - # "file_size" => 9544, "file_unique_id" => "AQADRv09JF0AA2VTAQAB", "height" => 95, "width" => 320}, - # %{"file_id" => "AgACAgQAAxkBAAMdXtYyz4RQqLcpOlR6xKK3w3NayHAAAnCzMRuL4bFSgl_cTXMl4m5G_T0kXQADAQADAgADeAADZFMBAAEaBA", - # "file_size" => 21420, "file_unique_id" => "AQADRv09JF0AA2RTAQAB", "height" => 148, "width" => 501}]}, - # "update_id" => 218161546} - - for type <- ~w(photo voice video document animation) do - def handle_update(data = %{"message" => %{unquote(type) => _}}, token, state) do - start_upload(unquote(type), data, token, state) - end - end - - #[debug] Unhandled update: %{"callback_query" => - # %{ - # "chat_instance" => "-7948978714441865930", "data" => "evolu.net/#dmz", - # "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"}, - # "id" => "8913804780149600", - # "message" => %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"}, - # "date" => 1591098553, "from" => %{"first_name" => "devbeautte", "id" => 1293058838, "is_bot" => true, "username" => "devbeauttebot"}, - # "message_id" => 62, - # "reply_markup" => %{"inline_keyboard" => [[%{"callback_data" => "random/#", "text" => "random/#"}, - # %{"callback_data" => "evolu.net/#dmz", "text" => "evolu.net/#dmz"}]]}, - # "text" => "Where should I send the file?"} - # } - # , "update_id" => 218161568} - - #def handle_update(t, %{"callback_query" => cb = %{"data" => "resend", "id" => id, "message" => m = %{"message_id" => m_id, "chat" => %{"id" => chat_id}, "reply_to_message" => op}}}) do - #end - - def handle_update(%{"callback_query" => cb = %{"data" => "start-upload:"<>target, "id" => id, "message" => m = %{"message_id" => m_id, "chat" => %{"id" => chat_id}, "reply_to_message" => op}}}, t, state) do - account = IRC.Account.find_meta_account("telegram-id", chat_id) - if account do - target = case String.split(target, "/") do - ["everywhere"] -> IRC.Membership.of_account(account) - [net, chan] -> [{net, chan}] - end - Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Processing...", reply_markup: %{}) - - {content, type} = cond do - op["photo"] -> {op["photo"], ""} - op["voice"] -> {op["voice"], " a voice message"} - op["video"] -> {op["video"], ""} - op["document"] -> {op["document"], ""} - op["animation"] -> {op["animation"], ""} - end - - file = if is_list(content) && Enum.count(content) > 1 do - Enum.sort_by(content, fn(p) -> p["file_size"] end, &>=/2) - |> List.first() - else - content - end - file_id = file["file_id"] - file_unique_id = file["file_unique_id"] - text = if(op["caption"], do: ": "<> op["caption"] <> "", else: "") - resend = %{"inline_keyboard" => [ [%{"text" => "re-share", "callback_data" => "resend"}] ]} - spawn(fn() -> - with \ - {:ok, file} <- Telegram.Api.request(t, "getFile", file_id: file_id), - path = "https://api.telegram.org/file/bot#{t}/#{file["file_path"]}", - {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(path), - <> = body, - {:ok, magic} <- GenMagic.Pool.perform(LSG.GenMagic, {:bytes, smol_body}), - bucket = Application.get_env(:lsg, :s3, []) |> Keyword.get(:bucket), - ext = Path.extname(file["file_path"]), - s3path = "#{account.id}/#{file_unique_id}#{ext}", - Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "*Uploading...*", reply_markup: %{}, parse_mode: "MarkdownV2"), - s3req = ExAws.S3.put_object(bucket, s3path, body, acl: :public_read, content_type: magic.mime_type), - {:ok, _} <- ExAws.request(s3req) - do - path = LSGWeb.Router.Helpers.url(LSGWeb.Endpoint) <> "/files/#{s3path}" - sent = for {net, chan} <- target do - txt = "sent#{type}#{text} #{path}" - IRC.send_message_as(account, net, chan, txt) - "#{net}/#{chan}" - end - if caption = op["caption"], do: as_irc_message(chat_id, caption, account) - text = "Sent on " <> Enum.join(sent, ", ") <> " !" - Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "_Sent!_", reply_markup: %{}, parse_mode: "MarkdownV2") - else - error -> - Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Something failed.", reply_markup: %{}, parse_mode: "MarkdownV2") - Logger.error("Failed upload from Telegram: #{inspect error}") - end - end) - end - {:ok, state} - end - - def handle_update(%{"message" => m = %{"chat" => %{"id" => id, "type" => "private"}, "text" => text}}, _, state) do - account = IRC.Account.find_meta_account("telegram-id", id) - if account do - as_irc_message(id, text, account) - end - {:ok, state} - end - - def handle_update(m, _, state) do - Logger.debug("Unhandled update: #{inspect m}") - {:ok, state} - end - - @impl Telegram.ChatBot - def handle_info(info, %{room_state: room_state}) do - {:ok, room_state} = LSG.TelegramRoom.handle_info(info, room_state) - {:ok, %{room_state: room_state}} - end - - def handle_info(_info, state) do - {:ok, state} - end - - defp as_irc_message(id, text, account) do - reply_fun = fn(text) -> send_message(id, text) end - trigger_text = cond do - String.starts_with?(text, "/") -> - "/"<>text = text - "!"<>text - Enum.any?(IRC.Connection.triggers(), fn({trigger, _}) -> String.starts_with?(text, trigger) end) -> - text - true -> - "!"<>text - end - message = %IRC.Message{ - transport: :telegram, - network: "telegram", - channel: nil, - text: text, - account: account, - sender: %ExIRC.SenderInfo{nick: account.name}, - replyfun: reply_fun, - trigger: IRC.Connection.extract_trigger(trigger_text), - at: nil - } - IRC.Connection.publish(message, ["messages:private", "messages:telegram", "telegram/#{account.id}:messages"]) - message - end - - defp start_upload(_type, %{"message" => m = %{"chat" => %{"id" => id, "type" => "private"}}}, token, state) do - account = IRC.Account.find_meta_account("telegram-id", id) - if account do - text = if(m["text"], do: m["text"], else: nil) - targets = IRC.Membership.of_account(account) - |> Enum.map(fn({net, chan}) -> "#{net}/#{chan}" end) - |> Enum.map(fn(i) -> %{"text" => i, "callback_data" => "start-upload:#{i}"} end) - kb = if Enum.count(targets) > 1 do - [%{"text" => "everywhere", "callback_data" => "start-upload:everywhere"}] ++ targets - else - targets - end - |> Enum.chunk_every(2) - keyboard = %{"inline_keyboard" => kb} - Telegram.Api.request(token, "sendMessage", chat_id: id, text: "Where should I send this file?", reply_markup: keyboard, reply_to_message_id: m["message_id"], parse_mode: "MarkdownV2") - end - {:ok, state} - end - -end diff --git a/lib/lsg/telegram_room.ex b/lib/lsg/telegram_room.ex deleted file mode 100644 index f7e42c6..0000000 --- a/lib/lsg/telegram_room.ex +++ /dev/null @@ -1,130 +0,0 @@ -defmodule LSG.TelegramRoom do - require Logger - @behaviour Telegram.ChatBot - alias Telegram.Api - - def dets() do - (LSG.data_path() <> "/telegram-rooms.dets") |> String.to_charlist() - end - - def setup() do - {:ok, _} = :dets.open_file(dets(), []) - :ok - end - - def after_start() do - rooms = :dets.foldl(fn({id, _, _}, acc) -> [id | acc] end, [], dets()) - for id <- rooms, do: Telegram.Bot.ChatBot.Chat.Session.Supervisor.start_child(LSG.Telegram, id) - end - - @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{} -> - {:ok, _} = Registry.register(IRC.PubSub, "#{net}/#{chan}:messages", plugin: __MODULE__) - {:ok, _} = Registry.register(IRC.PubSub, "#{net}/#{chan}:triggers", plugin: __MODULE__) - {:ok, _} = Registry.register(IRC.PubSub, "#{net}/#{chan}:outputs", plugin: __MODULE__) - err -> - Logger.warn("Did not found telegram match for #{id} \"#{chat["title"]}\"") - end - :dets.insert(dets(), {id, net, chan}) - {: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) - connection = IRC.Connection.get_network(state.net) - IRC.send_message_as(account, state.net, state.chan, text, true) - {:ok, state} - end - - def handle_update(data = %{"message" => %{"from" => %{"id" => user_id}, "location" => %{"latitude" => lat, "longitude" => lon}}}, _token, state) do - account = IRC.Account.find_meta_account("telegram-id", user_id) - connection = IRC.Connection.get_network(state.net) - IRC.send_message_as(account, state.net, state.chan, "@ #{lat}, #{lon}", true) - {:ok, state} - end - - for type <- ~w(photo voice video document animation) do - def handle_update(data = %{"message" => %{unquote(type) => _}}, token, state) do - upload(unquote(type), data, token, state) - end - end - - def handle_update(update, token, state) do - {:ok, state} - end - - def handle_info({:irc, _, _, message}, state) do - handle_info({:irc, nil, message}, state) - end - - def handle_info({:irc, _, message = %IRC.Message{sender: %{nick: nick}, text: text}}, state) do - if Map.get(message.meta, :from) == self() do - else - body = if Map.get(message.meta, :self), do: text, else: "<#{nick}> #{text}" - LSG.Telegram.send_message(state.id, body) - end - {:ok, state} - end - - def handle_info(info, state) do - Logger.info("UNhandled #{inspect info}") - {:ok, state} - end - - defp upload(_type, %{"message" => m = %{"chat" => %{"id" => chat_id}, "from" => %{"id" => user_id}}}, token, state) do - account = IRC.Account.find_meta_account("telegram-id", user_id) - if account do - {content, type} = cond do - m["photo"] -> {m["photo"], "photo"} - m["voice"] -> {m["voice"], "voice message"} - m["video"] -> {m["video"], "video"} - m["document"] -> {m["document"], "file"} - m["animation"] -> {m["animation"], "gif"} - end - - file = if is_list(content) && Enum.count(content) > 1 do - Enum.sort_by(content, fn(p) -> p["file_size"] end, &>=/2) - |> List.first() - else - content - end - - file_id = file["file_id"] - file_unique_id = file["file_unique_id"] - text = if(m["caption"], do: m["caption"] <> " ", else: "") - - spawn(fn() -> - with \ - {:ok, file} <- Telegram.Api.request(token, "getFile", file_id: file_id), - path = "https://api.telegram.org/file/bot#{token}/#{file["file_path"]}", - {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(path), - <> = body, - {:ok, magic} <- GenMagic.Pool.perform(LSG.GenMagic, {:bytes, smol_body}), - bucket = Application.get_env(:lsg, :s3, []) |> Keyword.get(:bucket), - ext = Path.extname(file["file_path"]), - s3path = "#{account.id}/#{file_unique_id}#{ext}", - s3req = ExAws.S3.put_object(bucket, s3path, body, acl: :public_read, content_type: magic.mime_type), - {:ok, _} <- ExAws.request(s3req) - do - path = LSGWeb.Router.Helpers.url(LSGWeb.Endpoint) <> "/files/#{s3path}" - txt = "#{type}: #{text}#{path}" - connection = IRC.Connection.get_network(state.net) - IRC.send_message_as(account, state.net, state.chan, txt, true) - else - error -> - Telegram.Api.request(token, "sendMessage", chat_id: chat_id, text: "File upload failed, sorry.") - Logger.error("Failed upload from Telegram: #{inspect error}") - end - end) - - {:ok, state} - end - end - -end diff --git a/lib/lsg_irc.ex b/lib/lsg_irc.ex deleted file mode 100644 index c2782ad..0000000 --- a/lib/lsg_irc.ex +++ /dev/null @@ -1,37 +0,0 @@ -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/lsg_irc.ex b/lib/lsg_irc/lsg_irc.ex new file mode 100644 index 0000000..c2782ad --- /dev/null +++ b/lib/lsg_irc/lsg_irc.ex @@ -0,0 +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_telegram/room.ex b/lib/lsg_telegram/room.ex new file mode 100644 index 0000000..f7e42c6 --- /dev/null +++ b/lib/lsg_telegram/room.ex @@ -0,0 +1,130 @@ +defmodule LSG.TelegramRoom do + require Logger + @behaviour Telegram.ChatBot + alias Telegram.Api + + def dets() do + (LSG.data_path() <> "/telegram-rooms.dets") |> String.to_charlist() + end + + def setup() do + {:ok, _} = :dets.open_file(dets(), []) + :ok + end + + def after_start() do + rooms = :dets.foldl(fn({id, _, _}, acc) -> [id | acc] end, [], dets()) + for id <- rooms, do: Telegram.Bot.ChatBot.Chat.Session.Supervisor.start_child(LSG.Telegram, id) + end + + @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{} -> + {:ok, _} = Registry.register(IRC.PubSub, "#{net}/#{chan}:messages", plugin: __MODULE__) + {:ok, _} = Registry.register(IRC.PubSub, "#{net}/#{chan}:triggers", plugin: __MODULE__) + {:ok, _} = Registry.register(IRC.PubSub, "#{net}/#{chan}:outputs", plugin: __MODULE__) + err -> + Logger.warn("Did not found telegram match for #{id} \"#{chat["title"]}\"") + end + :dets.insert(dets(), {id, net, chan}) + {: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) + connection = IRC.Connection.get_network(state.net) + IRC.send_message_as(account, state.net, state.chan, text, true) + {:ok, state} + end + + def handle_update(data = %{"message" => %{"from" => %{"id" => user_id}, "location" => %{"latitude" => lat, "longitude" => lon}}}, _token, state) do + account = IRC.Account.find_meta_account("telegram-id", user_id) + connection = IRC.Connection.get_network(state.net) + IRC.send_message_as(account, state.net, state.chan, "@ #{lat}, #{lon}", true) + {:ok, state} + end + + for type <- ~w(photo voice video document animation) do + def handle_update(data = %{"message" => %{unquote(type) => _}}, token, state) do + upload(unquote(type), data, token, state) + end + end + + def handle_update(update, token, state) do + {:ok, state} + end + + def handle_info({:irc, _, _, message}, state) do + handle_info({:irc, nil, message}, state) + end + + def handle_info({:irc, _, message = %IRC.Message{sender: %{nick: nick}, text: text}}, state) do + if Map.get(message.meta, :from) == self() do + else + body = if Map.get(message.meta, :self), do: text, else: "<#{nick}> #{text}" + LSG.Telegram.send_message(state.id, body) + end + {:ok, state} + end + + def handle_info(info, state) do + Logger.info("UNhandled #{inspect info}") + {:ok, state} + end + + defp upload(_type, %{"message" => m = %{"chat" => %{"id" => chat_id}, "from" => %{"id" => user_id}}}, token, state) do + account = IRC.Account.find_meta_account("telegram-id", user_id) + if account do + {content, type} = cond do + m["photo"] -> {m["photo"], "photo"} + m["voice"] -> {m["voice"], "voice message"} + m["video"] -> {m["video"], "video"} + m["document"] -> {m["document"], "file"} + m["animation"] -> {m["animation"], "gif"} + end + + file = if is_list(content) && Enum.count(content) > 1 do + Enum.sort_by(content, fn(p) -> p["file_size"] end, &>=/2) + |> List.first() + else + content + end + + file_id = file["file_id"] + file_unique_id = file["file_unique_id"] + text = if(m["caption"], do: m["caption"] <> " ", else: "") + + spawn(fn() -> + with \ + {:ok, file} <- Telegram.Api.request(token, "getFile", file_id: file_id), + path = "https://api.telegram.org/file/bot#{token}/#{file["file_path"]}", + {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(path), + <> = body, + {:ok, magic} <- GenMagic.Pool.perform(LSG.GenMagic, {:bytes, smol_body}), + bucket = Application.get_env(:lsg, :s3, []) |> Keyword.get(:bucket), + ext = Path.extname(file["file_path"]), + s3path = "#{account.id}/#{file_unique_id}#{ext}", + s3req = ExAws.S3.put_object(bucket, s3path, body, acl: :public_read, content_type: magic.mime_type), + {:ok, _} <- ExAws.request(s3req) + do + path = LSGWeb.Router.Helpers.url(LSGWeb.Endpoint) <> "/files/#{s3path}" + txt = "#{type}: #{text}#{path}" + connection = IRC.Connection.get_network(state.net) + IRC.send_message_as(account, state.net, state.chan, txt, true) + else + error -> + Telegram.Api.request(token, "sendMessage", chat_id: chat_id, text: "File upload failed, sorry.") + Logger.error("Failed upload from Telegram: #{inspect error}") + end + end) + + {:ok, state} + end + end + +end diff --git a/lib/lsg_telegram/telegram.ex b/lib/lsg_telegram/telegram.ex new file mode 100644 index 0000000..63940dc --- /dev/null +++ b/lib/lsg_telegram/telegram.ex @@ -0,0 +1,232 @@ +defmodule LSG.Telegram do + require Logger + @behaviour Telegram.ChatBot + + def my_path() do + "https://t.me/beauttebot" + end + + def send_message(id, text, md2 \\ false) do + md = if md2, do: "MarkdownV2", else: "Markdown" + token = Keyword.get(Application.get_env(:lsg, :telegram, []), :key) + Telegram.Bot.ChatBot.Chat.Session.Supervisor.start_child(LSG.Telegram, id) + Telegram.Api.request(token, "sendMessage", chat_id: id, text: text, parse_mode: "Markdown") + end + + @impl Telegram.ChatBot + def init(chat_id) when chat_id < 0 do + {:ok, state} = LSG.TelegramRoom.init(chat_id) + {:ok, %{room_state: state}} + end + def init(chat_id) do + Logger.info("Telegram session starting: #{chat_id}") + account = IRC.Account.find_meta_account("telegram-id", chat_id) + account_id = if account, do: account.id + {:ok, %{account: account_id}} + end + + @impl Telegram.ChatBot + def handle_update(update, token, %{room_state: room_state}) do + {:ok, room_state} = LSG.TelegramRoom.handle_update(update, token, room_state) + {:ok, %{room_state: room_state}} + end + + def handle_update(%{"message" => m = %{"chat" => %{"type" => "private"}, "text" => text = "/start"<>_}}, _token, state) do + text = "*Welcome to beautte!*\n\nQuery the bot on IRC and say \"enable-telegram\" to continue." + send_message(m["chat"]["id"], text) + {:ok, %{account: nil}} + end + + def handle_update(%{"message" => m = %{"chat" => %{"type" => "private"}, "text" => text = "/enable"<>_}}, _token, state) do + key = case String.split(text, " ") do + ["/enable", key | _] -> key + _ -> "nil" + end + + #Handled message "1247435154:AAGnSSCnySn0RuVxy_SUcDEoOX_rbF6vdq0" %{"message" => + # %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"}, + # "date" => 1591027272, "entities" => + # [%{"length" => 7, "offset" => 0, "type" => "bot_command"}], + # "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"}, + # "message_id" => 11, "text" => "/enable salope"}, "update_id" => 764148578} + account = IRC.Account.find_meta_account("telegram-validation-code", String.downcase(key)) + text = if account do + net = IRC.Account.get_meta(account, "telegram-validation-target") + IRC.Account.put_meta(account, "telegram-id", m["chat"]["id"]) + IRC.Account.put_meta(account, "telegram-username", m["chat"]["username"]) + IRC.Account.put_meta(account, "telegram-username", m["chat"]["username"]) + IRC.Account.delete_meta(account, "telegram-validation-code") + IRC.Account.delete_meta(account, "telegram-validation-target") + IRC.Connection.broadcast_message(net, account, "Telegram #{m["chat"]["username"]} account added!") + "Yay! Linked to account **#{account.name}**." + else + "Token invalid" + end + send_message(m["chat"]["id"], text) + {:ok, %{account: account.id}} + end + + #[debug] Unhandled update: %{"message" => + # %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"}, + # "date" => 1591096015, + # "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"}, + # "message_id" => 29, + # "photo" => [ + # %{"file_id" => "AgACAgQAAxkBAAMdXtYyz4RQqLcpOlR6xKK3w3NayHAAAnCzMRuL4bFSgl_cTXMl4m5G_T0kXQADAQADAgADbQADZVMBAAEaBA", + # "file_size" => 9544, "file_unique_id" => "AQADRv09JF0AA2VTAQAB", "height" => 95, "width" => 320}, + # %{"file_id" => "AgACAgQAAxkBAAMdXtYyz4RQqLcpOlR6xKK3w3NayHAAAnCzMRuL4bFSgl_cTXMl4m5G_T0kXQADAQADAgADeAADZFMBAAEaBA", + # "file_size" => 21420, "file_unique_id" => "AQADRv09JF0AA2RTAQAB", "height" => 148, "width" => 501}]}, + # "update_id" => 218161546} + + for type <- ~w(photo voice video document animation) do + def handle_update(data = %{"message" => %{unquote(type) => _}}, token, state) do + start_upload(unquote(type), data, token, state) + end + end + + #[debug] Unhandled update: %{"callback_query" => + # %{ + # "chat_instance" => "-7948978714441865930", "data" => "evolu.net/#dmz", + # "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"}, + # "id" => "8913804780149600", + # "message" => %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"}, + # "date" => 1591098553, "from" => %{"first_name" => "devbeautte", "id" => 1293058838, "is_bot" => true, "username" => "devbeauttebot"}, + # "message_id" => 62, + # "reply_markup" => %{"inline_keyboard" => [[%{"callback_data" => "random/#", "text" => "random/#"}, + # %{"callback_data" => "evolu.net/#dmz", "text" => "evolu.net/#dmz"}]]}, + # "text" => "Where should I send the file?"} + # } + # , "update_id" => 218161568} + + #def handle_update(t, %{"callback_query" => cb = %{"data" => "resend", "id" => id, "message" => m = %{"message_id" => m_id, "chat" => %{"id" => chat_id}, "reply_to_message" => op}}}) do + #end + + def handle_update(%{"callback_query" => cb = %{"data" => "start-upload:"<>target, "id" => id, "message" => m = %{"message_id" => m_id, "chat" => %{"id" => chat_id}, "reply_to_message" => op}}}, t, state) do + account = IRC.Account.find_meta_account("telegram-id", chat_id) + if account do + target = case String.split(target, "/") do + ["everywhere"] -> IRC.Membership.of_account(account) + [net, chan] -> [{net, chan}] + end + Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Processing...", reply_markup: %{}) + + {content, type} = cond do + op["photo"] -> {op["photo"], ""} + op["voice"] -> {op["voice"], " a voice message"} + op["video"] -> {op["video"], ""} + op["document"] -> {op["document"], ""} + op["animation"] -> {op["animation"], ""} + end + + file = if is_list(content) && Enum.count(content) > 1 do + Enum.sort_by(content, fn(p) -> p["file_size"] end, &>=/2) + |> List.first() + else + content + end + file_id = file["file_id"] + file_unique_id = file["file_unique_id"] + text = if(op["caption"], do: ": "<> op["caption"] <> "", else: "") + resend = %{"inline_keyboard" => [ [%{"text" => "re-share", "callback_data" => "resend"}] ]} + spawn(fn() -> + with \ + {:ok, file} <- Telegram.Api.request(t, "getFile", file_id: file_id), + path = "https://api.telegram.org/file/bot#{t}/#{file["file_path"]}", + {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(path), + <> = body, + {:ok, magic} <- GenMagic.Pool.perform(LSG.GenMagic, {:bytes, smol_body}), + bucket = Application.get_env(:lsg, :s3, []) |> Keyword.get(:bucket), + ext = Path.extname(file["file_path"]), + s3path = "#{account.id}/#{file_unique_id}#{ext}", + Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "*Uploading...*", reply_markup: %{}, parse_mode: "MarkdownV2"), + s3req = ExAws.S3.put_object(bucket, s3path, body, acl: :public_read, content_type: magic.mime_type), + {:ok, _} <- ExAws.request(s3req) + do + path = LSGWeb.Router.Helpers.url(LSGWeb.Endpoint) <> "/files/#{s3path}" + sent = for {net, chan} <- target do + txt = "sent#{type}#{text} #{path}" + IRC.send_message_as(account, net, chan, txt) + "#{net}/#{chan}" + end + if caption = op["caption"], do: as_irc_message(chat_id, caption, account) + text = "Sent on " <> Enum.join(sent, ", ") <> " !" + Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "_Sent!_", reply_markup: %{}, parse_mode: "MarkdownV2") + else + error -> + Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Something failed.", reply_markup: %{}, parse_mode: "MarkdownV2") + Logger.error("Failed upload from Telegram: #{inspect error}") + end + end) + end + {:ok, state} + end + + def handle_update(%{"message" => m = %{"chat" => %{"id" => id, "type" => "private"}, "text" => text}}, _, state) do + account = IRC.Account.find_meta_account("telegram-id", id) + if account do + as_irc_message(id, text, account) + end + {:ok, state} + end + + def handle_update(m, _, state) do + Logger.debug("Unhandled update: #{inspect m}") + {:ok, state} + end + + @impl Telegram.ChatBot + def handle_info(info, %{room_state: room_state}) do + {:ok, room_state} = LSG.TelegramRoom.handle_info(info, room_state) + {:ok, %{room_state: room_state}} + end + + def handle_info(_info, state) do + {:ok, state} + end + + defp as_irc_message(id, text, account) do + reply_fun = fn(text) -> send_message(id, text) end + trigger_text = cond do + String.starts_with?(text, "/") -> + "/"<>text = text + "!"<>text + Enum.any?(IRC.Connection.triggers(), fn({trigger, _}) -> String.starts_with?(text, trigger) end) -> + text + true -> + "!"<>text + end + message = %IRC.Message{ + transport: :telegram, + network: "telegram", + channel: nil, + text: text, + account: account, + sender: %ExIRC.SenderInfo{nick: account.name}, + replyfun: reply_fun, + trigger: IRC.Connection.extract_trigger(trigger_text), + at: nil + } + IRC.Connection.publish(message, ["messages:private", "messages:telegram", "telegram/#{account.id}:messages"]) + message + end + + defp start_upload(_type, %{"message" => m = %{"chat" => %{"id" => id, "type" => "private"}}}, token, state) do + account = IRC.Account.find_meta_account("telegram-id", id) + if account do + text = if(m["text"], do: m["text"], else: nil) + targets = IRC.Membership.of_account(account) + |> Enum.map(fn({net, chan}) -> "#{net}/#{chan}" end) + |> Enum.map(fn(i) -> %{"text" => i, "callback_data" => "start-upload:#{i}"} end) + kb = if Enum.count(targets) > 1 do + [%{"text" => "everywhere", "callback_data" => "start-upload:everywhere"}] ++ targets + else + targets + end + |> Enum.chunk_every(2) + keyboard = %{"inline_keyboard" => kb} + Telegram.Api.request(token, "sendMessage", chat_id: id, text: "Where should I send this file?", reply_markup: keyboard, reply_to_message_id: m["message_id"], parse_mode: "MarkdownV2") + end + {:ok, state} + end + +end diff --git a/lib/lsg_web.ex b/lib/lsg_web.ex deleted file mode 100644 index 3d9ab9a..0000000 --- a/lib/lsg_web.ex +++ /dev/null @@ -1,99 +0,0 @@ -defmodule LSGWeb do - @moduledoc """ - The entrypoint for defining your web interface, such - as controllers, views, channels and so on. - - This can be used in your application as: - - use LSGWeb, :controller - use LSGWeb, :view - - The definitions below will be executed for every view, - controller, etc, so keep them short and clean, focused - on imports, uses and aliases. - - Do NOT define functions inside the quoted expressions - below. Instead, define any helper function in modules - and import those modules here. - """ - - def format_chan("##") do - "♯♯" - end - - def format_chan("#") do - "♯" - end - - def format_chan("#"<>chan) do - chan - end - - def format_chan(chan = "!"<>_), do: chan - - def reformat_chan("♯") do - "#" - end - def reformat_chan("♯♯") do - "##" - end - def reformat_chan(chan = "!"<>_), do: chan - - def reformat_chan(chan) do - "#"<>chan - end - - def controller do - quote do - use Phoenix.Controller, namespace: LSGWeb - import Plug.Conn - import LSGWeb.Router.Helpers - import LSGWeb.Gettext - alias LSGWeb.Router.Helpers, as: Routes - end - end - - def view do - quote do - use Phoenix.View, root: "lib/lsg_web/templates", - namespace: LSGWeb - - # Import convenience functions from controllers - import Phoenix.Controller, only: [get_flash: 2, view_module: 1] - - # Use all HTML functionality (forms, tags, etc) - use Phoenix.HTML - - import LSGWeb.Router.Helpers - import LSGWeb.ErrorHelpers - import LSGWeb.Gettext - - import Phoenix.LiveView.Helpers - - alias LSGWeb.Router.Helpers, as: Routes - end - end - - def router do - quote do - use Phoenix.Router - import Plug.Conn - import Phoenix.Controller - import Phoenix.LiveView.Router - end - end - - def channel do - quote do - use Phoenix.Channel - import LSGWeb.Gettext - end - end - - @doc """ - When used, dispatch to the appropriate controller/view/etc. - """ - defmacro __using__(which) when is_atom(which) do - apply(__MODULE__, which, []) - end -end diff --git a/lib/lsg_web/lsg_web.ex b/lib/lsg_web/lsg_web.ex new file mode 100644 index 0000000..3d9ab9a --- /dev/null +++ b/lib/lsg_web/lsg_web.ex @@ -0,0 +1,99 @@ +defmodule LSGWeb do + @moduledoc """ + The entrypoint for defining your web interface, such + as controllers, views, channels and so on. + + This can be used in your application as: + + use LSGWeb, :controller + use LSGWeb, :view + + The definitions below will be executed for every view, + controller, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. Instead, define any helper function in modules + and import those modules here. + """ + + def format_chan("##") do + "♯♯" + end + + def format_chan("#") do + "♯" + end + + def format_chan("#"<>chan) do + chan + end + + def format_chan(chan = "!"<>_), do: chan + + def reformat_chan("♯") do + "#" + end + def reformat_chan("♯♯") do + "##" + end + def reformat_chan(chan = "!"<>_), do: chan + + def reformat_chan(chan) do + "#"<>chan + end + + def controller do + quote do + use Phoenix.Controller, namespace: LSGWeb + import Plug.Conn + import LSGWeb.Router.Helpers + import LSGWeb.Gettext + alias LSGWeb.Router.Helpers, as: Routes + end + end + + def view do + quote do + use Phoenix.View, root: "lib/lsg_web/templates", + namespace: LSGWeb + + # Import convenience functions from controllers + import Phoenix.Controller, only: [get_flash: 2, view_module: 1] + + # Use all HTML functionality (forms, tags, etc) + use Phoenix.HTML + + import LSGWeb.Router.Helpers + import LSGWeb.ErrorHelpers + import LSGWeb.Gettext + + import Phoenix.LiveView.Helpers + + alias LSGWeb.Router.Helpers, as: Routes + end + end + + def router do + quote do + use Phoenix.Router + import Plug.Conn + import Phoenix.Controller + import Phoenix.LiveView.Router + end + end + + def channel do + quote do + use Phoenix.Channel + import LSGWeb.Gettext + end + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end -- cgit v1.2.3