/*
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 2011 Samuel Marshall.
*/
package com.leafdigital.ircui;
import java.text.SimpleDateFormat;
import java.util.*;
import util.StringUtils;
import util.xml.XML;
import com.leafdigital.irc.api.*;
import leafchat.core.api.*;
/**
* Class tracks a list of known users so that we can display information about
* other channels they're in, etc.
*/
public class KnownUsers
{
private final static long EXPIRY = 12*60*60*1000; // 12 hours
/** Information about a particular user in a particular channel */
private class UserChannel
{
/**
* Nicks that this user currently is operating under in the channel. Note
* that it's possible the same user/host combination has multiple nicks.
*/
HashSet<String> currentNicks = new HashSet<String>();
/**
* Previously-used nicks (map of nick -> Long of time when the nick ceased
* being current)
*/
HashMap<String, Long> pastNicks = new HashMap<String, Long>();
}
/** Map of information about users */
private HashMap<Server, Map<String, Map<String, UserChannel>>> users =
new HashMap<Server, Map<String, Map<String, UserChannel>>>();
KnownUsers(PluginContext context)
{
context.requestMessages(ServerDisconnectedMsg.class,this,Msg.PRIORITY_EARLY);
context.requestMessages(MinuteMsg.class,this,new MinuteFilter(60));
}
private Map<String, UserChannel> findUser(Server s,IRCUserAddress ua)
{
Map<String, Map<String, UserChannel>> server = users.get(s);
if(server==null)
{
server = new HashMap<String, Map<String, UserChannel>>();
users.put(s,server);
}
String key = ua.getUser() + "@" + ua.getHost();
Map<String, UserChannel> user = server.get(key);
if(user==null)
{
user = new HashMap<String, UserChannel>();
server.put(key,user);
}
return user;
}
/**
* Should be called when somebody (else) joins a channel.
* @param s Server
* @param chan Channel
* @param ua Joiner
* @return Either null or a string to add with extra information
*/
synchronized String chanJoin(Server s,String chan,IRCUserAddress ua)
{
String info;
Map<String, UserChannel> user = findUser(s, ua);
UserChannel c=user.get(chan);
if(c==null)
{
// Weren't in this channel recently. Are they in any other current ones?
LinkedList<String> current = new LinkedList<String>(),
previous = new LinkedList<String>();
for(Map.Entry<String, UserChannel> entry : user.entrySet())
{
String otherChan = entry.getKey();
UserChannel otherChanDetails = entry.getValue();
if(otherChanDetails.currentNicks.isEmpty())
{
if(otherChanDetails.pastNicks.containsKey(ua.getNick()))
{
// Previously in other channel with same nick
previous.add(XML.esc(otherChan));
}
else
{
// Previously in other channel with different nick
previous.add(XML.esc(otherChan) + " ("
+ getPastNicks(otherChanDetails.pastNicks, ua.getNick()) + ")");
}
}
else
{
if(otherChanDetails.currentNicks.contains(ua.getNick()))
{
// Currently in other channel with same nick
current.add(XML.esc(otherChan));
}
else
{
// Currently in other channel with different nick
current.add(XML.esc(otherChan)+" (as <nick>"
+ XML.esc(otherChanDetails.currentNicks.iterator().next())
+ "</nick>)");
}
}
}
if(!current.isEmpty()) // Show current channels if available
{
info="<knownuser><nick>"+XML.esc(ua.getNick())+"</nick> is also in "+
StringUtils.formatList(current.toArray(new String[0]))+"</knownuser>";
}
else if(!previous.isEmpty()) // Otherwise show previous channels
{
info="<knownuser><nick>"+XML.esc(ua.getNick())+"</nick> was previously in "+
StringUtils.formatList(previous.toArray(new String[0]))+"</knownuser>";
}
else // No information about them at all
info=null;
c=new UserChannel();
user.put(chan,c);
}
else
{
// OK, they've been here before. Are they still here?
if(!c.currentNicks.isEmpty())
{
String[] list=new String[c.currentNicks.size()];
int count=0;
for(String nick : c.currentNicks)
{
list[count++]="<nick>"+XML.esc(nick)+"</nick>";
}
info="<knownuser><nick>"+XML.esc(ua.getNick())+"</nick> is also here as "+
StringUtils.formatList(list)+"</knownuser>";
}
else
{
// Not here now, let's find the most recent
Map.Entry<String, Long> me = getMostRecentNick(c.pastNicks);
String recentNick = me.getKey();
long recentTime = me.getValue().longValue();
info="<knownuser><nick>"+XML.esc(ua.getNick())+"</nick> was previously here at <key>"+
(new SimpleDateFormat("HH:mm")).format(new Date(recentTime))+"</key>";
if(!recentNick.equals(ua.getNick()))
{
info += getPastNicks(c.pastNicks, ua.getNick());
}
info+="</knownuser>";
}
}
c.currentNicks.add(ua.getNick());
return info;
}
private static String getPastNicks(Map<String, Long> pastNicks,
String currentNick)
{
// Prepare set sorted by time
TreeSet<Map.Entry<String, Long>> entries =
new TreeSet<Map.Entry<String, Long>>(
new Comparator<Map.Entry<String, Long>>()
{
@Override
public int compare(Map.Entry<String, Long> arg0,
Map.Entry<String, Long> arg1)
{
long
time0 = arg0.getValue().longValue(),
time1 = arg1.getValue().longValue();
return (int)(time1-time0);
}
});
// Add all entries except those with same nick as current to the set
for(Map.Entry<String, Long> me : pastNicks.entrySet())
{
if(!me.getKey().toString().equalsIgnoreCase(currentNick))
{
entries.add(me);
}
}
if(entries.isEmpty())
{
return "";
}
String[] nicks = new String[entries.size()];
int count = 0;
for(Map.Entry<String, Long> me : entries)
{
nicks[count] = "<nick>" + XML.esc(me.getKey()) + "</nick>";
}
return " as " + StringUtils.formatList(nicks, 5);
}
private static Map.Entry<String, Long> getMostRecentNick(
Map<String, Long> pastNicks)
{
Map.Entry<String, Long> best=null;
for(Map.Entry<String, Long> me : pastNicks.entrySet())
{
long entryTime=me.getValue().longValue();
if(best==null || entryTime>best.getValue().longValue())
{
best = me;
}
}
return best;
}
/**
* Called to inform about text in a channel.
* @param s Server
* @param chan Channel
* @param ua User
*/
public synchronized void chanText(Server s,String chan,IRCUserAddress ua)
{
Map<String, UserChannel> user = findUser(s,ua);
UserChannel c = user.get(chan);
if(c==null)
{
c = new UserChannel();
user.put(chan,c);
}
c.currentNicks.add(ua.getNick());
}
/**
* Called to inform about a kick in a channel.
* @param s Server
* @param chan Channel
* @param nick User
*/
public synchronized void chanKick(Server s,String chan,String nick)
{
// Find user
Map<String, Map<String, UserChannel>> server = users.get(s);
if(server==null) return;
for(Map.Entry<String, Map<String, UserChannel>> entry : server.entrySet())
{
Map<String, UserChannel> user = entry.getValue();
UserChannel c=user.get(chan);
if(c!=null && c.currentNicks.contains(nick))
{
// Do the same as for part
c.pastNicks.put(nick,new Long(System.currentTimeMillis()));
c.currentNicks.remove(nick);
return;
}
}
}
/**
* Called to inform about a part in a channel.
* @param s Server
* @param chan Channel
* @param ua User
*/
public synchronized void chanPart(Server s,String chan,IRCUserAddress ua)
{
Map<String, UserChannel> user = findUser(s,ua);
UserChannel c=user.get(chan);
if(c==null)
{
c = new UserChannel();
user.put(chan,c);
}
c.pastNicks.put(ua.getNick(),new Long(System.currentTimeMillis()));
c.currentNicks.remove(ua.getNick());
}
/**
* Called to inform about a quit in a channel.
* @param s Server
* @param chan Channel
* @param ua User
*/
public synchronized void chanQuit(Server s,String chan,IRCUserAddress ua)
{
// Actually does the same as part
chanPart(s,chan,ua);
}
/**
* Called to inform about nick change in a channel.
* @param s Server
* @param chan Channel
* @param ua User
* @param newNick New nickname
*/
public synchronized void chanNick(Server s,String chan,IRCUserAddress ua,String newNick)
{
Map<String, UserChannel> user = findUser(s,ua);
UserChannel c=user.get(chan);
if(c==null)
{
c = new UserChannel();
user.put(chan, c);
}
c.pastNicks.put(ua.getNick(),new Long(System.currentTimeMillis()));
c.currentNicks.remove(ua.getNick());
c.currentNicks.add(newNick);
}
/**
* Must be called when user parts from a channel or is kicked from it, etc.
* @param s Server
* @param chan Channel
*/
synchronized void wePart(Server s, String chan)
{
Map<String, Map<String, UserChannel>> server = users.get(s);
if(server==null) return;
for(Map<String, UserChannel> user : server.values())
{
user.remove(chan);
}
}
/**
* Message: Server is disconnected.
* @param msg
*/
synchronized public void msg(ServerDisconnectedMsg msg)
{
users.remove(msg.getServer());
}
/**
* Message: periodic housekeeping per-minute call.
* @param msg
*/
synchronized public void msg(MinuteMsg msg)
{
long now=System.currentTimeMillis();
for(Iterator<Map<String, Map<String, UserChannel>>> i =
users.values().iterator(); i.hasNext();)
{
Map<String, Map<String, UserChannel>> server = i.next();
for(Iterator<Map<String, UserChannel>> j =
server.values().iterator(); j.hasNext();)
{
Map<String, UserChannel> user = j.next();
for(Iterator<UserChannel> k = user.values().iterator(); k.hasNext();)
{
UserChannel uc =k.next();
if(!uc.currentNicks.isEmpty())
{
continue;
}
for(Iterator<Long> l = uc.pastNicks.values().iterator(); l.hasNext();)
{
long time= l.next().longValue();
if(now-time > EXPIRY)
{
l.remove();
}
}
if(uc.pastNicks.isEmpty())
{
k.remove();
}
}
if(user.isEmpty())
{
j.remove();
}
}
if(server.isEmpty())
{
i.remove();
}
}
}
}