Mercurial > pidgin.yaz
comparison libpurple/protocols/myspace/myspace.c @ 16395:05e2df434077
Import msimprpl 0.4.
author | Jeffrey Connelly <jaconnel@calpoly.edu> |
---|---|
date | Sat, 21 Apr 2007 03:15:48 +0000 |
parents | |
children | 70c069168459 |
comparison
equal
deleted
inserted
replaced
16394:a7a0f0f371b4 | 16395:05e2df434077 |
---|---|
1 /* MySpaceIM Protocol Plugin | |
2 * | |
3 * \author Jeff Connelly | |
4 * | |
5 * Copyright (C) 2007, Jeff Connelly <myspaceim@xyzzy.cjb.net> | |
6 * | |
7 * Based on Gaim's "C Plugin HOWTO" hello world example. | |
8 * | |
9 * Code also drawn from myspace: | |
10 * http://snarfed.org/space/gaim+mock+protocol+plugin | |
11 * Copyright (C) 2004-2007, Ryan Barrett <mockprpl@ryanb.org> | |
12 * | |
13 * and some constructs also based on existing Gaim plugins, which are: | |
14 * Copyright (C) 2003, Robbert Haarman <gaim@inglorion.net> | |
15 * Copyright (C) 2003, Ethan Blanton <eblanton@cs.purdue.edu> | |
16 * Copyright (C) 2000-2003, Rob Flynn <rob@tgflinux.com> | |
17 * Copyright (C) 1998-1999, Mark Spencer <markster@marko.net> | |
18 * | |
19 * This program is free software; you can redistribute it and/or modify | |
20 * it under the terms of the GNU General Public License as published by | |
21 * the Free Software Foundation; either version 2 of the License, or | |
22 * (at your option) any later version. | |
23 * | |
24 * This program is distributed in the hope that it will be useful, | |
25 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
27 * GNU General Public License for more details. | |
28 * | |
29 * You should have received a copy of the GNU General Public License | |
30 * along with this program; if not, write to the Free Software | |
31 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |
32 */ | |
33 | |
34 #define GAIM_PLUGINS | |
35 | |
36 #include <string.h> | |
37 #include <errno.h> /* for EAGAIN */ | |
38 | |
39 #include <glib.h> | |
40 | |
41 #ifdef _WIN32 | |
42 #include "win32dep.h" | |
43 #else | |
44 /* For recv() and send(); needed to match Win32 */ | |
45 #include <sys/types.h> | |
46 #include <sys/socket.h> | |
47 #endif | |
48 | |
49 #include "notify.h" | |
50 #include "plugin.h" | |
51 #include "version.h" | |
52 #include "cipher.h" /* for SHA-1 */ | |
53 #include "util.h" /* for base64 */ | |
54 #include "debug.h" /* for gaim_debug_info */ | |
55 | |
56 #define MSIM_STATUS_ONLINE "online" | |
57 #define MSIM_STATUS_AWAY "away" | |
58 #define MSIM_STATUS_OFFLINE "offline" | |
59 #define MSIM_STATUS_INVISIBLE "invisible" | |
60 | |
61 /* Build version of MySpaceIM to report to servers */ | |
62 #define MSIM_CLIENT_VERSION 673 | |
63 | |
64 #define MSIM_SERVER "im.myspace.akadns.net" | |
65 //#define MSIM_SERVER "localhost" | |
66 #define MSIM_PORT 1863 /* TODO: alternate ports and automatic */ | |
67 | |
68 /* Constants */ | |
69 #define HASH_SIZE 0x14 /**< Size of SHA-1 hash for login */ | |
70 #define NONCE_HALF_SIZE 0x20 /**< Half of decoded 'nc' field */ | |
71 #define MSIM_READ_BUF_SIZE 5*1024 /**< Receive buffer size */ | |
72 #define MSIM_FINAL_STRING "\\final\\" /**< Message end marker */ | |
73 | |
74 /* Messages */ | |
75 #define MSIM_BM_INSTANT 1 | |
76 #define MSIM_BM_STATUS 100 | |
77 #define MSIM_BM_ACTION 121 | |
78 /*#define MSIM_BM_UNKNOWN1 122*/ | |
79 | |
80 /* Random number in every MsimSession, to ensure it is valid. */ | |
81 #define MSIM_SESSION_STRUCT_MAGIC 0xe4a6752b | |
82 | |
83 /* Everything needed to keep track of a session. */ | |
84 typedef struct _MsimSession | |
85 { | |
86 guint magic; /**< MSIM_SESSION_STRUCT_MAGIC */ | |
87 GaimAccount *account; | |
88 GaimConnection *gc; | |
89 gchar *sesskey; /**< Session key text string from server */ | |
90 gchar *userid; /**< This user's numeric user ID */ | |
91 gint fd; /**< File descriptor to/from server */ | |
92 | |
93 GHashTable *user_lookup_cb; /**< Username -> userid lookup callback */ | |
94 GHashTable *user_lookup_cb_data; /**< Username -> userid lookup callback data */ | |
95 GHashTable *user_lookup_cache; /**< Cached information on users */ | |
96 | |
97 gchar *rxbuf; /**< Receive buffer */ | |
98 guint rxoff; /**< Receive buffer offset */ | |
99 } MsimSession; | |
100 | |
101 #define MSIM_SESSION_VALID(s) (session != NULL && session->magic == MSIM_SESSION_STRUCT_MAGIC) | |
102 | |
103 /* Callback for when a user's information is received, initiated from a user lookup. */ | |
104 typedef void (*MSIM_USER_LOOKUP_CB)(MsimSession *session, GHashTable *userinfo, gpointer data); | |
105 | |
106 /* Passed to MSIM_USER_LOOKUP_CB for msim_send_im_cb - called when | |
107 * user information is available, ready to send a message. */ | |
108 typedef struct _send_im_cb_struct | |
109 { | |
110 gchar *who; | |
111 gchar *message; | |
112 GaimMessageFlags flags; | |
113 } send_im_cb_struct; | |
114 | |
115 | |
116 /* TODO: .h file */ | |
117 static void msim_lookup_user(MsimSession *session, const gchar *user, MSIM_USER_LOOKUP_CB cb, gpointer data); | |
118 static inline gboolean msim_is_userid(const gchar *user); | |
119 static void msim_session_destroy(MsimSession *session); | |
120 | |
121 static void init_plugin(GaimPlugin *plugin) | |
122 { | |
123 gaim_notify_message(plugin, GAIM_NOTIFY_MSG_INFO, "Hello World!", | |
124 "This is the Hello World! plugin :)", NULL, NULL, NULL); | |
125 } | |
126 | |
127 /** | |
128 * Get possible user status types. Based on mockprpl. | |
129 * | |
130 * @return GList of status types. | |
131 */ | |
132 static GList *msim_status_types(GaimAccount *acct) | |
133 { | |
134 GList *types; | |
135 GaimStatusType *type; | |
136 | |
137 gaim_debug_info("myspace", "returning status types for %s: %s, %s, %s\n", | |
138 acct->username, | |
139 MSIM_STATUS_ONLINE, MSIM_STATUS_AWAY, MSIM_STATUS_OFFLINE, MSIM_STATUS_INVISIBLE); | |
140 | |
141 | |
142 types = NULL; | |
143 | |
144 type = gaim_status_type_new(GAIM_STATUS_AVAILABLE, MSIM_STATUS_ONLINE, | |
145 MSIM_STATUS_ONLINE, TRUE); | |
146 gaim_status_type_add_attr(type, "message", "Online", | |
147 gaim_value_new(GAIM_TYPE_STRING)); | |
148 types = g_list_append(types, type); | |
149 | |
150 type = gaim_status_type_new(GAIM_STATUS_AWAY, MSIM_STATUS_AWAY, | |
151 MSIM_STATUS_AWAY, TRUE); | |
152 gaim_status_type_add_attr(type, "message", "Away", | |
153 gaim_value_new(GAIM_TYPE_STRING)); | |
154 types = g_list_append(types, type); | |
155 | |
156 type = gaim_status_type_new(GAIM_STATUS_OFFLINE, MSIM_STATUS_OFFLINE, | |
157 MSIM_STATUS_OFFLINE, TRUE); | |
158 gaim_status_type_add_attr(type, "message", "Offline", | |
159 gaim_value_new(GAIM_TYPE_STRING)); | |
160 types = g_list_append(types, type); | |
161 | |
162 type = gaim_status_type_new(GAIM_STATUS_INVISIBLE, MSIM_STATUS_INVISIBLE, | |
163 MSIM_STATUS_INVISIBLE, TRUE); | |
164 gaim_status_type_add_attr(type, "message", "Invisible", | |
165 gaim_value_new(GAIM_TYPE_STRING)); | |
166 types = g_list_append(types, type); | |
167 | |
168 return types; | |
169 } | |
170 | |
171 /** | |
172 * Parse a MySpaceIM protocol message into a hash table. | |
173 * | |
174 * @param msg The message string to parse, will be g_free()'d. | |
175 * | |
176 * @return Hash table of message. Caller should destroy with | |
177 * g_hash_table_destroy() when done. | |
178 */ | |
179 static GHashTable* msim_parse(gchar* msg) | |
180 { | |
181 GHashTable *table; | |
182 gchar *token; | |
183 gchar **tokens; | |
184 gchar *key; | |
185 gchar *value; | |
186 int i; | |
187 | |
188 g_return_val_if_fail(msg != NULL, NULL); | |
189 | |
190 gaim_debug_info("msim", "msim_parse: got <%s>\n", msg); | |
191 | |
192 key = NULL; | |
193 | |
194 /* All messages begin with a \ */ | |
195 if (msg[0] != '\\' || msg[1] == 0) | |
196 { | |
197 gaim_debug_info("msim", "msim_parse: incomplete/bad msg, " | |
198 "missing initial backslash: <%s>", msg); | |
199 /* XXX: Should we try to recover, and read to first backslash? */ | |
200 | |
201 g_free(msg); | |
202 return NULL; | |
203 } | |
204 | |
205 /* Create table of strings, set to call g_free on destroy. */ | |
206 table = g_hash_table_new_full((GHashFunc)g_str_hash, | |
207 (GEqualFunc)g_str_equal, g_free, g_free); | |
208 | |
209 for (tokens = g_strsplit(msg + 1, "\\", 0), i = 0; | |
210 (token = tokens[i]); | |
211 i++) | |
212 { | |
213 //printf("tok=<%s>, i%2=%d\n", token, i % 2); | |
214 if (i % 2) | |
215 { | |
216 value = token; | |
217 | |
218 /* Check if key already exists */ | |
219 if (g_hash_table_lookup(table, key) == NULL) | |
220 { | |
221 //printf("insert: |%s|=|%s|\n", key, value); | |
222 g_hash_table_insert(table, g_strdup(key), g_strdup(value)); | |
223 } else { | |
224 /* TODO: Some dictionaries have multiple values for the same | |
225 * key. Should append to a GList to handle this case. */ | |
226 gaim_debug_info("msim", "msim_parse: key %s already exists, " | |
227 "not overwriting or replacing; ignoring new value %s", key, | |
228 value); | |
229 } | |
230 } else { | |
231 key = token; | |
232 } | |
233 } | |
234 g_strfreev(tokens); | |
235 | |
236 /* Can free now since all data was copied to hash key/values */ | |
237 g_free(msg); | |
238 | |
239 return table; | |
240 } | |
241 | |
242 #include "crypt-rc4.c" | |
243 | |
244 /** | |
245 * Compute the base64'd login challenge response based on username, password, nonce, and IPs. | |
246 * | |
247 * @param nonce The base64 encoded nonce ('nc') field from the server. | |
248 * @param email User's email address (used as login name). | |
249 * @param password User's cleartext password. | |
250 * | |
251 * @return Encoded login challenge response, ready to send to the server. Must be g_free()'d | |
252 * when finished. | |
253 */ | |
254 static gchar* msim_compute_login_response(guchar nonce[2*NONCE_HALF_SIZE], gchar* email, gchar* password) | |
255 { | |
256 GaimCipherContext *key_context; | |
257 GaimCipher *sha1; | |
258 rc4_state_struct rc4; | |
259 guchar hash_pw[HASH_SIZE]; | |
260 guchar key[HASH_SIZE]; | |
261 gchar* password_utf16le; | |
262 guchar* data; | |
263 gchar* response; | |
264 int i, data_len; | |
265 | |
266 //memset(nonce, 0, NONCE_HALF_SIZE); | |
267 //memset(nonce + NONCE_HALF_SIZE, 1, NONCE_HALF_SIZE); | |
268 | |
269 /* Convert ASCII password to UTF16 little endian */ | |
270 /* TODO: use the built-in facility to do this, like Nathan Peterson does. */ | |
271 gaim_debug_info("msim", "converting password to utf16le\n"); | |
272 //printf("pw=<%s>\n",password); | |
273 password_utf16le = g_new0(gchar, strlen(password) * 2); | |
274 for (i = 0; i < strlen(password) * 2; i += 2) | |
275 { | |
276 password_utf16le[i] = password[i / 2]; | |
277 password_utf16le[i + 1] = 0; | |
278 } | |
279 | |
280 /* Compute password hash */ | |
281 gaim_cipher_digest_region("sha1", (guchar*)password_utf16le, strlen(password) * 2, | |
282 sizeof(hash_pw), hash_pw, NULL); | |
283 | |
284 #ifdef MSIM_DEBUG_LOGIN_CHALLENGE | |
285 printf("pwhash = "); | |
286 for (i = 0; i < sizeof(hash_pw); i++) | |
287 printf("%.2x ", hash_pw[i]); | |
288 printf("\n"); | |
289 #endif | |
290 | |
291 /* key = sha1(sha1(pw) + nonce2) */ | |
292 sha1 = gaim_ciphers_find_cipher("sha1"); | |
293 key_context = gaim_cipher_context_new(sha1, NULL); | |
294 gaim_cipher_context_append(key_context, hash_pw, HASH_SIZE); | |
295 gaim_cipher_context_append(key_context, nonce + NONCE_HALF_SIZE, NONCE_HALF_SIZE); | |
296 gaim_cipher_context_digest(key_context, sizeof(key), key, NULL); | |
297 | |
298 #ifdef MSIM_DEBUG_LOGIN_CHALLENGE | |
299 printf("key = "); | |
300 for (i = 0; i < sizeof(key); i++) | |
301 { | |
302 printf("%.2x ", key[i]); | |
303 } | |
304 printf("\n"); | |
305 #endif | |
306 | |
307 /* Note: 'key' variable is 0x14 bytes (from SHA-1 hash), | |
308 * but only first 0x10 used for the RC4 key. */ | |
309 crypt_rc4_init(&rc4, key, 0x10); | |
310 | |
311 /* TODO: obtain IPs of network interfaces. This is not immediately | |
312 * important because you can still connect and perform basic | |
313 * functions of the protocol. There is also a high chance that the addreses | |
314 * are RFC1918 private, so the servers couldn't do anything with them | |
315 * anyways except make note of that fact. Probably important for any | |
316 * kind of direct connection, or file transfer functionality. | |
317 */ | |
318 /* rc4 encrypt: | |
319 * nonce1+email+IP list */ | |
320 data_len = NONCE_HALF_SIZE + strlen(email) + 25; | |
321 data = g_new0(guchar, data_len); | |
322 memcpy(data, nonce, NONCE_HALF_SIZE); | |
323 memcpy(data + NONCE_HALF_SIZE, email, strlen(email)); | |
324 memcpy(data + NONCE_HALF_SIZE + strlen(email), | |
325 /* IP addresses of network interfaces */ | |
326 "\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); | |
327 crypt_rc4(&rc4, data, data_len); | |
328 | |
329 response = gaim_base64_encode(data, data_len); | |
330 #ifdef MSIM_DEBUG_LOGIN_CHALLENGE | |
331 printf("response=<%s>\n", response); | |
332 #endif | |
333 | |
334 return response; | |
335 } | |
336 | |
337 static void print_hash_item(gpointer key, gpointer value, gpointer user_data) | |
338 { | |
339 printf("%s=%s\n", (char*)key, (char*)value); | |
340 } | |
341 | |
342 /** | |
343 * Send an arbitrary protocol message. | |
344 * | |
345 * @param session | |
346 * @param msg The textual, encoded message to send. | |
347 * | |
348 * Note: this does not send instant messages. For that, see msim_send_im. | |
349 */ | |
350 static void msim_send(MsimSession *session, const gchar *msg) | |
351 { | |
352 int ret; | |
353 | |
354 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
355 g_return_if_fail(msg != NULL); | |
356 | |
357 gaim_debug_info("msim", "msim_send: writing <%s>\n", msg); | |
358 | |
359 ret = send(session->fd, msg, strlen(msg), 0); | |
360 | |
361 if (ret != strlen(msg)) | |
362 { | |
363 gaim_debug_info("msim", "msim_send(%s): strlen=%d, but only wrote %s\n", | |
364 msg, strlen(msg), ret); | |
365 /* TODO: better error */ | |
366 } | |
367 } | |
368 | |
369 /** | |
370 * Process a login challenge, sending a response. | |
371 * | |
372 * @param session | |
373 * @param table Hash table of login challenge message. | |
374 * | |
375 * @return 0, since the 'table' parameter is no longer needed. | |
376 */ | |
377 static int msim_login_challenge(MsimSession *session, GHashTable *table) | |
378 { | |
379 GaimAccount *account; | |
380 gchar *nc_str; | |
381 guchar *nc; | |
382 gchar *response_str; | |
383 gsize nc_len; | |
384 gchar *buf; | |
385 | |
386 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
387 g_return_val_if_fail(table != NULL, 0); | |
388 | |
389 nc_str = g_hash_table_lookup(table, "nc"); | |
390 | |
391 account = session->account; | |
392 //assert(account); | |
393 | |
394 gaim_connection_update_progress(session->gc, "Reading challenge", 1, 4); | |
395 | |
396 gaim_debug_info("msim", "nc=<%s>\n", nc_str); | |
397 | |
398 nc = (guchar*)gaim_base64_decode(nc_str, &nc_len); | |
399 gaim_debug_info("msim", "base64 decoded to %d bytes\n", nc_len); | |
400 if (nc_len != 0x40) | |
401 { | |
402 gaim_debug_info("msim", "bad nc length: %x != 0x40\n", nc_len); | |
403 gaim_connection_error(session->gc, "Unexpected challenge length from server"); | |
404 return 0; | |
405 } | |
406 | |
407 gaim_connection_update_progress(session->gc, "Logging in", 2, 4); | |
408 | |
409 printf("going to compute login response\n"); | |
410 //response_str = msim_compute_login_response(nc_str, "testuser", "testpw"); //session->gc->account->username, session->gc->account->password); | |
411 response_str = msim_compute_login_response(nc, account->username, account->password); | |
412 printf("got back login response\n"); | |
413 | |
414 g_free(nc); | |
415 | |
416 /* Reply */ | |
417 buf = g_strdup_printf("\\login2\\%d\\username\\%s\\response\\%s\\clientver\\%d\\reconn\\%d\\status\\%d\\id\\1\\final\\", | |
418 196610, account->username, response_str, MSIM_CLIENT_VERSION, 0, 100); | |
419 | |
420 g_free(response_str); | |
421 | |
422 gaim_debug_info("msim", "response=<%s>\n", buf); | |
423 | |
424 msim_send(session, buf); | |
425 | |
426 g_free(buf); | |
427 | |
428 return 0; | |
429 } | |
430 | |
431 /** | |
432 * Parse a \x1c-separated "dictionary" of key=value pairs into a hash table. | |
433 * | |
434 * @param body_str The text of the dictionary to parse. Often the | |
435 * value for the 'body' field. | |
436 * | |
437 * @return Hash table of the keys and values. Must g_hash_table_destroy() when done. | |
438 */ | |
439 static GHashTable *msim_parse_body(const gchar *body_str) | |
440 { | |
441 GHashTable *table; | |
442 gchar *item; | |
443 gchar **items; | |
444 gchar **elements; | |
445 guint i; | |
446 | |
447 g_return_val_if_fail(body_str != NULL, NULL); | |
448 | |
449 table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); | |
450 | |
451 for (items = g_strsplit(body_str, "\x1c", 0), i = 0; | |
452 (item = items[i]); | |
453 i++) | |
454 { | |
455 gchar *key, *value; | |
456 | |
457 //printf("TOK=<%s>\n", token); | |
458 elements = g_strsplit(item, "=", 2); | |
459 | |
460 key = elements[0]; | |
461 if (!key) | |
462 { | |
463 gaim_debug_info("msim", "msim_parse_body(%s): null key", body_str); | |
464 g_strfreev(elements); | |
465 break; | |
466 } | |
467 | |
468 value = elements[1]; | |
469 if (!value) | |
470 { | |
471 gaim_debug_info("msim", "msim_parse_body(%s): null value", body_str); | |
472 g_strfreev(elements); | |
473 break; | |
474 } | |
475 | |
476 //printf("-- %s: %s\n", key, value); | |
477 | |
478 /* XXX: This overwrites duplicates. */ | |
479 /* TODO: make the GHashTable values be GList's, and append to the list if | |
480 * there is already a value of the same key name. This is important for | |
481 * the WebChallenge message. */ | |
482 g_hash_table_insert(table, g_strdup(key), g_strdup(value)); | |
483 | |
484 g_strfreev(elements); | |
485 } | |
486 | |
487 g_strfreev(items); | |
488 | |
489 return table; | |
490 } | |
491 | |
492 /** | |
493 * Immediately send an IM to a user, by their numeric user ID. | |
494 * | |
495 * @param session | |
496 * @param userid ASCII numeric userid. | |
497 * @param message Text of message to send. | |
498 * @param flags Gaim instant message flags. | |
499 * | |
500 * @return 0, since the 'table' parameter is no longer needed. | |
501 * | |
502 */ | |
503 static int msim_send_im_by_userid(MsimSession *session, const gchar *userid, const gchar *message, GaimMessageFlags flags) | |
504 { | |
505 gchar *msg_string; | |
506 | |
507 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
508 g_return_val_if_fail(userid != NULL, 0); | |
509 g_return_val_if_fail(msim_is_userid(userid) == TRUE, 0); | |
510 g_return_val_if_fail(message != NULL, 0); | |
511 | |
512 /* TODO: constants for bm types */ | |
513 msg_string = g_strdup_printf("\\bm\\121\\sesskey\\%s\\t\\%s\\cv\\%d\\msg\\%s\\final\\", | |
514 session->sesskey, userid, MSIM_CLIENT_VERSION, message); | |
515 | |
516 gaim_debug_info("msim", "going to write: %s\n", msg_string); | |
517 | |
518 msim_send(session, msg_string); | |
519 | |
520 /* TODO: notify Gaim that we sent the IM. */ | |
521 | |
522 /* Not needed since sending messages to yourself is allowed by MSIM! */ | |
523 /*if (strcmp(from_username, who) == 0) | |
524 serv_got_im(gc, from_username, message, GAIM_MESSAGE_RECV, time(NULL)); | |
525 */ | |
526 | |
527 return 0; | |
528 } | |
529 | |
530 | |
531 /** | |
532 * Callback called when ready to send an IM by userid (the userid has been looked up). | |
533 * Calls msim_send_im_by_userid. | |
534 * | |
535 * @param session | |
536 * @param userinfo User info message from server containing a 'body' field | |
537 * with a 'UserID' key. This is where the user ID is taken from. | |
538 * @param data A send_im_cb_struct* of information on the IM to send. | |
539 * | |
540 */ | |
541 static void msim_send_im_by_userid_cb(MsimSession *session, GHashTable *userinfo, gpointer data) | |
542 { | |
543 send_im_cb_struct *s; | |
544 gchar *userid; | |
545 GHashTable *body; | |
546 | |
547 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
548 g_return_if_fail(userinfo != NULL); | |
549 | |
550 body = msim_parse_body(g_hash_table_lookup(userinfo, "body")); | |
551 g_assert(body); | |
552 | |
553 userid = g_hash_table_lookup(body, "UserID"); | |
554 | |
555 s = (send_im_cb_struct*)data; | |
556 msim_send_im_by_userid(session, userid, s->message, s->flags); | |
557 | |
558 g_hash_table_destroy(body); | |
559 g_hash_table_destroy(userinfo); | |
560 g_free(s->message); | |
561 g_free(s->who); | |
562 } | |
563 | |
564 /** | |
565 * Process a message reply from the server. | |
566 * | |
567 * @param session | |
568 * @param table Message reply from server. | |
569 * | |
570 * @return 0, since the 'table' field is no longer needed. | |
571 */ | |
572 static int msim_process_reply(MsimSession *session, GHashTable *table) | |
573 { | |
574 gchar *rid_str; | |
575 | |
576 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
577 g_return_val_if_fail(table != NULL, 0); | |
578 | |
579 rid_str = g_hash_table_lookup(table, "rid"); | |
580 | |
581 if (rid_str) /* msim_lookup_user sets callback for here */ | |
582 { | |
583 MSIM_USER_LOOKUP_CB cb; | |
584 gpointer data; | |
585 guint rid; | |
586 | |
587 GHashTable *body; | |
588 gchar *username; | |
589 | |
590 rid = atol(rid_str); | |
591 | |
592 /* Cache the user info. Currently, the GHashTable of user info in | |
593 * this cache never expires so is never freed. TODO: expire and destroy | |
594 * | |
595 * Some information never changes (username->userid map), some does. | |
596 * TODO: Cache what doesn't change only | |
597 */ | |
598 body = msim_parse_body(g_hash_table_lookup(table, "body")); | |
599 username = g_hash_table_lookup(body, "UserName"); | |
600 if (username) | |
601 { | |
602 g_hash_table_insert(session->user_lookup_cache, g_strdup(username), body); | |
603 } else { | |
604 gaim_debug_info("msim", "msim_process_reply: not caching <%s>, no UserName", | |
605 g_hash_table_lookup(table, "body")); | |
606 } | |
607 | |
608 /* If a callback is registered for this userid lookup, call it. */ | |
609 | |
610 cb = g_hash_table_lookup(session->user_lookup_cb, GUINT_TO_POINTER(rid)); | |
611 data = g_hash_table_lookup(session->user_lookup_cb_data, GUINT_TO_POINTER(rid)); | |
612 | |
613 if (cb) | |
614 { | |
615 gaim_debug_info("msim", "msim_process_body: calling callback now\n"); | |
616 cb(session, table, data); | |
617 g_hash_table_remove(session->user_lookup_cb, GUINT_TO_POINTER(rid)); | |
618 g_hash_table_remove(session->user_lookup_cb_data, GUINT_TO_POINTER(rid)); | |
619 | |
620 /* Return 1 to tell caller of msim_process (msim_input_cb) to | |
621 * not destroy 'table'; allow 'cb' to hang on to it and destroy | |
622 * it when it wants. */ | |
623 return 1; | |
624 } else { | |
625 gaim_debug_info("msim", "msim_process_body: no callback for rid %d\n", rid); | |
626 } | |
627 } | |
628 return 0; | |
629 } | |
630 | |
631 /** | |
632 * Handle an error from the server. | |
633 * | |
634 * @param session | |
635 * @param table The message. | |
636 * | |
637 * @return 0, since 'table' can be freed. | |
638 */ | |
639 static int msim_error(MsimSession *session, GHashTable *table) | |
640 { | |
641 gchar *err, *errmsg, *full_errmsg; | |
642 | |
643 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
644 g_return_val_if_fail(table != NULL, 0); | |
645 | |
646 err = g_hash_table_lookup(table, "err"); | |
647 errmsg = g_hash_table_lookup(table, "errmsg"); | |
648 | |
649 full_errmsg = g_strdup_printf("Protocol error, code %s: %s", err, errmsg); | |
650 | |
651 gaim_debug_info("msim", "msim_error: %s\n", full_errmsg); | |
652 | |
653 /* TODO: check 'fatal' and die if asked to. | |
654 * TODO: do something with the error # (localization of errmsg?) */ | |
655 gaim_notify_error(session->account, g_strdup("MySpaceIM Error"), | |
656 full_errmsg, NULL); | |
657 | |
658 if (g_hash_table_lookup(table, "fatal")) | |
659 { | |
660 gaim_debug_info("msim", "fatal error, destroy session\n"); | |
661 gaim_connection_error(session->gc, full_errmsg); | |
662 close(session->fd); | |
663 //msim_session_destroy(session); | |
664 } | |
665 | |
666 return 0; | |
667 } | |
668 | |
669 /** | |
670 * Callback to handle incoming messages, after resolving userid. | |
671 * | |
672 * @param session | |
673 * @param userinfo Message from server on user's info, containing UserName. | |
674 * @param data A gchar* of the incoming instant message's text. | |
675 */ | |
676 static void msim_incoming_im_cb(MsimSession *session, GHashTable *userinfo, gpointer data) | |
677 { | |
678 gchar *msg; | |
679 gchar *username; | |
680 GHashTable *body; | |
681 | |
682 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
683 g_return_if_fail(userinfo != NULL); | |
684 | |
685 body = msim_parse_body(g_hash_table_lookup(userinfo, "body")); | |
686 g_assert(body != NULL); | |
687 | |
688 username = g_hash_table_lookup(body, "UserName"); | |
689 | |
690 msg = (gchar*)data; | |
691 serv_got_im(session->gc, username, msg, GAIM_MESSAGE_RECV, time(NULL)); | |
692 | |
693 g_hash_table_destroy(body); | |
694 g_hash_table_destroy(userinfo); | |
695 } | |
696 | |
697 /** | |
698 * Handle an incoming message. | |
699 * | |
700 * @param session The session | |
701 * @param table Message from the server, containing 'f' (userid from) and 'msg'. | |
702 * | |
703 * @return 0, since table can be freed. | |
704 */ | |
705 static int msim_incoming_im(MsimSession *session, GHashTable *table) | |
706 { | |
707 gchar *userid; | |
708 gchar *msg; | |
709 | |
710 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
711 g_return_val_if_fail(table != NULL, 0); | |
712 | |
713 | |
714 userid = g_hash_table_lookup(table, "f"); | |
715 msg = g_hash_table_lookup(table, "msg"); | |
716 | |
717 gaim_debug_info("msim", "msim_incoming_im: got msg <%s> from <%s>, resolving username\n", | |
718 msg, userid); | |
719 | |
720 msim_lookup_user(session, userid, msim_incoming_im_cb, g_strdup(msg)); | |
721 | |
722 return 0; | |
723 } | |
724 | |
725 #if 0 | |
726 /* Not sure about this */ | |
727 static void msim_status_now(gchar *who, gpointer data) | |
728 { | |
729 printf("msim_status_now: %s\n", who); | |
730 } | |
731 #endif | |
732 | |
733 /** | |
734 * Callback to update incoming status messages, after looked up username. | |
735 * | |
736 * @param session | |
737 * @param userinfo Looked up user information from server. | |
738 * @param data gchar* status string. | |
739 * | |
740 */ | |
741 static void msim_status_cb(MsimSession *session, GHashTable *userinfo, gpointer data) | |
742 { | |
743 GaimBuddyList *blist; | |
744 GaimBuddy *buddy; | |
745 GaimPresence *presence; | |
746 GHashTable *body; | |
747 //GaimStatus *status; | |
748 gchar **status_array; | |
749 GList *list; | |
750 gchar *status_text, *status_code; | |
751 gchar *status_str; | |
752 gint i; | |
753 gchar *username; | |
754 | |
755 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
756 g_return_if_fail(userinfo != NULL); | |
757 | |
758 status_str = (gchar*)data; | |
759 | |
760 body = msim_parse_body(g_hash_table_lookup(userinfo, "body")); | |
761 g_assert(body); | |
762 | |
763 username = g_hash_table_lookup(body, "UserName"); | |
764 /* Note: DisplayName doesn't seem to be resolvable. It could be displayed on | |
765 * the buddy list, if the UserID was stored along with it. */ | |
766 | |
767 if (!username) | |
768 { | |
769 gaim_debug_info("msim", "msim_status_cb: no username?!\n"); | |
770 return; | |
771 } | |
772 | |
773 gaim_debug_info("msim", "msim_status_cb: updating status for <%s> to <%s>\n", username, status_str); | |
774 | |
775 /* TODO: generic functions to split into a GList */ | |
776 status_array = g_strsplit(status_str, "|", 0); | |
777 for (list = NULL, i = 0; | |
778 status_array[i]; | |
779 i++) | |
780 { | |
781 list = g_list_append(list, status_array[i]); | |
782 } | |
783 | |
784 /* Example fields: |s|0|ss|Offline */ | |
785 status_code = g_list_nth_data(list, 2); | |
786 status_text = g_list_nth_data(list, 4); | |
787 | |
788 blist = gaim_get_blist(); | |
789 | |
790 /* Add buddy if not found */ | |
791 buddy = gaim_find_buddy(session->account, username); | |
792 if (!buddy) | |
793 { | |
794 /* TODO: gaim aliases, userids and usernames */ | |
795 gaim_debug_info("msim", "msim_status: making new buddy for %s\n", username); | |
796 buddy = gaim_buddy_new(session->account, username, NULL); | |
797 | |
798 /* TODO: sometimes (when click on it), buddy list disappears. Fix. */ | |
799 gaim_blist_add_buddy(buddy, NULL, NULL, NULL); | |
800 } else { | |
801 gaim_debug_info("msim", "msim_status: found buddy %s\n", username); | |
802 } | |
803 | |
804 /* For now, always set status to online. | |
805 * TODO: make status reflect reality | |
806 * TODO: show headline */ | |
807 presence = gaim_presence_new_for_buddy(buddy); | |
808 gaim_presence_set_status_active(presence, MSIM_STATUS_ONLINE, TRUE); | |
809 | |
810 g_strfreev(status_array); | |
811 g_list_free(list); | |
812 g_hash_table_destroy(body); | |
813 g_hash_table_destroy(userinfo); | |
814 /* Do not free status_str - it will be freed by g_hash_table_destroy on session->userid_lookup_cb_data */ | |
815 } | |
816 | |
817 /** | |
818 * Process incoming status messages. | |
819 * | |
820 * @param session | |
821 * @param table Status update message. | |
822 * | |
823 * @return 0, since 'table' can be freed. | |
824 */ | |
825 static int msim_status(MsimSession *session, GHashTable *table) | |
826 { | |
827 gchar *status_str; | |
828 gchar *userid; | |
829 | |
830 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | |
831 g_return_val_if_fail(table != NULL, 0); | |
832 | |
833 status_str = g_hash_table_lookup(table, "msg"); | |
834 if (!status_str) | |
835 { | |
836 gaim_debug_info("msim", "msim_status: bm=100 but no status msg\n"); | |
837 return 0; | |
838 } | |
839 | |
840 userid = g_hash_table_lookup(table, "f"); | |
841 if (!userid) | |
842 { | |
843 gaim_debug_info("msim", "msim_status: bm=100 but no f field\n"); | |
844 return 0; | |
845 } | |
846 | |
847 /* TODO: if buddies were identified on buddy list by uid, wouldn't have to lookup | |
848 * before updating the status! Much more efficient. */ | |
849 gaim_debug_info("msim", "msim_status: got status msg <%s> for <%s>, scheduling lookup\n", | |
850 status_str, userid); | |
851 | |
852 /* Actually update status once obtain username */ | |
853 msim_lookup_user(session, userid, msim_status_cb, g_strdup(status_str)); | |
854 | |
855 return 0; | |
856 } | |
857 | |
858 | |
859 /** | |
860 * Process a message. | |
861 * | |
862 * @param gc Connection. | |
863 * @param table Any message from the server. | |
864 * | |
865 * @return The return value of the function used to process the message, or -1 if | |
866 * called with invalid parameters. | |
867 */ | |
868 static int msim_process(GaimConnection *gc, GHashTable *table) | |
869 { | |
870 MsimSession *session; | |
871 | |
872 g_return_val_if_fail(gc != NULL, -1); | |
873 g_return_val_if_fail(table != NULL, -1); | |
874 | |
875 session = (MsimSession*)gc->proto_data; | |
876 | |
877 printf("-------- message -------------\n"); | |
878 g_hash_table_foreach(table, print_hash_item, NULL); | |
879 printf("------------------------------\n"); | |
880 | |
881 if (g_hash_table_lookup(table, "nc")) | |
882 { | |
883 return msim_login_challenge(session, table); | |
884 } else if (g_hash_table_lookup(table, "sesskey")) { | |
885 printf("SESSKEY=<%s>\n", (gchar*)g_hash_table_lookup(table, "sesskey")); | |
886 | |
887 gaim_connection_update_progress(gc, "Connected", 3, 4); | |
888 | |
889 session->sesskey = g_strdup(g_hash_table_lookup(table, "sesskey")); | |
890 | |
891 /* Comes with: proof,profileid,userid,uniquenick -- all same values | |
892 * (at least for me). */ | |
893 session->userid = g_strdup(g_hash_table_lookup(table, "userid")); | |
894 | |
895 gaim_connection_set_state(gc, GAIM_CONNECTED); | |
896 | |
897 return 0; | |
898 } else if (g_hash_table_lookup(table, "bm")) { | |
899 guint bm; | |
900 | |
901 bm = atoi(g_hash_table_lookup(table, "bm")); | |
902 switch (bm) | |
903 { | |
904 case MSIM_BM_STATUS: | |
905 return msim_status(session, table); | |
906 case MSIM_BM_INSTANT: | |
907 return msim_incoming_im(session, table); | |
908 default: | |
909 /* Not really an IM, but show it for informational | |
910 * purposes during development. */ | |
911 return msim_incoming_im(session, table); | |
912 } | |
913 | |
914 if (bm == MSIM_BM_STATUS) | |
915 { | |
916 return msim_status(session, table); | |
917 } else { /* else if strcmp(bm, "1") == 0) */ | |
918 return msim_incoming_im(session, table); | |
919 } | |
920 } else if (g_hash_table_lookup(table, "rid")) { | |
921 return msim_process_reply(session, table); | |
922 } else if (g_hash_table_lookup(table, "error")) { | |
923 return msim_error(session, table); | |
924 } else if (g_hash_table_lookup(table, "ka")) { | |
925 gaim_debug_info("msim", "msim_process: got keep alive\n"); | |
926 return 0; | |
927 } else { | |
928 printf("<<unhandled>>\n"); | |
929 return 0; | |
930 } | |
931 } | |
932 | |
933 /** | |
934 * Callback when input available. | |
935 * | |
936 * @param gc_uncasted A GaimConnection pointer. | |
937 * @param source File descriptor. | |
938 * @param cond GAIM_INPUT_READ | |
939 * | |
940 * Reads the input, and dispatches calls msim_process to handle it. | |
941 */ | |
942 static void msim_input_cb(gpointer gc_uncasted, gint source, GaimInputCondition cond) | |
943 { | |
944 GaimConnection *gc; | |
945 GaimAccount *account; | |
946 MsimSession *session; | |
947 gchar *end; | |
948 int n; | |
949 | |
950 g_return_if_fail(gc_uncasted != NULL); | |
951 g_return_if_fail(source >= 0); /* Note: 0 is a valid fd */ | |
952 | |
953 gc = (GaimConnection*)(gc_uncasted); | |
954 account = gaim_connection_get_account(gc); | |
955 session = gc->proto_data; | |
956 | |
957 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
958 | |
959 g_assert(cond == GAIM_INPUT_READ); | |
960 | |
961 /* Only can handle so much data at once... | |
962 * If this happens, try recompiling with a higher MSIM_READ_BUF_SIZE. | |
963 * Should be large enough to hold the largest protocol message. | |
964 */ | |
965 if (session->rxoff == MSIM_READ_BUF_SIZE) | |
966 { | |
967 gaim_debug_error("msim", "msim_input_cb: %d-byte read buffer full!\n", | |
968 MSIM_READ_BUF_SIZE); | |
969 gaim_connection_error(gc, "Read buffer full"); | |
970 /* TODO: fix 100% CPU after closing */ | |
971 close(source); | |
972 return; | |
973 } | |
974 | |
975 gaim_debug_info("msim", "buffer at %d (max %d), reading up to %d\n", | |
976 session->rxoff, MSIM_READ_BUF_SIZE, MSIM_READ_BUF_SIZE - session->rxoff); | |
977 | |
978 /* Read into buffer. On Win32, need recv() not read(). session->fd also holds | |
979 * the file descriptor, but it sometimes differs from the 'source' parameter. | |
980 */ | |
981 n = recv(session->fd, session->rxbuf + session->rxoff, MSIM_READ_BUF_SIZE - session->rxoff, 0); | |
982 | |
983 if (n < 0 && errno == EAGAIN) | |
984 { | |
985 return; | |
986 } | |
987 else if (n < 0) | |
988 { | |
989 gaim_connection_error(gc, "Read error"); | |
990 gaim_debug_error("msim", "msim_input_cb: read error, ret=%d, " | |
991 "error=%s, source=%d, fd=%d (%X))\n", | |
992 n, strerror(errno), source, session->fd, session->fd); | |
993 close(source); | |
994 return; | |
995 } | |
996 else if (n == 0) | |
997 { | |
998 gaim_debug_info("msim", "msim_input_cb: server disconnected\n"); | |
999 gaim_connection_error(gc, "Server has disconnected"); | |
1000 return; | |
1001 } | |
1002 | |
1003 /* Null terminate */ | |
1004 session->rxbuf[session->rxoff + n] = 0; | |
1005 | |
1006 /* Check for embedded NULs. I don't handle them, and they shouldn't occur. */ | |
1007 if (strlen(session->rxbuf + session->rxoff) != n) | |
1008 { | |
1009 /* Occurs after login, but it is not a null byte. */ | |
1010 gaim_debug_info("msim", "msim_input_cb: strlen=%d, but read %d bytes" | |
1011 "--null byte encountered?\n", strlen(session->rxbuf + session->rxoff), n); | |
1012 //gaim_connection_error(gc, "Invalid message - null byte on input"); | |
1013 return; | |
1014 } | |
1015 | |
1016 session->rxoff += n; | |
1017 gaim_debug_info("msim", "msim_input_cb: read=%d\n", n); | |
1018 | |
1019 //printf("buf=<%s>\n", session->rxbuf); | |
1020 | |
1021 /* Look for \\final\\ end markers. If found, process message. */ | |
1022 while((end = strstr(session->rxbuf, MSIM_FINAL_STRING))) | |
1023 { | |
1024 GHashTable *table; | |
1025 | |
1026 //printf("in loop: buf=<%s>\n", session->rxbuf); | |
1027 *end = 0; | |
1028 table = msim_parse(g_strdup(session->rxbuf)); | |
1029 if (!table) | |
1030 { | |
1031 gaim_debug_info("msim", "msim_input_cb: couldn't parse <%s>\n", session->rxbuf); | |
1032 gaim_connection_error(gc, "Unparseable message"); | |
1033 } | |
1034 else | |
1035 { | |
1036 /* Process message. Returns 0 to free */ | |
1037 if (msim_process(gc, table) == 0) | |
1038 g_hash_table_destroy(table); | |
1039 } | |
1040 | |
1041 /* Move remaining part of buffer to beginning. */ | |
1042 session->rxoff -= strlen(session->rxbuf) + strlen(MSIM_FINAL_STRING); | |
1043 memmove(session->rxbuf, end + strlen(MSIM_FINAL_STRING), | |
1044 MSIM_READ_BUF_SIZE - (end + strlen(MSIM_FINAL_STRING) - session->rxbuf)); | |
1045 | |
1046 /* Clear end of buffer */ | |
1047 //memset(end, 0, MSIM_READ_BUF_SIZE - (end - session->rxbuf)); | |
1048 } | |
1049 } | |
1050 | |
1051 /** | |
1052 * Callback when connected. Sets up input handlers. | |
1053 * | |
1054 * @param data A GaimConnection pointer. | |
1055 * @param source File descriptor. | |
1056 * @param error_message | |
1057 */ | |
1058 static void msim_connect_cb(gpointer data, gint source, const gchar *error_message) | |
1059 { | |
1060 GaimConnection *gc; | |
1061 MsimSession *session; | |
1062 | |
1063 g_return_if_fail(data != NULL); | |
1064 | |
1065 gc = (GaimConnection*)data; | |
1066 session = gc->proto_data; | |
1067 | |
1068 if (source < 0) | |
1069 { | |
1070 gaim_connection_error(gc, "Couldn't connect to host"); | |
1071 gaim_connection_error(gc, g_strdup_printf("Couldn't connect to host: %s (%d)", | |
1072 error_message, source)); | |
1073 return; | |
1074 } | |
1075 | |
1076 session->fd = source; | |
1077 | |
1078 gc->inpa = gaim_input_add(source, GAIM_INPUT_READ, msim_input_cb, gc); | |
1079 } | |
1080 | |
1081 /* Session methods */ | |
1082 | |
1083 /** | |
1084 * Create a new MSIM session. | |
1085 * | |
1086 * @param acct The account to create the session from. | |
1087 * | |
1088 * @return Pointer to a new session. Free with msim_session_destroy. | |
1089 */ | |
1090 static MsimSession *msim_session_new(GaimAccount *acct) | |
1091 { | |
1092 MsimSession *session; | |
1093 | |
1094 g_return_val_if_fail(acct != NULL, NULL); | |
1095 | |
1096 session = g_new0(MsimSession, 1); | |
1097 | |
1098 session->magic = MSIM_SESSION_STRUCT_MAGIC; | |
1099 session->account = acct; | |
1100 session->gc = gaim_account_get_connection(acct); | |
1101 session->fd = -1; | |
1102 session->user_lookup_cb = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, NULL); /* do NOT free function pointers! */ | |
1103 session->user_lookup_cb_data = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free); | |
1104 session->user_lookup_cache = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_hash_table_destroy); | |
1105 session->rxoff = 0; | |
1106 session->rxbuf = g_new0(gchar, MSIM_READ_BUF_SIZE); | |
1107 | |
1108 return session; | |
1109 } | |
1110 | |
1111 /** | |
1112 * Free a session. | |
1113 * | |
1114 * @param session The session to destroy. | |
1115 */ | |
1116 static void msim_session_destroy(MsimSession *session) | |
1117 { | |
1118 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
1119 | |
1120 session->magic = -1; | |
1121 | |
1122 g_free(session->rxbuf); | |
1123 g_free(session->userid); | |
1124 g_free(session->sesskey); | |
1125 | |
1126 g_free(session); | |
1127 } | |
1128 | |
1129 /** | |
1130 * Start logging in to the MSIM servers. | |
1131 * | |
1132 * @param acct Account information to use to login. | |
1133 */ | |
1134 static void msim_login(GaimAccount *acct) | |
1135 { | |
1136 GaimConnection *gc; | |
1137 const char *host; | |
1138 int port; | |
1139 | |
1140 g_return_if_fail(acct != NULL); | |
1141 | |
1142 gaim_debug_info("myspace", "logging in %s\n", acct->username); | |
1143 | |
1144 gc = gaim_account_get_connection(acct); | |
1145 gc->proto_data = msim_session_new(acct); | |
1146 | |
1147 /* 1. connect to server */ | |
1148 gaim_connection_update_progress(gc, "Connecting", | |
1149 0, /* which connection step this is */ | |
1150 4); /* total number of steps */ | |
1151 | |
1152 /* TODO: GUI option to be user-modifiable. */ | |
1153 host = gaim_account_get_string(acct, "server", MSIM_SERVER); | |
1154 port = gaim_account_get_int(acct, "port", MSIM_PORT); | |
1155 /* TODO: connect */ | |
1156 /* From gaim.sf.net/api: | |
1157 * """Note that this function name can be misleading--although it is called | |
1158 * "proxy connect," it is used for establishing any outgoing TCP connection, | |
1159 * whether through a proxy or not.""" */ | |
1160 | |
1161 /* Calls msim_connect_cb when connected. */ | |
1162 if (gaim_proxy_connect(gc, acct, host, port, msim_connect_cb, gc) == NULL) | |
1163 { | |
1164 /* TODO: try other ports if in auto mode, then save | |
1165 * working port and try that first next time. */ | |
1166 gaim_connection_error(gc, "Couldn't create socket"); | |
1167 return; | |
1168 } | |
1169 | |
1170 } | |
1171 | |
1172 | |
1173 /** | |
1174 * Close the connection. | |
1175 * | |
1176 * @param gc The connection. | |
1177 */ | |
1178 static void msim_close(GaimConnection *gc) | |
1179 { | |
1180 g_return_if_fail(gc != NULL); | |
1181 | |
1182 msim_session_destroy(gc->proto_data); | |
1183 } | |
1184 | |
1185 /** | |
1186 * Return the icon name for a buddy and account. | |
1187 * | |
1188 * @param acct The account to find the icon for. | |
1189 * @param buddy The buddy to find the icon for, or NULL for the accoun icon. | |
1190 * | |
1191 * @return The base icon name string. | |
1192 */ | |
1193 static const gchar *msim_list_icon(GaimAccount *acct, GaimBuddy *buddy) | |
1194 { | |
1195 /* TODO: use a MySpace icon. hbons submitted one to | |
1196 * http://developer.pidgin.im/wiki/MySpaceIM - tried placing in | |
1197 * C:\cygwin\home\Jeff\gaim-2.0.0beta6\gtk\pixmaps\status\default | |
1198 * and returning "myspace" but icon shows up blank. | |
1199 */ | |
1200 if (acct == NULL) | |
1201 { | |
1202 gaim_debug_info("msim", "msim_list_icon: acct == NULL!\n"); | |
1203 //exit(-2); | |
1204 } | |
1205 return "meanwhile"; | |
1206 } | |
1207 | |
1208 /** | |
1209 * Check if a string is a userid (all numeric). | |
1210 * | |
1211 * @param user The user id, email, or name. | |
1212 * | |
1213 * @return TRUE if is userid, FALSE if not. | |
1214 */ | |
1215 static inline gboolean msim_is_userid(const gchar *user) | |
1216 { | |
1217 g_return_val_if_fail(user != NULL, FALSE); | |
1218 | |
1219 return strspn(user, "0123456789") == strlen(user); | |
1220 } | |
1221 | |
1222 /** | |
1223 * Check if a string is an email address (contains an @). | |
1224 * | |
1225 * @param user The user id, email, or name. | |
1226 * | |
1227 * @return TRUE if is an email, FALSE if not. | |
1228 * | |
1229 * This function is not intended to be used as a generic | |
1230 * means of validating email addresses, but to distinguish | |
1231 * between a user represented by an email address from | |
1232 * other forms of identification. | |
1233 */ | |
1234 static inline gboolean msim_is_email(const gchar *user) | |
1235 { | |
1236 g_return_val_if_fail(user != NULL, FALSE); | |
1237 | |
1238 return strchr(user, '@') != NULL; | |
1239 } | |
1240 | |
1241 | |
1242 /** | |
1243 * Asynchronously lookup user information, calling callback when receive result. | |
1244 * | |
1245 * @param session | |
1246 * @param user The user id, email address, or username. | |
1247 * @param cb Callback, called with user information when available. | |
1248 * @param data An arbitray data pointer passed to the callback. | |
1249 */ | |
1250 static void msim_lookup_user(MsimSession *session, const gchar *user, MSIM_USER_LOOKUP_CB cb, gpointer data) | |
1251 { | |
1252 gchar *field_name; | |
1253 gchar *msg_string; | |
1254 guint rid, cmd, dsn, lid; | |
1255 | |
1256 g_return_if_fail(MSIM_SESSION_VALID(session)); | |
1257 g_return_if_fail(user != NULL); | |
1258 g_return_if_fail(cb != NULL); | |
1259 | |
1260 gaim_debug_info("msim", "msim_lookup_userid", "asynchronously looking up <%s>\n", user); | |
1261 | |
1262 /* TODO: check if this user's info was cached and fresh; if so return immediately */ | |
1263 #if 0 | |
1264 /* If already know userid, then call callback immediately */ | |
1265 cached_userid = g_hash_table_lookup(session->userid_cache, who); | |
1266 if (cached_userid && !by_userid) | |
1267 { | |
1268 cb(cached_userid, NULL, NULL, data); | |
1269 return; | |
1270 } | |
1271 #endif | |
1272 | |
1273 rid = rand(); //om(); | |
1274 | |
1275 /* Setup callback. Response will be associated with request using 'rid'. */ | |
1276 g_hash_table_insert(session->user_lookup_cb, GUINT_TO_POINTER(rid), cb); | |
1277 g_hash_table_insert(session->user_lookup_cb_data, GUINT_TO_POINTER(rid), data); | |
1278 | |
1279 /* Send request */ | |
1280 | |
1281 cmd = 1; | |
1282 | |
1283 if (msim_is_userid(user)) | |
1284 { | |
1285 /* TODO: document cmd,dsn,lid */ | |
1286 field_name = "UserID"; | |
1287 dsn = 4; | |
1288 lid = 3; | |
1289 } else if (msim_is_email(user)) { | |
1290 field_name = "Email"; | |
1291 dsn = 5; | |
1292 lid = 7; | |
1293 } else { | |
1294 field_name = "UserName"; | |
1295 dsn = 5; | |
1296 lid = 7; | |
1297 } | |
1298 | |
1299 msg_string = g_strdup_printf("\\persist\\1\\sesskey\\%s\\cmd\\1\\dsn\\%d\\uid\\%s\\lid\\%d\\rid\\%d\\body\\%s=%s\\final\\", | |
1300 session->sesskey, dsn, session->userid, lid, rid, field_name, user); | |
1301 | |
1302 msim_send(session, msg_string); | |
1303 } | |
1304 | |
1305 /** | |
1306 * Schedule an IM to be sent once the user ID is looked up. | |
1307 * | |
1308 * @param gc Connection. | |
1309 * @param who A user id, email, or username to send the message to. | |
1310 * @param message Instant message text to send. | |
1311 * @param flags Flags. | |
1312 * | |
1313 * @return 1 in all cases, even if the message delivery is destined to fail. | |
1314 * | |
1315 * Allows sending to a user by username, email address, or userid. If | |
1316 * a username or email address is given, the userid must be looked up. | |
1317 * This function does that by calling msim_lookup_user(), setting up | |
1318 * a msim_send_im_by_userid_cb() callback function called when the userid | |
1319 * response is received from the server. | |
1320 * | |
1321 * The callback function calls msim_send_im_by_userid() to send the actual | |
1322 * instant message. If a userid is specified directly, this function is called | |
1323 * immediately here. | |
1324 */ | |
1325 static int msim_send_im(GaimConnection *gc, const char *who, | |
1326 const char *message, GaimMessageFlags flags) | |
1327 { | |
1328 MsimSession *session; | |
1329 const char *from_username = gc->account->username; | |
1330 send_im_cb_struct *cbinfo; | |
1331 | |
1332 g_return_val_if_fail(gc != NULL, 0); | |
1333 g_return_val_if_fail(who != NULL, 0); | |
1334 g_return_val_if_fail(message != NULL, 0); | |
1335 | |
1336 gaim_debug_info("msim", "sending message from %s to %s: %s\n", | |
1337 from_username, who, message); | |
1338 | |
1339 session = gc->proto_data; | |
1340 | |
1341 /* If numeric ID, can send message immediately without userid lookup */ | |
1342 if (msim_is_userid(who)) | |
1343 { | |
1344 gaim_debug_info("msim", "msim_send_im: numeric 'who' detected, sending asap\n"); | |
1345 msim_send_im_by_userid(session, who, message, flags); | |
1346 return 1; | |
1347 } | |
1348 | |
1349 /* Otherwise, add callback to IM when userid of destination is available */ | |
1350 | |
1351 /* Setup a callback for when the userid is available */ | |
1352 cbinfo = g_new0(send_im_cb_struct, 1); | |
1353 cbinfo->who = g_strdup(who); | |
1354 cbinfo->message = g_strdup(message); | |
1355 cbinfo->flags = flags; | |
1356 | |
1357 /* Send the request to lookup the userid */ | |
1358 msim_lookup_user(session, who, msim_send_im_by_userid_cb, cbinfo); | |
1359 | |
1360 /* msim_send_im_by_userid_cb will now be called once userid is looked up */ | |
1361 | |
1362 /* Return 1 to have Gaim show this IM as being sent, 0 to not. I always | |
1363 * return 1 even if the message could not be sent, since I don't know if | |
1364 * it has failed yet--because the IM is only sent after the userid is | |
1365 * retrieved from the server (which happens after this function returns). | |
1366 * | |
1367 * TODO: In MySpace, you login with your email address, but don't talk to other | |
1368 * users using their email address. So there is currently an asymmetry in the | |
1369 * IM windows when using this plugin: | |
1370 * | |
1371 * you@example.com: hello | |
1372 * some_other_user: what's going on? | |
1373 * you@example.com: just coding a prpl | |
1374 * | |
1375 * TODO: Make the sent IM's appear as from the user's username, instead of | |
1376 * their email address. Gaim uses the login (in MSIM, the email)--change this. | |
1377 */ | |
1378 return 1; | |
1379 } | |
1380 | |
1381 /** | |
1382 * Obtain the status text for a buddy. | |
1383 * | |
1384 * @param buddy The buddy to obtain status text for. | |
1385 * | |
1386 * @return Status text. | |
1387 * | |
1388 * Currently returns the display name. | |
1389 */ | |
1390 static char *msim_status_text(GaimBuddy *buddy) | |
1391 { | |
1392 MsimSession *session; | |
1393 GHashTable *userinfo; | |
1394 gchar *display_name; | |
1395 | |
1396 g_return_val_if_fail(buddy != NULL, NULL); | |
1397 | |
1398 session = (MsimSession*)buddy->account->gc->proto_data; | |
1399 g_assert(MSIM_SESSION_VALID(session)); | |
1400 g_assert(session->user_lookup_cache != NULL); | |
1401 | |
1402 userinfo = g_hash_table_lookup(session->user_lookup_cache, buddy->name); | |
1403 if (!userinfo) | |
1404 { | |
1405 return g_strdup(""); | |
1406 } | |
1407 | |
1408 display_name = g_hash_table_lookup(userinfo, "DisplayName"); | |
1409 g_assert(display_name != NULL); | |
1410 | |
1411 return g_strdup(display_name); | |
1412 } | |
1413 | |
1414 /** | |
1415 * Obtain the tooltip text for a buddy. | |
1416 * | |
1417 * @param buddy Buddy to obtain tooltip text on. | |
1418 * @param user_info Variable modified to have the tooltip text. | |
1419 * @param full TRUE if should obtain full tooltip text. | |
1420 * | |
1421 */ | |
1422 static void msim_tooltip_text(GaimBuddy *buddy, GaimNotifyUserInfo *user_info, gboolean full) | |
1423 { | |
1424 g_return_if_fail(buddy != NULL); | |
1425 g_return_if_fail(user_info != NULL); | |
1426 | |
1427 if (GAIM_BUDDY_IS_ONLINE(buddy)) | |
1428 { | |
1429 MsimSession *session; | |
1430 GHashTable *userinfo; | |
1431 | |
1432 session = (MsimSession*)buddy->account->gc->proto_data; | |
1433 | |
1434 g_assert(MSIM_SESSION_VALID(session)); | |
1435 g_assert(session->user_lookup_cache); | |
1436 | |
1437 userinfo = g_hash_table_lookup(session->user_lookup_cache, buddy->name); | |
1438 | |
1439 g_assert(userinfo != NULL); | |
1440 | |
1441 // TODO: if (full), do something different | |
1442 gaim_notify_user_info_add_pair(user_info, "User ID", g_hash_table_lookup(userinfo, "UserID")); | |
1443 gaim_notify_user_info_add_pair(user_info, "Display Name", g_hash_table_lookup(userinfo, "DisplayName")); | |
1444 gaim_notify_user_info_add_pair(user_info, "User Name", g_hash_table_lookup(userinfo, "UserName")); | |
1445 gaim_notify_user_info_add_pair(user_info, "Total Friends", g_hash_table_lookup(userinfo, "TotalFriends")); | |
1446 gaim_notify_user_info_add_pair(user_info, "Song", | |
1447 g_strdup_printf("%s - %s", | |
1448 (gchar*)g_hash_table_lookup(userinfo, "BandName"), | |
1449 (gchar*)g_hash_table_lookup(userinfo, "SongName"))); | |
1450 } | |
1451 } | |
1452 | |
1453 /** Callbacks called by Gaim, to access this plugin. */ | |
1454 static GaimPluginProtocolInfo prpl_info = | |
1455 { | |
1456 OPT_PROTO_MAIL_CHECK,/* options - TODO: myspace will notify of mail */ | |
1457 NULL, /* user_splits */ | |
1458 NULL, /* protocol_options */ | |
1459 NO_BUDDY_ICONS, /* icon_spec - TODO: eventually should add this */ | |
1460 msim_list_icon, /* list_icon */ | |
1461 NULL, /* list_emblems */ | |
1462 msim_status_text, /* status_text */ | |
1463 msim_tooltip_text, /* tooltip_text */ | |
1464 msim_status_types, /* status_types */ | |
1465 NULL, /* blist_node_menu */ | |
1466 NULL, /* chat_info */ | |
1467 NULL, /* chat_info_defaults */ | |
1468 msim_login, /* login */ | |
1469 msim_close, /* close */ | |
1470 msim_send_im, /* send_im */ | |
1471 NULL, /* set_info */ | |
1472 NULL, /* send_typing */ | |
1473 NULL, /* get_info */ | |
1474 NULL, /* set_away */ | |
1475 NULL, /* set_idle */ | |
1476 NULL, /* change_passwd */ | |
1477 NULL, /* add_buddy */ | |
1478 NULL, /* add_buddies */ | |
1479 NULL, /* remove_buddy */ | |
1480 NULL, /* remove_buddies */ | |
1481 NULL, /* add_permit */ | |
1482 NULL, /* add_deny */ | |
1483 NULL, /* rem_permit */ | |
1484 NULL, /* rem_deny */ | |
1485 NULL, /* set_permit_deny */ | |
1486 NULL, /* join_chat */ | |
1487 NULL, /* reject chat invite */ | |
1488 NULL, /* get_chat_name */ | |
1489 NULL, /* chat_invite */ | |
1490 NULL, /* chat_leave */ | |
1491 NULL, /* chat_whisper */ | |
1492 NULL, /* chat_send */ | |
1493 NULL, /* keepalive */ | |
1494 NULL, /* register_user */ | |
1495 NULL, /* get_cb_info */ | |
1496 NULL, /* get_cb_away */ | |
1497 NULL, /* alias_buddy */ | |
1498 NULL, /* group_buddy */ | |
1499 NULL, /* rename_group */ | |
1500 NULL, /* buddy_free */ | |
1501 NULL, /* convo_closed */ | |
1502 NULL, /* normalize */ | |
1503 NULL, /* set_buddy_icon */ | |
1504 NULL, /* remove_group */ | |
1505 NULL, /* get_cb_real_name */ | |
1506 NULL, /* set_chat_topic */ | |
1507 NULL, /* find_blist_chat */ | |
1508 NULL, /* roomlist_get_list */ | |
1509 NULL, /* roomlist_cancel */ | |
1510 NULL, /* roomlist_expand_category */ | |
1511 NULL, /* can_receive_file */ | |
1512 NULL, /* send_file */ | |
1513 NULL, /* new_xfer */ | |
1514 NULL, /* offline_message */ | |
1515 NULL, /* whiteboard_prpl_ops */ | |
1516 NULL, /* send_raw */ | |
1517 NULL /* roomlist_room_serialize */ | |
1518 }; | |
1519 | |
1520 | |
1521 | |
1522 /** Based on MSN's plugin info comments. */ | |
1523 static GaimPluginInfo info = | |
1524 { | |
1525 GAIM_PLUGIN_MAGIC, | |
1526 GAIM_MAJOR_VERSION, | |
1527 GAIM_MINOR_VERSION, | |
1528 GAIM_PLUGIN_PROTOCOL, /**< type */ | |
1529 NULL, /**< ui_requirement */ | |
1530 0, /**< flags */ | |
1531 NULL, /**< dependencies */ | |
1532 GAIM_PRIORITY_DEFAULT, /**< priority */ | |
1533 | |
1534 "prpl-myspace", /**< id */ | |
1535 "MySpaceIM", /**< name */ | |
1536 "0.4", /**< version */ | |
1537 /** summary */ | |
1538 "MySpaceIM Protocol Plugin", | |
1539 /** description */ | |
1540 "MySpaceIM Protocol Plugin", | |
1541 "Jeff Connelly <myspaceim@xyzzy.cjb.net>", /**< author */ | |
1542 "http://developer.pidgin.im/wiki/MySpaceIM/", /**< homepage */ | |
1543 | |
1544 NULL, /**< load */ | |
1545 NULL, /**< unload */ | |
1546 NULL, /**< destroy */ | |
1547 NULL, /**< ui_info */ | |
1548 &prpl_info, /**< extra_info */ | |
1549 NULL, /**< prefs_info */ | |
1550 | |
1551 /* msim_actions */ | |
1552 NULL | |
1553 }; | |
1554 | |
1555 | |
1556 GAIM_INIT_PLUGIN(myspace, init_plugin, info); |