openwrite 0.13.4__tar.gz → 0.15.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 (119) hide show
  1. {openwrite-0.13.4/openwrite.egg-info → openwrite-0.15.0}/PKG-INFO +5 -2
  2. {openwrite-0.13.4 → openwrite-0.15.0}/README.md +3 -1
  3. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/__init__.py +11 -11
  4. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/cli.py +28 -6
  5. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/gemini_multi.py +21 -1
  6. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/routes/auth.py +1 -1
  7. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/routes/blog.py +133 -93
  8. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/routes/dashboard.py +119 -69
  9. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/routes/main.py +29 -6
  10. openwrite-0.15.0/openwrite/static/favicon.png +0 -0
  11. openwrite-0.15.0/openwrite/static/feed/.gitkeep +0 -0
  12. openwrite-0.15.0/openwrite/static/feed/74.xml +12 -0
  13. openwrite-0.15.0/openwrite/static/feed/a.xml +2 -0
  14. openwrite-0.15.0/openwrite/static/feed/anim3sh1t.xml +12 -0
  15. openwrite-0.15.0/openwrite/static/feed/b.xml +2 -0
  16. openwrite-0.15.0/openwrite/static/feed/corrode.xml +4 -0
  17. openwrite-0.15.0/openwrite/static/feed/dirtysanchez.xml +6 -0
  18. openwrite-0.15.0/openwrite/static/feed/eikerammen.xml +165 -0
  19. openwrite-0.15.0/openwrite/static/feed/fedi.xml +9 -0
  20. openwrite-0.15.0/openwrite/static/feed/friskt.xml +2 -0
  21. openwrite-0.15.0/openwrite/static/feed/gordi.xml +6 -0
  22. openwrite-0.15.0/openwrite/static/feed/igilq.xml +226 -0
  23. openwrite-0.15.0/openwrite/static/feed/io.xml +50 -0
  24. openwrite-0.15.0/openwrite/static/feed/isyuricunha.xml +4 -0
  25. openwrite-0.15.0/openwrite/static/feed/kenny.xml +2 -0
  26. openwrite-0.15.0/openwrite/static/feed/m0bi.xml +6 -0
  27. openwrite-0.15.0/openwrite/static/feed/main.xml +236 -0
  28. openwrite-0.15.0/openwrite/static/feed/mywritese.xml +2 -0
  29. openwrite-0.15.0/openwrite/static/feed/nay-thoughs.xml +2 -0
  30. openwrite-0.15.0/openwrite/static/feed/nintendoflesz.xml +7 -0
  31. openwrite-0.15.0/openwrite/static/feed/phronima.xml +68 -0
  32. openwrite-0.15.0/openwrite/static/feed/ram.xml +75 -0
  33. openwrite-0.15.0/openwrite/static/feed/samah.xml +2 -0
  34. openwrite-0.15.0/openwrite/static/feed/themes-test.xml +18 -0
  35. openwrite-0.15.0/openwrite/static/feed/totylkoja.xml +78 -0
  36. openwrite-0.15.0/openwrite/static/feed/vegafjord.xml +48 -0
  37. openwrite-0.15.0/openwrite/static/feed/vijayvaradharaj.xml +2 -0
  38. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/style.css +400 -0
  39. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/blog.html +3 -1
  40. openwrite-0.15.0/openwrite/templates/dashboard.html +68 -0
  41. openwrite-0.15.0/openwrite/templates/discover.html +23 -0
  42. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/edit.html +44 -37
  43. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/footer.html +1 -1
  44. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/new_page.html +1 -0
  45. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/new_post.html +54 -2
  46. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/post.html +9 -0
  47. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/stats.html +26 -0
  48. openwrite-0.15.0/openwrite/utils/create_db.py +28 -0
  49. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/utils/helpers.py +3 -2
  50. openwrite-0.15.0/openwrite/utils/i18n.json +1 -0
  51. openwrite-0.15.0/openwrite/utils/migrations.py +136 -0
  52. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/utils/models.py +26 -1
  53. openwrite-0.15.0/openwrite/utils/tags.py +242 -0
  54. openwrite-0.15.0/openwrite/utils/valkey_client.py +161 -0
  55. openwrite-0.15.0/openwrite/utils/worker.py +134 -0
  56. {openwrite-0.13.4 → openwrite-0.15.0/openwrite.egg-info}/PKG-INFO +5 -2
  57. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite.egg-info/SOURCES.txt +33 -1
  58. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite.egg-info/requires.txt +1 -0
  59. {openwrite-0.13.4 → openwrite-0.15.0}/pyproject.toml +3 -2
  60. openwrite-0.13.4/openwrite/templates/dashboard.html +0 -48
  61. openwrite-0.13.4/openwrite/templates/discover.html +0 -11
  62. openwrite-0.13.4/openwrite/utils/create_db.py +0 -15
  63. openwrite-0.13.4/openwrite/utils/i18n.json +0 -1
  64. {openwrite-0.13.4 → openwrite-0.15.0}/LICENSE +0 -0
  65. {openwrite-0.13.4 → openwrite-0.15.0}/MANIFEST.in +0 -0
  66. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/db/__init__.py +0 -0
  67. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/db/base.py +0 -0
  68. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/gemini_single.py +0 -0
  69. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/routes/admin.py +0 -0
  70. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/routes/federation.py +0 -0
  71. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/routes/upload.py +0 -0
  72. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/IBMPlexSans-Regular.ttf +0 -0
  73. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/Merriweather.ttf +0 -0
  74. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/avatar.png +0 -0
  75. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/emoji.json +0 -0
  76. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/favicon.ico +0 -0
  77. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/logo.png +0 -0
  78. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/chart.js +0 -0
  79. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/highlight.min.js +0 -0
  80. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/nativeEmoji.js +0 -0
  81. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/openwrite.js +0 -0
  82. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/papaparse.min.js +0 -0
  83. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/showdown.min.js +0 -0
  84. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/showdown.min.js.map +0 -0
  85. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/site.compat.min.js +0 -0
  86. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/site.min.js +0 -0
  87. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/blog_design.css +0 -0
  88. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/default.css +0 -0
  89. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/hl.css +0 -0
  90. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/nativeEmoji.min.css +0 -0
  91. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/themes/cyberpunk.css +0 -0
  92. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/themes/light.css +0 -0
  93. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/themes/obscura.css +0 -0
  94. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/themes/paper.css +0 -0
  95. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/themes/stickynote.css +0 -0
  96. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/themes/terminal.css +0 -0
  97. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/themes/warmnight.css +0 -0
  98. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/themes/windows98.css +0 -0
  99. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/admin.html +0 -0
  100. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/blog_footer.html +0 -0
  101. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/blog_header.html +0 -0
  102. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/blog_preview.html +0 -0
  103. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/changepw.html +0 -0
  104. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/create.html +0 -0
  105. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/header.html +0 -0
  106. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/import.html +0 -0
  107. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/index.html +0 -0
  108. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/instances.html +0 -0
  109. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/login.html +0 -0
  110. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/page_preview.html +0 -0
  111. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/preview.html +0 -0
  112. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/register.html +0 -0
  113. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/settings.html +0 -0
  114. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/translations.html +0 -0
  115. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/utils/db.py +0 -0
  116. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite.egg-info/dependency_links.txt +0 -0
  117. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite.egg-info/entry_points.txt +0 -0
  118. {openwrite-0.13.4 → openwrite-0.15.0}/openwrite.egg-info/top_level.txt +0 -0
  119. {openwrite-0.13.4 → openwrite-0.15.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openwrite
3
- Version: 0.13.4
3
+ Version: 0.15.0
4
4
  License: GNU AFFERO GENERAL PUBLIC LICENSE
5
5
  Version 3, 19 November 2007
6
6
 
@@ -685,6 +685,7 @@ Requires-Dist: markdown
685
685
  Requires-Dist: md2gemini
686
686
  Requires-Dist: gmcapsule
687
687
  Requires-Dist: user-agents
688
+ Requires-Dist: redis
688
689
  Dynamic: license-file
689
690
 
690
691
  ![logo](https://openwrite.io/static/logo.png)
@@ -714,7 +715,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
714
715
  - Lightweight
715
716
  - No tracking, only data collected is anonymized(hashed) IP for post view counting and likes
716
717
  - Custom CSS per blog
717
- - Federation using ActivityPub protocol: Follow and Like
718
+ - Federation using ActivityPub protocol
718
719
  - Likes system
719
720
  - Posts importing from xml/csv
720
721
  - Blog themes
@@ -723,6 +724,8 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
723
724
  - Multilanguage (currently English and Polish)
724
725
  - Custom favicon per blog
725
726
  - Subpages per blog
727
+ - Valkey support
728
+ - Post tags, filtering
726
729
 
727
730
 
728
731
  ## Installation
@@ -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,8 @@ 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
37
39
 
38
40
 
39
41
  ## 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)
@@ -171,14 +175,10 @@ def create_app(test_config=None):
171
175
  from .routes.main import main_bp
172
176
  from .routes.upload import upload_bp
173
177
  from .routes.federation import federation_bp
174
- from .utils.db import init_engine, SessionLocal
175
178
  from flask_cors import CORS
176
179
 
177
180
  CORS(app)
178
181
 
179
- init_engine(db_type, db_path)
180
- from .utils.db import SessionLocal
181
-
182
182
  app.register_blueprint(auth_bp)
183
183
  app.register_blueprint(dashboard_bp)
184
184
  app.register_blueprint(blog_bp)
@@ -212,7 +212,7 @@ def create_app(test_config=None):
212
212
 
213
213
  trans = g.db.query(Home).filter(Home.type == "translation", Home.language == lang).all()
214
214
 
215
- g.version = "0.13.3"
215
+ g.version = "0.15.0"
216
216
  g.mode = os.getenv("MODE")
217
217
  f_abs_path = os.path.abspath(__file__)
218
218
  g.mainpath = "/".join(f_abs_path.split("/")[:-1])
@@ -100,6 +100,13 @@ def init():
100
100
  if captcha_enabled:
101
101
  captcha_sitekey = click.prompt("Friendly captcha sitekey")
102
102
  captcha_apikey = click.prompt("Friendly captcha API key")
103
+ valkey_enabled = click.confirm("Enable valkey (queue for likes and views)?", default=False)
104
+ if valkey_enabled:
105
+ valkey_host = click.prompt("Valkey host", default="localhost")
106
+ valkey_port = click.prompt("Valkey port", default="6379")
107
+ valkey_password = click.prompt("Valkey password(optional)", default="")
108
+ valkey_db = click.prompt("Valkey database", default="0")
109
+ valkey_interval = click.prompt("How often should valkey refresh data to DB? (minutes)", default="5")
103
110
 
104
111
 
105
112
 
@@ -145,6 +152,13 @@ def init():
145
152
  if captcha_enabled:
146
153
  f.write(f"FRIENDLY_CAPTCHA_SITEKEY={captcha_sitekey}\n")
147
154
  f.write(f"FRIENDLY_CAPTCHA_APIKEY={captcha_apikey}\n")
155
+ if valkey_enabled:
156
+ f.write(f"VALKEY_ENABLED={'yes' if valkey_enabled else 'no'}\n")
157
+ f.write(f"VALKEY_HOST={valkey_host}\n")
158
+ f.write(f"VALKEY_PORT={valkey_port}\n")
159
+ f.write(f"VALKEY_PASSWORD={valkey_password}\n")
160
+ f.write(f"VALKEY_DB={valkey_db}\n")
161
+ f.write(f"VALKEY_INTERVAL={valkey_interval}\n")
148
162
 
149
163
  click.echo("[+] .env file created")
150
164
  if logs_enabled and not os.path.exists(logs_dir):
@@ -185,7 +199,12 @@ def run(daemon):
185
199
  "gmcapsuled", "-c", f"{cwd}/gemini.ini"
186
200
  ]
187
201
 
202
+ worker_cmd = [
203
+ sys.executable, "-m", "openwrite.utils.worker"
204
+ ]
205
+
188
206
  gemini_log = open(os.path.join(logs_dir, "gemini.log"), "a") if logs_enabled else subprocess.PIPE
207
+ worker_log = open(os.path.join(logs_dir, "worker.log"), "a") if logs_enabled else subprocess.PIPE
189
208
 
190
209
  if gemini_proxy:
191
210
  gemini_cmd = gemini_cmd + ["proxy"]
@@ -193,33 +212,37 @@ def run(daemon):
193
212
  if logs_enabled:
194
213
  gunicorn_cmd = gunicorn_cmd + gunicorn_logs
195
214
 
196
-
197
215
  if daemon:
198
216
  click.echo(f"[+] Openwrite listening on {ip}:{port}")
199
217
  subprocess.Popen(gunicorn_cmd + ["--daemon"])
200
218
 
201
219
  if gemini:
202
220
  click.echo(f"[+] Gemini listening on {ip}:{gemini_port}")
203
-
204
221
  subprocess.Popen(gemini_cmd, stdout=gemini_log, stderr=subprocess.STDOUT)
205
222
 
223
+ click.echo("[+] Starting worker process")
224
+ subprocess.Popen(worker_cmd, stdout=worker_log, stderr=subprocess.STDOUT)
225
+
206
226
  else:
207
- gunicorn_proc = subprocess.Popen(
208
- gunicorn_cmd
209
- )
227
+ gunicorn_proc = subprocess.Popen(gunicorn_cmd)
210
228
 
211
229
  if gemini:
212
230
  click.echo(f"[+] Gemini started on {ip}:{gemini_port}")
213
231
  gemini_proc = subprocess.Popen(gemini_cmd, stdout=gemini_log)
214
232
 
233
+ click.echo("[+] Starting worker process")
234
+ worker_proc = subprocess.Popen(worker_cmd, stdout=worker_log)
235
+
215
236
  try:
216
237
  gunicorn_proc.wait()
217
238
  if gemini:
218
239
  gemini_proc.wait()
240
+ worker_proc.wait()
219
241
  except KeyboardInterrupt:
220
242
  gunicorn_proc.terminate()
221
243
  if gemini:
222
244
  gemini_proc.terminate()
245
+ worker_proc.terminate()
223
246
 
224
247
  @cli.command()
225
248
  def debugrun():
@@ -286,4 +309,3 @@ def stop(service):
286
309
 
287
310
  if __name__ == "__main__":
288
311
  cli()
289
-
@@ -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')
@@ -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')
@@ -1,6 +1,8 @@
1
- from flask import Blueprint, render_template, redirect, request, g, Response, abort
1
+ from flask import Blueprint, render_template, redirect, request, g, Response, abort, send_file
2
2
  from openwrite.utils.models import Blog, Post, User, View, Like, Page
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
5
+ from openwrite.utils.valkey_client import valkey_client
4
6
  from feedgen.feed import FeedGenerator
5
7
  from sqlalchemy import desc
6
8
  import os
@@ -11,6 +13,26 @@ from datetime import timezone, datetime
11
13
 
12
14
  blog_bp = Blueprint("blog", __name__)
13
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
+
14
36
  def create_activity_object(post, blog, url, full_content=True):
15
37
  """Create ActivityPub object (Article or Note) based on post content length"""
16
38
  dt = datetime.strptime(str(post.date), "%Y-%m-%d %H:%M:%S")
@@ -138,11 +160,9 @@ def show_blog(blog):
138
160
  pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
139
161
  blog_obj.url = f"/b/{blog_obj.name}"
140
162
  homepage = g.db.query(Page).filter(Page.blog == blog_obj.id, Page.url == "").first()
141
- if "{posts}" in homepage.content_raw:
142
- posts = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
143
- return render_template("blog.html", blog=blog_obj, page=homepage, posts=posts, pages=pages)
144
-
145
- 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)
146
166
 
