openwrite 0.14.0__tar.gz → 0.16.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. {openwrite-0.14.0/openwrite.egg-info → openwrite-0.16.0}/PKG-INFO +7 -3
  2. {openwrite-0.14.0 → openwrite-0.16.0}/README.md +6 -2
  3. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/__init__.py +12 -11
  4. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/cli.py +1 -1
  5. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/gemini_multi.py +21 -1
  6. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/routes/auth.py +8 -3
  7. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/routes/blog.py +162 -58
  8. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/routes/dashboard.py +83 -9
  9. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/routes/main.py +29 -6
  10. openwrite-0.16.0/openwrite/static/favicon.png +0 -0
  11. openwrite-0.16.0/openwrite/static/feed/.gitkeep +0 -0
  12. openwrite-0.16.0/openwrite/static/feed/74.xml +12 -0
  13. openwrite-0.16.0/openwrite/static/feed/a.xml +2 -0
  14. openwrite-0.16.0/openwrite/static/feed/anim3sh1t.xml +12 -0
  15. openwrite-0.16.0/openwrite/static/feed/b.xml +2 -0
  16. openwrite-0.16.0/openwrite/static/feed/corrode.xml +4 -0
  17. openwrite-0.16.0/openwrite/static/feed/dirtysanchez.xml +6 -0
  18. openwrite-0.16.0/openwrite/static/feed/eikerammen.xml +165 -0
  19. openwrite-0.16.0/openwrite/static/feed/fedi.xml +9 -0
  20. openwrite-0.16.0/openwrite/static/feed/friskt.xml +2 -0
  21. openwrite-0.16.0/openwrite/static/feed/gordi.xml +6 -0
  22. openwrite-0.16.0/openwrite/static/feed/igilq.xml +226 -0
  23. openwrite-0.16.0/openwrite/static/feed/io.xml +50 -0
  24. openwrite-0.16.0/openwrite/static/feed/isyuricunha.xml +4 -0
  25. openwrite-0.16.0/openwrite/static/feed/kenny.xml +2 -0
  26. openwrite-0.16.0/openwrite/static/feed/m0bi.xml +6 -0
  27. openwrite-0.16.0/openwrite/static/feed/main.xml +236 -0
  28. openwrite-0.16.0/openwrite/static/feed/mywritese.xml +2 -0
  29. openwrite-0.16.0/openwrite/static/feed/nay-thoughs.xml +2 -0
  30. openwrite-0.16.0/openwrite/static/feed/nintendoflesz.xml +7 -0
  31. openwrite-0.16.0/openwrite/static/feed/phronima.xml +68 -0
  32. openwrite-0.16.0/openwrite/static/feed/ram.xml +75 -0
  33. openwrite-0.16.0/openwrite/static/feed/samah.xml +2 -0
  34. openwrite-0.16.0/openwrite/static/feed/themes-test.xml +18 -0
  35. openwrite-0.16.0/openwrite/static/feed/totylkoja.xml +78 -0
  36. openwrite-0.16.0/openwrite/static/feed/vegafjord.xml +48 -0
  37. openwrite-0.16.0/openwrite/static/feed/vijayvaradharaj.xml +2 -0
  38. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/style/blog_design.css +74 -0
  39. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/style/default.css +55 -0
  40. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/style/style.css +5 -1
  41. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/style/themes/cyberpunk.css +69 -0
  42. openwrite-0.16.0/openwrite/static/style/themes/light.css +111 -0
  43. openwrite-0.16.0/openwrite/static/style/themes/obscura.css +82 -0
  44. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/style/themes/paper.css +73 -0
  45. openwrite-0.16.0/openwrite/static/style/themes/sepia.css +344 -0
  46. openwrite-0.16.0/openwrite/static/style/themes/stickynote.css +117 -0
  47. openwrite-0.16.0/openwrite/static/style/themes/synthwave.css +389 -0
  48. openwrite-0.16.0/openwrite/static/style/themes/terminal.css +126 -0
  49. openwrite-0.16.0/openwrite/static/style/themes/warmnight.css +141 -0
  50. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/style/themes/windows98.css +81 -0
  51. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/blog.html +3 -1
  52. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/dashboard.html +1 -1
  53. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/edit.html +6 -0
  54. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/footer.html +1 -1
  55. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/new_page.html +1 -0
  56. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/new_post.html +183 -5
  57. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/post.html +37 -0
  58. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/stats.html +26 -0
  59. openwrite-0.16.0/openwrite/utils/create_db.py +28 -0
  60. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/utils/helpers.py +3 -2
  61. openwrite-0.16.0/openwrite/utils/migrations.py +136 -0
  62. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/utils/models.py +59 -1
  63. openwrite-0.16.0/openwrite/utils/tags.py +242 -0
  64. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/utils/worker.py +8 -3
  65. {openwrite-0.14.0 → openwrite-0.16.0/openwrite.egg-info}/PKG-INFO +7 -3
  66. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite.egg-info/SOURCES.txt +32 -0
  67. {openwrite-0.14.0 → openwrite-0.16.0}/pyproject.toml +1 -1
  68. openwrite-0.14.0/openwrite/static/style/themes/light.css +0 -58
  69. openwrite-0.14.0/openwrite/static/style/themes/obscura.css +0 -27
  70. openwrite-0.14.0/openwrite/static/style/themes/stickynote.css +0 -60
  71. openwrite-0.14.0/openwrite/static/style/themes/terminal.css +0 -46
  72. openwrite-0.14.0/openwrite/static/style/themes/warmnight.css +0 -79
  73. openwrite-0.14.0/openwrite/utils/create_db.py +0 -15
  74. {openwrite-0.14.0 → openwrite-0.16.0}/LICENSE +0 -0
  75. {openwrite-0.14.0 → openwrite-0.16.0}/MANIFEST.in +0 -0
  76. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/db/__init__.py +0 -0
  77. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/db/base.py +0 -0
  78. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/gemini_single.py +0 -0
  79. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/routes/admin.py +0 -0
  80. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/routes/federation.py +0 -0
  81. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/routes/upload.py +0 -0
  82. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/IBMPlexSans-Regular.ttf +0 -0
  83. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/Merriweather.ttf +0 -0
  84. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/avatar.png +0 -0
  85. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/emoji.json +0 -0
  86. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/favicon.ico +0 -0
  87. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/logo.png +0 -0
  88. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/chart.js +0 -0
  89. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/highlight.min.js +0 -0
  90. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/nativeEmoji.js +0 -0
  91. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/openwrite.js +0 -0
  92. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/papaparse.min.js +0 -0
  93. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/showdown.min.js +0 -0
  94. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/showdown.min.js.map +0 -0
  95. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/site.compat.min.js +0 -0
  96. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/site.min.js +0 -0
  97. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/style/hl.css +0 -0
  98. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/style/nativeEmoji.min.css +0 -0
  99. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/admin.html +0 -0
  100. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/blog_footer.html +0 -0
  101. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/blog_header.html +0 -0
  102. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/blog_preview.html +0 -0
  103. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/changepw.html +0 -0
  104. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/create.html +0 -0
  105. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/discover.html +0 -0
  106. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/header.html +0 -0
  107. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/import.html +0 -0
  108. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/index.html +0 -0
  109. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/instances.html +0 -0
  110. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/login.html +0 -0
  111. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/page_preview.html +0 -0
  112. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/preview.html +0 -0
  113. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/register.html +0 -0
  114. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/settings.html +0 -0
  115. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/translations.html +0 -0
  116. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/utils/db.py +0 -0
  117. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/utils/i18n.json +0 -0
  118. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/utils/valkey_client.py +0 -0
  119. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite.egg-info/dependency_links.txt +0 -0
  120. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite.egg-info/entry_points.txt +0 -0
  121. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite.egg-info/requires.txt +0 -0
  122. {openwrite-0.14.0 → openwrite-0.16.0}/openwrite.egg-info/top_level.txt +0 -0
  123. {openwrite-0.14.0 → openwrite-0.16.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openwrite
3
- Version: 0.14.0
3
+ Version: 0.16.0
4
4
  License: GNU AFFERO GENERAL PUBLIC LICENSE
5
5
  Version 3, 19 November 2007
6
6
 
@@ -689,13 +689,13 @@ Requires-Dist: redis
689
689
  Dynamic: license-file
690
690
 
691
691
  ![logo](https://openwrite.io/static/logo.png)
692
+
692
693
  openwrite is a minimalist blogging platform built for writing freely, hosting independently, and publishing without noise.
693
694
 
694
695
  ![PyPI - Downloads](https://img.shields.io/pypi/dw/openwrite)
695
696
  ![PyPI - Version](https://img.shields.io/pypi/v/openwrite)
696
697
  [![License: AGPL-3.0-or-later](https://img.shields.io/badge/License-AGPL--3.0--or--later-blue.svg)](LICENSE)
697
698
  ![pythonver](https://img.shields.io/badge/python%20version-3.11-blue)
698
- ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/openwriteio/openwrite/python-tests.yml)
699
699
 
700
700
  ---
701
701
 
@@ -715,7 +715,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
715
715
  - Lightweight
716
716
  - No tracking, only data collected is anonymized(hashed) IP for post view counting and likes
717
717
  - Custom CSS per blog
718
- - Federation using ActivityPub protocol: Follow and Like
718
+ - Federation using ActivityPub protocol
719
719
  - Likes system
720
720
  - Posts importing from xml/csv
721
721
  - Blog themes
@@ -724,6 +724,10 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
724
724
  - Multilanguage (currently English and Polish)
725
725
  - Custom favicon per blog
726
726
  - Subpages per blog
727
+ - Valkey support
728
+ - Post tags, filtering
729
+ - Optional comments per post
730
+ - Auto save during typing
727
731
 
728
732
 
729
733
  ## Installation
@@ -1,11 +1,11 @@
1
1
  ![logo](https://openwrite.io/static/logo.png)
2
+
2
3
  openwrite is a minimalist blogging platform built for writing freely, hosting independently, and publishing without noise.
3
4
 
4
5
  ![PyPI - Downloads](https://img.shields.io/pypi/dw/openwrite)
5
6
  ![PyPI - Version](https://img.shields.io/pypi/v/openwrite)
6
7
  [![License: AGPL-3.0-or-later](https://img.shields.io/badge/License-AGPL--3.0--or--later-blue.svg)](LICENSE)
7
8
  ![pythonver](https://img.shields.io/badge/python%20version-3.11-blue)
8
- ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/openwriteio/openwrite/python-tests.yml)
9
9
 
10
10
  ---
11
11
 
@@ -25,7 +25,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
25
25
  - Lightweight
26
26
  - No tracking, only data collected is anonymized(hashed) IP for post view counting and likes
27
27
  - Custom CSS per blog
28
- - Federation using ActivityPub protocol: Follow and Like
28
+ - Federation using ActivityPub protocol
29
29
  - Likes system
30
30
  - Posts importing from xml/csv
31
31
  - Blog themes
@@ -34,6 +34,10 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
34
34
  - Multilanguage (currently English and Polish)
35
35
  - Custom favicon per blog
36
36
  - Subpages per blog
37
+ - Valkey support
38
+ - Post tags, filtering
39
+ - Optional comments per post
40
+ - Auto save during typing
37
41
 
38
42
 
39
43
  ## Installation
@@ -25,17 +25,21 @@ def create_app(test_config=None):
25
25
  db_path = test_config.get("DB_PATH", "data.db")
26
26
  load_dotenv(test_config.get("env"), override=True)
27
27
 
28
+ # Always run database migrations to ensure schema is up to date
29
+ from .utils.create_db import init_db
30
+ init_db(db_type, db_path)
31
+
32
+ # Initialize database engine and session
33
+ from .utils.models import User, Blog, Post, Settings, Home, Page
34
+ from .utils.db import init_engine, SessionLocal
35
+ init_engine(db_type, db_path)
36
+ from .utils.db import SessionLocal
37
+
28
38
  if not os.path.exists(".initialized") or test_config is not None:
29
39
  mode = os.getenv("MODE")
30
40
  domain = os.getenv("DOMAIN")
31
41
  f_abs_path = os.path.abspath(__file__)
32
42
  file_cwd = "/".join(f_abs_path.split("/")[:-1])
33
- from .utils.create_db import init_db
34
- init_db(db_type, db_path)
35
- from .utils.models import User, Blog, Post, Settings, Home, Page
36
- from .utils.db import init_engine, SessionLocal
37
- init_engine(db_type, db_path)
38
- from .utils.db import SessionLocal
39
43
  admin_password = os.urandom(16).hex()
40
44
  hashed = bcrypt.hashpw("openwrite".encode("utf-8"), bcrypt.gensalt())
41
45
  admin_user = User(username="admin", email="", password_hash=hashed.decode("utf-8"), verified=1, admin=1)
@@ -163,6 +167,7 @@ def create_app(test_config=None):
163
167
  app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
164
168
  app.secret_key = os.getenv("SECRET_KEY")
165
169
  app.config['SERVER_NAME'] = os.getenv("DOMAIN")
170
+ app.config['SESSION_COOKIE_DOMAIN'] = '.' + os.getenv("DOMAIN", "").split(':')[0]
166
171
 
167
172
  from .routes.auth import auth_bp
168
173
  from .routes.dashboard import dashboard_bp
@@ -171,14 +176,10 @@ def create_app(test_config=None):
171
176
  from .routes.main import main_bp
172
177
  from .routes.upload import upload_bp
173
178
  from .routes.federation import federation_bp
174
- from .utils.db import init_engine, SessionLocal
175
179
  from flask_cors import CORS
176
180
 
177
181
  CORS(app)
178
182
 
179
- init_engine(db_type, db_path)
180
- from .utils.db import SessionLocal
181
-
182
183
  app.register_blueprint(auth_bp)
183
184
  app.register_blueprint(dashboard_bp)
184
185
  app.register_blueprint(blog_bp)
@@ -212,7 +213,7 @@ def create_app(test_config=None):
212
213
 
213
214
  trans = g.db.query(Home).filter(Home.type == "translation", Home.language == lang).all()
214
215
 
215
- g.version = "0.14.0"
216
+ g.version = "0.15.0"
216
217
  g.mode = os.getenv("MODE")
217
218
  f_abs_path = os.path.abspath(__file__)
218
219
  g.mainpath = "/".join(f_abs_path.split("/")[:-1])
@@ -152,8 +152,8 @@ def init():
152
152
  if captcha_enabled:
153
153
  f.write(f"FRIENDLY_CAPTCHA_SITEKEY={captcha_sitekey}\n")
154
154
  f.write(f"FRIENDLY_CAPTCHA_APIKEY={captcha_apikey}\n")
155
+ f.write(f"VALKEY_ENABLED={'yes' if valkey_enabled else 'no'}\n")
155
156
  if valkey_enabled:
156
- f.write(f"VALKEY_ENABLED={'yes' if valkey_enabled else 'no'}\n")
157
157
  f.write(f"VALKEY_HOST={valkey_host}\n")
158
158
  f.write(f"VALKEY_PORT={valkey_port}\n")
159
159
  f.write(f"VALKEY_PASSWORD={valkey_password}\n")
@@ -22,8 +22,10 @@ def root(req):
22
22
  global session
23
23
  resp = f"```\n{openwrite_logo}\n```"
24
24
  resp += "\nopenwrite.io is a minimalist blogging platform built for writing freely, hosting independently, and publishing without noise. To create your own blog, visit:\n"
25
- resp += "=> https://openwrite.io openwrite.io\n"
25
+ resp += "=> https://openwrite.io https://openwrite.io\n"
26
26
  resp += "In here you can read posts published.\n"
27
+ resp += "If you want to list all blogs (that did not disable indexing) find them in the link below:\n"
28
+ resp += "=> /blogs All Blogs list"
27
29
  resp += "\n## 🗞️ Latest posts:\n"
28
30
  try:
29
31
  posts = (session.query(Post).filter(Post.feed == "1", Post.isdraft == "0").order_by(Post.id.desc()).limit(10).all())
@@ -36,6 +38,20 @@ def root(req):
36
38
  finally:
37
39
  session.close()
38
40
 
41
+ def all_blogs(req):
42
+ global session
43
+ blogs = session.query(Blog).filter_by(index='on').order_by(Blog.title).all()
44
+ resp = "Here you will find a list of all blogs in openwrite that did not disable their indexing:\n"
45
+ resp += "\n"
46
+ for b in blogs:
47
+ posts = session.query(Post).filter_by(blog=b.id).count()
48
+ last = session.query(Post).filter_by(blog=b.id).order_by(Post.date.desc()).first()
49
+ resp += f"=> /b/{b.name} {b.title} \n"
50
+ resp += f"Posts: {posts}\n"
51
+ resp += f"Last posted: {last.date if last is not None else 'never'}\n\n"
52
+
53
+ return resp
54
+
39
55
  def blog_index(req):
40
56
  global session
41
57
  path = req.path
@@ -134,3 +150,7 @@ def init(capsule):
134
150
  capsule.add("/b/*",
135
151
  blog_index,
136
152
  protocol='gemini')
153
+
154
+ capsule.add("/blogs",
155
+ all_blogs,
156
+ protocol='gemini')
@@ -1,4 +1,4 @@
1
- from flask import Blueprint, render_template, redirect, request, session, g
1
+ from flask import Blueprint, render_template, redirect, request, session, g, current_app
2
2
  from openwrite.utils.models import User
3
3
  import bcrypt
4
4
  import re
@@ -19,7 +19,7 @@ def register():
19
19
  return render_template('register.html')
20
20
 
21
21
  form_username = request.form.get('username')
22
- if not re.match(r"^[a-zA-Z0-9](?:[a-zA-Z0-9_-]{1,28}[a-zA-Z0-9])?$", form_username):
22
+ if not re.match(r"^[a-zA-Z0-9](?:[a-zA-Z0-9_-]{1,28}[a-zA-Z0-9])+$", form_username):
23
23
  return render_template('register.html', error=g.trans['wrong_username'])
24
24
  form_password = request.form.get('password')
25
25
  form_password2 = request.form.get('password2')
@@ -74,5 +74,10 @@ def login():
74
74
  @auth_bp.route("/logout")
75
75
  def logout():
76
76
  session.clear()
77
- return redirect("/")
77
+ response = redirect("/")
78
+ cookie_name = current_app.config.get('SESSION_COOKIE_NAME', 'session')
79
+ cookie_domain = current_app.config.get('SESSION_COOKIE_DOMAIN')
80
+ response.delete_cookie(cookie_name, path='/', domain=cookie_domain)
81
+ response.delete_cookie(cookie_name, path='/')
82
+ return response
78
83
 
@@ -1,5 +1,6 @@
1
- from flask import Blueprint, render_template, redirect, request, g, Response, abort
2
- from openwrite.utils.models import Blog, Post, User, View, Like, Page
1
+ from flask import Blueprint, render_template, redirect, request, g, Response, abort, send_file, jsonify
2
+ from openwrite.utils.models import Blog, Post, User, View, Like, Page, PostDraft, Comment
3
+ from openwrite.utils.tags import get_post_tags, parse_tags_from_posts_syntax, get_posts_by_tags
3
4
  from openwrite.utils.helpers import gen_link, sanitize_html, anonymize, get_ip
4
5
  from openwrite.utils.valkey_client import valkey_client
5
6
  from feedgen.feed import FeedGenerator
@@ -12,6 +13,26 @@ from datetime import timezone, datetime
12
13
 
13
14
  blog_bp = Blueprint("blog", __name__)
14
15
 
16
+ def process_posts_content(page, blog_id, blog_url, domain):
17
+ posts_matches = re.findall(r'{posts[^}]*}', page.content_raw)
18
+ if not posts_matches:
19
+ return [], page.content_html
20
+
21
+ posts_syntax = posts_matches[0]
22
+ tag_names = parse_tags_from_posts_syntax(posts_syntax)
23
+ posts = get_posts_by_tags(g.db, blog_id, tag_names)
24
+
25
+ posts_html = ""
26
+ for post in posts:
27
+ post_url = f"{blog_url}/{post.link}"
28
+ posts_html += f'<div class="postlist_item"><i class="datetime">{post.date}</i> - <a href="{post_url}">{post.title}</a></div>'
29
+
30
+ processed_content = page.content_html
31
+ for match in posts_matches:
32
+ processed_content = processed_content.replace(match, posts_html)
33
+
34
+ return posts, processed_content
35
+
15
36
  def create_activity_object(post, blog, url, full_content=True):
16
37
  """Create ActivityPub object (Article or Note) based on post content length"""
17
38
  dt = datetime.strptime(str(post.date), "%Y-%m-%d %H:%M:%S")
@@ -139,11 +160,9 @@ def show_blog(blog):
139
160
  pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
140
161
  blog_obj.url = f"/b/{blog_obj.name}"
141
162
  homepage = g.db.query(Page).filter(Page.blog == blog_obj.id, Page.url == "").first()
142
- if "{posts}" in homepage.content_raw:
143
- posts = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
144
- return render_template("blog.html", blog=blog_obj, page=homepage, posts=posts, pages=pages)
145
-
146
- return render_template("blog.html", blog=blog_obj, page=homepage, pages=pages)
163
+
164
+ posts, processed_content = process_posts_content(homepage, blog_obj.id, blog_obj.url, g.main_domain)
165
+ return render_template("blog.html", blog=blog_obj, page=homepage, posts=posts, pages=pages, processed_content=processed_content)
147
166
 
148
167
  @blog_bp.route("/", subdomain="<blog>")
149
168
  def show_subblog(blog):
@@ -167,11 +186,9 @@ def show_subblog(blog):
167
186
  blog_obj.url = f"https://{blog_obj.name}.{os.getenv('DOMAIN')}"
168
187
  pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
169
188
  homepage = g.db.query(Page).filter(Page.blog == blog_obj.id, Page.url == "").first()
170
- if "{posts}" in homepage.content_raw:
171
- posts = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
172
- return render_template("blog.html", blog=blog_obj, page=homepage, posts=posts, pages=pages)
173
-
174
- return render_template("blog.html", blog=blog_obj, page=homepage, pages=pages)
189
+
190
+ posts, processed_content = process_posts_content(homepage, blog_obj.id, blog_obj.url, g.main_domain)
191
+ return render_template("blog.html", blog=blog_obj, page=homepage, posts=posts, pages=pages, processed_content=processed_content)
175
192
 
176
193
  @blog_bp.route("/b/<blog>/<post>")
177
194
  def show_post(blog, post):
@@ -182,7 +199,7 @@ def show_post(blog, post):
182
199
  return redirect("/")
183
200
 
184
201
  if post == "rss":
185
- return _generate_rss(blog_obj)
202
+ return serve_rss(blog_obj)
186
203
 
187
204
  if blog_obj.access == "domain":
188
205
  return redirect(f"https://{blog_obj.name}.{os.getenv('DOMAIN')}/{post}")
@@ -196,11 +213,8 @@ def show_post(blog, post):
196
213
  if not page:
197
214
  return redirect("/")
198
215
 
199
- if "{posts}" in page.content_raw:
200
- posts = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
201
- return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages)
202
-
203
- return render_template("blog.html", blog=blog_obj, page=page, pages=pages)
216
+ posts, processed_content = process_posts_content(page, blog_obj.id, blog_obj.url, g.main_domain)
217
+ return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages, processed_content=processed_content)
204
218
 
205
219
  if is_activitypub_request():
206
220
  url = f"https://{g.main_domain}/b/{blog}"
@@ -240,8 +254,11 @@ def show_post(blog, post):
240
254
  one_post.likes = likes
241
255
  one_post.liked = liked
242
256
 
257
+ post_tags = get_post_tags(g.db, one_post)
258
+ comments = g.db.query(Comment).filter_by(blog_id=blog_obj.id, post_id=one_post.id).order_by(Comment.created_at.asc()).all() if one_post.comments_allowed == "1" else []
259
+
243
260
  user = g.db.query(User).filter_by(id=g.user) if g.user else None
244
- return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages)
261
+ return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages, post_tags=post_tags, comments=comments)
245
262
 
246
263
  @blog_bp.route("/<post>", subdomain="<blog>")
247
264
  def show_subpost(blog, post):
@@ -252,7 +269,7 @@ def show_subpost(blog, post):
252
269
  return redirect(f"https://{os.getenv('DOMAIN')}/")
253
270
 
254
271
  if post == "rss":
255
- return _generate_rss(blog_obj)
272
+ return serve_rss(blog_obj)
256
273
 
257
274
  if blog_obj.access == "path":
258
275
  return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog_obj.name}/{post}")
@@ -267,11 +284,8 @@ def show_subpost(blog, post):
267
284
  if not page:
268
285
  return redirect("/")
269
286
 
270
- if "{posts}" in page.content_raw:
271
- posts = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
272
- return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages)
273
-
274
- return render_template("blog.html", blog=blog_obj, page=page, pages=pages)
287
+ posts, processed_content = process_posts_content(page, blog_obj.id, blog_obj.url, g.main_domain)
288
+ return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages, processed_content=processed_content)
275
289
 
276
290
  if is_activitypub_request():
277
291
  url = f"https://{blog}.{g.main_domain}"
@@ -310,9 +324,12 @@ def show_subpost(blog, post):
310
324
 
311
325
  one_post.likes = likes
312
326
  one_post.liked = liked
327
+
328
+ post_tags = get_post_tags(g.db, one_post)
329
+ comments = g.db.query(Comment).filter_by(blog_id=blog_obj.id, post_id=one_post.id).order_by(Comment.created_at.asc()).all() if one_post.comments_allowed == "1" else []
313
330
 
314
331
  user = g.db.query(User).filter_by(id=g.user) if g.user else None
315
- return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages)
332
+ return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages, post_tags=post_tags, comments=comments)
316
333
 
317
334
  @blog_bp.route("/.well-known/webfinger")
318
335
  def webfinger():
@@ -382,12 +399,8 @@ def single_showpost(post):
382
399
  if not page:
383
400
  return redirect("/")
384
401
 
385
- if "{posts}" in page.content_raw:
386
- posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
387
-
388
- return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages)
389
-
390
- return render_template("blog.html", blog=blog, page=page, pages=pages)
402
+ posts, processed_content = process_posts_content(page, blog.id, blog.url, g.main_domain)
403
+ return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages, processed_content=processed_content)
391
404
 
