openwrite 0.13.3__tar.gz → 0.14.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 (90) hide show
  1. {openwrite-0.13.3/openwrite.egg-info → openwrite-0.14.0}/PKG-INFO +5 -3
  2. {openwrite-0.13.3 → openwrite-0.14.0}/README.md +2 -1
  3. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/cli.py +28 -6
  4. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/routes/blog.py +85 -40
  5. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/routes/dashboard.py +44 -65
  6. openwrite-0.14.0/openwrite/static/style/nativeEmoji.min.css +1 -0
  7. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/style/style.css +396 -0
  8. openwrite-0.14.0/openwrite/static/style/themes/paper.css +203 -0
  9. openwrite-0.14.0/openwrite/templates/dashboard.html +68 -0
  10. openwrite-0.14.0/openwrite/templates/discover.html +23 -0
  11. openwrite-0.14.0/openwrite/templates/edit.html +501 -0
  12. openwrite-0.14.0/openwrite/utils/i18n.json +1 -0
  13. openwrite-0.14.0/openwrite/utils/valkey_client.py +161 -0
  14. openwrite-0.14.0/openwrite/utils/worker.py +133 -0
  15. {openwrite-0.13.3 → openwrite-0.14.0/openwrite.egg-info}/PKG-INFO +5 -3
  16. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite.egg-info/SOURCES.txt +4 -1
  17. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite.egg-info/requires.txt +1 -0
  18. {openwrite-0.13.3 → openwrite-0.14.0}/pyproject.toml +4 -3
  19. openwrite-0.13.3/openwrite/static/style/nativeEmoji.min.css +0 -1
  20. openwrite-0.13.3/openwrite/templates/dashboard.html +0 -48
  21. openwrite-0.13.3/openwrite/templates/discover.html +0 -11
  22. openwrite-0.13.3/openwrite/templates/edit.html +0 -223
  23. openwrite-0.13.3/openwrite/utils/i18n.json +0 -1
  24. {openwrite-0.13.3 → openwrite-0.14.0}/LICENSE +0 -0
  25. {openwrite-0.13.3 → openwrite-0.14.0}/MANIFEST.in +0 -0
  26. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/__init__.py +0 -0
  27. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/db/__init__.py +0 -0
  28. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/db/base.py +0 -0
  29. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/gemini_multi.py +0 -0
  30. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/gemini_single.py +0 -0
  31. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/routes/admin.py +0 -0
  32. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/routes/auth.py +0 -0
  33. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/routes/federation.py +0 -0
  34. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/routes/main.py +0 -0
  35. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/routes/upload.py +0 -0
  36. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/IBMPlexSans-Regular.ttf +0 -0
  37. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/Merriweather.ttf +0 -0
  38. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/avatar.png +0 -0
  39. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/emoji.json +0 -0
  40. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/favicon.ico +0 -0
  41. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/logo.png +0 -0
  42. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/script/chart.js +0 -0
  43. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/script/highlight.min.js +0 -0
  44. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/script/nativeEmoji.js +0 -0
  45. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/script/openwrite.js +0 -0
  46. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/script/papaparse.min.js +0 -0
  47. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/script/showdown.min.js +0 -0
  48. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/script/showdown.min.js.map +0 -0
  49. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/script/site.compat.min.js +0 -0
  50. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/script/site.min.js +0 -0
  51. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/style/blog_design.css +0 -0
  52. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/style/default.css +0 -0
  53. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/style/hl.css +0 -0
  54. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/style/themes/cyberpunk.css +0 -0
  55. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/style/themes/light.css +0 -0
  56. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/style/themes/obscura.css +0 -0
  57. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/style/themes/stickynote.css +0 -0
  58. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/style/themes/terminal.css +0 -0
  59. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/style/themes/warmnight.css +0 -0
  60. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/static/style/themes/windows98.css +0 -0
  61. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/admin.html +0 -0
  62. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/blog.html +0 -0
  63. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/blog_footer.html +0 -0
  64. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/blog_header.html +0 -0
  65. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/blog_preview.html +0 -0
  66. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/changepw.html +0 -0
  67. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/create.html +0 -0
  68. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/footer.html +0 -0
  69. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/header.html +0 -0
  70. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/import.html +0 -0
  71. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/index.html +0 -0
  72. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/instances.html +0 -0
  73. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/login.html +0 -0
  74. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/new_page.html +0 -0
  75. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/new_post.html +0 -0
  76. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/page_preview.html +0 -0
  77. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/post.html +0 -0
  78. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/preview.html +0 -0
  79. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/register.html +0 -0
  80. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/settings.html +0 -0
  81. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/stats.html +0 -0
  82. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/templates/translations.html +0 -0
  83. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/utils/create_db.py +0 -0
  84. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/utils/db.py +0 -0
  85. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/utils/helpers.py +0 -0
  86. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite/utils/models.py +0 -0
  87. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite.egg-info/dependency_links.txt +0 -0
  88. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite.egg-info/entry_points.txt +0 -0
  89. {openwrite-0.13.3 → openwrite-0.14.0}/openwrite.egg-info/top_level.txt +0 -0
  90. {openwrite-0.13.3 → openwrite-0.14.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openwrite
3
- Version: 0.13.3
3
+ Version: 0.14.0
4
4
  License: GNU AFFERO GENERAL PUBLIC LICENSE
5
5
  Version 3, 19 November 2007
6
6
 
@@ -664,7 +664,7 @@ License: GNU AFFERO GENERAL PUBLIC LICENSE
664
664
  <https://www.gnu.org/licenses/>.
665
665
 
666
666
  Project-URL: Homepage, https://openwrite.io
667
- Project-URL: GitHub repository, https://github.com/openwriteio/openwrite
667
+ Project-URL: Source Code, https://codeberg.org/v0id1st/openwrite
668
668
  Requires-Python: >=3.11
669
669
  Description-Content-Type: text/markdown
670
670
  License-File: LICENSE
@@ -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)
@@ -694,6 +695,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
694
695
  ![PyPI - Version](https://img.shields.io/pypi/v/openwrite)
695
696
  [![License: AGPL-3.0-or-later](https://img.shields.io/badge/License-AGPL--3.0--or--later-blue.svg)](LICENSE)
696
697
  ![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)
697
699
 
698
700
  ---
699
701
 
@@ -713,7 +715,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
713
715
  - Lightweight
714
716
  - No tracking, only data collected is anonymized(hashed) IP for post view counting and likes
715
717
  - Custom CSS per blog
716
- - Federation using ActivityPub protocol
718
+ - Federation using ActivityPub protocol: Follow and Like
717
719
  - Likes system
718
720
  - Posts importing from xml/csv
719
721
  - Blog themes
@@ -5,6 +5,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
5
5
  ![PyPI - Version](https://img.shields.io/pypi/v/openwrite)
6
6
  [![License: AGPL-3.0-or-later](https://img.shields.io/badge/License-AGPL--3.0--or--later-blue.svg)](LICENSE)
7
7
  ![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)
8
9
 
9
10
  ---
10
11
 
@@ -24,7 +25,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
24
25
  - Lightweight
25
26
  - No tracking, only data collected is anonymized(hashed) IP for post view counting and likes
26
27
  - Custom CSS per blog
27
- - Federation using ActivityPub protocol
28
+ - Federation using ActivityPub protocol: Follow and Like
28
29
  - Likes system
29
30
  - Posts importing from xml/csv
30
31
  - Blog themes
@@ -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
-
@@ -1,6 +1,7 @@
1
1
  from flask import Blueprint, render_template, redirect, request, g, Response, abort
2
2
  from openwrite.utils.models import Blog, Post, User, View, Like, Page
3
3
  from openwrite.utils.helpers import gen_link, sanitize_html, anonymize, get_ip
4
+ from openwrite.utils.valkey_client import valkey_client
4
5
  from feedgen.feed import FeedGenerator
5
6
  from sqlalchemy import desc
6
7
  import os
@@ -215,19 +216,29 @@ def show_post(blog, post):
215
216
  blog_obj.url = f"/b/{blog_obj.name}"
216
217
 
217
218
  user_agent = request.headers.get('User-Agent')
219
+ ip = anonymize(get_ip())
218
220
 
219
221
  if 'Bot' not in user_agent:
220
- ip = anonymize(get_ip())
221
222
  v = g.db.query(View).filter(View.blog == blog_obj.id, View.post == one_post.id, View.hash == ip).count()
222
223
  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
224
+ if valkey_client.is_enabled():
225
+ valkey_client.queue_view(blog_obj.id, one_post.id, ip, user_agent)
226
+ else:
227
+ now = datetime.now(timezone.utc).replace(microsecond=0)
228
+ new_view = View(blog=blog_obj.id, post=one_post.id, hash=ip, date=now, agent=user_agent)
229
+ g.db.add(new_view)
230
+ g.db.commit()
231
+
232
+ likes_db = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id).count()
233
+ likes_queued = valkey_client.count_queued_likes(blog_obj.id, one_post.id) if valkey_client.is_enabled() else 0
234
+ likes = likes_db + likes_queued
235
+
236
+ liked_db = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id, Like.hash == ip).count()
237
+ liked_queued = valkey_client.check_queued_like(blog_obj.id, one_post.id, ip) if valkey_client.is_enabled() else False
238
+ liked = liked_db or liked_queued
239
+
240
+ one_post.likes = likes
241
+ one_post.liked = liked
231
242
 
232
243
  user = g.db.query(User).filter_by(id=g.user) if g.user else None
233
244
  return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages)
@@ -275,24 +286,33 @@ def show_subpost(blog, post):
275
286
  one_post.authorname = post_author.username
276
287
  blog_obj.url = f"https://{blog_obj.name}.{os.getenv('DOMAIN')}"
277
288
 
278
- user_agent = request.headers.get('User-Agent', '')
289
+ user_agent = request.headers.get('User-Agent')
290
+ ip = anonymize(get_ip())
291
+
279
292
  if 'Bot' not in user_agent:
280
- ip = anonymize(get_ip())
281
293
  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
294
  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()
295
+ if valkey_client.is_enabled():
296
+ valkey_client.queue_view(blog_obj.id, one_post.id, ip, user_agent)
297
+ else:
298
+ now = datetime.now(timezone.utc).replace(microsecond=0)
299
+ new_view = View(blog=blog_obj.id, post=one_post.id, hash=ip, date=now, agent=user_agent)
300
+ g.db.add(new_view)
301
+ g.db.commit()
302
+
303
+ likes_db = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id).count()
304
+ likes_queued = valkey_client.count_queued_likes(blog_obj.id, one_post.id) if valkey_client.is_enabled() else 0
305
+ likes = likes_db + likes_queued
306
+
307
+ liked_db = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id, Like.hash == ip).count()
308
+ liked_queued = valkey_client.check_queued_like(blog_obj.id, one_post.id, ip) if valkey_client.is_enabled() else False
309
+ liked = liked_db or liked_queued
288
310
 
289
- likes = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id).count()
290
311
  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
