| 1 | | # -*- coding: utf-8 -*- |
|---|
| 2 | | import os |
|---|
| 3 | | import re |
|---|
| 4 | | import calendar |
|---|
| 5 | | from datetime import datetime |
|---|
| 6 | | from trac.core import * |
|---|
| 7 | | from trac.perm import IPermissionRequestor |
|---|
| 8 | | from trac.config import Option |
|---|
| 9 | | from trac.web.chrome import INavigationContributor, ITemplateProvider, \ |
|---|
| 10 | | add_stylesheet |
|---|
| 11 | | from trac.web.main import IRequestHandler |
|---|
| 12 | | from trac.util.html import escape, html, Markup |
|---|
| 13 | | from trac.util.text import to_unicode |
|---|
| 14 | | |
|---|
| 15 | | |
|---|
| 16 | | class IrclogsPlugin(Component): |
|---|
| 17 | | implements(INavigationContributor, ITemplateProvider, IRequestHandler, \ |
|---|
| 18 | | IPermissionRequestor) |
|---|
| 19 | | _url_re = re.compile(r'^/irclogs(/(?P<year>\d{4})(/(?P<month>\d{2})' |
|---|
| 20 | | r'(/(?P<day>\d{2}))?)?)?/?$') |
|---|
| 21 | | # TODO: make the line format somewhat configurable |
|---|
| 22 | | # Uncomment the following line if using a pipe as a divider and a space |
|---|
| 23 | | # between the date adn time. Make sure to comment out the existing |
|---|
| 24 | | # _line_re. |
|---|
| 25 | | # _line_re = re.compile('%s %s \| (%s)$' % ( |
|---|
| 26 | | _line_re = re.compile('%sT%s (%s)$' % ( |
|---|
| 27 | | r'(?P<date>\d{4}-\d{2}-\d{2})', |
|---|
| 28 | | r'(?P<time>\d{2}:\d{2}:\d{2})', |
|---|
| 29 | | '|'.join([ |
|---|
| 30 | | r'(<(?P<c_nickname>.*?)> (?P<c_text>.*?))', |
|---|
| 31 | | r'(\* (?P<a_nickname>.*?) (?P<a_text>.*?))', |
|---|
| 32 | | r'(\*\*\* (?P<s_nickname>.*?) (?P<s_text>.*?))' |
|---|
| 33 | | ])) |
|---|
| 34 | | ) |
|---|
| 35 | | charset = Option('irclogs', 'charset', 'utf-8', |
|---|
| 36 | | doc='Channel charset') |
|---|
| 37 | | file_format = Option('irclogs', 'file_format', '#channel.%Y-%m-%d.log', |
|---|
| 38 | | doc='Format of a logfile for a given day. Must ' |
|---|
| 39 | | 'include %Y, %m and %d. Example: ' |
|---|
| 40 | | '#channel.%Y-%m-%d.log') |
|---|
| 41 | | path = Option('irclogs', 'path', '', |
|---|
| 42 | | doc='The path where the irc logfiles are') |
|---|
| 43 | | navbutton = Option('irclogs', 'navigation_button', '', |
|---|
| 44 | | doc='If not empty an button with this value as caption ' |
|---|
| 45 | | 'is added to the navigation bar, pointing to the ' |
|---|
| 46 | | 'irc plugin') |
|---|
| 47 | | |
|---|
| 48 | | def _to_unicode(self, iterable): |
|---|
| 49 | | for line in iterable: |
|---|
| 50 | | yield to_unicode(line, self.charset) |
|---|
| 51 | | |
|---|
| 52 | | def _get_file_re(self): |
|---|
| 53 | | return re.compile(r'^%s$' % re.escape(self.file_format) |
|---|
| 54 | | .replace('\\%Y', '(?P<year>\d{4})') |
|---|
| 55 | | .replace('\\%m', '(?P<month>\d{2})') |
|---|
| 56 | | .replace('\\%d', '(?P<day>\d{2})') |
|---|
| 57 | | ) |
|---|
| 58 | | |
|---|
| 59 | | def _get_filename(self, year, month, day): |
|---|
| 60 | | return os.path.join(self.path, self.file_format |
|---|
| 61 | | .replace('%Y', str(year)) |
|---|
| 62 | | .replace('%m', str(month)) |
|---|
| 63 | | .replace('%d', str(day)) |
|---|
| 64 | | ) |
|---|
| 65 | | |
|---|
| 66 | | def _render_lines(self, iterable): |
|---|
| 67 | | dummy = lambda: {} |
|---|
| 68 | | result = [] |
|---|
| 69 | | for line in iterable: |
|---|
| 70 | | d = getattr(self._line_re.search(line), 'groupdict', dummy)() |
|---|
| 71 | | for mode in ('channel', 'action', 'server'): |
|---|
| 72 | | prefix = mode[0] |
|---|
| 73 | | text = d.get('%s_text' % prefix) |
|---|
| 74 | | if not text is None: |
|---|
| 75 | | nick = d['%s_nickname' % prefix] |
|---|
| 76 | | break |
|---|
| 77 | | else: |
|---|
| 78 | | continue |
|---|
| 79 | | result.append({ |
|---|
| 80 | | 'date': d['date'], |
|---|
| 81 | | 'time': d['time'], |
|---|
| 82 | | 'mode': mode, |
|---|
| 83 | | 'text': text, |
|---|
| 84 | | 'nickname': nick, |
|---|
| 85 | | 'nickcls': 'nick-%d' % (sum(ord(c) for c in nick) % 8) |
|---|
| 86 | | }) |
|---|
| 87 | | return result |
|---|
| 88 | | |
|---|
| 89 | | def _generate_calendar(self, req, entries): |
|---|
| 90 | | if not req.args['year'] is None: |
|---|
| 91 | | year = int(req.args['year']) |
|---|
| 92 | | else: |
|---|
| 93 | | year = datetime.now().year |
|---|
| 94 | | if not req.args['month'] is None: |
|---|
| 95 | | month = int(req.args['month']) |
|---|
| 96 | | else: |
|---|
| 97 | | month = datetime.now().month |
|---|
| 98 | | if not req.args['day'] is None: |
|---|
| 99 | | today = int(req.args['day']) |
|---|
| 100 | | else: |
|---|
| 101 | | today = -1 |
|---|
| 102 | | this_month_entries = entries.get(year, {}).get(month, {}) |
|---|
| 103 | | |
|---|
| 104 | | weeks = [] |
|---|
| 105 | | for week in calendar.monthcalendar(year, month): |
|---|
| 106 | | w = [] |
|---|
| 107 | | for day in week: |
|---|
| 108 | | if not day: |
|---|
| 109 | | w.append({ |
|---|
| 110 | | 'empty': True |
|---|
| 111 | | }) |
|---|
| 112 | | else: |
|---|
| 113 | | w.append({ |
|---|
| 114 | | 'caption': day, |
|---|
| 115 | | 'href': req.href('irclogs', year, |
|---|
| 116 | | '%02d' % month, '%02d' % day), |
|---|
| 117 | | 'today': day == today, |
|---|
| 118 | | 'has_log': day in this_month_entries |
|---|
| 119 | | }) |
|---|
| 120 | | weeks.append(w) |
|---|
| 121 | | |
|---|
| 122 | | next_month_year = year |
|---|
| 123 | | next_month = int(month) + 1 |
|---|
| 124 | | if next_month > 12: |
|---|
| 125 | | next_month_year += 1 |
|---|
| 126 | | next_month = 1 |
|---|
| 127 | | if today > -1: |
|---|
| 128 | | next_month_href = req.href('irclogs', next_month_year, |
|---|
| 129 | | '%02d' % next_month, '%02d' % today) |
|---|
| 130 | | else: |
|---|
| 131 | | next_month_href = req.href('irclogs', next_month_year, |
|---|
| 132 | | '%02d' % next_month) |
|---|
| 133 | | |
|---|
| 134 | | prev_month_year = year |
|---|
| 135 | | prev_month = int(month) - 1 |
|---|
| 136 | | if prev_month < 1: |
|---|
| 137 | | prev_month_year -= 1 |
|---|
| 138 | | prev_month = 12 |
|---|
| 139 | | if today > -1: |
|---|
| 140 | | prev_month_href = req.href('irclogs', prev_month_year, |
|---|
| 141 | | '%02d' % prev_month, '%02d' % today) |
|---|
| 142 | | else: |
|---|
| 143 | | prev_month_href = req.href('irclogs', prev_month_year, |
|---|
| 144 | | '%02d' % prev_month) |
|---|
| 145 | | |
|---|
| 146 | | return { |
|---|
| 147 | | 'year': year, |
|---|
| 148 | | 'month': month, |
|---|
| 149 | | 'weeks': weeks, |
|---|
| 150 | | 'next_year': { |
|---|
| 151 | | 'caption': str(year + 1), |
|---|
| 152 | | 'href': req.href('irclogs', year + 1) |
|---|
| 153 | | }, |
|---|
| 154 | | 'prev_year': { |
|---|
| 155 | | 'caption': str(year - 1), |
|---|
| 156 | | 'href': req.href('irclogs', year - 1) |
|---|
| 157 | | }, |
|---|
| 158 | | 'next_month': { |
|---|
| 159 | | 'caption': '%02d' % next_month, |
|---|
| 160 | | 'href': next_month_href |
|---|
| 161 | | }, |
|---|
| 162 | | 'prev_month': { |
|---|
| 163 | | 'caption': '%02d' % prev_month, |
|---|
| 164 | | 'href': prev_month_href |
|---|
| 165 | | }, |
|---|
| 166 | | } |
|---|
| 167 | | |
|---|
| 168 | | # INavigationContributor methods |
|---|
| 169 | | def get_active_navigation_item(self, req): |
|---|
| 170 | | if self.navbutton.strip(): |
|---|
| 171 | | return 'irclogs' |
|---|
| 172 | | |
|---|
| 173 | | def get_navigation_items(self, req): |
|---|
| 174 | | if req.perm.has_permission('IRCLOGS_VIEW'): |
|---|
| 175 | | title = self.navbutton.strip() |
|---|
| 176 | | if title: |
|---|
| 177 | | yield 'mainnav', 'irclogs', html.A(title, href=req.href.irclogs()) |
|---|
| 178 | | |
|---|
| 179 | | # IPermissionHandler methods |
|---|
| 180 | | def get_permission_actions(self): |
|---|
| 181 | | return ['IRCLOGS_VIEW'] |
|---|
| 182 | | |
|---|
| 183 | | # IRequestHandler methods |
|---|
| 184 | | def match_request(self, req): |
|---|
| 185 | | m = self._url_re.search(req.path_info) |
|---|
| 186 | | if m is None: |
|---|
| 187 | | return False |
|---|
| 188 | | req.args.update(m.groupdict()) |
|---|
| 189 | | return True |
|---|
| 190 | | |
|---|
| 191 | | def process_request(self, req): |
|---|
| 192 | | req.perm.assert_permission('IRCLOGS_VIEW') |
|---|
| 193 | | add_stylesheet(req, 'irclogs/style.css') |
|---|
| 194 | | |
|---|
| 195 | | file_re = self._get_file_re() |
|---|
| 196 | | |
|---|
| 197 | | context = {} |
|---|
| 198 | | entries = {} |
|---|
| 199 | | files = os.listdir(self.path) |
|---|
| 200 | | files.sort() |
|---|
| 201 | | for fn in files: |
|---|
| 202 | | m = file_re.search(fn) |
|---|
| 203 | | if m is None: |
|---|
| 204 | | continue |
|---|
| 205 | | d = m.groupdict() |
|---|
| 206 | | y = entries.setdefault(int(d['year']), {}) |
|---|
| 207 | | m = y.setdefault(int(d['month']), {}) |
|---|
| 208 | | m[int(d['day'])] = True |
|---|
| 209 | | |
|---|
| 210 | | if req.args['year'] is None: |
|---|
| 211 | | years = entries.keys() |
|---|
| 212 | | years.sort() |
|---|
| 213 | | context['years'] = [{ |
|---|
| 214 | | 'caption': y, |
|---|
| 215 | | 'href': req.href('irclogs', y) |
|---|
| 216 | | } for y in years] |
|---|
| 217 | | context['viewmode'] = 'years' |
|---|
| 218 | | elif req.args['month'] is None: |
|---|
| 219 | | months = entries.get(int(req.args['year']), {}).keys() |
|---|
| 220 | | months.sort() |
|---|
| 221 | | context['months'] = [{ |
|---|
| 222 | | 'caption': m, |
|---|
| 223 | | 'href': req.href('irclogs', req.args['year'], |
|---|
| 224 | | '%02d' % m) |
|---|
| 225 | | } for m in months] |
|---|
| 226 | | context['year'] = req.args['year'] |
|---|
| 227 | | context['viewmode'] = 'months' |
|---|
| 228 | | elif req.args['day'] is None: |
|---|
| 229 | | year = entries.get(int(req.args['year']), {}) |
|---|
| 230 | | days = year.get(int(req.args['month']), {}).keys() |
|---|
| 231 | | days.sort() |
|---|
| 232 | | context['days'] = [{ |
|---|
| 233 | | 'caption': d, |
|---|
| 234 | | 'href': req.href('irclogs', req.args['year'], |
|---|
| 235 | | req.args['month'], '%02d' % d) |
|---|
| 236 | | } for d in days] |
|---|
| 237 | | context['year'] = req.args['year'] |
|---|
| 238 | | context['month'] = req.args['month'] |
|---|
| 239 | | context['viewmode'] = 'days' |
|---|
| 240 | | |
|---|
| 241 | | context['cal'] = self._generate_calendar(req, entries) |
|---|
| 242 | | |
|---|
| 243 | | if req.args['day'] is not None: |
|---|
| 244 | | logfile = self._get_filename(req.args['year'], req.args['month'], |
|---|
| 245 | | req.args['day']) |
|---|
| 246 | | context['day'] = req.args['day'] |
|---|
| 247 | | context['month'] = req.args['month'] |
|---|
| 248 | | context['year'] = req.args['year'] |
|---|
| 249 | | context['viewmode'] = 'day' |
|---|
| 250 | | |
|---|
| 251 | | if not os.path.exists(logfile): |
|---|
| 252 | | context['missing'] = True |
|---|
| 253 | | else: |
|---|
| 254 | | context['missing'] = False |
|---|
| 255 | | f = file(logfile) |
|---|
| 256 | | try: |
|---|
| 257 | | context['lines'] = self._render_lines(self._to_unicode(f)) |
|---|
| 258 | | finally: |
|---|
| 259 | | f.close() |
|---|
| 260 | | |
|---|
| 261 | | return 'irclogs.html', context, None |
|---|
| 262 | | |
|---|
| 263 | | # ITemplateProvider methods |
|---|
| 264 | | def get_templates_dirs(self): |
|---|
| 265 | | from pkg_resources import resource_filename |
|---|
| 266 | | return [resource_filename(__name__, 'templates')] |
|---|
| 267 | | |
|---|
| 268 | | def get_htdocs_dirs(self): |
|---|
| 269 | | from pkg_resources import resource_filename |
|---|
| 270 | | return [('irclogs', resource_filename(__name__, 'htdocs'))] |
|---|
| | 1 | import web_ui |
|---|
| | 2 | import macros |
|---|
| | 3 | import search |
|---|
| | 4 | import wiki |
|---|
| | 5 | from console import update_irc_search |
|---|