147
167
  @blog_bp.route("/", subdomain="<blog>")
148
168
  def show_subblog(blog):
@@ -166,11 +186,9 @@ def show_subblog(blog):
166
186
  blog_obj.url = f"https://{blog_obj.name}.{os.getenv('DOMAIN')}"
167
187
  pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
168
188
  homepage = g.db.query(Page).filter(Page.blog == blog_obj.id, Page.url == "").first()
169
- if "{posts}" in homepage.content_raw:
170
- posts = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
171
- return render_template("blog.html", blog=blog_obj, page=homepage, posts=posts, pages=pages)
172
-
173
- 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)
174
192
 
175
193
  @blog_bp.route("/b/<blog>/<post>")
176
194
  def show_post(blog, post):
@@ -181,7 +199,7 @@ def show_post(blog, post):
181
199
  return redirect("/")
182
200
 
183
201
  if post == "rss":
184
- return _generate_rss(blog_obj)
202
+ return serve_rss(blog_obj)
185
203
 
186
204
  if blog_obj.access == "domain":
187
205
  return redirect(f"https://{blog_obj.name}.{os.getenv('DOMAIN')}/{post}")
@@ -195,11 +213,8 @@ def show_post(blog, post):
195
213
  if not page:
196
214
  return redirect("/")
