1
from htmlentitydefs import name2codepoint as n2cp
2
from htmlentitydefs import codepoint2name as cp2n
3
import twitter
4
import re, time
5
6
####
7
# Module declarations
8
##
9
10
twitter_settings = { "password": None,
11
                     "username": None,
12
                     "channel": None,
13
                     "last_friend_update": 0,
14
                     "last_reply_update": 0,
15
                     "last_directed": 0}
16
17
twitter_requires = ['webhelpers']
18
19
do_twitter_command = { 'description': "Different twitter functionalities",
20
                       'long help': """\
21
A set of commands to handle twitter streams and tweeting from a IRC-channel via the bot.""",
22
                       'arguments': [("update <update string>", "Update twitter status (>70)"),
23
                                     ("subscribe <user> [<user2> [<user3..]]", "Subscribe to user on twitter (>80)"),
24
                                     ("unsubscribe <user> [<user2> [<user3..]]", "Unsubscribe to user on twitter (>80)"),
25
                                     ("get friends", "List all friends on twitter. (>70)"),
26
                                     ("get last [nr]", "List nr (default 1) of updates listed in the channel. Only works in private."),
27
                                     ("get queue size", "Show the number of posts in queue to be outputed to the channel. (>70)"),
28
                                     ("clear queue", "Remove all posts in queue to be outputed to the channel.(>70)")],
29
                       'public': True,
30
                       'level': 0}
31
32
twitter_alias = {'t': 'twitter update',
33
                 'tsub': 'twitter subscribe',
34
                 'tun': 'twitter unsubscribe',
35
                 'tl': 'twitter get friends'}
36
37
update_twitter_delay = 50
38
update_twitter_time_left = 0
39
40
##
41
# End of module declarations
42
####
43
44
# If an update from twitter.com fails twitter_timeout is increased and lowered 1 each
45
# update. If twitter_timeout >0 nothing is done on update.
46
twitter_timeout = 0
47
48
# Next update is set according to which update function that's supposed to be called on
49
# next update iteration.
50
next_update = 0
51
52
# twitter_api is the twitter api handle, it needs to be initalized before use.
53
twitter_api = None
54
55
# left_over_updates is a list of updates generated by the update functions. Each update
56
# three of the lines are sent to the twitter channel.
57
left_over_updates = []
58
59
### last_updates stores the max_last_updates last update strings. last_updates are ###
60
### sorted from oldest to newest.                                                  ###
61
max_last_updates = 10
62
last_updates = []
63
64
### Keep a cache of old twitter friends. ###
65
old_friend_list = []
66
67
def init_twitter(bot, server, sender = None):
68
    """Initiate twitter and set user and password.
69
       This function is called on loading the module."""
70
    ### Check if all settings are set correctly. ###
71
    fail = False
72
    if not bot.settings.twitter_username:
73
        print " - No username set."
74
        if sender:
75
            server.privmsg(sender, "No twitter username.")
76
        fail = True
77
    if not bot.settings.twitter_password:
78
        print " - No password set."
79
        if sender:
80
            server.privmsg(sender, "No twitter password.")
81
        fail = True
82
    if not bot.settings.twitter_channel:
83
        print " - No channel set."
84
        if sender:
85
            server.privmsg(sender, "No twitter channel.")
86
        fail = True
87
    if fail:
88
        return False
89
    ### Login to twitter and return the api handle. ###
90
    print "Login to twitter:"
91
    return init_twitter_api(bot)
92
93
def update_twitter(bot, server):
94
    """Update twitter on regular intervals. On each update one api call will be made at
95
       most."""
96
    global twitter_timeout
97
    global next_update
98
    global left_over_updates
99
100
    print " * %s Twitter tick!" % time.strftime('[%H:%M:%S]')
101
    if twitter_timeout > 0:
102
        print " (timeouts: %d)" % twitter_timeout
103
104
    ### update_funcs are the functions that should be update. To add more just extend ###
105
    ### the list with a function of the same type as the once already there.          ###
106
    update_funcs = [get_friend_timeline, get_directed_messages, get_replies]
107
    if twitter_timeout == 0:
108
        if update_funcs[next_update](bot, server):
109
            next_update += 1
110
            if next_update == len(update_funcs):
111
                next_update = 0
112
        else:
113
            twitter_timeout += 1
114
    elif twitter_timeout > 0:
115
        twitter_timeout -= 1
116
        print "  - Timeout, waiting %d seconds." % (twitter_timeout*update_twitter.delay)
117
    else:
118
        twitter_timeout = 0
119
        print "  - Set timeout to 0."
120
    
121
    ### Print the update lines generated by the update funcs ###
122
    if left_over_updates:
123
        for i in xrange(0,2):
