author | Alberto Bertogli
<albertogli@telpin.com.ar> 2005-04-09 17:20:49 UTC |
committer | Alberto Bertogli
<albertogli@telpin.com.ar> 2005-04-09 17:20:49 UTC |
INSTALL.txt | +37 | -0 |
README | +42 | -0 |
doc/Changelog | +455 | -0 |
doc/CodingStyle | +10 | -0 |
doc/FAQ | +130 | -0 |
doc/LICENSE | +186 | -0 |
doc/TODO | +43 | -0 |
doc/URL | +10 | -0 |
doc/commands | +119 | -0 |
doc/dependencies | +6 | -0 |
doc/icons | +46 | -0 |
doc/log_format | +44 | -0 |
doc/msg_states | +47 | -0 |
doc/msncd_protocol | +128 | -0 |
doc/portability | +39 | -0 |
doc/profiles | +20 | -0 |
doc/reporting_bugs | +38 | -0 |
install | +31 | -0 |
msn | +1623 | -0 |
msncb.py | +507 | -0 |
msnlib.py | +743 | -0 |
msnrc.sample | +65 | -0 |
msnsetup | +87 | -0 |
setup.py | +12 | -0 |
utils/clean | +5 | -0 |
utils/hmerge | +110 | -0 |
utils/msnbot | +127 | -0 |
utils/msncd | +417 | -0 |
utils/msnlog.vim | +30 | -0 |
utils/msntk | +468 | -0 |
utils/vicl | +19 | -0 |
diff --git a/INSTALL.txt b/INSTALL.txt new file mode 100644 index 0000000..cb05367 --- /dev/null +++ b/INSTALL.txt @@ -0,0 +1,37 @@ + +To install the library and the client, run the 'install' file as root +(probably using something like './install'). + +It will copy the python modules to the proper place, and the client to +/usr/local/bin. + +Then, you can run (as your user) 'msnsetup' to create the initial +configuration file and directories (placed in ~/.msn); and finally run the +client with 'msn'. Skip this step if you are upgrading. + +Alternatively, you can create your own ~/.msn/msnrc file based on an example +named 'msnrc.sample'. + + +Here is a command line summary: + +# we became root +su + +# then install everything +./install + +# now drop the root privileges +exit + +# run the setup to create the configuration (only if you are not upgrading) +msnsetup + +# and finally start the client +msn + + +I would really like to hear your opinion, so please drop any comments to +msnlib-devel@auriga.wearlab.de. + + diff --git a/README b/README new file mode 100644 index 0000000..5ef35b6 --- /dev/null +++ b/README @@ -0,0 +1,42 @@ + +msnlib +Alberto Bertogli (albertogli@telpin.com.ar) +---------------------------------------------- + +This is a python implementation for the msn messenger protocol (version 8), +it's pretty simple and straightforward; but it works well. + +Please read the 'INSTALL' file to see how to install and use both the client +and the library. + +The client is really simple, uses a text-mode interface with commands similar +to 'micq' (http://micq.ukeer.de/), which is a great ICQ client. + +If you're looking for a good messaging system, forget about messenger and try +Jabber (http://www.jabber.org), it's the only one which is run in the open +based on public standards, it's safe, fast, has a lot of features and it's +quite scalable. + +But if you're stucked with messenger for whatever reason (your friends, +family, the dog, all use it =), I hope you find this useful. + + +The basic idea for the library is a main class which represents a connection +with the server and holds all the relevant information, it has only the a few +methods to login, change some info and status, and send messages; all the rest +is driven by an asyncronous callback scheme registered at runtime which can be +changed on the fly. + +If you're intrested, read the code to find out more; most of the documentation +is there either as command or as python documentation strings. You might also +want to check out the doc directory; specially if you use a non-unix platform, +take a look at the 'portability' file. + + +Comments and patches are always welcome; please send them to the mailing list, +msnlib-devel@auriga.wearlab.de. + +Thanks, + Alberto + + diff --git a/doc/Changelog b/doc/Changelog new file mode 100644 index 0000000..65742d5 --- /dev/null +++ b/doc/Changelog @@ -0,0 +1,455 @@ +10 Mar 05 00.34.03 - Alberto <albertogli@telpin.com.ar> + * msn: fix documentation for the add command. + +27 Jun 04 14.12.24 - Alberto <albertogli@telpin.com.ar> + * msntk: fixes from the console client. + +29 Mar 04 23.55.18 - Alberto <albertogli@telpin.com.ar> + * tag: 3.4 tag + +22 Mar 04 17.45.29 - Alberto <albertogli@telpin.com.ar> + * msn: change initial status after we got the full user list; this should fix + the problem that all the list appeared offline, but it needs testing + +21 Mar 04 21.43.39 - Alberto <albertogli@telpin.com.ar> + * msn: remove space after the first line which that messed up NetBSD. Thanks + to Cameron Kaiser. + * msn: allow Ctrl+U to clear the command line. Patch by Cameron Kaiser. + +01 Mar 04 04.33.38 - Alberto <albertogli@telpin.com.ar> + * msn: add 'ren' command to rename users + +20 Dec 03 22.29.01 - Alberto <albertogli@telpin.com.ar> + * msn: handle a crash when typing space and tab by doing proper param + checking in tab handling + +10 Dec 03 23.16.52 - Alberto <albertogli@telpin.com.ar> + * doc: update license to OSL v2.0 + +13 Nov 03 11.47.09 - Alberto <albertogli@telpin.com.ar> + * msncb, msn: add more information to XFRError to see if we can get what is + causing it + +12 Nov 03 13.13.27 - Alberto <albertogli@telpin.com.ar> + * doc: update URL + +12 Nov 03 12.52.27 - Alberto <albertogli@telpin.com.ar> + * tag: 3.3 tag + +12 Nov 03 00.06.32 - Alberto <albertogli@telpin.com.ar> + * msn: handle socket exceptions more properly so we cleanup the console on + the exit path even when the md is down + +11 Nov 03 23.35.05 - Alberto <albertogli@telpin.com.ar> + * msnlib: handle some server errors while logging in + +08 Nov 03 18.29.47 - Alberto <albertogli@telpin.com.ar> + * msn: handle \r input for mac keyboards + +03 Nov 03 20.19.07 - Alberto <albertogli@telpin.com.ar> + * msntk: add msntk, a tk-based client + +30 Oct 03 02.48.30 - Alberto <albertogli@telpin.com.ar> + * doc: various small updates + +29 Oct 03 22.46.13 - Alberto <albertogli@telpin.com.ar> + * msn: fix a crash when passing an unknown nick to block/unblock + * msn: do proper nick checking in lignore + +28 Oct 03 23.13.56 - Alberto <albertogli@telpin.com.ar> + * msn: fix a crash when the configuration file was passed as a parameter + +27 Oct 03 12.49.14 - Alberto <albertogli@telpin.com.ar> + * msnlib: do proper connect() checking using getsockopt() + * msnlib: fix user.__repr__() + +18 Oct 03 10.12.12 - Alberto <albertogli@telpin.com.ar> + * msn: mark blocked users in listings + * msn: use string.join() for multi-user chat filenames + * msnlib: remove FIXME from message lenght check; the way it's done is safe + * msnlib: allow sendmsg to get a destination sbd directly + +30 Sep 03 15.06.19 - Alberto <albertogli@telpin.com.ar> + * doc: upgrade documentation to reflect the need for at least Python 2.2.2 + * doc: minor change to TODO list + +29 Sep 03 09.17.33 - Alberto <albertogli@telpin.com.ar> + * tag: 3.2 tag + +28 Sep 03 11.39.59 - Alberto <albertogli@telpin.com.ar> + * msn: handle some Hotmail messages, now we display unread mail and new mail + notifications + * msnsetup: display profile name in the final message + * msn: implement multi-user chat logging, so now chats that involve multiple + users get logged properly to a different file + * msnlib: fix the login procedure to handle redirects properly, and clean up + and comment the code so we don't get lost in the parsing + +28 Sep 03 10.01.35 - Alberto <albertogli@telpin.com.ar> + * tag: 3.1 tag + +28 Sep 03 09.59.14 - Alberto <albertogli@telpin.com.ar> + * msnlib: fix SSL error handling, now raise the error properly + +27 Sep 03 15.58.21 - Alberto <albertogli@telpin.com.ar> + * msn: ignore gaim's x-clientcaps messages + * msn: allow spaces in nick + +25 Sep 03 23.02.38 - Alberto <albertogli@telpin.com.ar> + * msn: fix crashes when sending a null message + * msn: fix message content mangling when the first character matched the nick + +24 Sep 03 14.29.17 - Alberto <albertogli@telpin.com.ar> + * tag: 3.0 + * doc: small updates + +22 Sep 03 23.55.26 - Alberto <albertogli@telpin.com.ar> + * msn: preserve whitespace in messages + +22 Sep 03 02.10.15 - Alberto <albertogli@telpin.com.ar> + * msn, msnlib: implement user blocking and unblocking + * msncb: fix MSNP8 SYN so it doesn't step over existing users + * msncb: use the same user object for both reverse and forward lists + * msncb: fix MSNP8 ADD and BPR so both work well with the new SYN + +22 Sep 03 00.14.15 - Alberto <albertogli@telpin.com.ar> + * msnlib, msncb: implement MSNP8 + +25 Aug 03 23.14.08 - Alberto <albertogli@telpin.com.ar> + * tag: 2.1 + +25 Aug 03 14.09.17 - Alberto <albertogli@telpin.com.ar> + * msnlib: keep message queues when submitting sbds, to avoid loosing messages + when two XFRs happen at the 'same' time + * msn: priorize users with sbd for tab completion + +25 Aug 03 00.05.22 - Alberto <albertogli@telpin.com.ar> + * msn: use email instead of nick for tab completion when nick has spaces + * msn: implement tab completion for emails and nicks + +04 Aug 03 15.07.53 - Alberto <albertogli@telpin.com.ar> + * msn: fix user info to avoid crashing when asking for info on users + which are not on our list + * msn: some indentation work + * msncb: close sbd when deleting a user + +08 Jun 03 19.11.18 - Alberto <albertogli@telpin.com.ar> + * tag: 2.0 + +07 Jun 03 15.40.18 - Alberto <albertogli@telpin.com.ar> + * msn, msnlib, msncb: add group support + +05 Jun 03 17.16.21 - Alberto <albertogli@telpin.com.ar> + * doc: add an icons table + +05 Jun 03 00.22.48 - Alberto <albertogli@telpin.com.ar> + * msn: move the help to a separate string + * msn, msnlib, msncb: fix several weird corner cases with multi-user chats + +04 Jun 03 12.54.31 - Alberto <albertogli@telpin.com.ar> + * msncb: don't close the sbd on fln, so we can keep talking while invisible + +04 Jun 03 12.38.42 - Alberto <albertogli@telpin.com.ar> + * msn, msnlib: add invitation support for multi-user chats + * msncb: fix fln sbd handling + +04 Jun 03 11.00.04 - Alberto <albertogli@telpin.com.ar> + * msncb: add email to sbd.emails on joi and iro + * msncb: handle bye messages properly for multi-user sbds + +04 Jun 03 10.50.54 - Alberto <albertogli@telpin.com.ar> + * msncb, msn: fix flushing for sbd joins + * msncb, msn: implement cb_iro, for joining a multi-user sbd + +03 Jun 03 19.15.34 - Alberto <albertogli@telpin.com.ar> + * msn: fix a bug when sending raw commands + +02 Jun 03 17.14.56 - Alberto <albertogli@telpin.com.ar> + * utils: add hmerge utility + * msn: add notification for sbd joins + +24 May 03 19.57.58 - Alberto <albertogli@telpin.com.ar> + * msncb: use debug() from msnlib + * msnlib: fix a bug in _recv() that didn't decoded the parameters properly + * msnlib, msncb: remember real nicks + * msn: show real nicks + * doc: update the TODO list + +21 May 03 19.01.27 - Alberto <albertogli@telpin.com.ar> + * msnlib, msncb: sbd connection handling cleanup, also fixed a weird bug + that raised EINPROGRESS after connect + +20 May 03 18.51.25 - Alberto <albertogli@telpin.com.ar> + * license: change the license from GPL to OSL + +11 May 03 15.42.38 - Alberto <albertogli@telpin.com.ar> + * msn: allow to send messages when either we or the receiver is offline if we + already have a sbd + +11 May 03 02.30.09 - Alberto <albertogli@telpin.com.ar> + * msn, msnlib, msncb: handle a protocol bug that allows line feeds (0x0C) in + user nicks, now we split fields explicitly only by a space (' '); + thanks to menetas@menetas.net for the report + +01 May 03 19.20.41 - Alberto <albertogli@telpin.com.ar> + * tag: 1.0 tag + +01 May 03 19.16.39 - Alberto <albertogli@telpin.com.ar> + * utils: add msnbot and msncd to the utilities + * doc: small documentation updates + +23 Apr 03 18.08.25 - Alberto <albertogli@telpin.com.ar> + * msn: fix a very odd but important bug that could cause a protocol break by + miscalculating the lenght of an encoded message. Thanks to Ahilan + Sinnarajah for the report + +10 Apr 03 01.30.06 - Alberto <albertogli@telpin.com.ar> + * msn: implement color themes + +09 Apr 03 00.55.43 - Alberto <albertogli@telpin.com.ar> + * msn, msnsetup: implement profile support + +26 Mar 03 13.06.55 - Alberto <albertogli@telpin.com.ar> + * msn: when logging in, print the error code along with description + +18 Mar 03 15.45.37 - Alberto <albertogli@telpin.com.ar> + * msn: ask for password if not given in the configuration + +05 Mar 03 14.01.34 - Alberto <albertogli@telpin.com.ar> + * msnlib: don't send an extra new line in sendmsg + +16 Feb 03 16.48.08 - Alberto <albertogli@telpin.com.ar> + * msn: fix an exception in socket error handling + +20 Jan 03 14.31.43 - Alberto <albertogli@telpin.com.ar> + * tag: d4 tag (changelog entry not included in d4's tarballs) + +20 Jan 03 10.39.06 - Alberto <albertogli@telpin.com.ar> + * doc: update url and add mailing list information + * install: allow the user to specify an installation root directory + * install: install documentation + * setup: add url + +16 Jan 03 13.48.40 - Alberto <albertogli@telpin.com.ar> + * msnlib: moved the encoding support from the client to the library + * msncb: close the sbd when the user goes offline + * msnlib: print the discarded messages when the user goes offline + it fixes an annoying race where a user goes offline and we have an + open sbd. Thanks to Peter Stuge for the report + +14 Jan 03 23.04.23 - Alberto <albertogli@telpin.com.ar> + * msn: implemented local ignore + * msn: now show internal variables in 'config' + * doc: added a 'commands' doc to explan the commands in more detail + +13 Jan 03 20.52.13 - Alberto <albertogli@telpin.com.ar> + * msn: encoding support, thanks to Peter Stuge for the suggestion and code. + +05 Jan 03 14.35.17 - Alberto <albertogli@telpin.com.ar> + * msn: fixes to time.strftime to make it work under 2.0 + * doc: small documentation updates, including a new license note. + * msn: now use "/usr/bin/env python" to run msn + * msn: run python without the -Q switch + +26 Nov 02 11.36.00 - Alberto <albertogli@telpin.com.ar> + * tag: d3 tag + +24 Nov 02 12.50.11 - Alberto <albertogli@telpin.com.ar> + * msn: completed the FAQ, and added a reporting_bugs document + * msn: small changes to the INSTALL file + * msn: return nothing on log_msg as the return value is not important + +19 Nov 02 15.55.52 - Alberto <albertogli@telpin.com.ar> + * msn: fix two big bugs with terminal handling; using nonblocking io makes + flushes and write fail under certain conditions (quite easily + triggered when running into a X terminal), so now they're replaced + with safe wrappers. Thanks to kusamochi@msn.com for the report, and + ameoba@opn for the huge help with the fix. + * msnlib: improved the close call, so now it won't raise an exception on a + sbd that has a non-established socket + +17 Nov 02 10.26.31 - Alberto <albertogli@telpin.com.ar> + * msn: enabling debug prints the terminal size and termios use + +12 Nov 02 00.42.58 - Alberto <albertogli@telpin.com.ar> + * msn: changed some command output to look less cryptic + * doc: add the FAQ + +08 Nov 02 00.03.43 - Alberto <albertogli@telpin.com.ar> + * msn: improved tab completion adding basic cycling + * msn: be able to disable logging using the already-existing 'log history' + configuration variable + +31 Oct 02 15.38.45 - Alberto <albertogli@telpin.com.ar> + * msnlib: remove unused constant BSIZE + * msnlib: update VERSION + +30 Oct 02 18.35.28 - Alberto <albertogli@telpin.com.ar> + * msn: esthetic code changes + * tag: d2 tag + +29 Oct 02 08.50.14 - Alberto <albertogli@telpin.com.ar> + * msn: fix a small bug with the up and down keys + * msn: another small fix to ignore unknown escape sequences + * msn: change the order of tab keystrokes, now received goes first + * msn: don't print the prompt on exit + +28 Oct 02 18.45.12 - Alberto <albertogli@telpin.com.ar> + * msn: handle screen width appropiatedly + * tag: d1 tag + +24 Oct 02 13.22.34 - Alberto <albertogli@telpin.com.ar> + * msn: implemented command line history using escape codes, and added a + configuration option for its size + * msn: implemented basic tab completion + +23 Oct 02 21.43.08 - Alberto <albertogli@telpin.com.ar> + * msn: framework for advanced terminal handling + * doc: added a 'portability' doc with some info + * doc: several minor modifications + * msn: minor modifications to the help text + +23 Oct 02 09.33.51 - Alberto <albertogli@telpin.com.ar> + * msn: improve output on server disconnect + * msn: change the interpreter to /usr/bin/python, which is a more standard + location + +22 Oct 02 21.49.07 - Alberto <albertogli@telpin.com.ar> + * msn: use quit() instead of sys.exit() in several places + * msn: handle network errors when logging in + * tag: c2 tag + +10 Oct 02 18.43.59 - Alberto <albertogli@telpin.com.ar> + * msn: be able to use emails as nicks (can be useful for weird nicks) + * msn: fix a simple bug when tried to close a socket that didn't exist + * msn: unified the quit procedure and handle a keyboard interrupt properly + * msn: improved parameter handling in 'del' and 'privacy' commands, avoiding + crashes with incorrect user input + * msn: added 'ww' and 'ee' commands, which are like 'w' and 'e' but show also + email addresses + * msn: added some help + * msn: improved 'status' a bit + * msnlib: added a 'priv' field in the user class (as a dictionary) which can + be used by the client for private purposes + * msn: display 'typing' only once, and show the last typing time in the user + info + * msn: improved some code indentation, documentation and comments + * doc: minor changes in the INSTALL and README files + * msn: display time with status changes + +04 Oct 02 10.20.46 - Alberto <albertogli@telpin.com.ar> + * msn: now lstrip before parsing the command, so we don't crash on spaces + +28 Sep 02 13.26.23 - Alberto <albertogli@telpin.com.ar> + * msn: added a handler for a socket.socket exception when reading from + sockets, and now print the error + +24 Sep 02 16.56.12 - Alberto <albertogli@telpin.com.ar> + * msn: fixed some small error displays on unknown nick. + * msn: fixed a crash when not giving enough parameters to 'm' + +24 Sep 02 10.50.59 - Alberto <albertogli@telpin.com.ar> + * msnlib: fixed the only (known =) remaining bug, now connections are no + longer closed when we send a message + * tag: c1 tag + +23 Sep 02 13.30.08 - Alberto <albertogli@telpin.com.ar> + * msn: fixed a bug in the 'del' command which passed the nick instead of the + email to msnlib, so removals when nick != email (the common case) + failed + * msn: print emails when showing the the reverse user list + * msn: added command 'debug' to toggle debugging at runtime + * msnlib, msncb: print debugging output to stderr to make capturing easier + +22 Sep 02 13.54.29 - Alberto <albertogli@telpin.com.ar> + * msn: implement auto-away + * msn: fixed the configuration variable handling when set to an invalid value + * msn: added command 'config' to display the configuration + * msn: remove '\r' from the incoming messages + +18 Sep 02 21.18.17 - Alberto <albertogli@telpin.com.ar> + * msnlog.vim: added a vim syntax file to make log reading nicer + * scripts - utils: renamed 'scripts' to 'utils' + +17 Sep 02 15.48.50 - Alberto <albertogli@telpin.com.ar> + * doc: modified several doc files + * tag: b3 tag - released, thanks a lot to Henne Vogelsang for providing the hosting + +16 Sep 02 14.53.34 - Alberto <albertogli@telpin.com.ar> + * msnsetup: create a simple setup script for the configuration file + * install: create the installer scripts for the python modules and the client + * msn: fixed a multi-line message logging bug + +13 Sep 02 12.53.15 - Alberto <albertogli@telpin.com.ar> + * msn: implement history (and status change) logging + * msn: ignore Hotmail messages + * tag: b2 tag + +12 Sep 02 16.41.26 - Alberto <albertogli@telpin.com.ar> + * msncb.py: when flushing pending messages we used to keep the message in the + sbd msgqueue, fixed + * msn: show the pending messages on switchboard disconnect + * config: moved the configuration files default location to ~/.msn + +11 Sep 02 12.25.41 - Alberto <albertogli@telpin.com.ar> + * msnclient.py - msn: renamed msnclient.py to msn + * cb.py - msncb.py: renamed cb.py to msncb.py + * msn: s/cb/msncb/g to reflect the rename + * msn: added a configuration file, which led to a reorganization of some + variables and init procedures + * msn: some new-line changes to the print* functions + * msn: create print functions for outgoing and incoming messages, and now + print the time + * msn: display more information on some commands now that we have config + * msn: added 'close' command, which closes a sbd connection manually + +11 Sep 02 08.36.36 - Alberto <albertogli@telpin.com.ar> + * msnclient.py: create a nick2email and email2nick functions + * msnclient.py: added 'r' (reply) and 'a' (send to the last person you sent a + message to) commands + * msnclient.py: added 'info' command to display user info and simplified the + user list + * msnclient.py: modified several commands to accept and display nick instead + of email + +11 Sep 02 00.16.16 - Alberto <albertogli@telpin.com.ar> + * msnlib.py: make status_change return 0 if error + * msnclient.py: improve the status command + * msnclient.py: reverse list view (command 'wr') + +10 Sep 02 20.41.53 - Alberto <albertogli@telpin.com.ar> + * msnclient.py: change message command from 'msg' to 'm' + * msnclient.py: use nicks instead of email to send messages + +04 Sep 02 10.30.59 - Alberto <albertogli@telpin.com.ar> + * msnclient.py: create print_msg for message printing + * msnclient.py: add command: e, display only online contacts + * msnclient.py: add incoming message history through a size-limited list + * msnclient.py: defer outgoing message display until flushing + +02 Sep 02 21.48.28 - Alberto <albertogli@telpin.com.ar> + * tag: b1 tag; a lot of things got fixed, so far the library is solid (never + had a crash, but didn't get more test than myself) + +27 Aug 02 20.59.27 - Alberto <albertogli@telpin.com.ar> + * msnlib.py: added version number + * msnclient.py: make up an usable client, tons of changes + * cb.py: make the error table global + +25 Aug 02 13.17.59 - Alberto <albertogli@telpin.com.ar> + * msnlib.py: fixed a small bug that left the sb sockets nonblocking which + might bring some problems + * msnlib.py: fixed an important bug for sending messages that wrote two + newlines after a message, and apparently the server didn't like it + +29 Jul 02 00.44.30 - Alberto <albertogli@telpin.com.ar> + * tag: a1 tag; fixed several bugs regarding messaging and now it's probably + done (as in 'everything kinda works'). Obviously the interface still + sucks but i think i'm done with the callbacks. Also added a bit of + documentation, the basic README and cleaned up a little bit + +28 Jul 02 04.04.22 - Alberto <albertogli@telpin.com.ar> + * tag: Initial a0 tag. The basic working structure is there, it + hasn't been tested enough (ie. never received or sent a message) but + there is no need because i know it works =) + diff --git a/doc/CodingStyle b/doc/CodingStyle new file mode 100644 index 0000000..c6c2a18 --- /dev/null +++ b/doc/CodingStyle @@ -0,0 +1,10 @@ + +Please, if you are going to submit any patches, use the linux kernel coding +style (for those which are not familiar with it, it's basically just k&r). + +If you haven't, read it from linux/Documentation/CodingStyle; it's an +excellent doc and everything applies here. + +Yeah i'm aware that this is python and not C, but the style idea is the same +except a few minor things. + diff --git a/doc/FAQ b/doc/FAQ new file mode 100644 index 0000000..9a3927d --- /dev/null +++ b/doc/FAQ @@ -0,0 +1,130 @@ + +* How do I subscribe to the mailing list, or browse the archives? + +You can use the mailing list's web interface, +http://lists.auriga.wearlab.de/cgi-bin/mailman/listinfo/msnlib-devel +for both reading the archives or subscribing. +Note that you don't need to be subscribed to post. + + +* What are the 'Message for NNN queued for delivery' and 'Flushing messages + to...' and what do they do? + +When you send a message to someone, the client has to open a connection first; +as this takes time, the message is put in a queue until the connection is +ready, and then they are sent. +The 'Message for NNN queued for delivery' tells you that the message has been +queued, then, a connection is tried to establish, and once this is done, the +messages in the queue are sent, and you are told with 'Flushing messages +to...'. +The connection is not done for every message, so you don't see this every +time, but usually only once for the first message. + + +* What's the meaning of 'User NNN is typing' and why only appears once? + +Those are special messages telling you that the user 'NNN' is typing a +message, and are sent by the client. Note that this msn client does _not_ send +these messages. +You only see them one because they are usually sent every 5 seconds, which +makes them highly annoying, so the client only displays one. If you want to +know the last time one of these messages was sent, you can do it using the +'info' command. + + +* How does tab completion work? + +At this moment, it's simple and very basic: you can cycle through the person +who last you received a message from, and the one you last sent one to. +Note that this cycling only works when the commandline is empty, or when you +have previosuly tabbed. + + +* How does auto-away work? + +It's quite simple: you configure it in your msnrc file, using a line like +'auto away = SECS' where SECS is the number of seconds after that, if you +didn't type anything, you're automatically set to away. +Then, when you come back and type, your status is automatically changed back +to online. +Note that this will work only when your status is online, because if i'm +'busy' i don't want it going back to away =) + + +* Where are my log files, and what's the format to read them? + +The log files are kept by default in $HOME/.msn/history, but it can be changed +using the 'history directory' option in your msnrc file. +The format is described in doc/log_format. + + +* What are the different ways of sending messages? + +You have three basic commands for sending messages: +The first one is 'm' (or its alias, 'msg'), which takes a nick or an email as +a parameter, and sends a message to that person. This is what tab completion +types for you. +The other two are the 'a' and 'r' messages; 'a' sends a message to the last +person you sent a message to; and 'r' replies a message to the last person you +received a message from. + + +* What's the reverse contact list? + +It's simply the list of all the people who have you on their contact list. You +can see it with the 'wr' command. + + +* How do the privacy option work? What is it for? + +The privacy option lets you set if you want people to ask you for +authorization to add you (i never implemented user authorization, so i have no +idea about how or if this works), and to block messages from people which are +not on your list. This is all done by the server so no permanent configuration +is needed. + +The command is called 'privacy', and has two parameters, called 'a' and 'p'; +which are set to 1 (yes) or 0 (no), and come from 'Authorization' and +'Privacy', the two policies we just talked about'. + +So, if you don't want to receive messages from people which are not on your +list, but you want people to add you without any authorization, the command +would be 'privacy 0 1'. + + +* Is there a way of enabling debugging without it getting in the middle of my + session? + +Sure, just run "msn 2> msn.debug.output"; it will redirect stderr (where debug +messages go) to the file called 'msn.debug.output'. Remember to enable +debugging too, using either the 'debug' command or adding 'debug = yes' to the +msnrc file. + + +* Why does the contact list has this weird order? + +The order in the contact list is given by the email addresses, which obviously +has no reference whatsoever to the nicks; so even if it looks unsorted, you +notice an alphabetic sort quite clearly when you do 'ww'. + + +* I get 'Main socket closed' followed by a strange error everyonce in a while + +Output sample: + [msn] status invisible + Status changed to: invisible + [msn] + Main socket closed ((104, 'Connection reset by peer')) + Closing + ; + +This happens when some unexpected network error occurs. It depends highly on +the error, but most of the times it's because the MSN server has closed our +main connection without reason, or our internet connection just dropped. There +isn't much we can do about it; we could reconnect and that's on the TODO list. +But if you get these errors normally, after checking your internet connection +(it could be just that) please send me the error report (a copy/paste of the +output would be just fine). + + + diff --git a/doc/LICENSE b/doc/LICENSE new file mode 100644 index 0000000..bdf1e48 --- /dev/null +++ b/doc/LICENSE @@ -0,0 +1,186 @@ + +This project, 'msnlib', is copyrighted by Alberto Bertogli and licensed under +the Open Software License version 2.0 as obtained from www.opensource.org (and +included here-in for easy reference) (that license itself is copyrighted by +Larry Rosen). + +Note that the "Original Work" that this license covers is only the library +itself and the client, along with the sample callbacks. Thus just the act of +linking/importing this library into another program does NOT in itself make +that program considered a derivative work of this Original Work. + + Alberto Bertogli + 20 May 2003 + +------------------------------------------------------------------------- + + +Open Software License + v. 2.0 + +This Open Software License (the "License") applies to any original work of +authorship (the "Original Work") whose owner (the "Licensor") has placed the +following notice immediately following the copyright notice for the Original +Work: + + Licensed under the Open Software License version 2.0 + + +1) Grant of Copyright License. Licensor hereby grants You a world-wide, +royalty-free, non-exclusive, perpetual, sublicenseable license to do the +following: + + a) to reproduce the Original Work in copies; + + b) to prepare derivative works ("Derivative Works") based upon the + Original Work; + + c) to distribute copies of the Original Work and Derivative Works to + the public, with the proviso that copies of Original Work or + Derivative Works that You distribute shall be licensed under the + Open Software License; + + d) to perform the Original Work publicly; and + + e) to display the Original Work publicly. + +2) Grant of Patent License. Licensor hereby grants You a world-wide, +royalty-free, non-exclusive, perpetual, sublicenseable license, under patent +claims owned or controlled by the Licensor that are embodied in the Original +Work as furnished by the Licensor, to make, use, sell and offer for sale the +Original Work and Derivative Works. + +3) Grant of Source Code License. The term "Source Code" means the preferred +form of the Original Work for making modifications to it and all available +documentation describing how to modify the Original Work. Licensor hereby +agrees to provide a machine-readable copy of the Source Code of the Original +Work along with each copy of the Original Work that Licensor distributes. +Licensor reserves the right to satisfy this obligation by placing a +machine-readable copy of the Source Code in an information repository +reasonably calculated to permit inexpensive and convenient access by You for +as long as Licensor continues to distribute the Original Work, and by +publishing the address of that information repository in a notice immediately +following the copyright notice that applies to the Original Work. + +4) Exclusions From License Grant. Neither the names of Licensor, nor the names +of any contributors to the Original Work, nor any of their trademarks or +service marks, may be used to endorse or promote products derived from this +Original Work without express prior written permission of the Licensor. +Nothing in this License shall be deemed to grant any rights to trademarks, +copyrights, patents, trade secrets or any other intellectual property of +Licensor except as expressly stated herein. No patent license is granted to +make, use, sell or offer to sell embodiments of any patent claims other than +the licensed claims defined in Section 2. No right is granted to the +trademarks of Licensor even if such marks are included in the Original Work. +Nothing in this License shall be interpreted to prohibit Licensor from +licensing under different terms from this License any Original Work that +Licensor otherwise would have a right to license. + +5) External Deployment. The term "External Deployment" means the use or +distribution of the Original Work or Derivative Works in any way such that the +Original Work or Derivative Works may be used by anyone other than You, +whether the Original Work or Derivative Works are distributed to those persons +or made available as an application intended for use over a computer network. +As an express condition for the grants of license hereunder, You agree that +any External Deployment by You of a Derivative Work shall be deemed a +distribution and shall be licensed to all under the terms of this License, as +prescribed in section 1(c) herein. + +6) Attribution Rights. You must retain, in the Source Code of any Derivative +Works that You create, all copyright, patent or trademark notices from the +Source Code of the Original Work, as well as any notices of licensing and any +descriptive text identified therein as an "Attribution Notice." You must cause +the Source Code for any Derivative Works that You create to carry a prominent +Attribution Notice reasonably calculated to inform recipients that You have +modified the Original Work. + +7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that +the copyright in and to the Original Work and the patent rights granted herein +by Licensor are owned by the Licensor or are sublicensed to You under the +terms of this License with the permission of the contributor(s) of those +copyrights and patent rights. Except as expressly stated in the immediately +proceeding sentence, the Original Work is provided under this License on an +"AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, +without limitation, the warranties of NON-INFRINGEMENT, MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE +ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an +essential part of this License. No license to Original Work is granted +hereunder except under this disclaimer. + +8) Limitation of Liability. Under no circumstances and under no legal theory, +whether in tort (including negligence), contract, or otherwise, shall the +Licensor be liable to any person for any direct, indirect, special, +incidental, or consequential damages of any character arising as a result of +this License or the use of the Original Work including, without limitation, +damages for loss of goodwill, work stoppage, computer failure or malfunction, +or any and all other commercial damages or losses. This limitation of +liability shall not apply to liability for death or personal injury resulting +from Licensor's negligence to the extent applicable law prohibits such +limitation. Some jurisdictions do not allow the exclusion or limitation of +incidental or consequential damages, so this exclusion and limitation may not +apply to You. + +9) Acceptance and Termination. If You distribute copies of the Original Work +or a Derivative Work, You must make a reasonable effort under the +circumstances to obtain the express assent of recipients to the terms of this +License. Nothing else but this License (or another written agreement between +Licensor and You) grants You permission to create Derivative Works based upon +the Original Work or to exercise any of the rights granted in Section 1 +herein, and any attempt to do so except under the terms of this License (or +another written agreement between Licensor and You) is expressly prohibited by +U.S. copyright law, the equivalent laws of other countries, and by +international treaty. Therefore, by exercising any of the rights granted to +You in Section 1 herein, You indicate Your acceptance of this License and all +of its terms and conditions. This License shall terminate immediately and you +may no longer exercise any of the rights granted to You by this License upon +Your failure to honor the proviso in Section 1(c) herein. + +10) Termination for Patent Action. This License shall terminate automatically +and You may no longer exercise any of the rights granted to You by this +License as of the date You commence an action, including a cross-claim or +counterclaim, for patent infringement (i) against Licensor with respect to a +patent applicable to software or (ii) against any entity with respect to a +patent applicable to the Original Work (but excluding combinations of the +Original Work with other software or hardware). + +11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this +License may be brought only in the courts of a jurisdiction wherein the +Licensor resides or in which Licensor conducts its primary business, and under +the laws of that jurisdiction excluding its conflict-of-law provisions. The +application of the United Nations Convention on Contracts for the +International Sale of Goods is expressly excluded. Any use of the Original +Work outside the scope of this License or after its termination shall be +subject to the requirements and penalties of the U.S. Copyright Act, 17 U.S.C. +101 et seq., the equivalent laws of other countries, and international treaty. +This section shall survive the termination of this License. + +12) Attorneys Fees. In any action to enforce the terms of this License or +seeking damages relating thereto, the prevailing party shall be entitled to +recover its costs and expenses, including, without limitation, reasonable +attorneys' fees and costs incurred in connection with such action, including +any appeal of such action. This section shall survive the termination of this +License. + +13) Miscellaneous. This License represents the complete agreement concerning +the subject matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent necessary +to make it enforceable. + +14) Definition of "You" in This License. "You" throughout this License, +whether in upper or lower case, means an individual or a legal entity +exercising rights under, and complying with all of the terms of, this License. +For legal entities, "You" includes any entity that controls, is controlled by, +or is under common control with you. For purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the direction or +management of such entity, whether by contract or otherwise, or (ii) ownership +of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial +ownership of such entity. + +15) Right to Use. You may use the Original Work in all ways not otherwise +restricted or conditioned by this License or by law, and Licensor promises not +to interfere with or be responsible for such uses by You. + +This license is Copyright (C) 2003 Lawrence E. Rosen. All rights reserved. +Permission is hereby granted to copy and distribute this license without +modification. This license may not be modified without the express written +permission of its copyright owner. diff --git a/doc/TODO b/doc/TODO new file mode 100644 index 0000000..fe7cca6 --- /dev/null +++ b/doc/TODO @@ -0,0 +1,43 @@ + + +package TODO +* more documentation (client manpages, basic api, etc.) +* allow installer to detect python location + it is a possibility to have special installers for non-unix platforms + +msn client TODO +* be able to use nick with spaces + this can cause a lot of damage, is it worthy? +* line editing +* make tab completion work with any command besides 'm' +* handle SIGWINCH for terminal resizes if signals are available +* colours for the user list + + +Future / In doubt +----------------- +(things listed here are either marked to do in some future (because we have to +wait on some feature becoming popular) or are in doubt of ever being +implemented at all) + +msn client +* signal handling + when the signal module becames popular on distros, we can get rid of + the select() ugly stuff for auto-away and re-implement it using + alarm() or things like that; add support for SIGWINCH, send syncs + often, etc. + + +msn lib +* file transfer + this is waaaaay below in my priority lists. there are thousand of + better ways to do file transfer between two hosts; plus the protocol + is even more ugly than the messaging one (yes, it really is) +* micq integration + in a perfect world, this wouldn't even exist and we would have micq + talk the msn protocol. so the ideal solution would be to rewrite all + this in C and integrate it with micq; probably working in two + different levels: one would be the C implementation of this, and the + other one micq multiprotocol support, that allows a clean integration + with it + diff --git a/doc/URL b/doc/URL new file mode 100644 index 0000000..9527ce1 --- /dev/null +++ b/doc/URL @@ -0,0 +1,10 @@ + +The URL for the project is http://users.auriga.wearlab.de/~alb/msnlib +I want to thank Henne Vogelsang and Auriga for providing the hosting. + +There is also a freshmeat project page that it's easier to remember: +http://freshmeat.net/projects/msnlib + +And finally, there's the mailing list, msnlib-devel@auriga.wearlab.de, with url +http://lists.auriga.wearlab.de/cgi-bin/mailman/listinfo/msnlib-devel. + diff --git a/doc/commands b/doc/commands new file mode 100644 index 0000000..e5afc64 --- /dev/null +++ b/doc/commands @@ -0,0 +1,119 @@ + +This is a more detailed explanation of the commands provided in the client. +A brief list is available at runtime with the 'help' command. + +Note that the runtime help gets updated more frequently than this one. + + +status [mode] + Shows the current status, or changes it to "mode", which can be one of: + online, away, busy, brb, phone, lunch, invisible or idle. + +q + Quits the program. + +w + Prints your entire contact list. + Contacts connected are highlighted in bold. + +ww + Prints your entire contact list, including email addresses. + +e + Prints your online contacts. + +ee + Prints your online contacts, including email addresses. + +eg + Prints your online contacts with the groups + +wr + Prints your reverse contact list. + (see the FAQ for more details) + +h + Shows your incoming message history + This is a list of the last N messages that you received. The amount is + configurable with the directive "input history size" in the msnrc + file, which defaults to 10. + +add email nick + Adds the user "email" with the nick "nick". + +del nick + Deletes the user with nick "nick". + +info [nick] + Shows the user information and pending messages (if any), or our + personal info. It also includes some advanced information (like the + server socket or tid). + +lignore [nick] + Locally ignores the user, or display the locally ignored users list. + This makes the messages coming from the user not to be displayed, but + they are still logged so you can see them later. + +lunignore nick + Removes a user from the locally ignored users list. + +block nick + Blocks a user + +unblock nick + Unblocks a blocked user + +g + Shows the group list + +gadd gname + Adds the group "gname" + +gdel gname + Deletes the group "gname". Note that all the users in the group will + be deleted too. + +gren old new + Renames the group "old" with the name "new" + +color [theme] + Shows the available color themes, or set the color theme to "theme". + +close nick + Closes the switchboard connection with "nick". There is no need to use + this command and is included just for advanced users and debugging. + +config + Shows the configuration, in the internal representation, along with + other special automatic variables. This means that many commands and + values won't have the same format; for instance, instead of 'yes' you + will often see a 1. This command is mostly exclusively included for + bug reporting. + +nick newnick + Changes your nick to "newnick". + +privacy p a + Sets whether accept messages from people not on your list (p) and + require authorization (a). + (see the FAQ for more details) + +m nick text + Sends a message to "nick" with the "text". The standard and most + practical way of sending a message. Tab completion expands to this. + +a text + Sends a message with "text" to the last person you sent a message to. + +r text + Sends a message with "text" to the last person that sent you a + message. + +invite u1 to u2 + Invites u1 into the chat with u2 + + +In most cases, where you are asked for a nick, you can alternatively enter the email. +This makes it easier to handle people with weird or long nicks. + + diff --git a/doc/dependencies b/doc/dependencies new file mode 100644 index 0000000..00db0df --- /dev/null +++ b/doc/dependencies @@ -0,0 +1,6 @@ + +The only thing you need is a working python installation with SSL support +(most of them have it by default). + +Since release 3.0, it requires at least Python 2.2.2 because of SSL support. + diff --git a/doc/icons b/doc/icons new file mode 100644 index 0000000..c2fee09 --- /dev/null +++ b/doc/icons @@ -0,0 +1,46 @@ +(#) sun +(%) handcuffs +(&) dog +(*) star +(0) clock, same as (O) +(6) devil +(8) musical note +(@) cat +(A) angel +(B) beer +(C) cup of something +(E) envelope/email +(F) rose +(G) gift +(H) smile with sunglasses +(I) light bulb +(K) kiss/lips +(L) heart +(M) msn icon +(N) thumb down +(O) clock, same as (0) +(P) photo camera +(R) rainbow +(S) moon +(T) phone +(U) broken heart +(W) fallen rose +(X) woman +(Y) thumb up +(Z) man +(^) cake +({) man hug +(}) woman hug +(~) film +:$ blushed +:'( crying +:( sad +:) normal smile +:-O surprise +:@ angry +:D wide smile +:P tongue +:S confused +:| surprised, astonished +;) wink + diff --git a/doc/log_format b/doc/log_format new file mode 100644 index 0000000..61ee96a --- /dev/null +++ b/doc/log_format @@ -0,0 +1,44 @@ + +This document describes the msn client log format. + +The files are named only with the email address of the sender/receiver; except +for the multi-user chats (that is, a chat with more than you and somebody else +involved) where the name is composed of 'M::' and then the list of +participants (excluding yourself) sorted in alphabetical order, separated by +commas (','). + + +The format is very simple: + +Day/Month/Year HH:MM:SS email ID text + +where ID is one of: + '>>>' if the text is an outgoing message + '<<<' if the text is an incoming message + '***' if the text refers to a new status + '+++' if the text refers to multi-user chats (joining and leaving) + +and email is the email of the user you send the message to/got a message from, +except on the multi-user chat case, where for messages you sent your own +email address is displayed. + + +For example: +Day/Month/Year HH:MM:SS email *** online +Day/Month/Year HH:MM:SS email <<< incoming message +Day/Month/Year HH:MM:SS email >>> outgoing message +Day/Month/Year HH:MM:SS email *** offline +Day/Month/Year HH:MM:SS email +++ join + + +For multi-line messages, the message is indented with a tab, like: +Day/Month/Year HH:MM:SS email <<< + line1 + line2 + ... + linen + + +In the utils directory you can find the file "msnlog.vim", which is a vim +syntax highlighting file for this log format. + diff --git a/doc/msg_states b/doc/msg_states new file mode 100644 index 0000000..25124fe --- /dev/null +++ b/doc/msg_states @@ -0,0 +1,47 @@ + + +The graph below is an attempt of the simple state diagram that rules message +delivery. + +It's split in a half according to whether you are inviting or answering; and +on the sides are the different sbf's state. + +The middle zone (for both types) is the message type with an arrow indicatiing +the destination (left is us, right is the server); except for the only two +two-letter codes right in the middle, which show the common state to both +sides. + +Once the 'es' state is reached, the difference is forgotten (even tho it's +kept, it's never used) and both types of sbf are handled the same way. + + +------------------------+------------------------ + invite | answer +------------------------+------------------------ +state | | | state +------------------------+------------------------ + ----+ xfr-> + xf | | + ----+ <-xfr <-rng +---- + cp | \ / | cp + | \ / | + ----+ cp +---- + | / \ | + re | / \ | re + | / \ | + ----+ usr-> ans-> +---- + | | | | + us | <-usr <-iro | an + | | | | + ----+ cal-> <-ans +---- + ca | | | | + ----+ <-cal / | + jo | | / | es + ----+ <-joi / | + es | \ / | + V es V + + + + + diff --git a/doc/msncd_protocol b/doc/msncd_protocol new file mode 100644 index 0000000..7fddb93 --- /dev/null +++ b/doc/msncd_protocol @@ -0,0 +1,128 @@ +This is a description of the protocol used by msncd. + +It is still in an experimental stage as only few people dared to try it; if +you do please let me know by sending a message to the mailing list. + + +Add user: +-> ADD nick email\n + +Delete user: +-> DEL email\n + +Change our nick: +-> NICK newnick\n + +Change the privacy behaviour: +-> PRIV p a\n + +Change our status: +-> STATUS newstatus\n + +Log in: +-> LOGIN email password\n + +Log off: +-> LOGOFF\n + +Info requests: +-> INFO email\n +<- AttributeA = ValueA\n +<- AttributeB = ValueB\n +<- ... +<- \n + +Send message: +-> SENDMSG number_of_lines email\n +-> line1\n +-> line2\n +-> ... +-> lineN\n + +Poll for events: +-> POLL\n + (begin to send first message) +<- MSG number_of_lines end_of_header src_email\n +<- line1\n +<- line2\n +<- ... +<- lineN\n + (and repeat for number_of_messages) + (then send status changes) +<- STCH email newstatus [number_of_messages_discarded]\n + (a user has added us) +<- UADD email\n + (a user has deleted us) +<- UDEL email\n + (a user was added) +<- ADDFL email\n + (a user was deleted) +<- DELFL email\n + (flushed messages) + MFLUSH email\n + (typing notifications) + TYPING email\n + (a socket was closed, type is either 'MAIN' or 'USER') + SCLOSE type [email number_of_messages_discarded]\n + (unexpected errors) +<- ERR code description\n + (finally, end the poll) +<- POLLEND\n + +Get contact list: +-> GETCL\n +<- CL number_of_contacts\n +<- status1 email1 nick1\n +<- status2 email2 nick2\n +<- ... +<- statusN emailN nickN\n + +Get reverse contact list: +-> GETRCL\n + (same behaviour as GETCL) + + +And I think that'd be pretty much it. Anyway, it's easily extensible. + +Note that: + * In most places, email can be exchanged with the url-encoded nick. The + server _always_ replies using the email + * The 'server' (that is, the real client, that reads from the pipe) responses + an 'OK\n' for most commands, or 'ERR description\n'; here they are + ommited for brevity. + * If considered necesary, a timestamp could be sent before some responses to + indicate time. I'm not sure about this, because polling often (like + 250ms) has enough granularity and it doesn't represent any load, so we + could just avoid the overhead. Even with 1s poll time, there are no + problems regarding times. + * New pollable stuff could also be added later (files, for instance). Poll + responses can come in any order. + * This is syncronous and events get queued on the server side. Server _never_ + issues a pipe write without a request, that's what we have POLL for. + This avoid a huge load of races, and the client code to avoid them. + * Unexpected errors also come out from POLL, specially the network ones that + tend to be quite async to everything else. + + +So now, we start offline and then, on 'LOGIN' we connect and after we log in, +the 'OK' response is sent. Then the client changes the status to whatever he +wants, for instance with 'STATUS away'. + +The client will mostly like do a GETCL and GETRCL after that, and then start +polling for events, which now becomes the common path. This is very efficient +when no new events are pending (again, the common case), just: +-> POLL\n +<- POLLEND\n + +Disconnect is as simple as 'LOGOFF'. Note that this is not the same as +'STATUS offline', as the former closes everything and returns to the initial +state. + +Much simpler than everything else (remember that most other stablished +protocols require to have a library, a python binding and so on), it can be +implemented quite fast in any language, and it's simple and efficient. + +No connection or user tracking is required on the client, no state machines, +nothing. Just a frontend for a text protocol. + + diff --git a/doc/portability b/doc/portability new file mode 100644 index 0000000..cd17896 --- /dev/null +++ b/doc/portability @@ -0,0 +1,39 @@ + +The library itself should be portable, as it doesn't contain any specific +stuff that might have problems, and it has been reported to work under +different unixes and even under windows. + +I tend to code based on posix/sus, but I think it's pretty much generic python +both the library and the client, specially the former. I'm almost sure it +will run unmodified on unix platforms (and the only reason i say 'almost' is +because i didn't test it myself, but it certanly should). + + +About the text mode client the only thing that is tied to a unix environment +is the client terminal handling (which requires termios and fcntl modules), +but it's isolated and has runtime detection, so if you don't have any of these +modules, or they fail for some reason, the client will fall back to the normal +behaviour. Also, doing select() on stdin isn't ok for some platforms (windows +being the most popular one), but it's really safe for unix. + +Another thing that might be conflictive for non-unix platforms is that I +assume the python interpreter is callable using "/usr/bin/env python"; these +are the closest thing to a standard location on unix boxes. If you need to +change this, the places are the first line of 'msn', and somewhere inside +'install'. + +The next possible problem (always talking about non-unix platforms) is +'msnsetup' and the configuration file location; the first one requires bash, +so if you don't have it, you can just create your own msnrc file based on +'msnrc.sample'; but the location is assumed to be $HOME/.msn/msnrc, and maybe +you don't have '$HOME' (or you don't even have environment variables at all), +in this case you specify the location on the command line, as the first and +only argument to msn: "msn /path/to/msnrc". + + +If you run it under a different platform, please let me know; specially if you +had (or have) any problems. + +Thanks, + Alberto + diff --git a/doc/profiles b/doc/profiles new file mode 100644 index 0000000..40e37c1 --- /dev/null +++ b/doc/profiles @@ -0,0 +1,20 @@ + +The msn client, from release D5, supports different profiles. + +What this means is that now you can have several configurations under the same +unix user in a very simple way. + +Just create a profile like you normally create your configuration, telling the +setup the profile name, like: + +msnsetup profilename + + +Then, to run msn under an specific profile, do: + +msn profilename + +and you're done. + + + diff --git a/doc/reporting_bugs b/doc/reporting_bugs new file mode 100644 index 0000000..8a4f8a0 --- /dev/null +++ b/doc/reporting_bugs @@ -0,0 +1,38 @@ + +How to report bugs +------------------ + +If you think you've got a bug (or you are sure about it =), please report it +to msnlib-devel@auriga.wearlab.de; specifying: + +* msnlib version +* python version +* operating system information +* platform information +* obviously, a bug description + +Now, depending on what's the bug about, i'd like you to to include some of +these: + +* a copy of the output when the bug hits (see below for a complete to do this) +* reports on other versions (does this happens with an older version?) +* instructions on how to reproduce it +* terminal information (ie. running under normal console, some kind of xterm, + cygwin terminal, beos one, etc.) +* an strace -tt of the session +* the debug output + +On these last two, they're oftenly quite useful. Just in case you don't know +how to do it, a nice way of doing both at the same time and save to files +(which then you can send to me as attachments) is: + +* first, add 'debug = yes' to $HOME/.msn/msnrc; that enables debugging output +* then, run the command: + strace -tt -o msn_output-strace msn 2>msn_output-debug | tee msn_output-stdout + +and try to reproduce the bug, or just wait for it to happen =) + +Then, when reporting, send me these 3 files (msn_output-strace, +msn_output-debug, and msn_output-stdout), compressed if necesary. They are +really helpful, because I can see what's going on when the bug happened. + diff --git a/install b/install new file mode 100644 index 0000000..93260e8 --- /dev/null +++ b/install @@ -0,0 +1,31 @@ +#!/bin/bash + +# the first argument is the destination root directory +# for the package, defaults to /usr/local if not present + +DESTDIR="/usr/local" +if [ ".$1" != "." ]; then + DESTDIR="$1" +fi + +echo "*** Installing the library" +/usr/bin/env python setup.py install +echo + +echo "*** Installing the documentation" +rm -r $DESTDIR/doc/msnlib 2>/dev/null +mkdir $DESTDIR/doc/msnlib 2>/dev/null +cp -v README INSTALL $DESTDIR/doc/msnlib/ +cp -Rv doc/* $DESTDIR/doc/msnlib/ +echo + +echo "*** Installing the client" +mkdir $DESTDIR/bin 2>/dev/null +cp -v msn $DESTDIR/bin +cp -v msnsetup $DESTDIR/bin +echo + +echo "*** Done" +echo "Please read the INSTALL file to see the next step" +echo + diff --git a/msn b/msn new file mode 100644 index 0000000..1070f80 --- /dev/null +++ b/msn @@ -0,0 +1,1623 @@ +#!/usr/bin/env python + + +import sys +import os +import socket +import select +import string +import traceback +import urllib +import time + +import msnlib +import msncb + + +""" +MSN Client + +This is a fully usable msn client, which also serves as reference +implementation for msnlib-based code. +For further information refer to the documentation or the source (which is +always preferred). +Please direct any comments to the msnlib mailing list, +msnlib-devel@auriga.wearlab.de. +You can find more information, and the package itself, at +http://users.auriga.wearlab.de/~alb/msnlib +""" + + +# +# constant strings +# + +help = """\ +Command list: +status [mode] Shows the current status, or changes it to "mode", which can + be one of: online away busy brb phone lunch invisible idle +q Quits the program +w Prints your entire contact list +ww Prints your entire contact list, including email addresses +e Prints your online contacts +ee Prints your online contacts, including email addresses +eg Prints your online contacts with the groups +wr Prints your reverse contact list +h Shows your incoming message history +add e [n] [g] Adds the user "e" with the nick "n" to the group "g" +del nick Deletes the user with nick "nick" +ren nick new Renames the user with nick "nick" to appear as "new" +lignore [nick] Locally ignores the user, or display the locally ignored users +lunignore nick Removes a user from the locally ignored users list +block nick Blocks a user +unblock nick Unblocks a blocked user +g Shows the group list +gadd gname Adds the group "gname" +gdel gname Deletes the group "gname". Note that all the users in the + group will be deleted too. +gren old new Renames the group "old" with the name "new" +color [theme] Shows or set the color theme to "theme" +close nick Closes the switchboard connection with "nick" +config Shows the configuration +info [nick] Shows the user information and pending messages (if any), + or our personal info +nick newnick Changes your nick to "newnick" +privacy p a Sets whether accept messages from people not on your list (p) + and require authorization (a) +m nick text Sends a message to "nick" with the "text" +a text Sends a message to the last person you sent a message to +r text Sends a message to the last person that sent you a message +invite u1 to u2 Invites u1 into the chat with u2 + +In most cases, where you are asked for a nick, you can alternatively enter the +email. This makes it easier to handle people with weird or long nicks. +""" + + +# +# colors, for nice output +# + +class color_default: + def __init__(self): + self.name = 'default' + self.black = '\x1b[0;30m' + self.red = '\x1b[0;31m' + self.green = '\x1b[0;32m' + self.yellow = '\x1b[0;33m' + self.blue = '\x1b[0;34m' + self.magenta = '\x1b[0;35m' + self.cyan = '\x1b[0;36m' + self.white = '\x1b[0;37m' + self.normal = '\x1b[0m' + self.bold = '\x1b[1m' + self.clear = '\x1b[J' + +class color_bw: + def __init__(self): + self.name = 'bw' + self.black = '\x1b[0;30m' + self.red = '\x1b[0m' + self.green = '\x1b[0m' + self.yellow = '\x1b[0m' + self.blue = '\x1b[0m' + self.magenta = '\x1b[0m' + self.cyan = '\x1b[0m' + self.white = '\x1b[0m' + self.normal = '\x1b[0m' + self.bold = '\x1b[0m' + self.clear = '\x1b[J' + +color_classes = { + 'default': color_default, + 'bw': color_bw +} +c = color_classes['default']() + + +# +# different useful prints +# + +def printl(line, color = c.normal, bold = 0): + "Prints a line with a color" + out = '' + if line and line[0] == '\r': + clear_line() + if bold: + out = c.bold + out = color + out + line + c.normal + safe_write(out) + safe_flush() + +def perror(line): + "Prints an error" + out = '' + out += c.yellow + c.bold + '!' + c.normal + out += c.red + c.bold + '!' + c.normal + out += c.blue + c.bold + '!' + c.normal + out += ' ' + c.green + c.bold + line + c.normal + '\a' + safe_write(out) + safe_flush() + +def pexc(line): + "Prints an exception" + out = '\n' + out += ( c.cyan + c.bold + '!' + c.normal ) * 3 + safe_write(out) + safe_write(c.bold + line) + safe_flush() + traceback.print_exc() + safe_write(c.normal) + safe_write('\n') + safe_flush() + +def print_list(md, only_online = 0, userlist = None, include_emails = 0): + "Prints the user list" + if not userlist: + userlist = md.users + ul = userlist.keys() + ul.sort() + for email in ul: + u = userlist[email] + if u.status != 'FLN': + hl = 1 + else: + if only_online: continue + hl = 0 + status = msnlib.reverse_status[u.status] + printl('%7.7s :: %s ' % (status, u.nick), bold = hl) + if include_emails: + printl('(%s) ' % (email), bold = hl) + if 'B' in u.lists: + printl('[!]') + printl('\n') + +def print_group_list(md): + "Prints the group list" + gids = md.groups.keys() + gids.sort() + for gid in gids: + printl('%3.3s :: %s\n' % (gid, md.groups[gid])) + +def print_grouped_list(md, only_online = 0, include_emails = 0): + db = {} + for gid in md.groups.keys(): + db[gid] = [] + for gid in md.groups.keys(): + for e in md.users.keys(): + if md.users[e].gid == gid: + db[gid].append(e) + gids = db.keys() + gids.sort() + for gid in gids: + printl(':: %s ::\n' % md.groups[gid], bold = 1) + ul = db[gid] + ul.sort() + for email in ul: + u = m.users[email] + if u.status != 'FLN': + hl = 1 + else: + if only_online: continue + hl = 0 + status = msnlib.reverse_status[u.status] + printl('\t%7.7s :: %s ' % (status, u.nick), bold = hl) + if include_emails: + printl('(%s) ' % (email), bold = hl) + if 'B' in u.lists: + printl('[!]') + printl('\n') + +def print_user_info(email): + "Prints the user information, and pending messages" + u = m.users[email] + out = c.bold + out += c.bold + 'User info for ' + email + '\n' + out += c.bold + 'Nick:\t\t' + c.normal + u.nick + '\n' + out += c.bold + 'Status:\t\t' + c.normal + msnlib.reverse_status[u.status] + '\n' + if 'B' in u.lists: + out += c.bold + 'Mode:\t\t' + c.normal + 'blocked' + '\n' + if u.gid != None: + out += c.bold + 'Group:\t\t' + c.normal + m.groups[u.gid] + '\n' + if u.realnick: + out += c.bold + 'Real Nick:\t' + c.normal + u.realnick + '\n' + if u.homep: + out += c.bold + 'Home phone:\t' + c.normal + u.homep + '\n' + if u.workp: + out += c.bold + 'Work phone:\t' + c.normal + u.workp + '\n' + if u.mobilep: + out += c.bold + 'Mobile phone:\t' + c.normal + u.mobilep + '\n' + if u.priv.has_key('typing') and u.priv['typing']: + out += c.bold + 'Last typing at:\t' + c.normal + out += time.asctime(time.localtime(u.priv['typing'])) + '\n' + if u.sbd: + out += c.bold + 'Switchboard:\t' + c.normal + str(u.sbd) + '\n' + if u.sbd.msgqueue: + out += c.bold + 'Pending messages:' + '\n' + for msg in u.sbd.msgqueue: + out += c.bold + '\t>>> ' + c.normal + msg + '\n' + printl(out) + +def print_prompt(): + "Prints the user prompt" + safe_write('\r' + c.red + c.bold + '[msn] ' + c.normal) + safe_flush() + +def print_inc_msg(email, lines, eoh = 0, quiet = 0, ptime = 1, recvtime = 0): + """Prints an incoming message from a list of lines and an optional + end-of-header pointer. You can also pass the original received time as + a parameter, this is used for history printed.""" + nick = email2nick(email) + if not nick: nick = email + if email in ignored: + return + if ptime: + if recvtime: + ctime = time.strftime('%I:%M:%S%p', time.localtime(recvtime)) + else: + ctime = time.strftime('%I:%M:%S%p', now()) + printl('%s ' % ctime, c.blue) + printl('%s' % nick, c.cyan, 1) + if len(lines[eoh:]) == 1: + printl(' <<< %s\n' % lines[eoh], c.yellow, 1) + else: + printl(' <<< \n\t', c.yellow, 1) + msg = string.join(lines[eoh:], '\n\t') + msg = msg.replace('\r', '') + printl(msg + '\n', c.bold) + beep(quiet) + +def print_out_msg(nick, msg): + "Prints an outgoing message" + ctime = time.strftime('%I:%M:%S%p', now()) + printl('%s ' % ctime, c.blue) + printl('%s' % nick, c.cyan, 1) + printl(' >>> ', c.yellow, 1) + printl('%s' % msg) + + +def beep(q = 0): + "Beeps unless it's told to be quiet" + if not q: + printl('\a') + + +def safe_flush(): + """Safely flushes stdout. It fixes a strange issue with flush and + nonblocking io, when flushing too fast.""" + c = 0 + while c < 100: + try: + sys.stdout.flush() + return + except IOError: + c +=1 + time.sleep(0.01 * c) + raise Exception, 'flushed too many times, giving up. Please report!' + +def safe_write(text): + """Safely writes to stdout. It fixes the same issue that safe_flush, + that is, writing too fast raises errors due to nonblocking fd.""" + c = 1 + while c: + try: + sys.stdout.write(text) + return + except IOError: + c += 1 + time.sleep(0.01 * c) + raise Exception, 'wrote too many times, giving up. Please report!' + + +# +# useful functions +# + +def quit(code = 0): + "Exits" + printl('Closing\n', c.green, 1) + try: + try: + m.disconnect() + except: + pass + global oldtermattr + termios.tcsetattr(sys.stdin.fileno(), termios.TCSAFLUSH, oldtermattr) + except: + pass + sys.exit(code) + +def nick2email(nick): + "Returns an email according to the given nick, or None if noone matches" + for email in m.users.keys(): + if m.users[email].nick == nick: + return email + if nick in m.users.keys(): + return nick + return None + +def email2nick(email): + "Returns a nick accoriding to the given email, or None if noone matches" + if email in m.users.keys(): + return m.users[email].nick + else: + return None + +def matchemail(begin): + """"Returns a matching email/nick for the given beginning; it avoids + nicks with spaces""" + l = len(begin) + # first try the ones with sbd + for email in m.users.keys(): + if not m.users[email].sbd: + continue + nick = m.users[email].nick + if ' ' in nick: + nick = email + if len(nick) >= l and nick[0:l] == begin: + return nick + # then the nicks + for email in m.users.keys(): + nick = m.users[email].nick + if ' ' in nick: + continue + if len(nick) >= l and nick[0:l] == begin: + return nick + # and finally the emails + for email in m.users.keys(): + if len(email) >= l and email[0:l] == begin: + return email + return None + +def gname2gid(gname): + "Returns a group name according to the given group id" + for gid in m.groups.keys(): + if m.groups[gid] == gname: + return gid + return None + +def get_config(file): + "Parses a simple config file, and returns a var:value dict" + try: + fd = open(file) + except: + return None + lines = fd.readlines() + config = {} + for i in lines: + i = i.strip() + if i.find('=') < 0: + continue + if i[0] == '#': + continue + var, value = i.split('=', 1) + var = var.strip() + value = value.strip() + config[var] = value + return config + +def null(s): + "Null function, useful to void debug ones" + pass + +def log_msg(email, type, msg, mtime = 0, users = []): + """Logs the message or event of the 'type', related to 'email', + with the content 'msg', to a file in the specified directory. See + documentation for more specific details, specially about + formatting.""" + + if not config['log history']: + return + + if config['profile']: + prepend = config['profile'] + '::' + else: + prepend = '' + if users: + # copy and sort the user list, so we log always to the same + # file regarding the order the users were joined + # FIXME: sometimes we crash because filename is too long + usorted = users[:] + usorted.sort() + file = config['history directory'] + '/' + prepend + 'M::' + file += string.join(usorted, ',') + else: + file = config['history directory'] + '/' + prepend + email + if not mtime: + mtime = time.time() + out = '' + out += time.strftime('%d/%b/%Y %H:%M:%S ', time.localtime(mtime)) + out += email + ' ' + if type == 'in': + out += '<<< ' + msg = msg.replace('\r', '') + lines = msg.split('\n') + if len(lines) == 1: + out += msg + '\n' + else: + out += '\n\t' + out += string.join(lines[:], '\n\t') + out += '\n' + elif type == 'out': + out += '>>> ' + msg + '\n' + elif type == 'status': + out += '*** ' + msg + '\n' + elif type == 'multi': + out += '+++ ' + msg + '\n' + + fd = open(file, 'a') + fd.write(out) + fd.close() + del(fd) + return + +def now(): + "Returns the current time, in tuple format" + return time.localtime(time.time()) + + +# +# terminal handling +# + +# all this is _ugly_, a real mess; luckily it's pretty much self contained. +# if you're trying to follow the code, i highly recommend you to skip this +# section; you really don't need to know it, just think of redraw_cli() pretty +# much as print_prompt(), stdin_read() as sys.stdin.readline(), and +# clear_line() as printf('\r'). actually that's quite near true when we don't +# use termios. +# it has been written in a way that if termios is not available, we fall back +# to the normal and old behaviour which is guaranteed to work. + +try: + # all of this disables line-buffering on the terminal (thus allowing + # char-by-char reads) and echoing (so we output whatever we want); and + # finally sets the file nonblocking so we can read all that's + # available without complications. + # you should read termios and fcntl manpages to find out the details + import termios + stdinfd = sys.stdin.fileno() + oldtermattr = termios.tcgetattr(stdinfd) + newtermattr = termios.tcgetattr(stdinfd) + newtermattr[3] = newtermattr[3] & ~termios.ICANON & ~termios.ECHO + termios.tcsetattr(stdinfd, termios.TCSANOW, newtermattr) + import fcntl, os + fcntl.fcntl(stdinfd, fcntl.F_SETFL, os.O_NONBLOCK) + del(newtermattr) + use_termios = 1 +except: + use_termios = 0 + +# now we try to find out the console size; if we fail we fall back to +# the good old 80x24. +# note that the (' ' * 10) is just awful, but there is no sane way of +# doing this without using a C module. it's based on 'struct winsize', +# but as we only use the first 4 bytes, we don't ask for more; then we +# unpack the two shorts into (lenght, width) +try: + import struct + winsize = fcntl.ioctl(stdinfd, termios.TIOCGWINSZ, ' ' * 10) + winsize = struct.unpack('hh', winsize[:4]) +except: + winsize = (24, 80) +screenwidth = winsize[1] + +# input buffer, where all the characters written by the user are stored in +inbuf = '' + +# input history buffer, to store previous commands. +# we use a list [buffer, pointer] to avoid namespace pollution +inbuf_history = [[], -1] + +def stdin_read(): + """Reads from stdin, and acts in consecuense. If you don't use + termios, it's almost the same as calling readline(); but otherwise it + handles all the input reading.""" + global inbuf + if not use_termios: + inbuf = sys.stdin.readline() + tmpbuf = inbuf + inbuf = '' + out = parse_cmd(tmpbuf) + printl(out + '\n', c.green, 1) + redraw_cli() + return + + in_esc = 0 + input = sys.stdin.read() + for char in input: + if char == '\r': + # replace \r with \n, so we handle mac keyboard input + # properly (it breaks \r\n tho, but nobody uses it) + char = '\n' + inbuf = inbuf + char + if char == '\n': + # command history + if len(inbuf_history[0]) > config['input history size']: + del(inbuf_history[0][0]) + inbuf_history[0].append(inbuf[:-1]) + inbuf_history[1] = len(inbuf_history[0]) - 1 # moves the pointer + + safe_write(char) + tmpbuf = inbuf + inbuf = '' + out = parse_cmd(tmpbuf) + printl(out + '\n', c.green, 1) + redraw_cli() + + elif char == '\b' or ord(char) == 127: # ^H / DEL + inbuf = inbuf[:-2] + redraw_cli() + + elif ord(char) == 21: # ^U + inbuf = '' + redraw_cli() + + elif char == '\t': # tab + # we do a basic cycling between the last received and + # last sent; first we build the two strings and then + # we see which one applies according to some messy + # logic + if email2nick(last_received): + nick = email2nick(last_received) + if ' ' in nick: + nick = last_received + mtolrecv = 'm ' + nick + ' ' + else: + mtolrecv = None + + if email2nick(last_sent): + nick = email2nick(last_sent) + if ' ' in nick: + nick = last_sent + mtolsent = 'm ' + nick + ' ' + else: + mtolsent = None + + if len(inbuf) == 1: + if mtolsent: + inbuf = mtolsent + elif mtolrecv: + inbuf = mtolrecv + else: + inbuf = inbuf[:-1] + beep() + else: + # if we have mtolsent, replace with mtolrecv + # (if possible, otherwise beep) + if mtolsent and inbuf.strip() == mtolsent.strip(): + if mtolrecv: + inbuf = mtolrecv + else: + inbuf = inbuf[:-1] + beep() + # the opposite case + elif mtolrecv and inbuf.strip() == mtolrecv.strip(): + if mtolsent: + inbuf = mtolsent + else: + inbuf = inbuf[:-1] + beep() + # we have something that is neither mtolsent or + # mtolrecv, we try to find a matching + # email/nick + else: + p = inbuf.split() + if len(p) < 2: + # space + TAB or equivalent, + # just beep and ignore it + inbuf = inbuf[:-1] + beep() + elif p[0] != 'm' or len(p) != 2: + inbuf = inbuf[:-1] + beep() + else: + begin = p[1] + email = matchemail(begin) + if not email: + inbuf = inbuf[:-1] + beep() + else: + inbuf = 'm ' + email + ' ' + redraw_cli() + + elif ord(char) == 4: # EOT + safe_write('\n') + out = parse_cmd('') + printl(out + '\n', c.green, 1) + + elif ord(char) == 27: # ESC + # we use in_esc for escape secuenses (composed of + # ESC + '[' + LETTER). 1 means got ESC, 2 means got + # '['. Here we set to 1, and the rest are in the + # generic handling + in_esc = 1 + inbuf = inbuf[:-1] + + elif ord(char) < 32: # unhandled control + msnlib.debug('Got weird char: %d' % ord(char)) + redraw_cli_cond(char) + + else: # normal + if not in_esc: + redraw_cli_cond(char) + continue + + # comes from a escape code + elif in_esc == 1: + if char == '[': + in_esc = 2 + else: + in_esc = 0 + inbuf = inbuf[:-1] + elif in_esc == 2: + if char == 'A': # up + if inbuf_history[1] == -1: + # hit the top, or it's empty; + # remove it from the buffer + inbuf = inbuf[:-1] + else: + clear_line() + pos = inbuf_history[1] + inbuf = inbuf_history[0][pos] + inbuf_history[1] -= 1 + redraw_cli() + elif char == 'B': # down + if not inbuf_history[0]: + # it's empty, so we only + # remove it from the buffer + inbuf = inbuf[:-1] + elif inbuf_history[1] == len(inbuf_history[0]) - 1: + # hit the bottom, clear the buffer + clear_line() + inbuf = '' + redraw_cli() + else: + inbuf_history[1] += 1 + clear_line() + pos = inbuf_history[1] + inbuf = inbuf_history[0][pos] + redraw_cli() + else: # unhandled esc + inbuf = inbuf[:-1] + in_esc = 0 + +def redraw_cli(): + """Redraws the current prompt line, including user input; it first + clears the line, either automatically or up to 'lenght' chars.""" + global inbuf, screenwidth + clear_line() + print_prompt() + lenght = screenwidth - 7 # we subsctract the prompt lenght + 1 + safe_write(inbuf[-lenght:]) + safe_flush() + +def redraw_cli_cond(char): + """Same as redraw_cli, but conditional over the lenght of stdin. That + means that if inbuf is getting too big, we redraw; otherwise we just + write the character. It's used mostly to avoid innecesary redraw + overhead (it avoids 90% of cases).""" + global inbuf, screenwidth + if len(inbuf) >= (screenwidth - 7): + redraw_cli() + else: + safe_write(char) + safe_flush() + +def clear_line(): + """Clears the current line by overwriting it with spaces.""" + global inbuf, screenwidth + if use_termios: + safe_write('\r' + (screenwidth - 1) * ' ' + '\r') + + +# +# stdin command parser +# + +def parse_cmd(cmd): + """Parses the commands introduced by the user. It's pretty long and + boring, as expected.""" + + global c, last_sent, last_received # ugly but necesary + + if len(cmd) == 0: + quit() + elif len(cmd) == 1: + return '' + + # cut trailing newline and clean up + if cmd[-1] == '\n': + cmd = cmd[:-1] + cmd = cmd.lstrip() + orig_cmd = cmd + s = cmd.split() + if len(s) > 1: + cmd = s[0] + # recover original params to preserve whitespace + # use as index the first parameter to the command + params = orig_cmd[orig_cmd.find(s[1]):] + else: + if not cmd: return '' + cmd = s[0] + params = '' + + + # parse + if cmd == 'status': # change status + if not params: + return 'Your current status is %s' % msnlib.reverse_status[m.status] + if not m.change_status(params): + out = 'Status must be one of:\n' + out += '\tonline, away, busy, brb, phone, lunch, invisible or idle' + return out + return 'Status changed to: %s' % params + + elif cmd == 'q': # quit + quit() + + elif cmd == 'reload': # reload callbacks + reload(msncb) + m.cb = msncb.cb() + + elif cmd == 'w': # list + print_grouped_list(m) + + elif cmd == 'ww': # list, include emails + print_grouped_list(m, include_emails = 1) + + elif cmd == 'wr': # reverse list + print_list(m, userlist = m.reverse, include_emails = 1) + + elif cmd == 'e': # list (online only) + print_list(m, only_online = 1) + + elif cmd == 'eg': + print_grouped_list(m, only_online = 1) + + elif cmd == 'ee': + print_grouped_list(m, only_online = 1, include_emails = 1) + + elif cmd == 'g': # list groups + print_group_list(m) + + elif cmd == 'raw': # send a raw message + try: + cmd = params[0:3] + pars = params[4:] + except: + return 'Error parsing command' + m._send(cmd, pars) + + elif cmd == 'debug': # enable/disable debugging + p = params.split() + if len(p) != 1: + return 'Error parsing command' + if p[0] == 'off': + msnlib.debug = null + msncb.debug = null + return 'Debugging disabled' + elif p[0] == 'on': + reload(msnlib) + reload(msncb) + return 'Debugging enabled' + else: + return 'Unknown parameter - must be "on" or "off"' + + elif cmd == 'config': # show config variables + keys = config.keys() + keys.sort() + for var in keys: + value = str(config[var]) + if var == 'password': + value = '<not displayed>' + printl(c.bold + var + ' = ' + c.normal + value + '\n') + printl(c.bold + 'use_termios = ' + str(use_termios) + '\n') + printl(c.bold + 'screensize = ' + str(winsize) + '\n') + + elif cmd == 'color': # configure/show colors + p = params.split() + if len(p) != 1: + printl(c.bold + "Currently using theme " + c.name + '\n') + printl(c.bold + "Available themes:\n") + for i in color_classes.keys(): + printl(c.bold + "\t* " + i + '\n') + elif p[0] not in color_classes.keys(): + return "The specified theme is not available" + else: + c = color_classes[p[0]]() + return "Changed theme to " + p[0] + + elif cmd == 'close': # close a connection + p = params.split() + if len(p) != 1: + return 'Error parsing command' + email = nick2email(p[0]) + if not email: + return 'Unknown nick (%s)' % p[0] + if not m.users[email].sbd: + return 'No socket opened for %s' % p[0] + desc = str(m.users[email].sbd) + m.close(m.users[email].sbd) + return 'Closed socket %s' % desc + + elif cmd == 'privacy': # set privacy mode + p = params.split() + if len(p) != 2: + return 'Error parsing command' + try: + public = int(p[0]) + auth = int(p[1]) + if public not in (0, 1) or auth not in (0, 1): + return 'Error: both parameters must be 1 or 0' + except: + return 'Error: both parameters must be 1 or 0' + m.privacy(public, auth) + + elif cmd == 'lignore': # ignore a user locally + p = params.split() + if len(p) == 0: + printl(c.bold + 'Locally ignored users\n') + for e in ignored: + printl(email2nick(e) + ' (' + e + ')\n') + return '' + email = nick2email(p[0]) + if not email: + return 'Unknown nick (%s)' % p[0] + if email in ignored: + return 'User is already being locally ignored' + ignored.append(email) + return 'User is now being locally ignored' + + elif cmd == 'lunignore': # unignore a locally ignored user + p = params.split() + if len(p) == 0: + return 'Error parsing command' + email = nick2email(p[0]) + if email not in ignored: + return 'User is not being locally ignored' + ignored.remove(email) + return 'User is no longer locally ignored' + + elif cmd == 'block': + p = params.split() + if len(p) == 0: + return 'Error parsing command' + email = nick2email(p[0]) + if not email: + return 'Unknown nick (%s)' % p[0] + m.userblock(email) + return 'User %s blocked' % email + + elif cmd == 'unblock': + p = params.split() + if len(p) == 0: + return 'Error parsing command' + email = nick2email(p[0]) + if not email: + return 'Unknown nick (%s)' % p[0] + m.userunblock(email) + return 'User %s unblocked' % email + + elif cmd == 'add': # add a user + p = params.split() + if len(p) == 0: + return 'Error parsing command' + elif len(p) == 1: + email = nick = p[0] + gid = '0' + elif len(p) == 2: + email = p[0] + nick = p[1] + gid = '0' + else: + email = p[0] + nick = p[1] + group = p[2] + gid = gname2gid(group) + if not gid: gid = group + if gid not in m.groups.keys(): + return 'Unknown group' + m.useradd(email, nick, gid) + + elif cmd == 'del': # delete a user + p = params.split() + if len(p) != 1: return 'Error parsing command' + email = nick2email(p[0]) + if not email: + return 'Unknown nick (%s)' % p[0] + m.userdel(email) + + elif cmd == 'ren': # rename a user + p = params.split() + if len(p) != 2: return 'Error parsing command' + email = nick2email(p[0]) + if not email: + return 'Unkown nick (%s)' % p[0] + newnick = p[1] + u = m.users[email] + m.userdel(email) + m.useradd(email, newnick, u.gid) + + elif cmd == 'gadd': # add a group + p = params.split() + if len(p) != 1: return 'Error parsing command' + m.groupadd(p[0]) + + elif cmd == 'gdel': # delete a group + p = params.split() + if len(p) != 1: return 'Error parsing command' + gname = p[0] + gid = gname2gid(gname) + if not gid: gid = gname + if gid not in m.groups.keys(): + return 'Unknown group' + for e in m.users.keys(): + u = m.users[e] + if u.gid == gid: + printl('User %s (%s) will be deleted\n' % \ + (u.nick, e), bold = 1) + m.groupdel(gid) + + elif cmd == 'gren': # rename a group + p = params.split() + if len(p) != 2: return 'Error parsing command' + newname = p[1] + origname = p[0] + gid = gname2gid(origname) + if not gid: gid = origname + if gid not in m.groups.keys(): + return 'Unknown group' + m.groupren(gid, newname) + + elif cmd == 'invite': # invite a user to an existing sbd + p = params.split() + if len(p) != 3: return 'Error parsing command' + if p[1] != 'to': return 'Error parsing command' + email = nick2email(p[0]) + if not email: email = p[0] + dst = nick2email(p[2]) + if not dst: dst = p[2] + for i in (email, dst): + if i not in m.users.keys(): + return 'User %s unknown' % i + dst_sbd = m.users[dst].sbd + if not dst_sbd: + return 'No current chat with user %s' % dst + m.invite(email, dst_sbd) + + elif cmd == 'nick': # change our nick + if len(params) < 1: return 'Error parsing command' + nick = params + m.change_nick(nick) + + elif cmd == 'info': # user info + p = params.split() + if len(p) != 1: + out = '' + out += c.bold + 'Info for ' + m.email + '\n' + out += c.bold + 'Nick:\t\t' + c.normal + m.nick + '\n' + out += c.bold + 'Status:\t\t' \ + + c.normal + msnlib.reverse_status[m.status] + '\n' + out += c.bold + 'Home phone:\t' + c.normal + str(m.homep) + '\n' + out += c.bold + 'Work phone:\t' + c.normal + str(m.workp) + '\n' + out += c.bold + 'Mobile phone:\t' + c.normal + str(m.mobilep) + '\n' + out += c.bold + 'Users in contact list: ' + str(len(m.users)) + '\n' + out += c.bold + 'Users in reverse list: ' + str(len(m.reverse)) + '\n' + out += c.bold + 'Notification server: ' + c.normal + str(m) + '\n' + if m.sb_fds: + out += c.bold + 'Switchboard connections:\n' + for i in m.sb_fds: + out += c.bold + '\tSB: ' + c.normal + str(i) + '\n' + printl(out) + else: + email = nick2email(p[0]) + if not email: + return 'Unknown nick (%s)' % str(p[0]) + print_user_info(email) + + elif cmd == 'sync': # manual sync + m.sync() + + elif cmd == 'h': # show history + printl('Incoming Message History (last %d messages)\n' \ + % config['history size'], c.green, 1) + for i in history_ring: + rtime = i[0] + email = i[1] + msg = i[2] + print_inc_msg(email, msg, quiet = 1, ptime = 1, recvtime = rtime) + + # send a message + elif cmd == 'm' or cmd == 'msg' or cmd == 'r' or cmd == 'a': + if cmd == 'm' or cmd == 'msg': + p = params.split() + if len(p) < 1: + return 'Please enter a nick and a message' + nick = p[0] + email = nick2email(nick) + # begin the message content after the nick + begin = len(nick + ' ') + msg = params[begin:] + elif cmd == 'r': + email = last_received + nick = email2nick(email) + if not nick: nick = email + msg = params + elif cmd == 'a': + email = last_sent + nick = email2nick(email) + if not nick: nick = email + msg = params + if not email: + if cmd == 'a': return 'Please write a message first' + if cmd == 'r': return 'Please reply a message first' + else: return 'Unknown nick %s' % str(p[0]) + if m.users[email].status == 'FLN' and not m.users[email].sbd: + return 'Unable to send message: User is offline' + if (m.status == 'FLN' or m.status == 'HDN') and not m.users[email].sbd: + return 'Unable to send message: Not allowed when offline' + + r = m.sendmsg(email, msg) + last_sent = email + if r == 1: + return 'Message for %s queued for delivery' % nick + elif r == 2: + print_out_msg(nick, msg) + if len(m.users[email].sbd.emails) > 1: + log_msg(m.email, 'out', msg, \ + users = m.users[email].sbd.emails) + else: + log_msg(email, 'out', msg) + elif r == -2: + return 'Message too big' + else: + return 'Error %d sending message' % r + + elif cmd == 'help' or cmd == '?': + return help + else: + return 'Unknown command, type "help" for help' + + return '' + + + +# +# This are the callback replacements, which only handle the output and then +# call the original callbacks to do the lower level stuff +# + +# basic classes +m = msnlib.msnd() +m.cb = msncb.cb() + +# status change +def cb_iln(md, type, tid, params): + t = params.split(' ') + status = msnlib.reverse_status[t[0]] + email = t[1] + nick = md.users[email].nick + ctime = time.strftime('%I:%M:%S%p', now()) + printl('\r%s ' % ctime, c.blue) + printl(nick, c.blue, 1) + printl(' changed status to ', c.magenta) + printl('%s\n' % status, c.magenta, 1) + log_msg(email, 'status', status) + msncb.cb_iln(md, type, tid, params) +m.cb.iln = cb_iln + +def cb_nln(md, type, tid, params): + status = msnlib.reverse_status[tid] + t = params.split(' ') + email = t[0] + nick = md.users[email].nick + ctime = time.strftime('%I:%M:%S%p', now()) + printl('\r%s ' % ctime, c.blue) + printl(nick, c.blue, 1) + printl(' changed status to ', c.magenta) + printl('%s\n' % status, c.magenta, 1) + log_msg(email, 'status', status) + msncb.cb_nln(md, type, tid, params) +m.cb.nln = cb_nln + +def cb_fln(md, type, tid, params): + email = tid + nick = md.users[email].nick + ctime = time.strftime('%I:%M:%S%p', now()) + printl('\r%s ' % ctime, c.blue) + printl(nick, c.blue, 1) + printl(' disconnected\n', c.magenta) + u = m.users[email] + if u.sbd and u.sbd.msgqueue: + printl(c.bold + "The following messages for " + nick + " will be discarded:\n") + for msg in u.sbd.msgqueue: + printl(c.bold + '\t>>> ' + c.normal + msg + '\n') + log_msg(email, 'status', 'disconnect') + msncb.cb_fln(md, type, tid, params) +m.cb.fln = cb_fln + +# server disconnect +def cb_out(md, type, tid, params): + printl('\rServer sent disconnect (probably you logged in somewhere else)\n', c.green, 1) + msncb.cb_out(md, type, tid, params) +m.cb.out = cb_out + +def cb_bye(md, type, tid, params, sbd): + email = tid + if email != sbd.emails[0]: + nick = email2nick(email) + if not nick: nick = email + first_nick = email2nick(sbd.emails[0]) + if not first_nick: first_nick = sbd.emails[0] + printl('\rUser %s left the chat with %s\n' % (nick, first_nick), c.green, 1) + log_msg(email, 'multi', 'left', users = sbd.emails) + msncb.cb_bye(md, type, tid, params, sbd) +m.cb.bye = cb_bye + + +# message +def cb_msg(md, type, tid, params, sbd): + global last_received + t = tid.split(' ') + email = t[0] + + # parse + lines = params.split('\n') + headers = {} + eoh = 0 + for i in lines: + # end of headers + if i == '\r': + break + tv = i.split(':', 1) + type = tv[0] + value = tv[1].strip() + headers[type] = value + eoh += 1 + eoh +=1 + + # handle special hotmail messages + if email == 'Hotmail': + if not headers.has_key('Content-Type'): + return + hotmail_info = {} + + # parse the body + for i in lines: + i = i.strip() + if not i: + continue + tv = i.split(':', 1) + type = tv[0] + value = tv[1].strip() + hotmail_info[type] = value + + msnlib.debug(params) + if headers['Content-Type'] == 'text/x-msmsgsinitialemailnotification; charset=UTF-8': + newmsgs = int(hotmail_info['Inbox-Unread']) + if not newmsgs: + return + printl('\rYou have %s unread email(s)' % str(newmsgs) \ + + ' in your Hotmail account\n', c.green, 1) + elif headers['Content-Type'] == 'text/x-msmsgsemailnotification; charset=UTF-8': + from_name = hotmail_info['From'] + from_addr = hotmail_info['From-Addr'] + subject = hotmail_info['Subject'] + printl('\rYou have just received an email in your' + \ + ' Hotmail account:\n', c.green, 1) + printl('\r\tFrom: %s (%s)\n' % (from_name, from_addr), + c.green, 1) + printl('\r\tSubject: %s\n' % subject, c.green, 1) + return + + if headers.has_key('Content-Type') and headers['Content-Type'] == 'text/x-msmsgscontrol': + # the typing notices + nick = email2nick(email) + if not nick: nick = email + if not m.users[email].priv.has_key('typing'): + m.users[email].priv['typing'] = 0 + if not m.users[email].priv['typing'] and email not in ignored: + printl('\r') + ctime = time.strftime('%I:%M:%S%p', now()) + printl('%s ' % ctime, c.blue) + printl('%s' % nick, c.cyan, 1) + printl(' is typing\n', c.magenta) + m.users[email].priv['typing'] = time.time() + elif headers.has_key('Content-Type') and headers['Content-Type'] == 'text/x-clientcaps': + # ignore the x-clientcaps messages generated from gaim + pass + else: + # messages + m.users[email].priv['typing'] = 0 + printl('\r') + print_inc_msg(email, lines, eoh) + if len(sbd.emails) > 1: + log_msg(email, 'in', string.join(lines[eoh:], '\n'), \ + users = sbd.emails) + else: + log_msg(email, 'in', string.join(lines[eoh:], '\n')) + + # append the message to the history, keeping it below the configured limit + if len(history_ring) > config['history size']: + del(history_ring[0]) + history_ring.append((time.time(), email, lines[eoh:])) + + last_received = email + msncb.cb_msg(md, type, tid, params, sbd) +m.cb.msg = cb_msg + + +# join a conversation and send pending messages +def cb_joi(md, type, tid, params, sbd): + email = tid + nick = email2nick(email) + if not nick: nick = email + if sbd.emails and email != sbd.emails[0]: + first_nick = email2nick(sbd.emails[0]) + if not first_nick: first_nick = sbd.emails[0] + printl('\rUser %s has joined the chat with %s\n' % \ + (nick, first_nick), c.green, 1) + log_msg(email, 'multi', 'join', \ + users = sbd.emails + [email]) + elif len(sbd.msgqueue) > 0: + printl('\rFlushing messages for %s:\n' % nick, c.green, 1) + for msg in sbd.msgqueue: + print_out_msg(nick, msg) + printl('\n') + log_msg(email, 'out', msg) + msncb.cb_joi(md, type, tid, params, sbd) +m.cb.joi = cb_joi + +def cb_iro(md, type, tid, params, sbd): + p = params.split(' ') + uid, ucount, email, realnick = p + nick = email2nick(email) + if not nick: nick = email + + if ucount == '1': + # do nothing if we only have one participant + pass + else: + first_nick = email2nick(sbd.emails[0]) + if not first_nick: first_nick = sbd.emails[0] + # print a special message for the first user + if uid == '1': + printl('\rUser %s has invited us to a multi-user chat\n' % \ + first_nick, c.green, 1) + else: + printl('\rUser %s has joined the chat with %s\n' % \ + (nick, first_nick), c.green, 1) + log_msg(email, 'multi', 'join', \ + users = sbd.emails + [email]) + msncb.cb_iro(md, type, tid, params, sbd) +m.cb.iro = cb_iro + +# server errors +def cb_err(md, errno, params): + if not msncb.error_table.has_key(errno): + desc = 'Unknown' + else: + desc = msncb.error_table[errno] + desc = '\rServer sent error %d: %s\n' % (errno, desc) + perror(desc) + msncb.cb_err(md, errno, params) +m.cb.err = cb_err + +# users add, delete and modify +def cb_add(md, type, tid, params): + t = params.split(' ') + type = t[0] + if type == 'RL' or type == 'FL': + email = t[2] + nick = urllib.unquote(t[3]) + if type == 'RL': + out = '\r' + c.blue + c.bold + ('%s (%s) ' % (email, nick)) \ + + c.magenta + 'has added you to his contact list\n' + printl(out) + beep() + elif type == 'FL': + out = '\r' + c.blue + c.bold + ('%s (%s) ' % (email, nick)) \ + + c.magenta + 'has been added to your contact list\n' + printl(out) + msncb.cb_add(md, type, tid, params) +m.cb.add = cb_add + +def cb_rem(md, type, tid, params): + t = params.split(' ') + type = t[0] + if type == 'RL' or type == 'FL': + email = t[2] + if type == 'RL': + out = '\r' + c.blue + c.bold + email + ' ' + c.magenta \ + + 'has removed you from his contact list\n' + printl(out) + beep() + elif type == 'FL': + out = '\r' + c.blue + c.bold + email + ' ' + c.magenta \ + + 'has been removed from your contact list\n' + printl(out) + msncb.cb_rem(md, type, tid, params) +m.cb.rem = cb_rem + +def cb_adg(md, type, tid, params): + t = params.split(' ') + lver, name, gid = t[0:3] + name = urllib.unquote(name) + out = '\r' + c.magenta + 'Group ' + out += c.blue + c.bold + '%s (%s)' % (name, gid) + c.clear + out += c.magenta + ' has been added\n' + printl(out) + msncb.cb_adg(md, type, tid, params) +m.cb.adg = cb_adg + +def cb_rmg(md, type, tid, params): + t = params.split(' ') + lver, gid = t[0:2] + name = md.groups[gid] + out = '\r' + c.magenta + 'Group ' + out += c.blue + c.bold + '%s (%s)' % (name, gid) + c.clear + out += c.magenta + ' has been removed\n' + printl(out) + msncb.cb_rmg(md, type, tid, params) +m.cb.rmg = cb_rmg + +def cb_reg(md, type, tid, params): + t = params.split(' ') + gid = t[1] + origname = md.groups[gid] + origname = urllib.unquote(origname) + newname = t[2] + newname = urllib.unquote(newname) + out = '\r' + c.magenta + 'Group ' + out += c.blue + c.bold + '%s (%s)' % (origname, gid) + c.clear + out += c.magenta + ' has been renamed to ' + out += c.blue + c.bold + '%s' % newname + '\n' + printl(out) + msncb.cb_reg(md, type, tid, params) +m.cb.reg = cb_reg + + + +# +# now the real thing +# +printl('* MSN Client (3.4) *\n', c.yellow, 1) + +# first, the configuration +printl('Loading config... ', c.green, 1) +if len(sys.argv) > 1: + # first, try the arg as file + config = get_config(sys.argv[1]) + profile = None + if not config: + # then, as the profile + profile = sys.argv[1] + file = os.environ['HOME'] + '/.msn/msnrc-' + profile + config = get_config(file) +else: + profile = None + config = get_config(os.environ['HOME'] + '/.msn/msnrc') + +if not config: + perror('Error opening config file (%s), try running "msnsetup"\n' % file) + quit(1) + +config['profile'] = profile + +# set the mandatory values +if config.has_key('email'): + m.email = config['email'] +else: + perror('Error: email not specified in config file\n') + quit(1) + +if config.has_key('password'): + m.pwd = config['password'] +else: + # we ask for the password, setting, if necesary, blocking IO over + # stdin (which was disabled by the terminal handling stuff) + import getpass + try: fcntl.fcntl(stdinfd, fcntl.F_SETFL, os.O_SYNC) + except: pass + m.pwd = getpass.getpass(c.green + c.bold + "\nPassword: ") + try: fcntl.fcntl(stdinfd, fcntl.F_SETFL, os.O_NONBLOCK) + except: pass + +# and the optional ones, setting the defaults if not present +# history size +if not config.has_key('history size'): + config['history size'] = 10 +else: + try: + config['history size'] = int(config['history size']) + except: + perror('history size must be integer, using default\n') + config['history size'] = 10 + +# input history size +if not config.has_key('input history size'): + config['input history size'] = 10 +else: + try: + config['history size'] = int(config['history size']) + except: + error('input history size must be integer, using default\n') + config['input history size'] = 10 + +# initial status +if not config.has_key('initial status'): + config['initial status'] = 'online' +elif config['initial status'] not in msnlib.status_table.keys(): + perror('unknown initial status, using default\n') + config['initial status'] = 'online' + +# debug +if not config.has_key('debug'): + config['debug'] = 0 +elif config['debug'] != 'yes': + config['debug'] = 0 + +# colors +if not config.has_key('color theme'): + config['color theme'] = 'default' +try: + c = color_classes[config['color theme']]() +except: + perror("Unknown color theme, type 'color' for help\n") + +# log history +if not config.has_key('log history'): + config['log history'] = 1 +elif config['log history'] != 'yes': + config['log history'] = 0 + +# history directory +if not config.has_key('history directory'): + config['history directory'] = os.environ['HOME'] + '/.msn/history' + +# auto away time +if not config.has_key('auto away'): + config['auto away'] = 0 +else: + try: + config['auto away'] = int(config['auto away']) + except: + perror('auto away must be integer, using default\n') + config['auto away'] = 0 +if config['auto away'] and config['auto away'] < 60: # sanity check + perror('Warning: auto away time was set to less than a minute!\n') + +# encoding +if not config.has_key('encoding'): + # we use posix standard way of defining standard locale, or just fall + # back to iso-8859-1; see locale(7) for more details + if os.environ.has_key('LC_ALL') and os.environ['LC_ALL']: + config['encoding'] = os.environ['LC_ALL'] + elif os.environ.has_key('LANG') and os.environ['LANG']: + config['encoding'] = os.environ['LANG'] + else: + config['encoding'] = 'iso-8859-1' +m.encoding = config['encoding'] + +printl('done\n', c.green, 1) + + +# set or void the debug +if not config['debug']: + msnlib.debug = null + msncb.debug = null + +# debug some internal variables +msnlib.debug("Terminal Handling: %d" % use_termios) +msnlib.debug("Terminal Size: %s" % str(winsize)) + +# login to msn +printl('Logging in... ', c.green, 1) +try: + m.login() + printl('done\n', c.green, 1) +except 'AuthError', info: + errno = int(info[0]) + if not msncb.error_table.has_key(errno): + desc = 'Unknown' + else: + desc = msncb.error_table[errno] + perror('Error: %s (%s)\n' % (desc, errno)) + quit(1) +except KeyboardInterrupt: + quit() +except ('SocketError', socket.error), info: + perror('Network error: ' + str(info) + '\n') + quit(1) +except: + pexc('Exception logging in\n') + quit(1) + + +# call sync to get the lists and refresh +printl('Sending user list request... ', c.green, 1) +if m.sync(): + printl('done\n', c.green, 1) + list_complete = 0 +else: + perror('Error syncing users\n') + + +# global variables +history_ring = [] # history buffer +last_sent = '' # email of the last person we sent a message to +last_received = '' # email of the last person we received a message from +ignored = [] # people being locally ignored + +# auto-away +timeout = config['auto away'] +if not timeout: + timeout = None # must be None, not 0 because of select() semantics +auto_away = 0 + +# loop +redraw_cli() +while 1: + fds = m.pollable() + infd = fds[0] + outfd = fds[1] + infd.append(sys.stdin) + try: + fds = select.select(infd, outfd, [], timeout) + except KeyboardInterrupt: + quit() + + if timeout and len(fds[0] + fds[1]) == 0: + # timeout, set auto away + if m.status == 'NLN': + m.change_status('away') + auto_away = 1 + printl('\rAutomatically changing status to away\n', c.green, 1) + + for i in fds[0] + fds[1]: # see msnlib.msnd.pollable.__doc__ + if i == sys.stdin: + # auto away revival + if auto_away: + auto_away = 0 + m.change_status('online') + printl('\rAutomatically changing status back to online\n', c.green, 1) + # read from stdin + stdin_read() + else: + try: + m.read(i) + + # see if we got all the user list, so we can + # change our initial status (doing it earlier + # as we used to seems to break things for some + # people) + if not list_complete and \ + m.lst_total == m.syn_total: + list_complete = 1 + if m.change_status(config['initial status']): + printl('\rStatus set to %s\n' % config['initial status'], c.green, 1) + else: + perror('\rError setting status: unknown status %s\n' % config['initial status']) + + + + except ('SocketError', socket.error), err: + if i != m: + if i.msgqueue: + nick = email2nick(i.emails[0]) + printl("\rConnection with %s closed - the following messages couldn't be sent:\n" % (nick), c.green, 1) + for msg in i.msgqueue: + printl(c.bold + '\t>>> ' + c.normal + msg + '\n') + m.close(i) + else: + printl('\nMain socket closed (%s)\n' % str(err), c.red) + quit(1) + except 'XFRError', err: + printl("\rXFR Error: %s\n" % str(err)) + + # always redraw after network event + redraw_cli() + + diff --git a/msncb.py b/msncb.py new file mode 100644 index 0000000..a05047a --- /dev/null +++ b/msncb.py @@ -0,0 +1,507 @@ + + +import string +import urllib +import md5 + +import socket + +import msnlib + +""" +This is the home for the msn callback class and examples (that might move to +another file in the near future). + +There are three types of callbacks: the error one (this is only one), the +server ones (handle connections, notifications, lists and stuff like that), +and the switchboard ones (which handle messaging). + +All of them receive as their first argument an 'md' (msn descriptor) that is +the main connection object; you probably already know what it is. + +The models are: +error: def cb_err(md, errno, params) +server: def cb_def(md, type, tid, params) +switchboard: def cb_usr(md, type, tid, params, sbd) + +See below for more examples. + +Probably you should base your own callbacks on these ones, at least they were +thought with that in mind, so you can use yours as wrappers that handle only +your app-specific code and forget about the protocol-specific mess. + + Alberto (albertogli@telpin.com.ar) +""" + + +# use the debug function from msnlib +debug = msnlib.debug + + +class cb: + def __init__(self): + self.unk = cb_unk # unknown + self.err = cb_err # server error + self.msg = cb_msg # get a message + self.chl = cb_chl # challenge + self.qry = cb_ign # query response + self.iln = cb_iln # status notification + self.chg = cb_ign # status change + self.nln = cb_nln # status notification + self.fln = cb_fln # status offline + self.out = cb_out # disconnect + self.blp = cb_ign # privacy mode change + self.lst = cb_lst # list requests + self.bpr = cb_bpr # user info + self.gtc = cb_ign # add notification + self.syn = cb_syn # list sync confirmation + self.prp = cb_prp # private info + self.lsg = cb_lsg # group list + self.add = cb_add # user add + self.rem = cb_rem # user remove + self.adg = cb_adg # group add + self.rmg = cb_rmg # group del + self.reg = cb_reg # group rename + self.rea = cb_rea # nick change + self.rng = cb_rng # switchboard invitation + self.iro = cb_iro # multi-user chat + self.ans = cb_ans # answer confirmation + self.xfr = cb_xfr # switchboard request + self.usr = cb_usr # sb request initial identification + self.cal = cb_ign # call confirmation + self.joi = cb_joi # session join + self.ack = cb_ack # message acknowledge + self.nak = cb_nak # message negative acknowledge + self.bye = cb_bye # switchboard user disconnect + + + +error_table = { + -10: 'Local error', + 200: 'Syntax error', + 201: 'Invalid parameter', + 205: 'Invalid user', + 206: 'Domain name missing', + 207: 'Already logged in', + 208: 'Invalid username', + 209: 'Invalid fusername', + 210: 'User list full', + 215: 'User already there', + 216: 'User already on list', + 217: 'User not online', + 218: 'Already in mode', + 219: 'User is in the opposite list', + 280: 'Switchboard failed', + 281: 'Transfer to switchboard failed', + 300: 'Required field missing', + 302: 'Not logged in', + 500: 'Internal server error', + 501: 'Database server error', + 510: 'File operation failed', + 520: 'Memory allocation failed', + 600: 'Server is busy', + 601: 'Server is unavaliable', + 602: 'Peer nameserver is down', + 603: 'Database connection failed', + 604: 'Server is going down', + 707: 'Could not create connection', + 711: 'Write is blocking', + 712: 'Session is overloaded', + 713: 'Too many active users', + 714: 'Too many sessions', + 715: 'Not expected', + 717: 'Bad friend file', + 911: 'Authentication failed', + 913: 'Not allowed when offline', + 920: 'Not accepting new users', +} + +def cb_err(md, errno, params): + "Handle server errors" + if not error_table.has_key(errno): + desc = 'Unknown error %d' % errno + else: + desc = error_table[errno] + + debug('SERVER ERROR %d: %s - %s' % (errno, desc, params)) + + +def cb_def(md, type, tid, params): + "Default callback. It just prints the args" + debug('DEFAULT type: ' + type + ' :: Params: ' + str(params)) + + +def cb_unk(md, type, tid, params): + "Handles the unknown types" + debug('Error! unknown event type "%s"' % type) + debug('params: ' + str(params)) + + +def cb_chl(md, type, tid, params): + "Handles the challenges" + if type != 'CHL': raise 'CallbackMess', (md, type, params) + hash = params + 'VT6PX?UQTM4WM%YR' # magic from www.hypothetic.org + hash = md5.md5(hash).hexdigest() + md._send('QRY', 'PROD0038W!61ZTF9 32') + md.fd.send(hash) + + +def cb_ign(md, type, tid, params, nd = None): + "Ignores" + pass + + +def cb_out(md, type, tid, params): + "Server disconnected us" + debug('!!! Server closed the connection: ' + params) + + +def cb_iln(md, type, tid, params): + "Handles a friend status change" + t = params.split(' ') + status = t[0] + email = t[1] + if len(params) > 2: nick = urllib.unquote(t[2]) + else: nick = '' + + md.users[email].status = status + md.users[email].realnick = nick + debug('FRIEND %s (%s) changed status to :%s:' % (nick, email, status)) + + +def cb_fln(md, type, tid, params): + "Handles a friend disconnection" + email = tid + debug('FRIEND %s disconnected (%s)' % (email, type)) + md.users[email].status = type + + +def cb_nln(md, type, tid, params): + "Handles a friend status change" + status = tid + t = params.split(' ') + email = t[0] + if len(t) > 1: nick = urllib.unquote(t[1]) + else: nick = '' + + md.users[email].status = status + md.users[email].realnick = nick + debug('FRIEND %s (%s) changed status to :%s:' % (nick, email, status)) + + +def cb_bpr(md, type, tid, params): + "Update friend info" + # the email is deduced from the last lst we got; if it's None it means + # that we come from an add (the protocol behaves different if coming + # from SYN or ADD) + email = md._last_lst + if email: + # we come from SYN + type = tid + param = urllib.unquote(params) + else: + # we come from ADD + t = params.split(' ') + email = t[0] + type = t[1] + if len(t) >= 3: + param = urllib.unquote(t[2]) + else: + param = '' + + if not md.users.has_key(email): return + + if type == 'PHH': md.users[email].homep = param + elif type == 'PHW': md.users[email].workp = param + elif type == 'PHM': md.users[email].mobilep = param + else: pass + + +def cb_syn(md, type, tid, params): + "Receive a SYN notification" + t = params.split() + if len(t) != 3: + raise "SYNError" + + lver = int(t[0]) + total = int(t[1]) + ngroups = int(t[2]) + + md.syn_lver = lver + md.syn_total = total + md.syn_ngroups = ngroups + + +def cb_lst(md, type, tid, params): + p = params.split(' ') + email = tid + nick = urllib.unquote(p[0]) + listmask = int(p[1]) + if len(p) == 3: + groups = p[2] + else: + groups = '0' + + # we only use one main group id + gid = groups.split(',')[0] + + if email in md.users.keys(): + user = md.users[email] + else: + user = msnlib.user(email, nick, gid) + + # the list mask is a bitmask, composed of: + # FL: 1 + # AL: 2 + # BL: 4 + # RL: 8 + + # in forward + if listmask & 1: + user.lists.append('F') + md.users[email] = user + + # in reverse + if listmask & 8: + user.lists.append('R') + md.reverse[email] = user + + # in allow + if listmask & 2: + user.lists.append('A') + + # in block + if listmask & 4: + user.lists.append('B') + + md.lst_total += 1 + + # save in the global last_lst the email, because BPRs might need it + md._last_lst = email + +def cb_lsg(md, type, tid, params): + "Handles group list" + p = params.split(' ') + gid = tid + name, unk = p[0:] + # if we get the group 0, start from scratch + if gid == '0': + md.groups = {} + name = urllib.unquote(name) + md.groups[gid] = name + + +def cb_prp(md, type, tid, params): + "Handles private info" + t = params.split(' ') + type = t[0] + if len(t) > 1: param = urllib.unquote(t[1]) + else: param = '' + + if type == 'PHH': md.homep = param + elif type == 'PHW': md.workp = param + elif type == 'PHM': md.mobilep = param + else: pass + + +def cb_add(md, type, tid, params): + "Handles a user add; both you adding a user and a user adding you" + t = params.split(' ') + type = t[0] + if type == 'RL': + email = t[2] + nick = urllib.unquote(t[3]) + debug('ADD: %s (%s) added you' % (nick, email)) + elif type == 'FL': + email = t[2] + nick = urllib.unquote(t[3]) + gid = t[4] + md.users[email] = msnlib.user(email, nick, gid) + # put None in last_lst so BPRs know it's not coming from sync + md._last_lst = None + debug('ADD: adding %s (%s)' % (email, nick)) + else: + pass + +def cb_rem(md, type, tid, params): + """Handles a user del. + Only make something in the case of a user removing you""" + t = params.split(' ') + type = t[0] + if type == 'RL': + email = t[2] + debug('REM: %s removed you' % email) + elif type == 'FL': + email = t[2] + if md.users[email].sbd: + md.close(md.users[email].sbd) + del(md.users[email]) + debug('REM: removing %s' % email) + else: + pass + +def cb_adg(md, type, tid, params): + "Handle a group add" + t = params.split(' ') + lver, name, gid = t[0:3] + md.groups[gid] = name + debug('ADG: group %s (%s) added' % (name, gid)) + +def cb_rmg(md, type, tid, params): + "Handle a group del" + t = params.split(' ') + lver, gid = t[0:2] + for e in md.users.keys(): + if md.users[e].gid == gid: + if md.users[e].sbd: + md.close(md.users[e].sbd) + del(md.users[e]) + del(md.groups[gid]) + debug('RMG: group %s removed' % gid) + +def cb_reg(md, type, tid, params): + "Handle a group rename" + t = params.split(' ') + gid = t[1] + name = t[2] + md.groups[gid] = name + debug('REG: group %s renamed to %s' % (name, gid)) + +def cb_rea(md, type, tid, params): + "Handles our info change" + t = params.split(' ') + email = t[1] + nick = urllib.unquote(t[2]) + if email != md.email: + md.users[email].nick = nick + else: + md.nick = nick + debug('NICK CHANGE: email %s - nick %s' % (email, nick)) + + +def cb_rng(md, type, tid, params): + "Handles switchboard invitations." + t = params.split(' ') + sid = tid + ip, port = t[0].split(':') + port = int(port) + hash = t[2] + email = t[3] + + fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # we set the socket nonblocking so we don't block (duh!) on connect(); + # it will be picked up later from the select loop and handled via the + # main read() call, which you will have to see to find out the rest. + fd.setblocking(0) + fd.connect_ex((ip, port)) + + sbd = msnlib.sbd() + sbd.fd = fd + sbd.block = 0 + sbd.state = 'cp' + sbd.type = 'answer' + sbd.endpoint = (ip, port) + sbd.emails.append(email) + sbd.hash = hash + sbd.session_id = sid + md.submit_sbd(sbd) # it has the connect pending + + +def cb_xfr(md, type, tid, params): + "Handles switchboard requests" + t = params.split(' ') + ip, port = t[1].split(':') + port = int(port) + hash = t[3] + + fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + fd.setblocking(0) # see cb_rng + fd.connect_ex((ip, port)) + + # look for the sbd, matching the tid + sbd = None + for i in md.sb_fds: + if i.state == 'xf' and i.orig_tid == tid: + sbd = i + break + if not sbd: + debug('AIEEE: XFR without sbd!') + raise 'XFRError', (type, tid, params) + + sbd.fd = fd + sbd.block = 0 + sbd.state = 'cp' + sbd.endpoint = (ip, port) + sbd.hash = hash + +def cb_iro(md, type, tid, params, sbd): + "Handles the switchboard participant list" + p = params.split(' ') + uid, ucount, email, nick = p + if ucount == '1': + # do nothing if we only have one participant + return + else: + if email not in md.users.keys(): + md.users[email] = msnlib.user(email) + if email not in sbd.emails: + sbd.emails.append(email) + debug("FRIEND %s joined chat with %s" % (email, sbd.emails[0])) + +def cb_usr(md, type, tid, params, sbd): + "Handles switchboard requests initial identification" + email = sbd.emails[0] + md._send('CAL', email, sbd) + sbd.state = 'ca' + + +def cb_joi(md, type, tid, params, sbd): + "Handles a switchboard join, and sends the pending messages" + email = tid + # if it's a multi-user chat, just append it to the list + if sbd.emails and email != sbd.emails[0]: + sbd.emails.append(email) + if email not in md.users.keys(): + md.users[email] = msnlib.user(email) + debug('CALL: user %s joined chat with %s' % \ + (email, sbd.emails[0])) + # otherwise (common path) set up the sbd and flush the messages + else: + sbd.state = 'es' + debug('CALL: user %s replied your chat request; flushing' % email) + md.sendmsg(email) + debug('CALL: message queue for %s flushed' % email) + + +def cb_ans(md, type, tid, params, sbd): + """Answer confirmation to an invitation, replied after the connect() + ending by read()""" + sbd.state = 'es' + + +def cb_msg(md, type, tid, params, sbd): + "Get a message" + debug('MESSAGE\n+++ Header: %s\n%s\n\n' % (str(tid), str(params))) + + +def cb_ack(md, type, tid, params, sbd): + "Get a message acknowledge" + debug('ACK: tid:%s' % tid) + + +def cb_nak(md, type, tid, params, sbd): + "Get a message negative acknowledge" + debug('NAK: tid:%s' % tid) + + +def cb_bye(md, type, tid, params, sbd): + "Handles a user sb disconnect" + email = tid + if email != sbd.emails[0]: + debug('BYE: user %s leaving sbd' % email) + if email in sbd.emails: + sbd.emails.remove(email) + else: + debug('BYE: closing %s' % str(sbd)) + md.close(sbd) + diff --git a/msnlib.py b/msnlib.py new file mode 100644 index 0000000..1bedaac --- /dev/null +++ b/msnlib.py @@ -0,0 +1,743 @@ + + +import sys +import time +import string +import socket +import select +import md5 +import urllib + +""" +MSN Messenger Client Library +by Alberto Bertogli (albertogli@telpin.com.ar) +""" + +# constants +VERSION = 0x0304 +LOGIN_HOST = 'messenger.hotmail.com' +LOGIN_PORT = 1863 + +status_table = { + 'online': 'NLN', + 'away': 'AWY', + 'busy': 'BSY', + 'brb': 'BRB', + 'phone': 'PHN', + 'lunch': 'LUN', + 'invisible': 'HDN', + 'idle': 'IDL', + 'offline': 'FLN', +} + +reverse_status = { + 'NLN': 'online', + 'AWY': 'away', + 'BSY': 'busy', + 'BRB': 'brb', + 'PHN': 'phone', + 'LUN': 'lunch', + 'HDN': 'invisible', + 'IDL': 'idle', + 'FLN': 'offline', +} + + +def debug(s): + sys.stderr.write('\r' + str(s) + '\n') + sys.stderr.flush() + + +class user: + """User class, used to store your 'friends'""" + def __init__(self, email = '', nick = '', gid = None): + self.email = email + self.nick = nick + self.realnick = '' + self.status = 'FLN' + self.online = 0 + self.gid = gid + self.homep = None + self.workp = None + self.mobilep = None + self.sbd = None + self.priv = {} + self.lists = [] + + def __repr__(self): + return '<user email:%s nick:"%s" gid:%s>' % (self.email, + self.nick, self.gid) + + +class sbd: + """SwitchBoard Descriptor + Used as a pseudo-fd to store per-switchboard connection information. + The state is either one of (too many): + + [answer] + cp connect pending (just came from rng) + re ready (just came from connect) + an waiting for answer reply + + [invite] + xf waiting for xfr response (not even connected yet) + us waiting for usr response + ca waiting for cal response + jo waiting for a join response + + es established (waiting in boredom) + + You will find more information in the doc directory. + """ + + def __init__(self): + self.fd = None # connection fd + self.state = None # connection's state (see doc above) + self.emails = [] # emails we talk to through + self.msgqueue = [] # outgoing message queue + self.hash = None # server-sent hash + self.session_id = None # server-sent sid + self.endpoint = () # remote end (ip, port) + self.type = None # either 'answer' or 'invite' + self.tid = 1 # the transaction id, it needs to be + # unique for consistency + self.block = 1 # blocking state + self.orig_tid = None # tid of the original XFR + + def __repr__(self): + return '<sbd: emails:%s state:%s fd:%d endpoint:%s>' % \ + (str(self.emails), self.state, \ + self.fileno(), self.endpoint) + + def fileno(self): + return self.fd.fileno() + + def get_tid(self): + "Returns a valid tid as string" + self.tid = self.tid + 1 + return str(self.tid - 1) + + + +class msnd: + """MSN Descriptor + This is the main and most important class; it represents a msn + instance. + + It's, afaik, nonblocking (not through setblocking() but mainly because + it forces a select() i/o model (which you would probably have used + anyway, unless you think async/signal io worths the mess for a stupid + messenger protocol, or you are a thread freak)), then the reads should + always succed. Note that we sanely assume that writes do not block. + + Yes yes, you can use poll() too =) + + The only blocking call is the login() which is in charge of doing the + initial connection and setup, all the rest are cpu bound. + + Once you have created an instance you should assign an email and a + password at least, then do the login and i recommend you to call sync + after that (and everyonce in a while doesn't hurt either). Finally you + change your status and you're ready to idle. + + Oh, and don't forget to set the callbacks: they are the most important + part, they are the ones which allow you to control the protocol and + make this useful. + + They are completely asyncronous, are driven by the read method, and + never block. A special care should be taken if you use threads (which + you shouldn't need, that was the whole idea behind this), because + there is not a single lock on these lines, and it will remain that way. + + There is an example (a very bad one, but you'll see how it would work) + that should have come with this file; also the callback file has good + working code. + """ + + def __init__(self): + self.fd = None # socket fd + self.sb_fds = [] # switchboard fds + self.tid = 1 # transaction id + + self.email = None # login email + self.pwd = None # login pwd + self.nick = None # nick + + self.homep = None # home phone + self.workp = None # work phone + self.mobilep = None # mobile phone + + self.status = 'FLN' # status + self.encoding = 'iso-8859-1' # local encoding + + self.lhost = LOGIN_HOST + self.lport = LOGIN_PORT + self.ns = (None, None) # notification server + self.hash = None # hash used to authenticate + + self.syn_lver = 0 # user list version + self.syn_total = 10000 # qty. of users from SYN + self.syn_ngroups = 0 # qty. of groups from SYN + + self.lst_total = 0 # qty. of LSTs got + + self.cb = None # callbacks + + self.users = {} # forward user list + self.reverse = {} # reverse user list + self.groups = {} # group list + + + def __repr__(self): + return '<msnd object, fd:%s, email:%s, tid:%s>' % (self.fd, + self.email, self.tid) + + def fileno(self): + "Useful for select()" + return self.fd.fileno() + + def encode(self, s): + "Encodes a string from local encoding to utf8" + try: + return s.decode(self.encoding).encode('utf-8') + except: + return s + + def decode(self, s): + "Decodes a string from utf8 to local encoding" + try: + return s.decode('utf-8').encode(self.encoding) + except: + return s + + + def pollable(self): + """Return a pair of lists of poll()/select()ables network + descriptors (ie. they are not fds, but actually classes that + implement fileno() methods, like this one and the sbd). We do + it this way because then it's simpler to read(). + + The reason behind the tuple is that for connect-pending fds we + need to wait for writing readiness, so we must tell the + userspace so. Notice that it still goes with the read() path. + + Yes, it is a mess but i couldn't find anything better yet. It + works, it's efficient; let's pretend it's correct =) + + It includes the main file descriptor, and all the switchboards + connections; then you call self.read(fd) on what this returns, + and magic happens.""" + + iwtd = [] + owtd = [] + iwtd.append(self) + for nd in self.sb_fds: + if nd.state == 'cp': # connect is pending + owtd.append(nd) + elif nd.state == 'xf': # skip this case because it's + # not connected yet + pass + else: # readable! + iwtd.append(nd) + return (iwtd, owtd) + + + def get_tid(self): + "Returns a valid tid as string" + self.tid = self.tid + 1 + return str(self.tid - 1) + + + def _send(self, cmd, params = '', nd = None, raw = 0): + """Sends a command to the server, building it first as a + string; uses, if specified, the pseudo fd (it can be either + msnd or sbd).""" + if not nd: + nd = self + tid = nd.get_tid() + fd = nd.fd + c = cmd + ' ' + tid + if params: c = c + ' ' + params + debug(str(fd.fileno()) + ' >>> ' + c) + if not raw: + c = c + '\r\n' + c = self.encode(c) + return fd.send(c) + + + def _recv(self, fd = None): + "Reads a command from the server, returns (cmd, tid, params)" + if not fd: + fd = self.fd + # cheap and dirty readline, FIXME + buf = '' + c = fd.recv(1) + while c != '\n' and c != '': + buf = buf + c + c = fd.recv(1) + + if c == '': + raise 'SocketError' + + buf = buf.strip() + pbuf = buf.split(' ') + + cmd = pbuf[0] + + # it's possible that we don't have any params (errors being + # the most common) so we cover our backs + if len(pbuf) >= 3: + tid = pbuf[1] + params = self.decode(string.join(pbuf[2:])) + elif len(pbuf) == 2: + tid = pbuf[1] + params = '' + else: + tid = '0' + params = '' + + debug(str(fd.fileno()) + ' <<< ' + buf) + return (cmd, tid, params) + + + def _recvmsg(self, msglen, fd = None): + "Read a message from the server, returns it" + if not fd: + fd = self.fd + left = msglen + buf = '' + while len(buf) != msglen: + c = fd.recv(left) + #debug(str(fd.fileno()) + ' <<< ' + buf) + buf = buf + c + left = left - len(c) + + return self.decode(buf) + + + def submit_sbd(self, sbd): + """Submits a switchboard descriptor to add to our list; it is + also put on our global list. + + Note that if there is no such user, we create it in order to + be able to do operations on users that are not in our server + list.""" + + self.sb_fds.append(sbd) + email = sbd.emails[0] + if email not in self.users.keys(): + self.users[email] = user(email) + if self.users[email].sbd and self.users[email].sbd != sbd: + # override the sbd, but keep the message queue + sbd.msgqueue = self.users[email].sbd.msgqueue[:] + self.close(self.users[email].sbd) + self.users[email].sbd = sbd + return + + + def change_status(self, st): + """Changes the current status to: online, away, busy, brb, + phone, lunch, invisible, idle, offline""" + if not status_table.has_key(st): return 0 + self.status = status_table[st] + self._send('CHG', self.status) + return 1 + + + def privacy(self, public = 1, auth = 0): + """Sets our privacy state. First parameter define if you get + messages from everybody or only from people on your list; the + second defines if you want users to ask for authorization or + let everybody add you""" + if public: self._send('BLP', 'AL') # be social + else: self._send('BLP', 'BL') # live in a cave + + if auth: self._send('GTC', 'A') # ask for auth + else: self._send('GTC', 'N') # let them add you + + return 1 + + + def change_nick(self, nick): + "Changes our nick" + nick = urllib.quote(nick) + self._send('REA', self.email + ' ' + nick) + return 1 + + + def sync(self): + "Syncronizes the tables" + self._send('SYN', '0') + return 1 + + + def useradd(self, email, nick = None, gid = '0'): + "Adds a user" + if not nick: nick = email + nick = urllib.quote(nick) + self._send('ADD', 'AL ' + email + ' ' + nick) + self._send('ADD', 'FL ' + email + ' ' + nick + ' ' + gid) + return 1 + + + def userdel(self, email): + "Removes a user" + self._send('REM', 'AL ' + email) + self._send('REM', 'FL ' + email) + return 1 + + def userblock(self, email): + self._send('REM', 'AL ' + email) + self._send('ADD', 'BL ' + email + ' ' + email) + if 'B' not in self.users[email].lists: + self.users[email].lists.append('B') + + def userunblock(self, email): + self._send('REM', 'BL ' + email) + self._send('ADD', 'AL ' + email + ' ' + email) + if 'B' in self.users[email].lists: + self.users[email].lists.remove('B') + + def groupadd(self, name): + "Adds a group" + name = urllib.quote(name) + self._send('ADG', name + ' 0') + return 1 + + def groupdel(self, gid): + "Removes a group" + self._send('RMG', gid) + return 1 + + def groupren(self, gid, newname): + newname = urllib.quote(newname) + self._send('REG', gid + ' ' + newname) + return 1 + + def disconnect(self): + "Disconnect from the server" + self.fd.send('OUT\r\n') + self.fd.close() + + + def close(self, sb): + "Closes a given sbd" + self.sb_fds.remove(sb) + self.users[sb.emails[0]].sbd = None + try: + self._send('BYE', self.email, nd = sb) + sb.fd.close() + except: + pass + del(sb) + + + def invite(self, email, sbd): + "Invites a user into an existing sbd" + self._send('CAL', email, nd = sbd) + + def login(self): + "Logins to the server, really boring" + + # open socket + self.fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.fd.connect((self.lhost, self.lport)) + + # version information + self._send('VER', 'MSNP8 CVR0') + + r = self._recv() + if r[0] != 'VER' and r[2][0:4] != 'MSNP8': + raise 'VersionError', r + + # lie the version, just in case + self._send('CVR', '0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS ' + self.email) + self._recv() # we just don't care what we get + + # ask for notification server + self._send('USR', 'TWN I ' + self.email) + + r = self._recv() + if r[0] != 'XFR' and r[2][0:2] != 'NS': + raise 'NSError', r + + # parse the notification server ip and port (as int) + ns = string.split(r[2])[1] + self.ns = ns.split(':') + self.ns[1] = int(self.ns[1]) + self.ns = tuple(self.ns) + + # close the fd and reopen it on the ns + self.fd.close() + self.fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.fd.connect(self.ns) + + # version, same as before + self._send('VER', 'MSNP8 CVR0') + r = self._recv() + if r[0] != 'VER' and r[2][0:4] != 'MSNP8': + raise 'VersionError', r + + # lie the version, just in case + self._send('CVR', '0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS ' + self.email) + self._recv() # we just don't care what we get + + # auth: send user, get hash + self._send('USR', 'TWN I ' + self.email) + + r = self._recv() + if r[0] != 'USR': + raise 'AuthError', r + hash = string.split(r[2])[2] + + # get and use the passport id + passportid = self.passport_auth(hash) + self._send('USR', 'TWN S ' + passportid) + + r = self._recv() + if r[0] != 'USR' and r[2][0:2] != 'OK': + raise 'AuthError', r + self.nick = string.split(r[2])[2] + self.nick = urllib.unquote(self.nick) + + return 1 + + + def passport_auth(self, hash): + """Logins into passport and obtains an ID used for + authorization; it's a helper function for login""" + import urllib + import httplib + + # initial connection + debug('PASSPORT begin') + nexus = urllib.urlopen('https://nexus.passport.com/rdr/pprdr.asp') + h = nexus.headers + purl = h['PassportURLs'] + + # parse the info + d = {} + for i in purl.split(','): + key, val = i.split('=', 1) + d[key] = val + + # get the login server + login_server = 'https://' + d['DALogin'] + login_host = d['DALogin'].split('/')[0] + + # build the authentication headers + ahead = 'Passport1.4 OrgVerb=GET' + ahead += ',OrgURL=http%3A%2F%2Fmessenger%2Emsn%2Ecom' + ahead += ',sign-in=' + urllib.quote(self.email) + ahead += ',pwd=' + urllib.quote(self.pwd) + ahead += ',lc=1033,id=507,tw=40,fs=1,' + ahead += 'ru=http%3A%2F%2Fmessenger%2Emsn%2Ecom,ct=1062764229,' + ahead += 'kpp=1,kv=5,ver=2.1.0173.1,tpf=' + hash + headers = { 'Authorization': ahead } + + # connect to the given server + debug('SSL Connect to %s' % login_server) + ls = httplib.HTTPSConnection(login_host) + + # make the request + debug('SSL GET') + ls.request('GET', login_server, '', headers) + resp = ls.getresponse() + + # loop if we get redirects until we get a definitive answer + debug('SSL Response %d' % resp.status) + while resp.status == 302: + login_server = resp.getheader('Location') + login_host = login_server.split('/')[2] + debug('SSL Redirect to %s' % login_server) + ls = httplib.HTTPSConnection(login_host) + headers = { 'Authorization': ahead } + ls.request('GET', login_server, '', headers) + resp = ls.getresponse() + debug('SSL Response %d' % resp.status) + + # now we have a definitive answer, if it's not 200 (success) + # just raise AuthError + if resp.status != 200: + # for now we raise 911, which means authentication + # failed; but maybe we can get more detailed + # information + raise 'AuthError', [911, 'SSL Auth failed'] + + # and parse the headers to get the passport id + try: + ainfo = resp.getheader('Authentication-Info') + except: + ainfo = resp.getheader('WWW-Authenticate') + + d = {} + for i in ainfo.split(','): + key, val = i.split('=', 1) + d[key] = val + + passportid = d['from-PP'] + passportid = passportid[1:-1] # remove the "'" + return passportid + + + def read(self, nd = None): + """Reads from the specified nd and run the callback. The nd + can be either a msnd or a sbd (that's why it's called 'nd' + from 'network descriptor'). + """ + if not nd: + nd = self + + # handle different stages of switchboard initialization + if nd in self.sb_fds: + # connect pending + if nd.state == 'cp': + # see if the connect went well + r = nd.fd.getsockopt(socket.SOL_SOCKET, + socket.SO_ERROR) + if r != 0: + raise 'SocketError', 'ConnectFailed' + nd.fd.setblocking(1) + nd.block = 1 + nd.state = 're' + + # need to send the answer to the remote invitation + if nd.type == 'answer' and nd.state == 're': + params = self.email + ' ' + nd.hash + ' ' + \ + nd.session_id + self._send('ANS', params, nd) + nd.state = 'an' + return + if nd.type == 'invite' and nd.state == 're': + params = self.email + ' ' + nd.hash + self._send('USR', params, nd) + nd.state = 'us' + return + + + + r = self._recv(nd.fd) + type = r[0] + tid = r[1] + params = string.strip(r[2]) + + if type == 'CHL': self.cb.chl(self, type, tid, params) + elif type == 'QRY': self.cb.qry(self, type, tid, params) + elif type == 'ILN': self.cb.iln(self, type, tid, params) + elif type == 'CHG': self.cb.chg(self, type, tid, params) + elif type == 'OUT': self.cb.out(self, type, tid, params) + elif type == 'FLN': self.cb.fln(self, type, tid, params) + elif type == 'NLN': self.cb.nln(self, type, tid, params) + elif type == 'BLP': self.cb.blp(self, type, tid, params) + elif type == 'LST': self.cb.lst(self, type, tid, params) + elif type == 'GTC': self.cb.gtc(self, type, tid, params) + elif type == 'SYN': self.cb.syn(self, type, tid, params) + elif type == 'PRP': self.cb.prp(self, type, tid, params) + elif type == 'LSG': self.cb.lsg(self, type, tid, params) + elif type == 'BPR': self.cb.bpr(self, type, tid, params) + elif type == 'ADD': self.cb.add(self, type, tid, params) + elif type == 'REA': self.cb.rea(self, type, tid, params) + elif type == 'REM': self.cb.rem(self, type, tid, params) + elif type == 'ADG': self.cb.adg(self, type, tid, params) + elif type == 'RMG': self.cb.rmg(self, type, tid, params) + elif type == 'REG': self.cb.reg(self, type, tid, params) + elif type == 'RNG': self.cb.rng(self, type, tid, params) + + elif type == 'IRO': self.cb.iro(self, type, tid, params, nd) + elif type == 'ANS': self.cb.ans(self, type, tid, params, nd) + elif type == 'XFR': self.cb.xfr(self, type, tid, params) + elif type == 'USR': self.cb.usr(self, type, tid, params, nd) + elif type == 'CAL': self.cb.cal(self, type, tid, params, nd) + elif type == 'JOI': self.cb.joi(self, type, tid, params, nd) + + elif type == 'ACK': self.cb.ack(self, type, tid, params, nd) + elif type == 'NAK': self.cb.nak(self, type, tid, params, nd) + elif type == 'BYE': self.cb.bye(self, type, tid, params, nd) + + + elif type == 'MSG': + params = tid + ' ' + params + mlen = int(r[2].split()[-1]) + msg = self._recvmsg(mlen, nd.fd) + self.cb.msg(self, type, params, msg, nd) + + else: + # catch server errors - always numeric type + try: + errno = int(type) + except: + errno = None + + if errno: + self.cb.err(self, errno, \ + str(tid) + ' ' + str(params)) + else: + # if we got this far, we have no idea + self.cb.unk(self, type, tid, params) + return + + + def sendmsg(self, email, msg = '', sb = None): + """Sends a message to the user identified by 'email', either + the one specified or flush the queue. + Returns: + 1 message queued for delivery + 2 queue flushed + -2 the message is too big + + To verify the message delivery, use the ack callbacks. + Message sending order is guaranteed within a sbd; but not the + acknowledge; that's what the ACK/NAK callbacks are for. + """ + + if email and email not in self.users.keys(): + self.users[email] = user(email) + + if len(msg) > 1500: + return -2 + + if not sb: + sb = self.users[email].sbd + + # we don't have a connection + if not sb: + sb = sbd() + sb.state = 'xf' + sb.type = 'invite' + sb.emails.append(email) + sb.msgqueue.append(msg) + + self.submit_sbd(sb) # no need to connect it yet + # we set the orig_tid of the sbd to the next tid (that + # is, the tid the XFR is going to have), in order to + # be able to identify it later, in cb.cb_xfr() + sb.orig_tid = str(self.tid) + self._send('XFR', 'SB') + return 1 + + # it's not ready yet + elif sb.state != 'es': + sb.msgqueue.append(msg) + return 1 + + # no more excuses, send it + else: + # we make a list with all the messages to send + pend = sb.msgqueue + if msg: + pend.append(msg) + while len(pend): + m = pend[0] + header = "MIME-Version: 1.0\r\n" + \ + "Content-Type: text/plain; " + \ + "charset=UTF-8\r\n\r\n" + m = header + m + msize = len(self.encode(m)) + params = 'A ' + str(msize) + '\r\n' + m + self._send('MSG', params, sb, raw = 1) + del(pend[0]) + + return 2 + + + diff --git a/msnrc.sample b/msnrc.sample new file mode 100644 index 0000000..ccb32ca --- /dev/null +++ b/msnrc.sample @@ -0,0 +1,65 @@ + +# This is the msn client configuration file. +# It should be placed in ~/.msn/msnrc, with permissions 0600 (but this are not +# checked by the client, so you have to take care of it) +# The format is: variable = value +# Anything that begins with a '#' or doesn't have a '=' is a comment. +# There are some variables that _must_ be set, and others that can be omitted, +# in which case the default value will be used. Here they are: + +# email, mandatory +email = myself@mydomain.whatever + + +# all the following are optional parameters and can be omited, in which case +# the default will be used + +# password, if you avoid it, the client will ask you for it at runtime +password = XXXX + +# number of message history to keep in memory +# defaults to 10 +history size = 10 + +# number of user input lines to keep in memory +# defaults to 10 +input history size = 10 + +# defines if you want to log your incoming/outgoing messages to the disk, must +# be yes or no. +# the default is yes +log history = yes + +# directory where to store your log history (if it's used, of course). +# defaults to $HOME/.msn/history +history directory = /home/myself/.msn/history + +# number of seconds after, if no command was received, we set automatically +# away. defaults to 0, which disables it. +auto away = 0 + +# initial status when we first connect +# defaults to online and must be valid, that is, one of online, away, busy, +# and so on) +initial status = online + +# local encoding +# it's the local character encoding, used to display and send the messages' +# text in an standard way. don't worry about it if you never had problems when +# sending non-english characters. it defaults to the environment variables +# LC_ALL, or LANG, and if none is available to iso-8859-1. +encoding = iso-8859-1 + +# debug variable, can be yes or no; note that you can enable and disable it at +# runtime using the 'debug' command, so this is mostly used when you want to +# debug the whole session init. +# the default is no +debug = no + +# color theme +# this configures which color theme to use; but you can still change it at +# runtime with the 'color' command (use it to see a list of available themes +# too) +color theme = default + + diff --git a/msnsetup b/msnsetup new file mode 100644 index 0000000..d722e70 --- /dev/null +++ b/msnsetup @@ -0,0 +1,87 @@ +#!/bin/bash + +function intro() { + echo + echo "This is a script for creating an initial configuration for the msn client." + echo "Any problems or questions regarding it, email albertogli@telpin.com.ar." + echo + echo "Now you will have to answer a few questions. If you are in doubt, press ENTER and the harmless default will be used." + echo "If you want to abort any time, just press CTRL+C" + echo +} + +function get_email() { + echo "* Email" + echo "This is the email address you use with your msn account, usually (but not necesarily) a hotmail.com account." + while [ -z "$EMAIL" ]; do + read -p "Please insert your email address: " EMAIL + echo + done + echo + export EMAIL +} + +function get_pass() { + echo "* Password" + echo "Your email address' password. Note that the characters won't be displayed for security issues." + echo "If you press ENTER, it won't be written, and you'll be asked for it at login time." + read -s -p "Please insert your password: " PASS + echo + export PASS +} + +function create_dirs() { + mkdir "$HOME/.msn" 2> /dev/null + mkdir "$HOME/.msn/history" 2> /dev/null + chmod -R og-rwx "$HOME/.msn" +} + +function create_rc() { + # first parameter is the rc file to create + if [ -s "$1" ]; then + echo "Error: file $1 already exists!" + exit + fi + touch "$1" + chmod -R 0600 "$1" + echo "# msn client configuration file" >> "$1" + echo "# created automatically by the msnsetup script" >> "$1" + echo >> "$1" + echo "email = $EMAIL" >> "$1" + if [ -z "$PASS" ]; then + echo "# password not configured" >> "$1" + else + echo "password = $PASS" >> "$1" + fi + echo >> "$1" +} + + +# main + +# we take only one optional parameter, the profile to create the rc for + +intro +get_email +get_pass + +echo "Creating the directory hierachy ($HOME/.msn)" +create_dirs + +RC="$HOME/.msn/msnrc" +# if we have the profile, use it +if [ "$1" ]; then + echo "Configuring for profile $1" + RC="$HOME/.msn/msnrc-$1" +fi +echo "Creating the configuration file ($RC)" +create_rc "$RC" + +RUN="msn" +if [ "$1" ]; then + RUN="msn $1" +fi +echo "Done! run '$RUN' to start the client" + + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1b192e1 --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ + +from distutils.core import setup + +setup(name="msnlib", + version="3.4", + description="MSN Messenger Library and Client", + author="Alberto Bertogli", + author_email="albertogli@telpin.com.ar", + url="http://users.auriga.wearlab.de/~alb/msnlib", + py_modules=['msnlib', 'msncb'], +) + diff --git a/utils/clean b/utils/clean new file mode 100644 index 0000000..168a843 --- /dev/null +++ b/utils/clean @@ -0,0 +1,5 @@ +#!/bin/sh + +rm -vrf *.pyc *.pyo *~ +rm -vrf build + diff --git a/utils/hmerge b/utils/hmerge new file mode 100644 index 0000000..2843d68 --- /dev/null +++ b/utils/hmerge @@ -0,0 +1,110 @@ +#!/usr/bin/env python + +""" +Merger for msnlib logfiles. + +It takes two logfiles as arguments, and prints out the merge between them, +sorting using the time. + +Quite useful when you have used msn in two different places and want to unify +the logs. + + +Note that this will not do absolute time sorting (as it's usual for time to go +backwards, as we all know =), but record-by-record time compares. + +Alberto Bertogli (albertogli@telpin.com.ar), 02/Jun/2003 +Please send any reports to msnlib-devel@auriga.wearlab.de. +""" + + +import sys +import time + + +def get_records(fd): + records = [] + l = fd.readline() + rec = l + l = fd.readline() + while rec: + # if the line begins with \t, then it's a multi-line record + if l and l[0] == '\t': + rec += l + l = fd.readline() + continue + + # process the actual record + ls = rec.split(' ', 2) + raw_date = ls[0] + ' ' + ls[1] + date = time.strptime(raw_date, '%d/%b/%Y %H:%M:%S ') + date = time.mktime(date) + records.append((date, rec)) + + # save the current line + rec = l + l = fd.readline() + return records + +def panic(s): + print s + sys.exit(1) + +try: + fd1 = open(sys.argv[1]) + fd2 = open(sys.argv[2]) +except: + panic("Use: hmerge file1 file2") + +# this is the invalid record to mark the end of the record list +eor_record = (0, '') + + +rec1 = get_records(fd1) +rec2 = get_records(fd2) + +if not rec1: panic("Error: file 1 doesn't have any records") +if not rec2: panic("Error: file 1 doesn't have any records") + +# append the eor_record to both lists +rec1.append(eor_record) +rec2.append(eor_record) + +len1 = len(rec1) +len2 = len(rec2) + +point1 = 0 +point2 = 0 + +while 1: + + r1 = rec1[point1] + r2 = rec2[point2] + + # if we have any at the end, print it or exit + if r1[0] == 0 or r2[0] == 0: + # if we reach the end of both lists, we exit + if r1[0] == 0 and r2[0] == 0: + break + if r1[0] == 0: + print r2[1], + point2 += 1 + elif r2[0] == 0: + print r1[1], + point1 += 1 + + # otherwise, compare and print the earlier + else: + if r1[0] < r2[0]: + print r1[1], + point1 += 1 + elif r1[0] > r2[0]: + print r2[1], + point2 += 1 + else: + print r1[1], + print r2[1], + point1 += 1 + point2 += 1 + + diff --git a/utils/msnbot b/utils/msnbot new file mode 100644 index 0000000..001d2d1 --- /dev/null +++ b/utils/msnbot @@ -0,0 +1,127 @@ +#!/usr/bin/env python + +""" +This is a very simple bot to show how automation using msnlib could be done. +It's not quite useful as-is, but provides a good example. + +If you play with it, please let me know. +""" + + +# sys, for getting the parameters +import sys + +# time, for sleeping +import time + +# select to wait for events +import select + +# socket, to catch errors +import socket + +# thread, for creating the worker thread +import thread + +# and, of course, msnlib +import msnlib +import msncb + + +m = msnlib.msnd() +m.cb = msncb.cb() + + +def do_work(): + """ + Here you do your stuff and send messages using m.sendmsg() + This is the only place your code lives + """ + + # wait a bit for everything to settle down (sync taking efect + # basically) + time.sleep(15) + + print '-' * 20 + 'SEND 1' + print m.sendmsg("xx@me.com", "Message One") + + print '-' * 20 + 'SEND 2' + print m.sendmsg("xx@me.com", "Message Two") + + # give time to send the messages + time.sleep(30) + + # and then quit + quit() + + +# you shouldn't need to touch anything past here + + +# get the login email and password from the parameters +try: + m.email = sys.argv[1] + m.pwd = sys.argv[2] +except: + print "Use: msnbot email password" + sys.exit(1) + + +print "Logging In" +m.login() + +print "Sync" +# this makes the server send you the contact list, and it's recommended that +# you do it because you can get in trouble when getting certain events from +# people that are not on your list; and it's not that expensive anyway +m.sync() + +print "Changing Status" +# any non-offline status will do, otherwise we'll get an error from msn when +# sending a message +m.change_status("away") + +def quit(): + try: + m.disconnect() + except: + pass + print "Exit" + sys.exit(0) + +# we start a thread to do the work. it's a thread because we want to share +# everything, and fork cow semantics cause problems here +thread.start_new_thread(do_work, ()) + + +# we loop over the network socket to get events +print "Loop" +while 1: + # we get pollable fds + t = m.pollable() + infd = t[0] + outfd = t[1] + + # we select, waiting for events + try: + fds = select.select(infd, outfd, [], 0) + except: + quit() + + for i in fds[0] + fds[1]: # see msnlib.msnd.pollable.__doc__ + try: + m.read(i) + except ('SocketError', socket.error), err: + if i != m: + # user closed a connection + # note that messages can be lost here + m.close(i) + else: + # main socket closed + quit() + + # sleep a bit so we don't take over the cpu + time.sleep(0.01) + + + diff --git a/utils/msncd b/utils/msncd new file mode 100644 index 0000000..82a7e7e --- /dev/null +++ b/utils/msncd @@ -0,0 +1,417 @@ +#!/usr/bin/env python + + +import sys +import os +import socket +import select +import string + +import msnlib +import msncb + + +""" +MSN Client Daemon + +This is a MSN client that reads commands from a named pipe, using +a little text-only protocol. It's main use is to serve as a 'glue' +to implement clients in other languages. + +This is yet experimental because lack of testing, please let me know if you +try it out. +""" + + +def null(s): + "Null function, useful to void debug ones" + pass + + +# +# This are the callback replacements, which only handle the output and then +# call the original callbacks to do the lower level stuff +# + +# basic classes +m = msnlib.msnd() +m.cb = msncb.cb() + +# status change +def cb_iln(md, type, tid, params): + t = params.split() + status = msnlib.reverse_status[t[0]] + email = t[1] + equeue.append('STCH %s %s\n' % (email, status)) + msncb.cb_iln(md, type, tid, params) +m.cb.iln = cb_iln + +def cb_nln(md, type, tid, params): + status = msnlib.reverse_status[tid] + t = string.split(params) + email = t[0] + equeue.append('STCH %s %s\n' % (email, status)) + msncb.cb_nln(md, type, tid, params) +m.cb.nln = cb_nln + +def cb_fln(md, type, tid, params): + email = tid + u = m.users[email] + discarded = 0 + if u.sbd and u.sbd.msgqueue: + discarded = len(u.sbd.msgqueue) + equeue.append('STCH %s offline %d\n' % (email, discarded)) + msncb.cb_fln(md, type, tid, params) +m.cb.fln = cb_fln + +# server disconnect +def cb_out(md, type, tid, params): + equeue.append('ERR SERV_DISC Server sent disconnect\n') + msncb.cb_out(md, type, tid, params) +m.cb.out = cb_out + + +# message +def cb_msg(md, type, tid, params, sbd): + t = string.split(tid) + email = t[0] + + # messages from hotmail are only when we connect, and send things + # regarding, aparently, hotmail issues. we ignore them (basically + # because i couldn't care less; however if somebody has intrest in + # these and provides some debug output i'll be happy to implement + # parsing). + if email == 'Hotmail': + return + + # parse + lines = string.split(params, '\n') + headers = {} + eoh = 1 + for i in lines: + # end of headers + if i == '\r': + break + tv = string.split(i, ':') + type = tv[0] + value = string.join(tv[1:], ':') + value = string.strip(value) + headers[type] = value + eoh += 1 + + if headers.has_key('Content-Type') and headers['Content-Type'] == 'text/x-msmsgscontrol': + # the typing notices + equeue.append('TYPING %s\n' % email) + else: + # messages + equeue.append('MSG %d %d %s\n%s\n' % \ + (len(lines), eoh, email, string.join(lines, '\n')) ) + + msncb.cb_msg(md, type, tid, params, sbd) +m.cb.msg = cb_msg + + +# join a conversation and send pending messages +def cb_joi(md, type, tid, params, sbd): + email = tid + if len(sbd.msgqueue) > 0: + equeue.append('MFLUSH %s\n' % email) + msncb.cb_joi(md, type, tid, params, sbd) +m.cb.joi = cb_joi + +# server errors +def cb_err(md, errno, params): + if not msncb.error_table.has_key(errno): + desc = 'Unknown' + else: + desc = msncb.error_table[errno] + equeue.append('ERR %s %s\n' % (errno, desc)) + msncb.cb_err(md, errno, params) +m.cb.err = cb_err + +# users add, delete and modify +def cb_add(md, type, tid, params): + t = params.split() + type = t[0] + if type == 'RL' or type == 'FL': + email = t[2] + if type == 'RL': + equeue.append('UADD %s\n' % email) + elif type == 'FL': + equeue.append('ADDFL %s\n' % email) + msncb.cb_add(md, type, tid, params) +m.cb.add = cb_add + +def cb_rem(md, type, tid, params): + t = params.split() + type = t[0] + if type == 'RL' or type == 'FL': + email = t[2] + if type == 'RL': + equeue.append('UDEL %s\n' % email) + elif type == 'FL': + equeue.append('DELFL %s\n' % email) + msncb.cb_rem(md, type, tid, params) +m.cb.rem = cb_rem + + +def login(email, password): + # login to msn + printl('Logging in... ', c.green, 1) + try: + m.login() + printl('done\n', c.green, 1) + except 'AuthError', info: + errno = int(info[0]) + if not msncb.error_table.has_key(errno): + desc = 'Unknown' + else: + desc = msncb.error_table[errno] + perror('Error: %s\n' % desc) + quit(1) + except KeyboardInterrupt: + quit() + except ('SocketError', socket.error), info: + perror('Network error: ' + str(info) + '\n') + quit(1) + except: + pexc('Exception logging in\n') + quit(1) + + +# +# the pipe read +# + +# first, a small send wrapper to avoid repeating 'addr' all over the place +# note that as they are implemented using udp, if more than one client +# connects it can get quite quite messy +addr = () +def psend(pipe, s): + print '-->', s, + return pipe.sendto(s, addr) + +# read from the pipe, c being the pipe socket passed from the caller +def pipe_read(c): + global m + global addr + global equeue + + # we don't worry about lines too much in this implementation because + # we use datagrams. however, when using stream sockets you should + s, addr = c.recvfrom(4 * 1024) # input buffer, should be enough + print '<--', s, + try: + s = s.split(' ', 1) + if len(s) == 2: + cmd, params = s + else: + cmd = s[0] + params = '' + + cmd = cmd.strip() + if params: + params = params.strip() + params = params.split(' ') + except: + psend(c, 'ERR EINVAL\n') + return + + if cmd == 'LOGIN': + if len(params) != 2: + psend(c, 'ERR PARAMS\n') + return + try: + email, pwd = params + m.email = email + m.pwd = pwd + m.login() + m.sync() + except 'AuthError', info: + errno = int(info[0]) + if not msncb.error_table.has_key(errno): + desc = 'Unknown' + else: + desc = msncb.error_table[errno] + psend(c, 'ERR MSN %d %s\n' % (errno, desc)) + return + except ('SocketError', socket.error), info: + psend(c, 'ERR SOCK %s\n' % str(info)) + return + psend(c, 'OK\n') + return + + elif cmd == 'LOGOFF': + m.disconnect() + psend(c, 'OK\n') + return + + # if we are not connected, the following commands are not available + if not m.fd: + psend(c, 'ERR ENOTCONN\n') + return + + + if cmd == 'STATUS': + status = string.join(params, ' ') + if not m.change_status(status): + psend(c, 'ERR UNK STATUS\n') + else: + psend(c, 'OK\n') + return + + if cmd == 'POLL': + equeue.append('POLLEND\n') + for evt in equeue: + psend(c, evt) + equeue = [] + return + + if cmd == 'GETCL': + psend(c, 'CL %d\n' % len(m.users.keys()) ) + for email in m.users.keys(): + u = m.users[email] + status = msnlib.reverse_status[u.status] + psend(c, '%s %s %s\n' % (status, email, u.nick)) + return + + if cmd == 'GETRCL': + psend(c, 'CL %d\n' % len(m.reverse.keys()) ) + for email in m.reverse.keys(): + u = m.reverse[email] + status = msnlib.reverse_status[u.status] + psend(c, '%s %s %s\n' % (status, email, u.nick)) + return + + if cmd == 'INFO': + if len(params) != 1: + psend(c, 'ERR PARAMS\n') + return + if not m.users.has_key(email): + psend(c, 'ERR UNK USER\n') + u = m.users[email] + psend(c, 'email = %s\n' % email) + psend(c, 'nick = %s\n' % u.nick) + psend(c, 'homep = %s\n' % u.homep) + psend(c, 'workp = %s\n' % u.workp) + psend(c, 'mobilep = %s\n' % u.mobilep) + psend(c, '\n') + return + + if cmd == 'ADD': + if len(params) != 2: + psend(c, 'ERR PARAMS\n') + return + nick, email = params + m.useradd(email, nick) + psend(c, 'OK\n') + return + + if cmd == 'DEL': + if len(params) != 1: + psend(c, 'ERR PARAMS\n') + return + m.userdel(params) + psend(c, 'OK\n') + return + + if cmd == 'NICK': + if len(params) != 1: + psend(c, 'ERR PARAMS\n') + return + m.change_nick(params) + psend(c, 'OK\n') + return + + if cmd == 'PRIV': + if len(params) != 2: + psend(c, 'ERR PARAMS\n') + return + try: + public = int(p[0]) + auth = int(p[1]) + if public not in (0, 1) or auth not in (0, 1): + raise + except: + psend(c, 'ERR EINVAL\n') + return + m.privacy(public, auth) + psend(c, 'OK\n') + return + + if cmd == 'SENDMSG': + params = string.join(params, ' ') + params = string.split(params, '\n', 2) + params, msg = params + params = string.split(params, ' ') + if len(params) < 2: + psend(c, 'ERR PARAMS\n') + return + lines = params[0] + email = params[1] + msg = msg + m.sendmsg(email, msg) + psend(c, 'OK\n') + return + + # if we got here is because the command is unknown + psend(c, 'ERR UNK\n') + return + + + +# +# now the real thing +# + +# void the debug +msnlib.debug = null +msncb.debug = null + +# POLL event queue +# We implement it in a very, very efficient way: text =) +# Yes, it's actually a list, but just because .append() is readable +# and allow us to keep track of the number of pending events +equeue = [] + +# open the socket for local communication +# we use datagram sockets to avoid complex reads and writes for now, but the +# protocol is line-oriented and perfectly capable of working over a stream +# socket. +pipe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0) +pipe.bind(('127.0.0.1', 3030)) + + +# loop, waiting for connections +while 1: + infd = outfd = [] + # if we are connected, poll from msn + if m.fd != None: + t = m.pollable() + infd = t[0] + outfd = t[1] + infd.append(pipe) + + fds = select.select(infd, outfd, [], 0) + + for i in fds[0] + fds[1]: # see msnlib.msnd.pollable.__doc__ + if i == pipe: + # read from the pipe + pipe_read(pipe) + else: + try: + m.read(i) + except ('SocketError', socket.error), err: + if i != m: + # user closed a connection + # note that messages can be + # lost here + equeue.append('SCLOSE USER %s %d\n' % (i.emails[0], len(i.msgqueue)) ) + m.close(i) + else: + # main socket closed + # report + equeue.append('SCLOSE MAIN\n') + quit(1) + + diff --git a/utils/msnlog.vim b/utils/msnlog.vim new file mode 100644 index 0000000..800ec75 --- /dev/null +++ b/utils/msnlog.vim @@ -0,0 +1,30 @@ + +" Vim syntax file for msnlib logfiles +" Alberto (albertogli@telpin.com.ar) 28/Sep/2003 + +" Use it to read your msnlib log files with color, makes it much easier. +" Install it by copying to ~/.vim/syntax/msnlog.vim and then run (from vim) +" :set syntax=msnlog to apply it. + +syntax clear +hi clear +syntax case ignore + + +syntax match mlogMultiStr "^\t.*$" +syntax match mlogIMsg "<<< .*$" +syntax match mlogOMsg ">>> .*$" +syntax match mlogStatus ".*\*\*\* .*$" +syntax match mlogMchat "+++ .*$" +syntax match mlogDate "^../.../.... ..:..:.." + + +hi mlogDate ctermfg=blue +hi mlogIMsg ctermfg=green +hi mlogOMsg ctermfg=cyan +hi mlogStatus ctermfg=yellow +hi mlogMchat ctermfg=yellow +hi mlogMultiStr ctermfg=magenta + + + diff --git a/utils/msntk b/utils/msntk new file mode 100644 index 0000000..a4d3376 --- /dev/null +++ b/utils/msntk @@ -0,0 +1,468 @@ +#!/usr/bin/env python + +import sys +import time +import string +import socket +import select +from Tkinter import * +import tkMessageBox +import tkSimpleDialog + +import msnlib +import msncb + +""" +MSN Tk Client + +This is a beta msn client based on msnlib. As you see, it's GUI based on the +Tk bindings, which provide an abstraction to create graphical interfaces; it +works both under linux, windows and probably others too. + +For further information refer to the documentation or the source (which is +always preferred). +Please direct any comments to the msnlib mailing list, +msnlib-devel@auriga.wearlab.de. +You can find more information, and the package itself, at +http://users.auriga.wearlab.de/~alb/msnlib +""" + + +# main msnlib classes +m = msnlib.msnd() +m.cb = msncb.cb() + +# void debug output +#def void(s): pass +#msnlib.debug = msncb.debug = void + + + +# +# useful functions +# + +#sys.setdefaultencoding("iso-8859-15") +encoding = 'iso-8859-1' + +def encode(s): + try: + return s.decode(encoding).encode('utf-8') + except: + return s + +def decode(s): + try: + return s.decode('utf-8').encode(encoding) + except: + return s + +def nick2email(nick): + "Returns an email according to the given nick, or None if noone matches" + for email in m.users.keys(): + if str(m.users[email].nick) == str(nick): + return email + if nick in m.users.keys(): + return nick + return None + +def email2nick(email): + "Returns a nick accoriding to the given email, or None if noone matches" + if email in m.users.keys(): + return m.users[email].nick + else: + return None + +def now(): + "Returns the current time in format HH:MM:SSTT" + return time.strftime('%I:%M:%S%p', time.localtime(time.time()) ) + +def quit(): + "Cleans up and quits everything" + try: + m.disconnect() + except: + pass + root.quit() + sys.exit(0) + + + +# +# GUI classes +# + +class userlist(Frame): + "The user list" + def __init__(self, master): + Frame.__init__(self, master) + self.scrollbar = Scrollbar(self, orient = VERTICAL) + self.list = Listbox(self, + yscrollcommand = self.scrollbar.set) + self.list.config(font = "Courier") + self.scrollbar.config(command = self.list.yview) + self.scrollbar.pack(side = RIGHT, fill = Y) + self.list.pack(side = LEFT, fill = BOTH, expand = 1) + + self.list.bind("<Double-Button-1>", self.create_chat) + + def create_chat(self, evt = None): + "Creates a chat window" + if m.status == 'HDN': + tkMessageBox.showwarning("Warning", + "You can't open chats when you're invisible") + return + nick = self.list.get(self.list.curselection())[4:] + email = nick2email(nick) + if email in emwin.keys(): + emwin[email].lift() + elif m.users[email].status == 'FLN': + tkMessageBox.showwarning("Warning", + "The user is offline") + else: + emwin[email] = chatwindow(root, email) + + +class mainmenu(Menu): + "Main menu used in the main window" + def __init__(self, master): + Menu.__init__(self, master) + self.status_menu = Menu(self, tearoff = 0) + self.add_cascade(label = "Status", menu = self.status_menu) + self.status_menu.add_command(label = "Online", + command = self.chst_online) + self.status_menu.add_command(label = "Away", + command = self.chst_away) + self.status_menu.add_command(label = "Busy", + command = self.chst_busy) + self.status_menu.add_command(label = "Be Right Back", + command = self.chst_brb) + self.status_menu.add_command(label = "Lunch", + command = self.chst_lunch) + self.status_menu.add_command(label = "Phone", + command = self.chst_phone) + self.status_menu.add_command(label = "Invisible", + command = self.chst_invisible) + + self.add_command(label = 'Info', command = self.show_info) + + def show_info(self, evt = None): + csel = mainlist.list.curselection() + if not csel: + return + nick = mainlist.list.get(csel)[4:] + email = nick2email(nick) + infowindow(root, email) + + # status change callbacks + def clear_heads(self): + for i in emwin.keys(): + emwin[i].head.config(text = '') + + def chst_online(self): + self.clear_heads() + m.change_status('online') + def chst_away(self): + self.clear_heads() + m.change_status('away') + def chst_busy(self): + self.clear_heads() + m.change_status('busy') + def chst_brb(self): + self.clear_heads() + m.change_status('brb') + def chst_lunch(self): + self.clear_heads() + m.change_status('lunch') + def chst_phone(self): + self.clear_heads() + m.change_status('phone') + def chst_invisible(self): + warn = "Warning: as you are invisible, it is possible that\n" + warn += "the messages you type here never get to the user." + for i in emwin.keys(): + emwin[i].head.config(text = warn) + m.change_status('invisible') + + +class chatwindow(Toplevel): + "Represents a chat window" + def __init__(self, master, email): + Toplevel.__init__(self, master) + self.email = email + self.protocol("WM_DELETE_WINDOW", self.destroy_window) + nick = email2nick(email) + # FIXME: update the title with status change + status = msnlib.reverse_status[m.users[email].status] + if nick: + self.wm_title(nick + ' (' + status + ')') + else: + self.wm_title(email + ' (' + status + ')') + + # head label + self.head = Label(self) + self.head.pack(side = TOP, fill = X, expand = 0) + self.head.config(justify = LEFT) + self.head.config(text = "") + + # text box (with scrollbar), where the message goes + self.frame = Frame(self) + self.scrollbar = Scrollbar(self.frame, orient = VERTICAL) + self.text = Text(self.frame, + yscrollcommand = self.scrollbar.set) + self.scrollbar.config(command = self.text.yview) + self.scrollbar.pack(side = RIGHT, fill = Y) + self.text.pack(side = TOP, fill = BOTH, expand = 1) + self.frame.pack(side = TOP, fill = BOTH, expand = 1) + + self.text.config(state = DISABLED) + self.text.tag_config('from', foreground = 'blue') + self.text.tag_config('to', foreground = 'red') + self.text.tag_config('typing', foreground = 'lightblue') + + # entry, where the user types + self.entry = Entry(self) + self.entry.pack(side = BOTTOM, fill = X, expand = 0) + self.entry.bind('<Return>', self.send_line) + + def append(self, s, direction, scroll = 1): + "Adds text to the window's text box" + self.text.config(state = NORMAL) + self.text.insert(END, s, direction) + self.text.yview(SCROLL, scroll, UNITS) + self.text.config(state = DISABLED) + + def send_line(self, evt = None): + "Sends the current entry as a message" + msg = self.entry.get() + lines = msg.split('\n') + if len(lines) == 1: + s = now() + ' >>> ' + msg + '\n' + else: + s = now() + ' >>>\n\t' + s += string.join(lines, '\n\t') + s = s[:-1] + self.append(s, 'to', scroll = len(lines)) + + # we need to encode it before sending because msg is already + # an unicode string; so use utf-8 + msg = msg.encode('utf-8') + + m.sendmsg(self.email, msg) + self.entry.delete(0, END) + + def destroy_window(self, evt = None): + "Clean up when the window is closed" + del(emwin[self.email]) + self.destroy() + + +class infowindow(Toplevel): + "Represents a window with user information" + def __init__(self, master, email): + Toplevel.__init__(self, master) + self.email = email + self.wm_title('Info on ' + email) + u = m.users[email] + out = '' + out += 'Information for user ' + email + '\n\n' + out += 'Nick: ' + u.nick + '\n' + out += 'Status: ' + msnlib.reverse_status[u.status] + '\n' + if 'B' in u.lists: + out += 'Mode: ' + 'blocked' + '\n' + if u.gid != None: + out += 'Group: ' + m.groups[u.gid] + '\n' + if u.realnick: + out += 'Real Nick: ' + u.realnick + '\n' + if u.homep: + out += 'Home phone: ' + u.homep + '\n' + if u.workp: + out += 'Work phone: ' + u.workp + '\n' + if u.mobilep: + out += 'Mobile phone: ' + u.mobilep + '\n' + + self.label = Label(self) + self.label.pack(side = TOP, fill = BOTH, expand = 1) + self.label.config(justify = LEFT) + self.label.config(text = out) + + +def redraw_main(): + "Redraws the main screen" + # sync the user list - FIXME: instead of redrawing, use the callbacks + # for status change notifications + nicks = [] + for i in m.users.keys(): + if m.users[i].status == 'FLN': + s = '[X] ' + elif m.users[i].status in ('NLN', 'IDL'): + s = '[ ] ' + else: + s = '[-] ' + if 'B' in m.users[i].lists: + s = '[!] ' + + s += m.users[i].nick + nicks.append(s) + nicks.sort() + mainlist.list.delete(0, END) + for i in nicks: + mainlist.list.insert(END, i) + + # update status + s = msnlib.reverse_status[m.status] + status.config(text = s) + + + +# +# callbacks +# + +def cb_msg(md, type, tid, params, sbd): + "Gets a message" + t = tid.split(' ') + email = t[0] + + # parse + lines = params.split('\n') + headers = {} + eoh = 0 + for i in lines: + # end of headers + if i == '\r': + break + tv = i.split(':', 1) + type = tv[0] + value = tv[1].strip() + headers[type] = value + eoh += 1 + eoh +=1 + + # ignore hotmail messages + if email == 'Hotmail': + return + + if email not in emwin.keys(): + emwin[email] = chatwindow(root, email) + + # typing notifications + if (headers.has_key('Content-Type') and + headers['Content-Type'] == 'text/x-msmsgscontrol'): + if not m.users[email].priv.has_key('typing'): + m.users[email].priv['typing'] = 1 + msg = now() + ' --- is typing\n' + emwin[email].append(msg, 'typing') + + # normal message + else: + if len(lines[eoh:]) > 1: + msg = now() + ' <<<\n\t' + msg += string.join(lines[eoh:], '\n\t') + msg = msg.replace('\r', '') + else: + msg = now() + ' <<< ' + lines[eoh] + '\n' + + if m.users[email].priv.has_key('typing'): + del(m.users[email].priv['typing']) + + emwin[email].append(msg, 'from') + root.bell() + + msncb.cb_msg(md, type, tid, params, sbd) +m.cb.msg = cb_msg + + + +# +# main +# + +# email - chatwindow dictionary +emwin = {} + +# gui init +root = Tk() +root.wm_title('msnlib') + +mainlist = userlist(root) +mainlist.pack(side = TOP, fill = BOTH, expand = 1) + +status = Label(root, text = "logging in...", bd=1, relief = SUNKEN, anchor = W) +status.pack(side = BOTTOM, fill = X, expand = 0) + +menu = mainmenu(root) +root.config(menu = menu) + +# initial update, to display at least something while we log in +root.update() + +# ask for username and password if not given in the command line +if len(sys.argv) < 3: + m.email = tkSimpleDialog.askstring("Username", + "Please insert your email") + if not m.email: + quit() + + m.pwd = tkSimpleDialog.askstring("Password", + "Please insert your password") + if not m.pwd: + quit() +else: + m.email = sys.argv[1] + m.pwd = sys.argv[2] + +m.email = m.email.strip() +m.pwd = m.pwd.strip() + +# the encoding is utf-8 because the text class uses unicode directly +m.encoding = 'utf-8' + +root.update() + +# login +try: + m.login() + m.sync() +except 'AuthError': + tkMessageBox.showerror("Login", "Error logging in: wrong password") + quit() + +# start as invisible +m.change_status('invisible') + + +# main loop +while 1: + fds = m.pollable() + infd = fds[0] + outfd = fds[1] + + try: + # both network and gui checks + fds = select.select(infd, outfd, [], 0) + root.update() + except KeyboardInterrupt: + quit() + except TclError: + quit() + + for i in fds[0] + fds[1]: + try: + m.read(i) + except ('SocketError', socket.error), err: + if i != m: + m.close(i) + else: + tkMessageBox.showwarning("Warning", + "Server disconnected us - you " + + "probably logged in somewhere else") + quit() + + # always redraw after a network event + redraw_main() + + # sleep a bit so we don't take over the cpu + time.sleep(0.05) + + diff --git a/utils/vicl b/utils/vicl new file mode 100644 index 0000000..2a87f61 --- /dev/null +++ b/utils/vicl @@ -0,0 +1,19 @@ +#!/bin/bash + +# yeah i work as root =) +DIR=/root/devel/msn/msnlib/cur + +cd $DIR +cd doc +cp Changelog Changelog~ + +DATE=`date '+%d %h %y %H.%M.%S'` +echo "$DATE - Alberto <albertogli@telpin.com.ar>" > Changelog +echo " * " >> Changelog +echo >> Changelog + +cat Changelog~ >> Changelog + +vi Changelog + +rm Changelog~