+#import "JubCLIChatUI.h"
+#import "JubChatClient.h"
+#import "JubCLIColor.h"
+#import "JubCLICommand.h"
+#import "JubCLIUI.h"
+
+BEGINCLICOMMAND(JubCLIReplyCommand, @":r", nil,
+ @"Sets the sender of the last incomming message as the default recipient")
+{
+ if (_ui.lastIn == nil) {
+ [of_stdout writeLine: @"No message has been received yet"];
+ return;
+ }
+
+ _ui.sink = (JubCLIChatUI*)[_ui.client chatForContact: _ui.lastIn];
+ [of_stdout writeFormat: @"Set sink to %@\n",
+ [_ui.lastIn.rosterItem.JID bareJID]];
+}
+ENDCLICOMMAND
+
+BEGINCLICOMMAND(JubCLISetSinkCommand, @":s", @"<who>",
+ @"Selects <who> as the default recipient")
+{
+ if ([parameters count] != 1) {
+ [of_stdout writeLine: @"Syntax: ':s <who>'"];
+ return;
+ }
+
+ OFString *param = parameters[0];
+ XMPPContact *contact = _ui.client.contactManager.contacts[param];
+
+ if (contact == nil) {
+ [of_stdout writeFormat: @"Contact '%@' not found in your "
+ @"roster\n", param];
+ return;
+ }
+
+ _ui.sink = (JubCLIChatUI*)[_ui.client chatForContact: contact];
+
+ [of_stdout writeFormat: @"Set sink to %@\n", param];
+}
+ENDCLICOMMAND
+
+BEGINCLICOMMAND(JubCLIMessageCommand, @":m", @"<who> <message>",
+ @"Sends a single message to <who>")
+{
+ if ([parameters count] < 2) {
+ [of_stdout writeLine: @"Syntax: ':m <who> <message>'"];
+ return;
+ }
+
+ XMPPContact *contact =
+ _ui.client.contactManager.contacts[parameters[0]];
+
+ if (contact == nil) {
+ [of_stdout writeFormat: @"Contact %@ not found in your "
+ @"roster\n", parameters[0]];
+ return;
+ }
+
+ JubCLIChatUI *chat =
+ (JubCLIChatUI*)[_ui.client chatForContact: contact];
+
+ OFArray *message =
+ [parameters arrayByRemovingObject: [parameters firstObject]];
+
+ [chat send: [message componentsJoinedByString: @" "]];
+}
+ENDCLICOMMAND
+
+BEGINCLICOMMAND(JubCLIPresenceCommand, @":t", @"<status> [<message>]",
+ @"Changes your presence")
+{
+ if ([parameters count] < 1) {
+ [of_stdout writeLine:
+ @"Syntax: ':t <status> [<message>]'"];
+ return;
+ }
+
+ XMPPPresence *presence;
+ OFString *show = parameters[0];
+
+ if (![@[ @"available", @"away", @"dnd", @"xa", @"chat", @"unavailable" ]
+ containsObject: show]) {
+ [of_stdout writeLine: @"<status> must be one of:"
+ @"available, away, dnd, xa, chat, unavailable"];
+ return;
+ }
+
+ if ([show isEqual: @"unavailable"])
+ presence = [XMPPPresence presenceWithType: show];
+ else
+ presence = [XMPPPresence presence];
+
+ if (![@[ @"available", @"unavailable" ] containsObject: show])
+ presence.show = show;
+
+ if ([parameters count] == 2) {
+ [_ui.client.connection sendStanza: presence];
+ return;
+ }
+
+ OFArray *message =
+ [parameters arrayByRemovingObject: [parameters firstObject]];
+ presence.status = [message componentsJoinedByString: @" "];
+
+ [_ui.client.connection sendStanza: presence];
+}
+ENDCLICOMMAND
+
+BEGINCLICOMMAND(JubCLIRosterCommand, @":roster", nil, @"Shows your roster")
+{
+ OFDictionary *contacts = _ui.client.contactManager.contacts;
+ for (OFString *key in contacts) {
+ XMPPContact *contact = contacts[key];
+ OFString *name = contact.rosterItem.name;
+ XMPPPresence *presence =
+ [[[contact.presences allObjects] sortedArray] firstObject];
+
+ if (name != nil)
+ [of_stdout writeFormat: @"%@ <%@> (", name, key];
+ else
+ [of_stdout writeFormat: @"%@ (", key];
+
+ if (presence == nil)
+ [of_stdout writeFormat: COL_OFFLINE(@"offline")];
+ else if ([presence.show isEqual: @"chat"])
+ [of_stdout writeFormat: COL_CHAT(@"free for chat")];
+ else if ([presence.show isEqual: @"away"])
+ [of_stdout writeFormat: COL_AWAY(@"away")];
+ else if ([presence.show isEqual: @"xa"])
+ [of_stdout writeFormat: COL_XA(@"extended away")];
+ else if ([presence.show isEqual: @"dnd"])
+ [of_stdout writeFormat: COL_DND(@"do not disturb")];
+ else
+ [of_stdout writeFormat: COL_ONLINE(@"online")];
+
+ [of_stdout writeString: @")\n"];
+ }
+}
+ENDCLICOMMAND
+
+@implementation JubCLIUI
+@synthesize client = _client;
+@synthesize lastIn = _lastIn;
+@synthesize sink = _sink;
+
+- initWithClient: (JubChatClient*)client
+{
+ self = [super init];
+
+ @try {
+ _commands = [OFMutableDictionary new];
+ _client = [client retain];
+ _contactManager = client.contactManager;
+ [_contactManager addDelegate: self];
+
+ [self addCommand: [[[JubCLIReplyCommand alloc]
+ initWithCLIUI: self] autorelease]];
+
+ [self addCommand: [[[JubCLISetSinkCommand alloc]
+ initWithCLIUI: self] autorelease]];
+
+ [self addCommand: [[[JubCLIMessageCommand alloc]
+ initWithCLIUI: self] autorelease]];
+
+ [self addCommand: [[[JubCLIPresenceCommand alloc]
+ initWithCLIUI: self] autorelease]];
+
+ [self addCommand: [[[JubCLIRosterCommand alloc]
+ initWithCLIUI: self] autorelease]];
+ } @catch (id e) {
+ [self release];
+ @throw e;
+ }
+
+ return self;
+}
+
+- (void)dealloc
+{
+ [_contactManager removeDelegate: self];
+ [_client release];
+ [_commands release];
+ [super dealloc];
+}
+
+- (void)startUIThread
+{
+ [of_stdin asyncReadLineWithTarget: self
+ selector: @selector(Jub_userInputWithStream:
+ line:exception:)];
+}
+
+- (void)client: (JubChatClient*)client
+ didChangePresence: (XMPPPresence*)presence
+{
+}
+
+- (Class)chatUIClass
+{
+ return [JubCLIChatUI class];
+}
+
+- (void)addCommand: (id<JubCLICommand>)command
+{
+ [_commands setObject: command
+ forKey: command.command];
+}
+
+- (BOOL)Jub_userInputWithStream: (OFStream*)stream
+ line: (OFString*)line
+ exception: (OFException*)exception
+{
+ if (line == nil)
+ [OFApplication terminate];
+
+ if ([line length] == 0)
+ return YES;
+
+ if ([line characterAtIndex: 0] != ':') {
+ if (_sink == nil)
+ [of_stdout writeLine: @"No default sink selected, "
+ @"type `:h` for help"];
+
+ [_sink send: line];
+
+ return YES;
+ }
+
+ line = [line stringByDeletingTrailingWhitespaces];
+
+ OFArray *input= [line componentsSeparatedByString: @" "];
+
+ if ([input[0] isEqual: @":h"]) {
+ __block size_t longest = 0;
+
+ [_commands enumerateKeysAndObjectsUsingBlock:
+ ^(OFString *key, id<JubCLICommand> command, BOOL *stop) {
+ size_t length = [command.command length] +
+ (command.params == nil ? 0 :
+ (1 + [command.params length]));
+
+ if (length > longest)
+ longest = length;
+ }];
+
+ for (OFString *key in [[_commands allKeys] sortedArray]) {
+ id<JubCLICommand> command = _commands[key];
+ size_t length = [command.command length] +
+ (command.params == nil ? 0 :
+ (1 + [command.params length]));
+
+ if (command.params == nil)
+ [of_stdout writeFormat: @"`%@`",
+ command.command];
+ else
+ [of_stdout writeFormat: @"`%@ %@`",
+ command.command, command.params];
+
+ // This is NOT distributive due to integer arithmetic
+ size_t padding = (longest + 2)/8 - (length + 2)/8;
+
+ for (size_t i = 0; i <= padding; i++)
+ [of_stdout writeString: @"\t"];
+
+ [of_stdout writeFormat: @"- %@\n", command.help];
+ };
+
+ return YES;
+ }
+
+ id<JubCLICommand> command = _commands[input[0]];
+
+ if (command) {
+ [command callWithParameters:
+ [input arrayByRemovingObject: [input firstObject]]];
+
+ return YES;
+ }
+
+ [of_stdout writeLine: @"Invalid command, type `:h` for help"];
+
+ return YES;
+}
+
+- (void)contact: (XMPPContact*)contact
+ didSendMessage: (XMPPMessage*)message
+{
+ if (message.body == nil || ![message.type isEqual: @"chat"])
+ return;
+
+ _lastIn = contact;
+}
+
+- (void)contact: (XMPPContact*)contact
+ didSendPresence: (XMPPPresence*)presence
+{
+ [of_stdout writeFormat: BOLD("%@") @" is now in state ", presence.from];
+
+ if ([presence.type isEqual: @"unavailable"])
+ [of_stdout writeFormat: COL_OFFLINE(@"offline")];
+ else if ([presence.show isEqual: @"chat"])
+ [of_stdout writeFormat: COL_CHAT(@"free for chat")];
+ else if ([presence.show isEqual: @"away"])
+ [of_stdout writeFormat: COL_AWAY(@"away")];
+ else if ([presence.show isEqual: @"xa"])
+ [of_stdout writeFormat: COL_XA(@"extended away")];
+ else if ([presence.show isEqual: @"dnd"])
+ [of_stdout writeFormat: COL_DND(@"do not disturb")];
+ else
+ [of_stdout writeFormat: COL_ONLINE(@"online")];
+
+ if (presence.status != nil)
+ [of_stdout writeFormat: @": %@", presence.status];
+
+ [of_stdout writeString: @"\n"];
+}
+@end