From c5b0c5910f6fe6c325ce3ed80975e6d07e7c2860 Mon Sep 17 00:00:00 2001 From: Jon Bergli Heier Date: Mon, 24 May 2010 21:54:56 +0200 Subject: Added a package tracking module for posten.no. --- modules/tracking.py | 294 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 modules/tracking.py (limited to 'modules/tracking.py') diff --git a/modules/tracking.py b/modules/tracking.py new file mode 100644 index 0000000..94b8d86 --- /dev/null +++ b/modules/tracking.py @@ -0,0 +1,294 @@ +info = { + 'author': 'Jon Bergli Heier', + 'title': 'Posten.no tracking', + 'description': 'Fetches tracking info from posten.no.', +} + +cfg_section = 'module/tracking' + +import urllib2, datetime, re +from lxml import etree +from twisted.internet.task import LoopingCall +from sqlalchemy import create_engine, Column, Integer, String, DateTime, ForeignKey +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, relation, backref +from sqlalchemy.orm.exc import NoResultFound +from sqlalchemy.exc import IntegrityError + +engine = Session = None +Base = declarative_base() + +class Consignment(Base): + __tablename__ = 'consignment' + + id = Column(Integer, primary_key = True) + nick = Column(String, nullable = False) + channel = Column(String) # NULL for private + code = Column(String, nullable = False, unique = True) + added = Column(DateTime, nullable = False) + + packages = relation('Package', backref = 'consignment', primaryjoin = 'Package.consignment_id == Consignment.id') + + def __init__(self, nick, channel, code, added): + self.nick = nick + self.channel = channel + self.code = code + self.added = added + +class Package(Base): + __tablename__ = 'package' + + id = Column(Integer, primary_key = True) + consignment_id = Column(Integer, ForeignKey('consignment.id')) + code = Column(String, nullable = False, unique = True) + last = Column(DateTime) # Last update + status = Column(String) # Last status + + def __init__(self, consignment_id, code): + self.consignment_id = consignment_id + self.code = code + +class Module: + url = 'http://sporing.posten.no/sporing.xml?q=%s' + + def __init__(self, bot): + self.setup_db() + self.irc = bot + self.tracking = [] + self.lc = LoopingCall(self.lc_callback) + if bot: + self.lc.start(config.getfloat(cfg_section, 'interval'), False) + + def stop(self): + try: + self.lc.stop() + except: + pass + + def setup_db(self): + global engine, Session + + if engine and Session: + return + + engine = create_engine(config.get(cfg_section, 'db_path')) + Base.metadata.create_all(engine) + Session = sessionmaker(bind = engine, autoflush = True, autocommit = False) + + def get_xml(self, url): + try: + u = urllib2.urlopen(url) + except: + return + if u.headers['content-type'].startswith('text/xml'): + xml = etree.parse(u) + else: + xml = None + u.close() + return xml + + def track(self, code): + xml = self.get_xml(self. url % code) + if not xml: + return + + ns = 'http://www.bring.no/sporing/1.0' + packages = xml.find('//{%s}PackageSet' % (ns,)) + if packages is None: + raise Exception('No packages found for \002%s\002.' % code) + + results = [] + for package in packages: + code = package.attrib['packageId'] + eventset = package.find('{%s}EventSet' % ns) + last = eventset[0] + desc = last.find('{%s}Description' % ns).text.replace('<', '<') + desc = re.sub(r'<[^>]*?>', '', desc).encode('utf8') + isodate = last.find('{%s}OccuredAtIsoDateTime' % ns).text[:-10] + isodate = datetime.datetime.strptime(isodate, '%Y-%m-%dT%H:%M:%S') + city = last.find('{%s}City' % ns).text + if city: + desc = '%s (%s)' % (desc, city.encode('utf8')) + date = last.find('{%s}OccuredAtDisplayDate' % ns).text + ' ' + last.find('{%s}OccuredAtDisplayTime' % ns).text + results.append((code, isodate, desc)) + return results + + def track_start(self, code, nick, channel): + msg = None + try: + session = Session() + consignment = Consignment(nick, channel, code, datetime.datetime.utcnow()) + session.add(consignment) + session.commit() + msg = 'Now tracking \002%s\002.' % code + except IntegrityError as e: + msg = 'Already tracking \002%s\002.' % code + finally: + session.close() + return msg + + def track_stop(self, code, nick, channel): + msg = None + try: + session = Session() + consignment = session.query(Consignment).filter_by(code = code).one() + for p in consignment.packages: + session.delete(p) + session.delete(consignment) + session.commit() + msg = 'No longer tracking \002%s\002' % code + except NoResultFound: + msg = '\002%s\002 is not being tracked.' % code + finally: + session.close() + return msg + + def track_status(self, code, nick, channel): + msg = [] + try: + session = Session() + consignments = session.query(Consignment).filter(Consignment.code.like(code + '%')) + if nick: + consignments = consignments.filter(Consignment.nick == nick) + results = [] + for row in consignments: + i = 0 + for package in row.packages: + i += 1 + date = package.last + desc = package.status + if date and desc: + s = '\002%s\002 %s - %s' % (package.code, date, desc) + else: + s = 'No tracking info found for \002%s\002' % package.code + results.append(s) + if i == 0: + results.append('No packages found for \002%s\002' % row.code) + if len(results): + msg = results + else: + try: + data = self.track(code) + if data: + for package in data: + msg.append('\002%s\002 %s - %s' % (data[0], data[1], data[2])) + elif code: + msg = 'Failed to fetch tracking data for \002%s\002.' % code + else: + msg = 'No tracking number given or registered.' + except Exception as e: + msg = str(e) + finally: + session.close() + return msg + + def track_list(self, nick): + msg = None + try: + session = Session() + trackings = [] + consignments = session.query(Consignment).filter_by(nick = nick) + for row in consignments: + trackings.append(row.code) + if len(trackings): + msg = 'Trackings for %s: %s' % (nick, ', '.join(trackings)) + else: + msg = 'No trackings for %s' % nick + except NoResultFound: + msg = 'No trackings registered for %s.' + finally: + session.close() + return msg + + def __call__(self, nick, channel, msg): + if not msg.startswith('!track'): + return + + args = msg.split() + if len(args) < 2: + self.irc.msg(channel if not channel == self.irc.nickname else nick.split('!')[0], 'Usage: !track (start|stop|status TRACKINGNO)|list') + return + + mode = args[1] + if mode.lower() in ('start', 'stop', 'status'): + if len(args) != 3 and mode.lower() != 'status': + self.irc.msg(channel if not channel == self.irc.nickname else nick.split('!')[0], 'Usage: !track (start|stop|status TRACKINGNO)|list') + return + code = args[2] if len(args) == 3 else '' + + msg = None + if mode.lower() == 'start': + msg = self.track_start(code, nick.split('!')[0], channel if not channel == self.irc.nickname else None) + elif mode.lower() == 'stop': + msg = self.track_stop(code, nick.split('!')[0], channel if not channel == self.irc.nickname else None) + elif mode.lower() == 'status': + msg = self.track_status(code, nick.split('!')[0], channel if not channel == self.irc.nickname else None) + elif mode.lower() == 'list': + msg = self.track_list(nick.split('!')[0]) + else: + msg = 'Invalid mode "%s".' % mode + + if msg: + if type(msg) == list: + for i in msg: + self.irc.msg(channel if not channel == self.irc.nickname else nick.split('!')[0], i.encode('utf-8')) + else: + self.irc.msg(channel if not channel == self.irc.nickname else nick.split('!')[0], msg.encode('utf-8')) + else: + self.irc.msg(channel if not channel == self.irc.nickname else nick.split('!')[0], 'No data returned (this is a bug).') + + def lc_callback(self): + try: + session = Session() + consignments = session.query(Consignment).filter(Consignment.channel.in_(config.get(self.irc.factory.server, 'channels').split())) + for row in consignments: + target = row.channel or row.nick + target = target.encode('utf-8') + removed = False + data_ = self.track(row.code) + if not data_: + continue + for data in data_: + try: + package = session.query(Package).filter_by(code = data[0]).one() + except: + package = Package(row.id, data[0]) + session.add(package) + package = session.query(Package).filter_by(code = data[0]).one() + + if package.last == None or data[1] > package.last: + code = data[0] + last = data[1] + desc = data[2] + msg = '%s: \002%s\002 %s - %s' % (row.nick.encode('utf-8'), package.code.encode('utf-8'), last, desc) + if desc.startswith('Sendingen er utlevert'): + session.delete(package) + msg += ' (Package delivered - tracking stopped)' + removed = True + else: + package.last = last + package.status = desc + session.add(package) + self.irc.msg(target, msg) + if removed and len(row.packages) == 0: + msg = '%s: \002%s\002 is no longer being tracked' % (row.nick, row.code) + self.irc.msg(target, msg.encode('utf-8')) + session.delete(row) + session.commit() + finally: + session.close() + +if __name__ == '__main__': + import sys, ConfigParser, os + from twisted.internet import reactor + config = ConfigParser.ConfigParser() + config.read([os.path.expanduser('~/.fot')]) + m = Module(None) + for code in sys.argv[1:]: + tracking = m.track(code) + if tracking: + for track in tracking: + if type(track) == list: + print '\n'.join(track) + else: + print track -- cgit v1.2.3