/* This file is part of leafdigital leafChat. leafChat 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. leafChat 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 leafChat. If not, see <http://www.gnu.org/licenses/>. Copyright 2012 Samuel Marshall. */ package com.leafdigital.ircui; import java.awt.event.MouseEvent; import java.text.SimpleDateFormat; import java.util.*; import java.util.regex.*; import org.w3c.dom.Element; import util.*; import com.leafdigital.idle.api.Idle; import com.leafdigital.irc.api.*; import com.leafdigital.ircui.api.*; import com.leafdigital.logs.api.Logger; import com.leafdigital.ui.api.*; import com.leafdigital.ui.api.TextView.ActionHandler; import leafchat.core.api.*; /** Channel window */ @UIHandler("chanwindow") public class ChanWindow extends ServerChatWindow { private String chan; private Map<String, NickInfo> nickInfo = new HashMap<String, NickInfo>(); private ListBox nameList; private ModeDisplay modes; /** Split panel bar */ public SplitPanel splitUI; /** Topic label */ public Label topicUI; private char ownStatus=0; // A-Za-z0-9- are official DNS characters. * is used on some networks to hide server details private final static Pattern SPLIT=Pattern.compile("([A-Za-z0-9*-]+(?:\\.[A-Za-z0-9*-]+){2,}) ([A-Za-z0-9*-]+(?:\\.[A-Za-z0-9*-.]+){2,})"); private int timerBold=-1; private final static int BOLDTIMER_PERIOD=30*1000; // Make names non-bold every 30 seconds private final static int BOLD_LENGTH=15*60*1000; // Once bold, names stay bold for 15 minutes private int timerWho=-1; private final static int WHOTIMER_STANDARD=180*1000; // Does /who every 3 minutes usually private final static int WHOTIMER_INCREMENT=60*1000; // But waits an extra 1 minute private final static int WHOTIMER_NAMES=20; // For every 20 names // If an error occurs during creation, this can cause problems because the // window-close callback still gets called. private boolean fullyCreated=false; private void setOwnStatus(char prefix) { ownStatus=prefix; modes.updateOpStatus(hasAtLeastPrefix('@')); updateTopicBar(); } /** * @param prefix A channel mode prefix such as @ * @return True if the current user has at least the specified channel mode prefix */ public boolean hasAtLeastPrefix(char prefix) { return getServer().isPrefixAtLeast(ownStatus,prefix); } /** * @param pc Plugin context for messages etc * @param jim Join message that we're responding to * @throws GeneralException */ public ChanWindow(PluginContext pc,JoinIRCMsg jim) throws GeneralException { super(pc, jim.getServer(), "chanwindow", false, true); this.chan=jim.getChannel(); setTitle(); nameList=(ListBox)getWindow().getWidget("names"); Server s=getServer(); modes=new ModeDisplay(pc,s,getWindow(),chan); VerticalPanel vp=(VerticalPanel)getWindow().getWidget("controls"); if(!PlatformUtils.isMac()) vp.setBorder(4); UI ui=pc.getSingle(UI.class); vp.add(ui.newJComponentWrapper(modes)); updateTopicBar(); topicUI.setAction("url",new ActionHandler() { @Override public void action(Element e, MouseEvent me) throws GeneralException { new TopicChangeDialog(); } }); // Request messages initServerInner(s,true); // And for actions too pc.requestMessages(IRCActionListMsg.class,this); // Run the join message so it gets displayed msg(jim); // Send the mode request s.sendLine(IRCMsg.constructBytes("MODE "+chan)); // Show the window getWindow().setRemember("channel",chan); getCommandEdit().setRemember("channel", chan); String extraRemember=getWindow().getExtraRemember(); if(extraRemember!=null && extraRemember.matches("[0-9]+")) { int divider=Integer.parseInt(extraRemember); splitUI.setSplitSize(divider); } ((IRCUIPlugin)getPluginContext().getPlugin()).informShown(this); getWindow().show(false); fullyCreated=true; // Set up bold timer triggerBoldTimer(); } private void triggerBoldTimer() { timerBold=TimeUtils.addTimedEvent(new Runnable() { @Override public void run() { boldTimer(); } }, BOLDTIMER_PERIOD,true); } private void boldTimer() { long now=System.currentTimeMillis(); for(Iterator<NickInfo> i=nickInfo.values().iterator();i.hasNext();) { NickInfo ni=i.next(); if(now-ni.lastMessage > BOLD_LENGTH) { nameList.setBold(ni.getNameInList(),false); } } triggerBoldTimer(); } private void triggerWhoTimer() { timerWho=TimeUtils.addTimedEvent(new Runnable() { @Override public void run() { whoTimer(); } }, WHOTIMER_STANDARD + (nameList.getItems().length / WHOTIMER_NAMES) *WHOTIMER_INCREMENT, true); } /** * Track which message our WHO is; -1 indicates we're not waiting for one, * 0 = it's the next one, etc. */ private int whoID; private void whoTimer() { if(getServer().isConnected()) { whoID=getServer().sendServerRequest(IRCMsg.constructBytes("WHO "+chan)); triggerWhoTimer(); } } private void whoReply(NumericIRCMsg m) { if(m.getParams().length>6) { // Remember user/host IRCUserAddress ua=new IRCUserAddress(m.getParamISO(5),m.getParamISO(2),m.getParamISO(3)); updateRecords(ua); // Update away status boolean away=m.getParamISO(6).startsWith("G"); NickInfo ni=nickInfo.get(ua.getNick()); if(ni!=null) nameList.setFaint(ni.getNameInList(),away); } } private int requestIDChan,requestIDQuit,requestIDNumeric,requestIDNick; @Override protected void initServerInner(Server s, boolean firstTime) { PluginContext pc=getPluginContext(); if(!firstTime) { pc.unrequestMessages(ChanIRCMsg.class,this,requestIDChan); pc.unrequestMessages(QuitIRCMsg.class,this,requestIDQuit); pc.unrequestMessages(NumericIRCMsg.class,this,requestIDNumeric); pc.unrequestMessages(NickIRCMsg.class,this,requestIDNick); if(s!=null) { modes.changeServer(s); String keyPart = ""; if(modes.hasMode('k')) { keyPart = " " + modes.getModeValue('k'); } s.sendLine(IRCMsg.constructBytes("JOIN " + chan + keyPart)); } } if(s==null) { // Clear names list nameList.clear(); nickInfo.clear(); // Forget who ID and stop requesting more if(timerWho!=-1) { TimeUtils.cancelTimedEvent(timerWho); timerWho=-1; } whoID=0; } requestIDChan=pc.requestMessages(ChanIRCMsg.class,this,new ChanAndServerFilter(s,chan),Msg.PRIORITY_NORMAL); requestIDQuit=pc.requestMessages(QuitIRCMsg.class,this,new ChanSourceFilter(),Msg.PRIORITY_NORMAL); requestIDNumeric=pc.requestMessages(NumericIRCMsg.class,this,new ServerFilter(s),Msg.PRIORITY_NORMAL); requestIDNick=pc.requestMessages(NickIRCMsg.class,this,new ChanSourceFilter(),Msg.PRIORITY_NORMAL); } @Override protected void setTitle() { getWindow().setTitle(chan+" ("+getServer().getCurrentShortName()+")"); } class ChanSourceFilter extends ServerFilter { ChanSourceFilter() { super(getServer()); } @Override public boolean accept(Msg m) { return super.accept(m) && nickInfo.containsKey(((UserSourceIRCMsg)m).getSourceUser().getNick()); } } @Override protected int getAvailableBytes() throws GeneralException { if(chan==null) return 400; // RFC limit is 512 per line including CRLF. The system will send to other users: // :nick!user@host PRIVMSG <channel> :<message><CRLF> // So there are 499 characters available for the prefix + channel + message // Channels etc. are always converted using ISO one byte per character. return 499 -chan.length() -getServer().getApproxPrefixLength(); } @Override protected void doCommand(Commands c,String line) throws GeneralException { getPluginContext().getSingle(Idle.class).userAwake( line.equals("/away") ? Idle.AWAKE_UNAWAY : Idle.AWAKE_COMMAND); c.doCommand(line,getServer(),null,chan,this,true); } @Override protected String getLogCategory() { return Logger.CATEGORY_CHAN; } @Override protected String getLogItem() { return chan; } private boolean kicked=false; @UIAction @Override public void windowClosed() throws GeneralException { if(fullyCreated) { getWindow().setExtraRemember(splitUI.getSplitSize()+""); } if(isConnected() && !kicked) { getServer().sendLine(IRCMsg.constructBytes("PART "+chan)); ((IRCUIPlugin)getPluginContext().getPlugin()).getKnownUsers().wePart(getServer(),chan); } if(timerBold!=-1) TimeUtils.cancelTimedEvent(timerBold); if(timerWho!=-1) TimeUtils.cancelTimedEvent(timerWho); super.windowClosed(); } /** * Server message: join. * @param jim Message * @throws GeneralException */ public void msg(JoinIRCMsg jim) throws GeneralException { listAddName(jim.getSourceUser().getNick(),false,jim.getSourceUser()); IRCUserAddress ua=jim.getSourceUser(); String extra=null; if(ua.getNick().equalsIgnoreCase(getServer().getOurNick())) { if(kicked) { kicked = false; commandUI.setEnabled(true); } } else { extra=((IRCUIPlugin)getPluginContext().getPlugin()).getKnownUsers().chanJoin(jim.getServer(),chan,ua); } if(jim.isHandled()) return; if(splitManager==null || !splitManager.rejoin(ua.getNick())) { addLine(EVENTSYMBOL+"<join>Joined:</join> <nick>"+esc(ua.getNick())+"</nick> ("+esc(ua.getUser())+"@"+esc(ua.getHost())+")","join"); if(extra!=null) addLine(extra); } jim.markHandled(); } /** * Server message: kick. * @param m Message * @throws GeneralException */ public void msg(KickIRCMsg m) throws GeneralException { listRemoveName(m.getVictim()); // Handle kick if it was us who got kicked if(m.getVictim().equalsIgnoreCase(getServer().getOurNick())) { ((IRCUIPlugin)getPluginContext().getPlugin()).getKnownUsers().wePart(getServer(),chan); commandUI.setEnabled(false); kicked=true; } else { ((IRCUIPlugin)getPluginContext().getPlugin()).getKnownUsers().chanKick(getServer(),chan,m.getVictim()); } if(m.isHandled()) return; addLine(EVENTSYMBOL+"<kick>Kicked:</kick> <nick>"+esc(m.getVictim())+"</nick> (by <nick>"+esc(m.getSourceUser().getNick())+ "</nick>"+esc(ifMessage(m,m.getText()))+")","kick"); m.markHandled(); } /** * Server message: part. * @param m Message * @throws GeneralException */ public void msg(PartIRCMsg m) throws GeneralException { IRCUserAddress ua=m.getSourceUser(); listRemoveName(ua.getNick()); ((IRCUIPlugin)getPluginContext().getPlugin()).getKnownUsers().chanPart(getServer(),chan,ua); if(m.isHandled()) return; addLine(EVENTSYMBOL+"<part>Left:</part> <nick>"+esc(ua.getNick())+"</nick> ("+esc(ua.getUser())+"@"+esc(ua.getHost())+")"+ esc(ifMessage(m,m.getText())),"part"); m.markHandled(); } /** * Server message: normal message. * @param m Message * @throws GeneralException */ public void msg(ChanMessageIRCMsg m) throws GeneralException { updateRecords(m.getSourceUser()); ((IRCUIPlugin)getPluginContext().getPlugin()).getKnownUsers().chanText(getServer(),chan,m.getSourceUser()); listUpdateRecent(m.getSourceUser().getNick()); if(m.isHandled()) return; IRCUserAddress ua=m.getSourceUser(); if(splitManager!=null) splitManager.forceRejoinDisplay(ua.getNick()); addLine("<<nick>"+esc(ua.getNick())+"</nick>> "+esc(m.convertEncoding(m.getText())),"msg"); reportActualMessage(chan,"<"+ua.getNick()+"> "+m.convertEncoding(m.getText())); m.markHandled(); } /** * Server message: action. * @param m Message * @throws GeneralException */ public void msg(ChanActionIRCMsg m) throws GeneralException { updateRecords(m.getSourceUser()); ((IRCUIPlugin)getPluginContext().getPlugin()).getKnownUsers().chanText(getServer(),chan,m.getSourceUser()); listUpdateRecent(m.getSourceUser().getNick()); if(m.isHandled()) return; IRCUserAddress ua=m.getSourceUser(); if(splitManager!=null) splitManager.forceRejoinDisplay(ua.getNick()); addLine(ACTIONSYMBOL+"<nick>"+esc(ua.getNick())+"</nick> "+esc(m.convertEncoding(m.getText())),"action"); reportActualMessage(chan,ACTIONSYMBOL+ua.getNick()+" "+m.convertEncoding(m.getText())); m.markHandled(); } /** * Server message: CTCP request. * @param m Message * @throws GeneralException */ public void msg(ChanCTCPRequestIRCMsg m) throws GeneralException { updateRecords(m.getSourceUser()); if(m.isHandled()) return; IRCUserAddress ua=m.getSourceUser(); addLine("[<nick>"+esc(ua.getNick())+"</nick> CTCP <ctcp>"+esc(m.getRequest())+"</ctcp>] "+esc(m.convertEncoding(m.getText())),"ctcp"); m.markHandled(); } /** * Server message: channel mode. * @param m Message * @throws GeneralException */ public void msg(ChanModeIRCMsg m) throws GeneralException { updateRecords(m.getSourceUser()); if(m.getSourceUser()==null) { // Actually numeric RPL_CHANNELMODEIS, so clear existing modes modes.clearModes(); } // Handle internal mode storage ChanModeIRCMsg.ModeChange[] changes=m.getChanges(); Server.StatusPrefix[] statusPrefixes=getServer().getPrefix(); for(int change=0;change<changes.length;change++) { char mode=changes[change].getMode(); int type=getServer().getChanModeType(mode); switch(type) { case Server.CHANMODE_USERSTATUS: // Handle status changes for(int prefix=0;prefix<statusPrefixes.length;prefix++) { if(mode==statusPrefixes[prefix].getMode()) { changeMode(changes[change].getParam(),statusPrefixes[prefix].getPrefix(),changes[change].isPositive()); break; } } break; case Server.CHANMODE_ALWAYSPARAM: case Server.CHANMODE_SETPARAM: if(changes[change].isPositive()) modes.addMode(mode,changes[change].getParam()); else modes.removeMode(mode); break; case Server.CHANMODE_NOPARAM: if(changes[change].isPositive()) modes.addMode(mode,null); else modes.removeMode(mode); break; default: break; } } updateTopicBar(); // In case +t changed if(m.isHandled()) return; if(m.getSourceUser()!=null) { IRCUserAddress ua=m.getSourceUser(); StringBuffer sbModes=new StringBuffer(); sbModes.append(m.getModes()); for(int i=0;i<m.getModeParams().length;i++) sbModes.append(" "+m.getModeParams()[i]); addLine(EVENTSYMBOL+"<nick>"+esc(ua.getNick())+"</nick> <mode>changed mode:</mode> "+esc(sbModes.toString()),"mode"); } m.markHandled(); } /** * Server message: channel notice. * @param m Message * @throws GeneralException */ public void msg(ChanNoticeIRCMsg m) throws GeneralException { updateRecords(m.getSourceUser()); ((IRCUIPlugin)getPluginContext().getPlugin()).getKnownUsers().chanText(getServer(),chan,m.getSourceUser()); listUpdateRecent(m.getSourceUser().getNick()); if(m.isHandled()) return; IRCUserAddress ua=m.getSourceUser(); addLine("["+(m.getStatus()==0?"":""+esc(""+m.getStatus()))+ esc(m.getChannel())+"] -<nick>"+esc(ua.getNick())+"</nick>- "+esc(m.convertEncoding(m.getText())),"notice"); reportActualMessage(chan, "["+(m.getStatus()==0?"":""+m.getStatus())+ m.getChannel()+"] -"+ua.getNick()+"- "+m.convertEncoding(m.getText())); m.markHandled(); } /** Split manager. When this is non-null, certain information needs to be fed to it */ private SplitManager splitManager=null; /** Information about a particular split between two servers */ private static class SplitInfo { long splitAt; Set<String> splitNotDisplayed = new TreeSet<String>(); Set<String> splitDisplayed = new HashSet<String>(); Set<String> rejoinNotDisplayed = new TreeSet<String>(); } /** Split manager handles display of split events */ private class SplitManager implements Runnable { /** Map of server,server -> SplitInfo */ HashMap<String, SplitInfo> servers = new HashMap<String, SplitInfo>(); SplitManager() { // Update every second TimeUtils.addTimedEvent(this,1000,true); } /** * Called whenever somebody is split. * @param server1 Server * @param server2 Other server * @param nick Nickname */ void add(String server1,String server2,String nick) { String key=server1+","+server2; SplitInfo info=servers.get(key); if(info==null) { addLine(EVENTSYMBOL+"Network split between <key>"+server1+"</key> and <key>"+server2+"</key>","split"); info=new SplitInfo(); info.splitAt=System.currentTimeMillis(); servers.put(key,info); } info.splitNotDisplayed.add(nick); } /** * Called whenever somebody joins if the split manager is active. * Allows for lists of joiners to be grouped rather than displayed as * normal joins. * @param nick Nick in question * @return True if this a rejoin and should not be displayed */ boolean rejoin(String nick) { // Is this rejoining person a splitee? boolean splitee=false; SplitInfo info=null; for(Iterator<SplitInfo> i=servers.values().iterator();i.hasNext();) { info=i.next(); if(info.splitDisplayed.contains(nick) || info.splitNotDisplayed.contains(nick)) { splitee=true; break; } } if(!splitee) return false; if(info.splitNotDisplayed.contains(nick)) { // Better do display now so that we show they've split and the rejoin // makes sense doDisplay(true); } info.splitDisplayed.remove(nick); info.rejoinNotDisplayed.add(nick); return true; } /** * Called whenever somebody says something if the split manager is active. * Basically ensures that the 'rejoin' display with their name appears before * anything they might say. * @param nick Nick in question */ void forceRejoinDisplay(String nick) { // Is this rejoining person a splitee? SplitInfo info=null; for(Iterator<SplitInfo> i=servers.values().iterator();i.hasNext();) { info=i.next(); if(info.rejoinNotDisplayed.contains(nick)) { doDisplay(false); return; } } } void doDisplay(boolean splitOnly) { for(SplitInfo info : servers.values()) { if(!info.splitNotDisplayed.isEmpty()) { StringBuffer sb=new StringBuffer(EVENTSYMBOL+"<part>Split:</part>"); for(String nick : info.splitNotDisplayed) { info.splitDisplayed.add(nick); sb.append(" <nick>"+esc(nick)+"</nick>"); } info.splitNotDisplayed.clear(); addLine(sb.toString(),"split"); } if(!splitOnly && !info.rejoinNotDisplayed.isEmpty()) { StringBuffer sb=new StringBuffer(EVENTSYMBOL+"<join>Rejoined:</join>"); for(String nick : info.rejoinNotDisplayed) { sb.append(" <nick>"+esc(nick)+"</nick>"); } info.rejoinNotDisplayed.clear(); addLine(sb.toString(),"split"); } } } @Override public void run() { // Got anything new to display? doDisplay(false); // Check if we should continue checking long now=System.currentTimeMillis(); boolean keepGoing=false; for(SplitInfo info : servers.values()) { if( // If there are any splitters left and the split happened less than // 10 minutes ago, continue (info.splitDisplayed.size()>1 && now-info.splitAt<10*60*1000) || // If the split happened less than 30 seconds ago, continue now-info.splitAt<30*1000) { keepGoing=true; break; } } if(keepGoing) TimeUtils.addTimedEvent(this,1000,true); else splitManager=null; } } @Override public void msg(QuitIRCMsg msg) throws GeneralException { listRemoveName(msg.getSourceUser().getNick()); ((IRCUIPlugin)getPluginContext().getPlugin()).getKnownUsers().chanQuit(getServer(),chan,msg.getSourceUser()); // Split detect byte[] messageBytes = msg.getMessage(); String message; if(messageBytes != null) { message = msg.convertEncoding(messageBytes); } else { message = ""; } Matcher m=SPLIT.matcher(message); if(m.matches()) { if(splitManager==null) { splitManager=new SplitManager(); } splitManager.add(m.group(1),m.group(2),msg.getSourceUser().getNick()); msg.markHandled(); } else { // Display quit super.msg(msg); } } @Override public void msg(NickIRCMsg m) throws GeneralException { listChangeName(m.getSourceUser().getNick(),m.getNewNick()); ((IRCUIPlugin)getPluginContext().getPlugin()).getKnownUsers().chanNick(getServer(),chan,m.getSourceUser(),m.getNewNick()); updateRecords(m.getSourceUser()); super.msg(m); } /** * Server message: numeric. * @param m Message * @throws GeneralException */ public void msg(NumericIRCMsg m) throws GeneralException { switch(m.getNumeric()) { case NumericIRCMsg.RPL_NAMREPLY: // :dream.esper.net 353 quentesting1 = #quentesting :@quentesting1 if(m.getParams().length<4 || !m.getParamISO(2).equalsIgnoreCase(chan)) return; String[] asNames=m.getParamISO(3).split(" "); for(int i=0;i<asNames.length;i++) { String sName=asNames[i]; listAddName(sName,true,null); } m.markHandled(); // Start doing /who if(timerWho==-1) whoTimer(); break; case NumericIRCMsg.RPL_ENDOFNAMES: // :dream.esper.net 366 quentesting1 #quentesting :End of /NAMES list. if(m.getParams().length<2 || !m.getParamISO(1).equalsIgnoreCase(chan)) return; m.markHandled(); break; case NumericIRCMsg.RPL_TOPIC: // :discworld.esper.net 332 quentesting2 #quentesting :My happy fun topic if(m.getParams().length<3 || !m.getParamISO(1).equalsIgnoreCase(chan)) return; setTopic(m.getParams()[2],null,m); m.markHandled(); break; case NumericIRCMsg.RPL_TOPICWHOTIME: // :iuturna.sorcery.net 333 quen #quentesting quen 1137247214 if(m.getParams().length<4 || !m.getParamISO(1).equalsIgnoreCase(chan)) return; int iUnixTime; try { iUnixTime=Integer.parseInt(m.getParamISO(3)); } catch(NumberFormatException nfe) { // Ignore, we'll display as an unrecognised server message. return; } // The nick is sometimes a full address IRCUserAddress ua=new IRCUserAddress(m.getParamISO(2),false); addLine(EVENTSYMBOL+"<topic>Set by</topic> <nick>"+esc(ua.getNick())+"</nick> at "+ formatTime(iUnixTime),"topictime"); m.markHandled(); break; case NumericIRCMsg.RPL_CHANNELMODEIS: // :naylean.draconic.com 324 quentesting1 #quentesting +pntcl 30 if(m.getParams().length<2 || !m.getParamISO(1).equalsIgnoreCase(chan)) return; ChanModeIRCMsg cmim=(ChanModeIRCMsg)m.getSimilar(); if(cmim!=null) { msg(cmim); m.markHandled(); } break; case NumericIRCMsg.RPL_CREATIONTIME: // :naylean.draconic.com 329 quentesting1 #quentesting 1155726202 if(m.getParams().length<3 || !m.getParamISO(1).equalsIgnoreCase(chan)) return; try { iUnixTime=Integer.parseInt(m.getParamISO(2)); } catch(NumberFormatException nfe) { // Ignore, we'll display as an unrecognised server message. return; } addLine(EVENTSYMBOL+"<chaninfo>Channel opened</chaninfo> at "+ formatTime(iUnixTime),"chanopened"); m.markHandled(); break; case NumericIRCMsg.RPL_ENDOFWHO: if(m.getResponseID()==whoID) m.markHandled(); break; case NumericIRCMsg.RPL_WHOREPLY: if(m.getResponseID()==whoID) { whoReply(m); m.markHandled(); } break; case NumericIRCMsg.RPL_NOWAWAY: { NickInfo ni=nickInfo.get(getServer().getOurNick()); if(ni!=null) nameList.setFaint(ni.getNameInList(),true); break; } case NumericIRCMsg.RPL_UNAWAY: { NickInfo ni=nickInfo.get(getServer().getOurNick()); if(ni!=null) nameList.setFaint(ni.getNameInList(),false); break; } } } /** * Server message: topic. * @param m Message * @throws GeneralException */ public void msg(TopicIRCMsg m) throws GeneralException { setTopic(m.getTopic(),m.getSourceUser(),m); m.markHandled(); } private static String formatTime(int iUnixTime) { Calendar cNow=Calendar.getInstance(),cThen=Calendar.getInstance(); cThen.setTimeInMillis(iUnixTime * 1000L); SimpleDateFormat sdf; if(cNow.get(Calendar.DAY_OF_YEAR) == cThen.get(Calendar.DAY_OF_YEAR) && cNow.get(Calendar.YEAR) == cThen.get(Calendar.YEAR) ) { // Today sdf=new SimpleDateFormat("HH:mm 'today'"); } else { // Check up to a week back (but not hitting this same day name) cNow.set(Calendar.HOUR,0); cNow.set(Calendar.MINUTE,0); cNow.set(Calendar.SECOND,0); cNow.set(Calendar.MILLISECOND,0); cNow.add(Calendar.DATE,-6); if(!cThen.before(cNow)) { // This week sdf=new SimpleDateFormat("HH:mm 'on' EEEE"); } else { // Use full date format sdf=new SimpleDateFormat( "HH:mm 'on' EEEE dd MMMM yyyy"); } } return sdf.format(cThen.getTime()); } /** * @param sName Name to add to list * @param bMayHavePrefix If name might be prefixed * @param ua Full user address or null if not known * @throws BugException Error in xml */ private void listAddName(String sName,boolean bMayHavePrefix,IRCUserAddress ua) { if(sName.length()==0) return; // Parse off prefix to build data entry and add to set NickInfo ni=new NickInfo(); ni.sName=sName; ni.ua=ua; if(bMayHavePrefix) { char cMaybePrefix=sName.charAt(0); Server.StatusPrefix[] asp=getServer().getPrefix(); for(int iPrefix=0;iPrefix<asp.length;iPrefix++) { if(asp[iPrefix].getPrefix() == cMaybePrefix) { ni.cPrefix=cMaybePrefix; ni.sName=sName.substring(1); break; } } } // See if name's already present NickInfo niOld=nickInfo.get(ni.sName); if(niOld!=null) nameList.removeItem(niOld.getNameInList()); // Put into map (replacing old one if there was one) nickInfo.put(ni.sName,ni); if(ni.sName.equals(getServer().getOurNick())) setOwnStatus(ni.cPrefix); // Add to listbox nameList.addItem(ni.getNameInList()); } /** Information stored about a person in namelist */ private static class NickInfo { String sName; IRCUserAddress ua; // May be null if not known yet char cPrefix=0; long lastMessage=0; String getNameInList() { return (cPrefix!=0 ? cPrefix+sName : sName); } } /** * @param name Name to remove from list * @throws BugException Error in xml */ private void listRemoveName(String name) { // Find in map NickInfo ni=nickInfo.get(name); if(ni==null) { getPluginContext().log("Warning: attempt to remove absent nick from channel "+chan+": "+name); return; } // Remove from listbox nameList.removeItem(ni.getNameInList()); // Remove from set nickInfo.remove(name); } private void listChangeName(String before,String after) { // Find in map NickInfo ni=nickInfo.get(before); if(ni==null) throw new BugException("Nick not found: "+before); // Remove from listbox & set nameList.removeItem(ni.getNameInList()); nickInfo.remove(before); // Add back to set & listbox ni.sName=after; nickInfo.put(after,ni); nameList.addItem(ni.getNameInList()); if(System.currentTimeMillis() - ni.lastMessage < BOLD_LENGTH) nameList.setBold(ni.getNameInList(),true); } private void listUpdateRecent(String name) { // Find in map NickInfo ni=nickInfo.get(name); if(ni==null) return; // Ignore missing nicks // Mark bold in list ni.lastMessage=System.currentTimeMillis(); nameList.setBold(ni.getNameInList(),true); } private void changeMode(String nick,char prefix,boolean on) { // Find in map NickInfo ni=nickInfo.get(nick); if(ni==null) { getPluginContext().log("Warning: attempt to change mode for missing nick "+chan+": "+nick); return; } // Remove from listbox nameList.removeItem(ni.getNameInList()); // Change mode if(on) ni.cPrefix=prefix; else if(ni.cPrefix==prefix) ni.cPrefix=0; if(ni.sName.equals(getServer().getOurNick())) setOwnStatus(ni.cPrefix); // Add back to list nameList.addItem(ni.getNameInList()); } private String currentTopic=""; private void setTopic(byte[] topicBytes,IRCUserAddress setter,IRCMsg encodingReference) throws GeneralException { String topic=encodingReference.convertEncoding(topicBytes); if(setter!=null) addLine(EVENTSYMBOL+"<topic>Topic set</topic> by <nick>"+setter.getNick()+"</nick>: "+esc(topic),"topicset"); else addLine(EVENTSYMBOL+"<topic>Topic</topic>: "+esc(topic),"topic"); currentTopic=topic; updateTopicBar(); } private void updateTopicBar() { String text; if(currentTopic.length()==0) { text="No topic."; } else { text="<strong>Topic</strong>: "+processColours(esc(currentTopic)); } topicUI.setText(text+ ((hasAtLeastPrefix('@') || !modes.hasMode('t'))?" (<url>change</url>)" : "")); } /** * Dialog used to change the topic. */ @UIHandler("changetopic") public class TopicChangeDialog { private Dialog d; /** Edit box for topic. */ public EditBox topicUI; /** Button to change. */ public Button changeUI; TopicChangeDialog() throws GeneralException { UI ui=getPluginContext().getSingle(UI.class); d=ui.createDialog("changetopic", this); topicUI.setValue(currentTopic); changeValue(); d.show(getWindow()); } /** Callback: Cancel button. */ @UIAction public void actionCancel() { d.close(); } /** Callback: Text change in edit box. */ @UIAction public void changeValue() { // Get encoding IRCEncoding encoding=getPluginContext().getSingle(IRCEncoding.class); int bytes=encoding.getEncoding(getServer(),chan,null).convertOutgoing( topicUI.getValue()).length; changeUI.setEnabled(bytes<=getServer().getMaxTopicLength()); } /** * Callback: Change button. * @throws GeneralException */ @UIAction public void actionChange() throws GeneralException { doCommand(getPluginContext().getSingle(Commands.class), "/topic "+chan+" "+topicUI.getValue()); d.close(); } } /** * Server message: general channel message (not used). * @param m Message */ public void msg(ChanIRCMsg m) { // Never called, but required because we requested the generic type rather than // making 6 individual requests. This is kind of OK technologically because // maybe we add another type later. } @Override protected boolean isUs(String sTarget) { return sTarget.equalsIgnoreCase(chan); } @Override public void fillTabCompletionList(TabCompletionList options) { options.add(chan,false); for(String name : nickInfo.keySet()) { options.add(name,true); } } @Override protected boolean displayTimeStamps() { return true; } @Override protected String getContextChannel() { return chan; } /** * Obtains menu actions for the popup menu on channel names list. * @param pm Menu to add actions to */ @UIAction public void menuNames(PopupMenu pm) { // Get selected nicks String[] selectedNicks=nameList.getMultiSelected(); Server.StatusPrefix[] asp=getServer().getPrefix(); for(int i=0;i<selectedNicks.length;i++) { // Get rid of prefix if any match for(int iPrefix=0;iPrefix<asp.length;iPrefix++) { if(asp[iPrefix].getPrefix() == selectedNicks[i].charAt(0)) { selectedNicks[i]=selectedNicks[i].substring(1); break; // No point checking other prefixes } } } // Add in actions on them from across plugin(s) IRCActionListMsg context=new IRCActionListMsg(getServer(), chan,null, null,selectedNicks); ((IRCUIPlugin)getPluginContext().getPlugin()).getActionListOwner().fillMenu( context,pm); } /** * Called when we know for sure a user's address, so that we can add it to * their record if needed. * @param ua Address */ private void updateRecords(IRCUserAddress ua) { if(ua==null) return; NickInfo info=nickInfo.get(ua.getNick()); if(info==null) return; if(info.ua==null) info.ua=ua; } /** * Adds default channel actions for menus * @param m Message that will receive data */ public void msg(IRCActionListMsg m) { if(!chan.equalsIgnoreCase(m.getContextChannel())) return; if(m.hasSelectedNicks() && hasAtLeastPrefix('@')) { String[] selected=m.getSelectedNicks(); boolean us = !m.notUs(); String name; if (selected.length == 1) { name = us ? "yourself" : selected[0]; } else { if(us) { name = "yourself & " + (selected.length-1) + " other" + (selected.length > 2 ? "s" : ""); } else { name = selected.length + " people"; } } m.addIRCAction(new NickAction(getPluginContext(), "Kick "+name+" out of "+chan,IRCAction.CATEGORY_USERCHAN,200, "/kick "+chan+" %%NICK%%")); m.addIRCAction(new NickAction(getPluginContext(), "Ban " + name + " from "+chan, IRCAction.CATEGORY_USERCHAN, 300, "/ban " + chan + " %%NICK%%")); // Do any of them already have ops? boolean anyOps=false,anyVoice=false,allOps=true,allVoice=true,fail=false; for(int i=0;i<selected.length;i++) { NickInfo info=nickInfo.get(selected[i]); if(info==null) { fail=true; break; } if(getServer().isPrefixAtLeast(info.cPrefix,'@')) anyOps=true; else allOps=false; if(getServer().isPrefixAtLeast(info.cPrefix,'+')) anyVoice=true; else allVoice=false; } if(!fail) { if(!anyOps) m.addIRCAction(new ModeAction( "Give "+name+" ops (@) on "+chan,100,true,'o')); if(!anyVoice) m.addIRCAction(new ModeAction( "Give "+name+" voice (+) on "+chan,110,true,'v')); if(allOps) m.addIRCAction(new ModeAction( "Remove ops (@) from "+name+" on "+chan,200,false,'o')); if(allVoice) m.addIRCAction(new ModeAction( "Remove voice (+) from "+name+" on "+chan,210,false,'v')); } } } class ModeAction extends AbstractIRCAction { private boolean plus; private char letter; /** * @param name Menu name * @param order Order within category * @param plus True for +mode, false for -mode * @param letter Letter being added/removed */ public ModeAction(String name,int order,boolean plus,char letter) { super(name,IRCAction.CATEGORY_USERCHAN,order); this.plus=plus; this.letter=letter; } @Override public void run(Server s,String contextChannel,String contextNick, String selectedChannel,String[] selectedNicks,MessageDisplay caller) { // Find int numModes=s.getMaxModeParams(); Commands c=getPluginContext().getSingle(Commands.class); LinkedList<String> l = new LinkedList<String>(Arrays.asList(selectedNicks)); while(!l.isEmpty()) { String letters="",names=""; for(int i=0;i<numModes && !l.isEmpty();i++) { letters+=letter; if(names.length()!=0) { names+=" "; } names += l.removeFirst(); } c.doCommand( "/mode "+chan+" "+(plus?"+" : "-")+letters+" "+names, s,contextNick==null?null:new IRCUserAddress(contextNick,false),contextChannel,caller,false); } } } @Override public void showOwnText(int type,String target,String text) { if(type==MessageDisplay.TYPE_MSG || type==MessageDisplay.TYPE_ACTION) listUpdateRecent(getOwnNick()); super.showOwnText(type,target,text); } }