openwrite 0.12.0__tar.gz → 0.13.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 (81) hide show
  1. {openwrite-0.12.0 → openwrite-0.13.0}/MANIFEST.in +3 -0
  2. {openwrite-0.12.0/openwrite.egg-info → openwrite-0.13.0}/PKG-INFO +2 -1
  3. {openwrite-0.12.0 → openwrite-0.13.0}/README.md +1 -0
  4. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/__init__.py +1 -1
  5. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/gemini_multi.py +33 -12
  6. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/gemini_single.py +37 -12
  7. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/routes/blog.py +12 -9
  8. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/routes/dashboard.py +11 -3
  9. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/routes/federation.py +176 -24
  10. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/routes/main.py +4 -2
  11. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/discover.html +1 -1
  12. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/edit.html +4 -4
  13. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/new_post.html +51 -3
  14. openwrite-0.13.0/openwrite/utils/i18n.json +1 -0
  15. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/utils/models.py +1 -0
  16. {openwrite-0.12.0 → openwrite-0.13.0/openwrite.egg-info}/PKG-INFO +2 -1
  17. {openwrite-0.12.0 → openwrite-0.13.0}/pyproject.toml +4 -1
  18. openwrite-0.12.0/openwrite/utils/i18n.json +0 -227
  19. {openwrite-0.12.0 → openwrite-0.13.0}/LICENSE +0 -0
  20. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/cli.py +0 -0
  21. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/db/__init__.py +0 -0
  22. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/db/base.py +0 -0
  23. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/routes/admin.py +0 -0
  24. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/routes/auth.py +0 -0
  25. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/routes/upload.py +0 -0
  26. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/IBMPlexSans-Regular.ttf +0 -0
  27. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/Merriweather.ttf +0 -0
  28. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/avatar.png +0 -0
  29. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/emoji.json +0 -0
  30. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/favicon.ico +0 -0
  31. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/logo.png +0 -0
  32. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/script/chart.js +0 -0
  33. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/script/highlight.min.js +0 -0
  34. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/script/nativeEmoji.js +0 -0
  35. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/script/openwrite.js +0 -0
  36. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/script/papaparse.min.js +0 -0
  37. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/script/showdown.min.js +0 -0
  38. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/script/showdown.min.js.map +0 -0
  39. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/script/site.compat.min.js +0 -0
  40. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/script/site.min.js +0 -0
  41. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/style/hl.css +0 -0
  42. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/style/nativeEmoji.min.css +0 -0
  43. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/style/style.css +0 -0
  44. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/style/themes/cyberpunk.css +0 -0
  45. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/style/themes/light.css +0 -0
  46. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/style/themes/obscura.css +0 -0
  47. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/style/themes/stickynote.css +0 -0
  48. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/style/themes/terminal.css +0 -0
  49. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/style/themes/warmnight.css +0 -0
  50. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/static/style/themes/windows98.css +0 -0
  51. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/admin.html +0 -0
  52. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/blog.html +0 -0
  53. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/blog_footer.html +0 -0
  54. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/blog_header.html +0 -0
  55. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/blog_preview.html +0 -0
  56. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/changepw.html +0 -0
  57. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/create.html +0 -0
  58. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/dashboard.html +0 -0
  59. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/footer.html +0 -0
  60. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/header.html +0 -0
  61. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/import.html +0 -0
  62. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/index.html +0 -0
  63. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/instances.html +0 -0
  64. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/login.html +0 -0
  65. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/new_page.html +0 -0
  66. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/page_preview.html +0 -0
  67. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/post.html +0 -0
  68. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/preview.html +0 -0
  69. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/register.html +0 -0
  70. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/settings.html +0 -0
  71. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/stats.html +0 -0
  72. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/templates/translations.html +0 -0
  73. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/utils/create_db.py +0 -0
  74. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/utils/db.py +0 -0
  75. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite/utils/helpers.py +0 -0
  76. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite.egg-info/SOURCES.txt +0 -0
  77. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite.egg-info/dependency_links.txt +0 -0
  78. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite.egg-info/entry_points.txt +0 -0
  79. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite.egg-info/requires.txt +0 -0
  80. {openwrite-0.12.0 → openwrite-0.13.0}/openwrite.egg-info/top_level.txt +0 -0
  81. {openwrite-0.12.0 → openwrite-0.13.0}/setup.cfg +0 -0