197
215
 
198
- if "{posts}" in page.content_raw:
199
- posts = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
200
- return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages)
201
-
202
- 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)
203
218
 
204
219
  if is_activitypub_request():
205
220
  url = f"https://{g.main_domain}/b/{blog}"
@@ -215,22 +230,34 @@ def show_post(blog, post):
215
230
  blog_obj.url = f"/b/{blog_obj.name}"
216
231
 
217
232
  user_agent = request.headers.get('User-Agent')
233
+ ip = anonymize(get_ip())
218
234
 
219
235
  if 'Bot' not in user_agent:
220
- ip = anonymize(get_ip())
221
236
  v = g.db.query(View).filter(View.blog == blog_obj.id, View.post == one_post.id, View.hash == ip).count()
222
237
  if v < 1:
223
- now = datetime.now(timezone.utc).replace(microsecond=0)
224
- new_view = View(blog=blog_obj.id, post=one_post.id, hash=ip, date=now, agent=user_agent)
225
- g.db.add(new_view)
226
- g.db.commit()
227
- likes = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id).count()
228
- one_post.likes = likes
229
- liked = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id, Like.hash == anonymize(get_ip())).count()
230
- one_post.liked = liked
238
+ if valkey_client.is_enabled():
239
+ valkey_client.queue_view(blog_obj.id, one_post.id, ip, user_agent)
240
+ else:
241
+ now = datetime.now(timezone.utc).replace(microsecond=0)
242
+ new_view = View(blog=blog_obj.id, post=one_post.id, hash=ip, date=now, agent=user_agent)
243
+ g.db.add(new_view)
244
+ g.db.commit()
245
+
246
+ likes_db = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id).count()
247
+ likes_queued = valkey_client.count_queued_likes(blog_obj.id, one_post.id) if valkey_client.is_enabled() else 0
248
+ likes = likes_db + likes_queued
249
+
250
+ liked_db = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id, Like.hash == ip).count()
251
+ liked_queued = valkey_client.check_queued_like(blog_obj.id, one_post.id, ip) if valkey_client.is_enabled() else False
252
+ liked = liked_db or liked_queued
253
+
254
+ one_post.likes = likes
255
+ one_post.liked = liked
256
+
257
+ post_tags = get_post_tags(g.db, one_post)
231
258
 
