__init__.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920
  1. #!./venv/bin/python3
  2. import bleach
  3. import bottle
  4. import configparser
  5. import dateutil.parser
  6. import functools
  7. import hashlib
  8. import importlib
  9. import json
  10. import markdown
  11. import timeago
  12. import urllib3
  13. import yaml
  14. from datetime import datetime, timezone
  15. from bottle import abort, get, jinja2_template as template, post, redirect, request, response, static_file
  16. from urllib.parse import urlparse
  17. # This is used to export the bottle object for a WSGI server
  18. # See passenger_wsgi.py
  19. application = bottle.app ()
  20. # Load user settings for this app
  21. with open ('settings.yaml', encoding='UTF-8') as file:
  22. settings = yaml.load (file)
  23. # Directories to search for app templates
  24. bottle.TEMPLATE_PATH = [ './freepost/templates' ]
  25. # Custom settings and functions for templates
  26. template = functools.partial (
  27. template,
  28. template_settings = {
  29. 'filters': {
  30. 'ago': lambda date: timeago.format(date),
  31. 'datetime': lambda date: date,# date.strftime ('%b %-d, %Y - %H:%M%p%z%Z'),
  32. # TODO this should be renamed. It's only a way to pretty print dates
  33. 'title': lambda date: dateutil.parser.parse(date).strftime('%b %-d, %Y - %H:%M%z%Z'),
  34. # Convert markdown to plain text
  35. 'md2txt': lambda text: bleach.clean (markdown.markdown (text),
  36. tags=[], attributes={}, styles=[], strip=True),
  37. # Convert markdown to html
  38. 'md2html': lambda text: bleach.clean (bleach.linkify (markdown.markdown (
  39. text,
  40. # https://python-markdown.github.io/extensions/
  41. extensions=[ 'extra', 'admonition', 'nl2br', 'smarty' ],
  42. output_format='html5'))),
  43. # Get the domain part of a URL
  44. 'netloc': lambda url: urlparse (url).netloc
  45. },
  46. 'globals': {
  47. 'new_messages': lambda user_id: database.count_unread_messages (user_id),
  48. 'now': lambda: datetime.now (timezone.utc),
  49. 'request': request,
  50. # Return a setting from 'settings.yaml'
  51. 'settings': lambda section, key: settings[section][key],
  52. # Get the current user of the session
  53. 'session_user': lambda: session.user (),
  54. # Split a string of topics into a list
  55. 'split_topics': lambda topics: [ topic for topic in topics.split (' ') ] if topics else [],
  56. 'url': application.get_url,
  57. },
  58. 'autoescape': True
  59. })
  60. # "bleach" library is used to sanitize the HTML output of jinja2's "md2html"
  61. # filter. The library has only a very restrictive list of white-listed
  62. # tags, so we add some more here.
  63. bleach.sanitizer.ALLOWED_TAGS += [ 'br', 'img', 'p', 'pre', 'h1', 'h2', 'h3' ]
  64. bleach.sanitizer.ALLOWED_ATTRIBUTES.update ({
  65. 'img': [ 'src' ]
  66. })
  67. from freepost import database, mail, session
  68. # Decorator.
  69. # Make sure user is logged in
  70. def requires_login (controller):
  71. def wrapper ():
  72. session_token = request.get_cookie (
  73. key = settings['session']['name'],
  74. secret = settings['cookies']['secret'])
  75. if database.is_valid_session (session_token):
  76. return controller ()
  77. else:
  78. redirect (application.get_url ('login'))
  79. return wrapper
  80. # Decorator.
  81. # Make sure user is logged out
  82. def requires_logout (controller):
  83. def wrapper ():
  84. session_token = request.get_cookie (
  85. key = settings['session']['name'],
  86. secret = settings['cookies']['secret'])
  87. if database.is_valid_session (session_token):
  88. redirect (application.get_url ('user_settings'))
  89. else:
  90. return controller ()
  91. return wrapper
  92. # Routes
  93. @get ('/', name='homepage')
  94. def homepage ():
  95. """
  96. Display homepage with posts sorted by 'hot'.
  97. """
  98. # Sort order
  99. sort = request.query.sort or 'hot'
  100. # Page number
  101. page = int (request.query.page or 0)
  102. if page < 0:
  103. redirect (application.get_url ('homepage'))
  104. user = session.user ()
  105. if sort in [ 'hot', 'new' ]:
  106. posts = database.get_posts (
  107. page, user['id'] if user else None,
  108. sort)
  109. else:
  110. posts = []
  111. # Disable browser caching
  112. # Fix issue: https://notabug.org/zPlus/freepost/issues/80
  113. response.set_header('cache-control', 'no-cache, no-store, must-revalidate')
  114. response.set_header('pragma', 'no-cache')
  115. response.set_header('expires', '0')
  116. return template ('homepage.html', posts=posts, sort=sort)
  117. # TODO implement this
  118. @get ('/topic/<name>', name='topic')
  119. def topic (name):
  120. """
  121. Display posts by topic.
  122. """
  123. # Sort order
  124. sort = request.query.sort or 'hot'
  125. # Page number
  126. page = int (request.query.page or 0)
  127. if page < 0:
  128. redirect (application.get_url ('homepage'))
  129. user = session.user ()
  130. if sort in [ 'hot', 'new' ]:
  131. posts = database.get_posts (
  132. page, user['id'] if user else None,
  133. sort, name)
  134. else:
  135. posts = []
  136. return template (
  137. 'homepage.html',
  138. topic=name,
  139. posts=posts)
  140. @get ('/about', name='about')
  141. def about ():
  142. """
  143. Display "About" page.
  144. """
  145. return template ('about.html')
  146. @get ('/login', name='login')
  147. @requires_logout
  148. def login ():
  149. """
  150. The login page.
  151. """
  152. return template ('login.html')
  153. @post ('/login')
  154. @requires_logout
  155. def login_check ():
  156. """
  157. Check login form.
  158. """
  159. username = request.forms.getunicode ('username')
  160. password = request.forms.getunicode ('password')
  161. remember = 'remember' in request.forms
  162. if not username or not password:
  163. return template (
  164. 'login.html',
  165. flash = 'Bad login!')
  166. # Check if user exists
  167. user = database.check_user_credentials (username, password)
  168. # Username/Password not working
  169. if not user:
  170. return template (
  171. 'login.html',
  172. flash = 'Bad login!')
  173. # Delete any existing "reset token"
  174. database.delete_password_reset_token (user['id'])
  175. # Start new session
  176. session.start (user['id'], remember)
  177. # Redirect logged in user to preferred feed
  178. if user['preferred_feed'] == 'new':
  179. redirect (application.get_url ('homepage') + '?sort=new')
  180. else:
  181. redirect (application.get_url ('homepage'))
  182. @get ('/register', name='register')
  183. @requires_logout
  184. def register ():
  185. """
  186. Register new account.
  187. """
  188. return template ('register.html')
  189. @post ('/register')
  190. @requires_logout
  191. def register_new_account ():
  192. """
  193. Check form for creating new account.
  194. """
  195. username = request.forms.getunicode ('username')
  196. password = request.forms.getunicode ('password')
  197. # Normalize username
  198. username = username.strip ()
  199. # Check if username already exists.
  200. # Use case-insensitive match to prevent two similar usernames.
  201. if len (username) == 0 or database.username_exists (username, case_sensitive=False):
  202. return template (
  203. 'register.html',
  204. flash='Name taken, please choose another.')
  205. # Password too short?
  206. if len (password) < 8:
  207. return template (
  208. 'register.html',
  209. flash = 'Password too short')
  210. # Username OK, Password OK: create new user
  211. database.new_user (username, password)
  212. # Retrieve user (check if it was created)
  213. user = database.check_user_credentials (username, password)
  214. # Something bad happened...
  215. if user is None:
  216. return template (
  217. 'register.html',
  218. flash = 'An error has occurred, please try again.')
  219. # Start session...
  220. session.start (user['id'])
  221. # ... and go to the homepage of the new user
  222. redirect (application.get_url ('user_settings'))
  223. @get ('/logout', name='logout')
  224. @requires_login
  225. def logout ():
  226. """
  227. Logout user and return to homepage.
  228. """
  229. session.close ()
  230. redirect (application.get_url ('homepage'))
  231. @get ('/password_reset', name='password_reset')
  232. @requires_logout
  233. def password_reset ():
  234. """
  235. Display form to reset users password.
  236. """
  237. return template ('login_reset.html')
  238. @post ('/password_reset', name='password_reset_send_code')
  239. @requires_logout
  240. def password_reset_send_code ():
  241. """
  242. Validate form for resetting password, and if valid send secret
  243. code via email.
  244. """
  245. username = request.forms.getunicode('username')
  246. email = request.forms.getunicode('email')
  247. if not username or not email:
  248. redirect(application.get_url('change_password'))
  249. user = database.get_user_by_username(username)
  250. if not user:
  251. redirect(application.get_url('change_password'))
  252. # Make sure the given email matches the one that we have in the database
  253. if user['email'] != email:
  254. redirect(application.get_url('change_password'))
  255. # Is there another valid token already (from a previous request)?
  256. # If yes, do not send another one (to prevent multiple requests or spam)
  257. if database.is_password_reset_token_valid(user['id']):
  258. redirect(application.get_url('change_password'))
  259. # Generate secret token to send via email
  260. secret_token = random.ascii_string(32)
  261. # Add token to database
  262. database.set_password_reset_token(user['id'], secret_token)
  263. # Send token via email
  264. client_ip = request.environ.get('HTTP_X_FORWARDED_FOR') or \
  265. request.environ.get('REMOTE_ADDR')
  266. email_to = user['email']
  267. email_subject = 'freepost password reset'
  268. email_body = template(
  269. 'email/password_reset.txt',
  270. ip=client_ip,
  271. secret_token=secret_token)
  272. mail.send(email_to, email_subject, email_body)
  273. redirect(application.get_url('change_password'))
  274. @get ('/change_password', name='change_password')
  275. @requires_logout
  276. def change_password ():
  277. """
  278. After the secret code was sent via email, display this form where
  279. the user can insert the secret code + new password.
  280. """
  281. return template ('login_change_password.html')
  282. @post ('/change_password', name='validate_new_password')
  283. @requires_logout
  284. def validate_new_password ():
  285. """
  286. Validate the new password, check the secret code, and if everything
  287. is OK change the user password.
  288. """
  289. username = request.forms.getunicode('username')
  290. email = request.forms.getunicode('email')
  291. password = request.forms.getunicode('password')
  292. secret_token = request.forms.getunicode('token')
  293. # We must have all fields
  294. if not username or not email or not password or not secret_token:
  295. redirect(application.get_url('login'))
  296. # Password too short?
  297. if len (password) < 8:
  298. return template (
  299. 'login_change_password.html',
  300. flash = 'Password must be at least 8 characters long')
  301. # OK, everything should be fine now. Reset user password.
  302. database.reset_password(username, email, password, secret_token)
  303. # Check if the password was successfully reset
  304. user = database.check_user_credentials (username, password)
  305. # Username/Password not working
  306. if not user:
  307. redirect (application.get_url ('login'))
  308. # Everything matched!
  309. # Notify user of password change.
  310. email_to = user['email']
  311. email_subject = 'freepost password changed'
  312. email_body = template ('email/password_changed.txt')
  313. mail.send (email_to, email_subject, email_body)
  314. # Start new session and redirect user
  315. session.start (user['id'])
  316. redirect (application.get_url ('user_settings'))
  317. @get ('/user/settings', name='user_settings')
  318. @requires_login
  319. def user_settings ():
  320. """
  321. A user's personal page.
  322. """
  323. return template ('user_settings.html')
  324. @post ('/user/settings')
  325. @requires_login
  326. def update_user_settings ():
  327. """
  328. Update user info (about, email, ...).
  329. """
  330. user = session.user ()
  331. about = request.forms.getunicode ('about')
  332. email = request.forms.getunicode ('email')
  333. preferred_feed = request.forms.getunicode ('preferred_feed')
  334. if about is None or email is None:
  335. redirect (application.get_url ('user_settings'))
  336. if preferred_feed not in [ 'hot', 'new' ]:
  337. preferred_feed = 'hot'
  338. # Update user info in the database
  339. database.update_user (user['id'], about, email, False, preferred_feed)
  340. redirect (application.get_url ('user_settings'))
  341. @get ('/user_activity/posts')
  342. @requires_login
  343. def user_posts ():
  344. user = session.user ()
  345. posts = database.get_user_posts (user['id'])
  346. return template ('user_posts.html', posts=posts)
  347. @get ('/user_activity/comments')
  348. @requires_login
  349. def user_comments ():
  350. user = session.user ()
  351. comments = database.get_user_comments (user['id'])
  352. return template ('user_comments.html', comments=comments)
  353. @get ('/user_activity/replies', name='user_replies')
  354. @requires_login
  355. def user_replies ():
  356. user = session.user ()
  357. replies = database.get_user_replies (user['id'])
  358. database.set_replies_as_read (user['id'])
  359. return template ('user_replies.html', replies=replies)
  360. @get ('/user/public/<username>', name='user_public')
  361. def user_public_homepage (username):
  362. """
  363. Display a publicly accessible page with public info about the user.
  364. """
  365. account = database.get_user_by_username (username)
  366. if account is None:
  367. redirect (application.get_url ('user_settings'))
  368. return template ('user_public_homepage.html', account=account)
  369. @get ('/post/<hash_id>', name='post')
  370. def post_thread (hash_id):
  371. """
  372. Display a single post with all its comments.
  373. """
  374. user = session.user ()
  375. post = database.get_post (hash_id, user['id'] if user else None)
  376. comments = database.get_post_comments (post['id'], user['id'] if user else None)
  377. topics = database.get_post_topics (post['id'])
  378. # Group comments by parent
  379. comments_tree = {}
  380. for comment in comments:
  381. if comment['parentId'] is None:
  382. if 0 not in comments_tree:
  383. comments_tree[0] = []
  384. comments_tree[0].append(dict(comment))
  385. else:
  386. if comment['parentId'] not in comments_tree:
  387. comments_tree[comment['parentId']] = []
  388. comments_tree[comment['parentId']].append(dict(comment))
  389. # Build ordered list of comments (recourse tree)
  390. def children (parent_id = 0, depth = 0):
  391. temp_comments = []
  392. if parent_id in comments_tree:
  393. for comment in comments_tree[parent_id]:
  394. comment['depth'] = depth
  395. temp_comments.append (comment)
  396. temp_comments.extend (children (comment['id'], depth + 1))
  397. return temp_comments
  398. comments = children ()
  399. # Show posts/comments Markdown instead of rendered text
  400. show_source = 'source' in request.query
  401. # Disable browser caching
  402. # Fix issue: https://notabug.org/zPlus/freepost/issues/80
  403. response.set_header('cache-control', 'no-cache, no-store, must-revalidate')
  404. response.set_header('pragma', 'no-cache')
  405. response.set_header('expires', '0')
  406. return template (
  407. 'post.html',
  408. post=post,
  409. comments=comments,
  410. topics=topics,
  411. show_source=show_source,
  412. votes = {
  413. 'post': {},
  414. 'comment': {}
  415. })
  416. @requires_login
  417. @post ('/post/<hash_id>')
  418. def new_comment (hash_id):
  419. # The comment text
  420. comment = request.forms.getunicode ('new_comment').strip ()
  421. # Empty comment?
  422. if len (comment) == 0:
  423. redirect (application.get_url ('post', hash_id=hash_id))
  424. # Retrieve the post
  425. post = database.get_post (hash_id)
  426. # Does post exist?
  427. if not post:
  428. redirect (application.get_url ('homepage'))
  429. user = session.user ()
  430. comment_hash_id = database.new_comment (comment, hash_id, user['id'], post['userId'])
  431. # Retrieve new comment
  432. comment = database.get_comment (comment_hash_id)
  433. # Automatically add an upvote for the original poster
  434. database.vote_comment (comment['id'], user['id'], +1)
  435. redirect (application.get_url ('post', hash_id=hash_id))
  436. @requires_login
  437. @get ('/edit/post/<hash_id>', name='edit_post')
  438. def edit_post (hash_id):
  439. user = session.user ()
  440. post = database.get_post (hash_id, user['id'])
  441. # Make sure the session user is the actual poster/commenter
  442. if post['userId'] != user['id']:
  443. redirect (application.get_url ('post', hash_id=hash_id))
  444. return template ('edit_post.html', post=post)
  445. @requires_login
  446. @post ('/edit/post/<hash_id>')
  447. def edit_post_check (hash_id):
  448. user = session.user ()
  449. post = database.get_post (hash_id, user['id'])
  450. # Make sure the session user is the actual poster/commenter
  451. if post['userId'] != user['id']:
  452. redirect (application.get_url ('homepage'))
  453. # MUST have a title. If empty, use original title
  454. title = request.forms.getunicode ('title').strip ()
  455. if len (title) == 0: title = post['title']
  456. link = request.forms.getunicode ('link').strip () if 'link' in request.forms else ''
  457. text = request.forms.getunicode ('text').strip () if 'text' in request.forms else ''
  458. # If there is a URL, make sure it has a "scheme"
  459. if len (link) > 0 and urlparse (link).scheme == '':
  460. link = 'http://' + link
  461. # Update post
  462. database.update_post (title, link, text, hash_id, user['id'])
  463. redirect (application.get_url ('post', hash_id=hash_id))
  464. @requires_login
  465. @get ('/edit/comment/<hash_id>', name='edit_comment')
  466. def edit_comment (hash_id):
  467. user = session.user ()
  468. comment = database.get_comment (hash_id, user['id'])
  469. # Make sure the session user is the actual poster/commenter
  470. if comment['userId'] != user['id']:
  471. redirect (application.get_url ('post', hash_id=comment['postHashId']))
  472. return template ('edit_comment.html', comment=comment)
  473. @requires_login
  474. @post ('/edit/comment/<hash_id>')
  475. def edit_comment_check (hash_id):
  476. user = session.user ()
  477. comment = database.get_comment (hash_id, user['id'])
  478. # Make sure the session user is the actual poster/commenter
  479. if comment['userId'] != user['id']:
  480. redirect (application.get_url ('homepage'))
  481. text = request.forms.getunicode ('text').strip () if 'text' in request.forms else ''
  482. # Only update if the text is not empty
  483. if len (text) > 0:
  484. database.update_comment (text, hash_id, user['id'])
  485. redirect (application.get_url ('post', hash_id=comment['postHashId']) + '#comment-' + hash_id)
  486. @get ('/submit')
  487. @requires_login
  488. def submit ():
  489. """
  490. Submit a new post.
  491. """
  492. return template ('submit.html')
  493. @post ('/submit', name='submit')
  494. @requires_login
  495. def submit_check ():
  496. """
  497. Check submission of new post.
  498. """
  499. # Somebody sent a <form> without a title???
  500. if not request.forms.getunicode ('title'):
  501. abort ()
  502. user = session.user ()
  503. # Retrieve title
  504. title = request.forms.getunicode ('title').strip ()
  505. # Bad title?
  506. if len (title) == 0:
  507. return template ('submit.html', flash='Title is missing.')
  508. # Retrieve link
  509. link = request.forms.getunicode ('link').strip ()
  510. # If there is a URL, make sure it has a "scheme"
  511. if len (link) > 0 and urlparse (link).scheme == '':
  512. link = 'http://' + link
  513. if database.post_exists(link):
  514. return template ('submit.html', flash='Link has already been submitted.')
  515. # Retrieve topics
  516. topics = request.forms.getunicode ('topics')
  517. # Retrieve text
  518. text = request.forms.getunicode ('text')
  519. # Add the new post
  520. post_hash_id = database.new_post (title, link, text, user['id'])
  521. # Retrieve the new post just created
  522. post = database.get_post (post_hash_id)
  523. # Add topics for this post
  524. database.replace_post_topics (post['id'], topics)
  525. # Automatically add an upvote for the original poster
  526. database.vote_post (post['id'], user['id'], +1)
  527. # Posted. Now go the new post's page
  528. redirect (application.get_url ('post', hash_id=post_hash_id))
  529. @requires_login
  530. @get ('/reply/<hash_id>', name='reply')
  531. def reply (hash_id):
  532. user = session.user ()
  533. # The comment to reply to
  534. comment = database.get_comment (hash_id)
  535. # Does the comment exist?
  536. if not comment:
  537. redirect (application.get_url ('homepage'))
  538. return template ('reply.html', comment=comment)
  539. @requires_login
  540. @post ('/reply/<hash_id>')
  541. def reply_check (hash_id):
  542. user = session.user ()
  543. # The comment to reply to
  544. comment = database.get_comment (hash_id)
  545. # The text of the reply
  546. text = request.forms.getunicode ('text').strip ()
  547. # Empty comment. Redirect to parent post
  548. if len (text) == 0:
  549. redirect (application.get_url ('post', hash_id=comment['postHashId']))
  550. # We have a text, add the reply and redirect to the new reply
  551. reply_hash_id = database.new_comment (
  552. text, comment['postHashId'], user['id'],
  553. comment['userId'], comment['id'])
  554. # Retrieve new comment
  555. comment = database.get_comment (reply_hash_id)
  556. # Automatically add an upvote for the original poster
  557. database.vote_comment (comment['id'], user['id'], +1)
  558. redirect (application.get_url ('post', hash_id=comment['postHashId']) + '#comment-' + reply_hash_id)
  559. @requires_login
  560. @post ('/vote', name='vote')
  561. def vote ():
  562. """
  563. Handle upvotes and downvotes.
  564. """
  565. user = session.user ()
  566. # Vote a post
  567. if request.forms.getunicode ('target') == 'post':
  568. # Retrieve the post
  569. post = database.get_post (request.forms.getunicode ('post'), user['id'])
  570. if not post:
  571. return
  572. # If user clicked the "upvote" button...
  573. if request.forms.getunicode ('updown') == 'up':
  574. # If user has upvoted this post before...
  575. if post['user_vote'] == 1:
  576. # Remove +1
  577. database.vote_post (post['id'], user['id'], -1)
  578. # If user has downvoted this post before...
  579. elif post['user_vote'] == -1:
  580. # Change vote from -1 to +1
  581. database.vote_post (post['id'], user['id'], +2)
  582. # If user hasn't voted this post...
  583. else:
  584. # Add +1
  585. database.vote_post (post['id'], user['id'], +1)
  586. # If user clicked the "downvote" button...
  587. if request.forms.getunicode ('updown') == 'down':
  588. # If user has downvoted this post before...
  589. if post['user_vote'] == -1:
  590. # Remove -1
  591. database.vote_post (post['id'], user['id'], +1)
  592. # If user has upvoted this post before...
  593. elif post['user_vote'] == 1:
  594. # Change vote from +1 to -1
  595. database.vote_post (post['id'], user['id'], -2)
  596. # If user hasn't voted this post...
  597. else:
  598. # Add -1
  599. database.vote_post (post['id'], user['id'], -1)
  600. # Vote a comment
  601. if request.forms.getunicode ('target') == 'comment':
  602. # Retrieve the comment
  603. comment = database.get_comment (request.forms.getunicode ('comment'), user['id'])
  604. if not comment:
  605. return
  606. # If user clicked the "upvote" button...
  607. if request.forms.getunicode ('updown') == 'up':
  608. # If user has upvoted this comment before...
  609. if comment['user_vote'] == 1:
  610. # Remove +1
  611. database.vote_comment (comment['id'], user['id'], -1)
  612. # If user has downvoted this comment before...
  613. elif comment['user_vote'] == -1:
  614. # Change vote from -1 to +1
  615. database.vote_comment (comment['id'], user['id'], +2)
  616. # If user hasn't voted this comment...
  617. else:
  618. # Add +1
  619. database.vote_comment (comment['id'], user['id'], +1)
  620. # If user clicked the "downvote" button...
  621. if request.forms.getunicode ('updown') == 'down':
  622. # If user has downvoted this comment before...
  623. if comment['user_vote'] == -1:
  624. # Remove -1
  625. database.vote_comment (comment['id'], user['id'], +1)
  626. # If user has upvoted this comment before...
  627. elif comment['user_vote'] == 1:
  628. # Change vote from +1 to -1
  629. database.vote_comment (comment['id'], user['id'], -2)
  630. # If user hasn't voted this comment...
  631. else:
  632. # Add -1
  633. database.vote_comment (comment['id'], user['id'], -1)
  634. @get ('/search', name='search')
  635. def search ():
  636. """
  637. Search content on this instance, and display the results.
  638. """
  639. # Get the search query
  640. query = request.query.getunicode ('q')
  641. # Get the offset
  642. page = int (request.query.getunicode ('page') or 0)
  643. if page < 0:
  644. return "Page cannot be less than zero."
  645. # Page increment
  646. if 'next' in request.query:
  647. page += 1
  648. if 'previous' in request.query:
  649. page -= 1
  650. # Results order
  651. sort = request.query.getunicode ('sort')
  652. if sort not in [ 'newest', 'points' ]:
  653. sort = 'newest'
  654. posts = database.search (query, sort=sort, page=page) or []
  655. return template ('search.html', posts=posts)
  656. @get ('/rss')
  657. def rss_default ():
  658. """
  659. Redirect base RSS URL to default "hot" feed.
  660. """
  661. return redirect (application.get_url ('rss', sorting='hot'))
  662. @get ('/rss/<sorting>', name='rss')
  663. def rss (sorting):
  664. """
  665. Output an RSS feed of posts.
  666. """
  667. posts = []
  668. # Retrieve the hostname from the HTTP request.
  669. # This is used to build absolute URLs in the RSS feed.
  670. base_url = request.urlparts.scheme + '://' + request.urlparts.netloc
  671. if sorting == 'new':
  672. posts = database.get_posts (sort='new')
  673. if sorting == 'hot':
  674. posts = database.get_posts (sort='hot')
  675. # Set correct Content-Type header for this RSS feed
  676. response.content_type = 'application/rss+xml; charset=UTF-8'
  677. return template ('rss.xml', base_url=base_url, posts=posts)
  678. @get ('/rss_comments', name='rss_comments')
  679. def rss_comments ():
  680. """
  681. Output an RSS feed of the latest comments.
  682. """
  683. # Retrieve the hostname from the HTTP request.
  684. # This is used to build absolute URLs in the RSS feed.
  685. base_url = request.urlparts.scheme + '://' + request.urlparts.netloc
  686. comments = database.get_latest_comments ()
  687. # Set correct Content-Type header for this RSS feed
  688. response.content_type = 'application/rss+xml; charset=UTF-8'
  689. return template ('rss_comments.xml', base_url=base_url, comments=comments)
  690. @get ('/<filename:path>', name='static')
  691. def static (filename):
  692. """
  693. Serve static files.
  694. """
  695. return static_file (filename, root='freepost/static/')