Merge lp:~max-rabkin/ibid/invite-502294 into lp:~ibid-core/ibid/old-release-0.1-1.6
- invite-502294
- Merge into 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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stefano Rivera | Needs Fixing | ||
Review via email: mp+23934@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
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 |
Looks like this isn't mergable into 0.1
bzr rebase --onto 908 -r 911..