312
  one_post.liked = liked
293
313
 
294
314
  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)
315
+ return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages)
296
316
 
297
317
  @blog_bp.route("/.well-known/webfinger")
298
318
  def webfinger():
@@ -382,23 +402,32 @@ def single_showpost(post):
382
402
  one_post.authorname = post_author.username
383
403
 
384
404
  user_agent = request.headers.get('User-Agent', '')
405
+ ip = anonymize(get_ip())
406
+
385
407
  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')
408
+ v = g.db.query(View).filter(View.blog == blog.id, View.post == one_post.id, View.hash == ip).count()
389
409
  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()
410
+ if valkey_client.is_enabled():
411
+ valkey_client.queue_view(blog.id, one_post.id, ip, user_agent)
412
+ else:
413
+ now = datetime.now(timezone.utc).replace(microsecond=0)
414
+ new_view = View(blog=blog.id, post=one_post.id, hash=ip, date=now, agent=user_agent)
415
+ g.db.add(new_view)
416
+ g.db.commit()
417
+
418
+ likes_db = g.db.query(Like).filter(Like.blog == blog.id, Like.post == one_post.id).count()
419
+ likes_queued = valkey_client.count_queued_likes(blog.id, one_post.id) if valkey_client.is_enabled() else 0
420
+ likes = likes_db + likes_queued
421
+
422
+ liked_db = g.db.query(Like).filter(Like.blog == blog.id, Like.post == one_post.id, Like.hash == ip).count()
423
+ liked_queued = valkey_client.check_queued_like(blog.id, one_post.id, ip) if valkey_client.is_enabled() else False
424
+ liked = liked_db or liked_queued
394
425
 
