Page MenuHomePhabricator

No OneTemporary

diff --git a/lib/exirc/client.ex b/lib/exirc/client.ex
index f84a1d2..7588f81 100644
--- a/lib/exirc/client.ex
+++ b/lib/exirc/client.ex
@@ -1,917 +1,917 @@
defmodule ExIRC.Client do
@moduledoc """
Maintains the state and behaviour for individual IRC client connections
"""
use ExIRC.Commands
use GenServer
import ExIRC.Logger
alias ExIRC.Channels
alias ExIRC.Utils
alias ExIRC.SenderInfo
alias ExIRC.Client.Transport
# 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,
channel_prefixes: "",
network: "",
user_prefixes: "",
login_time: "",
channels: [],
debug?: false,
retries: 0,
inet: :inet,
owner: nil,
whois_buffers: %{},
who_buffers: %{}
end
#################
# External API
#################
@doc """
Start a new IRC client process
Returns either {:ok, pid} or {:error, reason}
"""
@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}
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
"""
@spec stop!(client :: pid) :: {:stop, :normal, :ok, ClientState.t}
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
"""
@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
"""
@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
"""
@spec is_connected?(client :: pid) :: true | false
def is_connected?(client) do
GenServer.call(client, :is_connected?)
end
@doc """
Logon to a server
Example:
Client.logon pid, "password", "mynick", "username", "My Name"
"""
@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
"""
@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
Message types are:
:privmsg
:notice
:ctcp
"""
@spec msg(client :: pid, type :: atom, nick :: binary, msg :: binary) :: :ok | {:error, atom}
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)
"""
@spec me(client :: pid, channel :: binary, msg :: binary) :: :ok | {:error, atom}
def me(client, channel, msg) do
GenServer.call(client, {:me, channel, msg}, :infinity)
end
@doc """
Change the client's nick
"""
@spec nick(client :: pid, new_nick :: binary) :: :ok | {:error, atom}
def nick(client, new_nick) do
GenServer.call(client, {:nick, new_nick}, :infinity)
end
@doc """
Send a raw IRC command
"""
@spec cmd(client :: pid, raw_cmd :: binary) :: :ok | {:error, atom}
def cmd(client, raw_cmd) do
GenServer.call(client, {:cmd, raw_cmd})
end
@doc """
Join a channel, with an optional password
"""
@spec join(client :: pid, channel :: binary, key :: binary | nil) :: :ok | {:error, atom}
def join(client, channel, key \\ "") do
GenServer.call(client, {:join, channel, key}, :infinity)
end
@doc """
Leave a channel
"""
@spec part(client :: pid, channel :: binary) :: :ok | {:error, atom}
def part(client, channel) do
GenServer.call(client, {:part, channel}, :infinity)
end
@doc """
Kick a user from a channel
"""
@spec kick(client :: pid, channel :: binary, nick :: binary, message :: binary | nil) :: :ok | {:error, atom}
def kick(client, channel, nick, message \\ "") do
GenServer.call(client, {:kick, channel, nick, message}, :infinity)
end
@spec names(client :: pid, channel :: binary) :: :ok | {:error, atom}
def names(client, channel) do
GenServer.call(client, {:names, channel}, :infinity)
end
@doc """
Ask the server for the user's informations.
"""
@spec whois(client :: pid, user :: binary) :: :ok | {:error, atom()}
def whois(client, user) do
GenServer.call(client, {:whois, user}, :infinity)
end
@doc """
Ask the server for the channel's users
"""
@spec who(client :: pid, channel :: binary) :: :ok | {:error, atom()}
def who(client, channel) do
GenServer.call(client, {:who, channel}, :infinity)
end
@doc """
Change mode for a user or channel
"""
@spec mode(client :: pid, channel_or_nick :: binary, flags :: binary, args :: binary | nil) :: :ok | {:error, atom}
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
"""
@spec invite(client :: pid, nick :: binary, channel :: binary) :: :ok | {:error, atom}
def invite(client, nick, channel) do
GenServer.call(client, {:invite, nick, channel}, :infinity)
end
@doc """
Quit the server, with an optional part message
"""
@spec quit(client :: pid, msg :: binary | nil) :: :ok | {:error, atom}
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
"""
@spec channels(client :: pid) :: list(binary) | [] | {:error, atom}
def channels(client) do
GenServer.call(client, :channels)
end
@doc """
Get a list of users in the provided channel
"""
@spec channel_users(client :: pid, channel :: binary) :: list(binary) | [] | {:error, atom}
def channel_users(client, channel) do
GenServer.call(client, {:channel_users, channel})
end
@doc """
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
"""
@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
"""
@spec channel_has_user?(client :: pid, channel :: binary, nick :: binary) :: true | false | {: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
"""
@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
"""
@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
"""
@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
"""
@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
"""
@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]
end
###############
# GenServer API
###############
@doc """
Called when GenServer initializes the client
"""
@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
handlers =
Keyword.get(options, :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}}}
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
def handle_call(:state, _from, state), do: {:reply, state, state}
# 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
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
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
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
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
state = %{state | ssl?: ssl}
# 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
{: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
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} }
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)
# 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
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({: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
{:reply, :ok, state}
end
# Handle /me messages
def handle_call({:me, channel, msg}, _from, state) do
data = me!(channel, msg)
Transport.send state, data
{:reply, :ok, state}
end
# 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
def handle_call({:part, channel}, _from, state) do
Transport.send(state, part!(channel))
{:reply, :ok, state}
end
# 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
def handle_call({:names, channel}, _from, state) do
Transport.send(state, names!(channel))
{:reply, :ok, state}
end
def handle_call({:whois, user}, _from, state) do
Transport.send(state, whois!(user))
{:reply, :ok, state}
end
def handle_call({:who, channel}, _from, state) do
Transport.send(state, who!(channel))
{:reply, :ok, state}
end
# 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
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
def handle_call({:quit, msg}, _from, state) do
if state.connected? do
Transport.send state, quit!(msg)
send_event(:disconnected, 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
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
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
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
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 the client's 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()
}
{:noreply, new_state}
end
@doc """
Handle messages from the SSL socket connection.
"""
# Handles the client's 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
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()
}
{:stop, {:tcp_error, socket}, new_state}
end
# 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
def handle_info({:tcp, _, data}, state) do
debug? = state.debug?
case Utils.parse(data) do
%ExIRC.Message{ctcp: true} = msg ->
handle_data msg, state
{:noreply, state}
%ExIRC.Message{ctcp: false} = msg ->
handle_data msg, state
%ExIRC.Message{ctcp: :invalid} = msg when debug? ->
send_event msg, state
{:noreply, state}
_ ->
{:noreply, state}
end
end
# 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
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
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)
def handle_info(_, state) do
{:noreply, state}
end
@doc """
Handle termination
"""
def terminate(_reason, state) do
if state.socket != nil do
Transport.close state
%{state | socket: nil}
end
:ok
end
@doc """
Transform state for hot upgrades/downgrades
"""
def code_change(_old, state, _extra), do: {:ok, state}
################
# Data handling
################
@doc """
Handle ExIRC.Messages received from the server.
"""
# 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"
new_state = %{state | logged_on?: true, login_time: :erlang.timestamp()}
send_event :logged_in, new_state
{:noreply, new_state}
end
# 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"
{:noreply, Utils.isup(msg.args, state)}
end
# 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)
new_state = %{state | channels: channels}
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
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)
new_state = %{state | channels: channels}
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
# 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
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
if state.debug? do
debug "INITIAL TOPIC MSG"
debug "1. TOPIC SET FOR #{channel} TO #{topic}"
end
channels = Channels.set_topic(state.channels, channel, topic)
new_state = %{state | channels: channels}
send_event {:topic_changed, channel, topic}, new_state
{:noreply, new_state}
end
## WHOIS
def handle_data(%ExIRC.Message{cmd: @rpl_whoisuser, args: [_sender, nickname, username, hostname, _, realname]}, state) do
user = %{nickname: nickname, username: username, hostname: hostname, realname: realname}
{:noreply, %ClientState{state|whois_buffers: Map.put(state.whois_buffers, nickname, user)}}
end
def handle_data(%ExIRC.Message{cmd: @rpl_whoiscertfp, args: [_sender, nickname, "has client certificate fingerprint "<> fingerprint]}, state) do
{:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nickname, :certfp], fingerprint)}}
end
def handle_data(%ExIRC.Message{cmd: @rpl_whoisregnick, args: [_sender, nickname, _message]}, state) do
{:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nickname, :registered_nick?], true)}}
end
def handle_data(%ExIRC.Message{cmd: @rpl_whoishelpop, args: [_sender, nickname, _message]}, state) do
{:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nickname, :helpop?], true)}}
end
def handle_data(%ExIRC.Message{cmd: @rpl_whoischannels, args: [_sender, nickname, channels]}, state) do
chans = String.split(channels, " ")
{:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nickname, :channels], chans)}}
end
def handle_data(%ExIRC.Message{cmd: @rpl_whoisserver, args: [_sender, nickname, server_addr, server_name]}, state) do
new_buffer = state.whois_buffers
|> put_in([nickname, :server_name], server_name)
|> put_in([nickname, :server_address], server_addr)
{:noreply, %ClientState{state|whois_buffers: new_buffer}}
end
def handle_data(%ExIRC.Message{cmd: @rpl_whoisoperator, args: [_sender, nickname, _message]}, state) do
{:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nickname, :ircop?], true)}}
end
def handle_data(%ExIRC.Message{cmd: @rpl_whoisaccount, args: [_sender, nickname, account_name, _message]}, state) do
{:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nickname, :account_name], account_name)}}
end
def handle_data(%ExIRC.Message{cmd: @rpl_whoissecure, args: [_sender, nickname, _message]}, state) do
{:noreply, %ClientState{state|whois_buffers: put_in(state.whois_buffers, [nickname, :ssl?], true)}}
end
def handle_data(%ExIRC.Message{cmd: @rpl_whoisidle, args: [_sender, nickname, idling_time, signon_time, _message]}, state) do
new_buffer = state.whois_buffers
|> put_in([nickname, :idling_time], idling_time)
|> put_in([nickname, :signon_time], signon_time)
{:noreply, %ClientState{state|whois_buffers: new_buffer}}
end
def handle_data(%ExIRC.Message{cmd: @rpl_endofwhois, args: [_sender, nickname, _message]}, state) do
buffer = struct(ExIRC.Whois, state.whois_buffers[nickname])
send_event {:whois, buffer}, state
{:noreply, %ClientState{state|whois_buffers: Map.delete(state.whois_buffers, nickname)}}
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?,
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])}}
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)
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}}"
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
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)
new_state = %{state | channels: channels}
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
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)
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}"
new_state = %{state | nick: new_nick}
send_event {:nick_changed, new_nick}, new_state
{:noreply, new_state}
end
# 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)
new_state = %{state | channels: channels}
send_event {:nick_changed, nick, new_nick}, new_state
{:noreply, new_state}
end
# 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
{:noreply, state}
end
# 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)
new_state = %{state | channels: channels}
send_event {:parted, channel}, new_state
{:noreply, new_state}
end
# 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)
new_state = %{state | channels: channels}
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"
channels = Channels.user_quit(state.channels, from)
new_state = %{state | channels: channels}
send_event {:quit, reason, sender}, new_state
{:noreply, new_state}
end
# 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!"
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};
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
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
{: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
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
{: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
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
{: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
sender = %SenderInfo{nick: from, host: host, user: user}
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
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 we were mentioned, fire that event as well
if String.contains?(message, 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
sender = %SenderInfo{nick: from, host: host, user: user}
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}
if String.contains?(message, "identify") do
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)
end
{:noreply, state}
end
# 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
{: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)
end
defp do_add_handler(pid, handlers) do
case Enum.member?(handlers, pid) do
false ->
ref = Process.monitor(pid)
[{pid, ref} | handlers]
true ->
handlers
end
end
defp do_remove_handler(pid, handlers) do
case List.keyfind(handlers, pid, 0) do
{pid, ref} ->
Process.demonitor(ref)
List.keydelete(handlers, pid, 0)
nil ->
handlers
end
end
defp debug(msg) do
IO.puts(IO.ANSI.green() <> msg <> IO.ANSI.reset())
end
end
diff --git a/test/commands_test.exs b/test/commands_test.exs
index 38c2e90..97837ae 100644
--- a/test/commands_test.exs
+++ b/test/commands_test.exs
@@ -1,48 +1,48 @@
defmodule ExIRC.CommandsTest do
use ExUnit.Case, async: true
use ExIRC.Commands
test "Commands are formatted properly" do
expected = <<0o001, "TESTCMD", 0o001, ?\r, ?\n>>
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
expected = <<"PASS testpass", ?\r, ?\n>>
assert expected == pass!("testpass") |> IO.iodata_to_binary
expected = <<"NICK testnick", ?\r, ?\n>>
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
expected = <<"PONG testnick", ?\r, ?\n>>
assert expected == pong1!("testnick") |> IO.iodata_to_binary
expected = <<"PONG testnick othernick", ?\r, ?\n>>
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
expected = <<"NOTICE testnick :Test notice!", ?\r, ?\n>>
assert expected == notice!("testnick", "Test notice!") |> IO.iodata_to_binary
expected = <<"JOIN testchan", ?\r, ?\n>>
assert expected == join!("testchan") |> IO.iodata_to_binary
expected = <<"JOIN testchan chanpass", ?\r, ?\n>>
assert expected == join!("testchan", "chanpass") |> IO.iodata_to_binary
expected = <<"PART testchan", ?\r, ?\n>>
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
expected = <<"KICK #testchan testuser", ?\r, ?\n>>
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
expected = <<"MODE testuser -o", ?\r, ?\n>>
assert expected == mode!("testuser", "-o") |> IO.iodata_to_binary
expected = <<"MODE #testchan +im", ?\r, ?\n>>
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
expected = <<"INVITE testuser #testchan", ?\r, ?\n>>
assert expected == invite!("testuser", "#testchan") |> IO.iodata_to_binary
end
end

File Metadata

Mime Type
text/x-diff
Expires
Fri, Mar 14, 6:02 PM (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
33663
Default Alt Text
(39 KB)

Event Timeline