pax_global_header00006660000000000000000000000064115636106270014521gustar00rootroot0000000000000052 comment=8542fd0b515ea0e6f8d5d457c350f1c504556067 msnlib-3.8/000077500000000000000000000000001156361062700126575ustar00rootroot00000000000000msnlib-3.8/.gitignore000066400000000000000000000000141156361062700146420ustar00rootroot00000000000000*.pyc *.pyo msnlib-3.8/INSTALL.txt000066400000000000000000000015301156361062700145250ustar00rootroot00000000000000 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 albertito@blitiri.com.ar. msnlib-3.8/README000066400000000000000000000027771156361062700135540ustar00rootroot00000000000000 msnlib Alberto Bertogli (albertito@blitiri.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 'climm' (formerly 'micq', http://www.climm.org/), which is a great ICQ client. If you're looking for a good messaging system, forget about MSN 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 stuck with messenger for whatever reason, 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 a few methods to login, change some info and status, and send messages; all the rest is driven by an asynchronous callback scheme registered at runtime which can be changed on the fly. If you're interested, 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 albertito@blitiri.com.ar. Thanks, Alberto msnlib-3.8/doc/000077500000000000000000000000001156361062700134245ustar00rootroot00000000000000msnlib-3.8/doc/CodingStyle000066400000000000000000000005641156361062700156000ustar00rootroot00000000000000 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. msnlib-3.8/doc/FAQ000066400000000000000000000112471156361062700137630ustar00rootroot00000000000000 * 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). msnlib-3.8/doc/LICENSE000066400000000000000000000021601156361062700144300ustar00rootroot00000000000000 msnlib is licensed under the MIT license, as reproduced below. -------- Copyright (c) 2011 Alberto Bertogli Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. msnlib-3.8/doc/TODO000066400000000000000000000027301156361062700141160ustar00rootroot00000000000000 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 msnlib-3.8/doc/commands000066400000000000000000000057331156361062700151600ustar00rootroot00000000000000 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. msnlib-3.8/doc/dependencies000066400000000000000000000002751156361062700160010ustar00rootroot00000000000000 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. msnlib-3.8/doc/icons000066400000000000000000000011071156361062700144610ustar00rootroot00000000000000(#) 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 msnlib-3.8/doc/log_format000066400000000000000000000024541156361062700155050ustar00rootroot00000000000000 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. msnlib-3.8/doc/msg_states000066400000000000000000000023641156361062700155250ustar00rootroot00000000000000 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 msnlib-3.8/doc/msncd_protocol000066400000000000000000000070171156361062700164010ustar00rootroot00000000000000This 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. msnlib-3.8/doc/portability000066400000000000000000000035131156361062700157130ustar00rootroot00000000000000 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 msnlib-3.8/doc/profiles000066400000000000000000000006131156361062700151720ustar00rootroot00000000000000 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. msnlib-3.8/doc/reporting_bugs000066400000000000000000000025421156361062700164030ustar00rootroot00000000000000 How to report bugs ------------------ If you think you've got a bug (or you are sure about it =), please report it to albertito@blitiri.com.ar; 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. msnlib-3.8/install000077500000000000000000000012421156361062700142520ustar00rootroot00000000000000#!/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.txt $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.txt file to see the next step" echo msnlib-3.8/msn000077500000000000000000001437211156361062700134120ustar00rootroot00000000000000#!/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 albertito@blitiri.com.ar. You can find more information, and the package itself, at http://blitiri.com.ar/p/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 wn Prints your entire contact list, including real nicks wr Prints your reverse contact list wd Prints the differences between your forward and reverse lists e Prints your online contacts ee Prints your online contacts, including email addresses eg Prints your online contacts with the groups en Prints your online contacts, including real nicks 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" or shows own nick 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']() # command list for tab-completion purposes command_list = [ 'a', 'add', 'block', 'close', 'color', 'config', 'del', 'e', 'ee', 'eg', 'en', 'g', 'gadd', 'gdel', 'green', 'h', 'help', 'info', 'invite', 'lignore', 'luignore', 'm', 'nick', 'privacy', 'q', 'r', 'ren', 'status', 'unblock', 'w', 'wn', 'wd', 'wr', 'ww' ] # # 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, include_realnicks = 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 include_realnicks: printl(' (%s)' % (u.realnick), bold = hl) if 'B' in u.lists: printl(' [!]') if email not in md.reverse.keys(): printl(' [X]') printl('\n') def print_diff(md): "Prints the differences between forward and reverse lists" fwdl = md.users.keys() fwdl.sort() revl = md.reverse.keys() revl.sort() printl("People you have that don't have you:\n", bold = 1) for email in fwdl: if email not in revl: user = md.users[email] printl(" %s (%s)\n" % (user.nick, email)) printl('\n') printl("People you don't have that have you:\n", bold = 1) for email in revl: if email not in fwdl: user = md.reverse[email] printl(" %s (%s)\n" % (user.nick, email)) 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(' [!]') if email not in md.reverse.keys(): printl(' [X]') 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 findemailnick(email, begin): """Check if the email/nick of the given user begins with the given beginning. Returns 1 if it matches the nick, or 2 if it matches the email.""" if m.users[email].nick.find(begin) == 0: return 1 elif email.find(begin) == 0: return 2 return 0 # global variable for matchemail() start_from = 0 def matchemail(begin, only_online = 0, start = 0): """"Returns a matching email/nick for the given beginning; it avoids beginnings with spaces. If only_online is equal to 1 searchs only for not offline users or users with an sbd. If start=1 it iterates the last match and do a cyclical search.""" global start_from if ' ' in begin: return None emails = m.users.keys() if start_from >= len(emails): # the list has changed while iterating, reset start_from = 0 if start: pos = (start_from + 1) % len(emails) else: pos = start_from found = 0 while not found: # we made a complete loop without matches if start and pos == start_from: break elif pos == (start_from - 1) % len(emails): break #msnlib.debug("l: %d %s\n" % (pos, emails[pos])) current = emails[pos] if only_online and m.users[current].status == 'FLN': pos = (pos + 1) % len(emails) continue if findemailnick(emails[pos], begin): found = 1 break pos = (pos + 1) % len(emails) start_from = pos if found: if findemailnick(emails[pos], begin) == 1: # return the nick nick = email2nick(emails[pos]) if ' ' in nick: return emails[pos] return nick else: return emails[pos] else: start_from = 0 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' elif type == 'realnick': 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 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 = '' # vars to control the tabs completions: # match of commands matchc_last = 0 matchc_status = 0 matchc_root = '' # match of last send/received matchl_status = 0 # match of m command matchm_status = 0 matchm_root = '' # match of others arguments matchp_status = 0 matchp_root = '' # 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() global matchc_last global matchc_status global matchc_root global matchl_status global matchm_status global matchm_root global matchp_status global matchp_root for char in input: # decrease the flag of the tab completion of commands if matchc_status != 0: matchc_status = matchc_status - 1 # decrease the flag of the last received/last send completion elif matchl_status != 0: matchl_status = matchl_status - 1 # decrease the flag of the m completion elif matchm_status != 0: matchm_status = matchm_status - 1 # decrease the flag of the other arguments completion elif matchp_status != 0: matchp_status = matchp_status - 1 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 ord(char) == 23: # ^W inbuf = inbuf[:-1] inbuf = inbuf.rstrip() pos = inbuf.rfind(' ') if pos > 0: inbuf = inbuf[:pos].rstrip() + ' ' else: inbuf = '' redraw_cli() elif char == '\t': # tab p = inbuf.split() # 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 # FIXME: it fails if we haven't in our contact list # the person with we are talking 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 # in an empty buffer we fill with the last received or # the last sent if len(p) == 0: if mtolsent: inbuf = mtolsent matchl_status = 2 elif mtolrecv: inbuf = mtolrecv matchl_status = 2 else: inbuf = inbuf[:-1] beep() # if in the last cycle we have replaced with # mtolsent or mtolrecv we try to fill ciclical elif mtolsent and mtolrecv and matchl_status == 1: if inbuf.strip() == mtolrecv.strip(): inbuf = mtolsent matchl_status = 2 else: inbuf = mtolrecv matchl_status = 2 # temporarily if not mtolsent or mtolrecv we beep # FIXME it do nothing if there's only mtolrecv and # it changes between two tabs elif matchl_status == 1: # it avoids that in the next iteration the # empty buffer completion will be taked for # an m completion matchl_status = 2 inbuf = inbuf[:-1] beep() # we have something that is neither mtolsent or # mtolrecv, if is the m command we try to find a # matching email/nick elif p[0] == 'm' and len(p) == 2: begin = p[1] if matchm_status == 1: begin = matchm_root # we try to match with onlines contacts # and contacts with sbd email = matchemail(begin, 1, matchm_status) if not email: inbuf = inbuf[:-1] beep() else: matchm_root = begin matchm_status = 2 inbuf = 'm ' + email + ' ' # if there's an only word buffer we try to match # with one of the commands elif len(p) == 1: # if it's the 2nd tab we build a ciclical # matching; if not, we remember the last match if matchc_status == 1: p[0] = matchc_root matchc_last = matchc_last + 1 found = 1 while found or matchc_last != len(command_list): if matchc_last == len(command_list): matchc_last = 0 found = 0 continue elif command_list[matchc_last].find(p[0]) == 0: matchc_status = 2 matchc_root = p[0] break matchc_last = matchc_last + 1 if matchc_last == len(command_list): inbuf = inbuf[:-1] beep() else: inbuf = command_list[matchc_last] + ' ' else: pn = p[len(p) - 1] if matchp_status == 1: pn = matchp_root # we try to match with all of contacts email = matchemail(pn, 0, matchp_status) if not email: inbuf = inbuf[:-1] beep() else: matchp_root = pn matchp_status = 2 inbuf = inbuf.rstrip() pos = inbuf.rfind(' ') inbuf = inbuf[:pos] + ' ' + 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: # Never allow lines longer than 1500, since # that's the max for a single message. # Actually this calculates based on the whole # buffer and not on just the message, but the # code is nicer and 16 bytes won't make a # difference. if len(inbuf) > 1500: inbuf = inbuf[:1500] beep() redraw_cli() else: 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 == 'wn': # list, include real nicks print_list(m, include_realnicks = 1) elif cmd == 'wr': # reverse list print_list(m, userlist = m.reverse, include_emails = 1) elif cmd == 'wd': # difference list print_diff(m) 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 == 'en': print_list(m, only_online = 1, include_realnicks = 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 = '' 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(None, 1) if len(p) < 2: return 'Error parsing command' email = nick2email(p[0]) if not email: return 'Unkown nick (%s)' % p[0] newnick = p[1].strip() u = m.users[email] m.userren(email, newnick) 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': # show or change our nick if len(params) < 1: return "Your current nick is: %s" % m.nick 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] rnick = urllib.unquote(t[2]) 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(' is ', c.magenta) printl('%s' % status, c.magenta, 1) log_msg(email, 'status', status) if config["show realnick changes"]: printl(' with realnick ', c.magenta) printl('%s' % rnick, c.magenta, 1) log_msg(email, 'realnick', rnick) printl('\n') 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] if len(params) > 1: rnick = urllib.unquote(t[1]) else: rnick = '' nick = md.users[email].nick realnick = md.users[email].realnick ctime = time.strftime('%I:%M:%S%p', now()) if tid != md.users[email].status: printl('\r%s ' % ctime, c.blue) printl(nick, c.blue, 1) printl(' changed status to ', c.magenta) printl('%s' % status, c.magenta, 1) log_msg(email, 'status', status) # if we don't know the realnick yet, include it in the same line if not realnick and config["show realnick changes"]: printl(' with realnick ', c.magenta) printl('%s' % rnick, c.magenta, 1) log_msg(email, 'realnick', rnick) printl("\n") if realnick and rnick and realnick != rnick \ and config["show realnick changes"]: printl("\r%s " % ctime, c.blue) printl(nick, c.blue, 1) printl(' changed the realnick to ', c.magenta) printl('%s\n' % rnick, c.magenta, 1) log_msg(email, 'realnick', rnick) 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 elif headers.has_key('Content-Type') and headers['Content-Type'] == 'text/x-keepalive': # ignore kopete's keepalive messages 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_rea(md, type, tid, params): t = params.split(' ') email = t[1] nick = urllib.unquote(t[2]) if email != md.email: out = '\r' + c.blue + c.bold + email + ' ' + c.magenta \ + 'has been renamed to ' + c.bold + nick + '\n' printl(out) else: out = '\r' + c.magenta + 'Your nick has been changed to ' \ + c.bold + nick + '\n' printl(out) msncb.cb_rea(md, type, tid, params) m.cb.rea = cb_rea 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.8) *\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'].lower() 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: perror('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' # show realnick changes if not config.has_key('show realnick changes'): config['show realnick changes'] = 0 elif config['show realnick changes'] != 'yes': config['show realnick changes'] = 0 # 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 msnlib.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 (msnlib.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 (msnlib.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 msncb.XFRError, err: printl("\rXFR Error: %s\n" % str(err)) # always redraw after network event redraw_cli() msnlib-3.8/msncb.py000066400000000000000000000315561156361062700143450ustar00rootroot00000000000000 import urllib import socket # md5 is deprecated in favour of hashlib since python 2.5; hashlib was # introduced in python 2.5 try: from hashlib import md5 except ImportError: from md5 import md5 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. """ # 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.notice = cb_notice # notice notification 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 self.qng = cb_qng # pong! 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', } # # Possible exceptions # class CallbackMess (Exception): pass class SYNError (Exception): pass class XFRError (Exception): pass # # Callbacks # 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(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_notice(md, type, tid, params, sbd): "Get a notice" debug('NOTICE\n+++ %s\n\n' % str(params)) 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) def cb_qng(md, type, tid, params): "Get the response of a ping" debug('PONG!: answered by the server') msnlib-3.8/msnlib.py000066400000000000000000000504351156361062700145240ustar00rootroot00000000000000 import sys import string import socket import urllib """ MSN Messenger Client Library by Alberto Bertogli (albertito@blitiri.com.ar) """ # constants VERSION = 0x0308 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', } # Possible exceptions class SocketError (Exception): pass class VersionError (Exception): pass class NSError (Exception): pass class AuthError (Exception): pass def debug(s): sys.stderr.write('\r' + str(s) + '\n') sys.stderr.flush() def nickquote(nick): """Quotes a nick the way the server likes it: replacing spaces with '%20' but leaving extender characters alone, as they get sent UTF-8 encoded.""" nick = nick.replace(' ', '%20') return nick 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 '' % (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 '' % \ (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 '' % (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 = nickquote(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 = nickquote(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 userren(self, email, newnick): "Renames a user" newnick = nickquote(newnick) self._send('REA', email + ' ' + newnick) 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 = nickquote(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 = nickquote(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 ping(self): "Sends a ping to the server" try: self.fd.send('PNG\r\n') except: pass 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=0,' ahead += 'kpp=1,kv=5,ver=2.1.0173.1,' ahead += 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, 'Connect failed' 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 == 'QNG': self.cb.qng(self, type, tid, params) 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) elif type == 'NOT': mlen = int(tid) msg = self._recvmsg(mlen, nd.fd) self.cb.notice(self, type, "", 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 msnlib-3.8/msnrc.sample000066400000000000000000000043021156361062700152030ustar00rootroot00000000000000 # 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 # show changes in real nicks # the default is no show realnick changes = no # 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 msnlib-3.8/msnsetup000077500000000000000000000037571156361062700144770ustar00rootroot00000000000000#!/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 albertito@blitiri.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 necessarily) 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" msnlib-3.8/setup.py000066400000000000000000000004141156361062700143700ustar00rootroot00000000000000 from distutils.core import setup setup(name="msnlib", version="3.8", description="MSN Messenger Library and Client", author="Alberto Bertogli", author_email="albertogli@blitiri.com.ar", url="http://blitiri.com.ar/p/msnlib/", py_modules=['msnlib', 'msncb'], ) msnlib-3.8/utils/000077500000000000000000000000001156361062700140175ustar00rootroot00000000000000msnlib-3.8/utils/hmerge000077500000000000000000000040261156361062700152160ustar00rootroot00000000000000#!/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 (albertito@blitiri.com.ar), 02/Jun/2003 """ 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 msnlib-3.8/utils/msnbot000077500000000000000000000045111156361062700152500ustar00rootroot00000000000000#!/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, []) except: quit() for i in fds[0] + fds[1]: # see msnlib.msnd.pollable.__doc__ try: m.read(i) except (msnlib.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() msnlib-3.8/utils/msncd000077500000000000000000000221441156361062700150540ustar00rootroot00000000000000#!/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 msnlib.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 (msnlib.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 msnlib.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 (msnlib.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 (msnlib.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) msnlib-3.8/utils/msnlog.vim000066400000000000000000000014011156361062700160270ustar00rootroot00000000000000 " Vim syntax file for msnlib logfiles " Alberto (albertito@blitiri.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 mlogRnick "--- .*$" syntax match mlogDate "^../.../.... ..:..:.." hi mlogDate ctermfg=blue hi mlogIMsg ctermfg=green hi mlogOMsg ctermfg=cyan hi mlogStatus ctermfg=yellow hi mlogMchat ctermfg=yellow hi mlogRnick ctermfg=yellow hi mlogMultiStr ctermfg=magenta msnlib-3.8/utils/msnsbot000077500000000000000000000130451156361062700154350ustar00rootroot00000000000000#!/usr/bin/env python """ A simple scriptable msn bot. Can use gtalkbot plugins. ----------------------------- Alberto Bertogli albertito@blitiri.com.ar """ import sys import os.path import time import select import socket import thread import msnlib import msncb # null debug output msnlib.debug = lambda s: '' msncb.debug = lambda s: '' # # Our generic callbacks, used internally # # They depend on the md having a ._botobj reference to the bot object. Not so # nice, but simple enough. def generic_cb_msg(md, type, tid, params, sbd): t = tid.split() email = t[0] if email == 'Hotmail': return lines = params.split('\n') headers = {} eoh = 1 for i in lines: if i == '\r': break t, v = i.split(':', 1) headers[t] = v eoh += 1 if headers.get('Content-Type', '') == 'text/x-msmsgscontrol': # typing, ignore return md._botobj._handle_msg(email, headers, lines[eoh:]) msncb.cb_msg(md, type, tid, params, sbd) # # The bot itself # class bot: def __init__(self, email, passwd, userdict = None): self.email = email self.passwd = passwd self.msg_handlers = [] if userdict: self.userdict = userdict else: self.userdict = {} def _setup(self): self.m = msnlib.msnd() self.m.cb = msncb.cb() self.m.email = self.email self.m.pwd = self.passwd # used by the generic callbacks self.m._botobj = self # generic callbacks self.m.cb.msg = generic_cb_msg def login(self, status = 'online'): "Logs into the MSN network" self._setup() self.m.login() self.m.sync() # mini loop so we are sure we get the entire list before going # on with the normal stuff while self.m.lst_total != self.m.syn_total: infd, outfd = self.get_pollable_fds() fds = select.select(infd, outfd, [], None) for i in fds[0] + fds[1]: self.m.read(i) self.change_status(status) self._check_users() def close(self): self.m.disconnect() def reconnect(self): "Reconnects to the MSN network" self._setup() self.login(self.status) def change_status(self, status): "Changes the status" self.status = status self.m.change_status(status) def _check_users(self): # add everyone in the userlist if they're not already in our # roster for email in self.userdict.keys(): if email not in self.m.users: self.m.useradd(email, email) def get_pollable_fds(self): "Returns pollable fds, used for network pooling" return self.m.pollable() def loop(self): "Simple, exclusive network loop" while 1: infd, outfd = self.get_pollable_fds() fds = select.select(infd, outfd, [], 1) for i in fds[0] + fds[1]: try: self.m.read(i) except (msnlib.SocketError, socket.error), err: traceback.print_last() if i != self.m: # the user closed a connection m.close(i) else: # main socket closed return def register_msg_handler(self, f): "Registers a message handler" self.msg_handlers.append(f) def _handle_msg(self, email, header, msg): if email not in self.userdict: self.m.sendmsg(email, "Who are you?") return reply = [] for f in self.msg_handlers: r = f(email, self.userdict[email], header, msg) if r: reply.append(r) if reply: self.m.sendmsg(email, '\r\n'.join(reply)) # # Message handlers # def sample_msg_handler(email, info, header, msg): return "Echo!\n" + '\n'.join(msg) # gtalkbot-compatible message handler class gtalkbot_msg_handler: def __init__(self, path): self.plugins = [] sys.path.insert(0, path) for f in os.listdir(path): if f.endswith('.py'): root, ext = os.path.splitext(f) self.plugins.append(__import__(root)) sys.path.pop(0) self.verbs = {} for p in self.plugins: for v in p.Verbs(): if v not in self.verbs: self.verbs[v] = [] self.verbs[v].append(p) self.authenticated_users = [] def handle_msg(self, email, info, header, msg): # XXX: this only handles the first line vl = msg[0].split(None, 1) if not vl: return if len(vl) < 2: verb, line = vl[0], '' else: verb, line = vl if email not in self.authenticated_users and verb != 'auth': return 'You need to authenticate\n' \ + 'Use: auth ' if verb == 'auth': if line != info: return 'Wrong password, try again' self.authenticated_users.append(email) return 'Welcome!' elif verb == 'help': if not line: return 'Use: help ' reply = [] for p in self.plugins: if 'Help' not in dir(p): continue r = p.Help(line) if r: reply.append(r) if reply: return '\r\n'.join(reply) else: return 'Sorry, no help for ' + line elif verb in self.verbs: reply = [] for p in self.verbs[verb]: r = p.Command(verb, line) if r: reply.append(r) if reply: return '\r\n'.join(reply) else: return 'Unknown verb' else: return 'Unknown verb' def __call__(self, email, info, header, msg): return self.handle_msg(email, info, header, msg) def main(): # get the login email and password from the parameters try: email = sys.argv[1] passwd = sys.argv[2] userlistfname = sys.argv[3] pluginspath = sys.argv[4] except: print "Use: msnsbot email password userlist pluginspath" sys.exit(1) # create a user dictionary with email as key, and anything else as # value (as a single string) userlist = [ line.strip().split(None, 1) \ for line in open(userlistfname) ] userdict = dict( [ x for x in userlist if len(x) > 1 ] ) b = bot(email, passwd, userdict) #b.register_msg_handler(sample_msg_handler) b.register_msg_handler(gtalkbot_msg_handler(pluginspath)) b.login() b.loop() if __name__ == '__main__': main() msnlib-3.8/utils/msntk000077500000000000000000000256161156361062700151130ustar00rootroot00000000000000#!/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 albertito@blitiri.com.ar. You can find more information, and the package itself, at http://blitiri.com.ar/p/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("", 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('', 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 msnlib.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 (msnlib.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)