124
            if left_over_updates:
125
                status_string = left_over_updates.pop(0)
126
                print status_string
127
                send_to_channel(bot, server, status_string)
128
                last_updates.insert(0, status_string)
129
                if len(last_updates) > max_last_updates:
130
                    last_updates.pop()
131
            else:
132
                break
133
        print " + %d status updates left." % len(left_over_updates)
134
        
135
def do_twitter(bot, server, sender, target, args):
136
    """This is one of the commands, twitter in the name is not the module name but the
137
       commandname. The description of the command is specified in do_twitter_command"""
138
    if not twitter_api:
139
        init_twitter_api(bot)
140
    if args:
141
        sender_level = bot.get_level(sender)
142
        if sender_level > 70:
143
            if args[0] == 'update':
144
                return do_twitter_update(bot, server, sender, target, args[1:])
145
        if sender_level > 80:
146
            if args[0] == 'subscribe':
147
                return do_twitter_subscribe(bot, server, sender, target, args[1:])
148
            elif args[0] == 'unsubscribe':
149
                return do_twitter_unsubscribe(bot, server, sender, target, args[1:])
150
        if args[0] == 'get':
151
            return do_twitter_get(bot, server, sender, target, args[1:])
152
        elif args[0] == 'clear':
153
            return do_twitter_clear(bot, server, sender, target, args[1:])
154
    else:
155
        bot.respond(server, sender, target, "Not enough arguments.")
156
        return False
157
158
def handle_posts(bot, server, post_list):
159
    """Handle posts from twitter. Posts are returned by friend timeline, replies and
160
       public timeline."""
161
    global left_over_updates
162
    ### Iterate through the list of status updates and format it to a string that will ###
163
    ### end up on IRC.                                                                 ###
164
    for post in post_list:
165
        ### Get all the values from the post to shorten the rest of the codes. ###
166
        text         = post.GetText()
167
        realname     = post.GetUser().GetName()
168
        screename    = post.GetUser().GetScreenName()
169
        in_reply_to  = post.GetInReplyToScreenName()
170
        since_posted = post.GetRelativeCreatedAt()
171
        posted_at    = post.GetCreatedAtInSeconds()
172
        source       = post.GetSource()
173
        ### Try to expand shortened URL. ###
174
        for match in re.finditer(bot.regex_url, text):
175
            url = match.group(0)
176
            lurl = bot.expand_url(url)
177
            if len(lurl) > 70:
178
                print " + Expanding url '%s' to '%s'." % (url, lurl)
179
                text = text.replace(url, lurl)
180
        ### Look for html tags in the source string and strip them away to avoid ###
181
        ### junk in the IRC output.                                              ###
182
        m = re.match(r'<.*>(.*)<.*>', source)
183
        if m:
184
            source = m.group(1)
185
            print "Stripped source of html to '%s'." % source
186
        update_string = "\x02%s\x02 - Posted by %s [%s] from %s" % (text, realname, screename, source)
187
        if in_reply_to:
188
            update_string += " in reply to %s" % in_reply_to
189
        ### Only add time string if time exceed 5 minutes. ###
190
        if (time.time() - posted_at) > 5*60:
191
            update_string += " %s" % since_posted
192
        left_over_updates.append(update_string)
193
194
def get_friend_timeline(bot, server):
195
    """This update function fetches the timeline of all friends.
196
       Returns True if successfull, return False if got error from twitter.com"""
197
    global left_over_updates
198
    print " - Get twitter friend timeline."
199
    try:
200
        ### Make a call to the api and reverse the list so that the oldest message is ###
201
        ### first.                                                                    ###
202
        friend_timeline = twitter_api.GetFriendsTimeline(since_id = bot.settings.twitter_last_friend_update)
203
        friend_timeline.reverse()
204
    except Exception, e:
205
        global twitter_timeout
206
        twitter_timeout = (twitter_timeout + 1) * 2
207
        print "Failed to get friend timeline:", e
208
        return False
209
    ### Handle the post list and add them to the queue of posts to get posted on IRC ###
210
    handle_posts(bot, server, friend_timeline)
211
    ### If got any updates get the id for the last one and use that to look for updates ###
212
    ### later on. But only change the value in settings if post id is higher.           ###
213
    if friend_timeline:
214
        bot.settings.twitter_last_friend_update = max(friend_timeline[-1].id, bot.settings.twitter_last_friend_update)
215
        print " * Update twitter from '%d'." % bot.settings.twitter_last_friend_update
216
    return True
217
218
def get_replies(bot, server):
219
    """This update function fetches all replies from twitter. A reply is a tweet that
220
       contains @myscreenname.
221
       Returns True if successfull, return False if got error from twitter.com"""
222
    global left_over_updates
