openwrite 0.16.1__tar.gz → 0.17.1__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 (128) hide show
  1. {openwrite-0.16.1/openwrite.egg-info → openwrite-0.17.1}/PKG-INFO +21 -3
  2. {openwrite-0.16.1 → openwrite-0.17.1}/README.md +20 -2
  3. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/__init__.py +12 -6
  4. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/cli.py +4 -1
  5. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/routes/blog.py +24 -68
  6. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/routes/dashboard.py +47 -169
  7. openwrite-0.17.1/openwrite/routes/federation.py +262 -0
  8. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/routes/main.py +2 -2
  9. openwrite-0.17.1/openwrite/static/feed/default.xml +2 -0
  10. openwrite-0.17.1/openwrite/static/feed/test.xml +2 -0
  11. openwrite-0.17.1/openwrite/static/feed/testx.xml +2 -0
  12. openwrite-0.17.1/openwrite/static/script/post_preview.js +106 -0
  13. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/style/style.css +79 -0
  14. openwrite-0.17.1/openwrite/static/style/toc.css +28 -0
  15. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/blog.html +1 -1
  16. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/blog_header.html +2 -1
  17. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/blog_preview.html +1 -1
  18. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/dashboard.html +2 -2
  19. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/edit.html +6 -6
  20. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/footer.html +1 -1
  21. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/new_post.html +63 -18
  22. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/page_preview.html +1 -1
  23. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/post.html +2 -2
  24. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/preview.html +3 -2
  25. openwrite-0.17.1/openwrite/utils/activitypub.py +154 -0
  26. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/utils/db.py +4 -1
  27. openwrite-0.17.1/openwrite/utils/helpers.py +200 -0
  28. openwrite-0.17.1/openwrite/utils/i18n.json +1 -0
  29. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/utils/migrations.py +3 -3
  30. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/utils/models.py +44 -0
  31. openwrite-0.17.1/openwrite/utils/toc.py +47 -0
  32. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/utils/worker.py +37 -22
  33. {openwrite-0.16.1 → openwrite-0.17.1/openwrite.egg-info}/PKG-INFO +21 -3
  34. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite.egg-info/SOURCES.txt +9 -27
  35. {openwrite-0.16.1 → openwrite-0.17.1}/pyproject.toml +1 -1
  36. openwrite-0.17.1/tests/test_federation.py +726 -0
  37. openwrite-0.16.1/openwrite/routes/federation.py +0 -329
  38. openwrite-0.16.1/openwrite/static/feed/74.xml +0 -12
  39. openwrite-0.16.1/openwrite/static/feed/a.xml +0 -2
  40. openwrite-0.16.1/openwrite/static/feed/anim3sh1t.xml +0 -12
  41. openwrite-0.16.1/openwrite/static/feed/b.xml +0 -2
  42. openwrite-0.16.1/openwrite/static/feed/corrode.xml +0 -4
  43. openwrite-0.16.1/openwrite/static/feed/dirtysanchez.xml +0 -6
  44. openwrite-0.16.1/openwrite/static/feed/eikerammen.xml +0 -165
  45. openwrite-0.16.1/openwrite/static/feed/fedi.xml +0 -9
  46. openwrite-0.16.1/openwrite/static/feed/friskt.xml +0 -2
  47. openwrite-0.16.1/openwrite/static/feed/gordi.xml +0 -6
  48. openwrite-0.16.1/openwrite/static/feed/igilq.xml +0 -226
  49. openwrite-0.16.1/openwrite/static/feed/io.xml +0 -50
  50. openwrite-0.16.1/openwrite/static/feed/isyuricunha.xml +0 -4
  51. openwrite-0.16.1/openwrite/static/feed/kenny.xml +0 -2
  52. openwrite-0.16.1/openwrite/static/feed/m0bi.xml +0 -6
  53. openwrite-0.16.1/openwrite/static/feed/main.xml +0 -236
  54. openwrite-0.16.1/openwrite/static/feed/mywritese.xml +0 -2
  55. openwrite-0.16.1/openwrite/static/feed/nay-thoughs.xml +0 -2
  56. openwrite-0.16.1/openwrite/static/feed/nintendoflesz.xml +0 -7
  57. openwrite-0.16.1/openwrite/static/feed/phronima.xml +0 -68
  58. openwrite-0.16.1/openwrite/static/feed/ram.xml +0 -75
  59. openwrite-0.16.1/openwrite/static/feed/samah.xml +0 -2
  60. openwrite-0.16.1/openwrite/static/feed/themes-test.xml +0 -18
  61. openwrite-0.16.1/openwrite/static/feed/totylkoja.xml +0 -78
  62. openwrite-0.16.1/openwrite/static/feed/vegafjord.xml +0 -48
  63. openwrite-0.16.1/openwrite/static/feed/vijayvaradharaj.xml +0 -2
  64. openwrite-0.16.1/openwrite/utils/helpers.py +0 -217
  65. openwrite-0.16.1/openwrite/utils/i18n.json +0 -1
  66. {openwrite-0.16.1 → openwrite-0.17.1}/LICENSE +0 -0
  67. {openwrite-0.16.1 → openwrite-0.17.1}/MANIFEST.in +0 -0
  68. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/db/__init__.py +0 -0
  69. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/db/base.py +0 -0
  70. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/gemini_multi.py +0 -0
  71. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/gemini_single.py +0 -0
  72. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/routes/admin.py +0 -0
  73. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/routes/auth.py +0 -0
  74. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/routes/upload.py +0 -0
  75. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/IBMPlexSans-Regular.ttf +0 -0
  76. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/Merriweather.ttf +0 -0
  77. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/avatar.png +0 -0
  78. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/emoji.json +0 -0
  79. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/favicon.ico +0 -0
  80. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/favicon.png +0 -0
  81. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/feed/.gitkeep +0 -0
  82. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/logo.png +0 -0
  83. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/script/chart.js +0 -0
  84. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/script/highlight.min.js +0 -0
  85. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/script/nativeEmoji.js +0 -0
  86. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/script/openwrite.js +0 -0
  87. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/script/papaparse.min.js +0 -0
  88. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/script/showdown.min.js +0 -0
  89. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/script/showdown.min.js.map +0 -0
  90. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/script/site.compat.min.js +0 -0
  91. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/script/site.min.js +0 -0
  92. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/style/blog_design.css +0 -0
  93. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/style/default.css +0 -0
  94. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/style/hl.css +0 -0
  95. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/style/nativeEmoji.min.css +0 -0
  96. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/style/themes/cyberpunk.css +0 -0
  97. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/style/themes/light.css +0 -0
  98. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/style/themes/obscura.css +0 -0
  99. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/style/themes/paper.css +0 -0
  100. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/style/themes/sepia.css +0 -0
  101. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/style/themes/stickynote.css +0 -0
  102. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/style/themes/synthwave.css +0 -0
  103. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/style/themes/terminal.css +0 -0
  104. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/style/themes/warmnight.css +0 -0
  105. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/static/style/themes/windows98.css +0 -0
  106. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/admin.html +0 -0
  107. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/blog_footer.html +0 -0
  108. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/changepw.html +0 -0
  109. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/create.html +0 -0
  110. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/discover.html +0 -0
  111. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/header.html +0 -0
  112. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/import.html +0 -0
  113. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/index.html +0 -0
  114. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/instances.html +0 -0
  115. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/login.html +0 -0
  116. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/new_page.html +0 -0
  117. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/register.html +0 -0
  118. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/settings.html +0 -0
  119. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/stats.html +0 -0
  120. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/templates/translations.html +0 -0
  121. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/utils/create_db.py +0 -0
  122. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/utils/tags.py +0 -0
  123. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite/utils/valkey_client.py +0 -0
  124. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite.egg-info/dependency_links.txt +0 -0
  125. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite.egg-info/entry_points.txt +0 -0
  126. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite.egg-info/requires.txt +0 -0
  127. {openwrite-0.16.1 → openwrite-0.17.1}/openwrite.egg-info/top_level.txt +0 -0
  128. {openwrite-0.16.1 → openwrite-0.17.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openwrite
3
- Version: 0.16.1
3
+ Version: 0.17.1
4
4
  License: GNU AFFERO GENERAL PUBLIC LICENSE
5
5
  Version 3, 19 November 2007
6
6
 
@@ -761,7 +761,25 @@ to run it in background.
761
761
 
762
762
  5. Default user is `admin` with password `openwrite`. You can (and should!) change it in your dashboard. Enjoy!
763
763
 
764
+ ### Local development over HTTP
764
765
 
765
- #### Docker
766
+ For `http://localhost:8081`, set these values in `.env`:
766
767
 
767
- Docker image is still being prepared. Stay tuned!
768
+ ```dotenv
769
+ DOMAIN=localhost:8081
770
+ IP=127.0.0.1
771
+ PORT=8081
772
+ GEMINI=no
773
+ ```
774
+
775
+ When working from a checkout, install it with `./venv/bin/python -m pip install -e .`
776
+ so changes in this folder are used by the CLI.
777
+ Run `./venv/bin/openwrite debugrun` (or `./venv/bin/openwrite run`) and open
778
+ `http://localhost:8081`. `DOMAIN` must match the hostname and port in the browser,
779
+ without a protocol prefix. For a custom local hostname such as `openwrite.local`,
780
+ use `DOMAIN=openwrite.local:8081` and point that hostname to `127.0.0.1` in your hosts file.
781
+ Restart the server after changing `.env`.
782
+
783
+ Both commands serve plain HTTP. In an HTTPS deployment, terminate TLS at a reverse
784
+ proxy and forward the original host and scheme with `X-Forwarded-Host` and
785
+ `X-Forwarded-Proto` so browser links use the public address.
@@ -71,7 +71,25 @@ to run it in background.
71
71
 
72
72
  5. Default user is `admin` with password `openwrite`. You can (and should!) change it in your dashboard. Enjoy!
73
73
 
74
+ ### Local development over HTTP
74
75
 
75
- #### Docker
76
+ For `http://localhost:8081`, set these values in `.env`:
76
77
 
77
- Docker image is still being prepared. Stay tuned!
78
+ ```dotenv
79
+ DOMAIN=localhost:8081
80
+ IP=127.0.0.1
81
+ PORT=8081
82
+ GEMINI=no
83
+ ```
84
+
85
+ When working from a checkout, install it with `./venv/bin/python -m pip install -e .`
86
+ so changes in this folder are used by the CLI.
87
+ Run `./venv/bin/openwrite debugrun` (or `./venv/bin/openwrite run`) and open
88
+ `http://localhost:8081`. `DOMAIN` must match the hostname and port in the browser,
89
+ without a protocol prefix. For a custom local hostname such as `openwrite.local`,
90
+ use `DOMAIN=openwrite.local:8081` and point that hostname to `127.0.0.1` in your hosts file.
91
+ Restart the server after changing `.env`.
92
+
93
+ Both commands serve plain HTTP. In an HTTPS deployment, terminate TLS at a reverse
94
+ proxy and forward the original host and scheme with `X-Forwarded-Host` and
95
+ `X-Forwarded-Proto` so browser links use the public address.
@@ -6,7 +6,6 @@ import os
6
6
  from .utils.helpers import generate_nonce, get_ip
7
7
  import time
8
8
  from .utils.models import Settings, Home
9
- from sqlalchemy import distinct
10
9
  import bcrypt
11
10
  import json
12
11
  from cryptography.hazmat.primitives.asymmetric import rsa
@@ -87,8 +86,8 @@ def create_app(test_config=None):
87
86
  blog=1,
88
87
  name="Home",
89
88
  url="",
90
- content_raw=f"![hello](https://openwrite.b-cdn.net/hello.jpg =500x258)\n\n# Hello there! 👋\n\nYou can edit your blog home page in [dashboard](https://{domain}/dashboard/page/1/edit)\n\n---\n### Posts\n\n{{posts}}",
91
- content_html=f"<p><img src=\"https://openwrite.b-cdn.net/hello.jpg\" width=\"500\" height=\"258\"></p><h1>Hello there! 👋</h1><p>You can edit your blog home page in <a href=\"https://{domain}/dashboard/page/1/edit\">dashboard</a></p>\n\n<hr>\n<h3>Posts\n\n{{posts}}",
89
+ content_raw=f"![hello](https://openwrite.b-cdn.net/hello.jpg =500x258)\n\n# Hello there! 👋\n\nYou can edit your blog home page in [dashboard](/dashboard/page/1/edit)\n\n---\n### Posts\n\n{{posts}}",
90
+ content_html=f"<p><img src=\"https://openwrite.b-cdn.net/hello.jpg\" width=\"500\" height=\"258\"></p><h1>Hello there! 👋</h1><p>You can edit your blog home page in <a href=\"/dashboard/page/1/edit\">dashboard</a></p>\n\n<hr>\n<h3>Posts\n\n{{posts}}",
92
91
  show="0"
93
92
  )