395
- likes = g.db.query(Like).filter(Like.blog == 1, Like.post == one_post.id).count()
396
426
  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
427
  one_post.liked = liked
399
428
 
400
429
  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)
430
+ return render_template("post.html", blog=blog, post=one_post, user=user, views=v, likes=likes, pages=pages)
402
431
 
403
432
  @blog_bp.route("/rss")
404
433
  def single_rss():
@@ -420,21 +449,39 @@ def like():
420
449
  if post < 1:
421
450
  resp = {"status": "no_post"}
422
451
  return resp, 404
452
+
423
453
  ip = anonymize(get_ip())
424
454
 
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()
455
+ liked_db = g.db.query(Like).filter(Like.blog == blog_id, Like.post == post_id, Like.hash == ip).first()
456
+ liked_queued = valkey_client.check_queued_like(blog_id, post_id, ip) if valkey_client.is_enabled() else False
457
+
458
+ if liked_db:
459
+ if liked_db and valkey_client.is_enabled():
460
+ valkey_client.queue_like(blog_id, post_id, ip, 'remove')
461
+ elif liked_db and not valkey_client.is_enabled():
462
+ g.db.delete(liked_db)
463
+ g.db.commit()
464
+
465
+ resp = {"status": "deleted"}
466
+ status = 204
467
+
468
+ elif liked_queued and valkey_client.is_enabled():
469
+ valkey_client.queue_like(blog_id, post_id, ip, 'remove')
470
+
429
471
  resp = {"status": "deleted"}
