From e0554741bb837ceec941aa71b15c18f80b3e3d79 Mon Sep 17 00:00:00 2001 From: Ryan Winchester Date: Fri, 4 Mar 2022 14:40:03 -0400 Subject: [PATCH 1/5] Add static analysis (credo and dialyzer) and formatter --- .credo.exs | 210 ++++++++ .formatter.exs | 4 + .github/workflows/tests.yaml | 46 +- .gitignore | 2 + .travis.yml | 11 - lib/app.ex | 2 +- lib/exirc/channels.ex | 130 +++-- lib/exirc/client.ex | 997 +++++++++++++++++++++++------------ lib/exirc/commands.ex | 328 ++++++------ lib/exirc/example_handler.ex | 104 ++-- lib/exirc/exirc.ex | 26 +- lib/exirc/irc_message.ex | 26 +- lib/exirc/logger.ex | 18 +- lib/exirc/transport.ex | 51 +- lib/exirc/utils.ex | 159 +++--- lib/exirc/who.ex | 29 +- lib/exirc/whois.ex | 31 +- mix.exs | 9 +- mix.lock | 30 +- test/channels_test.exs | 115 ++-- test/client_test.exs | 4 +- test/commands_test.exs | 40 +- test/test_helper.exs | 2 +- test/utils_test.exs | 315 ++++++++--- 24 files changed, 1794 insertions(+), 895 deletions(-) create mode 100644 .credo.exs create mode 100644 .formatter.exs delete mode 100644 .travis.yml diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..94967b4 --- /dev/null +++ b/.credo.exs @@ -0,0 +1,210 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "lib/", + "src/", + "test/", + "web/", + "apps/*/lib/", + "apps/*/src/", + "apps/*/test/", + "apps/*/web/" + ], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/", ~r"/examples/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: true, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + {Credo.Check.Design.TagFIXME, []}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, false}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithSingleClause, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, [ignore_comments: true]}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.WrongTestFileExtension, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.UnsafeExec, []} + ], + disabled: [ + # + # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) + + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.Specs, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.UnsafeToAtom, []} + + # {Credo.Check.Refactor.MapInto, []}, + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + } + ] +} diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d51b102..c87147c 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -2,12 +2,50 @@ on: push jobs: test: - runs-on: ubuntu-latest + runs-on: ${{matrix.os}} + strategy: + matrix: + os: ['ubuntu-latest'] + otp: ['22.x', '23.x', '24.x'] + elixir: ['1.13.x'] steps: - uses: actions/checkout@v2 - - uses: actions/setup-elixir@v1 + - uses: erlef/setup-beam@v1 with: - otp-version: '21.3' - elixir-version: '1.6.6' + otp-version: ${{matrix.otp}} + elixir-version: ${{matrix.elixir}} + - name: Build cache + uses: actions/cache@v2 + with: + path: _build + key: build-${{ matrix.os }}-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('lib/**/*.ex*') }} + restore-keys: | + build-${{ matrix.os }}-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('lib/**/*.ex*') }} + - name: Dependencies cache + uses: actions/cache@v2 + with: + path: deps + key: mix-${{ matrix.os }}-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} + restore-keys: | + mix-${{ matrix.os }}-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} - run: mix deps.get + - run: mix format --check-formatted - run: mix test + - run: mix credo + # Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones + # Cache key based on Elixir & Erlang version (also useful when running in matrix) + - name: Restore PLT cache + uses: actions/cache@v2 + id: plt_cache + with: + key: | + plt-${{ matrix.os }}-${{ matrix.elixir }}-${{ matrix.otp }} + restore-keys: | + plt-${{ matrix.os }}-${{ matrix.elixir }}-${{ matrix.otp }} + path: | + priv/plts + # Create PLTs if no cache was found + - name: Create PLTs + if: steps.plt_cache.outputs.cache-hit != 'true' + run: mix dialyzer --plt + - run: mix dialyzer diff --git a/.gitignore b/.gitignore index f23358d..0f04d18 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ _build/ .exenv-version erl_crash.dump bench/snapshots +/priv/plts/*.plt +/priv/plts/*.plt.hash diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 246c6d6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -language: elixir -sudo: false -elixir: - - 1.6 -otp_release: - - 20.3 -script: - - "MIX_ENV=test mix do deps.get, compile, coveralls.travis" -notifications: - recipients: - - paulschoenfelder@gmail.com diff --git a/lib/app.ex b/lib/app.ex index 4cb5959..9e751d1 100644 --- a/lib/app.ex +++ b/lib/app.ex @@ -5,6 +5,6 @@ defmodule ExIRC.App do use Application def start(_type, _args) do - ExIRC.start! + ExIRC.start!() end end diff --git a/lib/exirc/channels.ex b/lib/exirc/channels.ex index c4e30e9..2b1d919 100644 --- a/lib/exirc/channels.ex +++ b/lib/exirc/channels.ex @@ -1,23 +1,34 @@ defmodule ExIRC.Channels do @moduledoc """ - Responsible for managing channel state + Responsible for managing channel state. """ use ExIRC.Commands import String, only: [downcase: 1] defmodule Channel do - defstruct name: '', + @moduledoc """ + IRC channel. + """ + defstruct name: '', topic: '', users: [], modes: '', - type: '' + type: '' + + @type t :: %__MODULE__{ + name: String.t() | charlist(), + topic: String.t() | charlist(), + users: [String.t() | charlist()], + modes: String.t() | charlist(), + type: String.t() | charlist() + } end @doc """ - Initialize a new Channels data store + Initialize a new Channels data store. """ - def init() do + def init do :gb_trees.empty() end @@ -26,26 +37,30 @@ defmodule ExIRC.Channels do ################## @doc """ - Add a channel to the data store when joining a channel + 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 + 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 @@ -56,34 +71,41 @@ defmodule ExIRC.Channels do ########################### @doc """ - Update the topic for a tracked channel when it changes + 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 + 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, String.to_charlist(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 + type = + case channel_type do + '@' -> :secret + '*' -> :private + '=' -> :public + end + :gb_trees.enter(name, %{channel | type: type}, channel_tree) + :none -> channel_tree end @@ -94,54 +116,58 @@ defmodule ExIRC.Channels do #################################### @doc """ - Add a user to a tracked channel when they join + 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) + 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 = trim_rank(nicks) - manipfn = fn(channel_nicks) -> :lists.usort(channel_nicks ++ pnicks) end + 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 + Remove a user from a tracked channel when they leave. """ def user_part(channel_tree, channel_name, nick) do pnick = trim_rank([nick]) - manipfn = fn(channel_nicks) -> :lists.usort(channel_nicks -- pnick) end + manipfn = fn channel_nicks -> :lists.usort(channel_nicks -- pnick) end users_manip(channel_tree, channel_name, manipfn) end def user_quit(channel_tree, nick) do pnick = trim_rank([nick]) - manipfn = fn(channel_nicks) -> :lists.usort(channel_nicks -- pnick) end - foldl = fn(channel_name, new_channel_tree) -> + manipfn = fn channel_nicks -> :lists.usort(channel_nicks -- pnick) 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 @doc """ - Update the nick of a user in a tracked channel when they change their nick + 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) -> + manipfn = fn channel_nicks -> case Enum.member?(channel_nicks, nick) do - true -> [new_nick | channel_nicks -- [nick]] |> Enum.uniq |> Enum.sort + true -> [new_nick | channel_nicks -- [nick]] |> Enum.uniq() |> Enum.sort() false -> channel_nicks - end + end end - foldl = fn(channel_name, new_channel_tree) -> + + 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 @@ -150,83 +176,93 @@ defmodule ExIRC.Channels do ################ @doc """ - Get a list of all currently tracked channels + 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 + 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 + Get a list of all users in a tracked channel. """ def channel_users(channel_tree, channel_name) do - case get_attr(channel_tree, channel_name, fn(%Channel{users: users}) -> users end) do + case get_attr(channel_tree, channel_name, fn %Channel{users: users} -> users end) do {:error, _} = error -> error users -> Enum.reverse(users) end end @doc """ - Get the current topic for a tracked channel + 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" + 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 + 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 + 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 + 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) + 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: + ## Example: + iex> Channels.to_proplist(channel_tree) [{"#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 + |> 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 trim_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 + Enum.map(nicks, fn n -> + case n do + <<"@", nick::binary>> -> nick + <<"+", nick::binary>> -> nick + <<"%", nick::binary>> -> nick + <<"&", nick::binary>> -> nick + <<"~", nick::binary>> -> nick nick -> nick end end) @@ -234,10 +270,10 @@ defmodule ExIRC.Channels 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 diff --git a/lib/exirc/client.ex b/lib/exirc/client.ex index 2ae34a9..c66432b 100644 --- a/lib/exirc/client.ex +++ b/lib/exirc/client.ex @@ -2,40 +2,68 @@ defmodule ExIRC.Client do @moduledoc """ Maintains the state and behaviour for individual IRC client connections """ - use ExIRC.Commands - use GenServer + use ExIRC.Commands + use GenServer + import ExIRC.Logger + require Logger alias ExIRC.Channels - alias ExIRC.Utils - alias ExIRC.SenderInfo alias ExIRC.Client.Transport + alias ExIRC.SenderInfo + alias ExIRC.Utils - # Client internal state defmodule ClientState do - defstruct event_handlers: [], - server: "localhost", - port: 6667, - socket: nil, - nick: "", - pass: "", - user: "", - name: "", - ssl?: false, - connected?: false, - logged_on?: false, - autoping: true, + @moduledoc "Internal client state" + + defstruct event_handlers: [], + server: "localhost", + port: 6667, + socket: nil, + nick: "", + pass: "", + user: "", + name: "", + ssl?: false, + connected?: false, + logged_on?: false, + autoping: true, channel_prefixes: "", - network: "", - user_prefixes: "", - login_time: "", - channels: [], - debug?: false, - retries: 0, - inet: :inet, - owner: nil, - whois_buffers: %{}, - who_buffers: %{} + network: "", + user_prefixes: "", + login_time: "", + channels: :gb_trees.empty(), + debug?: false, + retries: 0, + inet: :inet, + owner: nil, + whois_buffers: %{}, + who_buffers: %{} + + @type t :: %__MODULE__{ + event_handlers: [{pid(), reference()}], + server: String.t(), + port: pos_integer(), + socket: :gen_tcp.socket() | nil, + nick: String.t(), + pass: String.t(), + user: String.t(), + name: String.t(), + ssl?: boolean(), + connected?: boolean(), + autoping: boolean(), + channel_prefixes: String.t(), + network: String.t(), + user_prefixes: String.t(), + login_time: String.t(), + channels: :gb_trees.tree(String.t(), Channels.Channel.t()), + debug?: boolean(), + retries: non_neg_integer(), + inet: atom(), + owner: {pid(), reference()} | nil, + whois_buffers: map(), + who_buffers: map() + } end ################# @@ -43,51 +71,69 @@ defmodule ExIRC.Client do ################# @doc """ - Start a new IRC client process - - Returns either {:ok, pid} or {:error, reason} + Start a new IRC client process. """ @spec start!(options :: list() | nil) :: {:ok, pid} | {:error, term} def start!(options \\ []) do start_link(options) end + @doc """ Start a new IRC client process. - - Returns either {:ok, pid} or {:error, reason} """ - @spec start_link(options :: list() | nil, process_opts :: list() | nil) :: {:ok, pid} | {:error, term} + @spec start_link(options :: list() | nil, process_opts :: list() | nil) :: + {:ok, pid} | {:error, term} def start_link(options \\ [], process_opts \\ []) do options = Keyword.put_new(options, :owner, self()) GenServer.start_link(__MODULE__, options, process_opts) end + @doc """ - Stop the IRC client process + Stop the IRC client process. """ @spec stop!(client :: pid) :: :ok def stop!(client) do GenServer.call(client, :stop) end + @doc """ Connect to a server with the provided server and port - Example: - Client.connect! pid, "localhost", 6667 + ## Example: + + iex> Client.connect!(pid, "localhost", 6667) + :ok + """ - @spec connect!(client :: pid, server :: binary, port :: non_neg_integer, options :: list() | nil) :: :ok + @spec connect!( + client :: pid, + server :: binary, + port :: non_neg_integer, + options :: list() | nil + ) :: :ok def connect!(client, server, port, options \\ []) do GenServer.call(client, {:connect, server, port, options, false}, :infinity) end + @doc """ Connect to a server with the provided server and port via SSL - Example: - Client.connect! pid, "localhost", 6697 + ## Example: + + iex> Client.connect!(pid, "localhost", 6697) + :ok + """ - @spec connect_ssl!(client :: pid, server :: binary, port :: non_neg_integer, options :: list() | nil) :: :ok + @spec connect_ssl!( + client :: pid, + server :: binary, + port :: non_neg_integer, + options :: list() | nil + ) :: :ok def connect_ssl!(client, server, port, options \\ []) do GenServer.call(client, {:connect, server, port, options, true}, :infinity) end + @doc """ Determine if the provided client process has an open connection to a server """ @@ -95,76 +141,92 @@ defmodule ExIRC.Client do def is_connected?(client) do GenServer.call(client, :is_connected?) end + @doc """ Logon to a server - Example: - Client.logon pid, "password", "mynick", "user", "My Name" + ## Example: + + iex> Client.logon(pid, "password", "mynick", "user", "My Name") + :ok + """ - @spec logon(client :: pid, pass :: binary, nick :: binary, user :: binary, name :: binary) :: :ok | {:error, :not_connected} + @spec logon(client :: pid, pass :: binary, nick :: binary, user :: binary, name :: binary) :: + :ok | {:error, :not_connected} def logon(client, pass, nick, user, name) do GenServer.call(client, {:logon, pass, nick, user, name}, :infinity) end + @doc """ - Determine if the provided client is logged on to a server + Determine if the provided client is logged on to a server. """ @spec is_logged_on?(client :: pid) :: true | false def is_logged_on?(client) do GenServer.call(client, :is_logged_on?) end + @doc """ - Send a message to a nick or channel + Send a message to a nick or channel. + Message types are: - :privmsg - :notice - :ctcp + * `:privmsg` + * `:notice` + * `:ctcp` + """ @spec msg(client :: pid, type :: atom, nick :: binary, msg :: binary) :: :ok def msg(client, type, nick, msg) do GenServer.call(client, {:msg, type, nick, msg}, :infinity) end + @doc """ - Send an action message, i.e. (/me slaps someone with a big trout) + Send an action message, i.e. (/me slaps someone with a big trout). """ @spec me(client :: pid, channel :: binary, msg :: binary) :: :ok def me(client, channel, msg) do GenServer.call(client, {:me, channel, msg}, :infinity) end + @doc """ - Change the client's nick + Change the client's nick. """ @spec nick(client :: pid, new_nick :: binary) :: :ok def nick(client, new_nick) do GenServer.call(client, {:nick, new_nick}, :infinity) end + @doc """ - Send a raw IRC command + Send a raw IRC command. """ @spec cmd(client :: pid, raw_cmd :: binary) :: :ok def cmd(client, raw_cmd) do GenServer.call(client, {:cmd, raw_cmd}) end + @doc """ - Join a channel, with an optional password + Join a channel, with an optional password. """ @spec join(client :: pid, channel :: binary, key :: binary | nil) :: :ok def join(client, channel, key \\ "") do GenServer.call(client, {:join, channel, key}, :infinity) end + @doc """ - Leave a channel + Leave a channel. """ @spec part(client :: pid, channel :: binary) :: :ok def part(client, channel) do GenServer.call(client, {:part, channel}, :infinity) end + @doc """ - Kick a user from a channel + Kick a user from a channel. """ @spec kick(client :: pid, channel :: binary, nick :: binary, message :: binary | nil) :: :ok def kick(client, channel, nick, message \\ "") do GenServer.call(client, {:kick, channel, nick, message}, :infinity) end + @spec names(client :: pid, channel :: binary) :: :ok def names(client, channel) do GenServer.call(client, {:names, channel}, :infinity) @@ -179,7 +241,7 @@ defmodule ExIRC.Client do end @doc """ - Ask the server for the channel's users + Ask the server for the channel's users. """ @spec who(client :: pid, channel :: binary) :: :ok def who(client, channel) do @@ -187,112 +249,129 @@ defmodule ExIRC.Client do end @doc """ - Change mode for a user or channel + Change mode for a user or channel. """ - @spec mode(client :: pid, channel_or_nick :: binary, flags :: binary, args :: binary | nil) :: :ok + @spec mode(client :: pid, channel_or_nick :: binary, flags :: binary, args :: binary | nil) :: + :ok def mode(client, channel_or_nick, flags, args \\ "") do GenServer.call(client, {:mode, channel_or_nick, flags, args}, :infinity) end + @doc """ - Invite a user to a channel + Invite a user to a channel. """ @spec invite(client :: pid, nick :: binary, channel :: binary) :: :ok def invite(client, nick, channel) do GenServer.call(client, {:invite, nick, channel}, :infinity) end + @doc """ - Quit the server, with an optional part message + Quit the server, with an optional part message. """ @spec quit(client :: pid, msg :: binary | nil) :: :ok def quit(client, msg \\ "Leaving..") do GenServer.call(client, {:quit, msg}, :infinity) end + @doc """ - Get details about each of the client's currently joined channels + Get details about each of the client's currently joined channels. """ @spec channels(client :: pid) :: [binary] def channels(client) do GenServer.call(client, :channels) end + @doc """ - Get a list of users in the provided channel + Get a list of users in the provided channel. """ @spec channel_users(client :: pid, channel :: binary) :: [binary] | {:error, atom} def channel_users(client, channel) do GenServer.call(client, {:channel_users, channel}) end + @doc """ - Get the topic of the provided channel + Get the topic of the provided channel. """ @spec channel_topic(client :: pid, channel :: binary) :: binary | {:error, atom} def channel_topic(client, channel) do GenServer.call(client, {:channel_topic, channel}) end + @doc """ - Get the channel type of the provided channel + Get the channel type of the provided channel. """ @spec channel_type(client :: pid, channel :: binary) :: atom | {:error, atom} def channel_type(client, channel) do GenServer.call(client, {:channel_type, channel}) end + @doc """ - Determine if a nick is present in the provided channel + Determine if a nick is present in the provided channel. """ - @spec channel_has_user?(client :: pid, channel :: binary, nick :: binary) :: boolean | {:error, atom} + @spec channel_has_user?(client :: pid, channel :: binary, nick :: binary) :: + boolean | {:error, atom} def channel_has_user?(client, channel, nick) do GenServer.call(client, {:channel_has_user?, channel, nick}) end + @doc """ - Add a new event handler process + Add a new event handler process. """ @spec add_handler(client :: pid, pid) :: :ok def add_handler(client, pid) do GenServer.call(client, {:add_handler, pid}) end + @doc """ - Add a new event handler process, asynchronously + Add a new event handler process, asynchronously. """ @spec add_handler_async(client :: pid, pid) :: :ok def add_handler_async(client, pid) do GenServer.cast(client, {:add_handler, pid}) end + @doc """ - Remove an event handler process + Remove an event handler process. """ @spec remove_handler(client :: pid, pid) :: :ok def remove_handler(client, pid) do GenServer.call(client, {:remove_handler, pid}) end + @doc """ - Remove an event handler process, asynchronously + Remove an event handler process, asynchronously. """ @spec remove_handler_async(client :: pid, pid) :: :ok def remove_handler_async(client, pid) do GenServer.cast(client, {:remove_handler, pid}) end + @doc """ - Get the current state of the provided client + Get the current state of the provided client. """ @spec state(client :: pid) :: [{atom, any}] def state(client) do state = GenServer.call(client, :state) - [server: state.server, - port: state.port, - nick: state.nick, - pass: state.pass, - user: state.user, - name: state.name, - autoping: state.autoping, - ssl?: state.ssl?, - connected?: state.connected?, - logged_on?: state.logged_on?, - channel_prefixes: state.channel_prefixes, - user_prefixes: state.user_prefixes, - channels: Channels.to_proplist(state.channels), - network: state.network, - login_time: state.login_time, - debug?: state.debug?, - event_handlers: state.event_handlers] + + [ + server: state.server, + port: state.port, + nick: state.nick, + pass: state.pass, + user: state.user, + name: state.name, + autoping: state.autoping, + ssl?: state.ssl?, + connected?: state.connected?, + logged_on?: state.logged_on?, + channel_prefixes: state.channel_prefixes, + user_prefixes: state.user_prefixes, + channels: Channels.to_proplist(state.channels), + network: state.network, + login_time: state.login_time, + debug?: state.debug?, + event_handlers: state.event_handlers + ] end ############### @@ -300,118 +379,156 @@ defmodule ExIRC.Client do ############### @doc """ - Called when GenServer initializes the client + Called when GenServer initializes the client. """ - @spec init(list(any) | []) :: {:ok, ClientState.t} + @spec init(list(any) | []) :: {:ok, ClientState.t()} def init(options \\ []) do autoping = Keyword.get(options, :autoping, true) - debug = Keyword.get(options, :debug, false) - owner = Keyword.fetch!(options, :owner) - # Add event handlers + debug = Keyword.get(options, :debug, false) + owner = Keyword.fetch!(options, :owner) + + # Add event handlers. handlers = - Keyword.get(options, :event_handlers, []) + options + |> Keyword.get(:event_handlers, []) |> List.foldl([], &do_add_handler/2) + ref = Process.monitor(owner) - # Return initial state - {:ok, %ClientState{ - event_handlers: handlers, - autoping: autoping, - logged_on?: false, - debug?: debug, - channels: ExIRC.Channels.init(), - owner: {owner, ref}}} + + # Return initial state. + {:ok, + %ClientState{ + event_handlers: handlers, + autoping: autoping, + logged_on?: false, + debug?: debug, + channels: ExIRC.Channels.init(), + owner: {owner, ref} + }} end + @doc """ Handle calls from the external API. It is not recommended to call these directly. """ - # Handle call to get the current state of the client process + # Handle call to get the current state of the client process. def handle_call(:state, _from, state), do: {:reply, state, state} - # Handle call to stop the current client process + # Handle call to stop the current client process. def handle_call(:stop, _from, state) do - # Ensure the socket connection is closed if stop is called while still connected to the server + # Ensure the socket connection is closed if stop is called while still connected to the server. if state.connected?, do: Transport.close(state) {:stop, :normal, :ok, %{state | connected?: false, logged_on?: false, socket: nil}} end - # Handles call to add a new event handler process + + # Handles call to add a new event handler process. def handle_call({:add_handler, pid}, _from, state) do handlers = do_add_handler(pid, state.event_handlers) {:reply, :ok, %{state | event_handlers: handlers}} end - # Handles call to remove an event handler process + + # Handles call to remove an event handler process. def handle_call({:remove_handler, pid}, _from, state) do handlers = do_remove_handler(pid, state.event_handlers) {:reply, :ok, %{state | event_handlers: handlers}} end - # Handle call to connect to an IRC server + + # Handle call to connect to an IRC server. def handle_call({:connect, server, port, options, ssl}, _from, state) do # If there is an open connection already, close it. if state.socket != nil, do: Transport.close(state) - # Set SSL mode + # Set SSL mode. state = %{state | ssl?: ssl} - # Open a new connection - case Transport.connect(state, String.to_charlist(server), port, [:list, {:packet, :line}, {:keepalive, true}] ++ options) do + + # Open a new connection. + case Transport.connect( + state, + String.to_charlist(server), + port, + [:list, {:packet, :line}, {:keepalive, true}] ++ options + ) do {:ok, socket} -> - send_event {:connected, server, port}, state + send_event({:connected, server, port}, state) {:reply, :ok, %{state | connected?: true, server: server, port: port, socket: socket}} + error -> {:reply, error, state} end end - # Handle call to determine if the client is connected + + # Handle call to determine if the client is connected. def handle_call(:is_connected?, _from, state), do: {:reply, state.connected?, state} + # Prevents any of the following messages from being handled if the client is not connected to a server. # Instead, it returns {:error, :not_connected}. - def handle_call(_, _from, %ClientState{connected?: false} = state), do: {:reply, {:error, :not_connected}, state} - # Handle call to login to the connected IRC server - def handle_call({:logon, pass, nick, user, name}, _from, %ClientState{logged_on?: false} = state) do - Transport.send state, pass!(pass) - Transport.send state, nick!(nick) - Transport.send state, user!(user, name) - {:reply, :ok, %{state | pass: pass, nick: nick, user: user, name: name} } + def handle_call(_, _from, %ClientState{connected?: false} = state), + do: {:reply, {:error, :not_connected}, state} + + # Handle call to login to the connected IRC server. + def handle_call( + {:logon, pass, nick, user, name}, + _from, + %ClientState{logged_on?: false} = state + ) do + Transport.send(state, pass!(pass)) + Transport.send(state, nick!(nick)) + Transport.send(state, user!(user, name)) + {:reply, :ok, %{state | pass: pass, nick: nick, user: user, name: name}} end + # Handles call to change the client's nick. def handle_call({:nick, new_nick}, _from, %ClientState{logged_on?: false} = state) do - Transport.send state, nick!(new_nick) + Transport.send(state, nick!(new_nick)) + # Since we've not yet logged on, we won't get a nick change message, so we have to remember the nick here. {:reply, :ok, %{state | nick: new_nick}} end - # Handle call to determine if client is logged on to a server + + # Handle call to determine if client is logged on to a server. def handle_call(:is_logged_on?, _from, state), do: {:reply, state.logged_on?, state} + # Prevents any of the following messages from being handled if the client is not logged on to a server. # Instead, it returns {:error, :not_logged_in}. - def handle_call(_, _from, %ClientState{logged_on?: false} = state), do: {:reply, {:error, :not_logged_in}, state} - # Handles call to send a message + def handle_call(_, _from, %ClientState{logged_on?: false} = state), + do: {:reply, {:error, :not_logged_in}, state} + + # Handles call to send a message. def handle_call({:msg, type, nick, msg}, _from, state) do - data = case type do - :privmsg -> privmsg!(nick, msg) - :notice -> notice!(nick, msg) - :ctcp -> notice!(nick, ctcp!(msg)) - end - Transport.send state, data + data = + case type do + :privmsg -> privmsg!(nick, msg) + :notice -> notice!(nick, msg) + :ctcp -> notice!(nick, ctcp!(msg)) + end + + Transport.send(state, data) {:reply, :ok, state} end - # Handle /me messages + + # Handle /me messages. def handle_call({:me, channel, msg}, _from, state) do data = me!(channel, msg) - Transport.send state, data + Transport.send(state, data) {:reply, :ok, state} end - # Handles call to join a channel + + # Handles call to join a channel. def handle_call({:join, channel, key}, _from, state) do Transport.send(state, join!(channel, key)) {:reply, :ok, state} end - # Handles a call to leave a channel + + # Handles a call to leave a channel. def handle_call({:part, channel}, _from, state) do Transport.send(state, part!(channel)) {:reply, :ok, state} end - # Handles a call to kick a client + + # Handles a call to kick a client. def handle_call({:kick, channel, nick, message}, _from, state) do Transport.send(state, kick!(channel, nick, message)) {:reply, :ok, state} end - # Handles a call to send the NAMES command to the server + + # Handles a call to send the NAMES command to the server. def handle_call({:names, channel}, _from, state) do Transport.send(state, names!(channel)) {:reply, :ok, state} @@ -427,136 +544,177 @@ defmodule ExIRC.Client do {:reply, :ok, state} end - # Handles a call to change mode for a user or channel + # Handles a call to change mode for a user or channel. def handle_call({:mode, channel_or_nick, flags, args}, _from, state) do Transport.send(state, mode!(channel_or_nick, flags, args)) {:reply, :ok, state} end - # Handle call to invite a user to a channel + + # Handle call to invite a user to a channel. def handle_call({:invite, nick, channel}, _from, state) do Transport.send(state, invite!(nick, channel)) {:reply, :ok, state} end - # Handle call to quit the server and close the socket connection + + # Handle call to quit the server and close the socket connection. def handle_call({:quit, msg}, _from, state) do if state.connected? do - Transport.send state, quit!(msg) + Transport.send(state, quit!(msg)) send_event(:disconnected, state) - Transport.close state + Transport.close(state) end + {:reply, :ok, %{state | connected?: false, logged_on?: false, socket: nil}} end - # Handles call to change the client's nick - def handle_call({:nick, new_nick}, _from, state) do Transport.send(state, nick!(new_nick)); {:reply, :ok, state} end - # Handles call to send a raw command to the IRC server - def handle_call({:cmd, raw_cmd}, _from, state) do Transport.send(state, command!(raw_cmd)); {:reply, :ok, state} end - # Handles call to return the client's channel data + + # Handles call to change the client's nick. + def handle_call({:nick, new_nick}, _from, state) do + Transport.send(state, nick!(new_nick)) + {:reply, :ok, state} + end + + # Handles call to send a raw command to the IRC server. + def handle_call({:cmd, raw_cmd}, _from, state) do + Transport.send(state, command!(raw_cmd)) + {:reply, :ok, state} + end + + # Handles call to return the client's channel data. def handle_call(:channels, _from, state), do: {:reply, Channels.channels(state.channels), state} - # Handles call to return a list of users for a given channel - def handle_call({:channel_users, channel}, _from, state), do: {:reply, Channels.channel_users(state.channels, channel), state} - # Handles call to return the given channel's topic - def handle_call({:channel_topic, channel}, _from, state), do: {:reply, Channels.channel_topic(state.channels, channel), state} - # Handles call to return the type of the given channel - def handle_call({:channel_type, channel}, _from, state), do: {:reply, Channels.channel_type(state.channels, channel), state} - # Handles call to determine if a nick is present in the given channel + # Handles call to return a list of users for a given channel. + def handle_call({:channel_users, channel}, _from, state), + do: {:reply, Channels.channel_users(state.channels, channel), state} + + # Handles call to return the given channel's topic. + def handle_call({:channel_topic, channel}, _from, state), + do: {:reply, Channels.channel_topic(state.channels, channel), state} + + # Handles call to return the type of the given channel. + def handle_call({:channel_type, channel}, _from, state), + do: {:reply, Channels.channel_type(state.channels, channel), state} + + # Handles call to determine if a nick is present in the given channel. def handle_call({:channel_has_user?, channel, nick}, _from, state) do {:reply, Channels.channel_has_user?(state.channels, channel, nick), state} end - # Handles message to add a new event handler process asynchronously + + @doc """ + Handles asynchronous messages from the external API. Not recommended to call these directly. + """ + # Handles message to add a new event handler process asynchronously. def handle_cast({:add_handler, pid}, state) do handlers = do_add_handler(pid, state.event_handlers) {:noreply, %{state | event_handlers: handlers}} end - @doc """ - Handles asynchronous messages from the external API. Not recommended to call these directly. - """ - # Handles message to remove an event handler process asynchronously + + # Handles message to remove an event handler process asynchronously. def handle_cast({:remove_handler, pid}, state) do handlers = do_remove_handler(pid, state.event_handlers) {:noreply, %{state | event_handlers: handlers}} end + @doc """ - Handle messages from the TCP socket connection. + Handles asynchronous messages. """ - # Handles the client's socket connection 'closed' event + # Handles the client's TCP socket connection 'closed' event. def handle_info({:tcp_closed, _socket}, %ClientState{server: server, port: port} = state) do - info "Connection to #{server}:#{port} closed!" - send_event :disconnected, state - new_state = %{state | - socket: nil, - connected?: false, - logged_on?: false, - channels: Channels.init() + info("Connection to #{server}:#{port} closed!") + send_event(:disconnected, state) + + new_state = %{ + state + | socket: nil, + connected?: false, + logged_on?: false, + channels: Channels.init() } + {:noreply, new_state} end - @doc """ - Handle messages from the SSL socket connection. - """ - # Handles the client's socket connection 'closed' event + + # Handles the client's SSL socket connection 'closed' event. def handle_info({:ssl_closed, socket}, state) do handle_info({:tcp_closed, socket}, state) end - # Handles any TCP errors in the client's socket connection + + # Handles any TCP errors in the client's socket connection. def handle_info({:tcp_error, socket, reason}, %ClientState{server: server, port: port} = state) do - error "TCP error in connection to #{server}:#{port}:\r\n#{reason}\r\nClient connection closed." - new_state = %{state | - socket: nil, - connected?: false, - logged_on?: false, - channels: Channels.init() + error( + "TCP error in connection to #{server}:#{port}:\r\n#{reason}\r\nClient connection closed." + ) + + new_state = %{ + state + | socket: nil, + connected?: false, + logged_on?: false, + channels: Channels.init() } + {:stop, {:tcp_error, socket}, new_state} end - # Handles any SSL errors in the client's socket connection + + # Handles any SSL errors in the client's socket connection. def handle_info({:ssl_error, socket, reason}, state) do handle_info({:tcp_error, socket, reason}, state) end - # General handler for messages from the IRC server + + # General handler for messages from the IRC server. def handle_info({:tcp, _, data}, state) do debug? = state.debug? + case Utils.parse(data) do %ExIRC.Message{ctcp: true} = msg -> - handle_data msg, state + handle_data(msg, state) {:noreply, state} + %ExIRC.Message{ctcp: false} = msg -> - handle_data msg, state + handle_data(msg, state) + %ExIRC.Message{ctcp: :invalid} = msg when debug? -> - send_event msg, state + send_event(msg, state) {:noreply, state} + _ -> {:noreply, state} end end - # Wrapper for SSL socket messages + + # Wrapper for SSL socket messages. def handle_info({:ssl, socket, data}, state) do handle_info({:tcp, socket, data}, state) end - # If the owner process dies, we should die as well + + # If the owner process dies, we should die as well. def handle_info({:DOWN, ref, _, pid, reason}, %{owner: {pid, ref}} = state) do {:stop, reason, state} end - # If an event handler process dies, remove it from the list of event handlers + + # If an event handler process dies, remove it from the list of event handlers. def handle_info({:DOWN, _, _, pid, _}, state) do handlers = do_remove_handler(pid, state.event_handlers) {:noreply, %{state | event_handlers: handlers}} end - # Catch-all for unrecognized messages (do nothing) + + # Catch-all for unrecognized messages (do nothing). def handle_info(_, state) do {:noreply, state} end + @doc """ - Handle termination + Handle termination. """ def terminate(_reason, state) do if state.socket != nil do - Transport.close state + Transport.close(state) %{state | socket: nil} end + :ok end + @doc """ - Transform state for hot upgrades/downgrades + Transform state for hot upgrades/downgrades. """ def code_change(_old, state, _extra), do: {:ok, state} @@ -567,313 +725,452 @@ defmodule ExIRC.Client do @doc """ Handle ExIRC.Messages received from the server. """ - # Called upon successful login + # Called upon successful login. def handle_data(%ExIRC.Message{cmd: @rpl_welcome}, %ClientState{logged_on?: false} = state) do - if state.debug?, do: debug "SUCCESFULLY LOGGED ON" + if state.debug?, do: debug("SUCCESFULLY LOGGED ON") new_state = %{state | logged_on?: true, login_time: :erlang.timestamp()} - send_event :logged_in, new_state + send_event(:logged_in, new_state) {:noreply, new_state} end - # Called when trying to log in with a nickname that is in use + + # Called when trying to log in with a nickname that is in use. def handle_data(%ExIRC.Message{cmd: @err_nick_in_use}, %ClientState{logged_on?: false} = state) do - if state.debug?, do: debug "ERROR: NICK IN USE" - send_event {:login_failed, :nick_in_use}, state + if state.debug?, do: debug("ERROR: NICK IN USE") + send_event({:login_failed, :nick_in_use}, state) {:noreply, state} end - # Called when the server sends it's current capabilities + + # Called when the server sends it's current capabilities. def handle_data(%ExIRC.Message{cmd: @rpl_isupport} = msg, state) do - if state.debug?, do: debug "RECEIVING SERVER CAPABILITIES" + if state.debug?, do: debug("RECEIVING SERVER CAPABILITIES") {:noreply, Utils.isup(msg.args, state)} end - # Called when the client enters a channel + + # Called when the client enters a channel. def handle_data(%ExIRC.Message{nick: nick, cmd: "JOIN"} = msg, %ClientState{nick: nick} = state) do - channel = msg.args |> List.first |> String.trim - if state.debug?, do: debug "JOINED A CHANNEL #{channel}" - channels = Channels.join(state.channels, channel) + channel = msg.args |> List.first() |> String.trim() + if state.debug?, do: debug("JOINED A CHANNEL #{channel}") + channels = Channels.join(state.channels, channel) new_state = %{state | channels: channels} - send_event {:joined, channel}, new_state + send_event({:joined, channel}, new_state) {:noreply, new_state} end - # Called when another user joins a channel the client is in - def handle_data(%ExIRC.Message{nick: user_nick, cmd: "JOIN", host: host, user: user} = msg, state) do + + # Called when another user joins a channel the client is in. + def handle_data( + %ExIRC.Message{nick: user_nick, cmd: "JOIN", host: host, user: user} = msg, + state + ) do sender = %SenderInfo{nick: user_nick, host: host, user: user} - channel = msg.args |> List.first |> String.trim - if state.debug?, do: debug "ANOTHER USER JOINED A CHANNEL: #{channel} - #{user_nick}" - channels = Channels.user_join(state.channels, channel, user_nick) + channel = msg.args |> List.first() |> String.trim() + if state.debug?, do: debug("ANOTHER USER JOINED A CHANNEL: #{channel} - #{user_nick}") + channels = Channels.user_join(state.channels, channel, user_nick) new_state = %{state | channels: channels} - send_event {:joined, channel, sender}, new_state + send_event({:joined, channel, sender}, new_state) {:noreply, new_state} end - # Called on joining a channel, to tell us the channel topic - # Message with three arguments is not RFC compliant but very common - # Message with two arguments is RFC compliant + + # Called on joining a channel, to tell us the channel topic. + # Message with three arguments is not RFC compliant but very common. + # Message with two arguments is RFC compliant. # Message with a single argument is not RFC compliant, but is present # to handle poorly written IRC servers which send RPL_TOPIC with an empty - # topic (such as Slack's IRC bridge), when they should be sending RPL_NOTOPIC + # topic (such as Slack's IRC bridge), when they should be sending RPL_NOTOPIC. def handle_data(%ExIRC.Message{cmd: @rpl_topic} = msg, state) do - {channel, topic} = case msg.args do - [_nick, channel, topic] -> {channel, topic} - [channel, topic] -> {channel, topic} - [channel] -> {channel, "No topic is set"} - end + {channel, topic} = + case msg.args do + [_nick, channel, topic] -> {channel, topic} + [channel, topic] -> {channel, topic} + [channel] -> {channel, "No topic is set"} + end + if state.debug? do - debug "INITIAL TOPIC MSG" - debug "1. TOPIC SET FOR #{channel} TO #{topic}" + debug("INITIAL TOPIC MSG") + debug("1. TOPIC SET FOR #{channel} TO #{topic}") end - channels = Channels.set_topic(state.channels, channel, topic) + + channels = Channels.set_topic(state.channels, channel, topic) new_state = %{state | channels: channels} - send_event {:topic_changed, channel, topic}, new_state + send_event({:topic_changed, channel, topic}, new_state) {:noreply, new_state} end - ## WHOIS - def handle_data(%ExIRC.Message{cmd: @rpl_whoisuser, args: [_sender, nick, user, hostname, _, name]}, state) do + def handle_data( + %ExIRC.Message{cmd: @rpl_whoisuser, args: [_sender, nick, user, hostname, _, name]}, + state + ) do user = %{nick: nick, user: user, hostname: hostname, name: name} - {:noreply, %ClientState{state|whois_buffers: Map.put(state.whois_buffers, nick, user)}} + {:noreply, %ClientState{state | whois_buffers: Map.put(state.whois_buffers, nick, user)}} end - def handle_data(%ExIRC.Message{cmd: @rpl_whoiscertfp, args: [_sender, nick, "has client certificate fingerprint "<> fingerprint]}, state) do - {:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nick, :certfp], fingerprint)}} + def handle_data( + %ExIRC.Message{ + cmd: @rpl_whoiscertfp, + args: [_sender, nick, "has client certificate fingerprint " <> fingerprint] + }, + state + ) do + {:noreply, + %ClientState{ + state + | whois_buffers: put_in(state.whois_buffers, [nick, :certfp], fingerprint) + }} end def handle_data(%ExIRC.Message{cmd: @rpl_whoisregnick, args: [_sender, nick, _message]}, state) do - {:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nick, :registered_nick?], true)}} + {:noreply, + %ClientState{ + state + | whois_buffers: put_in(state.whois_buffers, [nick, :registered_nick?], true) + }} end def handle_data(%ExIRC.Message{cmd: @rpl_whoishelpop, args: [_sender, nick, _message]}, state) do - {:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nick, :helpop?], true)}} + {:noreply, + %ClientState{state | whois_buffers: put_in(state.whois_buffers, [nick, :helpop?], true)}} end def handle_data(%ExIRC.Message{cmd: @rpl_whoischannels, args: [_sender, nick, channels]}, state) do chans = String.split(channels, " ") - {:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nick, :channels], chans)}} + + {:noreply, + %ClientState{state | whois_buffers: put_in(state.whois_buffers, [nick, :channels], chans)}} end + def handle_data( + %ExIRC.Message{cmd: @rpl_whoisserver, args: [_sender, nick, server_addr, server_name]}, + state + ) do + new_buffer = + state.whois_buffers + |> put_in([nick, :server_name], server_name) + |> put_in([nick, :server_address], server_addr) - def handle_data(%ExIRC.Message{cmd: @rpl_whoisserver, args: [_sender, nick, server_addr, server_name]}, state) do - new_buffer = state.whois_buffers - |> put_in([nick, :server_name], server_name) - |> put_in([nick, :server_address], server_addr) - {:noreply, %ClientState{state|whois_buffers: new_buffer}} + {:noreply, %ClientState{state | whois_buffers: new_buffer}} end def handle_data(%ExIRC.Message{cmd: @rpl_whoisoperator, args: [_sender, nick, _message]}, state) do - {:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nick, :ircop?], true)}} + {:noreply, + %ClientState{state | whois_buffers: put_in(state.whois_buffers, [nick, :ircop?], true)}} end - def handle_data(%ExIRC.Message{cmd: @rpl_whoisaccount, args: [_sender, nick, account_name, _message]}, state) do - {:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nick, :account_name], account_name)}} + def handle_data( + %ExIRC.Message{cmd: @rpl_whoisaccount, args: [_sender, nick, account_name, _message]}, + state + ) do + {:noreply, + %ClientState{ + state + | whois_buffers: put_in(state.whois_buffers, [nick, :account_name], account_name) + }} end def handle_data(%ExIRC.Message{cmd: @rpl_whoissecure, args: [_sender, nick, _message]}, state) do - {:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nick, :ssl?], true)}} + {:noreply, + %ClientState{state | whois_buffers: put_in(state.whois_buffers, [nick, :ssl?], true)}} end - def handle_data(%ExIRC.Message{cmd: @rpl_whoisidle, args: [_sender, nick, idling_time, signon_time, _message]}, state) do - new_buffer = state.whois_buffers - |> put_in([nick, :idling_time], idling_time) - |> put_in([nick, :signon_time], signon_time) - {:noreply, %ClientState{state|whois_buffers: new_buffer}} + def handle_data( + %ExIRC.Message{ + cmd: @rpl_whoisidle, + args: [_sender, nick, idling_time, signon_time, _message] + }, + state + ) do + new_buffer = + state.whois_buffers + |> put_in([nick, :idling_time], idling_time) + |> put_in([nick, :signon_time], signon_time) + + {:noreply, %ClientState{state | whois_buffers: new_buffer}} end def handle_data(%ExIRC.Message{cmd: @rpl_endofwhois, args: [_sender, nick, _message]}, state) do buffer = struct(ExIRC.Whois, state.whois_buffers[nick]) - send_event {:whois, buffer}, state - {:noreply, %ClientState{state|whois_buffers: Map.delete(state.whois_buffers, nick)}} + send_event({:whois, buffer}, state) + {:noreply, %ClientState{state | whois_buffers: Map.delete(state.whois_buffers, nick)}} end ## WHO - def handle_data(%ExIRC.Message{:cmd => "352", :args => [_, channel, user, host, server, nick, mode, hop_and_realn]}, state) do + def handle_data( + %ExIRC.Message{ + :cmd => "352", + :args => [_, channel, user, host, server, nick, mode, hop_and_realn] + }, + state + ) do [hop, name] = String.split(hop_and_realn, " ", parts: 2) :binary.compile_pattern(["@", "&", "+"]) - admin? = String.contains?(mode, "&") - away? = String.contains?(mode, "G") - founder? = String.contains?(mode, "~") - half_operator? = String.contains?(mode, "%") - operator? = founder? || admin? || String.contains?(mode, "@") - server_operator? = String.contains?(mode, "*") - voiced? = String.contains?(mode, "+") - - nick = %{nick: nick, user: user, name: name, server: server, hops: hop, admin?: admin?, - away?: away?, founder?: founder?, half_operator?: half_operator?, host: host, - operator?: operator?, server_operator?: server_operator?, voiced?: voiced? - } + admin? = String.contains?(mode, "&") + away? = String.contains?(mode, "G") + founder? = String.contains?(mode, "~") + half_operator? = String.contains?(mode, "%") + operator? = founder? || admin? || String.contains?(mode, "@") + server_operator? = String.contains?(mode, "*") + voiced? = String.contains?(mode, "+") + + nick = %{ + nick: nick, + user: user, + name: name, + server: server, + hops: hop, + admin?: admin?, + away?: away?, + founder?: founder?, + half_operator?: half_operator?, + host: host, + operator?: operator?, + server_operator?: server_operator?, + voiced?: voiced? + } buffer = Map.get(state.who_buffers, channel, []) - {:noreply, %ClientState{state | who_buffers: Map.put(state.who_buffers, channel, [nick|buffer])}} + + {:noreply, + %ClientState{state | who_buffers: Map.put(state.who_buffers, channel, [nick | buffer])}} end def handle_data(%ExIRC.Message{:cmd => "315", :args => [_, channel, _]}, state) do - buffer = state - |> Map.get(:who_buffers) - |> Map.get(channel) - |> Enum.map(fn user -> struct(ExIRC.Who, user) end) + buffer = + state + |> Map.get(:who_buffers) + |> Map.get(channel) + |> Enum.map(fn user -> struct(ExIRC.Who, user) end) - send_event {:who, channel, buffer}, state + send_event({:who, channel, buffer}, state) {:noreply, %ClientState{state | who_buffers: Map.delete(state.who_buffers, channel)}} end def handle_data(%ExIRC.Message{cmd: @rpl_notopic, args: [channel]}, state) do if state.debug? do - debug "INITIAL TOPIC MSG" - debug "1. NO TOPIC SET FOR #{channel}}" + debug("INITIAL TOPIC MSG") + debug("1. NO TOPIC SET FOR #{channel}}") end + channels = Channels.set_topic(state.channels, channel, "No topic is set") new_state = %{state | channels: channels} {:noreply, new_state} end - # Called when the topic changes while we're in the channel + + # Called when the topic changes while we're in the channel. def handle_data(%ExIRC.Message{cmd: "TOPIC", args: [channel, topic]}, state) do - if state.debug?, do: debug "TOPIC CHANGED FOR #{channel} TO #{topic}" - channels = Channels.set_topic(state.channels, channel, topic) + if state.debug?, do: debug("TOPIC CHANGED FOR #{channel} TO #{topic}") + channels = Channels.set_topic(state.channels, channel, topic) new_state = %{state | channels: channels} - send_event {:topic_changed, channel, topic}, new_state + send_event({:topic_changed, channel, topic}, new_state) {:noreply, new_state} end - # Called when joining a channel with the list of current users in that channel, or when the NAMES command is sent + + # Called when joining a channel with the list of current users in that channel, or when the NAMES command is sent. def handle_data(%ExIRC.Message{cmd: @rpl_namereply} = msg, state) do - if state.debug?, do: debug "NAMES LIST RECEIVED" - {_nick, channel_type, channel, names} = case msg.args do - [nick, channel_type, channel, names] -> {nick, channel_type, channel, names} - [channel_type, channel, names] -> {nil, channel_type, channel, names} - end - channels = Channels.set_type( - Channels.users_join(state.channels, channel, String.split(names, " ", trim: true)), - channel, - channel_type) + if state.debug?, do: debug("NAMES LIST RECEIVED") + + {_nick, channel_type, channel, names} = + case msg.args do + [nick, channel_type, channel, names] -> {nick, channel_type, channel, names} + [channel_type, channel, names] -> {nil, channel_type, channel, names} + end + + channels = + Channels.set_type( + Channels.users_join(state.channels, channel, String.split(names, " ", trim: true)), + channel, + channel_type + ) send_event({:names_list, channel, names}, state) {:noreply, %{state | channels: channels}} end - # Called when our nick has succesfully changed - def handle_data(%ExIRC.Message{cmd: "NICK", nick: nick, args: [new_nick]}, %ClientState{nick: nick} = state) do - if state.debug?, do: debug "NICK CHANGED FROM #{nick} TO #{new_nick}" + + # Called when our nick has succesfully changed. + def handle_data( + %ExIRC.Message{cmd: "NICK", nick: nick, args: [new_nick]}, + %ClientState{nick: nick} = state + ) do + if state.debug?, do: debug("NICK CHANGED FROM #{nick} TO #{new_nick}") new_state = %{state | nick: new_nick} - send_event {:nick_changed, new_nick}, new_state + send_event({:nick_changed, new_nick}, new_state) {:noreply, new_state} end - # Called when someone visible to us changes their nick + + # Called when someone visible to us changes their nick. def handle_data(%ExIRC.Message{cmd: "NICK", nick: nick, args: [new_nick]}, state) do - if state.debug?, do: debug "#{nick} CHANGED THEIR NICK TO #{new_nick}" - channels = Channels.user_rename(state.channels, nick, new_nick) + if state.debug?, do: debug("#{nick} CHANGED THEIR NICK TO #{new_nick}") + channels = Channels.user_rename(state.channels, nick, new_nick) new_state = %{state | channels: channels} - send_event {:nick_changed, nick, new_nick}, new_state + send_event({:nick_changed, nick, new_nick}, new_state) {:noreply, new_state} end - # Called upon mode change + + # Called upon mode change. def handle_data(%ExIRC.Message{cmd: "MODE", args: [channel, op, user]}, state) do - if state.debug?, do: debug "MODE #{channel} #{op} #{user}" - send_event {:mode, [channel, op, user]}, state + if state.debug?, do: debug("MODE #{channel} #{op} #{user}") + send_event({:mode, [channel, op, user]}, state) {:noreply, state} end - # Called when we leave a channel + # Called when we leave a channel. def handle_data(%ExIRC.Message{cmd: "PART", nick: nick} = msg, %ClientState{nick: nick} = state) do - - channel = msg.args |> List.first |> String.trim - if state.debug?, do: debug "WE LEFT A CHANNEL: #{channel}" - channels = Channels.part(state.channels, channel) + channel = msg.args |> List.first() |> String.trim() + if state.debug?, do: debug("WE LEFT A CHANNEL: #{channel}") + channels = Channels.part(state.channels, channel) new_state = %{state | channels: channels} - send_event {:parted, channel}, new_state + send_event({:parted, channel}, new_state) {:noreply, new_state} end - # Called when someone else in our channel leaves + + # Called when someone else in our channel leaves. def handle_data(%ExIRC.Message{cmd: "PART", nick: from, host: host, user: user} = msg, state) do sender = %SenderInfo{nick: from, host: host, user: user} - channel = msg.args |> List.first |> String.trim - if state.debug?, do: debug "#{from} LEFT A CHANNEL: #{channel}" - channels = Channels.user_part(state.channels, channel, from) + channel = msg.args |> List.first() |> String.trim() + if state.debug?, do: debug("#{from} LEFT A CHANNEL: #{channel}") + channels = Channels.user_part(state.channels, channel, from) new_state = %{state | channels: channels} - send_event {:parted, channel, sender}, new_state + send_event({:parted, channel, sender}, new_state) {:noreply, new_state} end + def handle_data(%ExIRC.Message{cmd: "QUIT", nick: from, host: host, user: user} = msg, state) do sender = %SenderInfo{nick: from, host: host, user: user} - reason = msg.args |> List.first - if state.debug?, do: debug "#{from} QUIT" + reason = msg.args |> List.first() + if state.debug?, do: debug("#{from} QUIT") channels = Channels.user_quit(state.channels, from) new_state = %{state | channels: channels} - send_event {:quit, reason, sender}, new_state + send_event({:quit, reason, sender}, new_state) {:noreply, new_state} end - # Called when we receive a PING + + # Called when we receive a PING. def handle_data(%ExIRC.Message{cmd: "PING"} = msg, %ClientState{autoping: true} = state) do - if state.debug?, do: debug "RECEIVED A PING!" + if state.debug?, do: debug("RECEIVED A PING!") + case msg do %ExIRC.Message{args: [from]} -> if state.debug?, do: debug("SENT PONG2") Transport.send(state, pong2!(from, msg.server)) + _ -> if state.debug?, do: debug("SENT PONG1") Transport.send(state, pong1!(state.nick)) end - {:noreply, state}; + + {:noreply, state} end - # Called when we are invited to a channel - def handle_data(%ExIRC.Message{cmd: "INVITE", args: [nick, channel], nick: by, host: host, user: user} = msg, %ClientState{nick: nick} = state) do + + # Called when we are invited to a channel. + def handle_data( + %ExIRC.Message{cmd: "INVITE", args: [nick, channel], nick: by, host: host, user: user} = + msg, + %ClientState{nick: nick} = state + ) do sender = %SenderInfo{nick: by, host: host, user: user} - if state.debug?, do: debug "RECEIVED AN INVITE: #{msg.args |> Enum.join(" ")}" - send_event {:invited, sender, channel}, state + if state.debug?, do: debug("RECEIVED AN INVITE: #{msg.args |> Enum.join(" ")}") + send_event({:invited, sender, channel}, state) {:noreply, state} end - # Called when we are kicked from a channel - - def handle_data(%ExIRC.Message{cmd: "KICK", args: [channel, nick, reason], nick: by, host: host, user: user} = _msg, %ClientState{nick: nick} = state) do + # Called when we are kicked from a channel. + def handle_data( + %ExIRC.Message{ + cmd: "KICK", + args: [channel, nick, reason], + nick: by, + host: host, + user: user + } = _msg, + %ClientState{nick: nick} = state + ) do sender = %SenderInfo{nick: by, host: host, user: user} - if state.debug?, do: debug "WE WERE KICKED FROM #{channel} BY #{by}" - send_event {:kicked, sender, channel, reason}, state + if state.debug?, do: debug("WE WERE KICKED FROM #{channel} BY #{by}") + send_event({:kicked, sender, channel, reason}, state) {:noreply, state} end - # Called when someone else was kicked from a channel - - def handle_data(%ExIRC.Message{cmd: "KICK", args: [channel, nick, reason], nick: by, host: host, user: user} = _msg, state) do + # Called when someone else was kicked from a channel. + def handle_data( + %ExIRC.Message{ + cmd: "KICK", + args: [channel, nick, reason], + nick: by, + host: host, + user: user + } = _msg, + state + ) do sender = %SenderInfo{nick: by, host: host, user: user} - if state.debug?, do: debug "#{nick} WAS KICKED FROM #{channel} BY #{by}" - send_event {:kicked, nick, sender, channel, reason}, state + if state.debug?, do: debug("#{nick} WAS KICKED FROM #{channel} BY #{by}") + send_event({:kicked, nick, sender, channel, reason}, state) {:noreply, state} end - # Called when someone sends us a message - def handle_data(%ExIRC.Message{nick: from, cmd: "PRIVMSG", args: [nick, message], host: host, user: user} = _msg, %ClientState{nick: nick} = state) do + + # Called when someone sends us a message. + def handle_data( + %ExIRC.Message{nick: from, cmd: "PRIVMSG", args: [nick, message], host: host, user: user} = + _msg, + %ClientState{nick: nick} = state + ) do sender = %SenderInfo{nick: from, host: host, user: user} - if state.debug?, do: debug "#{from} SENT US #{message}" - send_event {:received, message, sender}, state + if state.debug?, do: debug("#{from} SENT US #{message}") + send_event({:received, message, sender}, state) {:noreply, state} end - # Called when someone sends a message to a channel we're in, or a list of users - def handle_data(%ExIRC.Message{nick: from, cmd: "PRIVMSG", args: [to, message], host: host, user: user} = _msg, %ClientState{nick: nick} = state) do + + # Called when someone sends a message to a channel we're in, or a list of users. + def handle_data( + %ExIRC.Message{nick: from, cmd: "PRIVMSG", args: [to, message], host: host, user: user} = + _msg, + %ClientState{nick: nick} = state + ) do sender = %SenderInfo{nick: from, host: host, user: user} - if state.debug?, do: debug "#{from} SENT #{message} TO #{to}" - send_event {:received, message, sender, to}, state + if state.debug?, do: debug("#{from} SENT #{message} TO #{to}") + send_event({:received, message, sender, to}, state) # If we were mentioned, fire that event as well - if String.contains?(String.downcase(message), String.downcase(nick)), do: send_event({:mentioned, message, sender, to}, state) + if String.contains?(String.downcase(message), String.downcase(nick)), + do: send_event({:mentioned, message, sender, to}, state) + {:noreply, state} end - # Called when someone uses ACTION, i.e. `/me dies` - def handle_data(%ExIRC.Message{nick: from, cmd: "ACTION", args: [channel, message], host: host, user: user} = _msg, state) do + + # Called when someone uses ACTION, i.e. `/me dies`. + def handle_data( + %ExIRC.Message{ + nick: from, + cmd: "ACTION", + args: [channel, message], + host: host, + user: user + } = _msg, + state + ) do sender = %SenderInfo{nick: from, host: host, user: user} - if state.debug?, do: debug "* #{from} #{message} in #{channel}" - send_event {:me, message, sender, channel}, state + if state.debug?, do: debug("* #{from} #{message} in #{channel}") + send_event({:me, message, sender, channel}, state) {:noreply, state} end # Called when a NOTICE is received by the client. - def handle_data(%ExIRC.Message{nick: from, cmd: "NOTICE", args: [_target, message], host: host, user: user} = _msg, state) do - - sender = %SenderInfo{nick: from, - host: host, - user: user} + def handle_data( + %ExIRC.Message{ + nick: from, + cmd: "NOTICE", + args: [_target, message], + host: host, + user: user + } = _msg, + state + ) do + sender = %SenderInfo{nick: from, host: host, user: user} if String.contains?(message, "identify") do - if state.debug?, do: debug("* Told to identify by #{from}: #{message}") - send_event({:identify, message, sender}, state) + if state.debug?, do: debug("* Told to identify by #{from}: #{message}") + send_event({:identify, message, sender}, state) else if state.debug?, do: debug("* #{message} from #{sender}") send_event({:notice, message, sender}, state) @@ -882,18 +1179,23 @@ defmodule ExIRC.Client do {:noreply, state} end - # Called any time we receive an unrecognized message + # Called any time we receive an unrecognized message. def handle_data(msg, state) do - if state.debug? do debug "UNRECOGNIZED MSG: #{msg.cmd}"; IO.inspect(msg) end - send_event {:unrecognized, msg.cmd, msg}, state + if state.debug? do + debug("UNRECOGNIZED MSG: #{msg.cmd}") + debug(inspect(msg)) + end + + send_event({:unrecognized, msg.cmd, msg}, state) {:noreply, state} end ############### # Internal API ############### + defp send_event(msg, %ClientState{event_handlers: handlers}) when is_list(handlers) do - Enum.each(handlers, fn({pid, _}) -> Kernel.send(pid, msg) end) + Enum.each(handlers, fn {pid, _} -> Kernel.send(pid, msg) end) end defp do_add_handler(pid, handlers) do @@ -901,6 +1203,7 @@ defmodule ExIRC.Client do false -> ref = Process.monitor(pid) [{pid, ref} | handlers] + true -> handlers end @@ -911,6 +1214,7 @@ defmodule ExIRC.Client do {pid, ref} -> Process.demonitor(ref) List.keydelete(handlers, pid, 0) + nil -> handlers end @@ -919,5 +1223,4 @@ defmodule ExIRC.Client do defp debug(msg) do IO.puts(IO.ANSI.green() <> msg <> IO.ANSI.reset()) end - end diff --git a/lib/exirc/commands.ex b/lib/exirc/commands.ex index ef3c60c..2167dcd 100644 --- a/lib/exirc/commands.ex +++ b/lib/exirc/commands.ex @@ -4,7 +4,6 @@ defmodule ExIRC.Commands do """ defmacro __using__(_) do - quote do import ExIRC.Commands @@ -16,39 +15,41 @@ defmodule ExIRC.Commands do @rpl_yourhost "002" @rpl_created "003" @rpl_myinfo "004" - @rpl_isupport "005" # Defacto standard for server support - @rpl_bounce "010" # Defacto replacement of "005" in RFC2812 + # Defacto standard for server support. + @rpl_isupport "005" + # Defacto replacement of "005" in RFC2812. + @rpl_bounce "010" @rpl_statsdline "250" - #@doc """ - #":There are users and invisible on servers" - #""" + # @doc """ + # ":There are users and invisible on servers". + # """ @rpl_luserclient "251" - #@doc """ - # " :operator(s) online" - #""" + # @doc """ + # " :operator(s) online". + # """ @rpl_luserop "252" - #@doc """ - #" :unknown connection(s)" - #""" + # @doc """ + # " :unknown connection(s)". + # """ @rpl_luserunknown "253" - #@doc """ - #" :channels formed" - #""" + # @doc """ + # " :channels formed". + # """ @rpl_luserchannels "254" - #@doc """ - #":I have clients and servers" - #""" + # @doc """ + # ":I have clients and servers". + # """ @rpl_luserme "255" - #@doc """ - #Local/Global user stats - #""" + # @doc """ + # Local/Global user stats. + # """ @rpl_localusers "265" @rpl_globalusers "266" - #@doc """ - #When sending a TOPIC message to determine the channel topic, - #one of two replies is sent. If the topic is set, RPL_TOPIC is sent back else - #RPL_NOTOPIC. - #""" + # @doc """ + # When sending a TOPIC message to determine the channel topic, + # one of two replies is sent. If the topic is set, RPL_TOPIC is sent back else + # RPL_NOTOPIC. + # """ @rpl_whoiscertfp "276" @rpl_whoisregnick "307" @rpl_whoishelpop "310" @@ -61,29 +62,29 @@ defmodule ExIRC.Commands do @rpl_whoisaccount "330" @rpl_notopic "331" @rpl_topic "332" - #@doc """ - #To reply to a NAMES message, a reply pair consisting - #of RPL_NAMREPLY and RPL_ENDOFNAMES is sent by the - #server back to the client. If there is no channel - #found as in the query, then only RPL_ENDOFNAMES is - #returned. The exception to this is when a NAMES - #message is sent with no parameters and all visible - #channels and contents are sent back in a series of - #RPL_NAMEREPLY messages with a RPL_ENDOFNAMES to mark - #the end. - - #Format: " :[[@|+] [[@|+] [...]]]" - #""" + # @doc """ + # To reply to a NAMES message, a reply pair consisting + # of RPL_NAMREPLY and RPL_ENDOFNAMES is sent by the + # server back to the client. If there is no channel + # found as in the query, then only RPL_ENDOFNAMES is + # returned. The exception to this is when a NAMES + # message is sent with no parameters and all visible + # channels and contents are sent back in a series of + # RPL_NAMEREPLY messages with a RPL_ENDOFNAMES to mark + # the end. + + # Format: " :[[@|+] [[@|+] [...]]]" + # """ @rpl_namereply "353" @rpl_endofnames "366" - #@doc """ - #When responding to the MOTD message and the MOTD file - #is found, the file is displayed line by line, with - #each line no longer than 80 characters, using - #RPL_MOTD format replies. These should be surrounded - #by a RPL_MOTDSTART (before the RPL_MOTDs) and an - #RPL_ENDOFMOTD (after). - #""" + # @doc """ + # When responding to the MOTD message and the MOTD file + # is found, the file is displayed line by line, with + # each line no longer than 80 characters, using + # RPL_MOTD format replies. These should be surrounded + # by a RPL_MOTDSTART (before the RPL_MOTDs) and an + # RPL_ENDOFMOTD (after). + # """ @rpl_motd "372" @rpl_motdstart "375" @rpl_endofmotd "376" @@ -94,72 +95,72 @@ defmodule ExIRC.Commands do # Error Codes ################ - #@doc """ - #Used to indicate the nick parameter supplied to a command is currently unused. - #""" + # @doc """ + # Used to indicate the nick parameter supplied to a command is currently unused. + # """ @err_no_such_nick "401" - #@doc """ - #Used to indicate the server name given currently doesn"t exist. - #""" + # @doc """ + # Used to indicate the server name given currently doesn"t exist. + # """ @err_no_such_server "402" - #@doc """ - #Used to indicate the given channel name is invalid. - #""" + # @doc """ + # Used to indicate the given channel name is invalid. + # """ @err_no_such_channel "403" - #@doc """ - #Sent to a user who is either (a) not on a channel which is mode +n or (b), - #not a chanop (or mode +v) on a channel which has mode +m set, and is trying - #to send a PRIVMSG message to that channel. - #""" + # @doc """ + # Sent to a user who is either (a) not on a channel which is mode +n or (b), + # not a chanop (or mode +v) on a channel which has mode +m set, and is trying + # to send a PRIVMSG message to that channel. + # """ @err_cannot_send_to_chan "404" - #@doc """ - #Sent to a user when they have joined the maximum number of allowed channels - #and they try to join another channel. - #""" + # @doc """ + # Sent to a user when they have joined the maximum number of allowed channels + # and they try to join another channel. + # """ @err_too_many_channels "405" - #@doc """ - #Returned to a registered client to indicate that the command sent is unknown by the server. - #""" + # @doc """ + # Returned to a registered client to indicate that the command sent is unknown by the server. + # """ @err_unknown_command "421" - #@doc """ - #Returned when a nick parameter expected for a command and isn"t found. - #""" + # @doc """ + # Returned when a nick parameter expected for a command and isn"t found. + # """ @err_no_nick_given "431" - #@doc """ - #Returned after receiving a NICK message which contains characters which do not fall in the defined set. - #""" + # @doc """ + # Returned after receiving a NICK message which contains characters which do not fall in the defined set. + # """ @err_erroneus_nick "432" - #@doc """ - #Returned when a NICK message is processed that results in an attempt to - #change to a currently existing nick. - #""" + # @doc """ + # Returned when a NICK message is processed that results in an attempt to + # change to a currently existing nick. + # """ @err_nick_in_use "433" - #@doc """ - #Returned by a server to a client when it detects a nick collision - #(registered of a NICK that already exists by another server). - #""" + # @doc """ + # Returned by a server to a client when it detects a nick collision + # (registered of a NICK that already exists by another server). + # """ @err_nick_collision "436" - #@doc """ - #""" + # @doc """ + # """ @err_unavail_resource "437" - #@doc """ - #Returned by the server to indicate that the client must be registered before - #the server will allow it to be parsed in detail. - #""" + # @doc """ + # Returned by the server to indicate that the client must be registered before + # the server will allow it to be parsed in detail. + # """ @err_not_registered "451" - #""" - # Returned by the server by numerous commands to indicate to the client that + # """ + # Returned by the server by numerous commands to indicate to the client that # it didn"t supply enough parameters. - #""" + # """ @err_need_more_params "461" - #@doc """ - #Returned by the server to any link which tries to change part of the registered - #details (such as password or user details from second USER message). - #""" + # @doc """ + # Returned by the server to any link which tries to change part of the registered + # details (such as password or user details from second USER message). + # """ @err_already_registered "462" - #@doc """ - #Returned by the server to the client when the issued command is restricted - #""" + # @doc """ + # Returned by the server to the client when the issued command is restricted. + # """ @err_restricted "484" @rpl_whoissecure "671" @@ -168,18 +169,27 @@ defmodule ExIRC.Commands do # Code groups ############### - @logon_errors [ @err_no_nick_given, @err_erroneus_nick, - @err_nick_in_use, @err_nick_collision, - @err_unavail_resource, @err_need_more_params, - @err_already_registered, @err_restricted ] + @logon_errors [ + @err_no_nick_given, + @err_erroneus_nick, + @err_nick_in_use, + @err_nick_collision, + @err_unavail_resource, + @err_need_more_params, + @err_already_registered, + @err_restricted + ] - @whois_rpls [ @rpl_whoisuser, @rpl_whoishost, - @rpl_whoishost, @rpl_whoisserver, - @rpl_whoismodes, @rpl_whoisidle, - @rpl_endofwhois - ] + @whois_rpls [ + @rpl_whoisuser, + @rpl_whoishost, + @rpl_whoishost, + @rpl_whoisserver, + @rpl_whoismodes, + @rpl_whoisidle, + @rpl_endofwhois + ] end - end ############ @@ -191,103 +201,121 @@ defmodule ExIRC.Commands do Builds a valid IRC command. """ def command!(cmd), do: [cmd, '\r\n'] + @doc """ Builds a valid CTCP command. """ - def ctcp!(cmd), do: command! [@ctcp_delimiter, cmd, @ctcp_delimiter] + def ctcp!(cmd), do: command!([@ctcp_delimiter, cmd, @ctcp_delimiter]) + def ctcp!(cmd, args) do expanded = args |> Enum.intersperse(' ') - command! [@ctcp_delimiter, cmd, expanded, @ctcp_delimiter] + command!([@ctcp_delimiter, cmd, expanded, @ctcp_delimiter]) end # IRC Commands @doc """ - Send a WHOIS request about a user + Send a WHOIS request about a user. """ - def whois!(user), do: command! ['WHOIS ', user] + def whois!(user), do: command!(['WHOIS ', user]) @doc """ - Send a WHO request about a channel + Send a WHO request about a channel. """ - def who!(channel), do: command! ['WHO ', channel] + def who!(channel), do: command!(['WHO ', channel]) @doc """ - Send password to server + Send password to server. """ - def pass!(pwd), do: command! ['PASS ', pwd] + def pass!(pwd), do: command!(['PASS ', pwd]) + @doc """ Send nick to server. (Changes or sets your nick) """ - def nick!(nick), do: command! ['NICK ', nick] + def nick!(nick), do: command!(['NICK ', nick]) + @doc """ Send username to server. (Changes or sets your username) """ def user!(user, name) do - command! ['USER ', user, ' 0 * :', name] + command!(['USER ', user, ' 0 * :', name]) end + @doc """ - Send PONG in response to PING + Send PONG in response to PING. """ - def pong1!(nick), do: command! ['PONG ', nick] + def pong1!(nick), do: command!(['PONG ', nick]) + @doc """ - Send a targeted PONG in response to PING + Send a targeted PONG in response to PING. """ - def pong2!(nick, to), do: command! ['PONG ', nick, ' ', to] + def pong2!(nick, to), do: command!(['PONG ', nick, ' ', to]) + @doc """ - Send message to channel or user + Send message to channel or user. """ - def privmsg!(nick, msg), do: command! ['PRIVMSG ', nick, ' :', msg] + def privmsg!(nick, msg), do: command!(['PRIVMSG ', nick, ' :', msg]) + @doc """ - Send a `/me ` CTCP command to t + Send a `/me ` CTCP command to t. """ - def me!(channel, msg), do: command! ['PRIVMSG ', channel, ' :', @ctcp_delimiter, 'ACTION ', msg, @ctcp_delimiter] + def me!(channel, msg), + do: command!(['PRIVMSG ', channel, ' :', @ctcp_delimiter, 'ACTION ', msg, @ctcp_delimiter]) + @doc """ - Sends a command to the server to get the list of names back + Sends a command to the server to get the list of names back. """ - def names!(_channel), do: command! ['NAMES'] + def names!(_channel), do: command!(['NAMES']) + @doc """ - Send notice to channel or user + Send notice to channel or user. """ - def notice!(nick, msg), do: command! ['NOTICE ', nick, ' :', msg] + def notice!(nick, msg), do: command!(['NOTICE ', nick, ' :', msg]) + @doc """ - Send join command to server (join a channel) + Send join command to server (join a channel). """ - def join!(channel), do: command! ['JOIN ', channel] - def join!(channel, key), do: command! ['JOIN ', channel, ' ', key] + def join!(channel), do: command!(['JOIN ', channel]) + def join!(channel, key), do: command!(['JOIN ', channel, ' ', key]) + @doc """ - Send part command to server (leave a channel) + Send part command to server (leave a channel). """ - def part!(channel), do: command! ['PART ', channel] + def part!(channel), do: command!(['PART ', channel]) + @doc """ - Send quit command to server (disconnect from server) + Send quit command to server (disconnect from server). """ - def quit!(msg \\ "Leaving"), do: command! ['QUIT :', msg] + def quit!(msg \\ "Leaving"), do: command!(['QUIT :', msg]) + @doc """ - Send kick command to server + Send kick command to server. """ def kick!(channel, nick, message \\ "") do - case "#{message}" |> String.length do - 0 -> command! ['KICK ', channel, ' ', nick] - _ -> command! ['KICK ', channel, ' ', nick, ' ', message] + case "#{message}" |> String.length() do + 0 -> command!(['KICK ', channel, ' ', nick]) + _ -> command!(['KICK ', channel, ' ', nick, ' ', message]) end end + @doc """ - Send mode command to server - MODE - MODE [] + Send mode command to server. + + MODE + MODE [] + """ def mode!(channel_or_nick, flags, args \\ "") do - case "#{args}" |> String.length do - 0 -> command! ['MODE ', channel_or_nick, ' ', flags] - _ -> command! ['MODE ', channel_or_nick, ' ', flags, ' ', args] + case "#{args}" |> String.length() do + 0 -> command!(['MODE ', channel_or_nick, ' ', flags]) + _ -> command!(['MODE ', channel_or_nick, ' ', flags, ' ', args]) end end + @doc """ - Send an invite command + Send an invite command. """ def invite!(nick, channel) do - command! ['INVITE ', nick, ' ', channel] + command!(['INVITE ', nick, ' ', channel]) end - end diff --git a/lib/exirc/example_handler.ex b/lib/exirc/example_handler.ex index b12ea06..c37d0ec 100644 --- a/lib/exirc/example_handler.ex +++ b/lib/exirc/example_handler.ex @@ -4,6 +4,7 @@ defmodule ExampleHandler do `add_handler` or `add_handler_async`. To remove, call `remove_handler` or `remove_handler_async` with the pid of the handler process. """ + def start! do start_link([]) end @@ -17,120 +18,141 @@ defmodule ExampleHandler do end @doc """ - Handle messages from the client - - Examples: - - def handle_info({:connected, server, port}, _state) do - IO.puts "Connected to \#{server}:\#{port}" - end - def handle_info(:logged_in, _state) do - IO.puts "Logged in!" - end - def handle_info(%ExIRC.Message{nick: from, cmd: "PRIVMSG", args: ["mynick", msg]}, _state) do - IO.puts "Received a private message from \#{from}: \#{msg}" - end - def handle_info(%ExIRC.Message{nick: from, cmd: "PRIVMSG", args: [to, msg]}, _state) do - IO.puts "Received a message in \#{to} from \#{from}: \#{msg}" - end + Handle messages from the client. + + ## Examples: + + def handle_info({:connected, server, port}, _state) do + IO.puts "Connected to \#{server}:\#{port}" + end + def handle_info(:logged_in, _state) do + IO.puts "Logged in!" + end + def handle_info(%ExIRC.Message{nick: from, cmd: "PRIVMSG", args: ["mynick", msg]}, _state) do + IO.puts "Received a private message from \#{from}: \#{msg}" + end + def handle_info(%ExIRC.Message{nick: from, cmd: "PRIVMSG", args: [to, msg]}, _state) do + IO.puts "Received a message in \#{to} from \#{from}: \#{msg}" + end + """ def handle_info({:connected, server, port}, _state) do - debug "Connected to #{server}:#{port}" + debug("Connected to #{server}:#{port}") {:noreply, nil} end + def handle_info(:logged_in, _state) do - debug "Logged in to server" + debug("Logged in to server") {:noreply, nil} end + def handle_info({:login_failed, :nick_in_use}, _state) do - debug "Login failed, nickname in use" + debug("Login failed, nickname in use") {:noreply, nil} end + def handle_info(:disconnected, _state) do - debug "Disconnected from server" + debug("Disconnected from server") {:noreply, nil} end + def handle_info({:joined, channel}, _state) do - debug "Joined #{channel}" + debug("Joined #{channel}") {:noreply, nil} end + def handle_info({:joined, channel, user}, _state) do - debug "#{user} joined #{channel}" + debug("#{user} joined #{channel}") {:noreply, nil} end + def handle_info({:topic_changed, channel, topic}, _state) do - debug "#{channel} topic changed to #{topic}" + debug("#{channel} topic changed to #{topic}") {:noreply, nil} end + def handle_info({:nick_changed, nick}, _state) do - debug "We changed our nick to #{nick}" + debug("We changed our nick to #{nick}") {:noreply, nil} end + def handle_info({:nick_changed, old_nick, new_nick}, _state) do - debug "#{old_nick} changed their nick to #{new_nick}" + debug("#{old_nick} changed their nick to #{new_nick}") {:noreply, nil} end + def handle_info({:parted, channel}, _state) do - debug "We left #{channel}" + debug("We left #{channel}") {:noreply, nil} end + def handle_info({:parted, channel, sender}, _state) do nick = sender.nick - debug "#{nick} left #{channel}" + debug("#{nick} left #{channel}") {:noreply, nil} end + def handle_info({:invited, sender, channel}, _state) do by = sender.nick - debug "#{by} invited us to #{channel}" + debug("#{by} invited us to #{channel}") {:noreply, nil} end + def handle_info({:kicked, sender, channel}, _state) do by = sender.nick - debug "We were kicked from #{channel} by #{by}" + debug("We were kicked from #{channel} by #{by}") {:noreply, nil} end + def handle_info({:kicked, nick, sender, channel}, _state) do by = sender.nick - debug "#{nick} was kicked from #{channel} by #{by}" + debug("#{nick} was kicked from #{channel} by #{by}") {:noreply, nil} end + def handle_info({:received, message, sender}, _state) do from = sender.nick - debug "#{from} sent us a private message: #{message}" + debug("#{from} sent us a private message: #{message}") {:noreply, nil} end + def handle_info({:received, message, sender, channel}, _state) do from = sender.nick - debug "#{from} sent a message to #{channel}: #{message}" + debug("#{from} sent a message to #{channel}: #{message}") {:noreply, nil} end + def handle_info({:mentioned, message, sender, channel}, _state) do from = sender.nick - debug "#{from} mentioned us in #{channel}: #{message}" + debug("#{from} mentioned us in #{channel}: #{message}") {:noreply, nil} end + def handle_info({:me, message, sender, channel}, _state) do from = sender.nick - debug "* #{from} #{message} in #{channel}" + debug("* #{from} #{message} in #{channel}") {:noreply, nil} end - # This is an example of how you can manually catch commands if ExIRC.Client doesn't send a specific message for it + + # This is an example of how you can manually catch commands if ExIRC.Client doesn't send a specific message for it. def handle_info(%ExIRC.Message{nick: from, cmd: "PRIVMSG", args: ["testnick", msg]}, _state) do - debug "Received a private message from #{from}: #{msg}" + debug("Received a private message from #{from}: #{msg}") {:noreply, nil} end + def handle_info(%ExIRC.Message{nick: from, cmd: "PRIVMSG", args: [to, msg]}, _state) do - debug "Received a message in #{to} from #{from}: #{msg}" + debug("Received a message in #{to} from #{from}: #{msg}") {:noreply, nil} end - # Catch-all for messages you don't care about + + # Catch-all for messages you don't care about. def handle_info(msg, _state) do - debug "Received ExIRC.Message:" - IO.inspect msg + debug("Received ExIRC.Message:") + debug(inspect(msg)) {:noreply, nil} end defp debug(msg) do - IO.puts IO.ANSI.yellow() <> msg <> IO.ANSI.reset() + IO.puts(IO.ANSI.yellow() <> msg <> IO.ANSI.reset()) end end diff --git a/lib/exirc/exirc.ex b/lib/exirc/exirc.ex index 865d50d..908c061 100644 --- a/lib/exirc/exirc.ex +++ b/lib/exirc/exirc.ex @@ -1,31 +1,31 @@ defmodule ExIRC do @moduledoc """ - Supervises IRC client processes + Supervises IRC client processes. - Usage: + ## Usage: - # Start the supervisor (started automatically when ExIRC is run as an application) + # Start the supervisor (started automatically when ExIRC is run as an application). ExIRC.start_link - # Start a new IRC client + # Start a new IRC client. {:ok, client} = ExIRC.start_client! - # Connect to an IRC server + # Connect to an IRC server. ExIRC.Client.connect! client, "localhost", 6667 - # Logon + # Logon. ExIRC.Client.logon client, "password", "nick", "user", "name" - # Join a channel (password is optional) + # Join a channel (password is optional). ExIRC.Client.join client, "#channel", "password" - # Send a message + # Send a message. ExIRC.Client.msg client, :privmsg, "#channel", "Hello world!" - # Quit (message is optional) + # Quit (message is optional). ExIRC.Client.quit client, "message" - # Stop and close the client connection + # Stop and close the client connection. ExIRC.Client.stop! client """ @@ -61,15 +61,15 @@ defmodule ExIRC do end @doc """ - Start a new ExIRC client under the ExIRC supervisor + Start a new ExIRC client under the ExIRC supervisor. """ @spec start_client!() :: DynamicSupervisor.on_start_child() - def start_client!() do + def start_client! do DynamicSupervisor.start_child(:exirc, {TemporaryClient, owner: self()}) end @doc """ - Start a new ExIRC client + Start a new ExIRC client. """ @spec start_link!() :: GenServer.on_start() def start_link! do diff --git a/lib/exirc/irc_message.ex b/lib/exirc/irc_message.ex index 2ea26e5..10e1cfb 100644 --- a/lib/exirc/irc_message.ex +++ b/lib/exirc/irc_message.ex @@ -1,9 +1,21 @@ defmodule ExIRC.Message do - defstruct server: '', - nick: '', - user: '', - host: '', - ctcp: nil, - cmd: '', - args: [] + @moduledoc false + + defstruct server: '', + nick: '', + user: '', + host: '', + ctcp: nil, + cmd: '', + args: [] + + @type t :: %__MODULE__{ + server: String.t() | charlist(), + nick: String.t() | charlist(), + user: String.t() | charlist(), + host: String.t() | charlist(), + ctcp: boolean() | :invalid | nil, + cmd: String.t() | charlist(), + args: [String.t() | charlist()] + } end diff --git a/lib/exirc/logger.ex b/lib/exirc/logger.ex index 9cee4bf..44a8a66 100644 --- a/lib/exirc/logger.ex +++ b/lib/exirc/logger.ex @@ -1,29 +1,31 @@ defmodule ExIRC.Logger do @moduledoc """ - A simple abstraction of :error_logger + A simple abstraction of `:error_logger`. """ @doc """ - Log an informational message report + Log an informational message report. """ @spec info(binary) :: :ok def info(msg) do - :error_logger.info_report String.to_charlist(msg) + :error_logger.info_report(String.to_charlist(msg)) end @doc """ - Log a warning message report + Log a warning message report. """ @spec warning(binary) :: :ok def warning(msg) do - :error_logger.warning_report String.to_charlist("#{IO.ANSI.yellow()}#{msg}#{IO.ANSI.reset()}") + :error_logger.warning_report( + String.to_charlist("#{IO.ANSI.yellow()}#{msg}#{IO.ANSI.reset()}") + ) end @doc """ - Log an error message report + Log an error message report. """ @spec error(binary) :: :ok def error(msg) do - :error_logger.error_report String.to_charlist("#{IO.ANSI.red()}#{msg}#{IO.ANSI.reset()}") + :error_logger.error_report(String.to_charlist("#{IO.ANSI.red()}#{msg}#{IO.ANSI.reset()}")) end -end \ No newline at end of file +end diff --git a/lib/exirc/transport.ex b/lib/exirc/transport.ex index dedd8ce..6bd4808 100644 --- a/lib/exirc/transport.ex +++ b/lib/exirc/transport.ex @@ -1,22 +1,29 @@ -defmodule ExIRC.Client.Transport do - def connect(%{ssl?: false}, host, port, options) do - :gen_tcp.connect(host, port, options) - end - def connect(%{ssl?: true}, host, port, options) do - :ssl.connect(host, port, options) - end - - def send(%{ssl?: false, socket: socket}, data) do - :gen_tcp.send(socket, data) - end - def send(%{ssl?: true, socket: socket}, data) do - :ssl.send(socket, data) - end - - def close(%{ssl?: false, socket: socket}) do - :gen_tcp.close(socket) - end - def close(%{ssl?: true, socket: socket}) do - :ssl.close(socket) - end -end +defmodule ExIRC.Client.Transport do + @moduledoc """ + IRC Transport module for TCP and SSL connections. + """ + + def connect(%{ssl?: false}, host, port, options) do + :gen_tcp.connect(host, port, options) + end + + def connect(%{ssl?: true}, host, port, options) do + :ssl.connect(host, port, options) + end + + def send(%{ssl?: false, socket: socket}, data) do + :gen_tcp.send(socket, data) + end + + def send(%{ssl?: true, socket: socket}, data) do + :ssl.send(socket, data) + end + + def close(%{ssl?: false, socket: socket}) do + :gen_tcp.close(socket) + end + + def close(%{ssl?: true, socket: socket}) do + :ssl.close(socket) + end +end diff --git a/lib/exirc/utils.ex b/lib/exirc/utils.ex index 428379d..f1035df 100644 --- a/lib/exirc/utils.ex +++ b/lib/exirc/utils.ex @@ -1,41 +1,44 @@ defmodule ExIRC.Utils do - - ###################### - # IRC Message Parsing - ###################### + @moduledoc """ + IRC Message Parsing. + """ @doc """ - Parse an IRC message + Parse an IRC message. - Example: + ## 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 :: charlist) :: ExIRC.Message.t + """ + @spec parse(raw_data :: charlist) :: ExIRC.Message.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, %ExIRC.Message{ctcp: false})) + [?: | _] -> + [[?: | from] | rest] = :string.tokens(data, ' ') + get_cmd(rest, parse_from(from, %ExIRC.Message{ctcp: false})) + data -> - get_cmd(:string.tokens(data, ' '), %ExIRC.Message{ctcp: false}) + get_cmd(:string.tokens(data, ' '), %ExIRC.Message{ctcp: false}) end end @prefix_pattern ~r/^(?[^!\s]+)(?:!(?:(?[^@\s]+)@)?(?:(?[\S]+)))?$/ defp parse_from(from, msg) do from_str = IO.iodata_to_binary(from) - parts = Regex.run(@prefix_pattern, from_str, capture: :all_but_first) + parts = Regex.run(@prefix_pattern, from_str, capture: :all_but_first) + case parts do [nick, user, host] -> %{msg | nick: nick, user: user, host: host} + [nick, host] -> %{msg | nick: nick, host: host} + [nick] -> if String.contains?(nick, ".") do %{msg | server: nick} @@ -45,22 +48,28 @@ defmodule ExIRC.Utils do end end - # Parse command from message - defp get_cmd([cmd, arg1, [?:, 1 | ctcp_trail] | restargs], msg) when cmd == 'PRIVMSG' or cmd == 'NOTICE' do + # 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, target, [1 | ctcp_cmd] | cmd_args], msg) when cmd == 'PRIVMSG' or cmd == 'NOTICE' do - args = cmd_args + defp get_cmd([cmd, target, [1 | ctcp_cmd] | cmd_args], msg) + when cmd == 'PRIVMSG' or cmd == 'NOTICE' do + args = + cmd_args |> Enum.map(&Enum.take_while(&1, fn c -> c != 0o001 end)) |> Enum.map(&List.to_string/1) + case args do args when args != [] -> - %{msg | - cmd: to_string(ctcp_cmd), - args: [to_string(target), args |> Enum.join(" ")], - ctcp: true + %{ + msg + | cmd: to_string(ctcp_cmd), + args: [to_string(target), args |> Enum.join(" ")], + ctcp: true } + _ -> %{msg | cmd: to_string(cmd), ctcp: :invalid} end @@ -70,31 +79,33 @@ defmodule ExIRC.Utils do get_args(rest, %{msg | cmd: to_string(cmd)}) end - - # Parse command args from message + # Parse command args from message. defp get_args([], msg) do - args = msg.args - |> Enum.reverse - |> Enum.filter(fn arg -> arg != [] end) - |> Enum.map(&trim_crlf/1) - |> Enum.map(&:binary.list_to_bin/1) - |> Enum.map(fn(s) -> - case String.valid?(s) do - true -> :unicode.characters_to_binary(s) - false -> :unicode.characters_to_binary(s, :latin1, :unicode) - end - end) + args = + msg.args + |> Enum.reverse() + |> Enum.filter(fn arg -> arg != [] end) + |> Enum.map(&trim_crlf/1) + |> Enum.map(&:binary.list_to_bin/1) + |> Enum.map(fn s -> + case String.valid?(s) do + true -> :unicode.characters_to_binary(s) + false -> :unicode.characters_to_binary(s, :latin1, :unicode) + end + end) post_process(%{msg | args: args}) end defp get_args([[?: | first_arg] | rest], msg) do - args = (for arg <- [first_arg | rest], do: ' ' ++ trim_crlf(arg)) |> List.flatten + args = for(arg <- [first_arg | rest], do: ' ' ++ trim_crlf(arg)) |> List.flatten() + case args do [_] -> - get_args([], %{msg | args: msg.args}) + get_args([], %{msg | args: msg.args}) + [_ | full_trail] -> - get_args([], %{msg | args: [full_trail | msg.args]}) + get_args([], %{msg | args: [full_trail | msg.args]}) end end @@ -108,6 +119,7 @@ defmodule ExIRC.Utils do # Handle malformed RPL_TOPIC messages which contain no topic %{msg | :cmd => "331", :args => [channel, "No topic is set"], :nick => nick} end + defp post_process(msg), do: msg ############################ @@ -120,8 +132,10 @@ defmodule ExIRC.Utils do 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 + @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)) @@ -134,15 +148,20 @@ defmodule ExIRC.Utils 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(&String.to_charlist/1) - |> List.zip + prefixes = + Regex.run(~r/\((.*)\)(.*)/, user_prefixes, capture: :all_but_first) + |> Enum.map(&String.to_charlist/1) + |> List.zip() + %{state | user_prefixes: prefixes} end + defp isup_param(_, state) do state end @@ -151,33 +170,50 @@ defmodule ExIRC.Utils do # 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'] + @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: + ## Example: iex> local_time = {{2013,12,6},{14,5,0}} - {{2013,12,6},{14,5,0}} - iex> ExIRC.Utils.ctcp_time local_time + 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 + @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_charlist(d)]), - ' ', - :io_lib.format("~2..0s", [Integer.to_charlist(h)]), - ':', - :io_lib.format("~2..0s", [Integer.to_charlist(n)]), - ':', - :io_lib.format("~2..0s", [Integer.to_charlist(s)]), - ' ', - Integer.to_charlist(y)] |> List.flatten |> List.to_string + [ + :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_charlist(d)]), + ' ', + :io_lib.format("~2..0s", [Integer.to_charlist(h)]), + ':', + :io_lib.format("~2..0s", [Integer.to_charlist(n)]), + ':', + :io_lib.format("~2..0s", [Integer.to_charlist(s)]), + ' ', + Integer.to_charlist(y) + ] + |> :erlang.iolist_to_binary() end defp trim_crlf(charlist) do @@ -186,5 +222,4 @@ defmodule ExIRC.Utils do _ -> charlist end end - end diff --git a/lib/exirc/who.ex b/lib/exirc/who.ex index 8bfe151..c7eee78 100644 --- a/lib/exirc/who.ex +++ b/lib/exirc/who.ex @@ -1,18 +1,17 @@ defmodule ExIRC.Who do + @moduledoc false - defstruct [ - admin?: nil, - away?: nil, - founder?: nil, - half_operator?: nil, - hops: nil, - host: nil, - name: nil, - nick: nil, - operator?: nil, - server: nil, - server_operator?: nil, - user: nil, - voiced?: nil - ] + defstruct admin?: nil, + away?: nil, + founder?: nil, + half_operator?: nil, + hops: nil, + host: nil, + name: nil, + nick: nil, + operator?: nil, + server: nil, + server_operator?: nil, + user: nil, + voiced?: nil end diff --git a/lib/exirc/whois.ex b/lib/exirc/whois.ex index 772aaac..0d4deac 100644 --- a/lib/exirc/whois.ex +++ b/lib/exirc/whois.ex @@ -1,19 +1,18 @@ defmodule ExIRC.Whois do + @moduledoc false - defstruct [account_name: nil, - channels: [], - helpop?: false, - hostname: nil, - idling_time: 0, - ircop?: false, - nick: nil, - name: nil, - registered_nick?: false, - server_address: nil, - server_name: nil, - signon_time: 0, - ssl?: false, - user: nil, - ] + defstruct account_name: nil, + channels: [], + helpop?: false, + hostname: nil, + idling_time: 0, + ircop?: false, + nick: nil, + name: nil, + registered_nick?: false, + server_address: nil, + server_name: nil, + signon_time: 0, + ssl?: false, + user: nil end - diff --git a/mix.exs b/mix.exs index 850c856..ff51516 100644 --- a/mix.exs +++ b/mix.exs @@ -15,7 +15,8 @@ defmodule ExIRC.Mixfile do "coveralls.html": :test, "coveralls.post": :test ], - deps: deps() + deps: deps(), + dialyzer: [plt_file: {:no_warn, "priv/plts/dialyzer.plt"}] ] end @@ -38,8 +39,10 @@ defmodule ExIRC.Mixfile do defp deps do [ - {:ex_doc, "~> 0.22", only: :dev}, - {:excoveralls, "~> 0.13", only: :test} + {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, + {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, + {:ex_doc, "~> 0.28", only: [:dev], runtime: false}, + {:excoveralls, "~> 0.14", only: [:test]} ] end end diff --git a/mix.lock b/mix.lock index 5219dd4..3eb6350 100644 --- a/mix.lock +++ b/mix.lock @@ -1,20 +1,26 @@ %{ - "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, + "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"}, + "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm", "000aaeff08919e95e7aea13e4af7b2b9734577b3e6a7c50ee31ee88cab6ec4fb"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, - "ex_doc": {:hex, :ex_doc, "0.22.2", "03a2a58bdd2ba0d83d004507c4ee113b9c521956938298eba16e55cc4aba4a6c", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "cf60e1b3e2efe317095b6bb79651f83a2c1b3edcb4d319c421d7fcda8b3aff26"}, - "excoveralls": {:hex, :excoveralls, "0.13.1", "b9f1697f7c9e0cfe15d1a1d737fb169c398803ffcbc57e672aa007e9fd42864c", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b4bb550e045def1b4d531a37fb766cbbe1307f7628bf8f0414168b3f52021cce"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.20", "89970db71b11b6b89759ce16807e857df154f8df3e807b2920a8c39834a9e5cf", [:mix], [], "hexpm", "1eb0d2dabeeeff200e0d17dc3048a6045aab271f73ebb82e416464832eb57bdd"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "ex_doc": {:hex, :ex_doc, "0.28.2", "e031c7d1a9fc40959da7bf89e2dc269ddc5de631f9bd0e326cbddf7d8085a9da", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "51ee866993ffbd0e41c084a7677c570d0fc50cb85c6b5e76f8d936d9587fa719"}, + "excoveralls": {:hex, :excoveralls, "0.14.4", "295498f1ae47bdc6dce59af9a585c381e1aefc63298d48172efaaa90c3d251db", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e3ab02f2df4c1c7a519728a6f0a747e71d7d6e846020aae338173619217931c1"}, "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]}, - "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, - "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, - "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], []}, - "makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, - "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, + "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } diff --git a/test/channels_test.exs b/test/channels_test.exs index b29551b..2e9b2b5 100644 --- a/test/channels_test.exs +++ b/test/channels_test.exs @@ -4,17 +4,22 @@ defmodule ExIRC.ChannelsTest do 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 + 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 + 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 + channels = + Channels.init() + |> Channels.join("#TestChannel") + |> Channels.join("#testchannel") + |> Channels.channels() + assert 1 == Enum.count(channels) end @@ -32,7 +37,11 @@ defmodule ExIRC.ChannelsTest do end test "Can set the topic for a channel" do - channels = Channels.init() |> Channels.join("#testchannel") |> Channels.set_topic("#testchannel", "Welcome to Test Channel!") + channels = + Channels.init() + |> Channels.join("#testchannel") + |> Channels.set_topic("#testchannel", "Welcome to Test Channel!") + assert "Welcome to Test Channel!" == Channels.channel_topic(channels, "#testchannel") end @@ -42,7 +51,9 @@ defmodule ExIRC.ChannelsTest do end test "Can set the channel type" do - channels = Channels.init() |> Channels.join("#testchannel") |> Channels.set_type("#testchannel", "@") + channels = + Channels.init() |> Channels.join("#testchannel") |> Channels.set_type("#testchannel", "@") + assert :secret == Channels.channel_type(channels, "#testchannel") channels = Channels.set_type(channels, "#testchannel", "*") assert :private == Channels.channel_type(channels, "#testchannel") @@ -57,23 +68,41 @@ defmodule ExIRC.ChannelsTest do 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", "!") + 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") + channels = + Channels.init() + |> Channels.join("#testchannel") + |> Channels.user_join("#testchannel", "testnick") + assert Channels.channel_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"]) + channels = + Channels.init() + |> Channels.join("#testchannel") + |> Channels.users_join("#testchannel", ["testnick", "anothernick"]) + assert Channels.channel_has_user?(channels, "#testchannel", "testnick") assert Channels.channel_has_user?(channels, "#testchannel", "anothernick") end test "Strips rank designations from nicks" do - channels = Channels.init() |> Channels.join("#testchannel") |> Channels.users_join("#testchannel", ["+testnick", "@anothernick", "&athirdnick", "%somanynicks", "~onemorenick"]) + channels = + Channels.init() + |> Channels.join("#testchannel") + |> Channels.users_join("#testchannel", [ + "+testnick", + "@anothernick", + "&athirdnick", + "%somanynicks", + "~onemorenick" + ]) + assert Channels.channel_has_user?(channels, "#testchannel", "testnick") assert Channels.channel_has_user?(channels, "#testchannel", "anothernick") assert Channels.channel_has_user?(channels, "#testchannel", "athirdnick") @@ -83,13 +112,22 @@ defmodule ExIRC.ChannelsTest do 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.channel_has_user?(channels, "#testchannel", "testnick") + + assert {:error, :no_such_channel} == + Channels.channel_has_user?(channels, "#testchannel", "testnick") + channels = Channels.init() |> Channels.users_join("#testchannel", ["testnick", "anothernick"]) - assert {:error, :no_such_channel} == Channels.channel_has_user?(channels, "#testchannel", "testnick") + + assert {:error, :no_such_channel} == + Channels.channel_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") + channels = + Channels.init() + |> Channels.join("#testchannel") + |> Channels.user_join("#testchannel", "testnick") + assert Channels.channel_has_user?(channels, "#testchannel", "testnick") channels = channels |> Channels.user_part("#testchannel", "testnick") refute Channels.channel_has_user?(channels, "#testchannel", "testnick") @@ -97,7 +135,9 @@ defmodule ExIRC.ChannelsTest do 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.channel_has_user?(channels, "#testchannel", "testnick") + + assert {:error, :no_such_channel} == + Channels.channel_has_user?(channels, "#testchannel", "testnick") end test "Can quit a user from all channels" do @@ -108,6 +148,7 @@ defmodule ExIRC.ChannelsTest do |> Channels.join("#anotherchannel") |> Channels.user_join("#anotherchannel", "testnick") |> Channels.user_join("#anotherchannel", "secondnick") + assert Channels.channel_has_user?(channels, "#testchannel", "testnick") channels = channels |> Channels.user_quit("testnick") refute Channels.channel_has_user?(channels, "#testchannel", "testnick") @@ -116,11 +157,13 @@ defmodule ExIRC.ChannelsTest do 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") + channels = + Channels.init() + |> Channels.join("#testchannel") + |> Channels.join("#anotherchan") + |> Channels.user_join("#testchannel", "testnick") + |> Channels.user_join("#anotherchan", "testnick") + assert Channels.channel_has_user?(channels, "#testchannel", "testnick") assert Channels.channel_has_user?(channels, "#anotherchan", "testnick") channels = Channels.user_rename(channels, "testnick", "newnick") @@ -131,24 +174,34 @@ defmodule ExIRC.ChannelsTest do end test "Renaming a user that doesn't exist is a noop" do - channels = Channels.init() |> Channels.join("#testchannel") |> Channels.user_rename("testnick", "newnick") + channels = + Channels.init() + |> Channels.join("#testchannel") + |> Channels.user_rename("testnick", "newnick") + refute Channels.channel_has_user?(channels, "#testchannel", "testnick") refute Channels.channel_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]} + 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 diff --git a/test/client_test.exs b/test/client_test.exs index 363f443..d9baee3 100644 --- a/test/client_test.exs +++ b/test/client_test.exs @@ -133,7 +133,7 @@ defmodule ExIRC.ClientTest do assert_receive {:mentioned, ^chat_message, ^expected_senderinfo, "#testchannel"}, 10 end - defp get_state() do + defp get_state do %ExIRC.Client.ClientState{ nick: "tester", logged_on?: true, @@ -142,7 +142,7 @@ defmodule ExIRC.ClientTest do } end - defp get_channel() do + defp get_channel do %ExIRC.Channels.Channel{ name: "testchannel", topic: "topic", diff --git a/test/commands_test.exs b/test/commands_test.exs index 97837ae..a665d51 100644 --- a/test/commands_test.exs +++ b/test/commands_test.exs @@ -5,44 +5,44 @@ defmodule ExIRC.CommandsTest do test "Commands are formatted properly" do expected = <<0o001, "TESTCMD", 0o001, ?\r, ?\n>> - assert expected == ctcp!("TESTCMD") |> IO.iodata_to_binary + assert expected == ctcp!("TESTCMD") |> IO.iodata_to_binary() expected = <<"PRIVMSG #testchan :", 0o001, "ACTION mind explodes!!", 0o001, ?\r, ?\n>> - assert expected == me!("#testchan", "mind explodes!!") |> IO.iodata_to_binary + assert expected == me!("#testchan", "mind explodes!!") |> IO.iodata_to_binary() expected = <<"PASS testpass", ?\r, ?\n>> - assert expected == pass!("testpass") |> IO.iodata_to_binary + assert expected == pass!("testpass") |> IO.iodata_to_binary() expected = <<"NICK testnick", ?\r, ?\n>> - assert expected == nick!("testnick") |> IO.iodata_to_binary + assert expected == nick!("testnick") |> IO.iodata_to_binary() expected = <<"USER testuser 0 * :Test User", ?\r, ?\n>> - assert expected == user!("testuser", "Test User") |> IO.iodata_to_binary + assert expected == user!("testuser", "Test User") |> IO.iodata_to_binary() expected = <<"PONG testnick", ?\r, ?\n>> - assert expected == pong1!("testnick") |> IO.iodata_to_binary + assert expected == pong1!("testnick") |> IO.iodata_to_binary() expected = <<"PONG testnick othernick", ?\r, ?\n>> - assert expected == pong2!("testnick", "othernick") |> IO.iodata_to_binary + assert expected == pong2!("testnick", "othernick") |> IO.iodata_to_binary() expected = <<"PRIVMSG testnick :Test message!", ?\r, ?\n>> - assert expected == privmsg!("testnick", "Test message!") |> IO.iodata_to_binary + assert expected == privmsg!("testnick", "Test message!") |> IO.iodata_to_binary() expected = <<"NOTICE testnick :Test notice!", ?\r, ?\n>> - assert expected == notice!("testnick", "Test notice!") |> IO.iodata_to_binary + assert expected == notice!("testnick", "Test notice!") |> IO.iodata_to_binary() expected = <<"JOIN testchan", ?\r, ?\n>> - assert expected == join!("testchan") |> IO.iodata_to_binary + assert expected == join!("testchan") |> IO.iodata_to_binary() expected = <<"JOIN testchan chanpass", ?\r, ?\n>> - assert expected == join!("testchan", "chanpass") |> IO.iodata_to_binary + assert expected == join!("testchan", "chanpass") |> IO.iodata_to_binary() expected = <<"PART testchan", ?\r, ?\n>> - assert expected == part!("testchan") |> IO.iodata_to_binary + assert expected == part!("testchan") |> IO.iodata_to_binary() expected = <<"QUIT :Leaving", ?\r, ?\n>> - assert expected == quit!() |> IO.iodata_to_binary + assert expected == quit!() |> IO.iodata_to_binary() expected = <<"QUIT :Goodbye, cruel world.", ?\r, ?\n>> - assert expected == quit!("Goodbye, cruel world.") |> IO.iodata_to_binary + assert expected == quit!("Goodbye, cruel world.") |> IO.iodata_to_binary() expected = <<"KICK #testchan testuser", ?\r, ?\n>> - assert expected == kick!("#testchan", "testuser") |> IO.iodata_to_binary + assert expected == kick!("#testchan", "testuser") |> IO.iodata_to_binary() expected = <<"KICK #testchan testuser Get outta here!", ?\r, ?\n>> - assert expected == kick!("#testchan", "testuser", "Get outta here!") |> IO.iodata_to_binary + assert expected == kick!("#testchan", "testuser", "Get outta here!") |> IO.iodata_to_binary() expected = <<"MODE testuser -o", ?\r, ?\n>> - assert expected == mode!("testuser", "-o") |> IO.iodata_to_binary + assert expected == mode!("testuser", "-o") |> IO.iodata_to_binary() expected = <<"MODE #testchan +im", ?\r, ?\n>> - assert expected == mode!("#testchan", "+im") |> IO.iodata_to_binary + assert expected == mode!("#testchan", "+im") |> IO.iodata_to_binary() expected = <<"MODE #testchan +o testuser", ?\r, ?\n>> - assert expected == mode!("#testchan", "+o", "testuser") |> IO.iodata_to_binary + assert expected == mode!("#testchan", "+o", "testuser") |> IO.iodata_to_binary() expected = <<"INVITE testuser #testchan", ?\r, ?\n>> - assert expected == invite!("testuser", "#testchan") |> IO.iodata_to_binary + assert expected == invite!("testuser", "#testchan") |> IO.iodata_to_binary() end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 4b8b246..869559e 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1 @@ -ExUnit.start +ExUnit.start() diff --git a/test/utils_test.exs b/test/utils_test.exs index 0370a82..c7d1116 100644 --- a/test/utils_test.exs +++ b/test/utils_test.exs @@ -1,32 +1,37 @@ defmodule ExIRC.UtilsTest do use ExUnit.Case, async: true - use ExIRC.Commands - alias ExIRC.Utils, as: Utils alias ExIRC.Client.ClientState, as: ClientState + alias ExIRC.Utils, as: Utils doctest ExIRC.Utils test "Given a local date/time as a tuple, can retrieve get the CTCP formatted time" do - local_time = {{2013,12,6},{14,5,0}} # Mimics output of :calendar.local_time() + # Mimics output of :calendar.local_time() + local_time = {{2013, 12, 6}, {14, 5, 0}} assert Utils.ctcp_time(local_time) == "Fri Dec 06 14:05:00 2013" end test "Can parse a CTCP command" do - message = ':pschoenf NOTICE #testchan :' ++ '#{<<0o001>>}' ++ 'ACTION mind explodes!!' ++ '#{<<0o001>>}' + message = + ':pschoenf NOTICE #testchan :' ++ + '#{<<0o001>>}' ++ 'ACTION mind explodes!!' ++ '#{<<0o001>>}' + expected = %ExIRC.Message{ nick: "pschoenf", - cmd: "ACTION", + cmd: "ACTION", ctcp: true, args: ["#testchan", "mind explodes!!"] } + result = Utils.parse(message) assert expected == result end test "Parse cloaked user" do message = ':foo!foo@unaffiliated/foo PRIVMSG #bar Hiya.' + expected = %ExIRC.Message{ nick: "foo", cmd: "PRIVMSG", @@ -35,12 +40,14 @@ defmodule ExIRC.UtilsTest do user: "foo", args: ["#bar", "Hiya."] } + result = Utils.parse(message) assert expected == result end test "Parse uncloaked (normal) user" do message = ':foo!foo@80.21.56.43 PRIVMSG #bar Hiya.' + expected = %ExIRC.Message{ nick: "foo", cmd: "PRIVMSG", @@ -49,89 +56,94 @@ defmodule ExIRC.UtilsTest do user: "foo", args: ["#bar", "Hiya."] } + result = Utils.parse(message) assert expected == result end test "Parse INVITE message" do message = ':pschoenf INVITE testuser #awesomechan' + assert %ExIRC.Message{ - :nick => "pschoenf", - :cmd => "INVITE", - :args => ["testuser", "#awesomechan"] - } = Utils.parse(message) + :nick => "pschoenf", + :cmd => "INVITE", + :args => ["testuser", "#awesomechan"] + } = Utils.parse(message) end test "Parse KICK message" do message = ':pschoenf KICK #testchan lameuser' + assert %ExIRC.Message{ - :nick => "pschoenf", - :cmd => "KICK", - :args => ["#testchan", "lameuser"] - } = Utils.parse(message) + :nick => "pschoenf", + :cmd => "KICK", + :args => ["#testchan", "lameuser"] + } = Utils.parse(message) end test "Can parse RPL_ISUPPORT commands" do message = ':irc.example.org 005 nick NETWORK=Freenode PREFIX=(ov)@+ CHANTYPES=#&' - parsed = Utils.parse(message) - state = %ClientState{} + parsed = Utils.parse(message) + state = %ClientState{} + assert %ClientState{ - :channel_prefixes => ["#", "&"], - :user_prefixes => [{?o, ?@}, {?v, ?+}], - :network => "Freenode" - } = Utils.isup(parsed.args, state) + :channel_prefixes => ["#", "&"], + :user_prefixes => [{?o, ?@}, {?v, ?+}], + :network => "Freenode" + } = Utils.isup(parsed.args, state) end test "Can parse full prefix in messages" do assert %ExIRC.Message{ - nick: "WiZ", - user: "jto", - host: "tolsun.oulu.fi", - } = Utils.parse(':WiZ!jto@tolsun.oulu.fi NICK Kilroy') + nick: "WiZ", + user: "jto", + host: "tolsun.oulu.fi" + } = Utils.parse(':WiZ!jto@tolsun.oulu.fi NICK Kilroy') end test "Can parse prefix with only hostname in messages" do assert %ExIRC.Message{ - nick: "WiZ", - host: "tolsun.oulu.fi", - } = Utils.parse(':WiZ!tolsun.oulu.fi NICK Kilroy') + nick: "WiZ", + host: "tolsun.oulu.fi" + } = Utils.parse(':WiZ!tolsun.oulu.fi NICK Kilroy') end test "Can parse reduced prefix in messages" do assert %ExIRC.Message{ - nick: "Trillian", - } = Utils.parse(':Trillian SQUIT cm22.eng.umd.edu :Server out of control') + nick: "Trillian" + } = Utils.parse(':Trillian SQUIT cm22.eng.umd.edu :Server out of control') end test "Can parse server-only prefix in messages" do assert %ExIRC.Message{ - server: "ircd.stealth.net" - } = Utils.parse(':ircd.stealth.net 302 yournick :syrk=+syrk@millennium.stealth.net') + server: "ircd.stealth.net" + } = Utils.parse(':ircd.stealth.net 302 yournick :syrk=+syrk@millennium.stealth.net') end test "Can parse FULL STOP in username in prefixes" do assert %ExIRC.Message{ - nick: "nick", - user: "user.name", - host: "irc.example.org" - } = Utils.parse(':nick!user.name@irc.example.org PART #channel') + nick: "nick", + user: "user.name", + host: "irc.example.org" + } = Utils.parse(':nick!user.name@irc.example.org PART #channel') end test "Can parse EXCLAMATION MARK in username in prefixes" do assert %ExIRC.Message{ - nick: "nick", - user: "user!name", - host: "irc.example.org" - } = Utils.parse(':nick!user!name@irc.example.org PART #channel') + nick: "nick", + user: "user!name", + host: "irc.example.org" + } = Utils.parse(':nick!user!name@irc.example.org PART #channel') end test "parse join message" do message = ':pschoenf JOIN #elixir-lang' + assert %ExIRC.Message{ - nick: "pschoenf", - cmd: "JOIN", - args: ["#elixir-lang"] - } = Utils.parse(message) + nick: "pschoenf", + cmd: "JOIN", + args: ["#elixir-lang"] + } = Utils.parse(message) end test "Parse Slack's inappropriate RPL_TOPIC message as if it were an RPL_NOTOPIC" do @@ -148,62 +160,201 @@ defmodule ExIRC.UtilsTest do # a good idea when I realized that there's nothing in ExIRc that does anything # with 331 at all - they just fall on the floor, no crashes to be seen (ideally) message = ':irc.tinyspeck.com 332 jadams #elm-playground-news :' + assert %ExIRC.Message{ - nick: "jadams", - cmd: "331", - args: ["#elm-playground-news", "No topic is set"] - } = Utils.parse(message) + nick: "jadams", + cmd: "331", + args: ["#elm-playground-news", "No topic is set"] + } = Utils.parse(message) end test "Can parse simple unicode" do # ':foo!~user@172.17.0.1 PRIVMSG #bar :éáçíóö\r\n' - message = [58, 102, 111, 111, 33, 126, 117, 115, 101, 114, 64, 49, 55, 50, - 46, 49, 55, 46, 48, 46, 49, 32, 80, 82, 73, 86, 77, 83, 71, 32, - 35, 98, 97, 114, 32, 58, 195, 169, 195, 161, 195, 167, 195, 173, - 195, 179, 195, 182, 13, 10] + message = [ + 58, + 102, + 111, + 111, + 33, + 126, + 117, + 115, + 101, + 114, + 64, + 49, + 55, + 50, + 46, + 49, + 55, + 46, + 48, + 46, + 49, + 32, + 80, + 82, + 73, + 86, + 77, + 83, + 71, + 32, + 35, + 98, + 97, + 114, + 32, + 58, + 195, + 169, + 195, + 161, + 195, + 167, + 195, + 173, + 195, + 179, + 195, + 182, + 13, + 10 + ] + assert %ExIRC.Message{ - args: ["#bar", "éáçíóö"], - cmd: "PRIVMSG", - ctcp: false, - host: "172.17.0.1", - nick: "foo", - server: [], - user: "~user" - } = Utils.parse(message) + args: ["#bar", "éáçíóö"], + cmd: "PRIVMSG", + ctcp: false, + host: "172.17.0.1", + nick: "foo", + server: [], + user: "~user" + } = Utils.parse(message) end test "Can parse complex unicode" do # ':foo!~user@172.17.0.1 PRIVMSG #bar :Ĥélłø 차\r\n' - message = [58, 102, 111, 111, 33, 126, 117, 115, 101, 114, 64, 49, 55, 50, - 46, 49, 55, 46, 48, 46, 49, 32, 80, 82, 73, 86, 77, 83, 71, 32, - 35, 98, 97, 114, 32, 58, 196, 164, 195, 169, 108, 197, 130, 195, - 184, 32, 236, 176, 168, 13, 10] + message = [ + 58, + 102, + 111, + 111, + 33, + 126, + 117, + 115, + 101, + 114, + 64, + 49, + 55, + 50, + 46, + 49, + 55, + 46, + 48, + 46, + 49, + 32, + 80, + 82, + 73, + 86, + 77, + 83, + 71, + 32, + 35, + 98, + 97, + 114, + 32, + 58, + 196, + 164, + 195, + 169, + 108, + 197, + 130, + 195, + 184, + 32, + 236, + 176, + 168, + 13, + 10 + ] + assert %ExIRC.Message{ - args: ["#bar", "Ĥélłø 차"], - cmd: "PRIVMSG", - ctcp: false, - host: "172.17.0.1", - nick: "foo", - server: [], - user: "~user" - } = Utils.parse(message) + args: ["#bar", "Ĥélłø 차"], + cmd: "PRIVMSG", + ctcp: false, + host: "172.17.0.1", + nick: "foo", + server: [], + user: "~user" + } = Utils.parse(message) end test "Can parse latin1" do # ':foo!~user@172.17.0.1 PRIVMSG #bar :ééé\r\n' - message = [58, 102, 111, 111, 33, 126, 117, 115, 101, 114, 64, 49, 55, 50, - 46, 49, 55, 46, 48, 46, 49, 32, 80, 82, 73, 86, 77, 83, 71, 32, - 35, 98, 97, 114, 32, 58, 233, 233, 233, 13, 10] + message = [ + 58, + 102, + 111, + 111, + 33, + 126, + 117, + 115, + 101, + 114, + 64, + 49, + 55, + 50, + 46, + 49, + 55, + 46, + 48, + 46, + 49, + 32, + 80, + 82, + 73, + 86, + 77, + 83, + 71, + 32, + 35, + 98, + 97, + 114, + 32, + 58, + 233, + 233, + 233, + 13, + 10 + ] assert %ExIRC.Message{ - args: ["#bar", "ééé"], - cmd: "PRIVMSG", - ctcp: false, - host: "172.17.0.1", - nick: "foo", - server: [], - user: "~user" - } = Utils.parse(message) + args: ["#bar", "ééé"], + cmd: "PRIVMSG", + ctcp: false, + host: "172.17.0.1", + nick: "foo", + server: [], + user: "~user" + } = Utils.parse(message) end - end From fbd681af7c8fb455ed8487fb9ec57c043da1931d Mon Sep 17 00:00:00 2001 From: Ryan Winchester Date: Sun, 6 Mar 2022 14:27:03 -0400 Subject: [PATCH 2/5] Fix typespecs missing error tuple --- lib/exirc/client.ex | 53 ++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/lib/exirc/client.ex b/lib/exirc/client.ex index c66432b..c302e00 100644 --- a/lib/exirc/client.ex +++ b/lib/exirc/client.ex @@ -110,7 +110,7 @@ defmodule ExIRC.Client do server :: binary, port :: non_neg_integer, options :: list() | nil - ) :: :ok + ) :: :ok | {:error, :timeout | :inet.posix()} def connect!(client, server, port, options \\ []) do GenServer.call(client, {:connect, server, port, options, false}, :infinity) end @@ -129,7 +129,7 @@ defmodule ExIRC.Client do server :: binary, port :: non_neg_integer, options :: list() | nil - ) :: :ok + ) :: :ok | {:error, any} def connect_ssl!(client, server, port, options \\ []) do GenServer.call(client, {:connect, server, port, options, true}, :infinity) end @@ -137,7 +137,7 @@ defmodule ExIRC.Client do @doc """ Determine if the provided client process has an open connection to a server """ - @spec is_connected?(client :: pid) :: true | false + @spec is_connected?(client :: pid) :: boolean def is_connected?(client) do GenServer.call(client, :is_connected?) end @@ -160,7 +160,7 @@ defmodule ExIRC.Client do @doc """ Determine if the provided client is logged on to a server. """ - @spec is_logged_on?(client :: pid) :: true | false + @spec is_logged_on?(client :: pid) :: boolean | {:error, :not_connected} def is_logged_on?(client) do GenServer.call(client, :is_logged_on?) end @@ -174,7 +174,8 @@ defmodule ExIRC.Client do * `:ctcp` """ - @spec msg(client :: pid, type :: atom, nick :: binary, msg :: binary) :: :ok + @spec msg(client :: pid, type :: atom, nick :: binary, msg :: binary) :: + :ok | {:error, :not_connected | :not_logged_in} def msg(client, type, nick, msg) do GenServer.call(client, {:msg, type, nick, msg}, :infinity) end @@ -182,7 +183,8 @@ defmodule ExIRC.Client do @doc """ Send an action message, i.e. (/me slaps someone with a big trout). """ - @spec me(client :: pid, channel :: binary, msg :: binary) :: :ok + @spec me(client :: pid, channel :: binary, msg :: binary) :: + :ok | {:error, :not_connected | :not_logged_in} def me(client, channel, msg) do GenServer.call(client, {:me, channel, msg}, :infinity) end @@ -190,7 +192,7 @@ defmodule ExIRC.Client do @doc """ Change the client's nick. """ - @spec nick(client :: pid, new_nick :: binary) :: :ok + @spec nick(client :: pid, new_nick :: binary) :: :ok | {:error, :not_connected} def nick(client, new_nick) do GenServer.call(client, {:nick, new_nick}, :infinity) end @@ -198,7 +200,7 @@ defmodule ExIRC.Client do @doc """ Send a raw IRC command. """ - @spec cmd(client :: pid, raw_cmd :: binary) :: :ok + @spec cmd(client :: pid, raw_cmd :: binary) :: :ok | {:error, :not_connected | :not_logged_in} def cmd(client, raw_cmd) do GenServer.call(client, {:cmd, raw_cmd}) end @@ -206,7 +208,8 @@ defmodule ExIRC.Client do @doc """ Join a channel, with an optional password. """ - @spec join(client :: pid, channel :: binary, key :: binary | nil) :: :ok + @spec join(client :: pid, channel :: binary, key :: binary | nil) :: + :ok | {:error, :not_connected | :not_logged_in} def join(client, channel, key \\ "") do GenServer.call(client, {:join, channel, key}, :infinity) end @@ -214,7 +217,7 @@ defmodule ExIRC.Client do @doc """ Leave a channel. """ - @spec part(client :: pid, channel :: binary) :: :ok + @spec part(client :: pid, channel :: binary) :: :ok | {:error, :not_connected | :not_logged_in} def part(client, channel) do GenServer.call(client, {:part, channel}, :infinity) end @@ -222,12 +225,13 @@ defmodule ExIRC.Client do @doc """ Kick a user from a channel. """ - @spec kick(client :: pid, channel :: binary, nick :: binary, message :: binary | nil) :: :ok + @spec kick(client :: pid, channel :: binary, nick :: binary, message :: binary | nil) :: + :ok | {:error, :not_connected | :not_logged_in} def kick(client, channel, nick, message \\ "") do GenServer.call(client, {:kick, channel, nick, message}, :infinity) end - @spec names(client :: pid, channel :: binary) :: :ok + @spec names(client :: pid, channel :: binary) :: :ok | {:error, :not_connected | :not_logged_in} def names(client, channel) do GenServer.call(client, {:names, channel}, :infinity) end @@ -235,7 +239,7 @@ defmodule ExIRC.Client do @doc """ Ask the server for the user's informations. """ - @spec whois(client :: pid, user :: binary) :: :ok + @spec whois(client :: pid, user :: binary) :: :ok | {:error, :not_connected | :not_logged_in} def whois(client, user) do GenServer.call(client, {:whois, user}, :infinity) end @@ -243,7 +247,7 @@ defmodule ExIRC.Client do @doc """ Ask the server for the channel's users. """ - @spec who(client :: pid, channel :: binary) :: :ok + @spec who(client :: pid, channel :: binary) :: :ok | {:error, :not_connected | :not_logged_in} def who(client, channel) do GenServer.call(client, {:who, channel}, :infinity) end @@ -252,7 +256,7 @@ defmodule ExIRC.Client do Change mode for a user or channel. """ @spec mode(client :: pid, channel_or_nick :: binary, flags :: binary, args :: binary | nil) :: - :ok + :ok | {:error, :not_connected | :not_logged_in} def mode(client, channel_or_nick, flags, args \\ "") do GenServer.call(client, {:mode, channel_or_nick, flags, args}, :infinity) end @@ -260,7 +264,8 @@ defmodule ExIRC.Client do @doc """ Invite a user to a channel. """ - @spec invite(client :: pid, nick :: binary, channel :: binary) :: :ok + @spec invite(client :: pid, nick :: binary, channel :: binary) :: + :ok | {:error, :not_connected | :not_logged_in} def invite(client, nick, channel) do GenServer.call(client, {:invite, nick, channel}, :infinity) end @@ -268,7 +273,8 @@ defmodule ExIRC.Client do @doc """ Quit the server, with an optional part message. """ - @spec quit(client :: pid, msg :: binary | nil) :: :ok + @spec quit(client :: pid, msg :: binary | nil) :: + :ok | {:error, :not_connected | :not_logged_in} def quit(client, msg \\ "Leaving..") do GenServer.call(client, {:quit, msg}, :infinity) end @@ -276,7 +282,7 @@ defmodule ExIRC.Client do @doc """ Get details about each of the client's currently joined channels. """ - @spec channels(client :: pid) :: [binary] + @spec channels(client :: pid) :: [binary] | {:error, :not_connected | :not_logged_in} def channels(client) do GenServer.call(client, :channels) end @@ -284,7 +290,8 @@ defmodule ExIRC.Client do @doc """ Get a list of users in the provided channel. """ - @spec channel_users(client :: pid, channel :: binary) :: [binary] | {:error, atom} + @spec channel_users(client :: pid, channel :: binary) :: + [binary] | {:error, :not_connected | :not_logged_in | :no_such_channel} def channel_users(client, channel) do GenServer.call(client, {:channel_users, channel}) end @@ -292,7 +299,8 @@ defmodule ExIRC.Client do @doc """ Get the topic of the provided channel. """ - @spec channel_topic(client :: pid, channel :: binary) :: binary | {:error, atom} + @spec channel_topic(client :: pid, channel :: binary) :: + binary | {:error, :not_connected | :not_logged_in | :no_such_channel} def channel_topic(client, channel) do GenServer.call(client, {:channel_topic, channel}) end @@ -300,7 +308,8 @@ defmodule ExIRC.Client do @doc """ Get the channel type of the provided channel. """ - @spec channel_type(client :: pid, channel :: binary) :: atom | {:error, atom} + @spec channel_type(client :: pid, channel :: binary) :: + atom | {:error, :not_connected | :not_logged_in | :no_such_channel} def channel_type(client, channel) do GenServer.call(client, {:channel_type, channel}) end @@ -309,7 +318,7 @@ defmodule ExIRC.Client do Determine if a nick is present in the provided channel. """ @spec channel_has_user?(client :: pid, channel :: binary, nick :: binary) :: - boolean | {:error, atom} + boolean | {:error, :not_connected | :not_logged_in | :no_such_channel} def channel_has_user?(client, channel, nick) do GenServer.call(client, {:channel_has_user?, channel, nick}) end From 2fe78db961f14d31977e8c88d0cb7d23b87954c6 Mon Sep 17 00:00:00 2001 From: Ryan Winchester Date: Sun, 6 Mar 2022 14:34:01 -0400 Subject: [PATCH 3/5] Bump version to 2.1.0 as well --- mix.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index ff51516..b6b7dea 100644 --- a/mix.exs +++ b/mix.exs @@ -4,8 +4,8 @@ defmodule ExIRC.Mixfile do def project do [ app: :exirc, - version: "2.0.0", - elixir: "~> 1.6", + version: "2.1.0", + elixir: "~> 1.13", description: "An IRC client library for Elixir.", package: package(), test_coverage: [tool: ExCoveralls], From b70444e96406108aba75da260c088ff6d36ed0c2 Mon Sep 17 00:00:00 2001 From: Ryan Winchester Date: Sun, 6 Mar 2022 14:45:29 -0400 Subject: [PATCH 4/5] Add name to actions workflow --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c87147c..01b9470 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,5 +1,5 @@ +name: Tests on: push - jobs: test: runs-on: ${{matrix.os}} From acb16b470144dcd664fb2026afa0b9a1727f2900 Mon Sep 17 00:00:00 2001 From: Ryan Winchester Date: Sat, 12 Mar 2022 12:49:30 -0400 Subject: [PATCH 5/5] Change typespecs to iodata --- lib/exirc/client.ex | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/exirc/client.ex b/lib/exirc/client.ex index c302e00..de378a7 100644 --- a/lib/exirc/client.ex +++ b/lib/exirc/client.ex @@ -151,7 +151,7 @@ defmodule ExIRC.Client do :ok """ - @spec logon(client :: pid, pass :: binary, nick :: binary, user :: binary, name :: binary) :: + @spec logon(client :: pid, pass :: iodata, nick :: iodata, user :: iodata, name :: iodata) :: :ok | {:error, :not_connected} def logon(client, pass, nick, user, name) do GenServer.call(client, {:logon, pass, nick, user, name}, :infinity) @@ -174,7 +174,7 @@ defmodule ExIRC.Client do * `:ctcp` """ - @spec msg(client :: pid, type :: atom, nick :: binary, msg :: binary) :: + @spec msg(client :: pid, type :: atom, nick :: iodata, msg :: iodata) :: :ok | {:error, :not_connected | :not_logged_in} def msg(client, type, nick, msg) do GenServer.call(client, {:msg, type, nick, msg}, :infinity) @@ -183,7 +183,7 @@ defmodule ExIRC.Client do @doc """ Send an action message, i.e. (/me slaps someone with a big trout). """ - @spec me(client :: pid, channel :: binary, msg :: binary) :: + @spec me(client :: pid, channel :: iodata, msg :: iodata) :: :ok | {:error, :not_connected | :not_logged_in} def me(client, channel, msg) do GenServer.call(client, {:me, channel, msg}, :infinity) @@ -192,7 +192,7 @@ defmodule ExIRC.Client do @doc """ Change the client's nick. """ - @spec nick(client :: pid, new_nick :: binary) :: :ok | {:error, :not_connected} + @spec nick(client :: pid, new_nick :: iodata) :: :ok | {:error, :not_connected} def nick(client, new_nick) do GenServer.call(client, {:nick, new_nick}, :infinity) end @@ -200,7 +200,7 @@ defmodule ExIRC.Client do @doc """ Send a raw IRC command. """ - @spec cmd(client :: pid, raw_cmd :: binary) :: :ok | {:error, :not_connected | :not_logged_in} + @spec cmd(client :: pid, raw_cmd :: iodata) :: :ok | {:error, :not_connected | :not_logged_in} def cmd(client, raw_cmd) do GenServer.call(client, {:cmd, raw_cmd}) end @@ -208,7 +208,7 @@ defmodule ExIRC.Client do @doc """ Join a channel, with an optional password. """ - @spec join(client :: pid, channel :: binary, key :: binary | nil) :: + @spec join(client :: pid, channel :: iodata, key :: iodata | nil) :: :ok | {:error, :not_connected | :not_logged_in} def join(client, channel, key \\ "") do GenServer.call(client, {:join, channel, key}, :infinity) @@ -217,7 +217,7 @@ defmodule ExIRC.Client do @doc """ Leave a channel. """ - @spec part(client :: pid, channel :: binary) :: :ok | {:error, :not_connected | :not_logged_in} + @spec part(client :: pid, channel :: iodata) :: :ok | {:error, :not_connected | :not_logged_in} def part(client, channel) do GenServer.call(client, {:part, channel}, :infinity) end @@ -225,13 +225,13 @@ defmodule ExIRC.Client do @doc """ Kick a user from a channel. """ - @spec kick(client :: pid, channel :: binary, nick :: binary, message :: binary | nil) :: + @spec kick(client :: pid, channel :: iodata, nick :: iodata, message :: iodata | nil) :: :ok | {:error, :not_connected | :not_logged_in} def kick(client, channel, nick, message \\ "") do GenServer.call(client, {:kick, channel, nick, message}, :infinity) end - @spec names(client :: pid, channel :: binary) :: :ok | {:error, :not_connected | :not_logged_in} + @spec names(client :: pid, channel :: iodata) :: :ok | {:error, :not_connected | :not_logged_in} def names(client, channel) do GenServer.call(client, {:names, channel}, :infinity) end @@ -239,7 +239,7 @@ defmodule ExIRC.Client do @doc """ Ask the server for the user's informations. """ - @spec whois(client :: pid, user :: binary) :: :ok | {:error, :not_connected | :not_logged_in} + @spec whois(client :: pid, user :: iodata) :: :ok | {:error, :not_connected | :not_logged_in} def whois(client, user) do GenServer.call(client, {:whois, user}, :infinity) end @@ -247,7 +247,7 @@ defmodule ExIRC.Client do @doc """ Ask the server for the channel's users. """ - @spec who(client :: pid, channel :: binary) :: :ok | {:error, :not_connected | :not_logged_in} + @spec who(client :: pid, channel :: iodata) :: :ok | {:error, :not_connected | :not_logged_in} def who(client, channel) do GenServer.call(client, {:who, channel}, :infinity) end @@ -255,7 +255,7 @@ defmodule ExIRC.Client do @doc """ Change mode for a user or channel. """ - @spec mode(client :: pid, channel_or_nick :: binary, flags :: binary, args :: binary | nil) :: + @spec mode(client :: pid, channel_or_nick :: iodata, flags :: iodata, args :: iodata | nil) :: :ok | {:error, :not_connected | :not_logged_in} def mode(client, channel_or_nick, flags, args \\ "") do GenServer.call(client, {:mode, channel_or_nick, flags, args}, :infinity) @@ -264,7 +264,7 @@ defmodule ExIRC.Client do @doc """ Invite a user to a channel. """ - @spec invite(client :: pid, nick :: binary, channel :: binary) :: + @spec invite(client :: pid, nick :: iodata, channel :: iodata) :: :ok | {:error, :not_connected | :not_logged_in} def invite(client, nick, channel) do GenServer.call(client, {:invite, nick, channel}, :infinity) @@ -273,7 +273,7 @@ defmodule ExIRC.Client do @doc """ Quit the server, with an optional part message. """ - @spec quit(client :: pid, msg :: binary | nil) :: + @spec quit(client :: pid, msg :: iodata | nil) :: :ok | {:error, :not_connected | :not_logged_in} def quit(client, msg \\ "Leaving..") do GenServer.call(client, {:quit, msg}, :infinity)