94
93
  SessionLocal.add(new_page)
@@ -193,12 +192,20 @@ def create_app(test_config=None):
193
192
 
194
193
  app.jinja_env.globals['_'] = _
195
194
 
195
+ @app.template_filter('with_toc')
196
+ def with_toc(content, enabled):
197
+ if enabled != "1":
198
+ return content
199
+ from .utils.toc import add_table_of_contents
200
+ label = g.trans.get('table-of-contents', 'Spis treści' if g.lang == 'pl' else 'Table of contents')
201
+ return add_table_of_contents(content, label)
202
+
196
203
  @app.before_request
197
204
  def before():
198
205
  from flask import request, session
199
206
  from openwrite.utils.models import Home, Settings
200
207
  g.db = SessionLocal
201
- t_query = g.db.query(distinct(Home.language)).all()
208
+ t_query = g.db.query(Home.language).distinct().all()
202
209
  translations = [row[0] for row in t_query]
203
210
  lang = request.cookies.get("lang")
204
211
  if not lang or lang not in translations:
@@ -253,7 +260,6 @@ def create_app(test_config=None):
253
260
 
254
261
  @app.after_request
255
262
  def after(response):
256
- nonce = g.nonce
257
263
  #response.headers["Content-Security-Policy"] = (
258
264
  # f"default-src 'none'; "