232
259
  user = g.db.query(User).filter_by(id=g.user) if g.user else None
233
- return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages)
260
+ return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages, post_tags=post_tags)
234
261
 
235
262
  @blog_bp.route("/<post>", subdomain="<blog>")
236
263
  def show_subpost(blog, post):
@@ -241,7 +268,7 @@ def show_subpost(blog, post):
241
268
  return redirect(f"https://{os.getenv('DOMAIN')}/")
242
269
 
243
270
  if post == "rss":
244
- return _generate_rss(blog_obj)
271
+ return serve_rss(blog_obj)
245
272
 
246
273
  if blog_obj.access == "path":
247
274
  return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog_obj.name}/{post}")
@@ -256,11 +283,8 @@ def show_subpost(blog, post):
256
283
  if not page:
257
284
  return redirect("/")
258
285
 
259
- if "{posts}" in page.content_raw:
260
- posts = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
261
- return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages)
262
-
263
- return render_template("blog.html", blog=blog_obj, page=page, pages=pages)
286
+ posts, processed_content = process_posts_content(page, blog_obj.id, blog_obj.url, g.main_domain)
287
+ return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages, processed_content=processed_content)
264
288
 
265
289
  if is_activitypub_request():
266
290
  url = f"https://{blog}.{g.main_domain}"
