chiark / gitweb /
new trouts from rejs
[irc.git] / commands.py
old mode 100644 (file)
new mode 100755 (executable)
index 6f6d1f7..3876987
 # Part of Acrobat.
-import string, cPickle, random, urllib, sys
+import string, cPickle, random, urllib, sys, time, re, os, twitter, subprocess, datetime
 from irclib import irc_lower, nm_to_n
 
 # query karma
-def karmaq(bot, cmd, nick, conn, public):
-    # in public
-    if public == 1:
-        try:
-            if bot.karma.dict.has_key(cmd.split()[1]):
-                conn.privmsg(bot.channel, "%s has karma %s."
-                             %(cmd.split()[1],
-                                   bot.karma.dict[cmd.split()[1]]))
-            else:
-                conn.privmsg(bot.channel, "%s has no karma set." %
-                             cmd.split()[1])
-        except IndexError:
-            conn.privmsg(bot.channel, "I have karma on %s items." %
-                         len(bot.karma.dict.keys()))
-    # in private
-    else:
-        try:
-            if bot.karma.dict.has_key(cmd.split()[1]):
-                conn.notice(nick, "%s has karma %s." %
-                            (cmd.split()[1],
-                             bot.karma.dict[cmd.split()[1]]))
-            else:
-                conn.notice(nick, "I have karma on %s items." %
-                            len(bot.karma.dict.keys()))
-        except IndexError:
-            conn.notice(nick, "I have karma on %s items." %
-                        len(bot.karma.dict.keys()))
-# query bot status
-def infoq(bot, cmd, nick, conn, public):
-    if public == 1:
-        conn.privmsg(bot.channel,
-                     "I am Acrobat %s, on %s, as nick %s." %
-                    (bot.revision.split()[1], bot.channel, conn.get_nickname()))
-        conn.privmsg(bot.channel,
-                     "My owner is %s; I have karma on %s items." %
-                     (bot.owner, len(bot.karma.dict.keys())))
+def karmaq(bot, cmd, nick, conn, public, karma):
+    try:
+       item=cmd.split()[1].lower()
+    except IndexError:
+       item=None
+    if item==None:
+       bot.automsg(public,nick,"I have karma on %s items." %
+                        len(karma.keys()))
+    elif karma.has_key(item):
+        bot.automsg(public,nick,"%s has karma %s."
+                     %(item,karma[item]))
     else:
-        conn.notice(nick, "I am Acrobat %s, on %s, as nick %s." %
-                    (bot.revision.split()[1], bot.channel, conn.get_nickname()))
-        conn.notice(nick, "My owner is %s; I have karma on %s items." %
-                    (bot.owner, len(bot.karma.dict.keys())))
+        bot.automsg(public,nick, "%s has no karma set." % item)
 
-# trout someone
-def troutq(bot, cmd, nick, conn, public):
+# delete karma
+def karmadelq(bot, cmd, nick, conn, public, karma):
     try:
-        target = string.join(cmd.split()[1:])
-        me = bot.connection.get_nickname()
-        trout_msg = random.choice(bot.trouts)
-        # The bot won't trout itself;
-        if irc_lower(me) == irc_lower(target):
-            target = nick
-        conn.action(bot.channel, trout_msg % target)
-        if public == 0:
-            if random.random() <= bot.config.selftroutrisk:
-                conn.action(bot.channel,
-                 "notes %s is conducting a whispering campaign." % nick)
+       item=cmd.split()[1].lower()
     except IndexError:
-        conn.notice(nick, "Who do you wish me to trout?")
+       conn.notice(nick, "What should I delete?")
+       return
+    if nick != bot.owner:
+       conn.notice(nick, "You are not my owner.")
+       return
+    if karma.has_key(item):
+       del karma[item]
+       conn.notice(nick, "Item %s deleted."%item)
+    else:
+       conn.notice(nick, "There is no karma stored for %s."%item)
+
+# help - provides the URL of the help file
+def helpq(bot, cmd, nick, conn, public):
+    bot.automsg(public,nick,
+                "For help see http://www.chiark.greenend.org.uk/~matthewv/irc/servus.html")
+
+
+# query bot status
+def infoq(bot, cmd, nick, conn, public, karma):
+    bot.automsg(public,nick,
+       ("I am Acrobat %s, on %s, as nick %s.  "+
+       "My owner is %s; I have karma on %s items.") %
+       (bot.revision.split()[1], bot.channel, conn.get_nickname(),
+        bot.owner, len(karma.keys())))
+
+# Check on fish stocks
+def fish_quota(pond):
+    if pond.DoS:
+        if time.time()>=pond.quotatime:
+            pond.DoS=0
+        else:
+            return
+    if (time.time()-pond.quotatime)>pond.fish_time_inc:
+        pond.cur_fish+=(((time.time()-pond.quotatime)
+                         /pond.fish_time_inc)*pond.fish_inc)
+        if pond.cur_fish>pond.max_fish:
+            pond.cur_fish=pond.max_fish
+        pond.quotatime=time.time()
+
+# List of things the bot might be called to work round the self-trouting code
+synonyms=["itself","the bot","themself"]
+
+# trout someone, or flirt with them
+def troutq(bot, cmd, nick, conn, public, cfg):
+    fishlist=cfg[0]
+    selftrout=cfg[1]
+    quietmsg=cfg[2]
+    notargetmsg=cfg[3]
+    nofishmsg=cfg[4]
+    fishpond=cfg[5]
+    selftroutchance=cfg[6]
+
+    fish_quota(fishpond)
+    if fishpond.DoS:
+        conn.notice(nick, quietmsg%fishpond.Boring_Git)
+       return
+    if fishpond.cur_fish<=0:
+        conn.notice(nick, nofishmsg)
+       return
+    target = string.join(cmd.split()[1:])
+    if len(target)==0:
+       conn.notice(nick, notargetmsg)
+       return
+    me = bot.connection.get_nickname()
+    trout_msg = random.choice(fishlist)
+    fishpond.last=trout_msg
+    # The bot won't trout or flirt with itself;
+    if irc_lower(me) == irc_lower(target) or irc_lower(target) in synonyms:
+        target = nick
+    # There's a chance the game may be given away if the request was not
+    # public...
+    if not public:
+       if random.random()<=selftroutchance:
+           trout_msg=trout_msg+(selftrout%nick)
+
+    conn.action(bot.channel, trout_msg % target)
+    fishpond.cur_fish-=1
+
+# slash a pair
+def slashq(bot, cmd, nick, conn, public, cfg):
+    fishlist=cfg[0]
+    selfslash=cfg[1]
+    quietmsg=cfg[2]
+    notargetmsg=cfg[3]
+    nofishmsg=cfg[4]
+    fishpond=cfg[5]
+    selfslashchance=cfg[6]
+
+    fish_quota(fishpond)
+    if fishpond.DoS:
+        conn.notice(nick, quietmsg%fishpond.Boring_Git)
+       return
+    if fishpond.cur_fish<=0:
+        conn.notice(nick, nofishmsg)
+       return
+    target = string.join(cmd.split()[1:])
+    #who = cmd.split()[1:]
+    who = ' '.join(cmd.split()[1:]).split(' / ')
+    if len(who) < 2:
+       conn.notice(nick, "it takes two to tango!")
+       return
+    elif len(who) > 2:
+       conn.notice(nick, "we'll have none of that round here")
+       return
+    me = bot.connection.get_nickname()
+    slash_msg = random.choice(fishlist)
+    fishpond.last=slash_msg
+    # The bot won't slash people with themselves
+    if irc_lower(who[0]) == irc_lower(who[1]):
+       conn.notice(nick, "oooooh no missus!")
+       return
+    # The bot won't slash with itself, instead slashing the requester
+    for n in [0,1]:
+       if irc_lower(me) == irc_lower(who[n]) or irc_lower(who[n]) in synonyms:
+           who[n] = nick
+    # Perhaps someone asked to slash themselves with the bot then we get
+    if irc_lower(who[0]) == irc_lower(who[1]):
+       conn.notice(nick, "you wish!")
+       return
+    # There's a chance the game may be given away if the request was not
+    # public...
+    if not public:
+       if random.random()<=selfslashchance:
+           slash_msg=slash_msg+(selfslash%nick)
+
+    conn.action(bot.channel, slash_msg % (who[0], who[1]))
+    fishpond.cur_fish-=1
+
+#query units
+def unitq(bot, cmd, nick, conn, public):
+    args = ' '.join(cmd.split()[1:]).split(' as ')
+    if len(args) != 2:
+        args = ' '.join(cmd.split()[1:]).split(' / ')
+        if len(args) != 2:
+            conn.notice(nick, "syntax: units arg1 as arg2")
+            return
+    if args[1]=='?':
+        sin,sout=os.popen4(["units","--verbose","--",args[0]],"r")
+    else:
+        sin,sout=os.popen4(["units","--verbose","--",args[0],args[1]],"r")
+    sin.close()
+    res=sout.readlines()
+    #popen2 doesn't clean up the child properly. Do this by hand
+    child=os.wait()
+    if os.WEXITSTATUS(child[1])==0:
+        bot.automsg(public,nick,res[0].strip())
+    else:
+        conn.notice(nick,'; '.join(map(lambda x: x.strip(),res)))
+
+# Shut up trouting for a minute
+def nofishq(bot, cmd, nick, conn, public, fish):
+    fish.cur_fish=0
+    fish.DoS=1
+    fish.Boring_Git=nick
+    fish.quotatime=time.time()
+    fish.quotatime+=fish.nofish_time
+    conn.notice(nick, "Fish stocks depleted, as you wish.")
 
 # rehash bot config
 def reloadq(bot, cmd, nick, conn, public):