259
265
  # f"script-src 'self' 'nonce-{nonce}' https://cdn.jsdelivr.net http://{g.main_domain}"
@@ -281,6 +287,6 @@ def create_app(test_config=None):
281
287
 
282
288
  @app.teardown_appcontext
283
289
  def shutdown_session(exception=None):
284
- g.db.remove()
290
+ SessionLocal.remove()
285
291
 
286
292
  return app
@@ -168,6 +168,9 @@ def init():
168
168
  @click.option("-d", "--daemon", is_flag=True, help="Run in background (daemon)")
169
169
  def run(daemon):
170
170
  print_banner()
171
+ # Initialize/migrate once before concurrent web workers and the queue worker start.
172
+ from openwrite import create_app
173
+ create_app()
171
174
 
172
175
  ip = os.getenv("IP", "0.0.0.0")
173
176
  port = int(os.getenv("PORT", 8080))
@@ -184,7 +187,7 @@ def run(daemon):
184
187
  gunicorn_error_log = os.path.join(logs_dir, "openwrite_error.log")
185
188
 
186
189
  gunicorn_cmd = [
187
- "gunicorn",
190
+ sys.executable, "-m", "gunicorn",
188
191
  "-w", "4",
189
192
  "openwrite:create_app()",
190
193
  "--bind", f"{ip}:{port}"
@@ -3,6 +3,7 @@ from openwrite.utils.models import Blog, Post, User, View, Like, Page, PostDraft
3
3
  from openwrite.utils.tags import get_post_tags, parse_tags_from_posts_syntax, get_posts_by_tags
4
4
  from openwrite.utils.helpers import gen_link, sanitize_html, anonymize, get_ip
5
5
  from openwrite.utils.valkey_client import valkey_client
6
+ from openwrite.utils.activitypub import actor_url, create_activity_object, iso_date
6
7
  from feedgen.feed import FeedGenerator
7
8
  from sqlalchemy import desc
8
9
  import os
@@ -33,59 +34,11 @@ def process_posts_content(page, blog_id, blog_url, domain):
33
34
 
34
35
  return posts, processed_content
35
36
 
36
- def create_activity_object(post, blog, url, full_content=True):
37
- """Create ActivityPub object (Article or Note) based on post content length"""
38
- dt = datetime.strptime(str(post.date), "%Y-%m-%d %H:%M:%S")
39
- dt = dt.replace(tzinfo=timezone.utc)
40
- iso = dt.isoformat(timespec="seconds").replace("+00:00", "Z")
41
-
42
- clean_content = re.sub(r'<[^>]+>', '', post.content_html)
43
- content_length = len(clean_content)
44
-
45
- base_object = {
46
- "id": f"{url}/{post.link}",
47
- "attributedTo": url,
48
- "published": iso,
49
- "url": f"{url}/{post.link}",
50
- "to": ["https://www.w3.org/ns/activitystreams#Public"],
51
- "cc": [f"https://{g.main_domain}/followers/{blog}"],
52
- "sensitive": False,
53
- "atomUri": f"{url}/{post.link}",
54
- "attachment": [],
55
- "tag": []
56
- }
57
-
58
- if content_length > 200:
59
- words = clean_content.split()[:50]
60
- summary = ' '.join(words) + '...' if len(words) == 50 else ' '.join(words)
61
-
62
- if full_content:
63
- content = f"<h1>{post.title}</h1>{post.content_html}"
64
- else:
65
- content = f"<p><a href=\"{url}/{post.link}\">Read full article: {post.title}</a></p>"
66
-
67
- base_object.update({
68
- "type": "Article",
69
- "name": post.title,
70
- "summary": summary,
71
- "content": content
72
- })
73
- else:
74
- base_object.update({
75
- "type": "Note",
76
- "summary": None,
77
- "content": f"<h3>{post.title}</h3>{post.content_html}<p><a href=\"{url}/{post.link}\">View post</a></p>"
78
- })
79
-
80
- return base_object
81
-
82
37
  def create_person_object(blog, url):
83
38
  """Create ActivityPub Person object for the blog"""
84
- published = blog.created
85
- dt = datetime.strptime(str(published), "%Y-%m-%d %H:%M:%S")
86
- dt = dt.replace(tzinfo=timezone.utc)
87
- iso = dt.isoformat(timespec="seconds").replace("+00:00", "Z")
88
- if blog.fedi_description != "":
39
+ url = actor_url(blog)
40
+ iso = iso_date(blog.created)
41
+ if blog.fedi_description:
89
42
  summary = blog.fedi_description
90
43
  else:
91
44
  summary = f"{blog.name} - Blog on {g.main_domain}"
@@ -131,6 +84,8 @@ def create_person_object(blog, url):
131
84
  "url": f"https://{g.main_domain}/static/avatar.png"
132
85
  }