430
472
  status = 204
431
473
  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()
474
+ if valkey_client.is_enabled():
475
+ valkey_client.queue_like(blog_id, post_id, ip, 'add')
476
+ else:
477
+ now = datetime.now(timezone.utc).replace(microsecond=0)
478
+ like = Like(blog=blog_id, post=post_id, hash=ip, date=now)
479
+ g.db.add(like)
480
+ g.db.commit()
481
+
436
482
  resp = {"status": "ok"}
437
483
  status = 201
484
+
438
485
  return resp, status
439
486
 
440
487
 
@@ -461,6 +508,4 @@ def _generate_rss(blog):
461
508
  fe.description(soup.get_text())
462
509
  fe.published(p.date.replace(tzinfo=timezone.utc))
463
510
 
464
- return Response(fg.rss_str(pretty=True).decode("utf-8"), mimetype="application/rss+xml")
465
-
466
-
511
+ return Response(fg.rss_str(pretty=True).decode("utf-8"), mimetype="application/rss+xml")
@@ -1,7 +1,8 @@
1
1
  from flask import Blueprint, render_template, redirect, request, g, abort, Response
2
- from openwrite.utils.models import Blog, Post, User, View, Page
2
+ from openwrite.utils.models import Blog, Post, User, View, Like, Page
3
3
  from openwrite.utils.helpers import sanitize_html, gen_link, safe_css, send_activity, is_html, get_themes
4
4
  from openwrite.routes.blog import create_activity_object, create_person_object
5
+ from openwrite.utils.valkey_client import valkey_client
5
6
  import requests
6
7
  from sqlalchemy import desc
7
8
  from datetime import datetime, timezone, timedelta
@@ -148,12 +149,10 @@ def delete_blog(name):
148
149
  "cc": [f"https://{g.main_domain}/followers/{blog.name}"],