@@ -1,3 +1,6 @@
1
1
  include openwrite/utils/i18n.json
2
2
  recursive-include openwrite/templates *
3
3
  recursive-include openwrite/static *
4
+ exclude logs/*
5
+ exclude gemini/*
6
+ exclude uploads/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openwrite
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  License: GNU AFFERO GENERAL PUBLIC LICENSE
5
5
  Version 3, 19 November 2007
6
6
 
@@ -704,6 +704,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
704
704
  - Single-blog mode
705
705
  - Supports sqlite and mysql databases
706
706
  - Upload images to local storage or bunny cdn
707
+ - Drafts
707
708
  - Simple markdown editor in posting
708
709
  - Discover section
709
710
  - Privacy:
@@ -15,6 +15,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
15
15
  - Single-blog mode
16
16
  - Supports sqlite and mysql databases
17
17
  - Upload images to local storage or bunny cdn
18
+ - Drafts
18
19
  - Simple markdown editor in posting
19
20
  - Discover section
20
21
  - Privacy:
@@ -85,7 +85,7 @@ def create_app(test_config=None):
85
85
  url="",
86
86
  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}}",
87
87
  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}}",
88
- show="off"
88
+ show="0"
89
89
  )
90
90
  SessionLocal.add(new_page)
91
91
 
@@ -1,10 +1,11 @@
1
1
  import os, subprocess
2
2
  from dotenv import load_dotenv
3
3
  from openwrite.utils.db import init_engine, SessionLocal
4
- from openwrite.utils.models import Blog, Post, User, View
4
+ from openwrite.utils.models import Blog, Post, User, View, Page
5
5
  from openwrite.utils.helpers import anonymize
6
6
  from md2gemini import md2gemini
7
7
  from datetime import datetime, timezone
8
+ import re
8
9
  session = None
9
10
 
10
11
  openwrite_logo = """
@@ -25,7 +26,7 @@ def root(req):
25
26
  resp += "In here you can read posts published.\n"
26
27
  resp += "\n## 🗞️ Latest posts:\n"
27
28
  try:
28
- posts = (session.query(Post).filter_by(feed="1").order_by(Post.id.desc()).limit(10).all())
29
+ posts = (session.query(Post).filter(Post.feed == "1", Post.isdraft == "0").order_by(Post.id.desc()).limit(10).all())
29
30
  for p in posts:
30
31
  blog = session.query(Blog).filter_by(id=p.blog).first()
31
32
  resp += f"\n{p.date} - 📓 {blog.title}\n"
@@ -45,15 +46,24 @@ def blog_index(req):
45
46
  if not blog:
46
47
  return "not found"
47
48
  posts = (session.query(Post)
48
- .filter_by(blog=blog.id)
49
- .order_by(Post.id.desc())
50
- .all())
49
+ .filter_by(blog=blog.id, isdraft="0")
50
+ .order_by(Post.id.desc())
51
+ .all())
52
+ pages = (session.query(Page)
53
+ .filter(Page.blog == blog.id, Page.show == "1").all())
54
+ homepage = session.query(Page).filter(Page.blog == blog.id, Page.url == "").first()
51
55
  body = f"# 📓 {blog.title}\n\n"
52
- body += md2gemini(blog.description_raw.replace("#", ""))
53
- body += "\n--------------------------------------------------\n"
54
- body += "### Posts:\n\n"
56
+ posts_data = ""
55
57
  for post in posts:
56
- body += f"=> /b/{blogname}/{post.link} {post.title}\n"
58
+ posts_data += f"=> /b/{blogname}/{post.link} {post.title}\n\n"
59
+ pages_data = ""
60
+ for page in pages:
61
+ pages_data += f"=> /b/{blogname}/{page.url} {page.name}\n\n"
62
+ content = pages_data + "\n"
63
+ content += homepage.content_raw.replace('{posts}', f"\n{posts_data}")
64
+ content = re.sub(r'!\[(.*?)\]\((.*?)\s*=\d+x\d+\)', r'![\1](\2)', content)
65
+ body += md2gemini(content)
66
+
57
67
  return body
58
68
  finally:
59
69
  session.close()
@@ -67,10 +77,19 @@ def blog_index(req):
67
77
  return "not found"
68
78
 
69
79
  post = (session.query(Post)
70
- .filter_by(blog=blog.id, link=slug)
80
+ .filter_by(blog=blog.id, link=slug, isdraft="0")
71
81
  .first())
72
82
  if not post:
73
- return "not found"
83
+ page = (session.query(Page)
84
+ .filter_by(blog=blog.id, url=slug)
85
+ .first())
86
+ if page:
87
+ content = f"=> /b/{blog.name} 📓 {blog.title}\n\n"
88
+ content += page.content_raw
89
+ content = re.sub(r'!\[(.*?)\]\((.*?)\s*=\d+x\d+\)', r'![\1](\2)', content)
90
+ return md2gemini(content)
91
+ else:
92
+ return "not found"
74
93
  user = session.query(User).filter_by(id=blog.owner).first()
75
94
 
76
95
  now = datetime.now(timezone.utc)
@@ -88,7 +107,9 @@ def blog_index(req):
88
107
  if post.author != "0":
89
108
  gemtext += f"by {post.authorname}"
90
109
  gemtext += "\n--------------------------------------------------\n"
91
- gemtext += f"\n\n{md2gemini(post.content_raw)}"
110
+ content = post.content_raw
111
+ content = re.sub(r'!\[(.*?)\]\((.*?)\s*=\d+x\d+\)', r'![\1](\2)', content)
112
+ gemtext += f"\n\n{md2gemini(content)}\n\n"
92
113
  return gemtext
93
114
 
94
115
  finally:
@@ -11,17 +11,28 @@ def blog_index(req):
11
11
  global session
12
12
  try:
13
13
  blog = session.query(Blog).filter_by(name="default").first()
14
- posts = (session.query(Post)
14
+ if not blog:
15
+ return "not found"
16
+ posts = (session.query(Post)
15
17
  .filter_by(blog=blog.id)
16
18
  .order_by(Post.id.desc())
17
19
  .all())
18
- body = f"# 📓 {blog.title}\n\n"
19
- body += md2gemini(blog.description_raw.replace("#", ""))
20
- body += "\n--------------------------------------------------\n"
21
- body += "### Posts:\n\n"
22
- for post in posts:
23
- body += f"=> /p/{post.link} {post.title}\n"
24
- return body
20
+ pages = (session.query(Page)
21
+ .filter(Page.blog == blog.id, Page.show == "1").all())
22
+ homepage = session.query(Page).filter(Page.blog == blog.id, Page.url == "").first()
23
+ body = f"# 📓 {blog.title}\n\n"
24
+ posts_data = ""
25
+ for post in posts:
26
+ posts_data += f"=> /b/{blogname}/{post.link} {post.title}\n\n"
27
+ pages_data = ""
28
+ for page in pages:
29
+ pages_data += f"=> /b/{blogname}/{page.url} {page.name}\n\n"
30
+ content = pages_data + "\n"
31
+ content += homepage.content_raw.replace('{posts}', f"\n{posts_data}")
32
+ content = re.sub(r'!\[(.*?)\]\((.*?)\s*=\d+x\d+\)', r'![\1](\2)', content)
33
+ body += md2gemini(content)
34
+
35
+ return body
25
36
  finally:
26
37
  session.close()
27
38
 
@@ -33,11 +44,23 @@ def blog_post(req):
33
44
  slug = path.split("/")[2]
34
45
  try:
35
46
  blog = session.query(Blog).filter_by(name="default").first()
47
+ if not blog:
48
+ return "not found"
49
+
36
50
  post = (session.query(Post)
37
- .filter_by(blog=blog.id, link=slug)
38
- .first())
51
+ .filter_by(blog=blog.id, link=slug, isdraft="0")
52
+ .first())
39
53
  if not post:
40
- return "not found"
54
+ page = (session.query(Page)
55
+ .filter_by(blog=blog.id, url=slug)
56
+ .first())
57
+ if page:
58
+ content = f"=> /b/{blog.name} 📓 {blog.title}\n\n"
59
+ content += page.content_raw
60
+ content = re.sub(r'!\[(.*?)\]\((.*?)\s*=\d+x\d+\)', r'![\1](\2)', content)
61
+ return md2gemini(content)
62
+ else:
63
+ return "not found"
41
64
  user = session.query(User).filter_by(id=blog.owner).first()
42
65
 
43
66
  now = datetime.now(timezone.utc)
@@ -55,7 +78,9 @@ def blog_post(req):
55
78
  if post.author != "0":
56
79
  gemtext += f"by {post.authorname}"
57
80
  gemtext += "\n--------------------------------------------------\n"
58
- gemtext += f"\n\n{md2gemini(post.content_raw)}"
81
+ content = post.content_raw
82
+ content = re.sub(r'!\[(.*?)\]\((.*?)\s*=\d+x\d+\)', r'![\1](\2)', content)
83
+ gemtext += f"\n\n{md2gemini(content)}\n\n"
59
84
  return gemtext
60
85
 
61
86
  finally:
@@ -24,7 +24,7 @@ def show_blog(blog):
24
24
  blog.url = f"/b/{blog.name}"
25
25
  homepage = g.db.query(Page).filter(Page.blog == blog.id, Page.url == "").first()
26
26
  if "{posts}" in homepage.content_raw:
27
- posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
27
+ posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
28
28
  return render_template("blog.html", blog=blog, page=homepage, posts=posts, pages=pages)
29
29
 
30
30
  return render_template("blog.html", blog=blog, page=homepage, pages=pages)
@@ -40,10 +40,11 @@ def show_subblog(blog):
40
40
  if blog.access == "path":
41
41
  return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog.name}")
42
42
 
43
+ blog.url = f"https://{blog.name}.{os.getenv('DOMAIN')}"
43
44
  pages = g.db.query(Page).filter_by(blog=blog.id).all()
44
45
  homepage = g.db.query(Page).filter(Page.blog == blog.id, Page.url == "").first()
45
46
  if "{posts}" in homepage.content_raw:
46
- posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
47
+ posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
47
48
  return render_template("blog.html", blog=blog, page=homepage, posts=posts, pages=pages)
48
49
 
49
50
  return render_template("blog.html", blog=blog, page=homepage, pages=pages)
@@ -64,14 +65,14 @@ def show_post(blog, post):
64
65
 
65
66
  blog.url = f"/b/{blog.name}"
66
67
  pages = g.db.query(Page).filter_by(blog=blog.id).all()
67
- one_post = g.db.query(Post).filter(Post.blog == blog.id, Post.link == post).first()
68
+ one_post = g.db.query(Post).filter(Post.blog == blog.id, Post.link == post, Post.isdraft == "0").first()
68
69
  if not one_post:
69
70
  page = g.db.query(Page).filter(Page.blog == blog.id, Page.url == post).first()
70
71
  if not page:
71
72
  return redirect("/")
72
73
 
73
74
  if "{posts}" in page.content_raw:
74
- posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
75
+ posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
75
76
 
76
77
  return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages)
77
78
 
@@ -112,8 +113,10 @@ def show_subpost(blog, post):
112
113
  if blog.access == "path":
113
114
  return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog.name}/{post}")
114
115
 
116
+ blog.url = f"https://{blog.name}.{os.getenv('DOMAIN')}"
117
+
115
118
  pages = g.db.query(Page).filter_by(blog=blog.id).all()
116
- one_post = g.db.query(Post).filter(Post.blog == blog.id, Post.link == post).first()
119
+ one_post = g.db.query(Post).filter(Post.blog == blog.id, Post.link == post, Post.isdraft == "0").first()
117
120
 
118
121
  if not one_post:
119
122
  page = g.db.query(Page).filter(Page.blog == blog.id, Page.url == post).first()
@@ -121,7 +124,7 @@ def show_subpost(blog, post):
121
124
  return redirect("/")
122
125
 
123
126
  if "{posts}" in page.content_raw:
124
- posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
127
+ posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
125
128
 
126
129
  return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages)
127
130
 
@@ -155,7 +158,7 @@ def single_showpost(post):
155
158
 
156
159
  blog = g.db.query(Blog).filter_by(id=1).first()
157
160
  blog.url = f"http://{g.main_domain}"
158
- one_post = g.db.query(Post).filter(Post.blog == 1, Post.link == post).first()
161
+ one_post = g.db.query(Post).filter(Post.blog == 1, Post.link == post, Post.isdraft == "0").first()
159
162
 
160
163
  pages = g.db.query(Page).filter_by(blog=1).all()
161
164
 
@@ -165,7 +168,7 @@ def single_showpost(post):
165
168
  return redirect("/")
166
169
 
167
170
  if "{posts}" in page.content_raw:
168
- posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
171
+ posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
169
172
 
170
173
  return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages)
171
174
 
@@ -230,7 +233,7 @@ def like():
230
233
 
231
234
 
232
235
  def _generate_rss(blog):
233
- posts = g.db.query(Post).filter_by(blog=blog.id).all()
236
+ posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").all()
234
237
  fg = FeedGenerator()
235
238
  fg.title(blog.title)
236
239
  fg.link(href=f"https://{os.getenv('DOMAIN')}/b/{blog.name}", rel="alternate")
@@ -99,8 +99,8 @@ def create_blog():
99
99
  name="Home",
100
100
  url="",
101
101
  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}}",
102
- 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\n\n{{posts}}",
103
- show="off"
102
+ 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}}",
103
+ show="0"
104
104
  )
105
105
  g.db.add(new_page)
106
106
  g.db.commit()
@@ -177,6 +177,9 @@ def new_post(name):
177
177
  title = request.form.get('title')
178
178
  if len(title) > 120:
179
179
  return render_template("new_post.html", blog=blog, error="Title too long! Max 120 characters.")
180
+
181
+ if len(title) < 1:
182
+ return render_template("new_post.html", blog=blog, error="Title cannot be empty!")
180
183
 
181
184
  link = gen_link(title)
182
185
  if link == "rss":
@@ -198,7 +201,8 @@ def new_post(name):
198
201
  author=request.form.get('author'),
199
202
  link=link,
200
203
  date=now,
201
- feed=request.form.get('feed')
204
+ feed=request.form.get('feed'),
205
+ isdraft=request.form.get('draft', '0')
202
206
  )
203
207
  g.db.add(post)
204
208
  g.db.commit()
@@ -319,6 +323,7 @@ def edit_post(blog, post):
319
323
  p.feed = request.form.get("feed")
320
324
  p.link = link
321
325
  p.updated = now
326
+ p.isdraft = request.form.get("draft", "0")
322
327
  g.db.commit()
323
328
  return redirect(f"/dashboard/edit/{blog_obj.name}")
324
329
 
@@ -557,6 +562,9 @@ def page_delete(page):
557
562
  if blog.owner != g.user:
558
563
  return abort(403)
559
564
 
565
+ if page.url == "":
566
+ return redirect("/dashboard")
567
+
560
568
  g.db.delete(page)
561
569
  g.db.commit()
562
570
 
@@ -78,7 +78,100 @@ def activity(blog):
78
78
  }
79
79
  }
80
80
 
81
- return Response(json.dumps(actor), content_type="application/activity+json")
81
+ return Response(json.dumps(actor), content_type="application/activity+json", headers={
82
+ 'Cache-Control': 'max-age=3600',
83
+ 'Vary': 'Accept'
84
+ })
85
+
86
+ @federation_bp.route("/activity/<blog>/create/<int:post_id>")
87
+ def activity_create(blog, post_id):
88
+ b = g.db.query(Blog).filter_by(name=blog).first()
89
+ if not b:
90
+ abort(404)
91
+
92
+ post = g.db.query(Post).filter_by(id=post_id, blog=b.id).first()
93
+ if not post:
94
+ abort(404)
95
+
96
+ if b.access == "path":
97
+ url = f"https://{g.main_domain}/b/{blog}"
98
+ else:
99
+ url = f"https://{blog}.{g.main_domain}"
100
+
101
+ dt = datetime.strptime(str(post.date), "%Y-%m-%d %H:%M:%S")
102
+ dt = dt.replace(tzinfo=timezone.utc)
103
+ iso = dt.isoformat(timespec="seconds").replace("+00:00", "Z")
104
+
105
+ create_activity = {
106
+ "@context": "https://www.w3.org/ns/activitystreams",
107
+ "id": f"https://{g.main_domain}/activity/{blog}/create/{post.id}",
108
+ "type": "Create",
109
+ "actor": f"https://{g.main_domain}/activity/{blog}",
110
+ "published": iso,
111
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
112
+ "cc": [f"https://{g.main_domain}/followers/{blog}"],
113
+ "object": {
114
+ "id": f"https://{g.main_domain}/activity/{blog}/posts/{post.id}",
115
+ "type": "Note",
116
+ "summary": None,
117
+ "attributedTo": f"https://{g.main_domain}/activity/{blog}",
118
+ "content": f"<h3>{post.title}</h3><p><a href=\"{url}/{post.link}\">{url}/{post.link}</a></p>",
119
+ "published": iso,
120
+ "url": f"{url}/{post.link}",
121
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
122
+ "cc": [f"https://{g.main_domain}/followers/{blog}"],
123
+ "sensitive": False,
124
+ "atomUri": f"https://{g.main_domain}/activity/{blog}/posts/{post.id}",
125
+ "attachment": [],
126
+ "tag": []
127
+ }
128
+ }
129
+
130
+ return Response(json.dumps(create_activity), content_type="application/activity+json", headers={
131
+ 'Cache-Control': 'max-age=3600',
132
+ 'Vary': 'Accept'
133
+ })
134
+
135
+ @federation_bp.route("/activity/<blog>/posts/<int:post_id>")
136
+ def activity_post(blog, post_id):
137
+ b = g.db.query(Blog).filter_by(name=blog).first()
138
+ if not b:
139
+ abort(404)
140
+
141
+ post = g.db.query(Post).filter_by(id=post_id, blog=b.id).first()
142
+ if not post:
143
+ abort(404)
144
+
145
+ if b.access == "path":
146
+ url = f"https://{g.main_domain}/b/{blog}"
147
+ else:
148
+ url = f"https://{blog}.{g.main_domain}"
149
+
150
+ dt = datetime.strptime(str(post.date), "%Y-%m-%d %H:%M:%S")
151
+ dt = dt.replace(tzinfo=timezone.utc)
152
+ iso = dt.isoformat(timespec="seconds").replace("+00:00", "Z")
153
+
154
+ note = {
155
+ "@context": "https://www.w3.org/ns/activitystreams",
156
+ "id": f"https://{g.main_domain}/activity/{blog}/posts/{post.id}",
157
+ "type": "Note",
158
+ "summary": None,
159
+ "attributedTo": f"https://{g.main_domain}/activity/{blog}",
160
+ "content": f"<h3>{post.title}</h3><p><a href=\"{url}/{post.link}\">{url}/{post.link}</a></p>",
161
+ "published": iso,
162
+ "url": f"{url}/{post.link}",
163
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
164
+ "cc": [f"https://{g.main_domain}/followers/{blog}"],
165
+ "sensitive": False,
166
+ "atomUri": f"https://{g.main_domain}/activity/{blog}/posts/{post.id}",
167
+ "attachment": [],
168
+ "tag": []
169
+ }
170
+
171
+ return Response(json.dumps(note), content_type="application/activity+json", headers={
172
+ 'Cache-Control': 'max-age=3600',
173
+ 'Vary': 'Accept'
174
+ })
82
175
 
83
176
  @federation_bp.route("/inbox/<blog>", methods=["POST"])
84
177
  def inbox(blog):
@@ -90,12 +183,6 @@ def inbox(blog):
90
183
  if not data:
91
184
  return "Bad Request", 400
92
185
 
93
- #print(f"""
94
- # Data received:
95
-
96
- # {data}
97
- #""")
98
-
99
186
  body = request.get_data(as_text=True)
100
187
  sign = verify_http_signature(request.headers, body, blog)
101
188
  if not sign:
@@ -190,24 +277,66 @@ def inbox(blog):
190
277
  @federation_bp.route("/outbox/<blog>")
191
278
  def outbox(blog):
192
279
  page = request.args.get("page")
280
+
193
281
  b = g.db.query(Blog).filter_by(name=blog).first()
194
282
  if not b:
195
283
  abort(404)
196
284
 
197
- p = g.db.query(Post).filter_by(blog=b.id)
285
+ p = g.db.query(Post).filter_by(blog=b.id).order_by(Post.date.desc())
198
286
  total = p.count()
199
287
  posts = p.all()
288
+
200
289
  first_outbox = {
201
290
  "@context": "https://www.w3.org/ns/activitystreams",
202
- "id": f"https://openwrite.io/outbox/{blog}",
291
+ "id": f"https://{g.main_domain}/outbox/{blog}",
203
292
  "type": "OrderedCollection",
204
293
  "totalItems": total,
205
- "first": f"https://openwrite.io/outbox/{blog}?page=1"
294
+ "first": f"https://{g.main_domain}/outbox/{blog}?page=true"
206
295
  }
207
296
 
208
- if page not in ("true", "1"):
209
- return Response(json.dumps(first_outbox), content_type="application/activity+json")
210
-
297
+ if not page:
298
+ if total <= 10:
299
+ orderedPosts = []
300
+ if b.access == "path":
301
+ url = f"https://{g.main_domain}/b/{blog}"
302
+ else:
303
+ url = f"https://{blog}.{g.main_domain}"
304
+
305
+ for post in posts:
306
+ dt = datetime.strptime(str(post.date), "%Y-%m-%d %H:%M:%S")
307
+ dt = dt.replace(tzinfo=timezone.utc)
308
+ iso = dt.isoformat(timespec="seconds").replace("+00:00", "Z")
309
+ orderedPosts.append({
310
+ "id": f"https://{g.main_domain}/activity/{blog}/create/{post.id}",
311
+ "type": "Create",
312
+ "actor": f"https://{g.main_domain}/activity/{blog}",
313
+ "published": iso,
314
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
315
+ "cc": [f"https://{g.main_domain}/followers/{blog}"],
316
+ "object": {
317
+ "id": f"https://{g.main_domain}/activity/{blog}/posts/{post.id}",
318
+ "type": "Note",
319
+ "attributedTo": f"https://{g.main_domain}/activity/{blog}",
320
+ "content": f"<p><strong>{post.title}</strong></p><p><a href=\"{url}/{post.link}\">{url}/{post.link}</a></p>",
321
+ "published": iso,
322
+ "url": f"{url}/{post.link}",
323
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
324
+ "cc": [f"https://{g.main_domain}/followers/{blog}"],
325
+ "sensitive": False
326
+ }
327
+ })
328
+
329
+ first_outbox["orderedItems"] = orderedPosts
330
+
331
+ return Response(json.dumps(first_outbox), content_type="application/activity+json; charset=utf-8", headers={
332
+ 'Cache-Control': 'max-age=300',
333
+ 'Vary': 'Accept'
334
+ })
335
+ else:
336
+ return Response(json.dumps(first_outbox), content_type="application/activity+json; charset=utf-8", headers={
337
+ 'Cache-Control': 'max-age=300',
338
+ 'Vary': 'Accept'
339
+ })
211
340
 
212
341
  orderedPosts = []
213
342
  if b.access == "path":
@@ -219,29 +348,52 @@ def outbox(blog):
219
348
  dt = dt.replace(tzinfo=timezone.utc)
220
349
  iso = dt.isoformat(timespec="seconds").replace("+00:00", "Z")
221
350
  orderedPosts.append({
222
- "id": f"https://{g.main_domain}/activity/{blog}#{post.link}",
351
+ "id": f"https://{g.main_domain}/activity/{blog}/create/{post.id}",
223
352
  "type": "Create",
224
353
  "actor": f"https://{g.main_domain}/activity/{blog}",
354
+ "published": iso,
355
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
356
+ "cc": [f"https://{g.main_domain}/followers/{blog}"],
225
357
  "object": {
226
- "id": f"{url}/{post.link}",
358
+ "id": f"https://{g.main_domain}/activity/{blog}/posts/{post.id}",
227
359
  "type": "Note",
360
+ "summary": None,
228
361
  "attributedTo": f"https://{g.main_domain}/activity/{blog}",
229
- "content": f"<p>{post.title}</p><a href=\"{url}/{post.link}\">{url}/{post.link}</a>",
230
- "published": iso
231
- },
232
- "published": iso,
233
- "to": ["https://www.w3.org/ns/activitystreams#Public"]
362
+ "content": f"<h3>{post.title}</h3><p><a href=\"{url}/{post.link}\">{url}/{post.link}</a></p>",
363
+ "published": iso,
364
+ "url": f"{url}/{post.link}",
365
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
366
+ "cc": [f"https://{g.main_domain}/followers/{blog}"],
367
+ "sensitive": False,
368
+ "atomUri": f"https://{g.main_domain}/activity/{blog}/posts/{post.id}",
369
+ "attachment": [],
370
+ "tag": []
371
+ }
234
372
  })
235
373
 
236
374
  outbox = {
237
- "@context": "https://www.w3.org/ns/activitystreams",
238
- "id": f"https://openwrite.io/outbox/{blog}?page={page}",
375
+ "@context": [
376
+ "https://www.w3.org/ns/activitystreams",
377
+ {
378
+ "ostatus": "http://ostatus.org#",
379
+ "atomUri": "ostatus:atomUri",
380
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
381
+ "conversation": "ostatus:conversation",
382
+ "sensitive": "as:sensitive",
383
+ "toot": "http://joinmastodon.org/ns#",
384
+ "votersCount": "toot:votersCount"
385
+ }
386
+ ],
387
+ "id": f"https://{g.main_domain}/outbox/{blog}?page={page}",
239
388
  "type": "OrderedCollectionPage",
240
389
  "partOf": f"https://{g.main_domain}/outbox/{blog}",
241
390
  "orderedItems": orderedPosts
242
391
  }
243
392
 
244
- return Response(json.dumps(outbox), content_type="application/activity+json")
393
+ return Response(json.dumps(outbox), content_type="application/activity+json", headers={
394
+ 'Cache-Control': 'max-age=300',
395
+ 'Vary': 'Accept'
396
+ })
245
397
 
246
398
  @federation_bp.route("/followers/<blog>")
247
399
  def followers(blog):
@@ -264,7 +416,7 @@ def followers(blog):
264
416
  "@context": "https://www.w3.org/ns/activitystreams",
265
417
  "id": f"https://{g.main_domain}/followers/{blog}?page=true",
266
418
  "type": "OrderedCollectionPage",
267
- "partOf": f"https://{g.main_domain}.followers/{blog}",
419
+ "partOf": f"https://{g.main_domain}/followers/{blog}",
268
420
  "totalItems": len(followers),
269
421
  "orderedItems": followers
270
422
  }
@@ -3,7 +3,7 @@ import os
3
3
  import json
4
4
  import datetime
5
5
  import requests
6
- from openwrite.utils.models import Post, Blog, Home, Settings, Page
6
+ from openwrite.utils.models import Post, Blog, Home, Settings, Page, Like
7
7
  import time
8
8
  from openwrite import start_time
9
9
 
@@ -71,7 +71,7 @@ def instances():
71
71
  def discover():
72
72
  if g.mode == "single":
73
73
  return redirect("/")
74
- posts = g.db.query(Post).filter_by(feed=1).order_by(desc(Post.id)).all()
74
+ posts = g.db.query(Post).filter(Post.feed == "1", Post.isdraft == "0").order_by(desc(Post.id)).all()
75
75
  for p in posts:
76
76
  b = g.db.query(Blog).filter_by(id=p.blog).first()
77
77
  if not b:
@@ -82,6 +82,8 @@ def discover():
82
82
  url = f"https://{b.name}.{os.getenv('DOMAIN')}/{p.link}"
83
83
  else:
84
84
  url = "#"
85
+ l = g.db.query(Like).filter(Like.post == p.id, Like.blog == b.id).count()
86
+ p.likes = l
85
87
  p.url = url
86
88
  p.blogname = b.title
87
89
 
@@ -4,7 +4,7 @@
4
4
  <div style="display: flex; flex-direction: column; gap: 1rem">
5
5
  {% for p in posts %}
6
6
  <div class="discover_post">
7
- <i class="datetime">{{ p.date }}</i>{{ p.blogname }}: <a class="action" href="{{ p.url }}" target="_blank">{{ p.title }}</a>
7
+ <i class="datetime">{{ p.date }}</i>{{ p.blogname }}: <a class="action" href="{{ p.url }}" target="_blank">{{ p.title }}</a> {{ p.likes }} ♡
8
8
  </div>
9
9
  {% endfor %}
10
10
  </div>