package net.sourceforge.scrumbot;
import org.jibble.pircbot.*;
import java.util.Collection;
import java.util.List;
import java.util.LinkedList;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Calendar;
public class ScrumBot extends PircBot implements Runnable {
public enum State { IDLE, WAITING_ATTENDEES, WAITING_REPORTS };
private State state = State.IDLE;
private Calendar previousStateChange = Calendar.getInstance();
// list of members that were active in the previous scrum
private List<Member> previousMembers = new LinkedList();
// list of members that are supposed to say something in this scrum
private List<Member> scrumMembers = new LinkedList();
// map of nick -> Member
private Map<String, Member> members = new HashMap();
private Member activeMember;
// TODO: get rid of this and actually have a list of Channel -objects in a map with all of the private members
// of this object so that all channels can have their own scrums.
private String channel;
public ScrumBot() {
this.setName("ScrumBot");
new Thread(this).start();
}
public void onJoin(String channel, String sender, String login, String hostname)
{
if( !sender.equals( getName() ) )
{
//sendMessage( channel, "Hi, " + sender + "! My name is " + getName() + ", and I'm the scrum bot here!" );
op( channel, sender );
}
}
public void onNickChange(String oldNick, String login, String hostname, String newNick)
{
// TODO
}
public void onMessage(String channel, String sender, String login, String hostname, String msg)
{
this.channel = channel;
Member m = getMember(channel, sender, login, hostname);
String message = msg.toLowerCase();
if( message.startsWith( getName().toLowerCase() + ": start" ) )
{
startScrum(channel);
}
if( message.startsWith( getName().toLowerCase() + ": next" ) )
{
if( state != State.WAITING_REPORTS ) startReporting(channel);
else nextMember(channel);
}
if( message.startsWith( getName().toLowerCase() + ": reset" ) )
{
if( getState(channel) != State.IDLE ) endScrum(channel);
scrumMembers = new LinkedList();
activeMember = null;
members = new HashMap();
previousMembers = new LinkedList();
}
if( message.contains( getName().toLowerCase() + ": quit" ) )
{
quitServer( sender + " told me to quit" );
}
if( message.contains( getName().toLowerCase() + ": help" ) )
{
sendMessage( sender, "I'll notify everyone when scrum starts. After that you have 30 seconds time to say \"hi\" to be included in scrum." );
sendMessage( sender, "I'll then ask everyone in reverse order to report. You should answer the following questions:" );
sendMessage( sender, " 1. What you did since last scrum?" );
sendMessage( sender, " 2. What you're going to do before next scrum?" );
sendMessage( sender, " 3. Are there impediments or obstacles that slow/stop your work?" );
sendMessage( sender, " When you're done include \"done.\" in your report." );
sendMessage( sender, "After all are done, I'll end the daily scrum. The next scrum will be held the next day." );
sendMessage( sender, "I understand the following commands: \"start\" (to start scrum), \"next\" (to skip to next reporter), \"reset\" (to" );
sendMessage( sender, "reset my internal state), \"help\" (this help) and \"quit\" (makes me quit)." );
sendMessage( sender, "Please note that you really should not need to use those commands. Use them only if there's real trouble." );
}
if( message.startsWith( "hi" ) )
{
if( !scrumMembers.contains(m) ) scrumMembers.add(m);
if( state == State.WAITING_REPORTS )
{
sendMessage( channel, m.getName() + ": Better late than never! You will be next!" );
}
}
if( message.contains("over.") )
{
memberDone(channel, m);
if( m.equals(activeMember) ) nextMember(channel);
}
log( channel, "Scrum Members: " + scrumMembers );
log( channel, "Active member: " + activeMember );
log( channel, "State: " + state + ", since: " + previousStateChange );
log( channel, "Previous members: " + previousMembers );
}
private void changeState(String channel, State state)
{
this.state = state;
this.previousStateChange = Calendar.getInstance();
}
private State getState(String channel)
{
return this.state;
}
private void startScrum(String channel)
{
sendAction( channel, "starts daily scrum... Please everyone say 'hi'!" );
changeState( channel, State.WAITING_ATTENDEES );
}
private boolean memberDone(String channel, Member m)
{
if( !previousMembers.contains(m) ) previousMembers.add(m);
return scrumMembers.remove(m);
}
private void startReporting(String channel)
{
changeState( channel, State.WAITING_REPORTS );
List<Member> list = new LinkedList( previousMembers );
list.removeAll( scrumMembers );
if( list.size() > 0 )
{
String members = null;
for(Member m: list)
{
if( members == null ) members = m.getName();
else members = members + ", " + m.getName();
}
sendMessage( channel, "Following participants not attending: " + members );
}
// this is the only place we can make a copy of the list of current scrum members
// to be used in the next round when deciding previous members.
this.previousMembers = new LinkedList( this.scrumMembers );
nextMember( channel );
}
private void nextMember(String channel)
{
//changeState( channel, State.WAITING_REPORTS );
Iterator<Member> i = scrumMembers.iterator();
if( i.hasNext() )
{
Member m = i.next();
// find the last on the list
while( i.hasNext() ) m = i.next();
sendMessage( channel, m.getName() + " please go ahead... (Just say 'over.' when you're done)" );
this.activeMember = m;
}
else
{
endScrum(channel);
}
}
private void endScrum(String channel)
{
this.activeMember = null;
sendAction( channel, "ends daily scrum... Thank you all!" );
changeState( channel, State.IDLE );
}
private void checkStatus(String channel)
{
State state = this.state;
Calendar now = Calendar.getInstance();
switch( state )
{
case IDLE:
int dow = now.get( Calendar.DAY_OF_WEEK );
if( now.get( Calendar.HOUR_OF_DAY ) == 11 && now.get( Calendar.MINUTE ) == 0 && now.get( Calendar.SECOND ) == 0 &&
dow != Calendar.SATURDAY && dow != Calendar.SUNDAY )
startScrum( channel );
break;
case WAITING_ATTENDEES:
{
// wait for 30 seconds of total silence
Calendar then = (Calendar)now.clone();
then.add( Calendar.SECOND, -30 );
if( then.after( previousStateChange ) ) startReporting(channel);
// wait for 10 seconds of silence after previous "hi"
then = (Calendar)now.clone();
then.add( Calendar.SECOND, -10 );
if( then.after( previousStateChange ) )
{
boolean all_active = true;
for(Member m2: previousMembers)
{
if( !scrumMembers.contains(m2) ) all_active = false;
}
// if all have activated themselves then lets get to work!
if( all_active )
{
startReporting(channel);
}
}
}
break;
// wait for 30 seconds
case WAITING_REPORTS:
now.add( Calendar.SECOND, -120 );
if( now.after( previousStateChange ) )
{
memberDone(channel, activeMember);
nextMember(channel);
}
break;
default:
quitServer( "Unknown state: " + state );
break;
}
}
private Member getMember(String channel, String nick, String login, String hostname)
{
if( members == null ) members = new HashMap();
if( members.containsKey( nick ) )
{
return members.get( nick );
}
else
{
Member m = new Member(nick);
members.put(nick, m);
return m;
}
}
private void log(String channel, String message)
{
System.out.println( channel + ": " + message );
}
/* ------------------------------------ */
public void run()
{
try {
while( true )
{
if( channel != null ) checkStatus(channel);
Thread.currentThread().sleep(800);
}
} catch(java.lang.InterruptedException e) {
}
}
}