1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
|
/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package net.java.sip.communicator.impl.protocol.irc;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;
import com.ircclouds.irc.api.*;
import com.ircclouds.irc.api.domain.*;
import com.ircclouds.irc.api.domain.messages.*;
import com.ircclouds.irc.api.listeners.*;
import com.ircclouds.irc.api.state.*;
/**
* Manager for message-related operations.
*
* FIXME MessageManager may miss some quick events such as NickServ messages right after establishing a connection.
*
* @author Danny van Heumen
*/
public class MessageManager
{
/**
* Logger.
*/
private static final Logger LOGGER = Logger.getLogger(MessageManager.class);
/**
* Maximum message size for IRC messages given the spec specifies a buffer
* of 512 bytes. The command ending (CRLF) takes up 2 bytes.
*/
private static final int IRC_PROTOCOL_MAXIMUM_MESSAGE_SIZE = 510;
/**
* IRCApi instance.
*
* Instance must be thread-safe!
*/
private final IRCApi irc;
/**
* Connection state.
*/
private final IIRCState connectionState;
/**
* Protocol provider service.
*/
private final ProtocolProviderServiceIrcImpl provider;
/**
* Identity manager.
*/
private final IdentityManager identity;
/**
* Constructor.
*
* @param irc thread-safe IRCApi instance
* @param connectionState the connection state
* @param provider the provider instance
* @param identity the identity manager
*/
public MessageManager(final IRCApi irc, final IIRCState connectionState,
final ProtocolProviderServiceIrcImpl provider,
final IdentityManager identity)
{
if (irc == null)
{
throw new IllegalArgumentException("irc cannot be null");
}
this.irc = irc;
if (connectionState == null)
{
throw new IllegalArgumentException(
"connectionState cannot be null");
}
this.connectionState = connectionState;
if (provider == null)
{
throw new IllegalArgumentException("provider cannot be null");
}
this.provider = provider;
if (identity == null)
{
throw new IllegalArgumentException("identity cannot be null");
}
this.identity = identity;
this.irc.addListener(new MessageManagerListener());
}
/**
* Send a command to the IRC server.
*
* @param chatroom the chat room
* @param message the command message
*/
public void command(final ChatRoomIrcImpl chatroom, final String message)
{
this.command(chatroom.getIdentifier(), message);
}
/**
* Send a command to the IRC server.
*
* @param contact the chat room
* @param message the command message
*/
public void command(final Contact contact, final MessageIrcImpl message)
{
this.command(contact.getAddress(), message.getContent());
}
/**
* Implementation of some commands. If the command is not recognized or
* implemented, it will be sent as if it were a normal message.
*
* TODO Eventually replace this with a factory such that we can easily
* extend with new commands.
*
* @param source Source contact or chat room from which the message is sent.
* @param message Command message that is sent.
*/
private void command(final String source, final String message)
{
if (!this.connectionState.isConnected())
{
throw new IllegalStateException("Not connected to IRC server.");
}
final String msg = message.toLowerCase();
if (msg.startsWith("/msg "))
{
final String part = message.substring(5);
int endOfNick = part.indexOf(' ');
if (endOfNick == -1)
{
throw new IllegalArgumentException("Invalid private message "
+ "format. Message was not sent.");
}
final String target = part.substring(0, endOfNick);
final String command = part.substring(endOfNick + 1);
this.irc.message(target, command);
}
else if (msg.startsWith("/me "))
{
final String command = message.substring(4);
this.irc.act(source, command);
}
else if (msg.startsWith("/join "))
{
final String part = message.substring(6);
final String channel;
final String password;
int indexOfSep = part.indexOf(' ');
if (indexOfSep == -1)
{
channel = part;
password = "";
}
else
{
channel = part.substring(0, indexOfSep);
password = part.substring(indexOfSep + 1);
}
if (channel.matches("[^,\\n\\r\\s\\a]+"))
{
this.irc.joinChannel(channel, password);
}
}
else
{
this.irc.message(source, message);
}
}
/**
* Send an IRC message.
*
* @param chatroom The chat room to send the message to.
* @param message The message to send.
*/
public void message(final ChatRoomIrcImpl chatroom, final String message)
{
if (!this.connectionState.isConnected())
{
throw new IllegalStateException("Not connected to an IRC server.");
}
final String target = chatroom.getIdentifier();
this.irc.message(target, message);
}
/**
* Send an IRC message.
*
* @param contact The contact to send the message to.
* @param message The message to send.
*/
public void message(final Contact contact, final Message message)
{
if (!this.connectionState.isConnected())
{
throw new IllegalStateException("Not connected to an IRC server.");
}
final String target = contact.getAddress();
try
{
this.irc.message(target, message.getContent());
LOGGER.trace("Message delivered to server successfully.");
}
catch (RuntimeException e)
{
LOGGER.trace("Failed to deliver message: " + e.getMessage(), e);
throw e;
}
}
/**
* Calculate maximum message size that can be transmitted.
*
* @param contact receiving contact
* @return returns maximum message size
*/
public int calculateMaximumMessageSize(final Contact contact)
{
final StringBuilder builder = new StringBuilder(":");
builder.append(this.identity.getIdentityString());
builder.append(" PRIVMSG ");
builder.append(contact.getAddress());
builder.append(" :");
return IRC_PROTOCOL_MAXIMUM_MESSAGE_SIZE - builder.length();
}
/**
* Message manager listener for handling message related events.
*
* @author Danny van Heumen
*/
private final class MessageManagerListener
extends VariousMessageListenerAdapter
{
/**
* IRC reply containing away message.
*/
private static final int RPL_AWAY = 301;
/**
* IRC error code for case of non-existing nick or channel name.
*/
private static final int ERR_NO_SUCH_NICK_CHANNEL =
IRCServerNumerics.NO_SUCH_NICK_CHANNEL;
/**
* On User Quit event.
*
* @param msg user quit message
*/
@Override
public void onUserQuit(final QuitMessage msg)
{
final String user = msg.getSource().getNick();
if (MessageManager.this.connectionState.getNickname().equals(user))
{
LOGGER.debug("Local user QUIT message received: removing "
+ "message manager listener.");
MessageManager.this.irc.deleteListener(this);
}
}
@Override
public void onServerNumericMessage(final ServerNumericMessage msg)
{
switch (msg.getNumericCode().intValue())
{
case ERR_NO_SUCH_NICK_CHANNEL:
// TODO Check if target is Contact, then update contact presence
// status to off-line since the nick apparently does not exist
// anymore.
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("Message did not get delivered: "
+ msg.asRaw());
}
final String msgText = msg.getText();
final int endOfTargetIndex = msgText.indexOf(' ');
if (endOfTargetIndex == -1)
{
LOGGER.trace("Expected target nick in error message, but "
+ "it cannot be found. Stop parsing.");
break;
}
final String targetNick =
msgText.substring(0, endOfTargetIndex);
// Send blank text string as the message, since we don't know
// what the actual message was. (We cannot reliably relate the
// NOSUCHNICK reply to the exact message that caused the error.)
MessageIrcImpl message =
new MessageIrcImpl(
"",
OperationSetBasicInstantMessaging.HTML_MIME_TYPE,
OperationSetBasicInstantMessaging.DEFAULT_MIME_ENCODING,
null);
final Contact to =
MessageManager.this.provider.getPersistentPresence()
.findOrCreateContactByID(targetNick);
MessageManager.this.provider
.getBasicInstantMessaging()
.fireMessageDeliveryFailed(
message,
to,
MessageDeliveryFailedEvent
.OFFLINE_MESSAGES_NOT_SUPPORTED);
break;
case RPL_AWAY:
final String rawAwayText = msg.getText();
final String awayUserNick =
rawAwayText.substring(0, rawAwayText.indexOf(' '));
final String awayText =
rawAwayText.substring(rawAwayText.indexOf(' ') + 2);
final MessageIrcImpl awayMessage =
MessageIrcImpl.newAwayMessageFromIRC(awayText);
final Contact awayUser =
MessageManager.this.provider.getPersistentPresence()
.findOrCreateContactByID(awayUserNick);
MessageManager.this.provider.getBasicInstantMessaging()
.fireMessageReceived(awayMessage, awayUser);
break;
default:
break;
}
}
/**
* Upon receiving a private message from a user, deliver that to an
* instant messaging contact and create one if it does not exist. We can
* ignore normal chat rooms, since they each have their own
* ChatRoomListener for managing chat room operations.
*
* @param msg the private message
*/
@Override
public void onUserPrivMessage(final UserPrivMsg msg)
{
final String user = msg.getSource().getNick();
final MessageIrcImpl message =
MessageIrcImpl.newMessageFromIRC(msg.getText());
final Contact from =
MessageManager.this.provider.getPersistentPresence()
.findOrCreateContactByID(user);
try
{
MessageManager.this.provider.getBasicInstantMessaging()
.fireMessageReceived(message, from);
}
catch (RuntimeException e)
{
// TODO remove once this is stable. Don't want to lose message
// when an accidental error occurs.
// It is likely that errors occurred because of some issues with
// MetaContactGroup for NonPersistent group, since this is an
// outstanding error.
LOGGER.error(
"Error occurred while delivering private message from user"
+ " '" + user + "': " + msg.getText(), e);
}
}
/**
* Upon receiving a user notice message from a user, deliver that to an
* instant messaging contact.
*
* @param msg user notice message
*/
@Override
public void onUserNotice(final UserNotice msg)
{
final String user = msg.getSource().getNick();
final Contact from =
MessageManager.this.provider.getPersistentPresence()
.findOrCreateContactByID(user);
final MessageIrcImpl message =
MessageIrcImpl.newNoticeFromIRC(from, msg.getText());
MessageManager.this.provider.getBasicInstantMessaging()
.fireMessageReceived(message, from);
}
/**
* Upon receiving a user action message from a user, deliver that to an
* instant messaging contact.
*
* @param msg user action message
*/
@Override
public void onUserAction(final UserActionMsg msg)
{
final String user = msg.getSource().getNick();
final Contact from =
MessageManager.this.provider.getPersistentPresence()
.findContactByID(user);
final MessageIrcImpl message =
MessageIrcImpl.newActionFromIRC(from, msg.getText());
MessageManager.this.provider.getBasicInstantMessaging()
.fireMessageReceived(message, from);
}
}
}
|