openwrite 0.13.1__tar.gz → 0.13.2__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.13.1/openwrite.egg-info → openwrite-0.13.2}/PKG-INFO +2 -1
  2. {openwrite-0.13.1 → openwrite-0.13.2}/README.md +1 -0
  3. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/__init__.py +1 -0
  4. openwrite-0.13.2/openwrite/routes/blog.py +466 -0
  5. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/routes/dashboard.py +183 -29
  6. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/routes/federation.py +90 -197
  7. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/routes/main.py +13 -2
  8. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/edit.html +46 -5
  9. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/footer.html +2 -2
  10. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/new_page.html +36 -8
  11. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/new_post.html +1 -0
  12. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/utils/models.py +1 -0
  13. {openwrite-0.13.1 → openwrite-0.13.2/openwrite.egg-info}/PKG-INFO +2 -1
  14. {openwrite-0.13.1 → openwrite-0.13.2}/pyproject.toml +1 -1
  15. openwrite-0.13.1/openwrite/routes/blog.py +0 -252
  16. {openwrite-0.13.1 → openwrite-0.13.2}/LICENSE +0 -0
  17. {openwrite-0.13.1 → openwrite-0.13.2}/MANIFEST.in +0 -0
  18. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/cli.py +0 -0
  19. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/db/__init__.py +0 -0
  20. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/db/base.py +0 -0
  21. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/gemini_multi.py +0 -0
  22. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/gemini_single.py +0 -0
  23. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/routes/admin.py +0 -0
  24. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/routes/auth.py +0 -0
  25. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/routes/upload.py +0 -0
  26. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/IBMPlexSans-Regular.ttf +0 -0
  27. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/Merriweather.ttf +0 -0
  28. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/avatar.png +0 -0
  29. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/emoji.json +0 -0
  30. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/favicon.ico +0 -0
  31. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/logo.png +0 -0
  32. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/script/chart.js +0 -0
  33. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/script/highlight.min.js +0 -0
  34. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/script/nativeEmoji.js +0 -0
  35. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/script/openwrite.js +0 -0
  36. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/script/papaparse.min.js +0 -0
  37. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/script/showdown.min.js +0 -0
  38. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/script/showdown.min.js.map +0 -0
  39. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/script/site.compat.min.js +0 -0
  40. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/script/site.min.js +0 -0
  41. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/style/hl.css +0 -0
  42. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/style/nativeEmoji.min.css +0 -0
  43. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/style/style.css +0 -0
  44. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/style/themes/cyberpunk.css +0 -0
  45. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/style/themes/light.css +0 -0
  46. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/style/themes/obscura.css +0 -0
  47. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/style/themes/stickynote.css +0 -0
  48. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/style/themes/terminal.css +0 -0
  49. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/style/themes/warmnight.css +0 -0
  50. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/static/style/themes/windows98.css +0 -0
  51. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/admin.html +0 -0
  52. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/blog.html +0 -0
  53. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/blog_footer.html +0 -0
  54. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/blog_header.html +0 -0
  55. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/blog_preview.html +0 -0
  56. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/changepw.html +0 -0
  57. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/create.html +0 -0
  58. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/dashboard.html +0 -0
  59. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/discover.html +0 -0
  60. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/header.html +0 -0
  61. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/import.html +0 -0
  62. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/index.html +0 -0
  63. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/instances.html +0 -0
  64. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/login.html +0 -0
  65. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/page_preview.html +0 -0
  66. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/post.html +0 -0
  67. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/preview.html +0 -0
  68. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/register.html +0 -0
  69. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/settings.html +0 -0
  70. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/stats.html +0 -0
  71. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/templates/translations.html +0 -0
  72. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/utils/create_db.py +0 -0
  73. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/utils/db.py +0 -0
  74. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/utils/helpers.py +0 -0
  75. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite/utils/i18n.json +0 -0
  76. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite.egg-info/SOURCES.txt +0 -0
  77. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite.egg-info/dependency_links.txt +0 -0
  78. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite.egg-info/entry_points.txt +0 -0
  79. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite.egg-info/requires.txt +0 -0
  80. {openwrite-0.13.1 → openwrite-0.13.2}/openwrite.egg-info/top_level.txt +0 -0
  81. {openwrite-0.13.1 → openwrite-0.13.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openwrite
3
- Version: 0.13.1
3
+ Version: 0.13.2
4
4
  License: GNU AFFERO GENERAL PUBLIC LICENSE
5
5
  Version 3, 19 November 2007
6
6
 
@@ -688,6 +688,7 @@ Requires-Dist: user-agents
688
688
  Dynamic: license-file
689
689
 
690
690
  ![logo](https://openwrite.io/static/logo.png)
691
+
691
692
  openwrite is a minimalist blogging platform built for writing freely, hosting independently, and publishing without noise.
692
693
 
693
694
  ![PyPI - Downloads](https://img.shields.io/pypi/dw/openwrite)
@@ -1,4 +1,5 @@
1
1
  ![logo](https://openwrite.io/static/logo.png)
2
+
2
3
  openwrite is a minimalist blogging platform built for writing freely, hosting independently, and publishing without noise.
3
4
 
4
5
  ![PyPI - Downloads](https://img.shields.io/pypi/dw/openwrite)
@@ -212,6 +212,7 @@ def create_app(test_config=None):
212
212
 
213
213
  trans = g.db.query(Home).filter(Home.type == "translation", Home.language == lang).all()
214
214
 
215
+ g.version = "0.14.0"
215
216
  g.mode = os.getenv("MODE")
216
217
  f_abs_path = os.path.abspath(__file__)
217
218
  g.mainpath = "/".join(f_abs_path.split("/")[:-1])
@@ -0,0 +1,466 @@
1
+ from flask import Blueprint, render_template, redirect, request, g, Response, abort
2
+ from openwrite.utils.models import Blog, Post, User, View, Like, Page
3
+ from openwrite.utils.helpers import gen_link, sanitize_html, anonymize, get_ip
4
+ from feedgen.feed import FeedGenerator
5
+ from sqlalchemy import desc
6
+ import os
7
+ import json
8
+ import re
9
+ from bs4 import BeautifulSoup
10
+ from datetime import timezone, datetime
11
+
12
+ blog_bp = Blueprint("blog", __name__)
13
+
14
+ def create_activity_object(post, blog, url, full_content=True):
15
+ """Create ActivityPub object (Article or Note) based on post content length"""
16
+ dt = datetime.strptime(str(post.date), "%Y-%m-%d %H:%M:%S")
17
+ dt = dt.replace(tzinfo=timezone.utc)
18
+ iso = dt.isoformat(timespec="seconds").replace("+00:00", "Z")
19
+
20
+ clean_content = re.sub(r'<[^>]+>', '', post.content_html)
21
+ content_length = len(clean_content)
22
+
23
+ base_object = {
24
+ "id": f"{url}/{post.link}",
25
+ "attributedTo": url,
26
+ "published": iso,
27
+ "url": f"{url}/{post.link}",
28
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
29
+ "cc": [f"https://{g.main_domain}/followers/{blog}"],
30
+ "sensitive": False,
31
+ "atomUri": f"{url}/{post.link}",
32
+ "attachment": [],
33
+ "tag": []
34
+ }
35
+
36
+ if content_length > 200:
37
+ words = clean_content.split()[:50]
38
+ summary = ' '.join(words) + '...' if len(words) == 50 else ' '.join(words)
39
+
40
+ if full_content:
41
+ content = f"<h1>{post.title}</h1>{post.content_html}"
42
+ else:
43
+ content = f"<p><a href=\"{url}/{post.link}\">Read full article: {post.title}</a></p>"
44
+
45
+ base_object.update({
46
+ "type": "Article",
47
+ "name": post.title,
48
+ "summary": summary,
49
+ "content": content
50
+ })
51
+ else:
52
+ base_object.update({
53
+ "type": "Note",
54
+ "summary": None,
55
+ "content": f"<h3>{post.title}</h3>{post.content_html}<p><a href=\"{url}/{post.link}\">View post</a></p>"
56
+ })
57
+
58
+ return base_object
59
+
60
+ def create_person_object(blog, url):
61
+ """Create ActivityPub Person object for the blog"""
62
+ published = blog.created
63
+ dt = datetime.strptime(str(published), "%Y-%m-%d %H:%M:%S")
64
+ dt = dt.replace(tzinfo=timezone.utc)
65
+ iso = dt.isoformat(timespec="seconds").replace("+00:00", "Z")
66
+ if blog.fedi_description != "":
67
+ summary = blog.fedi_description
68
+ else:
69
+ summary = f"{blog.name} - Blog on {g.main_domain}"
70
+
71
+ actor = {
72
+ "@context": [
73
+ "https://www.w3.org/ns/activitystreams",
74
+ "https://w3id.org/security/v1",
75
+ {
76
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
77
+ "discoverable": "as:discoverable",
78
+ "indexable": "as:indexable"
79
+ }
80
+ ],
81
+ "id": url,
82
+ "type": "Person",
83
+ "preferredUsername": blog.name,
84
+ "name": blog.title,
85
+ "summary": summary,
86
+ "inbox": f"https://{g.main_domain}/inbox/{blog.name}",
87
+ "outbox": f"https://{g.main_domain}/outbox/{blog.name}",
88
+ "followers": f"https://{g.main_domain}/followers/{blog.name}",
89
+ "published": iso,
90
+ "manuallyApprovesFollowers": False,
91
+ "discoverable": True,
92
+ "indexable": True,
93
+ "url": url,
94
+ "attachment": [
95
+ {
96
+ "type": "PropertyValue",
97
+ "name": "Blog",
98
+ "value": f"<a href=\"{url}\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\" translate=\"no\"><span class=\"\">{url}</span><span class=\"invisible\"></span></a>"
99
+ }
100
+ ],
101
+ "publicKey": {
102
+ "id": f"{url}#main-key",
103
+ "owner": url,
104
+ "publicKeyPem": blog.pub_key
105
+ },
106
+ "icon": {
107
+ "type": "Image",
108
+ "mediaType": "image/png",
109
+ "url": f"https://{g.main_domain}/static/avatar.png"
110
+ }
111
+ }
112
+ return actor
113
+
114
+ def is_activitypub_request():
115
+ """Check if the request is asking for ActivityPub content"""
116
+ accept = request.headers.get('Accept', '')
117
+ return 'application/activity+json' in accept or 'application/ld+json' in accept or 'application/json' in accept
118
+
119
+ @blog_bp.route("/b/<blog>")
120
+ def show_blog(blog):
121
+ if g.mode == "single":
122
+ return redirect("/")
123
+ blog_obj = g.db.query(Blog).filter_by(name=blog).first()
124
+ if blog_obj is None:
125
+ return redirect("/")
126
+
127
+ if blog_obj.access == "domain":
128
+ return redirect(f"https://{blog_obj.name}.{os.getenv('DOMAIN')}/")
129
+
130
+ if is_activitypub_request():
131
+ url = f"https://{g.main_domain}/b/{blog}"
132
+ person = create_person_object(blog_obj, url)
133
+ return Response(json.dumps(person), content_type="application/activity+json", headers={
134
+ 'Cache-Control': 'max-age=3600',
135
+ 'Vary': 'Accept'
136
+ })
137
+
138
+ pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
139
+ blog_obj.url = f"/b/{blog_obj.name}"
140
+ homepage = g.db.query(Page).filter(Page.blog == blog_obj.id, Page.url == "").first()
141
+ if "{posts}" in homepage.content_raw:
142
+ posts = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
143
+ return render_template("blog.html", blog=blog_obj, page=homepage, posts=posts, pages=pages)
144
+
145
+ return render_template("blog.html", blog=blog_obj, page=homepage, pages=pages)
146
+
147
+ @blog_bp.route("/", subdomain="<blog>")
148
+ def show_subblog(blog):
149
+ if g.mode == "single":
150
+ return redirect("/")
151
+ blog_obj = g.db.query(Blog).filter_by(name=blog).first()
152
+ if blog_obj is None:
153
+ return redirect(f"https://{os.getenv('DOMAIN')}/")
154
+
155
+ if blog_obj.access == "path":
156
+ return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog_obj.name}")
157
+
158
+ if is_activitypub_request():
159
+ url = f"https://{blog}.{g.main_domain}"
160
+ person = create_person_object(blog_obj, url)
161
+ return Response(json.dumps(person), content_type="application/activity+json", headers={
162
+ 'Cache-Control': 'max-age=3600',
163
+ 'Vary': 'Accept'
164
+ })
165
+
166
+ blog_obj.url = f"https://{blog_obj.name}.{os.getenv('DOMAIN')}"
167
+ pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
168
+ homepage = g.db.query(Page).filter(Page.blog == blog_obj.id, Page.url == "").first()
169
+ if "{posts}" in homepage.content_raw:
170
+ posts = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
171
+ return render_template("blog.html", blog=blog_obj, page=homepage, posts=posts, pages=pages)
172
+
173
+ return render_template("blog.html", blog=blog_obj, page=homepage, pages=pages)
174
+
175
+ @blog_bp.route("/b/<blog>/<post>")
176
+ def show_post(blog, post):
177
+ if g.mode == "single":
178
+ return redirect("/")
179
+ blog_obj = g.db.query(Blog).filter_by(name=blog).first()
180
+ if blog_obj is None:
181
+ return redirect("/")
182
+
183
+ if post == "rss":
184
+ return _generate_rss(blog_obj)
185
+
186
+ if blog_obj.access == "domain":
187
+ return redirect(f"https://{blog_obj.name}.{os.getenv('DOMAIN')}/{post}")
188
+
189
+ blog_obj.url = f"/b/{blog_obj.name}"
190
+ pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
191
+ one_post = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.link == post, Post.isdraft == "0").first()
192
+
193
+ if not one_post:
194
+ page = g.db.query(Page).filter(Page.blog == blog_obj.id, Page.url == post).first()
195
+ if not page:
196
+ return redirect("/")
197
+
198
+ if "{posts}" in page.content_raw:
199
+ posts = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
200
+ return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages)
201
+
202
+ return render_template("blog.html", blog=blog_obj, page=page, pages=pages)
203
+
204
+ if is_activitypub_request():
205
+ url = f"https://{g.main_domain}/b/{blog}"
206
+ activity_obj = create_activity_object(one_post, blog, url)
207
+ activity_obj["@context"] = "https://www.w3.org/ns/activitystreams"
208
+ return Response(json.dumps(activity_obj), content_type="application/activity+json", headers={
209
+ 'Cache-Control': 'max-age=3600',
210
+ 'Vary': 'Accept'
211
+ })
212
+
213
+ post_author = g.db.query(User).filter_by(id=blog_obj.owner).first()
214
+ one_post.authorname = post_author.username
215
+ blog_obj.url = f"/b/{blog_obj.name}"
216
+
217
+ user_agent = request.headers.get('User-Agent')
218
+
219
+ if 'Bot' not in user_agent:
220
+ ip = anonymize(get_ip())
221
+ v = g.db.query(View).filter(View.blog == blog_obj.id, View.post == one_post.id, View.hash == ip).count()
222
+ 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
231
+
232
+ user = g.db.query(User).filter_by(id=g.user) if g.user else None
233
+ return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages)
234
+
235
+ @blog_bp.route("/<post>", subdomain="<blog>")
236
+ def show_subpost(blog, post):
237
+ if g.mode == "single":
238
+ return redirect("/")
239
+ blog_obj = g.db.query(Blog).filter_by(name=blog).first()
240
+ if blog_obj is None:
241
+ return redirect(f"https://{os.getenv('DOMAIN')}/")
242
+
243
+ if post == "rss":
244
+ return _generate_rss(blog_obj)
245
+
246
+ if blog_obj.access == "path":
247
+ return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog_obj.name}/{post}")
248
+
249
+ blog_obj.url = f"https://{blog_obj.name}.{os.getenv('DOMAIN')}"
250
+
251
+ pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
252
+ one_post = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.link == post, Post.isdraft == "0").first()
253
+
254
+ if not one_post:
255
+ page = g.db.query(Page).filter(Page.blog == blog_obj.id, Page.url == post).first()
256
+ if not page:
257
+ return redirect("/")
258
+
259
+ if "{posts}" in page.content_raw:
260
+ posts = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
261
+ return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages)
262
+
263
+ return render_template("blog.html", blog=blog_obj, page=page, pages=pages)
264
+
265
+ if is_activitypub_request():
266
+ url = f"https://{blog}.{g.main_domain}"
267
+ activity_obj = create_activity_object(one_post, blog, url)
268
+ activity_obj["@context"] = "https://www.w3.org/ns/activitystreams"
269
+ return Response(json.dumps(activity_obj), content_type="application/activity+json", headers={
270
+ 'Cache-Control': 'max-age=3600',
271
+ 'Vary': 'Accept'
272
+ })
273
+
274
+ post_author = g.db.query(User).filter_by(id=blog_obj.owner).first()
275
+ one_post.authorname = post_author.username
276
+ blog_obj.url = f"https://{blog_obj.name}.{os.getenv('DOMAIN')}"
277
+
278
+ user_agent = request.headers.get('User-Agent', '')
279
+ if 'Bot' not in user_agent:
280
+ ip = anonymize(get_ip())
281
+ 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
+ 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()
288
+
289
+ likes = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id).count()
290
+ 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
+ one_post.liked = liked
293
+
294
+ 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)
296
+
297
+ @blog_bp.route("/.well-known/webfinger")
298
+ def webfinger():
299
+ resource = request.args.get("resource")
300
+ if not resource or not resource.startswith("acct:"):
301
+ abort(400)
302
+
303
+ account_part = resource.split(":")[1]
304
+ if "@" not in account_part:
305
+ abort(400)
306
+
307
+ username, domain = account_part.split("@", 1)
308
+
309
+ blog_obj = g.db.query(Blog).filter_by(name=username).first()
310
+ if not blog_obj:
311
+ abort(404)
312
+
313
+ if blog_obj.access == "domain":
314
+ blog_url = f"https://{username}.{g.main_domain}"
315
+ else:
316
+ blog_url = f"https://{g.main_domain}/b/{username}"
317
+
318
+ data = {
319
+ "subject": f"acct:{username}@{g.main_domain}",
320
+ "links": [{
321
+ "rel": "self",
322
+ "type": "application/activity+json",
323
+ "href": blog_url
324
+ }]
325
+ }
326
+
327
+ return Response(
328
+ response=json.dumps(data),
329
+ status=200,
330
+ content_type="application/jrd+json"
331
+ )
332
+
333
+ @blog_bp.route("/.well-known/webfinger", subdomain="<blog>")
334
+ def subdomain_webfinger(blog):
335
+ """Redirect subdomain webfinger requests to main domain"""
336
+ resource = request.args.get("resource")
337
+ if not resource:
338
+ abort(400)
339
+
340
+ if resource.startswith("acct:"):
341
+ account_part = resource.split(":")[1]
342
+ if "@" in account_part:
343
+ username = account_part.split("@")[0]
344
+ corrected_resource = f"acct:{username}@{g.main_domain}"
345
+ return redirect(f"https://{g.main_domain}/.well-known/webfinger?resource={corrected_resource}")
346
+
347
+ return redirect(f"https://{g.main_domain}/.well-known/webfinger?resource={resource}")
348
+
349
+ @blog_bp.route("/p/<post>")
350
+ def single_showpost(post):
351
+ if g.mode == "multi":
352
+ return redirect("/")
353
+
354
+ blog = g.db.query(Blog).filter_by(id=1).first()
355
+ blog.url = f"http://{g.main_domain}"
356
+ one_post = g.db.query(Post).filter(Post.blog == 1, Post.link == post, Post.isdraft == "0").first()
357
+
358
+ pages = g.db.query(Page).filter_by(blog=1).all()
359
+
360
+ if not one_post:
361
+ page = g.db.query(Page).filter(Page.blog == blog.id, Page.url == post).first()
362
+ if not page:
363
+ return redirect("/")
364
+
365
+ if "{posts}" in page.content_raw:
366
+ posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
367
+
368
+ return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages)
369
+
370
+ return render_template("blog.html", blog=blog, page=page, pages=pages)
371
+
372
+ if is_activitypub_request():
373
+ url = f"https://{g.main_domain}"
374
+ activity_obj = create_activity_object(one_post, "default", url)
375
+ activity_obj["@context"] = "https://www.w3.org/ns/activitystreams"
376
+ return Response(json.dumps(activity_obj), content_type="application/activity+json", headers={
377
+ 'Cache-Control': 'max-age=3600',
378
+ 'Vary': 'Accept'
379
+ })
380
+
381
+ post_author = g.db.query(User).filter_by(id=1).first()
382
+ one_post.authorname = post_author.username
383
+
384
+ user_agent = request.headers.get('User-Agent', '')
385
+ 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')
389
+ 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()
394
+
395
+ likes = g.db.query(Like).filter(Like.blog == 1, Like.post == one_post.id).count()
396
+ 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
+ one_post.liked = liked
399
+
400
+ 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)
402
+
403
+ @blog_bp.route("/rss")
404
+ def single_rss():
405
+ if g.mode == "multi":
406
+ return redirect("/")
407
+
408
+ blog = g.db.query(Blog).first()
409
+ return _generate_rss(blog)
410
+
411
+ @blog_bp.route("/like", methods=["POST"])
412
+ def like():
413
+ data = request.get_json()
414
+ if not data:
415
+ abort(400)
416
+
417
+ blog_id = data.get("blog")
418
+ post_id = data.get("post")
419
+ post = g.db.query(Post).filter(Post.blog == blog_id, Post.id == post_id).count()
420
+ if post < 1:
421
+ resp = {"status": "no_post"}
422
+ return resp, 404
423
+ ip = anonymize(get_ip())
424
+
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()
429
+ resp = {"status": "deleted"}
430
+ status = 204
431
+ 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()
436
+ resp = {"status": "ok"}
437
+ status = 201
438
+ return resp, status
439
+
440
+
441
+ def _generate_rss(blog):
442
+ posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").all()
443
+ fg = FeedGenerator()
444
+ fg.title(blog.title)
445
+
446
+ if blog.access == "domain":
447
+ blog_url = f"https://{blog.name}.{g.main_domain}"
448
+ post_base_url = blog_url
449
+ else:
450
+ blog_url = f"https://{g.main_domain}/b/{blog.name}"
451
+ post_base_url = blog_url
452
+
453
+ fg.link(href=blog_url, rel="alternate")
454
+ fg.description(blog.description_html)
455
+
456
+ for p in posts:
457
+ soup = BeautifulSoup(p.content_html, "html.parser")
458
+ fe = fg.add_entry()
459
+ fe.title(p.title)
460
+ fe.link(href=f"{post_base_url}/{p.link}")
461
+ fe.description(soup.get_text())
462
+ fe.published(p.date.replace(tzinfo=timezone.utc))
463
+
464
+ return Response(fg.rss_str(pretty=True).decode("utf-8"), mimetype="application/rss+xml")
465
+
466
+