Merge lp:~max-rabkin/ibid/invite-502294 into lp:~ibid-core/ibid/old-release-0.1-1.6

Proposed by marcog
Status: Rejected
Rejected by: Stefano Rivera
Proposed branch: lp:~max-rabkin/ibid/invite-502294
Merge into: lp:~ibid-core/ibid/old-release-0.1-1.6
Diff against target: 407 lines (+338/-3)
4 files modified
ibid/plugins/geography.py (+302/-3)
ibid/plugins/sources.py (+13/-0)
ibid/source/irc.py (+14/-0)
ibid/source/silc.py (+9/-0)
To merge this branch: bzr merge lp:~max-rabkin/ibid/invite-502294
Reviewer Review Type Date Requested Status
Stefano Rivera Needs Fixing
Review via email: mp+23934@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Stefano Rivera (stefanor) wrote :

Looks like this isn't mergable into 0.1

bzr rebase --onto 908 -r 911..

review: Needs Fixing

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ibid/plugins/geography.py'
2--- ibid/plugins/geography.py 2010-03-22 15:05:21 +0000
3+++ ibid/plugins/geography.py 2010-04-22 14:08:19 +0000
4@@ -2,21 +2,23 @@
5 # Released under terms of the MIT/X/Expat Licence. See COPYING for details.
6
7 from math import acos, sin, cos, radians
8-from urllib import quote
9+from urllib import quote, urlencode
10 from urlparse import urljoin
11 import re
12 import logging
13 from os.path import exists, join
14 from datetime import datetime
15 from os import walk
16+import csv
17+from sys import maxint
18
19 from dateutil.parser import parse
20 from dateutil.tz import gettz, tzlocal, tzoffset
21
22 from ibid.plugins import Processor, match
23-from ibid.utils import json_webservice, human_join, format_date
24+from ibid.utils import json_webservice, human_join, format_date, cacheable_download
25 from ibid.utils.html import get_html_parse_tree
26-from ibid.config import Option, DictOption
27+from ibid.config import Option, DictOption, IntOption
28 from ibid.compat import defaultdict
29
30 log = logging.getLogger('plugins.geography')
31@@ -362,4 +364,301 @@
32 def time(self, event, place):
33 self.convert(event, None, None, place)
34
35+features['flight'] = {
36+ 'description': u'Search for flights on travelocity',
37+ 'categories': ('lookup', 'web',),
38+}
39+class Flight:
40+ def __init__(self):
41+ self.flight, self.depart_time, self.depart_ap, self.arrive_time, \
42+ self.arrive_ap, self.duration, self.stops, self.price = \
43+ [], None, None, None, None, None, None, None
44+
45+ def int_price(self):
46+ try:
47+ return int(self.price[1:])
48+ except ValueError:
49+ return maxint
50+
51+ def int_duration(self):
52+ hours, minutes = 0, 0
53+ match = re.search(r'(\d+)hr', self.duration)
54+ if match:
55+ hours = int(match.group(1))
56+ match = re.search(r'(\d+)min', self.duration)
57+ if match:
58+ minutes = int(match.group(1))
59+ return int(hours)*60 + int(minutes)
60+
61+MONTH_SHORT = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')
62+MONTH_LONG = ('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December')
63+OTHER_STUFF = ('am', 'pm', 'st', 'nd', 'rd', 'th', 'morning', 'afternoon', 'evening', 'anytime')
64+DATE = r'(?:[0-9.:/hT -]|%s)+' % '|'.join(MONTH_SHORT+MONTH_LONG+OTHER_STUFF)
65+
66+class FlightException(Exception):
67+ pass
68+
69+class FlightSearch(Processor):
70+ usage = u"""airport [in] <name|location|code>
71+ [<cheapest|quickest>] flight from <departure> to <destination> from <depart_date> [anytime|morning|afternoon|evening|<time>] to <return_date> [anytime|morning|afternoon|evening|<time>]"""
72+
73+ feature = ('flight',)
74+
75+ airports_url = u'http://openflights.svn.sourceforge.net/viewvc/openflights/openflights/data/airports.dat'
76+ max_results = IntOption('max_results', 'Maximum number of results to list', 5)
77+
78+ airports = {}
79+
80+ def read_airport_data(self):
81+ # File is listed as ISO 8859-1 (Latin-1) encoded on
82+ # http://openflights.org/data.html, but from decoding it appears to
83+ # actually be UTF8
84+ filename = cacheable_download(self.airports_url, u'flight/airports.dat')
85+ reader = csv.reader(open(filename), delimiter=',', quotechar='"')
86+ for row in reader:
87+ self.airports[int(row[0])] = [unicode(r, u'utf-8') for r in row[1:]]
88+
89+ def _airport_search(self, query, search_loc = True):
90+ if not self.airports:
91+ self.read_airport_data()
92+ if search_loc:
93+ ids = self._airport_search(query, False)
94+ if len(ids) == 1:
95+ return ids
96+ query = [q for q in query.lower().split()]
97+ else:
98+ query = [query.lower()]
99+ ids = []
100+ for id, airport in self.airports.items():
101+ if search_loc:
102+ data = (u' '.join(c.lower() for c in airport[:5])).split()
103+ elif len(query[0]) == 3:
104+ data = [airport[3].lower()]
105+ else: # assume length 4 (won't break if not)
106+ data = [airport[4].lower()]
107+ if len(filter(lambda q: q in data, query)) == len(query):
108+ ids.append(id)
109+ return ids
110+
111+ def repr_airport(self, id):
112+ airport = self.airports[id]
113+ code = u''
114+ if airport[3] or airport[4]:
115+ code = u' (%s)' % u'/'.join(filter(lambda c: c, airport[3:5]))
116+ return u'%s%s' % (airport[0], code)
117+
118+ @match(r'^airports?\s+((?:in|for)\s+)?(.+)$')
119+ def airport_search(self, event, search_loc, query):
120+ search_loc = search_loc is not None
121+ if not search_loc and not 3 <= len(query) <= 4:
122+ event.addresponse(u'Airport code must be 3 or 4 characters')
123+ return
124+ ids = self._airport_search(query, search_loc)
125+ if len(ids) == 0:
126+ event.addresponse(u"Sorry, I don't know that airport")
127+ elif len(ids) == 1:
128+ id = ids[0]
129+ airport = self.airports[id]
130+ code = u'unknown code'
131+ if airport[3] and airport[4]:
132+ code = u'codes %s and %s' % (airport[3], airport[4])
133+ elif airport[3]:
134+ code = u'code %s' % airport[3]
135+ elif airport[4]:
136+ code = u'code %s' % airport[4]
137+ event.addresponse(u'%(airport)s in %(city)s, %(country)s has %(code)s', {
138+ u'airport': airport[0],
139+ u'city': airport[1],
140+ u'country': airport[2],
141+ u'code': code,
142+ })
143+ else:
144+ event.addresponse(u'Found the following airports: %s', human_join(self.repr_airport(id) for id in ids)[:480])
145+
146+ def _flight_search(self, event, dpt, to, dep_date, ret_date):
147+ airport_dpt = self._airport_search(dpt)
148+ airport_to = self._airport_search(to)
149+ if len(airport_dpt) == 0:
150+ event.addresponse(u"Sorry, I don't know the airport you want to leave from")
151+ return
152+ if len(airport_to) == 0:
153+ event.addresponse(u"Sorry, I don't know the airport you want to fly to")
154+ return
155+ if len(airport_dpt) > 1:
156+ event.addresponse(u'The following airports match the departure: %s', human_join(self.repr_airport(id) for id in airport_dpt)[:480])
157+ return
158+ if len(airport_to) > 1:
159+ event.addresponse(u'The following airports match the destination: %s', human_join(self.repr_airport(id) for id in airport_to)[:480])
160+ return
161+
162+ dpt = airport_dpt[0]
163+ to = airport_to[0]
164+
165+ def to_travelocity_date(date):
166+ date = date.lower()
167+ time = None
168+ for period in [u'anytime', u'morning', u'afternoon', u'evening']:
169+ if period in date:
170+ time = period.title()
171+ date = date.replace(period, u'')
172+ break
173+ try:
174+ date = parse(date)
175+ except ValueError:
176+ raise FlightException(u"Sorry, I can't understand the date %s" % date)
177+ if time is None:
178+ if date.hour == 0 and date.minute == 0:
179+ time = u'Anytime'
180+ else:
181+ time = date.strftime('%I:00')
182+ if time[0] == u'0':
183+ time = time[1:]
184+ if date.hour < 12:
185+ time += u'am'
186+ else:
187+ time += u'pm'
188+ date = date.strftime('%m/%d/%Y')
189+ return (date, time)
190+
191+ (dep_date, dep_time) = to_travelocity_date(dep_date)
192+ (ret_date, ret_time) = to_travelocity_date(ret_date)
193+
194+ params = {}
195+ params[u'leavingFrom'] = self.airports[dpt][3]
196+ params[u'goingTo'] = self.airports[to][3]
197+ params[u'leavingDate'] = dep_date
198+ params[u'dateLeavingTime'] = dep_time
199+ params[u'returningDate'] = ret_date
200+ params[u'dateReturningTime'] = ret_time
201+ etree = get_html_parse_tree('http://travel.travelocity.com/flights/InitialSearch.do', data=urlencode(params), treetype='etree')
202+ while True:
203+ script = [script for script in etree.getiterator(u'script')][1]
204+ matches = script.text and re.search(r'var finurl = "(.*)"', script.text)
205+ if matches:
206+ url = u'http://travel.travelocity.com/flights/%s' % matches.group(1)
207+ etree = get_html_parse_tree(url, treetype=u'etree')
208+ else:
209+ break
210+
211+ # Handle error
212+ div = [d for d in etree.getiterator(u'div') if d.get(u'class') == u'e_content']
213+ if len(div):
214+ error = div[0].find(u'h3').text
215+ raise FlightException(error)
216+
217+ departing_flights = self._parse_travelocity(etree)
218+ return_url = None
219+ table = [t for t in etree.getiterator(u'table') if t.get(u'id') == u'tfGrid'][0]
220+ for tr in table.getiterator(u'tr'):
221+ for td in tr.getiterator(u'td'):
222+ if td.get(u'class').strip() in [u'tfPrice', u'tfPriceOrButton']:
223+ onclick = td.find(u'div/button').get(u'onclick')
224+ match = re.search(r"location.href='\.\./flights/(.+)'", onclick)
225+ url_page = match.group(1)
226+ match = re.search(r'^(.*?)[^/]*$', url)
227+ url_base = match.group(1)
228+ return_url = url_base + url_page
229+
230+ etree = get_html_parse_tree(return_url, treetype=u'etree')
231+ returning_flights = self._parse_travelocity(etree)
232+
233+ return (departing_flights, returning_flights, url)
234+
235+ def _parse_travelocity(self, etree):
236+ flights = []
237+ table = [t for t in etree.getiterator(u'table') if t.get(u'id') == u'tfGrid'][0]
238+ trs = [t for t in table.getiterator(u'tr')]
239+ tr_index = 1
240+ while tr_index < len(trs):
241+ tds = []
242+ while True:
243+ new_tds = [t for t in trs[tr_index].getiterator(u'td')]
244+ tds.extend(new_tds)
245+ tr_index += 1
246+ if len(filter(lambda t: t.get(u'class').strip() == u'tfAirlineSeatsMR', new_tds)):
247+ break
248+ flight = Flight()
249+ for td in tds:
250+ if td.get(u'class').strip() == u'tfAirline':
251+ anchor = td.find(u'a')
252+ if anchor is not None:
253+ airline = anchor.text.strip()
254+ else:
255+ airline = td.text.split(u'\n')[0].strip()
256+ flight.flight.append(u'%s %s' % (airline, td.findtext(u'div').strip()))
257+ if td.get(u'class').strip() == u'tfDepart' and td.text:
258+ flight.depart_time = td.text.split(u'\n')[0].strip()
259+ flight.depart_ap = u'%s %s' % (td.findtext(u'div').strip(),
260+ td.findtext(u'div/span').strip())
261+ if td.get(u'class').strip() == u'tfArrive' and td.text:
262+ flight.arrive_time = td.text.split(u'\n')[0].strip()
263+ span = td.find(u'span')
264+ if span is not None and span.get(u'class').strip() == u'tfNextDayDate':
265+ flight.arrive_time = u'%s %s' % (flight.arrive_time, span.text.strip()[2:])
266+ span = [s for s in td.find(u'div').getiterator(u'span')][1]
267+ flight.arrive_ap = u'%s %s' % (td.findtext(u'div').strip(),
268+ span.text.strip())
269+ else:
270+ flight.arrive_ap = u'%s %s' % (td.findtext(u'div').strip(),
271+ td.findtext(u'div/span').strip())
272+ if td.get(u'class').strip() == u'tfTime' and td.text:
273+ flight.duration = td.text.strip()
274+ flight.stops = td.findtext(u'span/a').strip()
275+ if td.get(u'class').strip() in [u'tfPrice', u'tfPriceOr'] and td.text:
276+ flight.price = td.text.strip()
277+ flight.flight = human_join(flight.flight)
278+ flights.append(flight)
279+
280+ return flights
281+
282+ @match(r'^(?:(cheapest|quickest)\s+)?flights?\s+from\s+(.+)\s+to\s+(.+)\s+from\s+(%s)\s+to\s+(%s)$' % (DATE, DATE))
283+ def flight_search(self, event, priority, dpt, to, dep_date, ret_date):
284+ try:
285+ flights = self._flight_search(event, dpt, to, dep_date, ret_date)
286+ except FlightException, e:
287+ event.addresponse(unicode(e))
288+ return
289+ if flights is None:
290+ return
291+ if len(flights[0]) == 0:
292+ event.addresponse(u'No matching departure flights found')
293+ return
294+ if len(flights[1]) == 0:
295+ event.addresponse(u'No matching return flights found')
296+ return
297+
298+ cmp = None
299+ if priority is not None:
300+ priority = priority.lower()
301+ if priority == u'cheapest':
302+ cmp = lambda a, b: a.int_price() < b.int_price()
303+ elif priority == u'quickest':
304+ cmp = lambda a, b: a.int_duration() < b.int_duration()
305+ if cmp:
306+ # select best flight based on priority
307+ for i in xrange(2):
308+ flights[i].sort(cmp=cmp)
309+ del flights[i][1:]
310+ response = []
311+ for i, flight_type in zip(xrange(2), [u'Departing', u'Returning']):
312+ if len(flights[i]) > 1:
313+ response.append(u'%s flights:' % flight_type)
314+ for flight in flights[i][:self.max_results]:
315+ leading = u''
316+ if len(flights[i]) == 1:
317+ leading = u'%s flight: ' % flight_type
318+ response.append(u'%(leading)s%(flight)s departing %(depart_time)s from %(depart_airport)s, arriving %(arrive_time)s at %(arrive_airport)s (flight time %(duration)s, %(stops)s) costs %(price)s per person' % {
319+ 'leading': leading,
320+ 'flight': flight.flight,
321+ 'depart_time': flight.depart_time,
322+ 'depart_airport': flight.depart_ap,
323+ 'arrive_time': flight.arrive_time,
324+ 'arrive_airport': flight.arrive_ap,
325+ 'duration': flight.duration,
326+ 'stops': flight.stops,
327+ 'price': flight.price or 'unknown'
328+ })
329+ response.append(u'Full results: %s' % flights[2])
330+ event.addresponse(u'\n'.join(response), conflate=False)
331+
332 # vi: set et sta sw=4 ts=4:
333
334=== modified file 'ibid/plugins/sources.py'
335--- ibid/plugins/sources.py 2010-02-20 21:58:38 +0000
336+++ ibid/plugins/sources.py 2010-04-22 14:08:19 +0000
337@@ -74,6 +74,19 @@
338 source.change_nick(nick)
339 event.addresponse(u'Changing nick to %s', nick)
340
341+class Invited(Processor):
342+ feature = ('actions',)
343+
344+ event_types = ('invite',)
345+ permission = 'sources'
346+
347+ @handler
348+ @authorise()
349+ def invited(self, event):
350+ event.addresponse(u'Joining %s', event.channel,
351+ target=event.sender['nick'])
352+ ibid.sources[event.source].join(event.channel)
353+
354 class NickServ(Processor):
355 event_types = (u'notice',)
356
357
358=== modified file 'ibid/source/irc.py'
359--- ibid/source/irc.py 2010-02-04 11:06:58 +0000
360+++ ibid/source/irc.py 2010-04-22 14:08:19 +0000
361@@ -168,6 +168,18 @@
362 message = unicode(message, 'utf-8', 'replace')
363 self._state_event(kickee, channel, u'kicked', kicker, message)
364
365+ def invited(self, user, channel):
366+ user = unicode(user, 'utf-8', 'replace')
367+ channel = unicode(channel, 'utf-8', 'replace')
368+
369+ event = self._create_event(u'invite', user, channel)
370+ event.public = False
371+ event.addressed = True
372+ self.factory.log.debug(u'Invited into %s by %s',
373+ channel,
374+ event.sender['id'])
375+ ibid.dispatcher.dispatch(event).addCallback(self.respond)
376+
377 def respond(self, event):
378 for response in event.responses:
379 self.send(response)
380@@ -236,6 +248,8 @@
381 self.do_auth_callback(params[1], True)
382 elif command == "RPL_ENDOFWHOIS":
383 self.do_auth_callback(params[1], False)
384+ elif command == 'INVITE' and params[0].lower() == self.nickname.lower():
385+ self.invited(prefix, params[1])
386
387 def irc_RPL_BOUNCE(self, prefix, params):
388 # Broken in IrcClient :/
389
390=== modified file 'ibid/source/silc.py'
391--- ibid/source/silc.py 2010-02-04 15:07:38 +0000
392+++ ibid/source/silc.py 2010-04-22 14:08:19 +0000
393@@ -169,6 +169,15 @@
394 del self.users[self._to_hex(user.user_id)]
395 del self.users[self._to_hex(user.fingerprint)]
396
397+ def notify_invite(self, channel, channel_name, inviter):
398+ event = self._create_event(u'invite', inviter, channel)
399+ event.public = False
400+ event.addressed = True
401+ self.factory.log.debug(u'Invited into %s by %s',
402+ channel_name,
403+ event.sender['id'])
404+ ibid.dispatcher.dispatch(event).addCallback(self.respond)
405+
406 def running(self):
407 self.connect_to_server(self.factory.server, self.factory.port)
408

Subscribers

People subscribed via source and target branches