223
    print " - Get twitter replies."
224
    try:
225
        ### Make a call to the api and reverse the list so that the oldest message is ###
226
        ### first.                                                                    ###
227
        replies = twitter_api.GetReplies(since_id=bot.settings.twitter_last_reply_update)
228
        replies.reverse()
229
    except Exception, e:
230
        global twitter_timeout
231
        twitter_timeout = (twitter_timeout + 1) * 2
232
        print "Failed to get replies:", e
233
        return False
234
    ### Handle the reply list and add them to the queue of posts to get posted on IRC ###
235
    handle_posts(bot, server, replies)
236
    ### If got any updates get the id for the last one and use that to look for updates ###
237
    ### later on. But only change the value in settings if post id is higher.           ###
238
    if replies:
239
        bot.settings.twitter_last_reply_update = max(replies[-1].id, bot.settings.twitter_last_reply_update)
240
        print " * Update twitter from '%d'." % bot.settings.twitter_last_reply_update
241
    return True
242
243
def get_directed_messages(bot, server):
244
    """This update function fetches all direct messages from twitter.
245
       Direct messages are not posts and can not be handled by handle_posts.
246
       Returns True if successfull, return False if got error from twitter.com"""
247
    global left_over_updates
248
    print " - Get direct messages from twitter."
249
    try:
250
        directed_messages = twitter_api.GetDirectMessages(since_id=bot.settings.twitter_last_directed)
251
        directed_messages.reverse()
252
    except Exception, e:
253
        global twitter_timeout
254
        twitter_timeout = (twitter_timeout + 5) * 2
255
        print "Failed to get directed messages:", e
256
        return False
257
    ### Iterate through the list of messages and 
258
    for message in directed_messages:
259
        ### Assign variables to each value from message to simplify the code. ###
260
        text              = message.GetText()
261
        sender_screenname = message.GetSenderScreenName()
262
        recip_screenname  = message.GetRecipientScreenName()
263
        ### Try to expand shortened URL. ###
264
        for match in re.finditer(bot.regex_url, text):
265
            url = match.group(0)
266
            lurl = bot.expand_url(url)
267
            if len(surl) > 70:
268
                print " + Expanding url '%s' to '%s'." % (url, lurl)
269
                text = text.replace(url, lurl)
270
        update_string = "Direct message: \x02%s\x02 from %s to %s" % (text, sender_screenname, recip_screenname)
271
        left_over_updates.append(update_string)
272
    if directed_messages:
273
        bot.settings.twitter_last_directed = max(directed_messages[-1].id, bot.settings.twitter_last_directed)
274
        print " * Update twitter directed messages from '%d'." % bot.settings.twitter_last_directed
275
    return True
276
277
def do_twitter_get(bot, server, sender, target, args):
278
    """Subcommand to twitter, get values from twitter. Each argument to get should only
279
       make one API call."""
280
    global twitter_api
281
    global old_friend_list
282
    if not twitter_api:
283
        init_twitter_api(bot)
284
    if args:
285
        if args[0] == 'friends' and bot.get_level(sender) > 70:
286
            friend_string = "My twitter friends are:"
287
            users = twitter_api.GetFriends()
288
            user_names = [u.GetScreenName() for u in users]
289
            first_friend = True
290
            for screenname in user_names:
291
                if not screenname in old_friend_list:
292
                    friend_string += "%s \x02%s\x02" % (("", ",")[first_friend], screenname)
293
                else:
294
                    friend_string += "%s %s" % (("", ",")[first_friend], screenname)
295
            bot.respond(server, sender, target, bot.convert_to_irc_string(friend_string))
296
            old_friend_list = user_names
297
            return True
298
        elif args[0] == 'queue' and bot.get_level(sender) > 70:
299
            queue_size = len(left_over_updates)
300
            bot.respond(server, sender, target, "There are currently %d twitter message%s in queue." % (queue_size,
301
                                                                                                        ("", "s")[queue_size > 1]))
302
            return True
303
        ### If command is called in private also handle these subcommands to 'get'. ###
304
        elif target == bot.settings.bot_nick_name:
305
            if args[0] == 'last':
306
                max_updates = 1
307
                try:
308
                    if len(args) > 1:
309
                        max_updates = int(''.join(args[1:]))
310
                except Exception, e:
311
                    pass
312
                for update in last_updates[:max_updates]:
313
                    bot.respond(server, sender, target, bot.convert_to_irc_string(update))
314
                return True
315
            else:
316
                bot.respond(server, sender, target, "'%s' is not a valid argument." % args[0])
317
                return False
318
        else:
319
            bot.respond(server, sender, target, "'%s' is not a valid argument." % args[0])
320
            return False
321
    else:
322
        bot.respond(server, sender, target, "Not enough arguments.")
