diff --git a/lib/irc_bot.ex b/lib/irc_bot.ex index 4db9e9c..a0464d6 100755 --- a/lib/irc_bot.ex +++ b/lib/irc_bot.ex @@ -4,6 +4,7 @@ defmodule Discordirc.IRC do """ use GenServer require Logger + import Discordirc.ByteSplit defmodule State do defstruct server: nil, @@ -15,7 +16,8 @@ defmodule Discordirc.IRC do name: nil, channels: nil, client: nil, - network: nil + network: nil, + me: nil def from_params(params) when is_map(params) do Enum.reduce(params, %State{}, fn {k, v}, acc -> @@ -29,6 +31,7 @@ defmodule Discordirc.IRC do alias ExIRC.Client alias ExIRC.SenderInfo + alias ExIRC.Whois alias Discordirc.ChannelMap alias Discordirc.Formatter alias Nostrum.Api, as: DiscordAPI @@ -60,71 +63,36 @@ defmodule Discordirc.IRC do def handle_info(:logged_in, state) do Logger.debug("Logged in to #{state.server}:#{state.port}") + Client.whois(state.client, state.nick) for c <- state.channels, do: Client.join(state.client, c) {:noreply, state} end - def ircsplit(str, pfxlen) do - str - |> String.split(" ") - |> Enum.chunk_while( - [], - fn ele, acc -> - if Enum.join(Enum.reverse([ele | acc]), " ") |> byte_size() > 512 - pfxlen do - {:cont, Enum.reverse(acc), [ele]} - else - {:cont, [ele | acc]} - end - end, - fn - [] -> {:cont, []} - acc -> {:cont, Enum.reverse(acc), []} - end - ) - |> Enum.map(fn x -> Enum.join(x, " ") end) - |> Enum.map(fn x -> - case byte_size(x) do - n when is_integer(n) and n > 512 -> - x - |> String.to_charlist() - |> Enum.chunk_every(512 - pfxlen) - |> Enum.map(&List.to_string(&1)) - - _ -> - x - end - end) - |> List.flatten() - |> Enum.filter(&(&1 !== "")) - end - - def discord_ircsplit(msg, nick, target) do - pfx = "PRIVMSG #{target} :" |> byte_size() - nkl = "<#{nick}> " |> byte_size() - - msg - |> String.split("\n") - |> Enum.map(&ircsplit(&1, pfx + nkl)) - |> List.flatten() - end - def handle_info({:discordmsg, msg}, state) do channel = ChannelMap.irc(msg.channel_id) {usr, response} = Formatter.from_discord(msg) case channel do {:ok, _, chan} -> + pfx = ":#{state.me} PRIVMSG #{chan} :" |> byte_size() + nkl = "<#{usr}> " |> byte_size() + prefixlen = pfx + nkl # irc messages can only be 512b in length split_response = case response do - x when is_binary(x) -> - discord_ircsplit(x, usr, chan) - x when is_list(x) -> x - |> Enum.map(&discord_ircsplit(&1, usr, chan)) - |> List.flatten() + + x when is_binary(x) -> + [x] end + |> Enum.map(fn x -> + x + |> String.split("\n") + |> Enum.map(&ircsplit(&1, prefixlen)) + |> List.flatten() + end) + |> List.flatten() case split_response do x when is_binary(x) -> @@ -172,6 +140,24 @@ defmodule Discordirc.IRC do {:noreply, state} end + def handle_info({:whois, whois = %Whois{:hostname => host}}, state) do + case whois do + %Whois{nick: n, user: user} when n == state.nick -> + me = "#{state.nick}!#{user}@#{host}" + Logger.debug("Setting host to #{me} #{inspect(whois)}") + {:noreply, %State{state | :me => me}} + + _ -> + {:noreply, state} + end + end + + def handle_info({:unrecognized, "396", %{args: args}}, state) do + Logger.debug("Received UnrealIRCD host change notification, double checking host") + Client.whois(state.client, state.nick) + {:noreply, state} + end + def handle_info({:me, msg, %SenderInfo{:nick => nick}, channel}, state) do discordid = ChannelMap.discord(state.network, channel) fmsg = Formatter.from_irc(nick, msg, true) @@ -203,7 +189,8 @@ defmodule Discordirc.IRC do # {:noreply, state} # end - def handle_info(_event, state) do + def handle_info(event, state) do + Logger.debug("unknown event: inspect output: " <> inspect(event)) {:noreply, state} end diff --git a/lib/splitter.ex b/lib/splitter.ex new file mode 100755 index 0000000..8a746ef --- /dev/null +++ b/lib/splitter.ex @@ -0,0 +1,67 @@ +defmodule Discordirc.ByteSplit do + @moduledoc """ + Module that splits text by bytes, Unicode Aware. + """ + # use 510 to \r\n newline in mind + @irclen 510 + + @doc """ + split a string into a number `bytes`, optionally subtracting a number of `hold` bytes for prefix/suffix + """ + def byte_split(str, bytes, hold \\ 0) do + case byte_size(str) do + n when is_integer(n) and n > bytes -> + str + |> String.split("") + |> Enum.chunk_while( + [], + fn ele, acc -> + if Enum.join(Enum.reverse([ele | acc])) |> byte_size() > bytes - hold do + {:cont, Enum.reverse(acc), [ele]} + else + {:cont, [ele | acc]} + end + end, + fn + [] -> {:cont, []} + acc -> {:cont, Enum.reverse(acc), []} + end + ) + |> Enum.map(&Enum.join(&1)) + + _ -> + str + end + end + + def ircsplit(str, pfxlen) do + str + |> String.split(" ") + |> Enum.chunk_while( + [], + fn ele, acc -> + if Enum.join(Enum.reverse([ele | acc]), " ") |> byte_size() > @irclen - pfxlen do + {:cont, Enum.reverse(acc), [ele]} + else + {:cont, [ele | acc]} + end + end, + fn + [] -> {:cont, []} + acc -> {:cont, Enum.reverse(acc), []} + end + ) + |> Enum.map(fn x -> Enum.join(x, " ") end) + |> Enum.map(fn x -> + case byte_size(x) do + n when is_integer(n) and n > @irclen - pfxlen -> + byte_split(x, @irclen - pfxlen) + + _ -> + x + end + end) + |> List.flatten() + |> Enum.filter(&(&1 !== "")) + end +end diff --git a/test/discordirc_test.exs b/test/discordirc_test.exs old mode 100644 new mode 100755 index 3a2ddb1..f57139f --- a/test/discordirc_test.exs +++ b/test/discordirc_test.exs @@ -1,8 +1,4 @@ defmodule DiscordircTest do use ExUnit.Case doctest Discordirc - - test "greets the world" do - assert Discordirc.hello() == :world - end end diff --git a/test/splitter_test.exs b/test/splitter_test.exs new file mode 100755 index 0000000..4c4a15b --- /dev/null +++ b/test/splitter_test.exs @@ -0,0 +1,82 @@ +defmodule Discordirc.SplitterTest do + use ExUnit.Case + import Discordirc.ByteSplit + doctest Discordirc.ByteSplit + + test "split by bytes" do + assert byte_split("test", 2) == ["te", "st"] + end + + test "split with emoji" do + assert byte_split("testπŸ¦€", 4) == ["test", "πŸ¦€"] + end + + test "ircsplit without emoji" do + lorem_ipsum = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam lorem nunc, vestibulum ac magna et, egestas accumsan felis. " <> + "Morbi dolor quam, venenatis in molestie ullamcorper, fringilla nec tellus. Cras viverra purus ut ante iaculis consequat. " <> + "Donec convallis id velit id vulputate. Nullam vel libero at sem consequat dapibus non in lectus. Nunc nec lectus aliquet, " <> + "faucibus erat eget, feugiat justo. Duis imperdiet ligula at sem consectetur, porta semper massa sagittis. Duis sit amet risus sit amet nisi lectus." + + irc_result = [ + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam lorem nunc, vestibulum ac magna et, egestas accumsan felis. " <> + "Morbi dolor quam, venenatis in molestie ullamcorper, fringilla nec tellus. Cras viverra purus ut ante iaculis consequat. " <> + "Donec convallis id velit id vulputate. Nullam vel libero at sem consequat dapibus non in lectus. Nunc nec lectus aliquet, " <> + "faucibus erat eget, feugiat justo. Duis imperdiet ligula at sem consectetur, porta semper massa sagittis. Duis sit amet risus", + "sit amet nisi lectus." + ] + + prefix = "PRIVMSG #test :" + prefix_len = prefix |> byte_size() + irc_after_split = ircsplit(lorem_ipsum, prefix_len) + assert irc_after_split == irc_result + + assert Enum.map(irc_after_split, &byte_size(prefix <> &1)) |> Enum.map(&(&1 <= 512)) == [ + true, + true + ] + end + + test "ircsplit with emoji" do + lorem_ipsum = + "Lorem πŸ“¨πŸ°πŸ‡πŸŒ€πŸ•£πŸ‘» gravida enim suspendisse vel πŸ’΅ πŸ”·πŸ”ƒπŸŒ™πŸ¦ blandit 🌝🎌🌈 quis πŸ¬πŸ”«πŸŸ tincidunt odio quis a morbi ipsum, " <> + "lectus at nunc, nunc πŸ“–πŸ· morbi amet velit mattis netus est id πŸ‘­πŸŒ› mauris id massa massa lorem feugiat et πŸŒƒπŸ₯πŸ‘‡ πŸ“΄ tellus. Purus " <> + "pulvinar sed integer ipsum, porta πŸ“•πŸ† posuere nunc mauris, elit vitae volutpat lacinia nulla et pellentesque elit πŸ’™πŸ’πŸπŸ“—πŸ›πŸŒ° πŸ‘ " <> + "hendrerit sit πŸ’΄πŸ“£πŸ’ etiam πŸ…πŸˆ πŸ“— 🌹 curabitur purus" + + irc_result = [ + "Lorem πŸ“¨πŸ°πŸ‡πŸŒ€πŸ•£πŸ‘» gravida enim suspendisse vel πŸ’΅ πŸ”·πŸ”ƒπŸŒ™πŸ¦ blandit 🌝🎌🌈 quis πŸ¬πŸ”«πŸŸ tincidunt odio quis a morbi ipsum, " <> + "lectus at nunc, nunc πŸ“–πŸ· morbi amet velit mattis netus est id πŸ‘­πŸŒ› mauris id massa massa lorem feugiat et πŸŒƒπŸ₯πŸ‘‡ πŸ“΄ tellus. Purus " <> + "pulvinar sed integer ipsum, porta πŸ“•πŸ† posuere nunc mauris, elit vitae volutpat lacinia nulla et pellentesque elit πŸ’™πŸ’πŸπŸ“—πŸ›πŸŒ° πŸ‘ " <> + "hendrerit sit πŸ’΄πŸ“£πŸ’ etiam πŸ…πŸˆ", + "πŸ“— 🌹 curabitur purus" + ] + + prefix = "PRIVMSG #test :" + prefix_len = prefix |> byte_size() + irc_after_split = ircsplit(lorem_ipsum, prefix_len) + assert irc_after_split == irc_result + + assert Enum.map(irc_after_split, &byte_size(prefix <> &1)) |> Enum.map(&(&1 <= 512)) == [ + true, + true + ] + end + + test "ircsplit only emoji" do + crab = "πŸ¦€" + crabs = for _ <- 1..129, into: "", do: crab + + good_split = [ + for(_ <- 1..123, into: "", do: crab), + for(_ <- 1..6, into: "", do: crab) + ] + + prefix = "PRIVMSG #test :" + prefix_len = prefix |> byte_size() + + irc_split = ircsplit(crabs, prefix_len) + assert irc_split == good_split + assert Enum.map(irc_split, &byte_size(prefix <> &1)) |> Enum.map(&(&1 <= 512)) == [true, true] + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs old mode 100644 new mode 100755