133
86
  }
87
+ if blog.updated:
88
+ actor['updated'] = iso_date(blog.updated)
134
89
  return actor
135
90
 
136
91
  def is_activitypub_request():
@@ -147,7 +102,7 @@ def show_blog(blog):
147
102
  return redirect("/")
148
103
 
149
104
  if blog_obj.access == "domain":
150
- return redirect(f"https://{blog_obj.name}.{os.getenv('DOMAIN')}/")
105
+ return redirect(f"{request.scheme}://{blog_obj.name}.{os.getenv('DOMAIN')}/")
151
106
 
152
107
  if is_activitypub_request():
153
108
  url = f"https://{g.main_domain}/b/{blog}"
@@ -170,10 +125,10 @@ def show_subblog(blog):
170
125
  return redirect("/")
171
126
  blog_obj = g.db.query(Blog).filter_by(name=blog).first()
172
127
  if blog_obj is None:
173
- return redirect(f"https://{os.getenv('DOMAIN')}/")
128
+ return redirect(f"{request.scheme}://{os.getenv('DOMAIN')}/")
174
129
 
175
130
  if blog_obj.access == "path":
176
- return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog_obj.name}")
131
+ return redirect(f"{request.scheme}://{os.getenv('DOMAIN')}/b/{blog_obj.name}")
177
132
 
178
133
  if is_activitypub_request():
179
134
  url = f"https://{blog}.{g.main_domain}"
@@ -183,7 +138,7 @@ def show_subblog(blog):
183
138
  'Vary': 'Accept'
184
139
  })
185
140
 
186
- blog_obj.url = f"https://{blog_obj.name}.{os.getenv('DOMAIN')}"
141
+ blog_obj.url = f"{request.scheme}://{blog_obj.name}.{os.getenv('DOMAIN')}"
187
142
  pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
188
143
  homepage = g.db.query(Page).filter(Page.blog == blog_obj.id, Page.url == "").first()
