A Python content management system designed for kirsle.net featuring a blog, comments and photo albums. https://rophako.kirsle.net/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

271 lines
7.4 KiB

  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals, absolute_import
  3. """Blog models."""
  4. from flask import g
  5. import time
  6. import re
  7. import glob
  8. import os
  9. import sys
  10. if sys.version_info[0] > 2:
  11. def unicode(s):
  12. return s
  13. from rophako.settings import Config
  14. import rophako.jsondb as JsonDB
  15. from rophako.log import logger
  16. def get_index():
  17. """Get the blog index.
  18. The index is the cache of available blog posts. It has the format:
  19. ```
  20. {
  21. 'post_id': {
  22. fid: Friendly ID for the blog post (for URLs)
  23. time: epoch time of the post
  24. sticky: the stickiness of the post (shows first on global views)
  25. author: the author user ID of the post
  26. categories: [ list of categories ]
  27. privacy: the privacy setting
  28. subject: the post subject
  29. },
  30. ...
  31. }
  32. ```
  33. """
  34. # Index doesn't exist?
  35. if not JsonDB.exists("blog/index"):
  36. return rebuild_index()
  37. db = JsonDB.get("blog/index")
  38. # Hide any private posts if we aren't logged in.
  39. new_db = dict()
  40. if not g.info["session"]["login"]:
  41. for post_id, data in db.items():
  42. if data["privacy"] == "private":
  43. continue
  44. new_db[post_id] = db[post_id]
  45. return new_db
  46. def rebuild_index():
  47. """Rebuild the index.json if it goes missing."""
  48. index = {}
  49. entries = JsonDB.list_docs("blog/entries")
  50. for post_id in entries:
  51. db = JsonDB.get("blog/entries/{}".format(post_id))
  52. update_index(post_id, db, index, False)
  53. JsonDB.commit("blog/index", index)
  54. return index
  55. def update_index(post_id, post, index=None, commit=True):
  56. """Update a post's meta-data in the index. This is also used for adding a
  57. new post to the index for the first time.
  58. * post_id: The ID number for the post
  59. * post: The DB object for a blog post
  60. * index: If you already have the index open, you can pass it here
  61. * commit: Write the DB after updating the index (default True)"""
  62. if index is None:
  63. index = get_index()
  64. index[post_id] = dict(
  65. fid = post["fid"],
  66. time = post["time"] or int(time.time()),
  67. categories = post["categories"],
  68. sticky = False, # TODO
  69. author = post["author"],
  70. privacy = post["privacy"] or "public",
  71. subject = post["subject"],
  72. )
  73. if commit:
  74. JsonDB.commit("blog/index", index)
  75. def get_categories():
  76. """Get the blog categories and their popularity."""
  77. index = get_index()
  78. # Group by tags.
  79. tags = {}
  80. for post, data in index.items():
  81. for tag in data["categories"]:
  82. if not tag in tags:
  83. tags[tag] = 0
  84. tags[tag] += 1
  85. return tags
  86. def get_entry(post_id):
  87. """Load a full blog entry."""
  88. if not JsonDB.exists("blog/entries/{}".format(post_id)):
  89. return None
  90. db = JsonDB.get("blog/entries/{}".format(post_id))
  91. # If no FID, set it to the ID.
  92. if len(db["fid"]) == 0:
  93. db["fid"] = str(post_id)
  94. # If no "format" option, set it to HTML (legacy)
  95. if db.get("format", "") == "":
  96. db["format"] = "html"
  97. return db
  98. def post_entry(post_id, fid, epoch, author, subject, avatar, categories,
  99. privacy, ip, emoticons, comments, format, body):
  100. """Post (or update) a blog entry."""
  101. # Fetch the index.
  102. index = get_index()
  103. # Editing an existing post?
  104. if not post_id:
  105. post_id = get_next_id(index)
  106. logger.debug("Posting blog post ID {}".format(post_id))
  107. # Get a unique friendly ID.
  108. if not fid:
  109. # The default friendly ID = the subject.
  110. fid = subject.lower()
  111. fid = re.sub(r'[^A-Za-z0-9]', '-', fid)
  112. fid = re.sub(r'\-+', '-', fid)
  113. fid = fid.strip("-")
  114. logger.debug("Chosen friendly ID: {}".format(fid))
  115. # Make sure the friendly ID is unique!
  116. if len(fid):
  117. test = fid
  118. loop = 1
  119. logger.debug("Verifying the friendly ID is unique: {}".format(fid))
  120. while True:
  121. collision = False
  122. for k, v in index.items():
  123. # Skip the same post, for updates.
  124. if k == post_id: continue
  125. if v["fid"] == test:
  126. # Not unique.
  127. loop += 1
  128. test = fid + "_" + unicode(loop)
  129. collision = True
  130. logger.debug("Collision with existing post {}: {}".format(k, v["fid"]))
  131. break
  132. # Was there a collision?
  133. if collision:
  134. continue # Try again.
  135. # Nope!
  136. break
  137. fid = test
  138. # DB body for the post.
  139. db = dict(
  140. fid = fid,
  141. ip = ip,
  142. time = epoch or int(time.time()),
  143. categories = categories,
  144. sticky = False, # TODO: implement sticky
  145. comments = comments,
  146. emoticons = emoticons,
  147. avatar = avatar,
  148. privacy = privacy or "public",
  149. author = author,
  150. subject = subject,
  151. format = format,
  152. body = body,
  153. )
  154. # Write the post.
  155. JsonDB.commit("blog/entries/{}".format(post_id), db)
  156. # Update the index cache.
  157. update_index(post_id, db, index)
  158. return post_id, fid
  159. def delete_entry(post_id):
  160. """Remove a blog entry."""
  161. # Fetch the blog information.
  162. index = get_index()
  163. post = get_entry(post_id)
  164. if post is None:
  165. logger.warning("Can't delete post {}, it doesn't exist!".format(post_id))
  166. # Delete the post.
  167. JsonDB.delete("blog/entries/{}".format(post_id))
  168. # Update the index cache.
  169. del index[str(post_id)] # Python JSON dict keys must be strings, never ints
  170. JsonDB.commit("blog/index", index)
  171. def resolve_id(fid):
  172. """Resolve a friendly ID to the blog ID number."""
  173. index = get_index()
  174. # If the ID is all numeric, it's the blog post ID directly.
  175. if re.match(r'^\d+$', fid):
  176. if fid in index:
  177. return int(fid)
  178. else:
  179. logger.error("Tried resolving blog post ID {} as an EntryID, but it wasn't there!".format(fid))
  180. return None
  181. # It's a friendly ID. Scan for it.
  182. for post_id, data in index.items():
  183. if data["fid"] == fid:
  184. return int(post_id)
  185. logger.error("Friendly post ID {} wasn't found!".format(fid))
  186. return None
  187. def list_avatars():
  188. """Get a list of all the available blog avatars."""
  189. avatars = set()
  190. paths = [
  191. # Load avatars from both locations. We check the built-in set first,
  192. # so if you have matching names in your local site those will override.
  193. "rophako/www/static/avatars/*.*",
  194. os.path.join(Config.site.site_root, "static", "avatars", "*.*"),
  195. ]
  196. for path in paths:
  197. for filename in glob.glob(path):
  198. filename = filename.split("/")[-1]
  199. avatars.add(filename)
  200. return sorted(avatars, key=lambda x: x.lower())
  201. def get_next_id(index):
  202. """Get the next free ID for a blog post."""
  203. logger.debug("Getting next available blog ID number")
  204. sort = sorted(index.keys(), key=lambda x: int(x))
  205. next_id = 1
  206. if len(sort) > 0:
  207. next_id = int(sort[-1]) + 1
  208. logger.debug("Highest post ID is: {}".format(next_id))
  209. # Sanity check!
  210. if next_id in index:
  211. raise Exception("Failed to get_next_id for the blog. Chosen ID is still in the index!")
  212. return next_id