@@ -275,24 +299,35 @@ def show_subpost(blog, post):
275
299
  one_post.authorname = post_author.username
276
300
  blog_obj.url = f"https://{blog_obj.name}.{os.getenv('DOMAIN')}"
277
301
 
278
- user_agent = request.headers.get('User-Agent', '')
302
+ user_agent = request.headers.get('User-Agent')
303
+ ip = anonymize(get_ip())
304
+
279
305
  if 'Bot' not in user_agent:
280
- ip = anonymize(get_ip())
281
306
  v = g.db.query(View).filter(View.blog == blog_obj.id, View.post == one_post.id, View.hash == ip).count()
282
- user_agent = request.headers.get('User-Agent')
283
307
  if v < 1:
284
- now = datetime.now(timezone.utc).replace(microsecond=0)
285
- new_view = View(blog=blog_obj.id, post=one_post.id, hash=ip, date=now, agent=user_agent)
286
- g.db.add(new_view)
287
- g.db.commit()
308
+ if valkey_client.is_enabled():
309
+ valkey_client.queue_view(blog_obj.id, one_post.id, ip, user_agent)
310
+ else:
311
+ now = datetime.now(timezone.utc).replace(microsecond=0)
312
+ new_view = View(blog=blog_obj.id, post=one_post.id, hash=ip, date=now, agent=user_agent)
313
+ g.db.add(new_view)
314
+ g.db.commit()
315
+
316
+ likes_db = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id).count()
317
+ likes_queued = valkey_client.count_queued_likes(blog_obj.id, one_post.id) if valkey_client.is_enabled() else 0
318
+ likes = likes_db + likes_queued
319
+
320
+ liked_db = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id, Like.hash == ip).count()
321
+ liked_queued = valkey_client.check_queued_like(blog_obj.id, one_post.id, ip) if valkey_client.is_enabled() else False
322
+ liked = liked_db or liked_queued
288
323
 