189
144
 
@@ -202,7 +157,7 @@ def show_post(blog, post):
202
157
  return serve_rss(blog_obj)
203
158
 
204
159
  if blog_obj.access == "domain":
205
- return redirect(f"https://{blog_obj.name}.{os.getenv('DOMAIN')}/{post}")
160
+ return redirect(f"{request.scheme}://{blog_obj.name}.{os.getenv('DOMAIN')}/{post}")
206
161
 
207
162
  blog_obj.url = f"/b/{blog_obj.name}"
208
163
  pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
@@ -266,15 +221,15 @@ def show_subpost(blog, post):
266
221
  return redirect("/")
267
222
  blog_obj = g.db.query(Blog).filter_by(name=blog).first()
268
223
  if blog_obj is None:
269
- return redirect(f"https://{os.getenv('DOMAIN')}/")
224
+ return redirect(f"{request.scheme}://{os.getenv('DOMAIN')}/")
270
225
 
271
226
  if post == "rss":
272
227
  return serve_rss(blog_obj)
273
228
 
274
229
  if blog_obj.access == "path":
275
- return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog_obj.name}/{post}")
230
+ return redirect(f"{request.scheme}://{os.getenv('DOMAIN')}/b/{blog_obj.name}/{post}")
276
231
 
277
- blog_obj.url = f"https://{blog_obj.name}.{os.getenv('DOMAIN')}"
232
+ blog_obj.url = f"{request.scheme}://{blog_obj.name}.{os.getenv('DOMAIN')}"
278
233
 
279
234
  pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
280
235
  one_post = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.link == post, Post.isdraft == "0").first()
@@ -298,7 +253,7 @@ def show_subpost(blog, post):
298
253
 
299
254
  post_author = g.db.query(User).filter_by(id=blog_obj.owner).first()
300
255
  one_post.authorname = post_author.username
301
- blog_obj.url = f"https://{blog_obj.name}.{os.getenv('DOMAIN')}"
256
+ blog_obj.url = f"{request.scheme}://{blog_obj.name}.{os.getenv('DOMAIN')}"
302
257
 
303
258
  user_agent = request.headers.get('User-Agent')
304
259
  ip = anonymize(get_ip())
@@ -347,11 +302,10 @@ def webfinger():
347
302
  if not blog_obj:
348
303
  abort(404)
349
304
 
350
- if blog_obj.access == "domain":
351
- blog_url = f"https://{username}.{g.main_domain}"
352
- else:
353
- blog_url = f"https://{g.main_domain}/b/{username}"
354
-
305
+ if domain != g.main_domain:
306
+ abort(404)
307
+ blog_url = actor_url(blog_obj)
308
+
355
309
  data = {
356
310
  "subject": f"acct:{username}@{g.main_domain}",
357
311
  "links": [{
@@ -379,9 +333,9 @@ def subdomain_webfinger(blog):
379
333
  if "@" in account_part:
380
334
  username = account_part.split("@")[0]
381
335
  corrected_resource = f"acct:{username}@{g.main_domain}"
382
- return redirect(f"https://{g.main_domain}/.well-known/webfinger?resource={corrected_resource}")
336
+ return redirect(f"{request.scheme}://{g.main_domain}/.well-known/webfinger?resource={corrected_resource}")
383
337
 
384
- return redirect(f"https://{g.main_domain}/.well-known/webfinger?resource={resource}")
338
+ return redirect(f"{request.scheme}://{g.main_domain}/.well-known/webfinger?resource={resource}")
385
339
 
386
340
  @blog_bp.route("/p/<post>")
387
341
  def single_showpost(post):
@@ -574,7 +528,8 @@ def autosave_post(blog_name):
574
528
  session_key = data.get('session_key') if post_id is None else None
575
529
 
576
530
  now = datetime.now(timezone.utc).replace(microsecond=0)
577
- draft = PostDraft(post_id=post_id, blog_id=blog.id, session_key=session_key, title=title, content_raw=content_raw, saved_at=now)
531
+ draft = PostDraft(post_id=post_id, blog_id=blog.id, session_key=session_key, title=title, content_raw=content_raw, saved_at=now,
532
+ toc_enabled='1' if data.get('toc_enabled') == '1' else '0')
578
533
  g.db.add(draft)
579
534
  g.db.flush()
580
535
 
@@ -613,5 +568,6 @@ def get_autosaves(blog_name):
613
568
  'id': d.id,
614
569
  'title': d.title,
615
570
  'content_raw': d.content_raw,
571
+ 'toc_enabled': d.toc_enabled,
616
572
  'saved_at': d.saved_at.strftime('%Y-%m-%dT%H:%M:%SZ')
617
573
  } for d in drafts])
@@ -1,8 +1,11 @@
1
1
  from flask import Blueprint, render_template, redirect, request, g, abort, Response
2
2
  from openwrite.utils.models import Blog, Post, User, View, Like, Page, Settings, Tag, PostTag
3
- from openwrite.utils.helpers import sanitize_html, gen_link, safe_css, send_activity, is_html, get_themes
3
+ from openwrite.utils.helpers import sanitize_html, gen_link, safe_css, is_html, get_themes
4
4
  from openwrite.utils.tags import parse_tags_from_input, update_post_tags, get_post_tags
