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.
 
 
 
 
 

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