diff --git a/lib/exirc/channels.ex b/lib/exirc/channels.ex index 7fbb1e1..d2284f4 100644 --- a/lib/exirc/channels.ex +++ b/lib/exirc/channels.ex @@ -1,147 +1,167 @@ defmodule ExIrc.Channels do @moduledoc """ Responsible for managing channel interaction """ use Irc.Commands + import String, only: [downcase: 1] + defrecord Channel, name: '', topic: '', users: [], modes: '', type: '' def init() do :gb_trees.empty() end ################## # Self JOIN/PART ################## - def join(struct, channel_name) do - name = chan2lower(channel_name) - case :gb_trees.lookup(name, struct) do + def join(channel_tree, channel_name) do + name = downcase(channel_name) + case :gb_trees.lookup(name, channel_tree) do {:value, _} -> - struct + channel_tree :none -> - :gb_trees.insert(name, Channel.new(name: name), struct) + :gb_trees.insert(name, Channel.new(name: name), channel_tree) end end - def part(struct, channel_name) do - name = chan2lower(channel_name) - :gb_trees.delete(name, struct) + def part(channel_tree, channel_name) do + name = downcase(channel_name) + case :gb_trees.lookup(name, channel_tree) do + {:value, _} -> + :gb_trees.delete(name, channel_tree) + :none -> + channel_tree + end end ########################### # Channel Modes/Attributes ########################### - def set_topic(struct, channel_name, topic) do - name = chan2lower(channel_name) - channel = :gb_trees.get(name, struct) - :gb_trees.enter(name, channel.topic(topic), struct) - end - - def set_type(struct, channel_name, channel_type) do - name = chan2lower(channel_name) - channel = :gb_trees.get(name, struct) - type = case channel_type do - '@' -> :secret - '*' -> :private - '=' -> :public + def set_topic(channel_tree, channel_name, topic) do + name = downcase(channel_name) + case :gb_trees.lookup(name, channel_tree) do + {:value, channel} -> + :gb_trees.enter(name, channel.topic(topic), channel_tree) + :none -> + channel_tree + end + end + + def set_type(channel_tree, channel_name, channel_type) when is_binary(channel_type) do + set_type(channel_tree, channel_name, String.to_char_list!(channel_type)) + end + def set_type(channel_tree, channel_name, channel_type) do + name = downcase(channel_name) + case :gb_trees.lookup(name, channel_tree) do + {:value, channel} -> + type = case channel_type do + '@' -> :secret + '*' -> :private + '=' -> :public + end + :gb_trees.enter(name, channel.type(type), channel_tree) + :none -> + channel_tree end - :gb_trees.enter(name, channel.type(type), struct) end #################################### # Users JOIN/PART/AKAs(namechange) #################################### - def user_join(struct, channel_name, nick) do - users_join(struct, channel_name, [nick]) + def user_join(channel_tree, channel_name, nick) when not is_list(nick) do + users_join(channel_tree, channel_name, [nick]) end - def users_join(struct, channel_name, nicks) do + def users_join(channel_tree, channel_name, nicks) do pnicks = strip_rank(nicks) manipfn = fn(channel_nicks) -> :lists.usort(channel_nicks ++ pnicks) end - users_manip(struct, channel_name, manipfn) + users_manip(channel_tree, channel_name, manipfn) end - def user_part(struct, channel_name, nick) do + def user_part(channel_tree, channel_name, nick) do pnick = strip_rank([nick]) manipfn = fn(channel_nicks) -> :lists.usort(channel_nicks -- pnick) end - users_manip(struct, channel_name, manipfn) + users_manip(channel_tree, channel_name, manipfn) end - def user_rename(struct, nick, new_nick) do + def user_rename(channel_tree, nick, new_nick) do manipfn = fn(channel_nicks) -> - case :lists.member(nick, channel_nicks) do - true -> :lists.usort([new_nick | channel_nicks -- [nick]]) + case Enum.member?(channel_nicks, nick) do + true -> [new_nick | channel_nicks -- [nick]] |> Enum.uniq |> Enum.sort false -> channel_nicks end end - foldl = fn(channel_name, new_struct) -> - name = chan2lower(channel_name) - users_manip(new_struct, name, manipfn) + foldl = fn(channel_name, new_channel_tree) -> + name = downcase(channel_name) + users_manip(new_channel_tree, name, manipfn) end - :lists.foldl(foldl, struct, channels(struct)) - end - - def users_manip(struct, channel_name, manipfn) do - name = chan2lower(channel_name) - channel = :gb_trees.get(name, struct) - channel_list = manipfn.(channel.users) - :gb_trees.enter(channel_name, channel.users(channel_list), struct) + :lists.foldl(foldl, channel_tree, channels(channel_tree)) end ################ # Introspection ################ - def channels(struct) do - lc {channel_name, _chan} inlist :gb_trees.to_list(struct), do: channel_name + def channels(channel_tree) do + (lc {channel_name, _chan} inlist :gb_trees.to_list(channel_tree), do: channel_name) |> Enum.reverse end - def chan_users(struct, channel_name) do - get_attr(struct, channel_name, fn(Channel[users: users]) -> users end) + def chan_users(channel_tree, channel_name) do + get_attr(channel_tree, channel_name, fn(Channel[users: users]) -> users end) |> Enum.reverse end - def chan_topic(struct, channel_name) do - get_attr(struct, channel_name, fn(Channel[topic: topic]) -> topic end) + def chan_topic(channel_tree, channel_name) do + get_attr(channel_tree, channel_name, fn(Channel[topic: topic]) -> topic end) end - def chan_type(struct, channel_name) do - get_attr(struct, channel_name, fn(Channel[type: type]) -> type end) + def chan_type(channel_tree, channel_name) do + get_attr(channel_tree, channel_name, fn(Channel[type: type]) -> type end) end - def chan_has_user(struct, channel_name, nick) do - get_attr(struct, channel_name, fn(Channel[users: users]) -> :lists.member(nick, users) end) + def chan_has_user?(channel_tree, channel_name, nick) do + get_attr(channel_tree, channel_name, fn(Channel[users: users]) -> :lists.member(nick, users) end) end - def to_proplist(struct) do - lc {channel_name, chan} inlist :gb_trees.to_list(struct), do: { + def to_proplist(channel_tree) do + (lc {channel_name, chan} inlist :gb_trees.to_list(channel_tree), do: { channel_name, [users: chan.users, topic: chan.topic, type: chan.type] - } + }) |> Enum.reverse end #################### # Internal API #################### - defp chan2lower(channel_name), do: String.downcase(channel_name) + defp users_manip(channel_tree, channel_name, manipfn) do + name = downcase(channel_name) + case :gb_trees.lookup(name, channel_tree) do + {:value, channel} -> + channel_list = manipfn.(channel.users) + :gb_trees.enter(channel_name, channel.users(channel_list), channel_tree) + :none -> + channel_tree + end + end defp strip_rank(nicks) do nicks |> Enum.map(fn(n) -> case n do [?@ | nick] -> nick [?+ | nick] -> nick nick -> nick end end) end - defp get_attr(struct, channel_name, getfn) do - name = chan2lower(channel_name) - case :gb_trees.lookup(name, struct) do + defp get_attr(channel_tree, channel_name, getfn) do + name = downcase(channel_name) + case :gb_trees.lookup(name, channel_tree) do {:value, channel} -> getfn.(channel) :none -> {:error, :no_such_channel} end end end \ No newline at end of file diff --git a/test/channels_test.exs b/test/channels_test.exs new file mode 100644 index 0000000..927a76a --- /dev/null +++ b/test/channels_test.exs @@ -0,0 +1,130 @@ +defmodule ExIrc.ChannelsTest do + use ExUnit.Case, async: true + + alias ExIrc.Channels, as: Channels + + test "Joining a channel adds it to the tree of currently joined channels" do + channels = Channels.init() |> Channels.join("#testchannel") |> Channels.channels + assert Enum.member?(channels, "#testchannel") + end + + test "The channel name is downcased when joining" do + channels = Channels.init() |> Channels.join("#TestChannel") |> Channels.channels + assert Enum.member?(channels, "#testchannel") + end + + test "Joining the same channel twice is a noop" do + channels = Channels.init() |> Channels.join("#TestChannel") |> Channels.join("#testchannel") |> Channels.channels + assert 1 == Enum.count(channels) + end + + test "Parting a channel removes it from the tree of currently joined channels" do + tree = Channels.init() |> Channels.join("#testchannel") + assert Enum.member?(Channels.channels(tree), "#testchannel") + tree = Channels.part(tree, "#testchannel") + refute Enum.member?(Channels.channels(tree), "#testchannel") + end + + test "Parting a channel not in the tree is a noop" do + tree = Channels.init() + {count, _} = Channels.part(tree, "#testchannel") + assert 0 == count + end + + test "Can set the topic for a channel" do + channels = Channels.init() |> Channels.join("#testchannel") |> Channels.set_topic("#testchannel", "Welcome to Test Channel!") + assert "Welcome to Test Channel!" == Channels.chan_topic(channels, "#testchannel") + end + + test "Setting the topic for a channel we haven't joined returns :error" do + channels = Channels.init() |> Channels.set_topic("#testchannel", "Welcome to Test Channel!") + assert {:error, :no_such_channel} == Channels.chan_topic(channels, "#testchannel") + end + + test "Can set the channel type" do + channels = Channels.init() |> Channels.join("#testchannel") |> Channels.set_type("#testchannel", "@") + assert :secret == Channels.chan_type(channels, "#testchannel") + channels = Channels.set_type(channels, "#testchannel", "*") + assert :private == Channels.chan_type(channels, "#testchannel") + channels = Channels.set_type(channels, "#testchannel", "=") + assert :public == Channels.chan_type(channels, "#testchannel") + end + + test "Setting the channel type for a channel we haven't joined returns :error" do + channels = Channels.init() |> Channels.set_type("#testchannel", "@") + assert {:error, :no_such_channel} == Channels.chan_type(channels, "#testchannel") + end + + test "Setting an invalid channel type raises CaseClauseError" do + assert_raise CaseClauseError, "no case clause matching: '!'", fn -> + Channels.init() |> Channels.join("#testchannel") |> Channels.set_type("#testchannel", "!") + end + end + + test "Can join a user to a channel" do + channels = Channels.init() |> Channels.join("#testchannel") |> Channels.user_join("#testchannel", "testnick") + assert Channels.chan_has_user?(channels, "#testchannel", "testnick") + end + + test "Can join multiple users to a channel" do + channels = Channels.init() |> Channels.join("#testchannel") |> Channels.users_join("#testchannel", ["testnick", "anothernick"]) + assert Channels.chan_has_user?(channels, "#testchannel", "testnick") + assert Channels.chan_has_user?(channels, "#testchannel", "anothernick") + end + + test "Joining a users to a channel we aren't in is a noop" do + channels = Channels.init() |> Channels.user_join("#testchannel", "testnick") + assert {:error, :no_such_channel} == Channels.chan_has_user?(channels, "#testchannel", "testnick") + channels = Channels.init() |> Channels.users_join("#testchannel", ["testnick", "anothernick"]) + assert {:error, :no_such_channel} == Channels.chan_has_user?(channels, "#testchannel", "testnick") + end + + test "Can part a user from a channel" do + channels = Channels.init() |> Channels.join("#testchannel") |> Channels.user_join("#testchannel", "testnick") + assert Channels.chan_has_user?(channels, "#testchannel", "testnick") + channels = channels |> Channels.user_part("#testchannel", "testnick") + refute Channels.chan_has_user?(channels, "#testchannel", "testnick") + end + + test "Parting a user from a channel we aren't in is a noop" do + channels = Channels.init() |> Channels.user_part("#testchannel", "testnick") + assert {:error, :no_such_channel} == Channels.chan_has_user?(channels, "#testchannel", "testnick") + end + + test "Can rename a user" do + channels = Channels.init() + |> Channels.join("#testchannel") + |> Channels.join("#anotherchan") + |> Channels.user_join("#testchannel", "testnick") + |> Channels.user_join("#anotherchan", "testnick") + assert Channels.chan_has_user?(channels, "#testchannel", "testnick") + assert Channels.chan_has_user?(channels, "#anotherchan", "testnick") + channels = Channels.user_rename(channels, "testnick", "newnick") + refute Channels.chan_has_user?(channels, "#testchannel", "testnick") + refute Channels.chan_has_user?(channels, "#anotherchan", "testnick") + assert Channels.chan_has_user?(channels, "#testchannel", "newnick") + assert Channels.chan_has_user?(channels, "#anotherchan", "newnick") + end + + test "Renaming a user that doesn't exist is a noop" do + channels = Channels.init() |> Channels.join("#testchannel") |> Channels.user_rename("testnick", "newnick") + refute Channels.chan_has_user?(channels, "#testchannel", "testnick") + refute Channels.chan_has_user?(channels, "#testchannel", "newnick") + end + + test "Can get the current set of channel data as a tuple of the channel name and it's data as a proplist" do + channels = Channels.init() + |> Channels.join("#testchannel") + |> Channels.set_type("#testchannel", "@") + |> Channels.set_topic("#testchannel", "Welcome to Test!") + |> Channels.join("#anotherchan") + |> Channels.set_type("#anotherchan", "=") + |> Channels.set_topic("#anotherchan", "Welcome to Another Channel!") + |> Channels.user_join("#testchannel", "testnick") + |> Channels.user_join("#anotherchan", "testnick") + |> Channels.to_proplist + testchannel = {"#testchannel", [users: ["testnick"], topic: "Welcome to Test!", type: :secret]} + anotherchan = {"#anotherchan", [users: ["testnick"], topic: "Welcome to Another Channel!", type: :public]} + assert [testchannel, anotherchan] == channels + end +end \ No newline at end of file