392
405
  if is_activitypub_request():
393
406
  url = f"https://{g.main_domain}"
@@ -426,8 +439,11 @@ def single_showpost(post):
426
439
  one_post.likes = likes
427
440
  one_post.liked = liked
428
441
 
442
+ post_tags = get_post_tags(g.db, one_post)
443
+ comments = g.db.query(Comment).filter_by(blog_id=blog.id, post_id=one_post.id).order_by(Comment.created_at.asc()).all() if one_post.comments_allowed == "1" else []
444
+
429
445
  user = g.db.query(User).filter_by(id=g.user) if g.user else None
430
- return render_template("post.html", blog=blog, post=one_post, user=user, views=v, likes=likes, pages=pages)
446
+ return render_template("post.html", blog=blog, post=one_post, user=user, views=v, likes=likes, pages=pages, post_tags=post_tags, comments=comments)
431
447
 
432
448
  @blog_bp.route("/rss")
433
449
  def single_rss():
@@ -435,7 +451,7 @@ def single_rss():
435
451
  return redirect("/")
436
452
 
437
453
  blog = g.db.query(Blog).first()
438
- return _generate_rss(blog)
454
+ return serve_rss(blog)
439
455
 
440
456
  @blog_bp.route("/like", methods=["POST"])
441
457
  def like():