149
150
  "object": person_object
150
151
  }
151
- send_activity(
152
- delete_activity,
153
- blog.priv_key,
154
- blog_url,
155
- f"{actor}/inbox"
156
- )
152
+ if valkey_client.is_enabled():
153
+ valkey_client.queue_federation_activity(delete_activity, blog.id, blog.priv_key, blog_url, f"{actor}/inbox")
154
+ else:
155
+ send_activity(delete_activity, blog.priv_key, blog_url, f"{actor}/inbox")
157
156
 
158
157
  g.db.delete(blog)
159
158
  g.db.commit()
@@ -226,15 +225,14 @@ def edit_blog(name):
226
225
  "cc": [f"https://{g.main_domain}/followers/{blog.name}"],
227
226
  "object": person_object
228
227
  }
229
- send_activity(
230
- update_activity,
231
- blog.priv_key,
232
- blog_url,
233
- f"{actor}/inbox"
234
- )
228
+ if valkey_client.is_enabled():
229
+ valkey_client.queue_federation_activity(update_activity, blog.id, blog.priv_key, blog_url, f"{actor}/inbox")
230
+ else:
231
+ send_activity(update_activity, blog.priv_key, blog_url, f"{actor}/inbox")
235
232
 
236
233
  return render_template("edit.html", blog=blog, posts=posts, themes=themes, pages=pages)
237
234
 
235
+
238
236
  @dashboard_bp.route("/dashboard/post/<name>", methods=['GET', 'POST'])
239
237
  def new_post(name):
240
238
  if g.user is None:
@@ -306,12 +304,10 @@ def new_post(name):
306
304
  "cc": [f"https://{g.main_domain}/followers/{blog.name}"],
307
305
  "object": activity_object
308
306
  }
309
- send_activity(
310
- activity,
311
- blog.priv_key,
312
- blog_url,
313
- f"{actor}/inbox"
314
- )
307
+ if valkey_client.is_enabled():
308
+ valkey_client.queue_federation_activity(activity, blog.id, blog.priv_key, blog_url, f"{actor}/inbox")
309
+ else:
310
+ send_activity(activity, blog.priv_key, blog_url, f"{actor}/inbox")
315
311
 
316
312
  return redirect(f"/dashboard/edit/{blog.name}")
317
313
 
@@ -411,46 +407,25 @@ def edit_post(blog, post):
411
407
 
412
408
  if followers:
413
409
  activity_object = create_activity_object(p, blog_obj.name, blog_url, full_content=False)
414
- if request.form.get("publish") == "1":
415
- for actor in followers:
416
- now_iso = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
417
- activity = {
418
- "@context": "https://www.w3.org/ns/activitystreams",
419
- "id": f"{blog_url}/{p.link}#create",
420
- "type": "Create",
421
- "actor": blog_url,
422
- "published": now_iso,
423
- "to": ["https://www.w3.org/ns/activitystreams#Public"],
424
- "cc": [f"https://{g.main_domain}/followers/{blog_obj.name}"],
425
- "object": activity_object
426
- }
427
- send_activity(
428
- activity,
429
- blog_obj.priv_key,
430
- blog_url,
431
- f"{actor}/inbox"
432
- )
433
- else:
434
- for actor in followers:
435
- now_iso = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
436
- update_activity = {
437
- "@context": "https://www.w3.org/ns/activitystreams",
438
- "id": f"{blog_url}/{p.link}#update",
439
- "type": "Update",
440
- "actor": blog_url,
441
- "published": now_iso,
442
- "to": ["https://www.w3.org/ns/activitystreams#Public"],
443
- "cc": [f"https://{g.main_domain}/followers/{blog_obj.name}"],
444
- "object": activity_object
445
- }
446
- send_activity(
447
- update_activity,
448
- blog_obj.priv_key,
449
- blog_url,
450
- f"{actor}/inbox"
451
- )
452
-
453
- return redirect(f"/dashboard/edit/{blog_obj.name}")
410
+
411
+ for actor in followers:
412
+ now_iso = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
413
+ update_activity = {
414
+ "@context": "https://www.w3.org/ns/activitystreams",
415
+ "id": f"{blog_url}/{p.link}#update",
416
+ "type": "Update",
417
+ "actor": blog_url,
418
+ "published": now_iso,
419
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
420
+ "cc": [f"https://{g.main_domain}/followers/{blog_obj.name}"],
421
+ "object": activity_object
422
+ }
423
+ if valkey_client.is_enabled():
424
+ valkey_client.queue_federation_activity(update_activity, blog_obj.id, blog_obj.priv_key, blog_url, f"{actor}/inbox")
425
+ else:
426
+ send_activity(update_activity, blog_obj.priv_key, blog_url, f"{actor}/inbox")
427
+
428
+ return redirect(f"/dashboard/edit/{blog}")
454
429
 
