comparison libpurple/protocols/myspace/myspace.c @ 16405:8063f163f411

Add MySpaceIM header file and reorganize functions.
author Jeffrey Connelly <jaconnel@calpoly.edu>
date Sat, 28 Apr 2007 21:19:40 +0000
parents 47e07438f01c
children f62023eddfc6
comparison
equal deleted inserted replaced
16404:47e07438f01c 16405:8063f163f411
1 /* MySpaceIM Protocol Plugin 1 /* MySpaceIM Protocol Plugin
2 * 2 *
3 * \author Jeff Connelly 3 * \author Jeff Connelly
4 * 4 *
5 * Copyright (C) 2007, Jeff Connelly <myspaceim@xyzzy.cjb.net> 5 * Copyright (C) 2007, Jeff Connelly <jeff2@homing.pidgin.im>
6 * 6 *
7 * Based on Purple's "C Plugin HOWTO" hello world example. 7 * Based on Purple's "C Plugin HOWTO" hello world example.
8 * 8 *
9 * Code also drawn from myspace: 9 * Code also drawn from myspace:
10 * http://snarfed.org/space/purple+mock+protocol+plugin 10 * http://snarfed.org/space/purple+mock+protocol+plugin
53 #include "version.h" 53 #include "version.h"
54 #include "cipher.h" /* for SHA-1 */ 54 #include "cipher.h" /* for SHA-1 */
55 #include "util.h" /* for base64 */ 55 #include "util.h" /* for base64 */
56 #include "debug.h" /* for purple_debug_info */ 56 #include "debug.h" /* for purple_debug_info */
57 57
58 #define MSIM_STATUS_ONLINE "online" 58 #include "myspace.h"
59 #define MSIM_STATUS_AWAY "away"
60 #define MSIM_STATUS_OFFLINE "offline"
61 #define MSIM_STATUS_INVISIBLE "invisible"
62
63 /* Build version of MySpaceIM to report to servers */
64 #define MSIM_CLIENT_VERSION 673
65
66 #define MSIM_SERVER "im.myspace.akadns.net"
67 //#define MSIM_SERVER "localhost"
68 #define MSIM_PORT 1863 /* TODO: alternate ports and automatic */
69
70 /* Constants */
71 #define HASH_SIZE 0x14 /**< Size of SHA-1 hash for login */
72 #define NONCE_HALF_SIZE 0x20 /**< Half of decoded 'nc' field */
73 #define MSIM_READ_BUF_SIZE 5*1024 /**< Receive buffer size */
74 #define MSIM_FINAL_STRING "\\final\\" /**< Message end marker */
75
76 /* Messages */
77 #define MSIM_BM_INSTANT 1
78 #define MSIM_BM_STATUS 100
79 #define MSIM_BM_ACTION 121
80 /*#define MSIM_BM_UNKNOWN1 122*/
81
82 /* Random number in every MsimSession, to ensure it is valid. */
83 #define MSIM_SESSION_STRUCT_MAGIC 0xe4a6752b
84
85 /* Everything needed to keep track of a session. */
86 typedef struct _MsimSession
87 {
88 guint magic; /**< MSIM_SESSION_STRUCT_MAGIC */
89 PurpleAccount *account;
90 PurpleConnection *gc;
91 gchar *sesskey; /**< Session key text string from server */
92 gchar *userid; /**< This user's numeric user ID */
93 gint fd; /**< File descriptor to/from server */
94
95 GHashTable *user_lookup_cb; /**< Username -> userid lookup callback */
96 GHashTable *user_lookup_cb_data; /**< Username -> userid lookup callback data */
97 GHashTable *user_lookup_cache; /**< Cached information on users */
98
99 gchar *rxbuf; /**< Receive buffer */
100 guint rxoff; /**< Receive buffer offset */
101 } MsimSession;
102
103 #define MSIM_SESSION_VALID(s) (session != NULL && session->magic == MSIM_SESSION_STRUCT_MAGIC)
104
105 /* Callback for when a user's information is received, initiated from a user lookup. */
106 typedef void (*MSIM_USER_LOOKUP_CB)(MsimSession *session, GHashTable *userinfo, gpointer data);
107
108 /* Passed to MSIM_USER_LOOKUP_CB for msim_send_im_cb - called when
109 * user information is available, ready to send a message. */
110 typedef struct _send_im_cb_struct
111 {
112 gchar *who;
113 gchar *message;
114 PurpleMessageFlags flags;
115 } send_im_cb_struct;
116
117
118 /* TODO: .h file */
119 static void msim_lookup_user(MsimSession *session, const gchar *user, MSIM_USER_LOOKUP_CB cb, gpointer data);
120 static inline gboolean msim_is_userid(const gchar *user);
121 static void msim_session_destroy(MsimSession *session);
122 59
123 static void init_plugin(PurplePlugin *plugin) 60 static void init_plugin(PurplePlugin *plugin)
124 { 61 {
125 purple_notify_message(plugin, PURPLE_NOTIFY_MSG_INFO, "Hello World!", 62 purple_notify_message(plugin, PURPLE_NOTIFY_MSG_INFO, "Hello World!",
126 "This is the Hello World! plugin :)", NULL, NULL, NULL); 63 "This is the Hello World! plugin :)", NULL, NULL, NULL);
141 MSIM_STATUS_ONLINE, MSIM_STATUS_AWAY, MSIM_STATUS_OFFLINE, MSIM_STATUS_INVISIBLE); 78 MSIM_STATUS_ONLINE, MSIM_STATUS_AWAY, MSIM_STATUS_OFFLINE, MSIM_STATUS_INVISIBLE);
142 79
143 80
144 types = NULL; 81 types = NULL;
145 82
83 /* TODO: Clean up - I don't like all this repetition */
146 type = purple_status_type_new(PURPLE_STATUS_AVAILABLE, MSIM_STATUS_ONLINE, 84 type = purple_status_type_new(PURPLE_STATUS_AVAILABLE, MSIM_STATUS_ONLINE,
147 MSIM_STATUS_ONLINE, TRUE); 85 MSIM_STATUS_ONLINE, TRUE);
148 purple_status_type_add_attr(type, "message", "Online", 86 purple_status_type_add_attr(type, "message", "Online",
149 purple_value_new(PURPLE_TYPE_STRING)); 87 purple_value_new(PURPLE_TYPE_STRING));
150 types = g_list_append(types, type); 88 types = g_list_append(types, type);
168 types = g_list_append(types, type); 106 types = g_list_append(types, type);
169 107
170 return types; 108 return types;
171 } 109 }
172 110
111 /**
112 * Return the icon name for a buddy and account.
113 *
114 * @param acct The account to find the icon for, or NULL for protocol icon.
115 * @param buddy The buddy to find the icon for, or NULL for the account icon.
116 *
117 * @return The base icon name string.
118 */
119 static const gchar *msim_list_icon(PurpleAccount *acct, PurpleBuddy *buddy)
120 {
121 /* TODO: use a MySpace icon. hbons submitted one to
122 * http://developer.pidgin.im/wiki/MySpaceIM - tried placing in
123 * C:\cygwin\home\Jeff\purple-2.0.0beta6\gtk\pixmaps\status\default
124 * and returning "myspace" but icon shows up blank.
125 */
126 if (acct == NULL)
127 {
128 purple_debug_info("msim", "msim_list_icon: acct == NULL!\n");
129 //exit(-2);
130 }
131 return "meanwhile";
132 }
173 /** 133 /**
174 * Parse a MySpaceIM protocol message into a hash table. 134 * Parse a MySpaceIM protocol message into a hash table.
175 * 135 *
176 * @param msg The message string to parse, will be g_free()'d. 136 * @param msg The message string to parse, will be g_free()'d.
177 * 137 *
240 200
241 return table; 201 return table;
242 } 202 }
243 203
244 /** 204 /**
205 * Parse a \x1c-separated "dictionary" of key=value pairs into a hash table.
206 *
207 * @param body_str The text of the dictionary to parse. Often the
208 * value for the 'body' field.
209 *
210 * @return Hash table of the keys and values. Must g_hash_table_destroy() when done.
211 */
212 static GHashTable *msim_parse_body(const gchar *body_str)
213 {
214 GHashTable *table;
215 gchar *item;
216 gchar **items;
217 gchar **elements;
218 guint i;
219
220 g_return_val_if_fail(body_str != NULL, NULL);
221
222 table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
223
224 for (items = g_strsplit(body_str, "\x1c", 0), i = 0;
225 (item = items[i]);
226 i++)
227 {
228 gchar *key, *value;
229
230 //printf("TOK=<%s>\n", token);
231 elements = g_strsplit(item, "=", 2);
232
233 key = elements[0];
234 if (!key)
235 {
236 purple_debug_info("msim", "msim_parse_body(%s): null key\n",
237 body_str);
238 g_strfreev(elements);
239 break;
240 }
241
242 value = elements[1];
243 if (!value)
244 {
245 purple_debug_info("msim", "msim_parse_body(%s): null value\n",
246 body_str);
247 g_strfreev(elements);
248 break;
249 }
250
251 //printf("-- %s: %s\n", key, value);
252
253 /* XXX: This overwrites duplicates. */
254 /* TODO: make the GHashTable values be GList's, and append to the list if
255 * there is already a value of the same key name. This is important for
256 * the WebChallenge message. */
257 g_hash_table_insert(table, g_strdup(key), g_strdup(value));
258
259 g_strfreev(elements);
260 }
261
262 g_strfreev(items);
263
264 return table;
265 }
266
267
268
269 static void print_hash_item(gpointer key, gpointer value, gpointer user_data)
270 {
271 printf("%s=%s\n", (char*)key, (char*)value);
272 }
273
274 /**
275 * Send an arbitrary protocol message.
276 *
277 * @param session
278 * @param msg The textual, encoded message to send.
279 *
280 * Note: this does not send instant messages. For that, see msim_send_im.
281 */
282 static void msim_send(MsimSession *session, const gchar *msg)
283 {
284 int ret;
285
286 g_return_if_fail(MSIM_SESSION_VALID(session));
287 g_return_if_fail(msg != NULL);
288
289 purple_debug_info("msim", "msim_send: writing <%s>\n", msg);
290
291 ret = send(session->fd, msg, strlen(msg), 0);
292
293 if (ret != strlen(msg))
294 {
295 purple_debug_info("msim",
296 "msim_send(%s): strlen=%d, but only wrote %s\n",
297 msg, strlen(msg), ret);
298 /* TODO: better error -or- TODO: send all, loop unless ret=-1 */
299 }
300 }
301
302 /**
303 * Start logging in to the MSIM servers.
304 *
305 * @param acct Account information to use to login.
306 */
307 static void msim_login(PurpleAccount *acct)
308 {
309 PurpleConnection *gc;
310 const char *host;
311 int port;
312
313 g_return_if_fail(acct != NULL);
314
315 purple_debug_info("myspace", "logging in %s\n", acct->username);
316
317 gc = purple_account_get_connection(acct);
318 gc->proto_data = msim_session_new(acct);
319
320 /* 1. connect to server */
321 purple_connection_update_progress(gc, "Connecting",
322 0, /* which connection step this is */
323 4); /* total number of steps */
324
325 /* TODO: GUI option to be user-modifiable. */
326 host = purple_account_get_string(acct, "server", MSIM_SERVER);
327 port = purple_account_get_int(acct, "port", MSIM_PORT);
328 /* TODO: connect */
329 /* From purple.sf.net/api:
330 * """Note that this function name can be misleading--although it is called
331 * "proxy connect," it is used for establishing any outgoing TCP connection,
332 * whether through a proxy or not.""" */
333
334 /* Calls msim_connect_cb when connected. */
335 if (purple_proxy_connect(gc, acct, host, port, msim_connect_cb, gc) == NULL)
336 {
337 /* TODO: try other ports if in auto mode, then save
338 * working port and try that first next time. */
339 purple_connection_error(gc, "Couldn't create socket");
340 return;
341 }
342
343 }
344 /**
345 * Process a login challenge, sending a response.
346 *
347 * @param session
348 * @param table Hash table of login challenge message.
349 *
350 * @return 0, since the 'table' parameter is no longer needed.
351 */
352 static int msim_login_challenge(MsimSession *session, GHashTable *table)
353 {
354 PurpleAccount *account;
355 gchar *nc_str;
356 guchar *nc;
357 gchar *response_str;
358 gsize nc_len;
359 gchar *buf;
360
361 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0);
362 g_return_val_if_fail(table != NULL, 0);
363
364 nc_str = g_hash_table_lookup(table, "nc");
365
366 account = session->account;
367 //assert(account);
368
369 purple_connection_update_progress(session->gc, "Reading challenge", 1, 4);
370
371 purple_debug_info("msim", "nc=<%s>\n", nc_str);
372
373 nc = (guchar*)purple_base64_decode(nc_str, &nc_len);
374 purple_debug_info("msim", "base64 decoded to %d bytes\n", nc_len);
375 if (nc_len != 0x40)
376 {
377 purple_debug_info("msim", "bad nc length: %x != 0x40\n", nc_len);
378 purple_connection_error(session->gc, "Unexpected challenge length from server");
379 return 0;
380 }
381
382 purple_connection_update_progress(session->gc, "Logging in", 2, 4);
383
384 printf("going to compute login response\n");
385 //response_str = msim_compute_login_response(nc_str, "testuser", "testpw"); //session->gc->account->username, session->gc->account->password);
386 response_str = msim_compute_login_response(nc, account->username, account->password);
387 printf("got back login response\n");
388
389 g_free(nc);
390
391 /* Reply */
392 buf = g_strdup_printf("\\login2\\%d\\username\\%s\\response\\%s\\clientver\\%d\\reconn\\%d\\status\\%d\\id\\1\\final\\",
393 196610, account->username, response_str, MSIM_CLIENT_VERSION, 0, 100);
394
395 g_free(response_str);
396
397 purple_debug_info("msim", "response=<%s>\n", buf);
398
399 msim_send(session, buf);
400
401 g_free(buf);
402
403 return 0;
404 }
405
406 /**
245 * Compute the base64'd login challenge response based on username, password, nonce, and IPs. 407 * Compute the base64'd login challenge response based on username, password, nonce, and IPs.
246 * 408 *
247 * @param nonce The base64 encoded nonce ('nc') field from the server. 409 * @param nonce The base64 encoded nonce ('nc') field from the server.
248 * @param email User's email address (used as login name). 410 * @param email User's email address (used as login name).
249 * @param password User's cleartext password. 411 * @param password User's cleartext password.
250 * 412 *
251 * @return Encoded login challenge response, ready to send to the server. Must be g_free()'d 413 * @return Encoded login challenge response, ready to send to the server. Must be g_free()'d
252 * when finished. 414 * when finished.
253 */ 415 */
254 static gchar* msim_compute_login_response(guchar nonce[2*NONCE_HALF_SIZE], 416 static gchar* msim_compute_login_response(guchar nonce[2*NONCE_SIZE],
255 gchar* email, gchar* password) 417 gchar* email, gchar* password)
256 { 418 {
257 PurpleCipherContext *key_context; 419 PurpleCipherContext *key_context;
258 PurpleCipher *sha1; 420 PurpleCipher *sha1;
259 PurpleCipherContext *rc4; 421 PurpleCipherContext *rc4;
265 gchar* response; 427 gchar* response;
266 size_t data_len, data_out_len; 428 size_t data_len, data_out_len;
267 gsize conv_bytes_read, conv_bytes_written; 429 gsize conv_bytes_read, conv_bytes_written;
268 GError* conv_error; 430 GError* conv_error;
269 431
270 //memset(nonce, 0, NONCE_HALF_SIZE); 432 //memset(nonce, 0, NONCE_SIZE);
271 //memset(nonce + NONCE_HALF_SIZE, 1, NONCE_HALF_SIZE); 433 //memset(nonce + NONCE_SIZE, 1, NONCE_SIZE);
272 434
273 /* Convert ASCII password to UTF16 little endian */ 435 /* Convert ASCII password to UTF16 little endian */
274 purple_debug_info("msim", "converting password to UTF-16LE\n"); 436 purple_debug_info("msim", "converting password to UTF-16LE\n");
275 conv_error = NULL; 437 conv_error = NULL;
276 password_utf16le = g_convert(password, -1, "UTF-16LE", "UTF-8", 438 password_utf16le = g_convert(password, -1, "UTF-16LE", "UTF-8",
282 "g_convert password UTF8->UTF16LE failed: %s", 444 "g_convert password UTF8->UTF16LE failed: %s",
283 conv_error->message); 445 conv_error->message);
284 g_error_free(conv_error); 446 g_error_free(conv_error);
285 } 447 }
286 448
287 #if 0
288 password_utf16le = g_new0(gchar, strlen(password) * 2);
289 for (i = 0; i < strlen(password) * 2; i += 2)
290 {
291 password_utf16le[i] = password[i / 2];
292 password_utf16le[i + 1] = 0;
293 }
294 #endif
295
296 /* Compute password hash */ 449 /* Compute password hash */
297 purple_cipher_digest_region("sha1", (guchar*)password_utf16le, 450 purple_cipher_digest_region("sha1", (guchar*)password_utf16le,
298 conv_bytes_written, sizeof(hash_pw), hash_pw, NULL); 451 conv_bytes_written, sizeof(hash_pw), hash_pw, NULL);
299 g_free(password_utf16le); 452 g_free(password_utf16le);
300 453
307 460
308 /* key = sha1(sha1(pw) + nonce2) */ 461 /* key = sha1(sha1(pw) + nonce2) */
309 sha1 = purple_ciphers_find_cipher("sha1"); 462 sha1 = purple_ciphers_find_cipher("sha1");
310 key_context = purple_cipher_context_new(sha1, NULL); 463 key_context = purple_cipher_context_new(sha1, NULL);
311 purple_cipher_context_append(key_context, hash_pw, HASH_SIZE); 464 purple_cipher_context_append(key_context, hash_pw, HASH_SIZE);
312 purple_cipher_context_append(key_context, nonce + NONCE_HALF_SIZE, NONCE_HALF_SIZE); 465 purple_cipher_context_append(key_context, nonce + NONCE_SIZE, NONCE_SIZE);
313 purple_cipher_context_digest(key_context, sizeof(key), key, NULL); 466 purple_cipher_context_digest(key_context, sizeof(key), key, NULL);
314 467
315 #ifdef MSIM_DEBUG_LOGIN_CHALLENGE 468 #ifdef MSIM_DEBUG_LOGIN_CHALLENGE
316 printf("key = "); 469 printf("key = ");
317 for (i = 0; i < sizeof(key); i++) 470 for (i = 0; i < sizeof(key); i++)
335 * anyways except make note of that fact. Probably important for any 488 * anyways except make note of that fact. Probably important for any
336 * kind of direct connection, or file transfer functionality. 489 * kind of direct connection, or file transfer functionality.
337 */ 490 */
338 /* rc4 encrypt: 491 /* rc4 encrypt:
339 * nonce1+email+IP list */ 492 * nonce1+email+IP list */
340 data_len = NONCE_HALF_SIZE + strlen(email) + 25; 493 data_len = NONCE_SIZE + strlen(email) + 25;
341 data = g_new0(guchar, data_len); 494 data = g_new0(guchar, data_len);
342 memcpy(data, nonce, NONCE_HALF_SIZE); 495 memcpy(data, nonce, NONCE_SIZE);
343 memcpy(data + NONCE_HALF_SIZE, email, strlen(email)); 496 memcpy(data + NONCE_SIZE, email, strlen(email));
344 memcpy(data + NONCE_HALF_SIZE + strlen(email), 497 memcpy(data + NONCE_SIZE + strlen(email),
345 /* IP addresses of network interfaces */ 498 /* IP addresses of network interfaces */
346 "\x00\x00\x00\x00\x05\x7f\x00\x00\x01\x00\x00\x00\x00\x0a\x00\x00\x40\xc0\xa8\x58\x01\xc0\xa8\x3c\x01", 25); 499 "\x00\x00\x00\x00\x05\x7f\x00\x00\x01\x00\x00\x00\x00\x0a\x00\x00\x40\xc0\xa8\x58\x01\xc0\xa8\x3c\x01", 25);
347 500
348 data_out = g_new0(guchar, data_len); 501 data_out = g_new0(guchar, data_len);
349 purple_cipher_context_encrypt(rc4, (const guchar*)data, 502 purple_cipher_context_encrypt(rc4, (const guchar*)data,
357 printf("response=<%s>\n", response); 510 printf("response=<%s>\n", response);
358 #endif 511 #endif
359 512
360 return response; 513 return response;
361 } 514 }
362
363 static void print_hash_item(gpointer key, gpointer value, gpointer user_data)
364 {
365 printf("%s=%s\n", (char*)key, (char*)value);
366 }
367
368 /**
369 * Send an arbitrary protocol message.
370 *
371 * @param session
372 * @param msg The textual, encoded message to send.
373 *
374 * Note: this does not send instant messages. For that, see msim_send_im.
375 */
376 static void msim_send(MsimSession *session, const gchar *msg)
377 {
378 int ret;
379
380 g_return_if_fail(MSIM_SESSION_VALID(session));
381 g_return_if_fail(msg != NULL);
382
383 purple_debug_info("msim", "msim_send: writing <%s>\n", msg);
384
385 ret = send(session->fd, msg, strlen(msg), 0);
386
387 if (ret != strlen(msg))
388 {
389 purple_debug_info("msim",
390 "msim_send(%s): strlen=%d, but only wrote %s\n",
391 msg, strlen(msg), ret);
392 /* TODO: better error */
393 }
394 }
395
396 /**
397 * Process a login challenge, sending a response.
398 *
399 * @param session
400 * @param table Hash table of login challenge message.
401 *
402 * @return 0, since the 'table' parameter is no longer needed.
403 */
404 static int msim_login_challenge(MsimSession *session, GHashTable *table)
405 {
406 PurpleAccount *account;
407 gchar *nc_str;
408 guchar *nc;
409 gchar *response_str;
410 gsize nc_len;
411 gchar *buf;
412
413 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0);
414 g_return_val_if_fail(table != NULL, 0);
415
416 nc_str = g_hash_table_lookup(table, "nc");
417
418 account = session->account;
419 //assert(account);
420
421 purple_connection_update_progress(session->gc, "Reading challenge", 1, 4);
422
423 purple_debug_info("msim", "nc=<%s>\n", nc_str);
424
425 nc = (guchar*)purple_base64_decode(nc_str, &nc_len);
426 purple_debug_info("msim", "base64 decoded to %d bytes\n", nc_len);
427 if (nc_len != 0x40)
428 {
429 purple_debug_info("msim", "bad nc length: %x != 0x40\n", nc_len);
430 purple_connection_error(session->gc, "Unexpected challenge length from server");
431 return 0;
432 }
433
434 purple_connection_update_progress(session->gc, "Logging in", 2, 4);
435
436 printf("going to compute login response\n");
437 //response_str = msim_compute_login_response(nc_str, "testuser", "testpw"); //session->gc->account->username, session->gc->account->password);
438 response_str = msim_compute_login_response(nc, account->username, account->password);
439 printf("got back login response\n");
440
441 g_free(nc);
442
443 /* Reply */
444 buf = g_strdup_printf("\\login2\\%d\\username\\%s\\response\\%s\\clientver\\%d\\reconn\\%d\\status\\%d\\id\\1\\final\\",
445 196610, account->username, response_str, MSIM_CLIENT_VERSION, 0, 100);
446
447 g_free(response_str);
448
449 purple_debug_info("msim", "response=<%s>\n", buf);
450
451 msim_send(session, buf);
452
453 g_free(buf);
454
455 return 0;
456 }
457
458 /**
459 * Parse a \x1c-separated "dictionary" of key=value pairs into a hash table.
460 *
461 * @param body_str The text of the dictionary to parse. Often the
462 * value for the 'body' field.
463 *
464 * @return Hash table of the keys and values. Must g_hash_table_destroy() when done.
465 */
466 static GHashTable *msim_parse_body(const gchar *body_str)
467 {
468 GHashTable *table;
469 gchar *item;
470 gchar **items;
471 gchar **elements;
472 guint i;
473
474 g_return_val_if_fail(body_str != NULL, NULL);
475
476 table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
477
478 for (items = g_strsplit(body_str, "\x1c", 0), i = 0;
479 (item = items[i]);
480 i++)
481 {
482 gchar *key, *value;
483
484 //printf("TOK=<%s>\n", token);
485 elements = g_strsplit(item, "=", 2);
486
487 key = elements[0];
488 if (!key)
489 {
490 purple_debug_info("msim", "msim_parse_body(%s): null key\n",
491 body_str);
492 g_strfreev(elements);
493 break;
494 }
495
496 value = elements[1];
497 if (!value)
498 {
499 purple_debug_info("msim", "msim_parse_body(%s): null value\n",
500 body_str);
501 g_strfreev(elements);
502 break;
503 }
504
505 //printf("-- %s: %s\n", key, value);
506
507 /* XXX: This overwrites duplicates. */
508 /* TODO: make the GHashTable values be GList's, and append to the list if
509 * there is already a value of the same key name. This is important for
510 * the WebChallenge message. */
511 g_hash_table_insert(table, g_strdup(key), g_strdup(value));
512
513 g_strfreev(elements);
514 }
515
516 g_strfreev(items);
517
518 return table;
519 }
520
521 /**
522 * Immediately send an IM to a user, by their numeric user ID.
523 *
524 * @param session
525 * @param userid ASCII numeric userid.
526 * @param message Text of message to send.
527 * @param flags Purple instant message flags.
528 *
529 * @return 0, since the 'table' parameter is no longer needed.
530 *
531 */
532 static int msim_send_im_by_userid(MsimSession *session, const gchar *userid, const gchar *message, PurpleMessageFlags flags)
533 {
534 gchar *msg_string;
535
536 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0);
537 g_return_val_if_fail(userid != NULL, 0);
538 g_return_val_if_fail(msim_is_userid(userid) == TRUE, 0);
539 g_return_val_if_fail(message != NULL, 0);
540
541 /* TODO: constants for bm types */
542 msg_string = g_strdup_printf("\\bm\\121\\sesskey\\%s\\t\\%s\\cv\\%d\\msg\\%s\\final\\",
543 session->sesskey, userid, MSIM_CLIENT_VERSION, message);
544
545 purple_debug_info("msim", "going to write: %s\n", msg_string);
546
547 msim_send(session, msg_string);
548
549 /* TODO: notify Purple that we sent the IM. */
550
551 /* Not needed since sending messages to yourself is allowed by MSIM! */
552 /*if (strcmp(from_username, who) == 0)
553 serv_got_im(gc, from_username, message, PURPLE_MESSAGE_RECV, time(NULL));
554 */
555
556 return 0;
557 }
558
559
560 /**
561 * Callback called when ready to send an IM by userid (the userid has been looked up).
562 * Calls msim_send_im_by_userid.
563 *
564 * @param session
565 * @param userinfo User info message from server containing a 'body' field
566 * with a 'UserID' key. This is where the user ID is taken from.
567 * @param data A send_im_cb_struct* of information on the IM to send.
568 *
569 */
570 static void msim_send_im_by_userid_cb(MsimSession *session, GHashTable *userinfo, gpointer data)
571 {
572 send_im_cb_struct *s;
573 gchar *userid;
574 GHashTable *body;
575
576 g_return_if_fail(MSIM_SESSION_VALID(session));
577 g_return_if_fail(userinfo != NULL);
578
579 body = msim_parse_body(g_hash_table_lookup(userinfo, "body"));
580 g_assert(body);
581
582 userid = g_hash_table_lookup(body, "UserID");
583
584 s = (send_im_cb_struct*)data;
585 msim_send_im_by_userid(session, userid, s->message, s->flags);
586
587 g_hash_table_destroy(body);
588 g_hash_table_destroy(userinfo);
589 g_free(s->message);
590 g_free(s->who);
591 }
592
593 /**
594 * Process a message reply from the server.
595 *
596 * @param session
597 * @param table Message reply from server.
598 *
599 * @return 0, since the 'table' field is no longer needed.
600 */
601 static int msim_process_reply(MsimSession *session, GHashTable *table)
602 {
603 gchar *rid_str;
604
605 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0);
606 g_return_val_if_fail(table != NULL, 0);
607
608 rid_str = g_hash_table_lookup(table, "rid");
609
610 if (rid_str) /* msim_lookup_user sets callback for here */
611 {
612 MSIM_USER_LOOKUP_CB cb;
613 gpointer data;
614 guint rid;
615
616 GHashTable *body;
617 gchar *username;
618
619 rid = atol(rid_str);
620
621 /* Cache the user info. Currently, the GHashTable of user info in
622 * this cache never expires so is never freed. TODO: expire and destroy
623 *
624 * Some information never changes (username->userid map), some does.
625 * TODO: Cache what doesn't change only
626 */
627 body = msim_parse_body(g_hash_table_lookup(table, "body"));
628 username = g_hash_table_lookup(body, "UserName");
629 if (username)
630 {
631 g_hash_table_insert(session->user_lookup_cache, g_strdup(username), body);
632 } else {
633 purple_debug_info("msim",
634 "msim_process_reply: not caching <%s>, no UserName\n",
635 g_hash_table_lookup(table, "body"));
636 }
637
638 /* If a callback is registered for this userid lookup, call it. */
639
640 cb = g_hash_table_lookup(session->user_lookup_cb, GUINT_TO_POINTER(rid));
641 data = g_hash_table_lookup(session->user_lookup_cb_data, GUINT_TO_POINTER(rid));
642
643 if (cb)
644 {
645 purple_debug_info("msim",
646 "msim_process_body: calling callback now\n");
647 cb(session, table, data);
648 g_hash_table_remove(session->user_lookup_cb, GUINT_TO_POINTER(rid));
649 g_hash_table_remove(session->user_lookup_cb_data, GUINT_TO_POINTER(rid));
650
651 /* Return 1 to tell caller of msim_process (msim_input_cb) to
652 * not destroy 'table'; allow 'cb' to hang on to it and destroy
653 * it when it wants. */
654 return 1;
655 } else {
656 purple_debug_info("msim",
657 "msim_process_body: no callback for rid %d\n", rid);
658 }
659 }
660 return 0;
661 }
662
663 /**
664 * Handle an error from the server.
665 *
666 * @param session
667 * @param table The message.
668 *
669 * @return 0, since 'table' can be freed.
670 */
671 static int msim_error(MsimSession *session, GHashTable *table)
672 {
673 gchar *err, *errmsg, *full_errmsg;
674
675 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0);
676 g_return_val_if_fail(table != NULL, 0);
677
678 err = g_hash_table_lookup(table, "err");
679 errmsg = g_hash_table_lookup(table, "errmsg");
680
681 full_errmsg = g_strdup_printf("Protocol error, code %s: %s", err, errmsg);
682
683 purple_debug_info("msim", "msim_error: %s\n", full_errmsg);
684
685 /* TODO: check 'fatal' and die if asked to.
686 * TODO: do something with the error # (localization of errmsg?) */
687 purple_notify_error(session->account, g_strdup("MySpaceIM Error"),
688 full_errmsg, NULL);
689
690 if (g_hash_table_lookup(table, "fatal"))
691 {
692 purple_debug_info("msim", "fatal error, destroy session\n");
693 purple_connection_error(session->gc, full_errmsg);
694 close(session->fd);
695 //msim_session_destroy(session);
696 }
697
698 return 0;
699 }
700
701 /**
702 * Callback to handle incoming messages, after resolving userid.
703 *
704 * @param session
705 * @param userinfo Message from server on user's info, containing UserName.
706 * @param data A gchar* of the incoming instant message's text.
707 */
708 static void msim_incoming_im_cb(MsimSession *session, GHashTable *userinfo, gpointer data)
709 {
710 gchar *msg;
711 gchar *username;
712 GHashTable *body;
713
714 g_return_if_fail(MSIM_SESSION_VALID(session));
715 g_return_if_fail(userinfo != NULL);
716
717 body = msim_parse_body(g_hash_table_lookup(userinfo, "body"));
718 g_assert(body != NULL);
719
720 username = g_hash_table_lookup(body, "UserName");
721
722 msg = (gchar*)data;
723 serv_got_im(session->gc, username, msg, PURPLE_MESSAGE_RECV, time(NULL));
724
725 g_hash_table_destroy(body);
726 g_hash_table_destroy(userinfo);
727 }
728
729 /**
730 * Handle an incoming message.
731 *
732 * @param session The session
733 * @param table Message from the server, containing 'f' (userid from) and 'msg'.
734 *
735 * @return 0, since table can be freed.
736 */
737 static int msim_incoming_im(MsimSession *session, GHashTable *table)
738 {
739 gchar *userid;
740 gchar *msg;
741
742 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0);
743 g_return_val_if_fail(table != NULL, 0);
744
745
746 userid = g_hash_table_lookup(table, "f");
747 msg = g_hash_table_lookup(table, "msg");
748
749 purple_debug_info("msim",
750 "msim_incoming_im: got msg <%s> from <%s>, resolving username\n",
751 msg, userid);
752
753 msim_lookup_user(session, userid, msim_incoming_im_cb, g_strdup(msg));
754
755 return 0;
756 }
757
758 #if 0
759 /* Not sure about this */
760 static void msim_status_now(gchar *who, gpointer data)
761 {
762 printf("msim_status_now: %s\n", who);
763 }
764 #endif
765
766 /**
767 * Callback to update incoming status messages, after looked up username.
768 *
769 * @param session
770 * @param userinfo Looked up user information from server.
771 * @param data gchar* status string.
772 *
773 */
774 static void msim_status_cb(MsimSession *session, GHashTable *userinfo, gpointer data)
775 {
776 PurpleBuddyList *blist;
777 PurpleBuddy *buddy;
778 PurplePresence *presence;
779 GHashTable *body;
780 //PurpleStatus *status;
781 gchar **status_array;
782 GList *list;
783 gchar *status_text, *status_code;
784 gchar *status_str;
785 gint i;
786 gchar *username;
787
788 g_return_if_fail(MSIM_SESSION_VALID(session));
789 g_return_if_fail(userinfo != NULL);
790
791 status_str = (gchar*)data;
792
793 body = msim_parse_body(g_hash_table_lookup(userinfo, "body"));
794 g_assert(body);
795
796 username = g_hash_table_lookup(body, "UserName");
797 /* Note: DisplayName doesn't seem to be resolvable. It could be displayed on
798 * the buddy list, if the UserID was stored along with it. */
799
800 if (!username)
801 {
802 purple_debug_info("msim", "msim_status_cb: no username?!\n");
803 return;
804 }
805
806 purple_debug_info("msim",
807 "msim_status_cb: updating status for <%s> to <%s>\n",
808 username, status_str);
809
810 /* TODO: generic functions to split into a GList */
811 status_array = g_strsplit(status_str, "|", 0);
812 for (list = NULL, i = 0;
813 status_array[i];
814 i++)
815 {
816 list = g_list_append(list, status_array[i]);
817 }
818
819 /* Example fields: |s|0|ss|Offline */
820 status_code = g_list_nth_data(list, 2);
821 status_text = g_list_nth_data(list, 4);
822
823 blist = purple_get_blist();
824
825 /* Add buddy if not found */
826 buddy = purple_find_buddy(session->account, username);
827 if (!buddy)
828 {
829 /* TODO: purple aliases, userids and usernames */
830 purple_debug_info("msim",
831 "msim_status: making new buddy for %s\n", username);
832 buddy = purple_buddy_new(session->account, username, NULL);
833
834 /* TODO: sometimes (when click on it), buddy list disappears. Fix. */
835 purple_blist_add_buddy(buddy, NULL, NULL, NULL);
836 } else {
837 purple_debug_info("msim", "msim_status: found buddy %s\n", username);
838 }
839
840 /* For now, always set status to online.
841 * TODO: make status reflect reality
842 * TODO: show headline */
843 presence = purple_presence_new_for_buddy(buddy);
844 purple_presence_set_status_active(presence, MSIM_STATUS_ONLINE, TRUE);
845
846 g_strfreev(status_array);
847 g_list_free(list);
848 g_hash_table_destroy(body);
849 g_hash_table_destroy(userinfo);
850 /* Do not free status_str - it will be freed by g_hash_table_destroy on session->userid_lookup_cb_data */
851 }
852
853 /**
854 * Process incoming status messages.
855 *
856 * @param session
857 * @param table Status update message.
858 *
859 * @return 0, since 'table' can be freed.
860 */
861 static int msim_status(MsimSession *session, GHashTable *table)
862 {
863 gchar *status_str;
864 gchar *userid;
865
866 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0);
867 g_return_val_if_fail(table != NULL, 0);
868
869 status_str = g_hash_table_lookup(table, "msg");
870 if (!status_str)
871 {
872 purple_debug_info("msim", "msim_status: bm=100 but no status msg\n");
873 return 0;
874 }
875
876 userid = g_hash_table_lookup(table, "f");
877 if (!userid)
878 {
879 purple_debug_info("msim", "msim_status: bm=100 but no f field\n");
880 return 0;
881 }
882
883 /* TODO: if buddies were identified on buddy list by uid, wouldn't have to lookup
884 * before updating the status! Much more efficient. */
885 purple_debug_info("msim",
886 "msim_status: got status msg <%s> for <%s>, scheduling lookup\n",
887 status_str, userid);
888
889 /* Actually update status once obtain username */
890 msim_lookup_user(session, userid, msim_status_cb, g_strdup(status_str));
891
892 return 0;
893 }
894
895
896 /**
897 * Process a message.
898 *
899 * @param gc Connection.
900 * @param table Any message from the server.
901 *
902 * @return The return value of the function used to process the message, or -1 if
903 * called with invalid parameters.
904 */
905 static int msim_process(PurpleConnection *gc, GHashTable *table)
906 {
907 MsimSession *session;
908
909 g_return_val_if_fail(gc != NULL, -1);
910 g_return_val_if_fail(table != NULL, -1);
911
912 session = (MsimSession*)gc->proto_data;
913
914 printf("-------- message -------------\n");
915 g_hash_table_foreach(table, print_hash_item, NULL);
916 printf("------------------------------\n");
917
918 if (g_hash_table_lookup(table, "nc"))
919 {
920 return msim_login_challenge(session, table);
921 } else if (g_hash_table_lookup(table, "sesskey")) {
922 printf("SESSKEY=<%s>\n", (gchar*)g_hash_table_lookup(table, "sesskey"));
923
924 purple_connection_update_progress(gc, "Connected", 3, 4);
925
926 session->sesskey = g_strdup(g_hash_table_lookup(table, "sesskey"));
927
928 /* Comes with: proof,profileid,userid,uniquenick -- all same values
929 * (at least for me). */
930 session->userid = g_strdup(g_hash_table_lookup(table, "userid"));
931
932 purple_connection_set_state(gc, PURPLE_CONNECTED);
933
934 return 0;
935 } else if (g_hash_table_lookup(table, "bm")) {
936 guint bm;
937
938 bm = atoi(g_hash_table_lookup(table, "bm"));
939 switch (bm)
940 {
941 case MSIM_BM_STATUS:
942 return msim_status(session, table);
943 case MSIM_BM_INSTANT:
944 return msim_incoming_im(session, table);
945 default:
946 /* Not really an IM, but show it for informational
947 * purposes during development. */
948 return msim_incoming_im(session, table);
949 }
950
951 if (bm == MSIM_BM_STATUS)
952 {
953 return msim_status(session, table);
954 } else { /* else if strcmp(bm, "1") == 0) */
955 return msim_incoming_im(session, table);
956 }
957 } else if (g_hash_table_lookup(table, "rid")) {
958 return msim_process_reply(session, table);
959 } else if (g_hash_table_lookup(table, "error")) {
960 return msim_error(session, table);
961 } else if (g_hash_table_lookup(table, "ka")) {
962 purple_debug_info("msim", "msim_process: got keep alive\n");
963 return 0;
964 } else {
965 printf("<<unhandled>>\n");
966 return 0;
967 }
968 }
969
970 /**
971 * Callback when input available.
972 *
973 * @param gc_uncasted A PurpleConnection pointer.
974 * @param source File descriptor.
975 * @param cond PURPLE_INPUT_READ
976 *
977 * Reads the input, and dispatches calls msim_process to handle it.
978 */
979 static void msim_input_cb(gpointer gc_uncasted, gint source, PurpleInputCondition cond)
980 {
981 PurpleConnection *gc;
982 PurpleAccount *account;
983 MsimSession *session;
984 gchar *end;
985 int n;
986
987 g_return_if_fail(gc_uncasted != NULL);
988 g_return_if_fail(source >= 0); /* Note: 0 is a valid fd */
989
990 gc = (PurpleConnection*)(gc_uncasted);
991 account = purple_connection_get_account(gc);
992 session = gc->proto_data;
993
994 g_return_if_fail(MSIM_SESSION_VALID(session));
995
996 g_assert(cond == PURPLE_INPUT_READ);
997
998 /* Only can handle so much data at once...
999 * If this happens, try recompiling with a higher MSIM_READ_BUF_SIZE.
1000 * Should be large enough to hold the largest protocol message.
1001 */
1002 if (session->rxoff == MSIM_READ_BUF_SIZE)
1003 {
1004 purple_debug_error("msim", "msim_input_cb: %d-byte read buffer full!\n",
1005 MSIM_READ_BUF_SIZE);
1006 purple_connection_error(gc, "Read buffer full");
1007 /* TODO: fix 100% CPU after closing */
1008 close(source);
1009 return;
1010 }
1011
1012 purple_debug_info("msim", "buffer at %d (max %d), reading up to %d\n",
1013 session->rxoff, MSIM_READ_BUF_SIZE,
1014 MSIM_READ_BUF_SIZE - session->rxoff);
1015
1016 /* Read into buffer. On Win32, need recv() not read(). session->fd also holds
1017 * the file descriptor, but it sometimes differs from the 'source' parameter.
1018 */
1019 n = recv(session->fd, session->rxbuf + session->rxoff, MSIM_READ_BUF_SIZE - session->rxoff, 0);
1020
1021 if (n < 0 && errno == EAGAIN)
1022 {
1023 return;
1024 }
1025 else if (n < 0)
1026 {
1027 purple_connection_error(gc, "Read error");
1028 purple_debug_error("msim", "msim_input_cb: read error, ret=%d, "
1029 "error=%s, source=%d, fd=%d (%X))\n",
1030 n, strerror(errno), source, session->fd, session->fd);
1031 close(source);
1032 return;
1033 }
1034 else if (n == 0)
1035 {
1036 purple_debug_info("msim", "msim_input_cb: server disconnected\n");
1037 purple_connection_error(gc, "Server has disconnected");
1038 return;
1039 }
1040
1041 /* Null terminate */
1042 session->rxbuf[session->rxoff + n] = 0;
1043
1044 /* Check for embedded NULs. I don't handle them, and they shouldn't occur. */
1045 if (strlen(session->rxbuf + session->rxoff) != n)
1046 {
1047 /* Occurs after login, but it is not a null byte. */
1048 purple_debug_info("msim", "msim_input_cb: strlen=%d, but read %d bytes"
1049 "--null byte encountered?\n",
1050 strlen(session->rxbuf + session->rxoff), n);
1051 //purple_connection_error(gc, "Invalid message - null byte on input");
1052 return;
1053 }
1054
1055 session->rxoff += n;
1056 purple_debug_info("msim", "msim_input_cb: read=%d\n", n);
1057
1058 //printf("buf=<%s>\n", session->rxbuf);
1059
1060 /* Look for \\final\\ end markers. If found, process message. */
1061 while((end = strstr(session->rxbuf, MSIM_FINAL_STRING)))
1062 {
1063 GHashTable *table;
1064
1065 //printf("in loop: buf=<%s>\n", session->rxbuf);
1066 *end = 0;
1067 table = msim_parse(g_strdup(session->rxbuf));
1068 if (!table)
1069 {
1070 purple_debug_info("msim", "msim_input_cb: couldn't parse <%s>\n",
1071 session->rxbuf);
1072 purple_connection_error(gc, "Unparseable message");
1073 }
1074 else
1075 {
1076 /* Process message. Returns 0 to free */
1077 if (msim_process(gc, table) == 0)
1078 g_hash_table_destroy(table);
1079 }
1080
1081 /* Move remaining part of buffer to beginning. */
1082 session->rxoff -= strlen(session->rxbuf) + strlen(MSIM_FINAL_STRING);
1083 memmove(session->rxbuf, end + strlen(MSIM_FINAL_STRING),
1084 MSIM_READ_BUF_SIZE - (end + strlen(MSIM_FINAL_STRING) - session->rxbuf));
1085
1086 /* Clear end of buffer */
1087 //memset(end, 0, MSIM_READ_BUF_SIZE - (end - session->rxbuf));
1088 }
1089 }
1090
1091 /**
1092 * Callback when connected. Sets up input handlers.
1093 *
1094 * @param data A PurpleConnection pointer.
1095 * @param source File descriptor.
1096 * @param error_message
1097 */
1098 static void msim_connect_cb(gpointer data, gint source, const gchar *error_message)
1099 {
1100 PurpleConnection *gc;
1101 MsimSession *session;
1102
1103 g_return_if_fail(data != NULL);
1104
1105 gc = (PurpleConnection*)data;
1106 session = gc->proto_data;
1107
1108 if (source < 0)
1109 {
1110 purple_connection_error(gc, "Couldn't connect to host");
1111 purple_connection_error(gc, g_strdup_printf("Couldn't connect to host: %s (%d)",
1112 error_message, source));
1113 return;
1114 }
1115
1116 session->fd = source;
1117
1118 gc->inpa = purple_input_add(source, PURPLE_INPUT_READ, msim_input_cb, gc);
1119 }
1120
1121 /* Session methods */
1122
1123 /**
1124 * Create a new MSIM session.
1125 *
1126 * @param acct The account to create the session from.
1127 *
1128 * @return Pointer to a new session. Free with msim_session_destroy.
1129 */
1130 static MsimSession *msim_session_new(PurpleAccount *acct)
1131 {
1132 MsimSession *session;
1133
1134 g_return_val_if_fail(acct != NULL, NULL);
1135
1136 session = g_new0(MsimSession, 1);
1137
1138 session->magic = MSIM_SESSION_STRUCT_MAGIC;
1139 session->account = acct;
1140 session->gc = purple_account_get_connection(acct);
1141 session->fd = -1;
1142 session->user_lookup_cb = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, NULL); /* do NOT free function pointers! */
1143 session->user_lookup_cb_data = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free);
1144 session->user_lookup_cache = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_hash_table_destroy);
1145 session->rxoff = 0;
1146 session->rxbuf = g_new0(gchar, MSIM_READ_BUF_SIZE);
1147
1148 return session;
1149 }
1150
1151 /**
1152 * Free a session.
1153 *
1154 * @param session The session to destroy.
1155 */
1156 static void msim_session_destroy(MsimSession *session)
1157 {
1158 g_return_if_fail(MSIM_SESSION_VALID(session));
1159
1160 session->magic = -1;
1161
1162 g_free(session->rxbuf);
1163 g_free(session->userid);
1164 g_free(session->sesskey);
1165
1166 g_free(session);
1167 }
1168
1169 /**
1170 * Start logging in to the MSIM servers.
1171 *
1172 * @param acct Account information to use to login.
1173 */
1174 static void msim_login(PurpleAccount *acct)
1175 {
1176 PurpleConnection *gc;
1177 const char *host;
1178 int port;
1179
1180 g_return_if_fail(acct != NULL);
1181
1182 purple_debug_info("myspace", "logging in %s\n", acct->username);
1183
1184 gc = purple_account_get_connection(acct);
1185 gc->proto_data = msim_session_new(acct);
1186
1187 /* 1. connect to server */
1188 purple_connection_update_progress(gc, "Connecting",
1189 0, /* which connection step this is */
1190 4); /* total number of steps */
1191
1192 /* TODO: GUI option to be user-modifiable. */
1193 host = purple_account_get_string(acct, "server", MSIM_SERVER);
1194 port = purple_account_get_int(acct, "port", MSIM_PORT);
1195 /* TODO: connect */
1196 /* From purple.sf.net/api:
1197 * """Note that this function name can be misleading--although it is called
1198 * "proxy connect," it is used for establishing any outgoing TCP connection,
1199 * whether through a proxy or not.""" */
1200
1201 /* Calls msim_connect_cb when connected. */
1202 if (purple_proxy_connect(gc, acct, host, port, msim_connect_cb, gc) == NULL)
1203 {
1204 /* TODO: try other ports if in auto mode, then save
1205 * working port and try that first next time. */
1206 purple_connection_error(gc, "Couldn't create socket");
1207 return;
1208 }
1209
1210 }
1211
1212
1213 /**
1214 * Close the connection.
1215 *
1216 * @param gc The connection.
1217 */
1218 static void msim_close(PurpleConnection *gc)
1219 {
1220 g_return_if_fail(gc != NULL);
1221
1222 msim_session_destroy(gc->proto_data);
1223 }
1224
1225 /**
1226 * Return the icon name for a buddy and account.
1227 *
1228 * @param acct The account to find the icon for.
1229 * @param buddy The buddy to find the icon for, or NULL for the accoun icon.
1230 *
1231 * @return The base icon name string.
1232 */
1233 static const gchar *msim_list_icon(PurpleAccount *acct, PurpleBuddy *buddy)
1234 {
1235 /* TODO: use a MySpace icon. hbons submitted one to
1236 * http://developer.pidgin.im/wiki/MySpaceIM - tried placing in
1237 * C:\cygwin\home\Jeff\purple-2.0.0beta6\gtk\pixmaps\status\default
1238 * and returning "myspace" but icon shows up blank.
1239 */
1240 if (acct == NULL)
1241 {
1242 purple_debug_info("msim", "msim_list_icon: acct == NULL!\n");
1243 //exit(-2);
1244 }
1245 return "meanwhile";
1246 }
1247
1248 /**
1249 * Check if a string is a userid (all numeric).
1250 *
1251 * @param user The user id, email, or name.
1252 *
1253 * @return TRUE if is userid, FALSE if not.
1254 */
1255 static inline gboolean msim_is_userid(const gchar *user)
1256 {
1257 g_return_val_if_fail(user != NULL, FALSE);
1258
1259 return strspn(user, "0123456789") == strlen(user);
1260 }
1261
1262 /**
1263 * Check if a string is an email address (contains an @).
1264 *
1265 * @param user The user id, email, or name.
1266 *
1267 * @return TRUE if is an email, FALSE if not.
1268 *
1269 * This function is not intended to be used as a generic
1270 * means of validating email addresses, but to distinguish
1271 * between a user represented by an email address from
1272 * other forms of identification.
1273 */
1274 static inline gboolean msim_is_email(const gchar *user)
1275 {
1276 g_return_val_if_fail(user != NULL, FALSE);
1277
1278 return strchr(user, '@') != NULL;
1279 }
1280
1281
1282 /**
1283 * Asynchronously lookup user information, calling callback when receive result.
1284 *
1285 * @param session
1286 * @param user The user id, email address, or username.
1287 * @param cb Callback, called with user information when available.
1288 * @param data An arbitray data pointer passed to the callback.
1289 */
1290 static void msim_lookup_user(MsimSession *session, const gchar *user, MSIM_USER_LOOKUP_CB cb, gpointer data)
1291 {
1292 gchar *field_name;
1293 gchar *msg_string;
1294 guint rid, cmd, dsn, lid;
1295
1296 g_return_if_fail(MSIM_SESSION_VALID(session));
1297 g_return_if_fail(user != NULL);
1298 g_return_if_fail(cb != NULL);
1299
1300 purple_debug_info("msim", "msim_lookup_userid",
1301 "asynchronously looking up <%s>\n", user);
1302
1303 /* TODO: check if this user's info was cached and fresh; if so return immediately */
1304 #if 0
1305 /* If already know userid, then call callback immediately */
1306 cached_userid = g_hash_table_lookup(session->userid_cache, who);
1307 if (cached_userid && !by_userid)
1308 {
1309 cb(cached_userid, NULL, NULL, data);
1310 return;
1311 }
1312 #endif
1313
1314 rid = rand(); //om();
1315
1316 /* Setup callback. Response will be associated with request using 'rid'. */
1317 g_hash_table_insert(session->user_lookup_cb, GUINT_TO_POINTER(rid), cb);
1318 g_hash_table_insert(session->user_lookup_cb_data, GUINT_TO_POINTER(rid), data);
1319
1320 /* Send request */
1321
1322 cmd = 1;
1323
1324 if (msim_is_userid(user))
1325 {
1326 /* TODO: document cmd,dsn,lid */
1327 field_name = "UserID";
1328 dsn = 4;
1329 lid = 3;
1330 } else if (msim_is_email(user)) {
1331 field_name = "Email";
1332 dsn = 5;
1333 lid = 7;
1334 } else {
1335 field_name = "UserName";
1336 dsn = 5;
1337 lid = 7;
1338 }
1339
1340 msg_string = g_strdup_printf("\\persist\\1\\sesskey\\%s\\cmd\\1\\dsn\\%d\\uid\\%s\\lid\\%d\\rid\\%d\\body\\%s=%s\\final\\",
1341 session->sesskey, dsn, session->userid, lid, rid, field_name, user);
1342
1343 msim_send(session, msg_string);
1344 }
1345 515
1346 /** 516 /**
1347 * Schedule an IM to be sent once the user ID is looked up. 517 * Schedule an IM to be sent once the user ID is looked up.
1348 * 518 *
1349 * @param gc Connection. 519 * @param gc Connection.
1417 * TODO: Make the sent IM's appear as from the user's username, instead of 587 * TODO: Make the sent IM's appear as from the user's username, instead of
1418 * their email address. Purple uses the login (in MSIM, the email)--change this. 588 * their email address. Purple uses the login (in MSIM, the email)--change this.
1419 */ 589 */
1420 return 1; 590 return 1;
1421 } 591 }
592
593 /**
594 * Immediately send an IM to a user, by their numeric user ID.
595 *
596 * @param session
597 * @param userid ASCII numeric userid.
598 * @param message Text of message to send.
599 * @param flags Purple instant message flags.
600 *
601 * @return 0, since the 'table' parameter is no longer needed.
602 *
603 */
604 static int msim_send_im_by_userid(MsimSession *session, const gchar *userid, const gchar *message, PurpleMessageFlags flags)
605 {
606 gchar *msg_string;
607
608 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0);
609 g_return_val_if_fail(userid != NULL, 0);
610 g_return_val_if_fail(msim_is_userid(userid) == TRUE, 0);
611 g_return_val_if_fail(message != NULL, 0);
612
613 /* TODO: constants for bm types */
614 msg_string = g_strdup_printf("\\bm\\121\\sesskey\\%s\\t\\%s\\cv\\%d\\msg\\%s\\final\\",
615 session->sesskey, userid, MSIM_CLIENT_VERSION, message);
616
617 purple_debug_info("msim", "going to write: %s\n", msg_string);
618
619 msim_send(session, msg_string);
620
621 /* TODO: notify Purple that we sent the IM. */
622
623 /* Not needed since sending messages to yourself is allowed by MSIM! */
624 /*if (strcmp(from_username, who) == 0)
625 serv_got_im(gc, from_username, message, PURPLE_MESSAGE_RECV, time(NULL));
626 */
627
628 return 0;
629 }
630
631
632 /**
633 * Callback called when ready to send an IM by userid (the userid has been looked up).
634 * Calls msim_send_im_by_userid.
635 *
636 * @param session
637 * @param userinfo User info message from server containing a 'body' field
638 * with a 'UserID' key. This is where the user ID is taken from.
639 * @param data A send_im_cb_struct* of information on the IM to send.
640 *
641 */
642 static void msim_send_im_by_userid_cb(MsimSession *session, GHashTable *userinfo, gpointer data)
643 {
644 send_im_cb_struct *s;
645 gchar *userid;
646 GHashTable *body;
647
648 g_return_if_fail(MSIM_SESSION_VALID(session));
649 g_return_if_fail(userinfo != NULL);
650
651 body = msim_parse_body(g_hash_table_lookup(userinfo, "body"));
652 g_assert(body);
653
654 userid = g_hash_table_lookup(body, "UserID");
655
656 s = (send_im_cb_struct*)data;
657 msim_send_im_by_userid(session, userid, s->message, s->flags);
658
659 g_hash_table_destroy(body);
660 g_hash_table_destroy(userinfo);
661 g_free(s->message);
662 g_free(s->who);
663 }
664
665 /**
666 * Callback to handle incoming messages, after resolving userid.
667 *
668 * @param session
669 * @param userinfo Message from server on user's info, containing UserName.
670 * @param data A gchar* of the incoming instant message's text.
671 */
672 static void msim_incoming_im_cb(MsimSession *session, GHashTable *userinfo, gpointer data)
673 {
674 gchar *msg;
675 gchar *username;
676 GHashTable *body;
677
678 g_return_if_fail(MSIM_SESSION_VALID(session));
679 g_return_if_fail(userinfo != NULL);
680
681 body = msim_parse_body(g_hash_table_lookup(userinfo, "body"));
682 g_assert(body != NULL);
683
684 username = g_hash_table_lookup(body, "UserName");
685
686 msg = (gchar*)data;
687 serv_got_im(session->gc, username, msg, PURPLE_MESSAGE_RECV, time(NULL));
688
689 g_hash_table_destroy(body);
690 g_hash_table_destroy(userinfo);
691 }
692
693 /**
694 * Handle an incoming message.
695 *
696 * @param session The session
697 * @param table Message from the server, containing 'f' (userid from) and 'msg'.
698 *
699 * @return 0, since table can be freed.
700 */
701 static int msim_incoming_im(MsimSession *session, GHashTable *table)
702 {
703 gchar *userid;
704 gchar *msg;
705
706 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0);
707 g_return_val_if_fail(table != NULL, 0);
708
709
710 userid = g_hash_table_lookup(table, "f");
711 msg = g_hash_table_lookup(table, "msg");
712
713 purple_debug_info("msim",
714 "msim_incoming_im: got msg <%s> from <%s>, resolving username\n",
715 msg, userid);
716
717 msim_lookup_user(session, userid, msim_incoming_im_cb, g_strdup(msg));
718
719 return 0;
720 }
721
722
723 /**
724 * Process a message.
725 *
726 * @param gc Connection.
727 * @param table Any message from the server.
728 *
729 * @return The return value of the function used to process the message, or -1 if
730 * called with invalid parameters.
731 */
732 static int msim_process(PurpleConnection *gc, GHashTable *table)
733 {
734 MsimSession *session;
735
736 g_return_val_if_fail(gc != NULL, -1);
737 g_return_val_if_fail(table != NULL, -1);
738
739 session = (MsimSession*)gc->proto_data;
740
741 printf("-------- message -------------\n");
742 g_hash_table_foreach(table, print_hash_item, NULL);
743 printf("------------------------------\n");
744
745 if (g_hash_table_lookup(table, "nc"))
746 {
747 return msim_login_challenge(session, table);
748 } else if (g_hash_table_lookup(table, "sesskey")) {
749 printf("SESSKEY=<%s>\n", (gchar*)g_hash_table_lookup(table, "sesskey"));
750
751 purple_connection_update_progress(gc, "Connected", 3, 4);
752
753 session->sesskey = g_strdup(g_hash_table_lookup(table, "sesskey"));
754
755 /* Comes with: proof,profileid,userid,uniquenick -- all same values
756 * (at least for me). */
757 session->userid = g_strdup(g_hash_table_lookup(table, "userid"));
758
759 purple_connection_set_state(gc, PURPLE_CONNECTED);
760
761 return 0;
762 } else if (g_hash_table_lookup(table, "bm")) {
763 guint bm;
764
765 bm = atoi(g_hash_table_lookup(table, "bm"));
766 switch (bm)
767 {
768 case MSIM_BM_STATUS:
769 return msim_status(session, table);
770 case MSIM_BM_INSTANT:
771 return msim_incoming_im(session, table);
772 default:
773 /* Not really an IM, but show it for informational
774 * purposes during development. */
775 return msim_incoming_im(session, table);
776 }
777
778 if (bm == MSIM_BM_STATUS)
779 {
780 return msim_status(session, table);
781 } else { /* else if strcmp(bm, "1") == 0) */
782 return msim_incoming_im(session, table);
783 }
784 } else if (g_hash_table_lookup(table, "rid")) {
785 return msim_process_reply(session, table);
786 } else if (g_hash_table_lookup(table, "error")) {
787 return msim_error(session, table);
788 } else if (g_hash_table_lookup(table, "ka")) {
789 purple_debug_info("msim", "msim_process: got keep alive\n");
790 return 0;
791 } else {
792 printf("<<unhandled>>\n");
793 return 0;
794 }
795 }
796 /**
797 * Process a message reply from the server.
798 *
799 * @param session
800 * @param table Message reply from server.
801 *
802 * @return 0, since the 'table' field is no longer needed.
803 */
804 static int msim_process_reply(MsimSession *session, GHashTable *table)
805 {
806 gchar *rid_str;
807
808 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0);
809 g_return_val_if_fail(table != NULL, 0);
810
811 rid_str = g_hash_table_lookup(table, "rid");
812
813 if (rid_str) /* msim_lookup_user sets callback for here */
814 {
815 MSIM_USER_LOOKUP_CB cb;
816 gpointer data;
817 guint rid;
818
819 GHashTable *body;
820 gchar *username;
821
822 rid = atol(rid_str);
823
824 /* Cache the user info. Currently, the GHashTable of user info in
825 * this cache never expires so is never freed. TODO: expire and destroy
826 *
827 * Some information never changes (username->userid map), some does.
828 * TODO: Cache what doesn't change only
829 */
830 body = msim_parse_body(g_hash_table_lookup(table, "body"));
831 username = g_hash_table_lookup(body, "UserName");
832 if (username)
833 {
834 g_hash_table_insert(session->user_lookup_cache, g_strdup(username), body);
835 } else {
836 purple_debug_info("msim",
837 "msim_process_reply: not caching <%s>, no UserName\n",
838 g_hash_table_lookup(table, "body"));
839 }
840
841 /* If a callback is registered for this userid lookup, call it. */
842
843 cb = g_hash_table_lookup(session->user_lookup_cb, GUINT_TO_POINTER(rid));
844 data = g_hash_table_lookup(session->user_lookup_cb_data, GUINT_TO_POINTER(rid));
845
846 if (cb)
847 {
848 purple_debug_info("msim",
849 "msim_process_body: calling callback now\n");
850 cb(session, table, data);
851 g_hash_table_remove(session->user_lookup_cb, GUINT_TO_POINTER(rid));
852 g_hash_table_remove(session->user_lookup_cb_data, GUINT_TO_POINTER(rid));
853
854 /* Return 1 to tell caller of msim_process (msim_input_cb) to
855 * not destroy 'table'; allow 'cb' to hang on to it and destroy
856 * it when it wants. */
857 return 1;
858 } else {
859 purple_debug_info("msim",
860 "msim_process_body: no callback for rid %d\n", rid);
861 }
862 }
863 return 0;
864 }
865
866 /**
867 * Handle an error from the server.
868 *
869 * @param session
870 * @param table The message.
871 *
872 * @return 0, since 'table' can be freed.
873 */
874 static int msim_error(MsimSession *session, GHashTable *table)
875 {
876 gchar *err, *errmsg, *full_errmsg;
877
878 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0);
879 g_return_val_if_fail(table != NULL, 0);
880
881 err = g_hash_table_lookup(table, "err");
882 errmsg = g_hash_table_lookup(table, "errmsg");
883
884 full_errmsg = g_strdup_printf("Protocol error, code %s: %s", err, errmsg);
885
886 purple_debug_info("msim", "msim_error: %s\n", full_errmsg);
887
888 /* TODO: check 'fatal' and die if asked to.
889 * TODO: do something with the error # (localization of errmsg?) */
890 purple_notify_error(session->account, g_strdup("MySpaceIM Error"),
891 full_errmsg, NULL);
892
893 if (g_hash_table_lookup(table, "fatal"))
894 {
895 purple_debug_info("msim", "fatal error, destroy session\n");
896 purple_connection_error(session->gc, full_errmsg);
897 close(session->fd);
898 //msim_session_destroy(session);
899 }
900
901 return 0;
902 }
903
904 #if 0
905 /* Not sure about this */
906 static void msim_status_now(gchar *who, gpointer data)
907 {
908 printf("msim_status_now: %s\n", who);
909 }
910 #endif
911
912 /**
913 * Callback to update incoming status messages, after looked up username.
914 *
915 * @param session
916 * @param userinfo Looked up user information from server.
917 * @param data gchar* status string.
918 *
919 */
920 static void msim_status_cb(MsimSession *session, GHashTable *userinfo, gpointer data)
921 {
922 PurpleBuddyList *blist;
923 PurpleBuddy *buddy;
924 PurplePresence *presence;
925 GHashTable *body;
926 //PurpleStatus *status;
927 gchar **status_array;
928 GList *list;
929 gchar *status_text, *status_code;
930 gchar *status_str;
931 gint i;
932 gchar *username;
933
934 g_return_if_fail(MSIM_SESSION_VALID(session));
935 g_return_if_fail(userinfo != NULL);
936
937 status_str = (gchar*)data;
938
939 body = msim_parse_body(g_hash_table_lookup(userinfo, "body"));
940 g_assert(body);
941
942 username = g_hash_table_lookup(body, "UserName");
943 /* Note: DisplayName doesn't seem to be resolvable. It could be displayed on
944 * the buddy list, if the UserID was stored along with it. */
945
946 if (!username)
947 {
948 purple_debug_info("msim", "msim_status_cb: no username?!\n");
949 return;
950 }
951
952 purple_debug_info("msim",
953 "msim_status_cb: updating status for <%s> to <%s>\n",
954 username, status_str);
955
956 /* TODO: generic functions to split into a GList */
957 status_array = g_strsplit(status_str, "|", 0);
958 for (list = NULL, i = 0;
959 status_array[i];
960 i++)
961 {
962 list = g_list_append(list, status_array[i]);
963 }
964
965 /* Example fields: |s|0|ss|Offline */
966 status_code = g_list_nth_data(list, 2);
967 status_text = g_list_nth_data(list, 4);
968
969 blist = purple_get_blist();
970
971 /* Add buddy if not found */
972 buddy = purple_find_buddy(session->account, username);
973 if (!buddy)
974 {
975 /* TODO: purple aliases, userids and usernames */
976 purple_debug_info("msim",
977 "msim_status: making new buddy for %s\n", username);
978 buddy = purple_buddy_new(session->account, username, NULL);
979
980 /* TODO: sometimes (when click on it), buddy list disappears. Fix. */
981 purple_blist_add_buddy(buddy, NULL, NULL, NULL);
982 } else {
983 purple_debug_info("msim", "msim_status: found buddy %s\n", username);
984 }
985
986 /* For now, always set status to online.
987 * TODO: make status reflect reality
988 * TODO: show headline */
989 presence = purple_presence_new_for_buddy(buddy);
990 purple_presence_set_status_active(presence, MSIM_STATUS_ONLINE, TRUE);
991
992 g_strfreev(status_array);
993 g_list_free(list);
994 g_hash_table_destroy(body);
995 g_hash_table_destroy(userinfo);
996 /* Do not free status_str - it will be freed by g_hash_table_destroy on session->userid_lookup_cb_data */
997 }
998
999 /**
1000 * Process incoming status messages.
1001 *
1002 * @param session
1003 * @param table Status update message.
1004 *
1005 * @return 0, since 'table' can be freed.
1006 */
1007 static int msim_status(MsimSession *session, GHashTable *table)
1008 {
1009 gchar *status_str;
1010 gchar *userid;
1011
1012 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0);
1013 g_return_val_if_fail(table != NULL, 0);
1014
1015 status_str = g_hash_table_lookup(table, "msg");
1016 if (!status_str)
1017 {
1018 purple_debug_info("msim", "msim_status: bm=100 but no status msg\n");
1019 return 0;
1020 }
1021
1022 userid = g_hash_table_lookup(table, "f");
1023 if (!userid)
1024 {
1025 purple_debug_info("msim", "msim_status: bm=100 but no f field\n");
1026 return 0;
1027 }
1028
1029 /* TODO: if buddies were identified on buddy list by uid, wouldn't have to lookup
1030 * before updating the status! Much more efficient. */
1031 purple_debug_info("msim",
1032 "msim_status: got status msg <%s> for <%s>, scheduling lookup\n",
1033 status_str, userid);
1034
1035 /* Actually update status once obtain username */
1036 msim_lookup_user(session, userid, msim_status_cb, g_strdup(status_str));
1037
1038 return 0;
1039 }
1040
1041
1042
1043 /**
1044 * Callback when input available.
1045 *
1046 * @param gc_uncasted A PurpleConnection pointer.
1047 * @param source File descriptor.
1048 * @param cond PURPLE_INPUT_READ
1049 *
1050 * Reads the input, and dispatches calls msim_process to handle it.
1051 */
1052 static void msim_input_cb(gpointer gc_uncasted, gint source, PurpleInputCondition cond)
1053 {
1054 PurpleConnection *gc;
1055 PurpleAccount *account;
1056 MsimSession *session;
1057 gchar *end;
1058 int n;
1059
1060 g_return_if_fail(gc_uncasted != NULL);
1061 g_return_if_fail(source >= 0); /* Note: 0 is a valid fd */
1062
1063 gc = (PurpleConnection*)(gc_uncasted);
1064 account = purple_connection_get_account(gc);
1065 session = gc->proto_data;
1066
1067 g_return_if_fail(MSIM_SESSION_VALID(session));
1068
1069 g_assert(cond == PURPLE_INPUT_READ);
1070
1071 /* Only can handle so much data at once...
1072 * If this happens, try recompiling with a higher MSIM_READ_BUF_SIZE.
1073 * Should be large enough to hold the largest protocol message.
1074 */
1075 if (session->rxoff == MSIM_READ_BUF_SIZE)
1076 {
1077 purple_debug_error("msim", "msim_input_cb: %d-byte read buffer full!\n",
1078 MSIM_READ_BUF_SIZE);
1079 purple_connection_error(gc, "Read buffer full");
1080 /* TODO: fix 100% CPU after closing */
1081 close(source);
1082 return;
1083 }
1084
1085 purple_debug_info("msim", "buffer at %d (max %d), reading up to %d\n",
1086 session->rxoff, MSIM_READ_BUF_SIZE,
1087 MSIM_READ_BUF_SIZE - session->rxoff);
1088
1089 /* Read into buffer. On Win32, need recv() not read(). session->fd also holds
1090 * the file descriptor, but it sometimes differs from the 'source' parameter.
1091 */
1092 n = recv(session->fd, session->rxbuf + session->rxoff, MSIM_READ_BUF_SIZE - session->rxoff, 0);
1093
1094 if (n < 0 && errno == EAGAIN)
1095 {
1096 return;
1097 }
1098 else if (n < 0)
1099 {
1100 purple_connection_error(gc, "Read error");
1101 purple_debug_error("msim", "msim_input_cb: read error, ret=%d, "
1102 "error=%s, source=%d, fd=%d (%X))\n",
1103 n, strerror(errno), source, session->fd, session->fd);
1104 close(source);
1105 return;
1106 }
1107 else if (n == 0)
1108 {
1109 purple_debug_info("msim", "msim_input_cb: server disconnected\n");
1110 purple_connection_error(gc, "Server has disconnected");
1111 return;
1112 }
1113
1114 /* Null terminate */
1115 session->rxbuf[session->rxoff + n] = 0;
1116
1117 /* Check for embedded NULs. I don't handle them, and they shouldn't occur. */
1118 if (strlen(session->rxbuf + session->rxoff) != n)
1119 {
1120 /* Occurs after login, but it is not a null byte. */
1121 purple_debug_info("msim", "msim_input_cb: strlen=%d, but read %d bytes"
1122 "--null byte encountered?\n",
1123 strlen(session->rxbuf + session->rxoff), n);
1124 //purple_connection_error(gc, "Invalid message - null byte on input");
1125 return;
1126 }
1127
1128 session->rxoff += n;
1129 purple_debug_info("msim", "msim_input_cb: read=%d\n", n);
1130
1131 //printf("buf=<%s>\n", session->rxbuf);
1132
1133 /* Look for \\final\\ end markers. If found, process message. */
1134 while((end = strstr(session->rxbuf, MSIM_FINAL_STRING)))
1135 {
1136 GHashTable *table;
1137
1138 //printf("in loop: buf=<%s>\n", session->rxbuf);
1139 *end = 0;
1140 table = msim_parse(g_strdup(session->rxbuf));
1141 if (!table)
1142 {
1143 purple_debug_info("msim", "msim_input_cb: couldn't parse <%s>\n",
1144 session->rxbuf);
1145 purple_connection_error(gc, "Unparseable message");
1146 }
1147 else
1148 {
1149 /* Process message. Returns 0 to free */
1150 if (msim_process(gc, table) == 0)
1151 g_hash_table_destroy(table);
1152 }
1153
1154 /* Move remaining part of buffer to beginning. */
1155 session->rxoff -= strlen(session->rxbuf) + strlen(MSIM_FINAL_STRING);
1156 memmove(session->rxbuf, end + strlen(MSIM_FINAL_STRING),
1157 MSIM_READ_BUF_SIZE - (end + strlen(MSIM_FINAL_STRING) - session->rxbuf));
1158
1159 /* Clear end of buffer */
1160 //memset(end, 0, MSIM_READ_BUF_SIZE - (end - session->rxbuf));
1161 }
1162 }
1163
1164 /**
1165 * Callback when connected. Sets up input handlers.
1166 *
1167 * @param data A PurpleConnection pointer.
1168 * @param source File descriptor.
1169 * @param error_message
1170 */
1171 static void msim_connect_cb(gpointer data, gint source, const gchar *error_message)
1172 {
1173 PurpleConnection *gc;
1174 MsimSession *session;
1175
1176 g_return_if_fail(data != NULL);
1177
1178 gc = (PurpleConnection*)data;
1179 session = gc->proto_data;
1180
1181 if (source < 0)
1182 {
1183 purple_connection_error(gc, "Couldn't connect to host");
1184 purple_connection_error(gc, g_strdup_printf("Couldn't connect to host: %s (%d)",
1185 error_message, source));
1186 return;
1187 }
1188
1189 session->fd = source;
1190
1191 gc->inpa = purple_input_add(source, PURPLE_INPUT_READ, msim_input_cb, gc);
1192 }
1193
1194 /* Session methods */
1195
1196 /**
1197 * Create a new MSIM session.
1198 *
1199 * @param acct The account to create the session from.
1200 *
1201 * @return Pointer to a new session. Free with msim_session_destroy.
1202 */
1203 static MsimSession *msim_session_new(PurpleAccount *acct)
1204 {
1205 MsimSession *session;
1206
1207 g_return_val_if_fail(acct != NULL, NULL);
1208
1209 session = g_new0(MsimSession, 1);
1210
1211 session->magic = MSIM_SESSION_STRUCT_MAGIC;
1212 session->account = acct;
1213 session->gc = purple_account_get_connection(acct);
1214 session->fd = -1;
1215 session->user_lookup_cb = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, NULL); /* do NOT free function pointers! */
1216 session->user_lookup_cb_data = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free);
1217 session->user_lookup_cache = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_hash_table_destroy);
1218 session->rxoff = 0;
1219 session->rxbuf = g_new0(gchar, MSIM_READ_BUF_SIZE);
1220
1221 return session;
1222 }
1223
1224 /**
1225 * Free a session.
1226 *
1227 * @param session The session to destroy.
1228 */
1229 static void msim_session_destroy(MsimSession *session)
1230 {
1231 g_return_if_fail(MSIM_SESSION_VALID(session));
1232
1233 session->magic = -1;
1234
1235 g_free(session->rxbuf);
1236 g_free(session->userid);
1237 g_free(session->sesskey);
1238
1239 g_free(session);
1240 }
1241
1242
1243
1244 /**
1245 * Close the connection.
1246 *
1247 * @param gc The connection.
1248 */
1249 static void msim_close(PurpleConnection *gc)
1250 {
1251 g_return_if_fail(gc != NULL);
1252
1253 msim_session_destroy(gc->proto_data);
1254 }
1255
1256
1257 /**
1258 * Check if a string is a userid (all numeric).
1259 *
1260 * @param user The user id, email, or name.
1261 *
1262 * @return TRUE if is userid, FALSE if not.
1263 */
1264 static inline gboolean msim_is_userid(const gchar *user)
1265 {
1266 g_return_val_if_fail(user != NULL, FALSE);
1267
1268 return strspn(user, "0123456789") == strlen(user);
1269 }
1270
1271 /**
1272 * Check if a string is an email address (contains an @).
1273 *
1274 * @param user The user id, email, or name.
1275 *
1276 * @return TRUE if is an email, FALSE if not.
1277 *
1278 * This function is not intended to be used as a generic
1279 * means of validating email addresses, but to distinguish
1280 * between a user represented by an email address from
1281 * other forms of identification.
1282 */
1283 static inline gboolean msim_is_email(const gchar *user)
1284 {
1285 g_return_val_if_fail(user != NULL, FALSE);
1286
1287 return strchr(user, '@') != NULL;
1288 }
1289
1290
1291 /**
1292 * Asynchronously lookup user information, calling callback when receive result.
1293 *
1294 * @param session
1295 * @param user The user id, email address, or username.
1296 * @param cb Callback, called with user information when available.
1297 * @param data An arbitray data pointer passed to the callback.
1298 */
1299 static void msim_lookup_user(MsimSession *session, const gchar *user, MSIM_USER_LOOKUP_CB cb, gpointer data)
1300 {
1301 gchar *field_name;
1302 gchar *msg_string;
1303 guint rid, cmd, dsn, lid;
1304
1305 g_return_if_fail(MSIM_SESSION_VALID(session));
1306 g_return_if_fail(user != NULL);
1307 g_return_if_fail(cb != NULL);
1308
1309 purple_debug_info("msim", "msim_lookup_userid",
1310 "asynchronously looking up <%s>\n", user);
1311
1312 /* TODO: check if this user's info was cached and fresh; if so return immediately */
1313 #if 0
1314 /* If already know userid, then call callback immediately */
1315 cached_userid = g_hash_table_lookup(session->userid_cache, who);
1316 if (cached_userid && !by_userid)
1317 {
1318 cb(cached_userid, NULL, NULL, data);
1319 return;
1320 }
1321 #endif
1322
1323 rid = rand(); //om();
1324
1325 /* Setup callback. Response will be associated with request using 'rid'. */
1326 g_hash_table_insert(session->user_lookup_cb, GUINT_TO_POINTER(rid), cb);
1327 g_hash_table_insert(session->user_lookup_cb_data, GUINT_TO_POINTER(rid), data);
1328
1329 /* Send request */
1330
1331 cmd = 1;
1332
1333 if (msim_is_userid(user))
1334 {
1335 /* TODO: document cmd,dsn,lid */
1336 field_name = "UserID";
1337 dsn = 4;
1338 lid = 3;
1339 } else if (msim_is_email(user)) {
1340 field_name = "Email";
1341 dsn = 5;
1342 lid = 7;
1343 } else {
1344 field_name = "UserName";
1345 dsn = 5;
1346 lid = 7;
1347 }
1348
1349 msg_string = g_strdup_printf("\\persist\\1\\sesskey\\%s\\cmd\\1\\dsn\\%d\\uid\\%s\\lid\\%d\\rid\\%d\\body\\%s=%s\\final\\",
1350 session->sesskey, dsn, session->userid, lid, rid, field_name, user);
1351
1352 msim_send(session, msg_string);
1353 }
1354
1422 1355
1423 /** 1356 /**
1424 * Obtain the status text for a buddy. 1357 * Obtain the status text for a buddy.
1425 * 1358 *
1426 * @param buddy The buddy to obtain status text for. 1359 * @param buddy The buddy to obtain status text for.