@@ -483,29 +499,117 @@ def like():
483
499
  status = 201
484
500
 
485
501
  return resp, status
486
-
487
502
 
488
- def _generate_rss(blog):
489
- posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").all()
490
- fg = FeedGenerator()
491
- fg.title(blog.title)
492
-
493
- if blog.access == "domain":
494
- blog_url = f"https://{blog.name}.{g.main_domain}"
495
- post_base_url = blog_url
503
+
504
+
505
+ def serve_rss(blog):
506
+ """
507
+ Here we just serve static RSS files :)
508
+ """
509
+ return send_file(f"{g.mainpath}/static/feed/{blog.name}.xml")
510
+
511
+
512
+ @blog_bp.route("/comment", methods=['POST'])
513
+ def add_comment():
514
+ blog_id = request.form.get('blog_id', type=int)
515
+ post_id = request.form.get('post_id', type=int)
516
+ author_name = request.form.get('author_name', '').strip()[:50]
517
+ content = request.form.get('content', '').strip()[:1000]
518
+ redirect_to = request.referrer or '/'
519
+
520
+ if not blog_id or not post_id:
521
+ abort(400)
522
+
523
+ post = g.db.query(Post).filter_by(id=post_id, blog=blog_id).first()
524
+ if not post or post.comments_allowed != '1':
525
+ abort(403)
526
+
527
+ if not author_name or not content:
528
+ return redirect(redirect_to)
529
+
530
+ now = datetime.now(timezone.utc).replace(microsecond=0)
531
+ comment = Comment(blog_id=blog_id, post_id=post_id, author_name=author_name, content=content, created_at=now)
532
+ g.db.add(comment)
533
+ g.db.commit()
534
+ return redirect(redirect_to)
535
+
536
+
537
+ @blog_bp.route("/comment/<int:comment_id>/delete", methods=['POST'])
538
+ def delete_comment(comment_id):
539
+ if g.user is None:
540
+ abort(403)
541
+
542
+ comment = g.db.query(Comment).filter_by(id=comment_id).first()
543
+ if not comment:
544
+ abort(404)
545
+
546
+ blog = g.db.query(Blog).filter_by(id=comment.blog_id).first()
547
+ if not blog or blog.owner != g.user:
548
+ abort(403)
549
+
550
+ redirect_to = request.referrer or '/'
551
+ g.db.delete(comment)
552
+ g.db.commit()
553
+ return redirect(redirect_to)
554
+
555
+
556
+ @blog_bp.route("/dashboard/autosave/<blog_name>", methods=['POST'])
557
+ def autosave_post(blog_name):
558
+ if g.user is None:
559
+ abort(403)
560
+
561
+ blog = g.db.query(Blog).filter_by(name=blog_name).first()
562
+ if blog is None or blog.owner != g.user:
563
+ abort(403)
564
+
565
+ data = request.get_json()
566
+ if not data:
567
+ abort(400)
568
+
569
+ title = data.get('title', '')[:128]
570
+ content_raw = data.get('content_raw', '')
571
+ post_id = data.get('post_id')
572
+ session_key = data.get('session_key') if post_id is None else None
573
+
574
+ now = datetime.now(timezone.utc).replace(microsecond=0)
575
+ draft = PostDraft(post_id=post_id, blog_id=blog.id, session_key=session_key, title=title, content_raw=content_raw, saved_at=now)
576
+ g.db.add(draft)
577
+ g.db.flush()
578
+
579
+ if post_id is not None:
580
+ existing = g.db.query(PostDraft).filter_by(post_id=post_id, blog_id=blog.id).order_by(PostDraft.saved_at.desc()).all()
496
581
  else:
497
- blog_url = f"https://{g.main_domain}/b/{blog.name}"
498
- post_base_url = blog_url
499
-
500
- fg.link(href=blog_url, rel="alternate")
501
- fg.description(blog.description_html)
502
-
503
- for p in posts:
504
- soup = BeautifulSoup(p.content_html, "html.parser")
505
- fe = fg.add_entry()
506
- fe.title(p.title)
507
- fe.link(href=f"{post_base_url}/{p.link}")
508
- fe.description(soup.get_text())
509
- fe.published(p.date.replace(tzinfo=timezone.utc))
510
-
511
- return Response(fg.rss_str(pretty=True).decode("utf-8"), mimetype="application/rss+xml")
582
+ existing = g.db.query(PostDraft).filter_by(session_key=session_key, blog_id=blog.id).order_by(PostDraft.saved_at.desc()).all()
583
+
584
+ for old in existing[20:]:
585
+ g.db.delete(old)
586
+
587
+ g.db.commit()
588
+ return {'status': 'ok'}, 200
589
+
590
+
591
+ @blog_bp.route("/dashboard/autosaves/<blog_name>", methods=['GET'])
592
+ def get_autosaves(blog_name):
593
+ if g.user is None:
594
+ abort(403)
595
+
596
+ blog = g.db.query(Blog).filter_by(name=blog_name).first()
597
+ if blog is None or blog.owner != g.user:
598
+ abort(403)
599
+
600
+ post_id = request.args.get('post_id', type=int)
601
+ session_key = request.args.get('session_key')
602
+
603
+ if post_id is not None:
604
+ drafts = g.db.query(PostDraft).filter_by(post_id=post_id, blog_id=blog.id).order_by(PostDraft.saved_at.desc()).limit(20).all()
605
+ elif session_key:
606
+ drafts = g.db.query(PostDraft).filter_by(session_key=session_key, blog_id=blog.id).order_by(PostDraft.saved_at.desc()).limit(20).all()
607
+ else:
608
+ drafts = []
609
+
610
+ return jsonify([{
611
+ 'id': d.id,
612
+ 'title': d.title,
613
+ 'content_raw': d.content_raw,
614
+ 'saved_at': d.saved_at.strftime('%Y-%m-%dT%H:%M:%SZ')
615
+ } for d in drafts])