Java tutorial
/* * This file is part of Rice. * Copyright Massimo Neri 2014 <hello@mneri.me> * * Rice is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Rice is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Rice. If not, see <http://www.gnu.org/licenses/>. */ package me.mneri.rice; import static me.mneri.rice.Commands.*; import java.io.OutputStreamWriter; import java.net.Socket; import java.nio.charset.Charset; import java.security.KeyStore; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManagerFactory; import me.mneri.rice.ctcp.CTCP; import me.mneri.rice.dcc.DCC; import me.mneri.rice.event.Event; import me.mneri.rice.event.EventEmitter; import me.mneri.rice.filter.Filter; import me.mneri.rice.filter.IgnoreFilter; import me.mneri.rice.flood.FloodController; import me.mneri.rice.flood.StandardFloodController; import me.mneri.rice.store.MemoryStoreFactory; import me.mneri.rice.store.StoreFactory; import me.mneri.rice.text.CaseMapping; import me.mneri.rice.text.StringBundler; import me.mneri.rice.text.TextUtils; import org.apache.commons.codec.binary.Base64; public class Connection extends EventEmitter { public static final String BOUNCE = "BOUNCE"; public static final String CLOSE = "CLOSE"; public static final String CONNECT = "CONNECT"; public static final String NEW_CONVERSATION = "NEW_CONVERSATION"; public static final String REGISTER = "REGISTER"; public static final String REMOVE_CONVERSATION = "REMOVE_CONVERSATION"; public static final String START = "START"; public static final Set<String> SUPPORTED_CAPABILITIES = Collections .unmodifiableSet(new HashSet<>(Arrays.asList("away-notify", "extended-join", "multi-prefix", "sasl", "server-time", "znc.in/server-time-iso"))); public final CTCP ctcp; public final DCC dcc; private boolean mAutoBounce; private List<String> mAutoJoin; private boolean mAutoNickChange; private int mAutoReconnectTimeout; private Set<Character> mAvailChannelModes = new HashSet<>(); private Set<String> mCapabilities = new HashSet<>(); private CaseMapping mCaseMapping = CaseMapping.RFC1459; private LinkedHashMap<String, Conversation> mConversations = new LinkedHashMap<>(); private String mCurrentHost; private Charset mEncoding; private HashMap<String, Object> mExtras; private StoreFactory<Message> mFactory; private ArrayList<Filter> mFilters = new ArrayList<>(); private FloodController mFloodController; private String mHost; private Set<String> mHostCapabilities = new HashSet<>(); private String mHostIrcdVersion; private IgnoreFilter mIgnoreFilter = new IgnoreFilter(); private boolean mIsupportCompilant; private InputThread mInputThread; private String mLoginMode; private String mNetwork; private String mNick; private OutputInterface mOutputInterface; private String mPass; private int mPort; private String mReal; private boolean mSecure; private int mSoTimeout; private Socket mSocket; private State mState = State.CLOSED; private String mUser; private HashMap<Character, Boolean> mUserMode = new HashMap<>(); private HashMap<String, User> mUsers = new HashMap<>(); private Set<String> mWantedCapabilities; private String mWantedNick; public enum State { CLOSED, STARTED, CONNECTED, REGISTERED } public static class Builder { private static final int DEFAULT_SOCKET_TIMEOUT = 5 * 60 * 1000; private boolean mAutoBounce = true; private List<String> mAutoJoin = new ArrayList<>(); private boolean mAutoNickChange = true; private int mAutoReconnectTimeout = -1; private Charset mEncoding; private HashMap<String, Object> mExtras = new HashMap<>(); private StoreFactory<Message> mFactory; private ArrayList<Filter> mFilters = new ArrayList<>(); private FloodController mFloodController; private String mHost; private HashSet<String> mIgnores = new HashSet<>(); private String mLoginMode; private String mNick; private String mPass; private int mPort; private String mReal; private boolean mSecure; private int mSoTimeout; private String mUser; private Set<String> mWantedCapabilities; public Builder autoBounce(boolean bounce) { mAutoBounce = bounce; return this; } public Builder autoJoin(List<String> channels) { mAutoJoin.addAll(channels); return this; } public Builder autoJoin(String... channels) { autoJoin(Arrays.asList(channels)); return this; } public Builder autoNickChange(boolean auto) { mAutoNickChange = auto; return this; } public Builder autoReconnect(int timeout) { mAutoReconnectTimeout = timeout; return this; } public Connection build() { if (TextUtils.isEmpty(mHost) || TextUtils.isEmpty(mNick) || TextUtils.isEmpty(mUser) || TextUtils.isEmpty(mReal)) throw new IllegalStateException("You must set at least a host, a nick, a user and a real name."); Connection connection = new Connection(); connection.mAutoBounce = mAutoBounce; connection.mAutoJoin = mAutoJoin; connection.mAutoNickChange = mAutoNickChange; connection.mAutoReconnectTimeout = mAutoReconnectTimeout; connection.mWantedCapabilities = (mWantedCapabilities != null ? mWantedCapabilities : SUPPORTED_CAPABILITIES); connection.mEncoding = (mEncoding != null ? mEncoding : Charset.defaultCharset()); connection.mExtras = mExtras; connection.mFactory = (mFactory != null ? mFactory : new MemoryStoreFactory<Message>()); connection.mFloodController = (mFloodController != null ? mFloodController : new StandardFloodController()); connection.mHost = mHost; connection.mLoginMode = (!TextUtils.isEmpty(mLoginMode) ? mLoginMode : "8"); connection.mWantedNick = mNick; connection.mPass = mPass; connection.mPort = (mPort != 0 ? mPort : (mSecure ? 6697 : 6667)); connection.mReal = mReal; connection.mSecure = mSecure; connection.mSoTimeout = (mSoTimeout > 0 ? mSoTimeout : DEFAULT_SOCKET_TIMEOUT); connection.mUser = mUser; connection.mIgnoreFilter.add(mIgnores); connection.mFilters.add(connection.mIgnoreFilter); connection.mFilters.addAll(mFilters); return connection; } public Builder capabilities(Set<String> capabilities) { mWantedCapabilities = capabilities; return this; } public Builder encoding(Charset charset) { mEncoding = charset; return this; } public Builder encoding(String charset) { encoding(Charset.forName(charset)); return this; } public Builder extra(String key, Object value) { mExtras.put(key, value); return this; } public Builder filter(Filter filter) { mFilters.add(filter); return this; } public Builder floodController(FloodController fc) { mFloodController = fc; return this; } public Builder host(String host) { mHost = host; return this; } public Builder ignore(List<String> users) { mIgnores.addAll(users); return this; } public Builder ignore(String... users) { ignore(Arrays.asList(users)); return this; } public Builder mode(String mode) { mLoginMode = mode; return this; } public Builder nick(String nick) { mNick = nick; return this; } public Builder pass(String pass) { mPass = pass; return this; } public Builder port(int port) { this.mPort = port; return this; } public Builder real(String real) { mReal = real; return this; } public Builder secure(boolean secure) { mSecure = secure; return this; } public Builder soTimeout(int timeout) { mSoTimeout = timeout; return this; } public Builder storeFactory(StoreFactory<Message> factory) { mFactory = factory; return this; } public Builder user(String user) { mUser = user; return this; } } private class InputThreadObserver implements InputThread.Observer { @Override public void onDisconnect() { onDisconnection(); } @Override public void onLine(String line) { Message message = Message.from(line); for (Filter filter : mFilters) { if (filter.doFilter(message)) return; } emit(new Event(message.command, message)); } } private Connection() { initCallbacks(); ctcp = new CTCP(this); dcc = new DCC(this); } public void admin() { admin(null); } public void admin(String target) { if (TextUtils.isEmpty(target)) sendLine(ADMIN); else sendLine(ADMIN, target); } public void away() { away(null); } public void away(String text) { if (TextUtils.isEmpty(text)) sendLine(AWAY); else sendLine(AWAY, text); } public void cap(String subcommand) { sendLine(CAP, subcommand); } public void cap(String subcommand, String... params) { String[] stuff = new String[params.length + 2]; stuff[0] = CAP; stuff[1] = subcommand; System.arraycopy(params, 0, stuff, 2, params.length); sendLine(stuff); } public void devoice(String channel, String nick) { mode(channel, "-v " + nick); } public Set<String> getCapabilities() { return mCapabilities; } public CaseMapping getCaseMapping() { return mCaseMapping; } public Conversation getConversation(int position) { return new ArrayList<>(mConversations.values()).get(position); } public Conversation getConversation(String name) { return mConversations.get(name); } public List<Conversation> getConversations() { return new ArrayList<>(mConversations.values()); } public Charset getEncoding() { return mEncoding; } public Object getExtra(String key) { return getExtra(key, null); } public Object getExtra(String key, Object defaultValue) { Object value = mExtras.get(key); return (value != null ? value : defaultValue); } public String getHost() { return (!TextUtils.isEmpty(mCurrentHost) ? mCurrentHost : mHost); } public Set<String> getHostCapabilities() { return Collections.unmodifiableSet(mHostCapabilities); } public String getHostIrcdVersion() { return mHostIrcdVersion; } public Set<String> getIgnores() { return mIgnoreFilter.get(); } public HashMap<Character, Boolean> getMode() { return mUserMode; } public boolean getMode(char m) { Boolean b = mUserMode.get(m); return b != null && b; } public String getNetwork() { return mNetwork; } public String getNick() { return mNick; } public int getPort() { return mPort; } public String getReal() { return mReal; } public State getState() { return mState; } public String getUser() { return mUser; } public void ignore(String nick) { mIgnoreFilter.add(nick); } public void info() { info(null); } public void info(String target) { if (TextUtils.isEmpty(target)) sendLine(INFO); else sendLine(INFO, target); } private void initCallbacks() { // Cambi di stato on(REGISTER, event -> { if (mAutoJoin.size() > 0) { for (String channel : mAutoJoin) join(channel); mAutoJoin.clear(); } else { for (Conversation conversation : mConversations.values()) { if (conversation.getType() == Conversation.Type.CHANNEL) join(conversation.getName()); } } }); on(CLOSE, event -> { OutputInterfaceFactory f = OutputInterfaceFactory.instance(); //@formatter:off try { mSocket.close(); } catch (Exception ignored) { } finally { mSocket = null; } try { mInputThread.quit(); } catch (Exception ignored) { } finally { mInputThread = null; } try { f.release(mOutputInterface); } catch (Exception ignored) { } finally { mOutputInterface = null; } //@formatter:on mCapabilities.clear(); mCurrentHost = null; mHostCapabilities.clear(); mHostIrcdVersion = null; }); // Ricezione messaggi on(AUTHENTICATE, event -> { String body = mWantedNick + "\0" + mUser + "\0" + mPass; sendRawString("AUTHENTICATE " + new String(Base64.encodeBase64(body.getBytes())) + "\r\n"); }); on(AWAY, (Event event) -> { Message message = (Message) event.data; if (message.params.size() > 0) { // TODO: User is away } else { // TODO: User is returned } }); on(CAP, event -> { Message message = (Message) event.data; String sub = message.params.get(1); if (sub.equals("ACK")) { ArrayList<String> caps = new ArrayList<>(Arrays.asList(message.params.get(2).split("\\s+"))); for (String cap : caps) { if (cap.startsWith("-")) mCapabilities.remove(cap.substring(1)); else mCapabilities.add(cap.replaceAll("[-~=]", "")); } } }); on(CAP, event -> { Message message = (Message) event.data; String sub = message.params.get(1); if (mState == State.CONNECTED) { ArrayList<String> caps = new ArrayList<>(Arrays.asList(message.params.get(2).split("\\s+"))); switch (sub) { case "ACK": if (caps.contains("sasl")) sendRawString("AUTHENTICATE PLAIN\r\n"); else cap("END"); break; case "LS": mHostCapabilities.addAll(caps); HashSet<String> wanted = new HashSet<>(mWantedCapabilities); wanted.retainAll(caps); if (TextUtils.isEmpty(mPass)) wanted.remove("sasl"); if (wanted.isEmpty()) { cap("END"); } else { StringBundler requesting = new StringBundler(); for (String s : wanted) requesting.append(s).append(" "); cap("REQ", requesting.toString()); } } } }); on(ERR_NICKNAMEINUSE, new Callback() { private int mTries; @Override public void performAction(Event event) { if (mState == State.CONNECTED && mAutoNickChange) nick(mWantedNick + (++mTries)); } }); on(JOIN, event -> { Message message = (Message) event.data; User user = mUsers.get(message.nick); if (mUser == null) user = new User(message.nick, message.user, message.host); if (mCapabilities.contains("extended-join")) { user.setAccount(message.params.get(1)); user.setReal(message.params.get(2)); } String channel = message.params.get(0); }); on(MODE, event -> { Message message = (Message) event.data; String target = message.params.get(0); String specs = message.params.get(1); // Ci interessa solo la nostra user mode if (target.equals(mNick)) { char mode; boolean value = false; for (int i = 0; i < specs.length(); i++) { mode = specs.charAt(i); //@formatter:off if (mode == '+') value = true; else if (mode == '-') value = false; else if (mode != ' ') mUserMode.put(mode, value); //@formatter:on } } }); on(NICK, event -> { Message message = (Message) event.data; if (TextUtils.compareIgnoreCase(mNick, message.nick, mCaseMapping) == 0) mNick = message.params.get(0); }); on(PING, event -> { Message message = (Message) event.data; pong(message.params.get(0)); }); on(RPL_ISUPPORT, event -> { Message message = (Message) event.data; Pattern pattern = Pattern.compile("Try server (.+), port (\\d+)"); Matcher matcher = pattern.matcher(message.params.get(1)); boolean isRplBounce = matcher.find(); if (isRplBounce) { mIsupportCompilant = false; try { if (mAutoBounce) { mHost = matcher.group(1); mPort = Integer.parseInt(matcher.group(2)); emit(new Event(BOUNCE, this)); stop(); start(); } } catch (Exception ignored) { } } else { mIsupportCompilant = true; pattern = Pattern.compile("([A-Z]+)(=(\\S+))?"); for (int i = 1; i < message.params.size(); i++) { matcher = pattern.matcher(message.params.get(i)); while (matcher.find()) { String key = matcher.group(1); String value = matcher.group(3); try { switch (key) { case "CASEMAPPING": if (value.equals("ascii")) mCaseMapping = CaseMapping.ASCII; else if (value.equals("strict-rfc1459")) mCaseMapping = CaseMapping.STRICT_RFC1459; break; case "NETWORK": mNetwork = value; break; } } catch (Exception ignored) { } } } } }); on(RPL_MYINFO, event -> { Message message = (Message) event.data; mCurrentHost = message.params.get(1); mHostIrcdVersion = message.params.get(2); String userModes = message.params.get(3); String channelModes = message.params.get(4); for (int i = 0; i < userModes.length(); i++) mUserMode.put(userModes.charAt(i), false); for (int i = 0; i < channelModes.length(); i++) mAvailChannelModes.add(channelModes.charAt(i)); mState = State.REGISTERED; emit(new Event(REGISTER, this)); }); on(RPL_SASLSUCCESS, event -> { if (mState == State.CONNECTED) cap("END"); }); on(RPL_WELCOME, (Event event) -> { Message message = (Message) event.data; mNick = message.params.get(0); }); // Stato delle conversazioni on(JOIN, event -> { Message message = (Message) event.data; String target = message.params.get(0); Conversation conversation = mConversations.get(target); if (conversation == null) { conversation = new Conversation(target, mFactory); mConversations.put(target, conversation); emit(new Event(NEW_CONVERSATION, conversation)); } conversation.addUser(message.nick); }); on(new String[] { KICK, PART }, event -> { Message message = (Message) event.data; String conversationName = message.params.get(0); String target; if (message.command.equals(PART)) target = message.nick; else target = message.params.get(1); if (TextUtils.compareIgnoreCase(target, mNick, mCaseMapping) == 0) { Conversation conversation = mConversations.remove(conversationName); emit(new Event(REMOVE_CONVERSATION, conversation)); } else { Conversation conversation = mConversations.get(conversationName); if (conversation == null) { conversation = new Conversation(conversationName, mFactory); mConversations.put(conversationName, conversation); emit(new Event(NEW_CONVERSATION, conversation)); } conversation.removeUser(target); conversation.putMessage(message); } }); on(new String[] { JOIN, NOTICE, PRIVMSG }, event -> { Message message = (Message) event.data; String target = message.params.get(0); if (message.command.equals(PRIVMSG) || message.command.equals(NOTICE)) { if (mState == State.REGISTERED) { if (TextUtils.compareIgnoreCase(target, mNick, mCaseMapping) == 0) target = message.nick; } } Conversation conversation = mConversations.get(target); if (conversation == null) { conversation = new Conversation(target, mFactory); mConversations.put(target, conversation); emit(new Event(NEW_CONVERSATION, conversation)); } conversation.putMessage(message); }); on(NICK, event -> { Message message = (Message) event.data; for (Conversation conversation : mConversations.values()) { if (conversation.contains(message.nick)) { conversation.removeUser(message.nick); conversation.addUser(message.params.get(0)); conversation.putMessage(message); } } }); on(new String[] { RPL_NAMREPLY, RPL_ENDOFNAMES }, new Callback() { StringBundler sb; @Override public void performAction(Event event) { Message message = (Message) event.data; switch (message.command) { case RPL_NAMREPLY: if (sb == null) sb = new StringBundler(); sb.append(" "); sb.append(message.params.get(3)); break; case RPL_ENDOFNAMES: String target = message.params.get(1); Conversation conversation = mConversations.get(target); if (conversation == null) { conversation = new Conversation(target, mFactory); mConversations.put(target, conversation); emit(new Event(NEW_CONVERSATION, conversation)); } conversation.clearUsers(); conversation.addUsers(Arrays.asList(sb.toString().split("\\s+"))); sb = null; } } }); on(RPL_WHOREPLY, event -> { Message message = (Message) event.data; String user = message.params.get(2); String host = message.params.get(3); String server = message.params.get(4); String nick = message.params.get(5); boolean away = message.params.get(6).equals("G"); }); on(QUIT, event -> { Message message = (Message) event.data; for (Conversation conversation : mConversations.values()) { if (conversation.contains(message.nick)) { conversation.removeUser(message.nick); conversation.putMessage(message); } } }); } public void invite(String nick, String channel) { sendLine(INVITE, nick, channel); } public boolean isIgnored(String nick) { return mIgnoreFilter.contains(nick); } public boolean isIsupportCompilant() { return mIsupportCompilant; } public boolean isSecure() { return mSecure; } public void ison(String nicks) { sendLine(ISON, nicks); } public void join(String channels) { join(channels, null); } public void join(String channels, String keys) { if (TextUtils.isEmpty(keys)) sendLine(JOIN, channels); else sendLine(JOIN, channels, keys); } public void kick(String channel, String user) { kick(channel, user, null); } public void kick(String channel, String user, String comment) { if (TextUtils.isEmpty(comment)) sendLine(KICK, channel, user); else sendLine(KICK, channel, user, comment); } public void kill(String nick, String comment) { sendLine(KILL, nick, comment); } public void links() { links(null, null); } public void links(String mask) { links(null, mask); } public void links(String remote, String mask) { if (TextUtils.isEmpty(mask)) { sendLine(LINKS); } else { if (TextUtils.isEmpty(remote)) sendLine(LINKS, mask); else sendLine(LINKS, mask, remote); } } public void list() { sendLine(LIST); } public void lusers() { lusers(null, null); } public void lusers(String mask) { lusers(mask, null); } public void lusers(String mask, String target) { if (TextUtils.isEmpty(mask)) { sendLine(LUSERS); } else { if (TextUtils.isEmpty(target)) sendLine(LUSERS, mask); else sendLine(LUSERS, mask, target); } } public void mind(String user) { mIgnoreFilter.remove(user); } public void mode(String nickOrChannel, String modeSpecs) { sendLine(MODE, nickOrChannel, modeSpecs); } public void motd() { sendLine(MOTD); } public void names(String channel) { sendLine(NAMES, channel); } public void nick(String nick) { sendLine(NICK, nick); } public void notice(String target, String text) { sendLine(true, NOTICE, target, text); } private void onDisconnection() { mState = State.CLOSED; emit(new Event(CLOSE, this)); if (mAutoReconnectTimeout >= 0) { (new Thread() { @Override public void run() { try { sleep(mAutoReconnectTimeout); start(); } catch (InterruptedException ignored) { } } }).start(); } } public void oper(String name, String password) { sendLine(OPER, name, password); } public void part(String channel) { part(channel, null); } public void part(String channel, String message) { if (TextUtils.isEmpty(message)) sendLine(PART, channel); else sendLine(PART, channel, message); } public void pass(String pass) { sendLine(PASS, pass); } public void pong(String target) { sendLine(PONG, target); } public void privmsg(String target, String text) { sendLine(true, PRIVMSG, target, text); } public void putExtra(String key, Object value) { mExtras.put(key, value); } public void quit() { sendLine(QUIT); } public void quit(String message) { sendLine(QUIT, message); } private void sendLine(String... strings) { sendLine(false, strings); } private void sendLine(boolean emitEvent, String... strings) { String line; if (strings.length == 1) { line = strings[0]; } else { StringBundler sb = new StringBundler(strings.length * 2 + 1); for (int i = 0; i < strings.length - 1; i++) { sb.append(strings[i]); sb.append(" "); } String trailing = strings[strings.length - 1]; if (TextUtils.isEmpty(trailing) || trailing.contains(":") || trailing.contains(" ")) sb.append(":"); sb.append(trailing); if (!trailing.endsWith("\r\n")) sb.append("\r\n"); line = sb.toString(); } sendRawString(line); if (emitEvent) { line = line.substring(0, line.length() - 2); Message message = Message.from(":" + mNick + "!" + mUser + "@localhost " + line); emit(new Event(message.command, message)); } } private void sendRawString(String string) { long delay; if (mState == State.CONNECTED) delay = 0; else delay = mFloodController.getDelay(string); mOutputInterface.write(string, delay); } public void servlist() { servlist(null, null); } public void servlist(String mask) { servlist(mask, null); } public void servlist(String mask, String type) { if (TextUtils.isEmpty(mask)) { sendLine(SERVLIST); } else { if (TextUtils.isEmpty(type)) sendLine(SERVLIST, mask); else sendLine(SERVLIST, mask, type); } } public void squery(String service, String text) { sendLine(SQUERY, service, text); } public void squit(String server, String comment) { sendLine(SQUIT, server, comment); } public void start() { if (mState != State.CLOSED) return; mState = State.STARTED; emit(new Event(START, this)); new Thread(() -> { try { if (mSecure) { SSLContext sslContext = SSLContext.getInstance("TLS"); String algorithm = TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(algorithm); tmFactory.init((KeyStore) null); sslContext.init(null, tmFactory.getTrustManagers(), null); SSLSocketFactory sslFactory = sslContext.getSocketFactory(); SSLSocket sslSocket = (SSLSocket) sslFactory.createSocket(mHost, mPort); sslSocket.startHandshake(); mSocket = sslSocket; } else { mSocket = new Socket(mHost, mPort); } mSocket.setSoTimeout(mSoTimeout); mInputThread = new InputThread(mSocket.getInputStream(), mEncoding, new InputThreadObserver()); mInputThread.start(); OutputInterfaceFactory outFactory = OutputInterfaceFactory.instance(); OutputStreamWriter outWriter = new OutputStreamWriter(mSocket.getOutputStream(), mEncoding); mOutputInterface = outFactory.createInterface(outWriter); mState = State.CONNECTED; emit(new Event(CONNECT, this)); cap("LS"); if (!TextUtils.isEmpty(mPass)) pass(mPass); nick(mWantedNick); user(mUser, mLoginMode, "*", mReal); } catch (Exception e) { onDisconnection(); } }).start(); } public void stats() { stats(null, null); } public void stats(String query) { stats(query, null); } public void stats(String query, String target) { if (TextUtils.isEmpty(query)) { sendLine(STATS); } else { if (TextUtils.isEmpty(target)) sendLine(STATS, query); else sendLine(STATS, query, target); } } public void stop() { if (mState == State.CLOSED) return; mState = State.CLOSED; emit(new Event(CLOSE, this)); } public void time() { time(null); } public void time(String target) { if (TextUtils.isEmpty(target)) sendLine(TIME); else sendLine(TIME, target); } public void topic(String channel) { topic(channel, null); } public void topic(String channel, String topic) { if (TextUtils.isEmpty(topic)) sendLine(TOPIC, channel); else sendLine(TOPIC, channel, topic); } public void trace() { trace(null); } public void trace(String target) { if (TextUtils.isEmpty(target)) sendLine(TRACE); else sendLine(TRACE, target); } public void user(String user, String mode, String unused, String real) { sendLine(USER, user, mode, unused, real); } public void userhost(String... nicks) { StringBundler sb = new StringBundler(); for (int i = 0; i < nicks.length - 1; i++) sb.append(nicks[i]).append(" "); sb.append(nicks[nicks.length - 1]); sendLine(USERHOST, sb.toString()); } public void users() { users(null); } public void users(String target) { if (TextUtils.isEmpty(target)) sendLine(USERS); else sendLine(USERS, target); } public void version() { version(null); } public void version(String target) { if (TextUtils.isEmpty(target)) sendLine(VERSION); else sendLine(VERSION, target); } public void voice(String channel, String nick) { mode(channel, "+v " + nick); } public void wallops(String text) { sendLine(WALLOPS, text); } public void who() { who(null, false); } public void who(String mask) { who(mask, false); } public void who(String mask, boolean operator) { if (TextUtils.isEmpty(mask)) { sendLine(WHO); } else { if (!operator) sendLine(WHO, mask); else sendLine(WHO, mask, "o"); } } public void whois(String user) { whois(null, user); } public void whois(String target, String user) { if (TextUtils.isEmpty(target)) sendLine(WHOIS, user); else sendLine(WHOIS, target, user); } public void whowas(String nick) { whowas(nick, 1, null); } public void whowas(String nick, int count) { whowas(nick, count, null); } public void whowas(String nick, int count, String target) { if (TextUtils.isEmpty(target)) sendLine(WHOWAS, nick, Integer.toString(count)); else sendLine(WHOWAS, nick, Integer.toString(count), target); } }