This fix required extra work than i was expecting. I ended up moving the split
    functionality into its own module because it's more involved than i expected. And
    to enable testing of the code, for which i addeed tests.
master
Rachel Fae Fox 2022-11-08 20:33:03 -05:00
parent dff5ec0bde
commit 98bb443e94
5 changed files with 187 additions and 55 deletions

View File

@ -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

67
lib/splitter.ex Executable file
View File

@ -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

4
test/discordirc_test.exs Normal file → Executable file
View File

@ -1,8 +1,4 @@
defmodule DiscordircTest do
use ExUnit.Case
doctest Discordirc
test "greets the world" do
assert Discordirc.hello() == :world
end
end

82
test/splitter_test.exs Executable file
View File

@ -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

0
test/test_helper.exs Normal file → Executable file
View File