5
- from openwrite.routes.blog import create_activity_object, create_person_object
5
+ from openwrite.routes.blog import create_person_object
6
+ from openwrite.utils.activitypub import (
7
+ actor_url, post_url, create_activity_object, activity_for, queue_activity, deliver_pending,
8
+ )
6
9
  from openwrite.utils.valkey_client import valkey_client
7
10
  import requests
8
11
  from sqlalchemy import desc
@@ -105,8 +108,8 @@ def create_blog():
105
108
  blog=b_id,
106
109
  name="Home",
107
110
  url="",
108
- content_raw=f"![hello](https://openwrite.b-cdn.net/hello.jpg =500x258)\n\n# Hello there! 👋\n\nYou can edit your blog home page in [dashboard](https://{g.main_domain}/dashboard/edit/{form_url})\n\n---\n### Posts\n\n{{posts}}",
109
- content_html=f"<p><img src=\"https://openwrite.b-cdn.net/hello.jpg\" width=\"500\" height=\"258\"></p><h1>Hello there! 👋</h1><p>You can edit your blog home page in <a href=\"https://{g.main_domain}/dashboard/edit/{form_url}\">dashboard</a></p>\n\n<hr>\n<h3>Posts</h3>\n\n{{posts}}",
111
+ content_raw=f"![hello](https://openwrite.b-cdn.net/hello.jpg =500x258)\n\n# Hello there! 👋\n\nYou can edit your blog home page in [dashboard](/dashboard/edit/{form_url})\n\n---\n### Posts\n\n{{posts}}",
112
+ content_html=f"<p><img src=\"https://openwrite.b-cdn.net/hello.jpg\" width=\"500\" height=\"258\"></p><h1>Hello there! 👋</h1><p>You can edit your blog home page in <a href=\"/dashboard/edit/{form_url}\">dashboard</a></p>\n\n<hr>\n<h3>Posts</h3>\n\n{{posts}}",
110
113
  show="0"
111
114
  )
112
115
  g.db.add(new_page)
@@ -127,37 +130,11 @@ def delete_blog(name):
127
130
  if blog is None or blog.owner != g.user:
128
131
  abort(403)
129
132
 
130
- if blog.access == "domain":
131
- blog_url = f"https://{blog.name}.{g.main_domain}"
132
- else:
133
- blog_url = f"https://{g.main_domain}/b/{blog.name}"
134
-
135
- followers = []
136
- if blog.followers not in (None, "null", "NULL"):
137
- followers = json.loads(blog.followers)
138
-
139
- if followers:
140
- person_object = create_person_object(blog, blog_url)
141
-
142
- for actor in followers:
143
- now_iso = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
144
- delete_activity = {
145
- "@context": "https://www.w3.org/ns/activitystreams",
146
- "id": f"{blog_url}#delete-{now_iso}",
147
- "type": "Delete",
148
- "actor": blog_url,
149
- "published": now_iso,
150
- "to": ["https://www.w3.org/ns/activitystreams#Public"],
151
- "cc": [f"https://{g.main_domain}/followers/{blog.name}"],
152
- "object": person_object
153
- }
154
- if valkey_client.is_enabled():
155
- valkey_client.queue_federation_activity(delete_activity, blog.id, blog.priv_key, blog_url, f"{actor}/inbox")
156
- else:
157
- send_activity(delete_activity, blog.priv_key, blog_url, f"{actor}/inbox")
158
-
133
+ activity = activity_for(blog, 'Delete', {'id': actor_url(blog), 'type': 'Tombstone', 'formerType': 'Person'})
134
+ jobs = queue_activity(g.db, blog, activity)
159
135
  g.db.delete(blog)
160
136
  g.db.commit()
137
+ deliver_pending(g.db, jobs, budget_seconds=1)
161
138
  return redirect("/dashboard")
162
139
 
163
140
  @dashboard_bp.route("/dashboard/edit/<name>", methods=['GET', 'POST'])
@@ -200,37 +177,12 @@ def edit_blog(name):
200
177
  if selected_theme not in themes:
201
178
  return render_template("edit.html", blog=blog, posts=posts, themes=themes, pages=pages, error="wrong-theme")
202
179
  blog.theme = selected_theme
203
- g.db.commit()
204
-
180
+ jobs = []
205
181
  if old_title != new_title or old_fedi_description != new_fedi_description:
206
- if blog.access == "domain":
207
- blog_url = f"https://{blog.name}.{g.main_domain}"
208
- else:
209
- blog_url = f"https://{g.main_domain}/b/{blog.name}"
210
-
211
- followers = []
212
- if blog.followers not in (None, "null", "NULL"):
213
- followers = json.loads(blog.followers)
214
-
215
- if followers:
216
- person_object = create_person_object(blog, blog_url)
217
-
218
- for actor in followers:
219
- now_iso = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
220
- update_activity = {
221
- "@context": "https://www.w3.org/ns/activitystreams",
222
- "id": f"{blog_url}#update-{now_iso}",
223
- "type": "Update",
224
- "actor": blog_url,
225
- "published": now_iso,
226
- "to": ["https://www.w3.org/ns/activitystreams#Public"],
227
- "cc": [f"https://{g.main_domain}/followers/{blog.name}"],
228
- "object": person_object
229
- }
230
- if valkey_client.is_enabled():
231
- valkey_client.queue_federation_activity(update_activity, blog.id, blog.priv_key, blog_url, f"{actor}/inbox")
232
- else:
233
- send_activity(update_activity, blog.priv_key, blog_url, f"{actor}/inbox")
182
+ obj = create_person_object(blog, actor_url(blog))
183
+ jobs = queue_activity(g.db, blog, activity_for(blog, 'Update', obj))
184
+ g.db.commit()
185
+ deliver_pending(g.db, jobs, budget_seconds=1)
234
186
 
