diff --git a/lib/app.ex b/lib/app.ex index a02b70a..489008a 100644 --- a/lib/app.ex +++ b/lib/app.ex @@ -1,10 +1,10 @@ defmodule ExIrc.App do @moduledoc """ Entry point for the ExIrc application. """ - use Application.Behaviour + use Application def start(_type, _args) do ExIrc.start! end -end \ No newline at end of file +end diff --git a/lib/exirc/channels.ex b/lib/exirc/channels.ex index 17f5d24..a96cb2e 100644 --- a/lib/exirc/channels.ex +++ b/lib/exirc/channels.ex @@ -1,230 +1,230 @@ defmodule ExIrc.Channels do @moduledoc """ Responsible for managing channel state """ use Irc.Commands import String, only: [downcase: 1] defmodule Channel do defstruct name: '', topic: '', users: [], modes: '', type: '' end @doc """ Initialize a new Channels data store """ def init() do :gb_trees.empty() end ################## # Self JOIN/PART ################## @doc """ Add a channel to the data store when joining a channel """ def join(channel_tree, channel_name) do name = downcase(channel_name) case :gb_trees.lookup(name, channel_tree) do {:value, _} -> channel_tree :none -> :gb_trees.insert(name, %Channel{name: name}, channel_tree) end end @doc """ Remove a channel from the data store when leaving a channel """ 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 ########################### @doc """ Update the topic for a tracked channel when it changes """ 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 @doc """ Update the type of a tracked channel when it changes """ def set_type(channel_tree, channel_name, channel_type) when is_binary(channel_type) do - set_type(channel_tree, channel_name, List.from_char_data!(channel_type)) + 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 end #################################### # Users JOIN/PART/AKAs(namechange) #################################### @doc """ Add a user to a tracked channel when they join """ def user_join(channel_tree, channel_name, nick) when not is_list(nick) do users_join(channel_tree, channel_name, [nick]) end @doc """ Add multiple users to a tracked channel (used primarily in conjunction with the NAMES command) """ 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(channel_tree, channel_name, manipfn) end @doc """ Remove a user from a tracked channel when they leave """ 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(channel_tree, channel_name, manipfn) end @doc """ Update the nick of a user in a tracked channel when they change their nick """ def user_rename(channel_tree, nick, new_nick) do manipfn = fn(channel_nicks) -> 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_channel_tree) -> name = downcase(channel_name) users_manip(new_channel_tree, name, manipfn) end :lists.foldl(foldl, channel_tree, channels(channel_tree)) end ################ # Introspection ################ @doc """ Get a list of all currently tracked channels """ def channels(channel_tree) do (for {channel_name, _chan} <- :gb_trees.to_list(channel_tree), do: channel_name) |> Enum.reverse end @doc """ Get a list of all users in a tracked channel """ def channel_users(channel_tree, channel_name) do get_attr(channel_tree, channel_name, fn(%Channel{:users => users}) -> users end) |> Enum.reverse end @doc """ Get the current topic for a tracked channel """ def channel_topic(channel_tree, channel_name) do case get_attr(channel_tree, channel_name, fn(%Channel{:topic => topic}) -> topic end) do [] -> "No topic" topic -> topic end end @doc """ Get the type of a tracked channel """ def channel_type(channel_tree, channel_name) do case get_attr(channel_tree, channel_name, fn(%Channel{:type => type}) -> type end) do [] -> :unknown type -> type end end @doc """ Determine if a user is present in a tracked channel """ def channel_has_user?(channel_tree, channel_name, nick) do get_attr(channel_tree, channel_name, fn(%Channel{:users => users}) -> :lists.member(nick, users) end) end @doc """ Get all channel data as a tuple of the channel name and a proplist of metadata. Example Result: [{"#testchannel", [users: ["userA", "userB"], topic: "Just a test channel.", type: :public] }] """ def to_proplist(channel_tree) do for {channel_name, chan} <- :gb_trees.to_list(channel_tree) do {channel_name, [users: chan.users, topic: chan.topic, type: chan.type]} end |> Enum.reverse end #################### # Internal API #################### 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 :: binary >> -> nick << "+", nick :: binary >> -> nick << "%", nick :: binary >> -> nick << "&", nick :: binary >> -> nick << "~", nick :: binary >> -> nick nick -> nick end end) end 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 diff --git a/lib/exirc/exirc.ex b/lib/exirc/exirc.ex index 0f818c1..7f1ff63 100644 --- a/lib/exirc/exirc.ex +++ b/lib/exirc/exirc.ex @@ -1,64 +1,64 @@ defmodule ExIrc do @moduledoc """ Supervises IRC client processes Usage: # Start the supervisor (started automatically when ExIrc is run as an application) ExIrc.start_link # Start a new IRC client {:ok, client} = ExIrc.start_client! # Connect to an IRC server ExIrc.Client.connect! client, "localhost", 6667 # Logon ExIrc.Client.logon client, "password", "nick", "user", "name" # Join a channel (password is optional) ExIrc.Client.join client, "#channel", "password" # Send a message ExIrc.Client.msg client, :privmsg, "#channel", "Hello world!" # Quit (message is optional) ExIrc.Client.quit client, "message" - + # Stop and close the client connection ExIrc.Client.stop! client """ - use Supervisor.Behaviour + use Supervisor ############## # Public API ############## @doc """ Start the ExIrc supervisor. """ @spec start! :: {:ok, pid} | {:error, term} def start! do :supervisor.start_link({:local, :exirc}, __MODULE__, []) end @doc """ Start a new ExIrc client """ @spec start_client! :: {:ok, pid} | {:error, term} def start_client! do # Start the client worker :supervisor.start_child(:exirc, worker(ExIrc.Client, [])) end ############## # Supervisor API ############## @spec init(any) :: {:ok, pid} | {:error, term} def init(_) do supervise [], strategy: :one_for_one end end diff --git a/lib/exirc/utils.ex b/lib/exirc/utils.ex index 45e6c76..3da2832 100644 --- a/lib/exirc/utils.ex +++ b/lib/exirc/utils.ex @@ -1,171 +1,169 @@ defmodule ExIrc.Utils do - import String, only: [from_char_data!: 1] - ###################### # IRC Message Parsing ###################### @doc """ Parse an IRC message Example: data = ':irc.example.org 005 nick NETWORK=Freenode PREFIX=(ov)@+ CHANTYPES=#&' message = ExIrc.Utils.parse data assert "irc.example.org" = message.server """ @spec parse(raw_data :: char_list) :: IrcMessage.t def parse(raw_data) do data = :string.substr(raw_data, 1, length(raw_data)) case data do [?:|_] -> [[?:|from]|rest] = :string.tokens(data, ' ') get_cmd(rest, parse_from(from, %IrcMessage{ctcp: false})) data -> get_cmd(:string.tokens(data, ' '), %IrcMessage{ctcp: false}) end end defp parse_from(from, msg) do - case Regex.split(~r/(!|@|\.)/, iodata_to_binary(from)) do + case Regex.split(~r/(!|@|\.)/, IO.iodata_to_binary(from)) do [nick, "!", user, "@", host | host_rest] -> %{msg | :nick => nick, :user => user, :host => host <> host_rest} [nick, "@", host | host_rest] -> %{msg | :nick => nick, :host => host <> host_rest} [_, "." | _] -> # from is probably a server name - %{msg | :server => from_char_data!(from)} + %{msg | :server => to_string(from)} [nick] -> %{msg | :nick => nick} end end # Parse command from message defp get_cmd([cmd, arg1, [?:, 1 | ctcp_trail] | restargs], msg) when cmd == 'PRIVMSG' or cmd == 'NOTICE' do get_cmd([cmd, arg1, [1 | ctcp_trail] | restargs], msg) end defp get_cmd([cmd, _arg1, [1 | ctcp_trail] | restargs], msg) when cmd == 'PRIVMSG' or cmd == 'NOTICE' do args = ctcp_trail ++ for arg <- restargs, do: ' ' ++ arg |> Enum.flatten |> Enum.reverse case args do [1 | ctcp_rev] -> [ctcp_cmd | args] = ctcp_rev |> Enum.reverse |> :string.tokens(' ') - %{msg | :cmd => from_char_data!(ctcp_cmd), :args => args, :ctcp => true} + %{msg | :cmd => to_string(ctcp_cmd), :args => args, :ctcp => true} _ -> - %{msg | :cmd => from_char_data!(cmd), :ctcp => :invalid} + %{msg | :cmd => to_string(cmd), :ctcp => :invalid} end end defp get_cmd([cmd | rest], msg) do - get_args(rest, %{msg | :cmd => from_char_data!(cmd)}) + get_args(rest, %{msg | :cmd => to_string(cmd)}) end # Parse command args from message defp get_args([], msg) do args = msg.args - |> Enum.reverse + |> Enum.reverse |> Enum.filter(fn(arg) -> arg != [] end) - |> Enum.map(&String.from_char_data!/1) + |> Enum.map(&List.to_string/1) %{msg | :args => args} end defp get_args([[?: | first_arg] | rest], msg) do args = (for arg <- [first_arg | rest], do: ' ' ++ trim_crlf(arg)) |> List.flatten case args do [_ | []] -> get_args [], %{msg | :args => [msg.args]} [_ | full_trail] -> get_args [], %{msg | :args => [full_trail | msg.args]} end end defp get_args([arg | []], msg) do get_args [], %{msg | :args => [arg | msg.args]} end defp get_args([arg | rest], msg) do get_args rest, %{msg | :args => [arg | msg.args]} end ############################ # Parse RPL_ISUPPORT (005) ############################ @doc """ Parse RPL_ISUPPORT message. If an empty list is provided, do nothing, otherwise parse CHANTYPES, NETWORK, and PREFIX parameters for relevant data. """ @spec isup(parameters :: list(binary), state :: ExIrc.Client.ClientState.t) :: ExIrc.Client.ClientState.t def isup([], state), do: state def isup([param | rest], state) do try do isup(rest, isup_param(param, state)) rescue _ -> isup(rest, state) end end defp isup_param("CHANTYPES=" <> channel_prefixes, state) do prefixes = channel_prefixes |> String.split("", trim: true) %{state | :channel_prefixes => prefixes} end defp isup_param("NETWORK=" <> network, state) do %{state | :network => network} end defp isup_param("PREFIX=" <> user_prefixes, state) do prefixes = Regex.run(~r/\((.*)\)(.*)/, user_prefixes, capture: :all_but_first) - |> Enum.map(&List.from_char_data!/1) + |> Enum.map(&String.to_char_list/1) |> List.zip %{state | :user_prefixes => prefixes} end defp isup_param(_, state) do state end ################### # Helper Functions ################### @days_of_week ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] @months_of_year ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'] @doc """ Get CTCP formatted time from a tuple representing the current calendar time: Example: iex> local_time = {{2013,12,6},{14,5,00}} {{2013,12,6},{14,5,00}} iex> ExIrc.Utils.ctcp_time local_time "Fri Dec 06 14:05:00 2013" """ @spec ctcp_time(datetime :: {{integer, integer, integer}, {integer, integer, integer}}) :: binary def ctcp_time({{y, m, d}, {h, n, s}} = _datetime) do [:lists.nth(:calendar.day_of_the_week(y,m,d), @days_of_week), ' ', :lists.nth(m, @months_of_year), ' ', - :io_lib.format("~2..0s", [integer_to_list(d)]), + :io_lib.format("~2..0s", [Integer.to_char_list(d)]), ' ', - :io_lib.format("~2..0s", [integer_to_list(h)]), + :io_lib.format("~2..0s", [Integer.to_char_list(h)]), ':', - :io_lib.format("~2..0s", [integer_to_list(n)]), + :io_lib.format("~2..0s", [Integer.to_char_list(n)]), ':', - :io_lib.format("~2..0s", [integer_to_list(s)]), + :io_lib.format("~2..0s", [Integer.to_char_list(s)]), ' ', - integer_to_list(y)] |> List.flatten |> String.from_char_data! + Integer.to_char_list(y)] |> List.flatten |> List.to_string end defp trim_crlf(charlist) do case Enum.reverse(charlist) do [?\n, ?\r | text] -> Enum.reverse(text) _ -> charlist end end -end \ No newline at end of file +end diff --git a/mix.exs b/mix.exs index 813d12d..535b7e1 100644 --- a/mix.exs +++ b/mix.exs @@ -1,26 +1,26 @@ defmodule ExIrc.Mixfile do use Mix.Project def project do [ app: :exirc, version: "0.5.0", - elixir: "~> 0.13.3", + elixir: "~> 0.14.1", description: "An IRC client library for Elixir.", package: package, deps: [] ] end # Configuration for the OTP application def application do [mod: {ExIrc.App, []}] end defp package do [ files: ["lib", "mix.exs", "README.md", "LICENSE"], contributors: ["Paul Schoenfelder"], licenses: ["MIT"], links: [ { "GitHub", "https://github.com/bitwalker/exirc" }, { "Home Page", "http://bitwalker.org/exirc"} ] ] end end