__init__.py 27 KB

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