235
187
  return render_template("edit.html", blog=blog, posts=posts, themes=themes, pages=pages)
236
188
 
@@ -277,7 +229,8 @@ def new_post(name):
277
229
  date=now,
278
230
  feed=request.form.get('feed'),
279
231
  isdraft=request.form.get('draft', '0'),
280
- comments_allowed=request.form.get('comments_allowed', '0')
232
+ comments_allowed=request.form.get('comments_allowed', '0'),
233
+ toc_enabled='1' if request.form.get('toc_enabled') == '1' else '0'
281
234
  )
282
235
  g.db.add(post)
283
236
  g.db.flush() # Get the post ID
@@ -288,37 +241,11 @@ def new_post(name):
288
241
  tag_names = parse_tags_from_input(tags_input)
289
242
  update_post_tags(g.db, post, tag_names)
290
243
 
244
+ jobs = []
245
+ if post.isdraft == '0':
246
+ jobs = queue_activity(g.db, blog, activity_for(blog, 'Create', create_activity_object(post, blog)))
291
247
  g.db.commit()
292
-
293
- if blog.access == "domain":
294
- blog_url = f"https://{blog.name}.{g.main_domain}"
295
- else:
296
- blog_url = f"https://{g.main_domain}/b/{blog.name}"
297
-
298
- followers = []
299
- if blog.followers not in (None, "null", "NULL"):
300
- followers = json.loads(blog.followers)
301
-
302
- if followers:
303
- if request.form.get("draft", "0") == "0":
304
- activity_object = create_activity_object(post, blog.name, blog_url, full_content=False)
305
-
306
- for actor in followers:
307
- now_iso = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
308
- activity = {
309
- "@context": "https://www.w3.org/ns/activitystreams",
310
- "id": f"{blog_url}/{post.link}#create",
311
- "type": "Create",
312
- "actor": blog_url,
313
- "published": now_iso,
314
- "to": ["https://www.w3.org/ns/activitystreams#Public"],
315
- "cc": [f"https://{g.main_domain}/followers/{blog.name}"],
316
- "object": activity_object
317
- }
318
- if valkey_client.is_enabled():
319
- valkey_client.queue_federation_activity(activity, blog.id, blog.priv_key, blog_url, f"{actor}/inbox")
320
- else:
321
- send_activity(activity, blog.priv_key, blog_url, f"{actor}/inbox")
248
+ deliver_pending(g.db, jobs, budget_seconds=1)
322
249
 
323
250
  generate_rss(blog.name)
324
251
  return redirect(f"/dashboard/edit/{blog.name}")