323
        return False
324
325
def do_twitter_clear(bot, server, sender, target, args):
326
    """Clear a variable from the module."""
327
    global left_over_updates
328
    if left_over_updates and bot.get_level(sender) > 70:
329
        queue_size = len(left_over_updates)
330
        left_over_updates = []
331
        bot.respond(server, sender, target, "Removed %d posts from queue." % queue_size)
332
        return True
333
    else:
334
        bot.respond(server, sender, target, "The queue is already empty.")
335
        return False
336
337
def do_twitter_update(bot, server, sender, target, args):
338
    """Subcommand to twitter, update the twitter status."""
339
    global twitter_api
340
    if not twitter_api:
341
        init_twitter_api(bot)
342
    if args:
343
        ### Join all the args to twitter update into one string. ###
344
        new_status = " ".join(args)
345
        ### If the status string is to long, shorten all the URL's in it and try again. ###
346
        if len(new_status) > 100:
347
            for match in re.finditer(bot.regex_url, new_status):
348
                url = match.group(0)
349
                surl = bot.shorten_url(url)
350
                if len(url) > len(surl):
351
                    print "Shortening url '%s' to '%s'." % (url, surl)
352
                    new_status = new_status.replace(url, surl)
353
            ### If the status is still to long don't even try to post to save api calls. ###
354
            if len(new_status) > 140:
355
                bot.respond(server, sender, target, "Message is %d characters too long." % (len(new_status)-140))
356
                print "Failed to update, too long message."
357
                return True
358
        try:
359
            ### Call the api and try to update status. ###
360
            status = twitter_api.PostUpdate(bot.convert_to_twitter_string(new_status))
361
        except Exception, e:
362
            print "Updating status failed:", e
363
            bot.respond(server, sender, target, "Twitter status update failed: %s" % str(e))
364
            return True
365
        print "Updated twitter status to %s by %s" % (bot.convert_to_irc_string(status.text), sender)
366
        server.notice(bot.settings.twitter_channel,
367
                      "Twitter status updated successfully to \x02'%s'\x02." % bot.convert_to_irc_string(status.text))
368
        return True
369
    else:
370
        bot.respond(server, sender, target, "Not enough arguments.")
371
        return False
372
373
def do_twitter_subscribe(bot, server, sender, target, args):
374
    global twitter_api
375
    if not twitter_api:
376
        init_twitter_api(bot)
377
    if args:
378
        print "Create friendship with:", ", ".join(args)
379
        for user in args:
380
            try:
381
                new_friend = twitter_api.CreateFriendship(user)
382
                if new_friend:
383
                    bot.respond(server, sender, target, "You are now friends with '%s'." % new_friend.GetName())
384
            except Exception, e:
385
                bot.respond(server, sender, target, "Could not make friends with '%s'." % user)
386
                print " - Could not make friends with '%s'." % user
387
        return True
388
    else:
389
        bot.respond(server, sender, target, "Not enough arguments.")
390
        return False
391
392
def do_twitter_unsubscribe(bot, server, sender, target, args):
393
    global twitter_api
394
    if not twitter_api:
395
        init_twitter_api(bot)
396
    if args:
397
        print "Destroy friendship with:", ", ".join(args)
398
        for user in args:
399
            try:
400
                old_friend = twitter_api.DestroyFriendship(user)
401
                if old_friend:
402
                    bot.respond(server, sender, target, "You are no longer friends with '%s'." % old_friend.GetName())
403
            except Exception, e:
404
                bot.respond(server, sender, target, "Could not remove friendship with '%s'." % user)
405
                print " - Could not remove friendship with '%s'." % user
406
        return True
407
    else:
408
        bot.respond(server, sender, target, "Not enough arguments.")
409
        return False
410
411
####
412
# The following declarations are only helper functions
413
##
414
        
415
def init_twitter_api(bot):
416
    """ [Internal] Initiate api"""
417
    global twitter_api
418
    global twitter_update_from
419
    twitter_api = twitter.Api(username = bot.settings.twitter_username,
420
                              password = bot.settings.twitter_password)
421
    if twitter_api:
422
        print " + Logged into twitter account '%s : %s'." % (bot.settings.twitter_username, bot.settings.twitter_password)
423
        twitter_api.SetUserAgent("KoFooBot")
424
        return True
425
    else:
426
        print " + Failed to login."
427
        return False
428
429
def send_to_channel(bot, server, msg):
430
    """ [Internal] Send string to channel with proper format."""
431
    line = ' '.join(msg.splitlines())
432
    line = re.sub(bot.regex_ahref, bot.rewrite_url, line)
433
    server.privmsg(bot.settings.twitter_channel, bot.convert_to_irc_string(line))