summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/tracking.py294
1 files changed, 294 insertions, 0 deletions
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('&lt;', '<')
+ 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