@@ -332,6 +259,7 @@ def preview():
332
259
  data = {
333
260
  'title': request.form.get('title'),
334
261
  'content': sanitize_html(request.form.get('content')),
262
+ 'toc_enabled': '1' if request.form.get('toc_enabled') == '1' else '0',
335
263
  'author': request.form.get('author'),
336
264
  'blog_title': request.form.get('blog_title'),
337
265
  'blog_name': request.form.get('blog_name'),
@@ -381,7 +309,9 @@ def edit_post(blog, post):
381
309
  if blog_obj is None or blog_obj.owner != g.user:
382
310
  abort(403)
383
311
 
384
- e_post = g.db.query(Post).filter_by(link=post).first()
312
+ e_post = g.db.query(Post).filter_by(link=post, blog=blog_obj.id).first()
313
+ if e_post is None:
314
+ abort(404)
385
315
  if request.method == "GET":
386
316
  # Get existing tags for the post
387
317
  existing_tags = get_post_tags(g.db, e_post) if e_post else []
@@ -395,25 +325,23 @@ def edit_post(blog, post):
395
325
  if len(title) > 120:
396
326
  existing_tags = get_post_tags(g.db, p) if p else []
397
327
  return render_template("new_post.html", blog=blog_obj, post=p, existing_tags=existing_tags, error="title-too-long", form_data=request.form)
398
- link = gen_link(title)
399
- if link == "rss":
400
- link = "p_rss"
401
- dupes_posts = g.db.query(Post).filter(Post.link.startswith(link), Post.blog == blog_obj.id).count()
402
- dupes_pages = g.db.query(Page).filter(Page.url == link, Page.blog == blog_obj.id).count()
403
- dupes = dupes_posts + dupes_pages
404
- if dupes > 0 and link != post:
405
- link += f"-{dupes + 1}"
328
+ if not title:
329
+ existing_tags = get_post_tags(g.db, p)
330
+ return render_template('new_post.html', blog=blog_obj, post=p, existing_tags=existing_tags,
331
+ error='title-empty', form_data=request.form)
332
+ was_draft = p.isdraft == '1'
406
333
 
407
334
  p.title = title
408
- now = datetime.now(timezone.utc).replace(microsecond=0)
335
+ now = datetime.now(timezone.utc)
409
336
  p.content_raw = request.form.get("content_raw")
410
337
  p.content_html = sanitize_html(request.form.get("content"))
411
338
  p.author = request.form.get("author")
412
339
  p.feed = request.form.get("feed")
413
340
  p.comments_allowed = request.form.get("comments_allowed", "0")
414
- p.link = link
415
- if request.form.get("publish") == "1":
341
+ p.toc_enabled = "1" if request.form.get("toc_enabled") == "1" else "0"
342
+ if was_draft and request.form.get("draft", "0") == "0":
416
343
  p.date = now
344
+ p.updated = None
417
345
  else:
418
346
  p.updated = now
419
347
  p.isdraft = request.form.get("draft", "0")
@@ -423,36 +351,15 @@ def edit_post(blog, post):
423
351
  tag_names = parse_tags_from_input(tags_input)
424
352
  update_post_tags(g.db, p, tag_names)
425
353
 
354
+ jobs = []
355
+ if p.isdraft == '0':
356
+ kind = 'Create' if was_draft else 'Update'
357
+ jobs = queue_activity(g.db, blog_obj, activity_for(blog_obj, kind, create_activity_object(p, blog_obj)))
358
+ elif not was_draft:
359
+ tombstone = {'id': post_url(p, blog_obj), 'type': 'Tombstone', 'formerType': 'Note'}
360
+ jobs = queue_activity(g.db, blog_obj, activity_for(blog_obj, 'Delete', tombstone))
426
361
  g.db.commit()
427
-
428
- if blog_obj.access == "domain":
429
- blog_url = f"https://{blog_obj.name}.{g.main_domain}"
430
- else:
431
- blog_url = f"https://{g.main_domain}/b/{blog_obj.name}"
432
-
433
- followers = []
434
- if blog_obj.followers not in (None, "null", "NULL"):
435
- followers = json.loads(blog_obj.followers)
436
-
437
- if followers:
438
- activity_object = create_activity_object(p, blog_obj.name, blog_url, full_content=False)
439
-
440
- for actor in followers:
441
- now_iso = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
442
- update_activity = {
443
- "@context": "https://www.w3.org/ns/activitystreams",
444
- "id": f"{blog_url}/{p.link}#update",
445
- "type": "Update",
446
- "actor": blog_url,
447
- "published": now_iso,
448
- "to": ["https://www.w3.org/ns/activitystreams#Public"],
449
- "cc": [f"https://{g.main_domain}/followers/{blog_obj.name}"],
450
- "object": activity_object
451
- }
452
- if valkey_client.is_enabled():
453
- valkey_client.queue_federation_activity(update_activity, blog_obj.id, blog_obj.priv_key, blog_url, f"{actor}/inbox")
454
- else:
455
- send_activity(update_activity, blog_obj.priv_key, blog_url, f"{actor}/inbox")
362
+ deliver_pending(g.db, jobs, budget_seconds=1)
456
363
 
457
364
  generate_rss(blog)
458
365
  return redirect(f"/dashboard/edit/{blog}")
@@ -468,42 +375,13 @@ def delete_post(blog, post):
468
375
 
469
376
  p = g.db.query(Post).filter_by(link=post, blog=blog_obj.id).first()
470
377
  if p:
471
- if blog_obj.access == "domain":
472
- blog_url = f"https://{blog_obj.name}.{g.main_domain}"
473
- else:
474
- blog_url = f"https://{g.main_domain}/b/{blog_obj.name}"
475
-
476
- followers = []
477
- if blog_obj.followers not in (None, "null", "NULL"):
478
- followers = json.loads(blog_obj.followers)
479
-
480
- if followers:
481
- activity_object = create_activity_object(p, blog_obj.name, blog_url, full_content=False)
482
-
483
- for actor in followers:
484
- now_iso = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
485
- delete_activity = {
486
- "@context": "https://www.w3.org/ns/activitystreams",
487
- "id": f"{blog_url}/{p.link}#delete",
488
- "type": "Delete",
489
- "actor": blog_url,
490
- "published": now_iso,
491
- "to": ["https://www.w3.org/ns/activitystreams#Public"],
492
- "cc": [f"https://{g.main_domain}/followers/{blog_obj.name}"],
493
- "object": activity_object
494
- }
495
- if valkey_client.is_enabled():
496
- valkey_client.queue_federation_activity(delete_activity, blog_obj.id, blog_obj.priv_key, blog_url, f"{actor}/inbox")
497
- else:
498
- send_activity(
499
- delete_activity,
500
- blog_obj.priv_key,
501
- blog_url,
502
- f"{actor}/inbox"
503
- )
504
-
378
+ jobs = []
379
+ if p.isdraft == '0':
380
+ tombstone = {'id': post_url(p, blog_obj), 'type': 'Tombstone', 'formerType': 'Note'}
381
+ jobs = queue_activity(g.db, blog_obj, activity_for(blog_obj, 'Delete', tombstone))
505
382
  g.db.delete(p)
506
383
  g.db.commit()
384
+ deliver_pending(g.db, jobs, budget_seconds=1)
507
385
 
508
386
  generate_rss(blog)
509
387
  return redirect(f"/dashboard/edit/{blog_obj.name}")