1 #import "JubCLIChatUI.h"
2 #import "JubChatClient.h"
3 #import "JubCLIColor.h"
4 #import "JubCLICommand.h"
9 BEGINCLICOMMAND(JubCLIReplyCommand, @":r", nil,
10 @"Sets the sender of the last incomming message as the default recipient")
12 if (_ui.lastIn == nil) {
13 [of_stdout writeLine: @"No message has been received yet"];
17 _ui.sink = (JubCLIChatUI*)[_ui.client chatForContact: _ui.lastIn];
18 [of_stdout writeFormat: @"Set sink to %@\n",
19 [_ui.lastIn.rosterItem.JID bareJID]];
23 BEGINCLICOMMAND(JubCLISetSinkCommand, @":s", @"<who>",
24 @"Selects <who> as the default recipient")
26 if ([parameters count] != 1) {
27 [of_stdout writeLine: @"Syntax: ':s <who>'"];
31 OFString *param = parameters[0];
32 XMPPContact *contact = _ui.client.contactManager.contacts[param];
35 [of_stdout writeFormat: @"Contact '%@' not found in your "
40 _ui.sink = (JubCLIChatUI*)[_ui.client chatForContact: contact];
42 [of_stdout writeFormat: @"Set sink to %@\n", param];
46 BEGINCLICOMMAND(JubCLIMessageCommand, @":m", @"<who> <message>",
47 @"Sends a single message to <who>")
49 if ([parameters count] < 2) {
50 [of_stdout writeLine: @"Syntax: ':m <who> <message>'"];
54 XMPPContact *contact =
55 _ui.client.contactManager.contacts[parameters[0]];
58 [of_stdout writeFormat: @"Contact %@ not found in your "
59 @"roster\n", parameters[0]];
64 (JubCLIChatUI*)[_ui.client chatForContact: contact];
67 [parameters arrayByRemovingObject: [parameters firstObject]];
69 [chat send: [message componentsJoinedByString: @" "]];
73 BEGINCLICOMMAND(JubCLIPresenceCommand, @":t", @"<status> [<message>]",
74 @"Changes your presence")
76 if ([parameters count] < 1) {
78 @"Syntax: ':t <status> [<message>]'"];
82 OFString *show = parameters[0];
84 if (![@[ @"available", @"away", @"dnd", @"xa", @"chat", @"unavailable" ]
85 containsObject: show]) {
86 [of_stdout writeLine: @"<status> must be one of: "
87 @"available, away, dnd, xa, chat, unavailable"];
91 if ([parameters count] == 2) {
92 [_ui.client sendPresenceWithStatus: show];
96 OFString *message = [[parameters
97 arrayByRemovingObject: [parameters firstObject]]
98 componentsJoinedByString: @" "];
100 [_ui.client sendPresenceWithStatus: show
105 BEGINCLICOMMAND(JubCLIRosterCommand, @":roster", nil, @"Shows your roster")
107 OFDictionary *contacts = _ui.client.contactManager.contacts;
108 for (OFString *key in contacts) {
109 XMPPContact *contact = contacts[key];
110 OFString *name = contact.rosterItem.name;
111 XMPPPresence *presence =
112 [[[contact.presences allObjects] sortedArray] firstObject];
115 [of_stdout writeFormat: @"%@ <%@> (", name, key];
117 [of_stdout writeFormat: @"%@ (", key];
120 [of_stdout writeFormat: COL_OFFLINE(@"offline")];
121 else if ([presence.show isEqual: @"chat"])
122 [of_stdout writeFormat: COL_CHAT(@"free for chat")];
123 else if ([presence.show isEqual: @"away"])
124 [of_stdout writeFormat: COL_AWAY(@"away")];
125 else if ([presence.show isEqual: @"xa"])
126 [of_stdout writeFormat: COL_XA(@"extended away")];
127 else if ([presence.show isEqual: @"dnd"])
128 [of_stdout writeFormat: COL_DND(@"do not disturb")];
130 [of_stdout writeFormat: COL_ONLINE(@"online")];
132 [of_stdout writeString: @")\n"];
137 BEGINCLICOMMAND(JubCLISubsCommand, @":subs",
138 @"[list | ack <who> | nak <who>]",
139 @"Lists, acknowledges or denies subscription requests")
141 if ([parameters count] < 1) {
142 [of_stdout writeLine: @"Syntax: ':subs "
143 @"[list | ack <who> | nak <who>]'"];
147 OFString *action = parameters[0];
148 if (![action isEqual: @"list"] && ([parameters count] != 2)) {
149 [of_stdout writeLine: @"Syntax: ':subs "
150 @"[list | ack <who> | nak <who>]'"];
154 if ([action isEqual: @"list"]) {
155 for (OFString *request in _ui.subRequests)
156 [of_stdout writeLine: request];
160 XMPPJID *JID = [XMPPJID JIDWithString: parameters[1]];
162 if ([action isEqual: @"ack"])
163 [_ui.client.contactManager sendSubscribedToJID: JID];
164 else if ([action isEqual: @"nak"])
165 [_ui.client.contactManager sendUnsubscribedToJID: JID];
167 [_ui.subRequests removeObject: parameters[1]];
171 @implementation JubCLIUI
172 @synthesize client = _client;
173 @synthesize lastIn = _lastIn;
174 @synthesize sink = _sink;
175 @synthesize subRequests = _subRequests;
177 - initWithClient: (JubChatClient*)client
182 _commands = [OFMutableDictionary new];
183 _subRequests = [OFMutableSet new];
184 _client = [client retain];
185 _contactManager = client.contactManager;
186 [_contactManager addDelegate: self];
188 [self addCommand: [[[JubCLIReplyCommand alloc]
189 initWithCLIUI: self] autorelease]];
191 [self addCommand: [[[JubCLISetSinkCommand alloc]
192 initWithCLIUI: self] autorelease]];
194 [self addCommand: [[[JubCLIMessageCommand alloc]
195 initWithCLIUI: self] autorelease]];
197 [self addCommand: [[[JubCLIPresenceCommand alloc]
198 initWithCLIUI: self] autorelease]];
200 [self addCommand: [[[JubCLIRosterCommand alloc]
201 initWithCLIUI: self] autorelease]];
202 [self addCommand: [[[JubCLISubsCommand alloc]
203 initWithCLIUI: self] autorelease]];
214 [_contactManager removeDelegate: self];
216 [_subRequests release];
221 static JubCLIUI *completionData;
222 static void completionCallback(OFString *buf, OFList *lc)
224 if ([buf length] < 3)
227 if (![buf hasPrefix: @":s "] && ![buf hasPrefix: @":m "] &&
228 ![buf hasPrefix: @":t "])
231 if ([buf hasPrefix: @":t"]) {
232 OFString *options[] = {
241 for (int i = 0; i < 6; i++) {
242 if (![options[i] hasPrefix: buf])
244 [lc appendObject: options[i]];
250 OFString *command = [buf substringWithRange: of_range(0, 3)];
251 OFString *query = [buf substringWithRange: of_range(3, [buf length]-3)];
252 OFDictionary *contacts = completionData.client.contactManager.contacts;
253 for (OFString *key in contacts) {
254 if (![key hasPrefix: query])
256 [lc appendObject: [command stringByAppendingString: key]];
260 - (void)startUIThread
262 completionData = self;
264 [[OFThread threadWithBlock: ^(void) {
266 Linenoise *reader = [Linenoise sharedLinenoise];
267 reader.multiline = true;
268 reader.completionCallback = completionCallback;
270 while ((line = [reader readInputWithPrompt: @"> "]) != nil) @autoreleasepool {
271 [self Jub_userInputWithStream: nil
274 if ([line length] != 0)
275 [reader addHistoryItem: line];
277 [self Jub_userInputWithStream: nil
285 - (void)client: (JubChatClient*)client
286 didChangePresence: (XMPPPresence*)presence
292 return [JubCLIChatUI class];
295 - (void)addCommand: (id<JubCLICommand>)command
297 [_commands setObject: command
298 forKey: command.command];
301 - (BOOL)Jub_userInputWithStream: (OFStream*)stream
302 line: (OFString*)line
303 exception: (OFException*)exception
305 if (line == nil || exception != nil)
306 [OFApplication terminate];
308 if ([line length] == 0)
311 if ([line characterAtIndex: 0] != ':') {
313 [of_stdout writeLine: @"No default sink selected, "
314 @"type `:h` for help"];
323 line = [line stringByDeletingTrailingWhitespaces];
325 OFArray *input= [line componentsSeparatedByString: @" "];
327 if ([input[0] isEqual: @":h"]) {
328 __block size_t longest = 0;
330 [_commands enumerateKeysAndObjectsUsingBlock:
331 ^(OFString *key, id<JubCLICommand> command, bool *stop) {
332 size_t length = [command.command length] +
333 (command.params == nil ? 0 :
334 (1 + [command.params length]));
336 if (length > longest)
340 for (OFString *key in [[_commands allKeys] sortedArray]) {
341 id<JubCLICommand> command = _commands[key];
342 size_t length = [command.command length] +
343 (command.params == nil ? 0 :
344 (1 + [command.params length]));
346 if (command.params == nil)
347 [of_stdout writeFormat: @"`%@`",
350 [of_stdout writeFormat: @"`%@ %@`",
351 command.command, command.params];
353 // This is NOT distributive due to integer arithmetic
354 size_t padding = (longest + 2)/8 - (length + 2)/8;
356 for (size_t i = 0; i <= padding; i++)
357 [of_stdout writeString: @"\t"];
359 [of_stdout writeFormat: @"- %@\n", command.help];
365 id<JubCLICommand> command = _commands[input[0]];
368 [command callWithParameters:
369 [input arrayByRemovingObject: [input firstObject]]];
374 [of_stdout writeLine: @"Invalid command, type `:h` for help"];
379 - (void)contactManager: (XMPPContactManager*)manager
380 didAddContact: (XMPPContact*)contact
382 XMPPRosterItem *rosterItem = contact.rosterItem;
383 OFString *subscription = rosterItem.subscription;
384 OFString *bareJID = [rosterItem.JID bareJID];
386 if ([subscription isEqual: @"from"] || [subscription isEqual: @"both"])
387 [_subRequests removeObject: bareJID];
390 - (void)contactManager: (XMPPContactManager*)manager
391 didReceiveSubscriptionRequest: (XMPPPresence*)presence
393 OFString *bareJID = [presence.from bareJID];
394 [_subRequests addObject: bareJID];
395 [of_stdout writeFormat: @"\r" BOLD("%@") @" send a request to "
396 @"subscribe to your presence\r\n"
397 @"Use the :subs command to answer it\n",
399 [[Linenoise sharedLinenoise] refreshLine];
402 - (void)contact: (XMPPContact*)contact
403 willUpdateWithRosterItem: (XMPPRosterItem*)rosterItem
405 OFString *subscription = rosterItem.subscription;
406 OFString *bareJID = [rosterItem.JID bareJID];
408 if ([subscription isEqual: @"from"] || [subscription isEqual: @"both"])
409 [_subRequests removeObject: bareJID];
412 - (void)contact: (XMPPContact*)contact
413 didSendMessage: (XMPPMessage*)message
415 if (message.body == nil || ![message.type isEqual: @"chat"])
421 - (void)contact: (XMPPContact*)contact
422 didSendPresence: (XMPPPresence*)presence
424 [of_stdout writeFormat: @"\r"];
425 [of_stdout writeFormat: BOLD("%@") @" is now in state ", presence.from];
427 if ([presence.type isEqual: @"unavailable"])
428 [of_stdout writeFormat: COL_OFFLINE(@"offline")];
429 else if ([presence.show isEqual: @"chat"])
430 [of_stdout writeFormat: COL_CHAT(@"free for chat")];
431 else if ([presence.show isEqual: @"away"])
432 [of_stdout writeFormat: COL_AWAY(@"away")];
433 else if ([presence.show isEqual: @"xa"])
434 [of_stdout writeFormat: COL_XA(@"extended away")];
435 else if ([presence.show isEqual: @"dnd"])
436 [of_stdout writeFormat: COL_DND(@"do not disturb")];
438 [of_stdout writeFormat: COL_ONLINE(@"online")];
440 if (presence.status != nil)
441 [of_stdout writeFormat: @": %@", presence.status];
443 [of_stdout writeString: @"\n"];
445 [[Linenoise sharedLinenoise] refreshLine];