]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/blog.py
Add bot that automatically tweets some things
[ipfire.org.git] / src / backend / blog.py
CommitLineData
0a6875dc
MT
1#!/usr/bin/python
2
541c952b 3import datetime
c70a7c29 4import feedparser
2de5ad8a 5import markdown2
c70a7c29 6import re
7e64f6a3 7import textile
d1a77356 8import tornado.gen
c70a7c29 9import unicodedata
7e64f6a3 10
0a6875dc 11from . import misc
9523790a 12from . import util
a3a850a4 13from .decorators import *
0a6875dc 14
2de5ad8a
MT
15# Used to automatically link some things
16link_patterns = (
17 # Find bug reports
18 (re.compile(r"(?:#(\d+))", re.I), r"https://bugzilla.ipfire.org/show_bug.cgi?id=\1"),
19
20 # Email Addresses
21 (re.compile(r"([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)"), r"mailto:\1"),
22
23 # CVE Numbers
5c6aa47c 24 (re.compile(r"(?:CVE)[\s\-](\d{4}\-\d+)"), r"https://cve.mitre.org/cgi-bin/cvename.cgi?name=\1"),
2de5ad8a
MT
25)
26
0a6875dc
MT
27class Blog(misc.Object):
28 def _get_post(self, query, *args):
29 res = self.db.get(query, *args)
30
31 if res:
32 return Post(self.backend, res.id, data=res)
33
34 def _get_posts(self, query, *args):
35 res = self.db.query(query, *args)
36
37 for row in res:
38 yield Post(self.backend, row.id, data=row)
39
487417ad
MT
40 def get_by_id(self, id):
41 return self._get_post("SELECT * FROM blog \
42 WHERE id = %s", id)
43
df157ede
MT
44 def get_by_slug(self, slug, published=True):
45 if published:
46 return self._get_post("SELECT * FROM blog \
47 WHERE slug = %s AND published_at <= NOW()", slug)
48
0a6875dc 49 return self._get_post("SELECT * FROM blog \
df157ede
MT
50 WHERE slug = %s", slug)
51
0a6875dc
MT
52 def get_newest(self, limit=None):
53 return self._get_posts("SELECT * FROM blog \
54 WHERE published_at IS NOT NULL \
55 AND published_at <= NOW() \
56 ORDER BY published_at DESC LIMIT %s", limit)
57
58 def get_by_tag(self, tag, limit=None):
59 return self._get_posts("SELECT * FROM blog \
60 WHERE published_at IS NOT NULL \
61 AND published_at <= NOW() \
62 AND %s = ANY(tags) \
4bde7f18 63 ORDER BY published_at DESC LIMIT %s", tag, limit)
0a6875dc 64
cdf85ee7 65 def get_by_author(self, author, limit=None):
0a6875dc 66 return self._get_posts("SELECT * FROM blog \
cdf85ee7 67 WHERE (author = %s OR author_uid = %s) \
0a6875dc
MT
68 AND published_at IS NOT NULL \
69 AND published_at <= NOW() \
cdf85ee7
MT
70 ORDER BY published_at DESC LIMIT %s",
71 author.name, author.uid, limit)
0a6875dc 72
7e64f6a3
MT
73 def get_by_year(self, year):
74 return self._get_posts("SELECT * FROM blog \
75 WHERE EXTRACT(year FROM published_at) = %s \
76 AND published_at IS NOT NULL \
77 AND published_at <= NOW() \
78 ORDER BY published_at DESC", year)
79
0b342a05
MT
80 def get_drafts(self, author=None, limit=None):
81 if author:
82 return self._get_posts("SELECT * FROM blog \
83 WHERE author_uid = %s \
84 AND (published_at IS NULL OR published_at > NOW()) \
85 ORDER BY COALESCE(updated_at, created_at) DESC LIMIT %s",
86 author.uid, limit)
87
88 return self._get_posts("SELECT * FROM blog \
89 WHERE (published_at IS NULL OR published_at > NOW()) \
90 ORDER BY COALESCE(updated_at, created_at) DESC LIMIT %s", limit)
91
0a6875dc 92 def search(self, query, limit=None):
9523790a 93 query = util.parse_search_query(query)
a47ddc3d 94
0a6875dc
MT
95 return self._get_posts("SELECT blog.* FROM blog \
96 LEFT JOIN blog_search_index search_index ON blog.id = search_index.post_id \
97 WHERE search_index.document @@ to_tsquery('english', %s) \
98 ORDER BY ts_rank(search_index.document, to_tsquery('english', %s)) DESC \
99 LIMIT %s", query, query, limit)
100
611adbfb
MT
101 def has_had_recent_activity(self, t=None):
102 if t is None:
103 t = datetime.timedelta(hours=24)
104
105 res = self.db.get("SELECT COUNT(*) AS count FROM blog \
106 WHERE published_at IS NOT NULL AND published_at >= NOW() - %s", t)
107
108 if res and res.count > 0:
109 return True
110
111 return False
112
694c4f08 113 def create_post(self, title, text, author, tags=[], lang="markdown"):
541c952b
MT
114 """
115 Creates a new post and returns the resulting Post object
116 """
694c4f08
MT
117 # Pre-render HTML
118 html = self._render_text(text, lang=lang)
119
120 return self._get_post("INSERT INTO blog(title, slug, text, html, lang, author_uid, tags) \
121 VALUES(%s, %s, %s, %s, %s, %s, %s) RETURNING *", title, self._make_slug(title), text,
122 html, lang, author.uid, list(tags))
541c952b 123
c70a7c29
MT
124 def _make_slug(self, s):
125 # Remove any non-ASCII characters
126 try:
127 s = unicodedata.normalize("NFKD", s)
128 except TypeError:
129 pass
130
131 # Remove excessive whitespace
132 s = re.sub(r"[^\w]+", " ", s)
133
134 slug = "-".join(s.split()).lower()
135
136 while True:
137 e = self.db.get("SELECT 1 FROM blog WHERE slug = %s", slug)
138 if not e:
139 break
140
141 slug += "-"
142
143 return slug
144
2de5ad8a
MT
145 def _render_text(self, text, lang="markdown"):
146 if lang == "markdown":
147 return markdown2.markdown(text, link_patterns=link_patterns,
e0ef6d39
MT
148 extras=[
149 "code-friendly",
150 "cuddled-lists",
151 "fenced-code-blocks",
152 "footnotes",
153 "header-ids",
154 "link-patterns",
155 "tables",
156 ])
2de5ad8a
MT
157
158 elif lang == "textile":
159 return textile.textile(text)
160
161 return text
162
0a6875dc
MT
163 def refresh(self):
164 """
165 Needs to be called after a post has been changed
166 and updates the search index.
167 """
168 self.db.execute("REFRESH MATERIALIZED VIEW blog_search_index")
169
7e64f6a3
MT
170 @property
171 def years(self):
172 res = self.db.query("SELECT DISTINCT EXTRACT(year FROM published_at)::integer AS year \
173 FROM blog WHERE published_at IS NOT NULL AND published_at <= NOW() \
174 ORDER BY year DESC")
175
176 for row in res:
177 yield row.year
178
d1a77356 179 @tornado.gen.coroutine
c70a7c29
MT
180 def update_feeds(self):
181 """
182 Updates all enabled feeds
183 """
184 for feed in self.db.query("SELECT * FROM blog_feeds WHERE enabled IS TRUE"):
185 try:
186 f = feedparser.parse(feed.url)
187 except Exception as e:
188 raise e
189
190 with self.db.transaction():
191 # Update name
192 self.db.execute("UPDATE blog_feeds SET name = %s \
193 WHERE id = %s", f.feed.title, feed.id)
194
195 # Walk through all entries
196 for entry in f.entries:
197 # Skip everything without the "blog.ipfire.org" tag
198 try:
199 tags = list((t.term for t in entry.tags))
200
201 if not "blog.ipfire.org" in tags:
202 continue
203 except AttributeError:
204 continue
205
206 # Get link to the posting site
207 link = entry.links[0].href
208
209 # Check if the entry has already been imported
210 res = self.db.get("SELECT id, (updated_at < %s) AS needs_update \
211 FROM blog WHERE feed_id = %s AND foreign_id = %s",
212 entry.updated, feed.id, entry.id)
213 if res:
214 # If the post needs to be updated, we do so
215 if res.needs_update:
216 self.db.execute("UPDATE blog SET title = %s, author = %s, \
217 published_at = %s, updated_at = %s, html = %s, link = %s, \
218 tags = %s WHERE id = %s", entry.title, entry.author,
219 entry.published, entry.updated, entry.summary, link,
220 feed.tags + tags, res.id)
221
222 # Done here
223 continue
224
225 # Insert the new post
226 self.db.execute("INSERT INTO blog(title, slug, author, \
227 published_at, html, link, tags, updated_at, feed_id, foreign_id) \
228 VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
229 entry.title, self._make_slug(entry.title), entry.author,
230 entry.published, entry.summary, link, feed.tags + tags,
231 entry.updated, feed.id, entry.id)
232
20277bf5
MT
233 # Mark feed as updated
234 self.db.execute("UPDATE blog_feeds SET last_updated_at = CURRENT_TIMESTAMP \
235 WHERE id = %s" % feed.id)
236
c70a7c29
MT
237 # Refresh the search index
238 with self.db.transaction():
239 self.refresh()
240
0a6875dc
MT
241
242class Post(misc.Object):
243 def init(self, id, data=None):
244 self.id = id
245 self.data = data
246
541c952b
MT
247 # Title
248
93725180
MT
249 @property
250 def title(self):
0a6875dc
MT
251 return self.data.title
252
253 @property
254 def slug(self):
255 return self.data.slug
256
a3a850a4 257 @lazy_property
0a6875dc
MT
258 def author(self):
259 if self.data.author_uid:
260 return self.backend.accounts.get_by_uid(self.data.author_uid)
261
cdf85ee7
MT
262 return self.data.author
263
0a6875dc
MT
264 @property
265 def created_at(self):
266 return self.data.created_at
267
2de5ad8a
MT
268 @property
269 def lang(self):
270 return self.data.lang
271
541c952b
MT
272 # Published?
273
0a6875dc
MT
274 @property
275 def published_at(self):
276 return self.data.published_at
277
541c952b
MT
278 def is_published(self):
279 """
280 Returns True if the post is already published
281 """
282 return self.published_at and self.published_at <= datetime.datetime.now()
283
9ea64cef 284 def publish(self, when=None):
541c952b
MT
285 if self.is_published():
286 return
287
9ea64cef
MT
288 self.db.execute("UPDATE blog SET published_at = COALESCE(%s, CURRENT_TIMESTAMP) \
289 WHERE id = %s", when, self.id)
541c952b
MT
290
291 # Update search indices
292 self.backend.blog.refresh()
293
294 # Updated?
295
7e64f6a3 296 @property
541c952b
MT
297 def updated_at(self):
298 return self.data.updated_at
299
541c952b
MT
300 # Text
301
93725180
MT
302 @property
303 def text(self):
541c952b
MT
304 return self.data.text
305
541c952b 306 # HTML
7e64f6a3 307
a3a850a4 308 @lazy_property
0a6875dc
MT
309 def html(self):
310 """
311 Returns this post as rendered HTML
312 """
2de5ad8a 313 return self.data.html or self.backend.blog._render_text(self.text, lang=self.lang)
8ebc98d4 314
541c952b
MT
315 # Tags
316
93725180
MT
317 @property
318 def tags(self):
8ebc98d4 319 return self.data.tags
1e76fec4 320
93725180 321 # Link
541c952b 322
1e76fec4
MT
323 @property
324 def link(self):
325 return self.data.link
984e4e7b 326
a3a850a4 327 @lazy_property
984e4e7b
MT
328 def release(self):
329 return self.backend.releases._get_release("SELECT * FROM releases \
330 WHERE published IS NOT NULL AND published <= NOW() AND blog_id = %s", self.id)
e8a81a70
MT
331
332 def is_editable(self, editor):
333 # Authors can edit their own posts
334 return self.author == editor
93725180
MT
335
336 def update(self, title, text, tags=[]):
337 """
338 Called to update the content of this post
339 """
340 # Update slug when post isn't published yet
baa294fb
MT
341 slug = self.backend.blog._make_slug(title) \
342 if not self.is_published() and not self.title == title else self.slug
93725180 343
694c4f08
MT
344 # Render and cache HTML
345 html = self.backend.blog._render_text(text, lang=self.lang)
93725180 346
694c4f08 347 self.db.execute("UPDATE blog SET title = %s, slug = %s, text = %s, html = %s, \
93725180 348 tags = %s, updated_at = CURRENT_TIMESTAMP WHERE id = %s",
694c4f08 349 title, slug, text, html, list(tags), self.id)
93725180
MT
350
351 # Update cache
352 self.data.update({
353 "title" : title,
354 "slug" : slug,
355 "text" : text,
d73edae7 356 "html" : html,
93725180
MT
357 "tags" : tags,
358 })
359
360 # Update search index if post is published
361 if self.is_published():
362 self.backend.blog.refresh()
914238a5
MT
363
364 def delete(self):
365 self.db.execute("DELETE FROM blog WHERE id = %s", self.id)
366
367 # Update search indices
368 self.backend.blog.refresh()