-    if irc_lower(nick) == irc_lower(bot.owner):
+    if not public and irc_lower(nick) == irc_lower(bot.owner):
         try:
             reload(bot.config)
-            bot.trouts = bot.config.trouts
-            conn.privmsg(nick, "Config reloaded.")
+            conn.notice(nick, "Config reloaded.")
         except ImportError:
             conn.notice(nick, "Config reloading failed!")
     else:
-        conn.notice(nick, "This command can only be invoked by my owner.")
+        bot.automsg(public,nick,
+               "Configuration can only be reloaded by my owner, by /msg.")
 
 # quit irc
 def quitq(bot, cmd, nick, conn, public):
     if irc_lower(nick) == irc_lower(bot.owner):
-        try:
-            f = open(bot.karmafilename, "w")
-            cPickle.dump(bot.karma, f)
-            f.close()
-        except IOError:
-            sys.stderr.write("Problems dumping karma: probably lost :(")
         bot.die(msg = "I have been chosen!")
-    elif public == 1:
-        conn.privmsg(nick, "Such aggression in public!")
+    elif public:
+        conn.notice(nick, "Such aggression in public!")
     else:
         conn.notice(nick, "You're not my owner.")
 
 # google for something
 def googleq(bot, cmd, nick, conn, public):
     cmdrest = string.join(cmd.split()[1:])
-    #sys.stderr.write(conn)
     # "I'm Feeling Lucky" rather than try and parse the html
     targ = ("http://www.google.com/search?q=%s&btnI=I'm+Feeling+Lucky"
             % urllib.quote_plus(cmdrest))
@@ -100,40 +211,314 @@ def googleq(bot, cmd, nick, conn, public):
         # get redirected and grab the resulting url for returning
         gsearch = urllib.urlopen(targ).geturl()
         if gsearch != targ: # we've found something
-            if public == 0:
-                conn.notice(nick, str(gsearch))
-            else: # we haven't found anything.
-                conn.privmsg(nick, str(gsearch))
+            bot.automsg(public,nick,str(gsearch))
+        else: # we haven't found anything.
+           bot.automsg(public,nick,"No pages found.")
+    except IOError: # if the connection times out. This blocks. :(
+       bot.automsg(public,nick,"The web's broken. Waah!")
+
+# Look up the definition of something using google
+def defineq(bot, cmd, nick, conn, public):
+    #this doesn't work any more
+    bot.automsg(public,nick,"'define' is broken because google are bastards :(")
+    return
+    cmdrest = string.join(cmd.split()[1:])
+    targ = ("http://www.google.co.uk/search?q=define%%3A%s&ie=utf-8&oe=utf-8"
+            % urllib.quote_plus(cmdrest))
+    try:
+        # Just slurp everything into a string
+        defnpage = urllib.urlopen(targ).read()
+        # For definitions we really do have to parse the HTML, sadly.
+        # This is of course going to be a bit fragile. We first look for
+        # 'Definitions of %s on the Web' -- if this isn't present we
+        # assume we have the 'no definitions found page'.
+        # The first defn starts after the following <p> tag, but as the
+        # first <li> in a <ul type="disc" class=std>
+        # Following that we assume that each definition is all the non-markup
+        # before a <br> tag. Currently we just dump out the first definition.
+        match = re.search(r"Definitions of <b>.*?</b> on the Web.*?<li>\s*([^>]*)((<br>)|(<li>))",defnpage,re.MULTILINE)
+        if match == None:
+           bot.automsg(public,nick,"Some things defy definition.")
         else:
-            if public == 0:
-                conn.notice(nick, "No pages found.")
-            else:
-                conn.privmsg(nick, "No pages found.")
+           # We assume google has truncated the definition for us so this
+           # won't flood the channel with text...
+           defn = " ".join(match.group(1).split("\n"))
+           bot.automsg(public,nick,defn)
     except IOError: # if the connection times out. This blocks. :(
-        if public == 0:
-            conn.notice(nick, "The web's broken. Waah!")
+         bot.automsg(public,nick,"The web's broken. Waah!")
+
+# Look up a currency conversion via xe.com
+def currencyq(bot, cmd, nick, conn, public):
+    args = ' '.join(cmd.split()[1:]).split(' as ')
+    if len(args) != 2 or len(args[0]) != 3 or len(args[1]) != 3:
+        conn.notice(nick, "syntax: currency arg1 as arg2")
+        return
+    targ = ("http://www.xe.com/ucc/convert.cgi?From=%s&To=%s" % (args[0], args[1]))
+    try:
+        currencypage = urllib.urlopen(targ).read()
+        match = re.search(r"(1&nbsp;%s&nbsp;=&nbsp;[\d\.]+&nbsp;%s)" % (args[0].upper(),args[1].upper()),currencypage,re.MULTILINE)
+        if match == None:
+            bot.automsg(public,nick,"Dear Chief Secretary, there is no money.")
         else:
-            conn.privmsg(nick, "The web's broken. Waah!")
+            conversion = match.group(1);
+            conversion = conversion.replace('&nbsp;',' ');
+            bot.automsg(public,nick,conversion + " (from xe.com)")
+    except IOError: # if the connection times out. This blocks. :(
+        bot.automsg(public,nick,"The web's broken. Waah!")
+                 
+
+### extract the commit message and timestamp for commit 
+def __getcommitinfo(commit):
+    cmd=["git","log","-n","1","--pretty=format:%ct|%s",commit]
+    x=subprocess.Popen(cmd,
+                       stdout=subprocess.PIPE,stderr=subprocess.PIPE)
+    out,err=x.communicate()
+
+    if len(err):
+       return(err)
+
+    ts,mes=out.split('|')
+    when=datetime.date.fromtimestamp(float(ts))
+    return mes.strip(), when
+
+###Return an array of commit messages and timestamps for lines in db that match what
+def __getcommits(db,keys,what):
+    ans=[]
+    for k in keys:
+        if what in k:
+           ret=__getcommitinfo(db[k])
+           if len(ret)==1: #error message
+               return ["Error message from git blame: %s" % ret]
+           else:
+               ans.append( (k,ret[0],ret[1]) )
+    return ans
+
+###search all three databases for what
+def __getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,what):
+    if what.strip()=="":
+        return []
+    tans=__getcommits(tdb,tdbk,what)
+    fans=__getcommits(fdb,fdbk,what)
+    sans=__getcommits(sdb,sdbk,what)
+    return tans+fans+sans
+
+def blameq(bot,cmd,nick,conn,public,fish,tdb,tdbk,fdb,fdbk,sdb,sdbk):
+    clist=cmd.split()
+    if len(clist) < 2:
+       bot.automsg(public,nick,"Who or what do you want to blame?")
+       return
+    cwhat=' '.join(clist[2:])
+    if clist[1]=="#last":
+       ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,fish.last)
+    elif clist[1]=="#trouts" or clist[1]=="#trout":
+       ans=__getcommits(tdb,tdbk,cwhat)
+    elif clist[1]=="#flirts" or clist[1]=="#flirt":
+       ans=__getcommits(fdb,fdbk,cwhat)
+    elif clist[1]=="#slashes" or clist[1]=="#slash":
+       ans=__getcommits(sdb,sdbk,cwhat)
+    else:
+       cwhat=' '.join(clist[1:])
+       ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,cwhat)
+    if len(ans)==0:
+       bot.automsg(public,nick,"No match found")
+    elif len(ans)==1:
+       if len(ans[0])==1:
+           bot.automsg(public,nick,ans[0])
+       else:
+           bot.automsg(public,nick,"Modified %s: %s" % (ans[0][2].isoformat(),ans[0][1]))
+    elif len(ans)>4:
+       bot.automsg(public,nick,"I found %d matches, which is too many. Please be more specific!" % (len(ans)) )
+    else:
+       for a in ans:
+           if len(a)==1:
+               bot.automsg(public,nick,a)
+           else:
+               bot.automsg(public,nick,"'%s' modified on %s: %s" % (a[0],a[2].isoformat(),a[1]))
 
 ### say to msg/channel            
 def sayq(bot, cmd, nick, conn, public):
     if irc_lower(nick) == irc_lower(bot.owner):
         conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
     else:
-        conn.privmsg(nick, "You're not my owner!")
+        if not public:
+           conn.notice(nick, "You're not my owner!")
 
 ### action to msg/channel
 def doq(bot, cmd, nick, conn, public):
     sys.stderr.write(irc_lower(bot.owner))
     sys.stderr.write(irc_lower(nick))
-    if public == 0:
+    if not public:
         if irc_lower(nick) == irc_lower(bot.owner):
             conn.action(bot.channel, string.join(cmd.split()[1:]))
         else:
-            conn.privmsg(nick, "You're not my owner!")
+            conn.notice(nick, "You're not my owner!")
 
 ###disconnect
 def disconnq(bot, cmd, nick, conn, public):
     if cmd == "disconnect": # hop off for 60s
         bot.disconnect(msg="Be right back.")
 
+### list keys of a dictionary
+def listkeysq(bot, cmd, nick, conn, public, dict, sort=False):
+    d=dict.keys()
+    if sort:
+        d.sort()
+    bot.automsg(public,nick,string.join(d))
+
+### rot13 text (yes, I could have typed out the letters....)
+### also "foo".encode('rot13') would have worked
+def rot13q(bot, cmd, nick, conn, public):
+    a=''.join(map(chr,range((ord('a')),(ord('z')+1))))
+    b=a[13:]+a[:13]
+    trans=string.maketrans(a+a.upper(),b+b.upper())
+    conn.notice(nick, string.join(cmd.split()[1:]).translate(trans))
+
+### URL-tracking stuff
+
+### return a easy-to-read approximation of a time period
+def nicetime(tempus):
+  if (tempus<120):
+    tm="%d seconds ago"%int(tempus)
+  elif (tempus<7200):
+    tm="%d minutes ago"%int(tempus/60)
+  if (tempus>7200):
+    tm="%d hours ago"%int(tempus/3600)
+  return tm
+
+### class to store URL data
+class UrlLog:
+    "contains meta-data about a URL seen on-channel"
+    def __init__(self,url,nick):
+        self.nick=nick
+        self.url=url
+        self.first=time.time()
+        self.count=1
+        self.lastseen=time.time()
+        self.lastasked=time.time()
+    def recenttime(self):
+        return max(self.lastseen,self.lastasked)
+    def firstmen(self):
+        return nicetime(time.time()-self.first)
+    def urltype(self):
+        z=min(len(urlcomplaints)-1, self.count-1)
+        return urlcomplaints[z]
+
+#(?:) is a regexp that doesn't group        
+urlre = re.compile(r"((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
+hturlre= re.compile(r"(http)(s?://[^ ]+)( |$)")
+#matches \bre\:?\s+ before a regexp; (?i)==case insensitive match
+shibboleth = re.compile(r"(?i)\bre\:?\s+((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
+urlcomplaints = ["a contemporary","an interesting","a fascinating","an overused","a vastly overused"]
+
+### Deal with /msg bot url or ~url in channel
+def urlq(bot, cmd, nick, conn, public,urldb):
+  if (not urlre.search(cmd)):
+    bot.automsg(False,nick,"Please use 'url' only with http, https, nsfw, or nsfws URLs")
+    return
+
+  urlstring=urlre.search(cmd).group(1)
+  url=canonical_url(urlstring)
+  if (url in urldb):
+    T = urldb[url]
+    complaint="That's %s URL that was first mentioned %s by %s" % \
+               (T.urltype(),T.firstmen(),T.nick)
+    if (public):
+      complaint=complaint+". Furthermore it defeats the point of this command to use it other than via /msg."
+      T.count+=1
+    bot.automsg(False,nick,complaint)
+    T.lastasked=time.time()
+    #URL suppressed, so mention in #urls
+    if urlstring != cmd.split()[1]: #first argument to URL was not the url
+      conn.privmsg("#urls","%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
+    else:
+      conn.privmsg("#urls","(via %s) %s"%(nick," ".join(cmd.split()[1:])))
+  else:
+    if (public):
+      bot.automsg(False,nick,"That URL was unique. There is little point in using !url out loud; please use it via /msg")
+    else:
+      if urlstring != cmd.split()[1]: #first argument to URL was not the url
+        conn.privmsg(bot.channel,"%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
+      else:
+        conn.privmsg(bot.channel,"(via %s) %s"%(nick," ".join(cmd.split()[1:])))
+    urldb[url]=UrlLog(url,nick)
+
+### Deal with URLs spotted in channel
+def dourl(bot,conn,nick,command,urldb):
+  urlstring=urlre.search(command).group(1)
+  urlstring=canonical_url(urlstring)
+
+  if urlstring in urldb:
+    T=urldb[urlstring]
+    message="observes %s URL, first mentioned %s by %s" % \
+             (T.urltype(),T.firstmen(),T.nick)
+    if shibboleth.search(command)==None:
+        conn.action(bot.channel, message)
+    T.lastseen=time.time()
+    T.count+=1
+  else:
+    urldb[urlstring]=UrlLog(urlstring,nick)
+
+### Expire old urls
+def urlexpire(urldb,expire):
+    urls=urldb.keys()
+    for u in urls:
+        if time.time() - urldb[u].recenttime() > expire:
+            del urldb[u]
+
+# canonicalise BBC URLs (internal use only)
+def canonical_url(urlstring):
+  if "nsfw://" in urlstring or "nsfws://" in urlstring:
+      urlstring=urlstring.replace("nsfw","http",1)
+  if (urlstring.find("news.bbc.co.uk") != -1):
+    for middle in ("/low/","/mobile/"):
+      x = urlstring.find(middle)
+      if (x != -1):
+        urlstring.replace(middle,"/hi/")
+  return urlstring
+
+# automatically make nsfw urls for you and pass them on to url
+def nsfwq(bot,cmd,nick,conn,public,urldb):
+  if (not hturlre.search(cmd)):
+    bot.automsg(False,nick,"Please use 'nsfw' only with http or https URLs")
+    return
+  newcmd=hturlre.sub(nsfwify,cmd)
+  urlq(bot,newcmd,nick,conn,public,urldb)
+
+def nsfwify(match):
+    a,b,c=match.groups()
+    return 'nsfw'+b+c
+
+#get tweet text
+def twitterq(bot,cmd,nick,conn,public,twitapi):
+  
+  if (not urlre.search(cmd)):
+    bot.automsg(False,nick,"Please use 'twit' only with http URLs")
+    return
+
+  urlstring = urlre.search(cmd).group(1)
+  if (urlstring.find("twitter.com") !=-1):
+    stringout = getTweet(urlstring,twitapi)
+    bot.automsg(public, nick, stringout)
+  
+def getTweet(urlstring,twitapi):
+  parts = string.split(urlstring,'/')
+  tweetID = parts[-1]
+  try:
+    status = twitapi.GetStatus(tweetID)
+    #print status, type(status), status=={}
+    if status == {}:
+        return "twitapi.GetStatus returned nothing :-("
+    if status.user == None and status.text == None:
+        return "Empty status object returned :("
+    if status.user is not None:
+        tweeter_screen = status.user.screen_name.encode('UTF-8', 'replace')
+        tweeter_name = status.user.name.encode('UTF-8', 'replace')
+    else:
+        tweeter_screen = "[not returned]" ; tweeter_name = "[not returned]"
+    tweetText = status.text.encode('UTF-8', 'replace')
+    tweetText = tweetText.replace('\n',' ')
+    stringout = "tweet by %s (%s): %s" %(tweeter_screen,tweeter_name,tweetText)
+  except twitter.TwitterError:
+    terror = sys.exc_info()
+    stringout = "Twitter error: %s" % terror[1].__str__()
+  return stringout