summaryrefslogtreecommitdiff
path: root/lib/irc/client
diff options
context:
space:
mode:
Diffstat (limited to 'lib/irc/client')
-rw-r--r--lib/irc/client/command.ex50
-rw-r--r--lib/irc/client/command/account.ex18
-rw-r--r--lib/irc/client/command/away.ex25
-rw-r--r--lib/irc/client/command/chghost.ex15
-rw-r--r--lib/irc/client/command/invite.ex21
-rw-r--r--lib/irc/client/command/join.ex40
-rw-r--r--lib/irc/client/command/names.ex22
-rw-r--r--lib/irc/client/command/who.ex62
-rw-r--r--lib/irc/client/user_cache.ex31
9 files changed, 284 insertions, 0 deletions
diff --git a/lib/irc/client/command.ex b/lib/irc/client/command.ex
new file mode 100644
index 0000000..a84324f
--- /dev/null
+++ b/lib/irc/client/command.ex
@@ -0,0 +1,50 @@
+defmodule Irc.Client.Command do
+
+ @moduledoc """
+ Implementations of IRC protocol commands and capabilities.
+
+ The Capabs modules are used by the BaseClient to implement everything.
+
+ Capabs modules are mostly stateless, except when they enter the buffering FSM state.
+
+ It is recommended that event names matches the command name, to help awaiting response.
+ """
+
+ @doc """
+ Returns which server-initiated and client-initiated commands the module supports, and IRCv3 capabs to be requested to the server.
+ """
+ @callback init(Keyword.t) :: {commands :: [String.t],
+ client_commands :: [atom],
+ capabs :: [String.t]}
+
+ @type buffer :: any()
+ @type event :: {client_command :: atom | atom, any()}
+
+ @type buffer_ret :: :buffer | {:buffer, buffer()}
+ @type line_return :: :ok | {:ok, [action]} | {:error, any()} | buffer_ret
+ @type buffer_return :: buffer_ret | line_return | :postpone
+
+ @type send_action :: {:send, Line.t()}
+ @type event_action :: {:event, event()}
+ @type action :: send_action | event_action
+
+ @doc """
+ Handles an incoming line.
+ """
+ @callback handle_line(Irc.Line.t(), Irc.Connection.t()) :: line_return
+
+ @doc """
+ Handles an incoming line when in buffering state.
+
+ Returning anything else than `:buffer` or `:postpone` will exit the buffering state.
+
+ Postponed lines will be treated as usual (per their respective module) once the buffering state exits.
+ """
+ @callback handle_buffer(Irc.Line.t(), buffer, Irc.Connection.t()) :: buffer_return
+
+ @doc """
+ Handles a user requested command.
+ """
+ @callback handle_command(client_command :: atom, args :: list, Irc.Connection.t()) :: line_return
+
+end
diff --git a/lib/irc/client/command/account.ex b/lib/irc/client/command/account.ex
new file mode 100644
index 0000000..c5ec1a9
--- /dev/null
+++ b/lib/irc/client/command/account.ex
@@ -0,0 +1,18 @@
+defmodule Irc.Client.Command.Account do
+ alias Irc.Parser.Line
+
+ @type t :: logout :: {:account, Irc.Mask.t} | login :: {:account, Irc.Mask.t, String.t}
+
+ def init() do
+ {"ACCOUNT", nil, "account-notify"}
+ end
+
+ def handle_line(%Line{command: "ACCOUNT", source: target, args: ["*"]}) do
+ {:event, {:account, target}}
+ end
+
+ def handle_line(%Line{command: "ACCOUNT", source: target, args: [account_name]}) do
+ {:event, {:account, target, account_name}}
+ end
+
+end
diff --git a/lib/irc/client/command/away.ex b/lib/irc/client/command/away.ex
new file mode 100644
index 0000000..3b6b38e
--- /dev/null
+++ b/lib/irc/client/command/away.ex
@@ -0,0 +1,25 @@
+defmodule Irc.Client.Command.Away do
+ alias Irc.Parser.Line
+
+ @type t :: away :: {:away, Irc.Mask.t, String.t} | unaway :: {:away, Irc.Mask.t}
+
+ def init() do
+ {"AWAY", :away, "away-notify"}
+ end
+
+ def handle_command(:away, args, _) do
+ command = case args do
+ [] -> ['AWAY']
+ [message] -> ['AWAY :', message]
+ end
+ {:send, command}
+ end
+
+ def hanle_line(%Line{command: "AWAY", source: source, args: args}, _) do
+ case args do
+ [] -> {:event, {:away, source}}
+ [message] -> {:event, {:away, {source, message}}}
+ end
+ end
+
+end
diff --git a/lib/irc/client/command/chghost.ex b/lib/irc/client/command/chghost.ex
new file mode 100644
index 0000000..2b4adab
--- /dev/null
+++ b/lib/irc/client/command/chghost.ex
@@ -0,0 +1,15 @@
+defmodule Irc.Client.Command.Chghost do
+ alias Irc.Parser.Line
+
+ @type t :: {:chghost, old_mask :: Irc.Mask.t, new_mask :: Irc.Mask.t}
+
+ def init() do
+ {"CHGHOST", nil, "chghost"}
+ end
+
+ def handle_line(%Line{source: old_mask, args: [new_user, new_host]}, _) do
+ new_mask = %Irc.Mask{old_mask | user: new_user, host: new_host}
+ {:event, {:chghost, old_mask, new_mask}}
+ end
+
+end
diff --git a/lib/irc/client/command/invite.ex b/lib/irc/client/command/invite.ex
new file mode 100644
index 0000000..833c2fc
--- /dev/null
+++ b/lib/irc/client/command/invite.ex
@@ -0,0 +1,21 @@
+defmodule Irc.Client.Command.Invite do
+ alias Irc.Parser.Line
+
+ @type invite :: {:invite, channel :: String.t(), inviter :: String.t(), invited_nick :: String.t()}
+ @type invited :: {:invite, channel :: String.t, inviter :: Irc.Mask.t}
+ @type t :: invite | invited
+
+ def init() do
+ {"INVITE", :invite, "invite-notify"}
+ end
+
+ def handle_line(%Line{command: "INVITE", source: inviter, args: [invited, channel]}) do
+ {:event, {:invite, channel, inviter, invited}}
+ end
+
+ def handle_line(%Line{command: "INVITE", source: inviter, args: [channel]}) do
+ {:event, {:invite, channel, inviter}}
+ end
+
+end
+
diff --git a/lib/irc/client/command/join.ex b/lib/irc/client/command/join.ex
new file mode 100644
index 0000000..f4728c7
--- /dev/null
+++ b/lib/irc/client/command/join.ex
@@ -0,0 +1,40 @@
+defmodule Irc.Client.Command.Join do
+ alias Irc.Parser.Line
+ require Line
+ import Line
+
+ @type t :: {:join, channel :: String.t()} | {:join, channel :: String.t(), Irc.User.t()}
+
+ def init(_args) do
+ {"JOIN", :join, ["extended-join"]}
+ end
+
+ # An user joined
+ def handle_line(line = %Line{command: "JOIN", source: source, args: [channel | args]}, conn) do
+ if Line.self?(line, conn) do
+ {:event, {:join, channel}}
+ else
+ user = Irc.User.from_mask(source)
+ user = case args do
+ [] -> user
+ [account, name] ->
+ account = if account != "*", do: account
+ {channel, %Irc.User{user | account: account, name: name}}
+ end
+ {:event, {:join, channel, user}}
+ end
+ end
+
+ # Join a channel
+ def handle_command(:join, [channel], _conn_info) do
+ {:send, ['JOIN', channel]}
+ end
+
+ # Outdated!
+ def handle_buffer(%Line{command: "JOIN", source: %Irc.Mask{nick: nick}, args: [channel]}, buffer = %{channel: channel}, %Irc.Connection{nick: nick}) do
+ {:finish, nil, {:event, {:join, channel}}}
+ end
+
+ def handle_buffer(_, _), do: :postpone
+
+end
diff --git a/lib/irc/client/command/names.ex b/lib/irc/client/command/names.ex
new file mode 100644
index 0000000..b5a84a6
--- /dev/null
+++ b/lib/irc/client/command/names.ex
@@ -0,0 +1,22 @@
+defmodule Irc.Client.Command.Names do
+ alias Irc.Parser.Line
+ require Irc.Parser.Numeric
+ import Irc.Parser.Numeric
+
+ def init() do
+ {[rpl_NAMREPLY(), rpl_ENDOFNAMES()], :names, ["userhost-in-names"]}
+ end
+
+ def handle_line(%Line{command: rpl_NAMREPLY()}) do
+ :buffer
+ end
+
+ def handle_line(%Line{command: rpl_ENDOFNAMES(), args: [_, target | _]}) do
+ :finish
+ end
+
+ def handle_buffer(buffer = [%Line{args: [_, target | _]} | _]) do
+ {:event, {:names, target, buffer}}
+ end
+
+end
diff --git a/lib/irc/client/command/who.ex b/lib/irc/client/command/who.ex
new file mode 100644
index 0000000..1654ef6
--- /dev/null
+++ b/lib/irc/client/command/who.ex
@@ -0,0 +1,62 @@
+defmodule Irc.Client.Command.Who do
+ alias Irc.Parser.Line
+ alias Irc.User
+ use Irc.Parser.Numeric
+
+ @type t :: {:who, target :: String.t, [Irc.User.t]}
+
+ def init(_) do
+ {nil, "WHO", nil}
+ end
+
+ def handle_command("WHO", [target], conn) do
+ args = if Irc.Connection.supports?(conn, "whox") do
+ " nuhs%cuhsnfdar"
+ end
+ {:buffer, %{target: target, acc: [], error: nil}, {:send, ['WHO ', target, args]}}
+ end
+
+ @errors [err_NOSUCHSERVER(), rpl_TRYAGAIN()]
+ def handle_buffer(%Line{command: error, args: args}, buffer, _conn) when error in @errors do
+ #?
+ {:buffer, %{buffer | error: {error, args}}}
+ end
+
+ def handle_buffer(%Line{command: rpl_ENDOFWHO()}, buffer, _conn) do
+ result = if buffer.error do
+ {:error, buffer.error}
+ else
+ {:ok, buffer.acc}
+ end
+ {:finish, nil, {:event, {:who, buffer.target, result}}}
+ end
+
+ def handle_buffer(%Line{command: rpl_WHOREPLY(), args: args}, buffer, conn) do
+ case args do
+ [_, c, u, h, s, n, f, d | a_r] ->
+ {a,r} = case a_r do
+ [a, r] -> {a, r}
+ [r] -> {nil, r}
+ end
+ {modes, _} = Irc.Parser.Prefix.parse(f, conn)
+ chanmodes = %{buffer.target => modes}
+ user = %Irc.User{
+ user: u,
+ host: h,
+ server: s,
+ nick: n,
+ account: a,
+ name: r,
+ channels: [buffer.target],
+ chanmodes: chanmodes
+ }
+
+ _ ->
+ {:finish, nil} # ERROFFEL
+ end
+ {:buffer, %{buffer | acc: [args | buffer.acc]}}
+ end
+
+ def handle_buffer(_, _), do: :postpone
+
+end
diff --git a/lib/irc/client/user_cache.ex b/lib/irc/client/user_cache.ex
new file mode 100644
index 0000000..2653630
--- /dev/null
+++ b/lib/irc/client/user_cache.ex
@@ -0,0 +1,31 @@
+defmodule Irc.Client.UserCache do
+
+ # tuple: {nick, user}
+
+ def init(args) do
+ opts = [{:read_concurrency, true}, :protected]
+ :ets.new(__MODULE__, opts)
+ end
+
+ def lookup(ets, nick) do
+ case :ets.lookup(ets, nick) do
+ [{^nick, user}] -> user
+ _ -> nil
+ end
+ end
+
+ def change(ets, nick, changes) do
+ initial = if user = lookup(ets, nick), do: user, else: %Irc.User{nick: nick}
+ user = User.change(initial, changes)
+ put(ets, nick, user)
+ end
+
+ defp put(ets, original_nick, user) do
+ tuple = {user.nick, user}
+ if original_nick != user.nick do
+ true = :ets.delete(ets, original_nick)
+ end
+ :ets.insert(ets, tuple)
+ end
+
+end