455
430
  @dashboard_bp.route("/dashboard/edit/<blog>/<post>/delete")
456
431
  def delete_post(blog, post):
@@ -487,12 +462,15 @@ def delete_post(blog, post):
487
462
  "cc": [f"https://{g.main_domain}/followers/{blog_obj.name}"],
488
463
  "object": activity_object
489
464
  }
490
- send_activity(
491
- delete_activity,
492
- blog_obj.priv_key,
493
- blog_url,
494
- f"{actor}/inbox"
495
- )
465
+ if valkey_client.is_enabled():
466
+ valkey_client.queue_federation_activity(delete_activity, blog_obj.id, blog_obj.priv_key, blog_url, f"{actor}/inbox")
467
+ else:
468
+ send_activity(
469
+ delete_activity,
470
+ blog_obj.priv_key,
471
+ blog_url,
472
+ f"{actor}/inbox"
473
+ )
496
474
 
497
475
  g.db.delete(p)
498
476
  g.db.commit()
@@ -500,6 +478,7 @@ def delete_post(blog, post):
500
478
  return redirect(f"/dashboard/edit/{blog_obj.name}")
501
479
 
502
480
 
481
+
503
482
  @dashboard_bp.route("/dashboard/changepw", methods=['GET', 'POST'])
504
483
  def changepw():
505
484
  if g.user is None:
@@ -0,0 +1 @@
1
+ .emoji-picker-container{position:relative}.emoji-picker-trigger{position:absolute;top:5px;left:8px;background:0 0;box-shadow:none;border:none;opacity:.5;cursor:pointer;outline:0!important}.emoji-picker-trigger:hover{opacity:1}.emoji-picker-open .emoji-picker-trigger>svg{opacity:0;visibility:hidden}.emoji-picker-open .emoji-picker-trigger:before{content:"\00D7";position:absolute;font-size:24px;background:#fff;top:-3px;right:10px}.emoji-picker{display:none;z-index:999;width:272px;overflow:hidden;background:#fff;border-radius:5px;box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23);margin-top:8px;outline:0}.emoji-picker-open .emoji-picker{display:block;position:absolute;right:0;top:30px}[data-native-emoji-large]+.emoji-picker{display:block;width:100%;margin-bottom:15px;position:static!important}.emoji-picker-tabs{padding:0;margin:0;display:table;width:100%;background:#eff0f1;list-style:none}.emoji-picker-tabs>li{display:table-cell}.emoji-picker-anchor{display:block;padding:5px;text-align:center;text-decoration:none;font-size:24px;position:initial;opacity:.75}.emoji-picker-anchor:hover{background:rgba(0,0,0,.05);opacity:1}.emoji-picker-anchor.active{background:rgba(0,0,0,.15)}.emoji-picker-list{padding:10px 0;margin:0;margin-left:3%;list-style:none;text-align:left;overflow:scroll;height:200px;}.emoji-picker-list>li{display:inline-block}.emoji-picker-emoji{background:0 0;border:none;-webkit-appearance:none;padding:5px;margin:1px;font-size:24px;line-height:1;border-radius:3px;cursor:pointer;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol'}.emoji-picker-emoji:hover{background:rgba(0,0,0,.07)}.emoji-picker-trigger svg {fill: #fff;}