#!/usr/bin/python """ * Reminder BOT for status.net 0.1 * * @author: Mark Styles * * Copyright (C) 2010 Mark Styles * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * """ import sys import os import string import time import datetime import subprocess import simplejson import urllib import urllib2 class DateHandler(): """ Process a string to decide if it's a valid date format or not. @type alert: string @ivar alert: Alert specified, e.g. +5 means notify me 5 days before @type repeat: string @ivar repeat: Repeat specified, e.g. *4 means notify me every 4 days @type weekday: string @ivar weekday: Abbreviated (3 char) day of week @type day: int @ivar day: Day of month @type month: string @ivar month: Abbreviated (3 char) month @type monthno: int @ivar monthno: Month of year (1 - 12) @type year: int @ivar year: 4 digit year @type valid: boolean @ivar valid: Is the argument passed a valid date? """ def __init__(self, arg = '', date=None): """ @param arg: the argument we are testing @param date: an existing instance we want to merge """ self.valid = True # Merge the passed object if it is an object if date is not None: self.alert=date.alert self.repeat=date.repeat self.weekday=date.weekday self.day=date.day self.month=date.month self.monthno=date.monthno self.year=date.year else: self.alert=None self.repeat=None self.weekday=None self.day=None self.month=None self.monthno=None self.year=None days = ('sun','mon','tue','wed','thu','fri','sat') months = ('','jan','feb','mar','apr','may','jun' ,'jul','aug','sep','oct','nov','dec') abbr = arg.lower()[0:3] # If the argument contains a dash, check it for dd-mm-yyyy format if arg.find('-') != -1: date = arg.split('-') if len(date) != 3: print "Couldn't find 3 data elements" self.valid = False else: for d in date: # Check that all values are ints try: tmp = int(d) except: print "Non-int found" self.valid = False if self.valid: # Check the month is valid m = int(date[1]) if m < 1 or m > 12: print "Invalid month" self.valid = False else: self.monthno = m self.month = months[m] # Use fake recursion to test day and year self.recurse(date[0]) self.recurse(date[2]) # If the argument starts with +, its an alert specifier elif arg.find('+') == 0: self.alert = arg # If the argument starts with * its a repeat specifier elif arg.find('*') == 0: self.repeat = arg # Check if it's an abbreviated (3 char) day of week elif abbr in days: self.weekday = abbr # Check if it's an abbreviated (3 char) month elif abbr in months: self.month = abbr self.monthno = months.index(abbr) # If all else fails, maybe is day or year else: try: tmp = int(arg) # Year has to be 4 chars for this to work if tmp > 2000: self.year = tmp else: self.day = tmp except: self.valid = False def recurse(self, arg): """ Re-process part of an arg for day or year. @param arg: The argument we are testing """ d = DateHandler(arg) if d.day: self.day = d.day if d.year: self.year = d.year self.valid = d.valid class ReminderBot(): """ The bot class. Not actually a bot exactly because it doesn't sit and listen but is rather called when needed @type datadir: string @ivar datadir: full path to directory holding bot data @type username: string @ivar username: username on status.net service @type password: string @ivar password: password on status.net service @type api_root: string @ivar api_root: root url for the service API @type auth_handler: urllib2.HTTPBasicAuthHandler @ivar auth_handler: Authentication handler for the API @type opener: urllib2.opener @ivar opener: URL opener @type message_queue: List @ivar message_queue: list of messages awaiting transmission @type last_id: int @ivar last_id: ID of last mention processed @type last_dm_id: int @ivar last_dm_id: ID of last direct message processed """ def __init__(self): # Change these to suit your needs self.datadir = os.path.expanduser('~/users') self.username = 'remindme' self.password = 'xxxxxxxx' self.api_root = 'https://identi.ca/api' # Authentication handler self.auth_handler = urllib2.HTTPBasicAuthHandler() self.auth_handler.add_password( 'Identi.ca API', 'identi.ca', self.username, self.password) self.opener = urllib2.build_opener(self.auth_handler) # We queue up messages to be processed by output() for cleanliness self.message_queue = [] # Get the last received @remindme dent ID we processed try: last_dent = open('%s/%s' % (self.datadir, '.last_dent'), 'r') self.last_id = int(last_dent.read()) last_dent.close except IOError: self.last_id = 0 # Get the last direct message ID we processed try: last_dm = open('%s/%s' % (self.datadir, '.last_dm'), 'r') self.last_dm_id = int(last_dm.read()) last_dm.close except IOError: self.last_dm_id = 0 def get_result(self, url, post=None): """ Get results from the passed URL @param url: the URL to fetch @param post: the POST contents if any """ url="%s/%s" % (self.api_root, url) if post is None: handle = self.opener.open(url) else: handle = self.opener.open(url, post) result = simplejson.load(handle) if 'Error' in result: raise result['Error'] return result def process_args(self, args, user, type): """ Process an argument to see if it is a remind spec @param args: tokenised dent/dm @param user: user sending the dent/dm @param type: type of message (DM or PUBLIC) """ if len(args) < 2: return None; # we must have at least one date element date = DateHandler(args[0]) if not date.valid: return "Bad date format." # and maybe more next=1 for arg in args[next:]: tmp = DateHandler(arg, date) if tmp.valid: date = tmp else: break next = next + 1 # Can't proceed unless we have at least a day or weekday if not date.day and not date.weekday: return "Bad date spec, must contain at least a day of the month or weekday" if date.day and date.monthno and date.year: # Got 3 ints, try to convert them into a valid date object try: d = datetime.date(date.year, date.monthno, date.day) except: return "Bad date found." # Put the rest of the message back together msg = string.join(args[next:]) err = '' # Create the remind command if date.day: command = 'REM %s' % date.day elif date.weekday: command = 'REM %s' % date.weekday if date.month: command = '%s %s' % (command, date.month) if date.year: command = '%s %s' % (command, date.year) if date.alert: command = '%s %s' % (command, date.alert) if date.repeat: if date.day and date.month and date.year: command = '%s %s' % (command, date.repeat) else: err = 'Repeat ignored, full date required.' command = '%s %s is %%b\n' % (command, msg) # Write the remind command to the appropriate user file if type == 'DM': userfile = open('%s/%s.dm' % (self.datadir, user), 'a') userfile.write(command.encode('utf-8')) userfile.close else: userfile = open('%s/%s' % (self.datadir, user), 'a') userfile.write(command.encode('utf-8')) userfile.close return "Reminder added. %s" % err def get_dents(self): """Process mentions received from status.net service""" url = "statuses/mentions.json&since_id=%d" % self.last_id result = self.get_result(url) if result: # Process each result for request in result: args = request['text'].split() user = request['user']['screen_name'] print user print request['text'] # All our errors start with 'Bad', hacky I know if args[0] != 'Bad': ret = self.process_args(args, user, 'PUBLIC') # If we got a string back, send it to the user if ret is not None: self.message_queue.append((user, ret, 'PUBLIC')) # Update the last dent so we don't reprocess last_dent = open('%s/%s' % (self.datadir, '.last_dent'), 'w') last_dent.write(str(result[0]['id'])) last_dent.close def get_dms(self): """Process direct messages received""" url = "direct_messages.json&since_id=%d" % self.last_dm_id result = self.get_result(url) if result: for request in result: args = request['text'].split() user = request['sender']['screen_name'] print user print request['text'] ret = self.process_args(args, user, 'DM') if ret is not None: self.message_queue.append((user, ret, 'DM')) # Update the last dent so we don't reprocess last_dm = open('%s/%s' % (self.datadir, '.last_dm'), 'w') last_dm.write(str(result[0]['id'])) last_dm.close def do_remind(self, file, reply=True): """ Run the remind command for the specified file @param file: the reminder file to process @param reply: whether the messages go to a user or not """ # Start remind as a sub process cmd = subprocess.Popen( ["remind", "-hgo", "%s/%s" % (self.datadir, file)], stdout=subprocess.PIPE, stderr=subprocess.PIPE) output = cmd.communicate() for out in output[0].split('\n'): # Skip the header just in case if out != '' and out.find('Reminders for') == -1: if reply: # files ending with .dm are for direct messages if file.find('.dm') == -1: self.message_queue.append((file, out, 'PUBLIC')) else: self.message_queue.append((file.split('.')[0], out, 'DM')) else: # No reply, just send the message with no user self.message_queue.append((None, out, 'PUBLIC')) def get_reminders(self): """run this once per day to give people their reminders""" files = os.listdir(self.datadir) # user defined reminders for user in files: if user.find('.') != 0: self.do_remind(user) elif user.find('last_') == -1: self.do_remind(user, False) def output(self): if len(self.message_queue): dm_url = "direct_messages/new.json" pub_url = "statuses/update.json" # empty the queue just in case messages = self.message_queue self.message_queue = [] for message in messages: if message[2] == 'DM': # send a direct message print("To: %s Message: %s Type: %s" % message) post = urllib.urlencode( {'user': message[0], 'text': message[1]}) result = self.get_result(dm_url, post) else: # send a public message if message[0] is None: print("Message: %s Type: %s" % message[1:]) post = urllib.urlencode({'status': message[1]}) else: print("To: %s Message: %s Type: %s" % message) post = urllib.urlencode( {'status': '@%s %s' % (message[0], message[1])}) result = self.get_result(pub_url, post) # sleep between each message so we don't look like a spammer time.sleep(2) if __name__ == '__main__': bot = ReminderBot() if len(sys.argv) < 2: print "Usage: remindme process|remind" elif sys.argv[1] == 'process': bot.get_dents() bot.get_dms() bot.output() elif sys.argv[1] == 'remind': bot.get_reminders() bot.output() else: print "Usage: remindme process|remind"