289
- likes = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id).count()
290
324
  one_post.likes = likes
291
- liked = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id, Like.hash == anonymize(get_ip())).count()
292
325
  one_post.liked = liked
326
+
327
+ post_tags = get_post_tags(g.db, one_post)
293
328
 
294
329
  user = g.db.query(User).filter_by(id=g.user) if g.user else None
295
- return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, pages=pages)
330
+ return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages, post_tags=post_tags)
296
331
 
297
332
  @blog_bp.route("/.well-known/webfinger")
298
333
  def webfinger():
@@ -362,12 +397,8 @@ def single_showpost(post):
362
397
  if not page:
363
398
  return redirect("/")
364
399
 
365
- if "{posts}" in page.content_raw:
366
- posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
367
-
368
- return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages)
369
-
370
- return render_template("blog.html", blog=blog, page=page, pages=pages)
400
+ posts, processed_content = process_posts_content(page, blog.id, blog.url, g.main_domain)
401
+ return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages, processed_content=processed_content)
371
402
 
372
403
  if is_activitypub_request():
373
404
  url = f"https://{g.main_domain}"
@@ -382,23 +413,34 @@ def single_showpost(post):
382
413
  one_post.authorname = post_author.username
383
414
 
384
415
  user_agent = request.headers.get('User-Agent', '')
416
+ ip = anonymize(get_ip())
417
+
385
418
  if 'Bot' not in user_agent:
386
- ip = anonymize(get_ip())
387
- v = g.db.query(View).filter(View.blog == 1, View.post == one_post.id, View.hash == ip).count()
388
- user_agent = request.headers.get('User-Agent')
419
+ v = g.db.query(View).filter(View.blog == blog.id, View.post == one_post.id, View.hash == ip).count()
389
420
  if v < 1:
390
- now = datetime.now(timezone.utc).replace(microsecond=0)
391
- new_view = View(blog=1, post=one_post.id, hash=ip, date=now, agent=user_agent)
392
- g.db.add(new_view)
393
- g.db.commit()
421
+ if valkey_client.is_enabled():
422
+ valkey_client.queue_view(blog.id, one_post.id, ip, user_agent)
423
+ else:
424
+ now = datetime.now(timezone.utc).replace(microsecond=0)
425
+ new_view = View(blog=blog.id, post=one_post.id, hash=ip, date=now, agent=user_agent)
426
+ g.db.add(new_view)
427
+ g.db.commit()
428
+
429
+ likes_db = g.db.query(Like).filter(Like.blog == blog.id, Like.post == one_post.id).count()
430
+ likes_queued = valkey_client.count_queued_likes(blog.id, one_post.id) if valkey_client.is_enabled() else 0
431
+ likes = likes_db + likes_queued
432
+
433
+ liked_db = g.db.query(Like).filter(Like.blog == blog.id, Like.post == one_post.id, Like.hash == ip).count()
434
+ liked_queued = valkey_client.check_queued_like(blog.id, one_post.id, ip) if valkey_client.is_enabled() else False
435
+ liked = liked_db or liked_queued
394
436
 
395
- likes = g.db.query(Like).filter(Like.blog == 1, Like.post == one_post.id).count()
396
437
  one_post.likes = likes
