Mercurial > pidgin.yaz
comparison libpurple/protocols/myspace/myspace.c @ 16747:496855295bd7
Use MsimMessage via msim_send(), instead of msim_send_raw(). This is a more
object-oriented approach that simplifies sending of messages.
Support for:
- Several field types used in the protocol (integer, string, binary).
- Transparently escape strings and base64 binary data as necessary.
- Multiple values per key are supported (unlike GHashTable), required for
WebChallenge message.
- Order is preserved (unlike GHashTable). Also required for WebChallenge.
Note that currently MsimMessage is only used for sending messages.
-
author | Jeffrey Connelly <jaconnel@calpoly.edu> |
---|---|
date | Wed, 23 May 2007 04:18:05 +0000 |
parents | 52357f6392a4 |
children | 0fa4a3e9b318 |
comparison
equal
deleted
inserted
replaced
16746:52357f6392a4 | 16747:496855295bd7 |
---|---|
4 * | 4 * |
5 * Copyright (C) 2007, Jeff Connelly <jeff2@homing.pidgin.im> | 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 mockprpl: |
10 * http://snarfed.org/space/purple+mock+protocol+plugin | 10 * http://snarfed.org/space/purple+mock+protocol+plugin |
11 * Copyright (C) 2004-2007, Ryan Barrett <mockprpl@ryanb.org> | 11 * Copyright (C) 2004-2007, Ryan Barrett <mockprpl@ryanb.org> |
12 * | 12 * |
13 * and some constructs also based on existing Purple plugins, which are: | 13 * and some constructs also based on existing Purple plugins, which are: |
14 * Copyright (C) 2003, Robbert Haarman <purple@inglorion.net> | 14 * Copyright (C) 2003, Robbert Haarman <purple@inglorion.net> |
31 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | 31 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
32 */ | 32 */ |
33 | 33 |
34 #define PURPLE_PLUGIN | 34 #define PURPLE_PLUGIN |
35 | 35 |
36 #include <string.h> | |
37 #include <errno.h> /* for EAGAIN */ | |
38 #include <stdarg.h> | |
39 | |
40 #include <glib.h> | |
41 | |
42 #ifdef _WIN32 | |
43 #include "win32dep.h" | |
44 #else | |
45 /* For recv() and send(); needed to match Win32 */ | |
46 #include <sys/types.h> | |
47 #include <sys/socket.h> | |
48 #endif | |
49 | |
50 #include "internal.h" | |
51 | |
52 #include "notify.h" | |
53 #include "plugin.h" | |
54 #include "accountopt.h" | |
55 #include "version.h" | |
56 #include "cipher.h" /* for SHA-1 */ | |
57 #include "util.h" /* for base64 */ | |
58 #include "debug.h" /* for purple_debug_info */ | |
59 | |
60 #include "myspace.h" | 36 #include "myspace.h" |
61 #include "message.h" | 37 #include "message.h" |
62 | 38 |
63 /** | 39 /** |
64 * Load the plugin. | 40 * Load the plugin. |
65 */ | 41 */ |
66 static gboolean msim_load(PurplePlugin *plugin) | 42 gboolean msim_load(PurplePlugin *plugin) |
67 { | 43 { |
68 #ifdef MSIM_USE_PURPLE_RC4 | 44 #ifdef MSIM_USE_PURPLE_RC4 |
69 /* If compiled to use RC4 from libpurple, check if it is really there. */ | 45 /* If compiled to use RC4 from libpurple, check if it is really there. */ |
70 if (!purple_ciphers_find_cipher("rc4")) | 46 if (!purple_ciphers_find_cipher("rc4")) |
71 { | 47 { |
72 purple_debug_error("msim", "compiled with MSIM_USE_PURPLE_RC4 but rc4 not in libpurple - not loading MySpaceIM plugin!\n"); | 48 purple_debug_error("msim", "compiled with MSIM_USE_PURPLE_RC4 but rc4 not in libpurple - not loading MySpaceIM plugin!\n"); |
73 purple_notify_error(plugin, _("Missing Cipher"), | 49 purple_notify_error(plugin, _("Missing Cipher"), |
74 _("The RC4 cipher could not be found"), | 50 _("The RC4 cipher could not be found"), |
75 _("Recompile without MSIM_USE_PURPLE_RC4, or upgrade " | 51 _("Recompile without MSIM_USE_PURPLE_RC4, or upgrade " |
76 "to a libpurple with RC4 support (>= 2.0.1). MySpaceIM " | 52 "to a libpurple with RC4 support (>= 2.0.1). MySpaceIM " |
77 "plugin will not be loaded."); | 53 "plugin will not be loaded.")); |
78 return FALSE; | 54 return FALSE; |
79 } | 55 } |
80 #endif | 56 #endif |
81 return TRUE; | 57 return TRUE; |
82 } | 58 } |
84 /** | 60 /** |
85 * Get possible user status types. Based on mockprpl. | 61 * Get possible user status types. Based on mockprpl. |
86 * | 62 * |
87 * @return GList of status types. | 63 * @return GList of status types. |
88 */ | 64 */ |
89 static GList *msim_status_types(PurpleAccount *acct) | 65 GList *msim_status_types(PurpleAccount *acct) |
90 { | 66 { |
91 GList *types; | 67 GList *types; |
92 PurpleStatusType *type; | 68 PurpleStatusType *type; |
93 | 69 |
94 purple_debug_info("myspace", "returning status types for %s: %s, %s, %s\n", | 70 purple_debug_info("myspace", "returning status types for %s: %s, %s, %s\n", |
132 * @param acct The account to find the icon for, or NULL for protocol icon. | 108 * @param acct The account to find the icon for, or NULL for protocol icon. |
133 * @param buddy The buddy to find the icon for, or NULL for the account icon. | 109 * @param buddy The buddy to find the icon for, or NULL for the account icon. |
134 * | 110 * |
135 * @return The base icon name string. | 111 * @return The base icon name string. |
136 */ | 112 */ |
137 static const gchar *msim_list_icon(PurpleAccount *acct, PurpleBuddy *buddy) | 113 const gchar *msim_list_icon(PurpleAccount *acct, PurpleBuddy *buddy) |
138 { | 114 { |
139 /* Use a MySpace icon submitted by hbons submitted one at | 115 /* Use a MySpace icon submitted by hbons submitted one at |
140 * http://developer.pidgin.im/wiki/MySpaceIM. */ | 116 * http://developer.pidgin.im/wiki/MySpaceIM. */ |
141 return "myspace"; | 117 return "myspace"; |
142 } | 118 } |
144 /** | 120 /** |
145 * Unescape a protocol message. | 121 * Unescape a protocol message. |
146 * | 122 * |
147 * @return The unescaped message. Caller must g_free(). | 123 * @return The unescaped message. Caller must g_free(). |
148 */ | 124 */ |
149 static gchar *msim_unescape(const gchar *msg) | 125 gchar *msim_unescape(const gchar *msg) |
150 { | 126 { |
151 /* TODO: make more elegant, refactor with msim_escape */ | 127 /* TODO: make more elegant, refactor with msim_escape */ |
152 gchar *tmp, *ret; | 128 gchar *tmp, *ret; |
153 | 129 |
154 tmp = str_replace(msg, "/1", "/"); | 130 tmp = str_replace(msg, "/1", "/"); |
160 /** | 136 /** |
161 * Escape a protocol message. | 137 * Escape a protocol message. |
162 * | 138 * |
163 * @return The escaped message. Caller must g_free(). | 139 * @return The escaped message. Caller must g_free(). |
164 */ | 140 */ |
165 static gchar *msim_escape(const gchar *msg) | 141 gchar *msim_escape(const gchar *msg) |
166 { | 142 { |
167 /* TODO: make more elegant, refactor with msim_unescape */ | 143 /* TODO: make more elegant, refactor with msim_unescape */ |
168 gchar *tmp, *ret; | 144 gchar *tmp, *ret; |
169 | 145 |
170 tmp = str_replace(msg, "/", "/1"); | 146 tmp = str_replace(msg, "/", "/1"); |
186 * | 162 * |
187 * This string replace method is based on | 163 * This string replace method is based on |
188 * http://mail.gnome.org/archives/gtk-app-devel-list/2000-July/msg00201.html | 164 * http://mail.gnome.org/archives/gtk-app-devel-list/2000-July/msg00201.html |
189 * | 165 * |
190 */ | 166 */ |
191 static gchar *str_replace(const gchar* str, const gchar *old, const gchar *new) | 167 gchar *str_replace(const gchar* str, const gchar *old, const gchar *new) |
192 { | 168 { |
193 char **items; | 169 char **items; |
194 char *ret; | 170 char *ret; |
195 | 171 |
196 items = g_strsplit(str, old, -1); | 172 items = g_strsplit(str, old, -1); |
207 * @param msg The message string to parse, will be g_free()'d. | 183 * @param msg The message string to parse, will be g_free()'d. |
208 * | 184 * |
209 * @return Hash table of message. Caller should destroy with | 185 * @return Hash table of message. Caller should destroy with |
210 * g_hash_table_destroy() when done. | 186 * g_hash_table_destroy() when done. |
211 */ | 187 */ |
212 static GHashTable* msim_parse(gchar* msg) | 188 GHashTable* msim_parse(gchar* msg) |
213 { | 189 { |
214 GHashTable *table; | 190 GHashTable *table; |
215 gchar *token; | 191 gchar *token; |
216 gchar **tokens; | 192 gchar **tokens; |
217 gchar *key; | 193 gchar *key; |
290 * @param body_str The text of the dictionary to parse. Often the | 266 * @param body_str The text of the dictionary to parse. Often the |
291 * value for the 'body' field. | 267 * value for the 'body' field. |
292 * | 268 * |
293 * @return Hash table of the keys and values. Must g_hash_table_destroy() when done. | 269 * @return Hash table of the keys and values. Must g_hash_table_destroy() when done. |
294 */ | 270 */ |
295 static GHashTable *msim_parse_body(const gchar *body_str) | 271 GHashTable *msim_parse_body(const gchar *body_str) |
296 { | 272 { |
297 GHashTable *table; | 273 GHashTable *table; |
298 gchar *item; | 274 gchar *item; |
299 gchar **items; | 275 gchar **items; |
300 gchar **elements; | 276 gchar **elements; |
349 } | 325 } |
350 | 326 |
351 | 327 |
352 | 328 |
353 #ifdef MSIM_DEBUG_MSG | 329 #ifdef MSIM_DEBUG_MSG |
354 static void print_hash_item(gpointer key, gpointer value, gpointer user_data) | 330 void print_hash_item(gpointer key, gpointer value, gpointer user_data) |
355 { | 331 { |
356 purple_debug_info("msim", "%s=%s\n", (char*)key, (char*)value); | 332 purple_debug_info("msim", "%s=%s\n", (char*)key, (char*)value); |
357 } | 333 } |
358 #endif | 334 #endif |
359 | 335 |
364 * @param msg The raw data to send. | 340 * @param msg The raw data to send. |
365 * | 341 * |
366 * @return TRUE if succeeded, FALSE if not. | 342 * @return TRUE if succeeded, FALSE if not. |
367 * | 343 * |
368 */ | 344 */ |
369 static gboolean msim_send_raw(MsimSession *session, const gchar *msg) | 345 gboolean msim_send_raw(MsimSession *session, const gchar *msg) |
370 { | 346 { |
371 int total_bytes_sent, total_bytes; | 347 int total_bytes_sent, total_bytes; |
372 | 348 |
373 purple_debug_info("msim", "msim_send: writing <%s>\n", msg); | 349 purple_debug_info("msim", "msim_send_raw: writing <%s>\n", msg); |
374 | 350 |
375 g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); | 351 g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE); |
376 g_return_val_if_fail(msg != NULL, FALSE); | 352 g_return_val_if_fail(msg != NULL, FALSE); |
377 | 353 |
378 | 354 |
396 | 372 |
397 } while(total_bytes_sent < total_bytes); | 373 } while(total_bytes_sent < total_bytes); |
398 return TRUE; | 374 return TRUE; |
399 } | 375 } |
400 | 376 |
401 /** Send an existing MsimMessage. | |
402 */ | |
403 | |
404 /* TODO: move to message.c */ | |
405 static gboolean msim_msg_send(MsimSession *session, MsimMessage *msg) | |
406 { | |
407 gchar *raw; | |
408 gboolean success; | |
409 | |
410 raw = msim_msg_pack(msg); | |
411 success = msim_send_raw(session, raw); | |
412 g_free(raw); | |
413 | |
414 return success; | |
415 } | |
416 | |
417 /** | |
418 * | |
419 * Send a message to the server, whose contents is specified using | |
420 * variable arguments. | |
421 * | |
422 * @param session | |
423 * @param ... A sequence of gchar* key/value pairs, terminated with NULL. | |
424 * | |
425 * IMPORTANT: The key/value pair strings are not copied. The 'key' strings | |
426 * are not freed, but the 'value' pair is. Use g_strdup() to pass a static | |
427 * string as a value. This function operates this way so that you can easily | |
428 * use a call to g_strdup_printf() for the 'value' strings and not have to | |
429 * worry about freeing the memory. | |
430 * | |
431 * It bears repeating: THE VALUE STRINGS WILL BE FREED. Copy if static. | |
432 * | |
433 * This function exists for coding convenience: a message can be created | |
434 * and sent in one line of code. Internally it calls msim_msg_send(). | |
435 * | |
436 */ | |
437 | |
438 static gboolean msim_send(MsimSession *session, ...) | |
439 { | |
440 va_list argp; | |
441 gchar *key, *value; | |
442 MsimMessageType type; | |
443 gboolean success; | |
444 MsimMessage *msg; | |
445 | |
446 msg = msim_msg_new(); | |
447 | |
448 /* Parse key, value pairs until NULL. */ | |
449 va_start(argp, session); | |
450 do | |
451 { | |
452 key = va_arg(argp, gchar *); | |
453 if (!key) | |
454 { | |
455 break; | |
456 } | |
457 | |
458 type = va_arg(argp, int); | |
459 | |
460 switch (type) | |
461 { | |
462 case MSIM_TYPE_INTEGER: | |
463 msim_msg_append(msg, key, type, GUINT_TO_POINTER(va_arg(argp, int))); | |
464 break; | |
465 | |
466 case MSIM_TYPE_STRING: | |
467 value = va_arg(argp, char *); | |
468 | |
469 g_return_val_if_fail(value != NULL, FALSE); | |
470 | |
471 msim_msg_append(msg, key, type, value); | |
472 break; | |
473 | |
474 default: | |
475 purple_debug_info("msim", "msim_send: unknown type %d\n", type); | |
476 break; | |
477 } | |
478 } while(key && value); | |
479 | |
480 /* Actually send the message. */ | |
481 success = msim_msg_send(session, msg); | |
482 | |
483 /* Cleanup. */ | |
484 va_end(argp); | |
485 msim_msg_free(msg); | |
486 | |
487 return success; | |
488 } | |
489 | |
490 /** | 377 /** |
491 * Start logging in to the MSIM servers. | 378 * Start logging in to the MSIM servers. |
492 * | 379 * |
493 * @param acct Account information to use to login. | 380 * @param acct Account information to use to login. |
494 */ | 381 */ |
495 static void msim_login(PurpleAccount *acct) | 382 void msim_login(PurpleAccount *acct) |
496 { | 383 { |
497 PurpleConnection *gc; | 384 PurpleConnection *gc; |
498 const char *host; | 385 const char *host; |
499 int port; | 386 int port; |
500 | 387 |
534 * @param session | 421 * @param session |
535 * @param table Hash table of login challenge message. | 422 * @param table Hash table of login challenge message. |
536 * | 423 * |
537 * @return 0, since the 'table' parameter is no longer needed. | 424 * @return 0, since the 'table' parameter is no longer needed. |
538 */ | 425 */ |
539 static int msim_login_challenge(MsimSession *session, GHashTable *table) | 426 int msim_login_challenge(MsimSession *session, GHashTable *table) |
540 { | 427 { |
541 PurpleAccount *account; | 428 PurpleAccount *account; |
542 gchar *nc_str; | 429 gchar *nc_str; |
543 guchar *nc; | 430 guchar *nc; |
544 gchar *response_str; | 431 gchar *response; |
545 gsize nc_len; | 432 gsize nc_len; |
546 gchar *buf; | 433 guint response_len; |
547 | 434 |
548 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | 435 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); |
549 g_return_val_if_fail(table != NULL, 0); | 436 g_return_val_if_fail(table != NULL, 0); |
550 | 437 |
551 nc_str = g_hash_table_lookup(table, "nc"); | 438 nc_str = g_hash_table_lookup(table, "nc"); |
566 return 0; | 453 return 0; |
567 } | 454 } |
568 | 455 |
569 purple_connection_update_progress(session->gc, _("Logging in"), 2, 4); | 456 purple_connection_update_progress(session->gc, _("Logging in"), 2, 4); |
570 | 457 |
571 response_str = msim_compute_login_response(nc, account->username, account->password); | 458 response = msim_compute_login_response(nc, account->username, account->password, &response_len); |
572 | 459 |
573 g_free(nc); | 460 g_free(nc); |
574 | 461 |
575 /* Reply */ | |
576 /* \status\100\id\1\login2\196610\username\msimprpl@xyzzy.cjb.net\clientver\673\reconn\0\response\+6M+DQhovMxwdt1ceIervus9O5K+X9BR02w6B4+4+Zrg66pWrSzX94ER8efSb8Ju9kye2MnDdpTDwybziACy2mQFWIB9Mf5/1Tdlr6kAtJA==\final\ | |
577 this fails. what is it? | |
578 - escaping? no, removing msim_escape() still causes login failure. should verify. | |
579 - message order? should login2 come first? | |
580 - something else? | |
581 */ | |
582 #if 0 | |
583 msim_send(session, | 462 msim_send(session, |
584 "login2", g_strdup_printf("%d", MSIM_AUTH_ALGORITHM), | 463 "login2", MSIM_TYPE_INTEGER, MSIM_AUTH_ALGORITHM, |
585 "username", g_strdup(account->username), | 464 "username", MSIM_TYPE_STRING, g_strdup(account->username), |
586 "response", g_strdup(response_str), | 465 /* GString and gchar * response will be freed in msim_msg_free() in msim_send(). */ |
587 "clientver", g_strdup_printf("%d", MSIM_CLIENT_VERSION), | 466 "response", MSIM_TYPE_BINARY, g_string_new_len(response, response_len), |
588 "reconn", g_strdup_printf("%d", 0), | 467 "clientver", MSIM_TYPE_INTEGER, MSIM_CLIENT_VERSION, |
589 "status", g_strdup_printf("%d", 100), | 468 "reconn", MSIM_TYPE_INTEGER, 0, |
590 "id", g_strdup_printf("%d", 1), | 469 "status", MSIM_TYPE_INTEGER, 100, |
470 "id", MSIM_TYPE_INTEGER, 1, | |
591 NULL); | 471 NULL); |
592 #else | |
593 | |
594 /* TODO: use msim_send. But, response_str must NOT be escaped. */ | |
595 /* \login2\196610\username\msimprpl@xyzzy.cjb.net\response\nseVXvvrwgQsv7FUAbHJLMP8YPEGKHftwN+Z0zCjmxOTOc0/nVPQWZ5Znv5i6kh26XfZlqNzvoPqaXNbXL6TsSZpU/guAAg0o6XBA1e/Sw==\clientver\673\reconn\0\status\100\id\1\final\ - works*/ | |
596 buf = g_strdup_printf("\\login2\\%d\\username\\%s\\response\\%s\\clientver\\%d\\reconn\\%d\\status\\%d\\id\\1\\final\\", | |
597 MSIM_AUTH_ALGORITHM, account->username, response_str, MSIM_CLIENT_VERSION, 0, 100); | |
598 | |
599 | |
600 purple_debug_info("msim", "response=<%s>\n", buf); | |
601 | |
602 msim_send_raw(session, buf); | |
603 | |
604 g_free(buf); | |
605 #endif | |
606 | |
607 g_free(response_str); | |
608 | 472 |
609 return 0; | 473 return 0; |
610 } | 474 } |
611 | 475 |
612 #ifndef MSIM_USE_PURPLE_RC4 | 476 #ifndef MSIM_USE_PURPLE_RC4 |
721 * Compute the base64'd login challenge response based on username, password, nonce, and IPs. | 585 * Compute the base64'd login challenge response based on username, password, nonce, and IPs. |
722 * | 586 * |
723 * @param nonce The base64 encoded nonce ('nc') field from the server. | 587 * @param nonce The base64 encoded nonce ('nc') field from the server. |
724 * @param email User's email address (used as login name). | 588 * @param email User's email address (used as login name). |
725 * @param password User's cleartext password. | 589 * @param password User's cleartext password. |
726 * | 590 * @param response_len Will be written with response length. |
727 * @return Encoded login challenge response, ready to send to the server. Must be g_free()'d | 591 * |
592 * @return Binary login challenge response, ready to send to the server. Must be g_free()'d | |
728 * when finished. | 593 * when finished. |
729 */ | 594 */ |
730 static gchar* msim_compute_login_response(guchar nonce[2*NONCE_SIZE], | 595 gchar* msim_compute_login_response(guchar nonce[2*NONCE_SIZE], |
731 gchar* email, gchar* password) | 596 gchar* email, gchar* password, guint* response_len) |
732 { | 597 { |
733 PurpleCipherContext *key_context; | 598 PurpleCipherContext *key_context; |
734 PurpleCipher *sha1; | 599 PurpleCipher *sha1; |
735 #ifdef MSIM_USE_PURPLE_RC4 | 600 #ifdef MSIM_USE_PURPLE_RC4 |
736 PurpleCipherContext *rc4; | 601 PurpleCipherContext *rc4; |
741 guchar hash_pw[HASH_SIZE]; | 606 guchar hash_pw[HASH_SIZE]; |
742 guchar key[HASH_SIZE]; | 607 guchar key[HASH_SIZE]; |
743 gchar* password_utf16le; | 608 gchar* password_utf16le; |
744 guchar* data; | 609 guchar* data; |
745 guchar* data_out; | 610 guchar* data_out; |
746 gchar* response; | |
747 size_t data_len, data_out_len; | 611 size_t data_len, data_out_len; |
748 gsize conv_bytes_read, conv_bytes_written; | 612 gsize conv_bytes_read, conv_bytes_written; |
749 GError* conv_error; | 613 GError* conv_error; |
750 #ifdef MSIM_DEBUG_LOGIN_CHALLENGE | 614 #ifdef MSIM_DEBUG_LOGIN_CHALLENGE |
751 int i; | 615 int i; |
839 data_out = data; | 703 data_out = data; |
840 #endif | 704 #endif |
841 | 705 |
842 g_assert(data_out_len == data_len); | 706 g_assert(data_out_len == data_len); |
843 | 707 |
844 response = purple_base64_encode(data_out, data_out_len); | 708 #ifdef MSIM_DEBUG_LOGIN_CHALLENGE |
845 #ifdef MSIM_USE_PURPLE_RC4 | 709 purple_debug_info("msim", "response=<%s>\n", data_out); |
846 g_free(data_out); | |
847 #endif | 710 #endif |
848 | 711 |
849 #ifdef MSIM_DEBUG_LOGIN_CHALLENGE | 712 *response_len = data_out_len; |
850 purple_debug_info("msim", "response=<%s>\n", response); | 713 |
851 #endif | 714 return (gchar*)data_out; |
852 | |
853 return response; | |
854 } | 715 } |
855 | 716 |
856 /** | 717 /** |
857 * Schedule an IM to be sent once the user ID is looked up. | 718 * Schedule an IM to be sent once the user ID is looked up. |
858 * | 719 * |
871 * | 732 * |
872 * The callback function calls msim_send_im_by_userid() to send the actual | 733 * The callback function calls msim_send_im_by_userid() to send the actual |
873 * instant message. If a userid is specified directly, this function is called | 734 * instant message. If a userid is specified directly, this function is called |
874 * immediately here. | 735 * immediately here. |
875 */ | 736 */ |
876 static int msim_send_im(PurpleConnection *gc, const char *who, | 737 int msim_send_im(PurpleConnection *gc, const char *who, |
877 const char *message, PurpleMessageFlags flags) | 738 const char *message, PurpleMessageFlags flags) |
878 { | 739 { |
879 MsimSession *session; | 740 MsimSession *session; |
880 const char *from_username = gc->account->username; | 741 const char *from_username = gc->account->username; |
881 send_im_cb_struct *cbinfo; | 742 send_im_cb_struct *cbinfo; |
939 * @param flags Purple instant message flags. | 800 * @param flags Purple instant message flags. |
940 * | 801 * |
941 * @return 0, since the 'table' parameter is no longer needed. | 802 * @return 0, since the 'table' parameter is no longer needed. |
942 * | 803 * |
943 */ | 804 */ |
944 static int msim_send_im_by_userid(MsimSession *session, const gchar *userid, const gchar *message, PurpleMessageFlags flags) | 805 int msim_send_im_by_userid(MsimSession *session, const gchar *userid, const gchar *message, PurpleMessageFlags flags) |
945 { | 806 { |
946 gchar *msg_string; | |
947 gchar *escaped_message; | |
948 | |
949 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | 807 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); |
950 g_return_val_if_fail(userid != NULL, 0); | 808 g_return_val_if_fail(userid != NULL, 0); |
951 g_return_val_if_fail(msim_is_userid(userid) == TRUE, 0); | 809 g_return_val_if_fail(msim_is_userid(userid) == TRUE, 0); |
952 g_return_val_if_fail(message != NULL, 0); | 810 g_return_val_if_fail(message != NULL, 0); |
953 | 811 |
954 /* TODO: Remove this code and use MsimMessage after it is implemented and escapes strings. */ | 812 msim_send(session, |
955 escaped_message = msim_escape(message); | 813 "bm", MSIM_TYPE_INTEGER, MSIM_BM_INSTANT, |
956 | 814 "sesskey", MSIM_TYPE_STRING, g_strdup(session->sesskey), |
957 /* TODO: escape values */ | 815 "t", MSIM_TYPE_STRING, g_strdup(userid), |
958 msg_string = g_strdup_printf("\\bm\\%d\\sesskey\\%s\\t\\%s\\cv\\%d\\msg\\%s\\final\\", | 816 "cv", MSIM_TYPE_INTEGER, MSIM_CLIENT_VERSION, |
959 MSIM_BM_INSTANT, session->sesskey, userid, MSIM_CLIENT_VERSION, message); | 817 "msg", MSIM_TYPE_STRING, g_strdup(message), |
960 | 818 NULL); |
961 /* TODO: Remove this code and use MsimMessage after it is implemented and escapes strings. */ | |
962 g_free(escaped_message); | |
963 | |
964 purple_debug_info("msim", "going to write: %s\n", msg_string); | |
965 | |
966 msim_send_raw(session, msg_string); | |
967 | |
968 /* TODO: notify Purple that we sent the IM. */ | |
969 | 819 |
970 /* Not needed since sending messages to yourself is allowed by MSIM! */ | 820 /* Not needed since sending messages to yourself is allowed by MSIM! */ |
971 /*if (strcmp(from_username, who) == 0) | 821 /*if (strcmp(from_username, who) == 0) |
972 serv_got_im(gc, from_username, message, PURPLE_MESSAGE_RECV, time(NULL)); | 822 serv_got_im(gc, from_username, message, PURPLE_MESSAGE_RECV, time(NULL)); |
973 */ | 823 */ |
984 * @param userinfo User info message from server containing a 'body' field | 834 * @param userinfo User info message from server containing a 'body' field |
985 * with a 'UserID' key. This is where the user ID is taken from. | 835 * with a 'UserID' key. This is where the user ID is taken from. |
986 * @param data A send_im_cb_struct* of information on the IM to send. | 836 * @param data A send_im_cb_struct* of information on the IM to send. |
987 * | 837 * |
988 */ | 838 */ |
989 static void msim_send_im_by_userid_cb(MsimSession *session, GHashTable *userinfo, gpointer data) | 839 void msim_send_im_by_userid_cb(MsimSession *session, GHashTable *userinfo, gpointer data) |
990 { | 840 { |
991 send_im_cb_struct *s; | 841 send_im_cb_struct *s; |
992 gchar *userid; | 842 gchar *userid; |
993 GHashTable *body; | 843 GHashTable *body; |
994 | 844 |
1014 * | 864 * |
1015 * @param session | 865 * @param session |
1016 * @param userinfo Message from server on user's info, containing UserName. | 866 * @param userinfo Message from server on user's info, containing UserName. |
1017 * @param data A gchar* of the incoming instant message's text. | 867 * @param data A gchar* of the incoming instant message's text. |
1018 */ | 868 */ |
1019 static void msim_incoming_im_cb(MsimSession *session, GHashTable *userinfo, gpointer data) | 869 void msim_incoming_im_cb(MsimSession *session, GHashTable *userinfo, gpointer data) |
1020 { | 870 { |
1021 gchar *msg; | 871 gchar *msg; |
1022 gchar *username; | 872 gchar *username; |
1023 GHashTable *body; | 873 GHashTable *body; |
1024 | 874 |
1043 * @param session The session | 893 * @param session The session |
1044 * @param table Message from the server, containing 'f' (userid from) and 'msg'. | 894 * @param table Message from the server, containing 'f' (userid from) and 'msg'. |
1045 * | 895 * |
1046 * @return 0, since table can be freed. | 896 * @return 0, since table can be freed. |
1047 */ | 897 */ |
1048 static int msim_incoming_im(MsimSession *session, GHashTable *table) | 898 int msim_incoming_im(MsimSession *session, GHashTable *table) |
1049 { | 899 { |
1050 gchar *userid; | 900 gchar *userid; |
1051 gchar *msg; | 901 gchar *msg; |
1052 | 902 |
1053 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | 903 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); |
1074 * @param table Any message from the server. | 924 * @param table Any message from the server. |
1075 * | 925 * |
1076 * @return The return value of the function used to process the message, or -1 if | 926 * @return The return value of the function used to process the message, or -1 if |
1077 * called with invalid parameters. | 927 * called with invalid parameters. |
1078 */ | 928 */ |
1079 static int msim_process(PurpleConnection *gc, GHashTable *table) | 929 int msim_process(PurpleConnection *gc, GHashTable *table) |
1080 { | 930 { |
1081 MsimSession *session; | 931 MsimSession *session; |
1082 | 932 |
1083 g_return_val_if_fail(gc != NULL, -1); | 933 g_return_val_if_fail(gc != NULL, -1); |
1084 g_return_val_if_fail(table != NULL, -1); | 934 g_return_val_if_fail(table != NULL, -1); |
1151 * @param session | 1001 * @param session |
1152 * @param table Message reply from server. | 1002 * @param table Message reply from server. |
1153 * | 1003 * |
1154 * @return 0, since the 'table' field is no longer needed. | 1004 * @return 0, since the 'table' field is no longer needed. |
1155 */ | 1005 */ |
1156 static int msim_process_reply(MsimSession *session, GHashTable *table) | 1006 int msim_process_reply(MsimSession *session, GHashTable *table) |
1157 { | 1007 { |
1158 gchar *rid_str; | 1008 gchar *rid_str; |
1159 | 1009 |
1160 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | 1010 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); |
1161 g_return_val_if_fail(table != NULL, 0); | 1011 g_return_val_if_fail(table != NULL, 0); |
1221 * @param session | 1071 * @param session |
1222 * @param table The message. | 1072 * @param table The message. |
1223 * | 1073 * |
1224 * @return 0, since 'table' can be freed. | 1074 * @return 0, since 'table' can be freed. |
1225 */ | 1075 */ |
1226 static int msim_error(MsimSession *session, GHashTable *table) | 1076 int msim_error(MsimSession *session, GHashTable *table) |
1227 { | 1077 { |
1228 gchar *err, *errmsg, *full_errmsg; | 1078 gchar *err, *errmsg, *full_errmsg; |
1229 | 1079 |
1230 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | 1080 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); |
1231 g_return_val_if_fail(table != NULL, 0); | 1081 g_return_val_if_fail(table != NULL, 0); |
1253 return 0; | 1103 return 0; |
1254 } | 1104 } |
1255 | 1105 |
1256 #if 0 | 1106 #if 0 |
1257 /* Not sure about this */ | 1107 /* Not sure about this */ |
1258 static void msim_status_now(gchar *who, gpointer data) | 1108 void msim_status_now(gchar *who, gpointer data) |
1259 { | 1109 { |
1260 printf("msim_status_now: %s\n", who); | 1110 printf("msim_status_now: %s\n", who); |
1261 } | 1111 } |
1262 #endif | 1112 #endif |
1263 | 1113 |
1267 * @param session | 1117 * @param session |
1268 * @param userinfo Looked up user information from server. | 1118 * @param userinfo Looked up user information from server. |
1269 * @param data gchar* status string. | 1119 * @param data gchar* status string. |
1270 * | 1120 * |
1271 */ | 1121 */ |
1272 static void msim_status_cb(MsimSession *session, GHashTable *userinfo, gpointer data) | 1122 void msim_status_cb(MsimSession *session, GHashTable *userinfo, gpointer data) |
1273 { | 1123 { |
1274 PurpleBuddyList *blist; | 1124 PurpleBuddyList *blist; |
1275 PurpleBuddy *buddy; | 1125 PurpleBuddy *buddy; |
1276 PurplePresence *presence; | 1126 PurplePresence *presence; |
1277 GHashTable *body; | 1127 GHashTable *body; |
1354 * @param session | 1204 * @param session |
1355 * @param table Status update message. | 1205 * @param table Status update message. |
1356 * | 1206 * |
1357 * @return 0, since 'table' can be freed. | 1207 * @return 0, since 'table' can be freed. |
1358 */ | 1208 */ |
1359 static int msim_status(MsimSession *session, GHashTable *table) | 1209 int msim_status(MsimSession *session, GHashTable *table) |
1360 { | 1210 { |
1361 gchar *status_str; | 1211 gchar *status_str; |
1362 gchar *userid; | 1212 gchar *userid; |
1363 | 1213 |
1364 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); | 1214 g_return_val_if_fail(MSIM_SESSION_VALID(session), 0); |
1399 * @param source File descriptor. | 1249 * @param source File descriptor. |
1400 * @param cond PURPLE_INPUT_READ | 1250 * @param cond PURPLE_INPUT_READ |
1401 * | 1251 * |
1402 * Reads the input, and dispatches calls msim_process to handle it. | 1252 * Reads the input, and dispatches calls msim_process to handle it. |
1403 */ | 1253 */ |
1404 static void msim_input_cb(gpointer gc_uncasted, gint source, PurpleInputCondition cond) | 1254 void msim_input_cb(gpointer gc_uncasted, gint source, PurpleInputCondition cond) |
1405 { | 1255 { |
1406 PurpleConnection *gc; | 1256 PurpleConnection *gc; |
1407 PurpleAccount *account; | 1257 PurpleAccount *account; |
1408 MsimSession *session; | 1258 MsimSession *session; |
1409 gchar *end; | 1259 gchar *end; |
1521 * | 1371 * |
1522 * @param data A PurpleConnection pointer. | 1372 * @param data A PurpleConnection pointer. |
1523 * @param source File descriptor. | 1373 * @param source File descriptor. |
1524 * @param error_message | 1374 * @param error_message |
1525 */ | 1375 */ |
1526 static void msim_connect_cb(gpointer data, gint source, const gchar *error_message) | 1376 void msim_connect_cb(gpointer data, gint source, const gchar *error_message) |
1527 { | 1377 { |
1528 PurpleConnection *gc; | 1378 PurpleConnection *gc; |
1529 MsimSession *session; | 1379 MsimSession *session; |
1530 | 1380 |
1531 g_return_if_fail(data != NULL); | 1381 g_return_if_fail(data != NULL); |
1553 * | 1403 * |
1554 * @param acct The account to create the session from. | 1404 * @param acct The account to create the session from. |
1555 * | 1405 * |
1556 * @return Pointer to a new session. Free with msim_session_destroy. | 1406 * @return Pointer to a new session. Free with msim_session_destroy. |
1557 */ | 1407 */ |
1558 static MsimSession *msim_session_new(PurpleAccount *acct) | 1408 MsimSession *msim_session_new(PurpleAccount *acct) |
1559 { | 1409 { |
1560 MsimSession *session; | 1410 MsimSession *session; |
1561 | 1411 |
1562 g_return_val_if_fail(acct != NULL, NULL); | 1412 g_return_val_if_fail(acct != NULL, NULL); |
1563 | 1413 |
1583 /** | 1433 /** |
1584 * Free a session. | 1434 * Free a session. |
1585 * | 1435 * |
1586 * @param session The session to destroy. | 1436 * @param session The session to destroy. |
1587 */ | 1437 */ |
1588 static void msim_session_destroy(MsimSession *session) | 1438 void msim_session_destroy(MsimSession *session) |
1589 { | 1439 { |
1590 g_return_if_fail(MSIM_SESSION_VALID(session)); | 1440 g_return_if_fail(MSIM_SESSION_VALID(session)); |
1591 | 1441 |
1592 session->magic = -1; | 1442 session->magic = -1; |
1593 | 1443 |
1603 /** | 1453 /** |
1604 * Close the connection. | 1454 * Close the connection. |
1605 * | 1455 * |
1606 * @param gc The connection. | 1456 * @param gc The connection. |
1607 */ | 1457 */ |
1608 static void msim_close(PurpleConnection *gc) | 1458 void msim_close(PurpleConnection *gc) |
1609 { | 1459 { |
1610 g_return_if_fail(gc != NULL); | 1460 g_return_if_fail(gc != NULL); |
1611 | 1461 |
1612 msim_session_destroy(gc->proto_data); | 1462 msim_session_destroy(gc->proto_data); |
1613 } | 1463 } |
1618 * | 1468 * |
1619 * @param user The user id, email, or name. | 1469 * @param user The user id, email, or name. |
1620 * | 1470 * |
1621 * @return TRUE if is userid, FALSE if not. | 1471 * @return TRUE if is userid, FALSE if not. |
1622 */ | 1472 */ |
1623 static inline gboolean msim_is_userid(const gchar *user) | 1473 gboolean msim_is_userid(const gchar *user) |
1624 { | 1474 { |
1625 g_return_val_if_fail(user != NULL, FALSE); | 1475 g_return_val_if_fail(user != NULL, FALSE); |
1626 | 1476 |
1627 return strspn(user, "0123456789") == strlen(user); | 1477 return strspn(user, "0123456789") == strlen(user); |
1628 } | 1478 } |
1637 * This function is not intended to be used as a generic | 1487 * This function is not intended to be used as a generic |
1638 * means of validating email addresses, but to distinguish | 1488 * means of validating email addresses, but to distinguish |
1639 * between a user represented by an email address from | 1489 * between a user represented by an email address from |
1640 * other forms of identification. | 1490 * other forms of identification. |
1641 */ | 1491 */ |
1642 static inline gboolean msim_is_email(const gchar *user) | 1492 gboolean msim_is_email(const gchar *user) |
1643 { | 1493 { |
1644 g_return_val_if_fail(user != NULL, FALSE); | 1494 g_return_val_if_fail(user != NULL, FALSE); |
1645 | 1495 |
1646 return strchr(user, '@') != NULL; | 1496 return strchr(user, '@') != NULL; |
1647 } | 1497 } |
1653 * @param session | 1503 * @param session |
1654 * @param user The user id, email address, or username. | 1504 * @param user The user id, email address, or username. |
1655 * @param cb Callback, called with user information when available. | 1505 * @param cb Callback, called with user information when available. |
1656 * @param data An arbitray data pointer passed to the callback. | 1506 * @param data An arbitray data pointer passed to the callback. |
1657 */ | 1507 */ |
1658 static void msim_lookup_user(MsimSession *session, const gchar *user, MSIM_USER_LOOKUP_CB cb, gpointer data) | 1508 void msim_lookup_user(MsimSession *session, const gchar *user, MSIM_USER_LOOKUP_CB cb, gpointer data) |
1659 { | 1509 { |
1660 gchar *field_name; | 1510 gchar *field_name; |
1661 gchar *msg_string; | |
1662 guint rid, cmd, dsn, lid; | 1511 guint rid, cmd, dsn, lid; |
1663 | 1512 |
1664 g_return_if_fail(MSIM_SESSION_VALID(session)); | 1513 g_return_if_fail(MSIM_SESSION_VALID(session)); |
1665 g_return_if_fail(user != NULL); | 1514 g_return_if_fail(user != NULL); |
1666 g_return_if_fail(cb != NULL); | 1515 g_return_if_fail(cb != NULL); |
1704 field_name = "UserName"; | 1553 field_name = "UserName"; |
1705 dsn = 5; | 1554 dsn = 5; |
1706 lid = 7; | 1555 lid = 7; |
1707 } | 1556 } |
1708 | 1557 |
1709 /* TODO: escape values */ | 1558 |
1710 msg_string = g_strdup_printf("\\persist\\1\\sesskey\\%s\\cmd\\1\\dsn\\%d\\uid\\%s\\lid\\%d\\rid\\%d\\body\\%s=%s\\final\\", | 1559 msim_send(session, |
1711 session->sesskey, dsn, session->userid, lid, rid, field_name, user); | 1560 "persist", MSIM_TYPE_INTEGER, 1, |
1712 | 1561 "sesskey", MSIM_TYPE_STRING, g_strdup(session->sesskey), |
1713 msim_send_raw(session, msg_string); | 1562 "cmd", MSIM_TYPE_INTEGER, 1, |
1563 "dsn", MSIM_TYPE_INTEGER, dsn, | |
1564 "uid", MSIM_TYPE_STRING, g_strdup(session->userid), | |
1565 "lid", MSIM_TYPE_INTEGER, lid, | |
1566 "rid", MSIM_TYPE_INTEGER, rid, | |
1567 /* TODO: dictionary field type */ | |
1568 "body", MSIM_TYPE_STRING, g_strdup_printf("%s=%s", field_name, user), | |
1569 NULL); | |
1714 } | 1570 } |
1715 | 1571 |
1716 | 1572 |
1717 /** | 1573 /** |
1718 * Obtain the status text for a buddy. | 1574 * Obtain the status text for a buddy. |
1720 * @param buddy The buddy to obtain status text for. | 1576 * @param buddy The buddy to obtain status text for. |
1721 * | 1577 * |
1722 * @return Status text, or NULL if error. | 1578 * @return Status text, or NULL if error. |
1723 * | 1579 * |
1724 */ | 1580 */ |
1725 static char *msim_status_text(PurpleBuddy *buddy) | 1581 char *msim_status_text(PurpleBuddy *buddy) |
1726 { | 1582 { |
1727 MsimSession *session; | 1583 MsimSession *session; |
1728 GHashTable *userinfo; | 1584 GHashTable *userinfo; |
1729 gchar *display_name; | 1585 gchar *display_name; |
1730 | 1586 |
1752 * @param buddy Buddy to obtain tooltip text on. | 1608 * @param buddy Buddy to obtain tooltip text on. |
1753 * @param user_info Variable modified to have the tooltip text. | 1609 * @param user_info Variable modified to have the tooltip text. |
1754 * @param full TRUE if should obtain full tooltip text. | 1610 * @param full TRUE if should obtain full tooltip text. |
1755 * | 1611 * |
1756 */ | 1612 */ |
1757 static void msim_tooltip_text(PurpleBuddy *buddy, PurpleNotifyUserInfo *user_info, gboolean full) | 1613 void msim_tooltip_text(PurpleBuddy *buddy, PurpleNotifyUserInfo *user_info, gboolean full) |
1758 { | 1614 { |
1759 g_return_if_fail(buddy != NULL); | 1615 g_return_if_fail(buddy != NULL); |
1760 g_return_if_fail(user_info != NULL); | 1616 g_return_if_fail(user_info != NULL); |
1761 | 1617 |
1762 if (PURPLE_BUDDY_IS_ONLINE(buddy)) | 1618 if (PURPLE_BUDDY_IS_ONLINE(buddy)) |
1784 (gchar*)g_hash_table_lookup(userinfo, "SongName"))); | 1640 (gchar*)g_hash_table_lookup(userinfo, "SongName"))); |
1785 } | 1641 } |
1786 } | 1642 } |
1787 | 1643 |
1788 /** Callbacks called by Purple, to access this plugin. */ | 1644 /** Callbacks called by Purple, to access this plugin. */ |
1789 static PurplePluginProtocolInfo prpl_info = | 1645 PurplePluginProtocolInfo prpl_info = |
1790 { | 1646 { |
1791 OPT_PROTO_MAIL_CHECK,/* options - TODO: myspace will notify of mail */ | 1647 OPT_PROTO_MAIL_CHECK,/* options - TODO: myspace will notify of mail */ |
1792 NULL, /* user_splits */ | 1648 NULL, /* user_splits */ |
1793 NULL, /* protocol_options */ | 1649 NULL, /* protocol_options */ |
1794 NO_BUDDY_ICONS, /* icon_spec - TODO: eventually should add this */ | 1650 NO_BUDDY_ICONS, /* icon_spec - TODO: eventually should add this */ |
1857 }; | 1713 }; |
1858 | 1714 |
1859 | 1715 |
1860 | 1716 |
1861 /** Based on MSN's plugin info comments. */ | 1717 /** Based on MSN's plugin info comments. */ |
1862 static PurplePluginInfo info = | 1718 PurplePluginInfo info = |
1863 { | 1719 { |
1864 PURPLE_PLUGIN_MAGIC, | 1720 PURPLE_PLUGIN_MAGIC, |
1865 PURPLE_MAJOR_VERSION, | 1721 PURPLE_MAJOR_VERSION, |
1866 PURPLE_MINOR_VERSION, | 1722 PURPLE_MINOR_VERSION, |
1867 PURPLE_PLUGIN_PROTOCOL, /**< type */ | 1723 PURPLE_PLUGIN_PROTOCOL, /**< type */ |
1896 NULL /**< reserved4 */ | 1752 NULL /**< reserved4 */ |
1897 }; | 1753 }; |
1898 | 1754 |
1899 #include "message.h" | 1755 #include "message.h" |
1900 | 1756 |
1901 static void init_plugin(PurplePlugin *plugin) | 1757 void init_plugin(PurplePlugin *plugin) |
1902 { | 1758 { |
1903 PurpleAccountOption *option; | 1759 PurpleAccountOption *option; |
1904 #ifdef _TEST_MSIM_MSG | 1760 #ifdef _TEST_MSIM_MSG |
1905 MsimMessage *msg = msim_msg_new(); | 1761 MsimMessage *msg = msim_msg_new(); |
1906 msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, "v1"); | 1762 msg = msim_msg_append(msg, "bx", MSIM_TYPE_BINARY, g_string_new_len(g_strdup("XXX"), 3)); |
1763 msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v1")); | |
1907 msg = msim_msg_append(msg, "k1", MSIM_TYPE_INTEGER, 42); | 1764 msg = msim_msg_append(msg, "k1", MSIM_TYPE_INTEGER, 42); |
1908 msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, "v43"); | 1765 msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v43")); |
1909 msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, "v5"); | 1766 msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v52/xxx\\yyy")); |
1910 msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, "v7"); | 1767 msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v7")); |
1768 purple_debug_info("msim", "msg=%s\n", msim_msg_debug_string(msg)); | |
1911 purple_debug_info("msim", "msg=%s\n", msim_msg_pack(msg)); | 1769 purple_debug_info("msim", "msg=%s\n", msim_msg_pack(msg)); |
1912 purple_debug_info("msim", "msg=%s\n", msim_msg_debug_string(msg)); | |
1913 msim_msg_free(msg); | 1770 msim_msg_free(msg); |
1914 exit(0); | 1771 exit(0); |
1915 #endif | 1772 #endif |
1916 | 1773 |
1917 /* TODO: default to automatically try different ports. Make the user be | 1774 /* TODO: default to automatically try different ports. Make the user be |