397
- liked = g.db.query(Like).filter(Like.blog == 1, Like.post == one_post.id, Like.hash == anonymize(get_ip())).count()
398
438
  one_post.liked = liked
399
439
 
440
+ post_tags = get_post_tags(g.db, one_post)
441
+
400
442
  user = g.db.query(User).filter_by(id=g.user) if g.user else None
401
- return render_template("post.html", blog=blog, post=one_post, user=user, views=v, pages=pages)
443
+ return render_template("post.html", blog=blog, post=one_post, user=user, views=v, likes=likes, pages=pages, post_tags=post_tags)
402
444
 
403
445
  @blog_bp.route("/rss")
404
446
  def single_rss():
@@ -406,7 +448,7 @@ def single_rss():
406
448
  return redirect("/")
407
449
 
408
450
  blog = g.db.query(Blog).first()
409
- return _generate_rss(blog)
451
+ return serve_rss(blog)
410
452
 
411
453
  @blog_bp.route("/like", methods=["POST"])
412
454
  def like():
@@ -420,47 +462,45 @@ def like():
420
462
  if post < 1:
421
463
  resp = {"status": "no_post"}
422
464
  return resp, 404
465
+
423
466
  ip = anonymize(get_ip())
424
467
 
425
- l = g.db.query(Like).filter(Like.blog == blog_id, Like.post == post_id, Like.hash == ip).first()
426
- if l:
427
- g.db.delete(l)
428
- g.db.commit()
468
+ liked_db = g.db.query(Like).filter(Like.blog == blog_id, Like.post == post_id, Like.hash == ip).first()
469
+ liked_queued = valkey_client.check_queued_like(blog_id, post_id, ip) if valkey_client.is_enabled() else False
470
+
471
+ if liked_db:
472
+ if liked_db and valkey_client.is_enabled():
473
+ valkey_client.queue_like(blog_id, post_id, ip, 'remove')
474
+ elif liked_db and not valkey_client.is_enabled():
475
+ g.db.delete(liked_db)
476
+ g.db.commit()
477
+
478
+ resp = {"status": "deleted"}
479
+ status = 204
480
+
481
+ elif liked_queued and valkey_client.is_enabled():
482
+ valkey_client.queue_like(blog_id, post_id, ip, 'remove')
483
+
429
484
  resp = {"status": "deleted"}
430
485
  status = 204
431
486
  else:
432
- now = datetime.now(timezone.utc).replace(microsecond=0)
433
- like = Like(blog=blog_id, post=post_id, hash=ip, date=now)
434
- g.db.add(like)
435
- g.db.commit()
487
+ if valkey_client.is_enabled():
488
+ valkey_client.queue_like(blog_id, post_id, ip, 'add')
489
+ else:
490
+ now = datetime.now(timezone.utc).replace(microsecond=0)
491
+ like = Like(blog=blog_id, post=post_id, hash=ip, date=now)
492
+ g.db.add(like)
493
+ g.db.commit()
494
+
436
495
  resp = {"status": "ok"}
437
496
  status = 201
438
- return resp, status
439
497
 
498
+ return resp, status
440
499
 
441
- def _generate_rss(blog):
442
- posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").all()
443
- fg = FeedGenerator()
444
- fg.title(blog.title)
445
-
446
- if blog.access == "domain":
447
- blog_url = f"https://{blog.name}.{g.main_domain}"
448
- post_base_url = blog_url
449
- else:
450
- blog_url = f"https://{g.main_domain}/b/{blog.name}"
451
- post_base_url = blog_url
452
-
453
- fg.link(href=blog_url, rel="alternate")
454
- fg.description(blog.description_html)
455
-
456
- for p in posts:
457
- soup = BeautifulSoup(p.content_html, "html.parser")
458
- fe = fg.add_entry()
459
- fe.title(p.title)
460
- fe.link(href=f"{post_base_url}/{p.link}")
461
- fe.description(soup.get_text())
462
- fe.published(p.date.replace(tzinfo=timezone.utc))
463
-
464
- return Response(fg.rss_str(pretty=True).decode("utf-8"), mimetype="application/rss+xml")
465
500
 
466
501
 
502
+ def serve_rss(blog):
503
+ """
504
+ Here we just serve static RSS files :)
505
+ """
506
+ return send_file(f"{g.mainpath}/static/feed/{blog.name}.xml")