openwrite 0.10.1__py3-none-any.whl

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 (67) hide show
  1. openwrite/__init__.py +134 -0
  2. openwrite/cli.py +340 -0
  3. openwrite/db/__init__.py +0 -0
  4. openwrite/db/base.py +3 -0
  5. openwrite/launch_gemini.py +8 -0
  6. openwrite/routes/admin.py +170 -0
  7. openwrite/routes/auth.py +77 -0
  8. openwrite/routes/blog.py +205 -0
  9. openwrite/routes/dashboard.py +408 -0
  10. openwrite/routes/federation.py +284 -0
  11. openwrite/routes/main.py +96 -0
  12. openwrite/routes/upload.py +81 -0
  13. openwrite/static/IBMPlexSans-Regular.ttf +0 -0
  14. openwrite/static/Merriweather.ttf +0 -0
  15. openwrite/static/avatar.png +0 -0
  16. openwrite/static/favicon.ico +0 -0
  17. openwrite/static/logo.png +0 -0
  18. openwrite/static/script/chart.js +14 -0
  19. openwrite/static/script/highlight.min.js +1244 -0
  20. openwrite/static/script/openwrite.js +88 -0
  21. openwrite/static/script/papaparse.min.js +7 -0
  22. openwrite/static/script/showdown.min.js +3 -0
  23. openwrite/static/script/showdown.min.js.map +1 -0
  24. openwrite/static/script/site.compat.min.js +7 -0
  25. openwrite/static/script/site.min.js +7 -0
  26. openwrite/static/style/hl.css +1 -0
  27. openwrite/static/style/style.css +611 -0
  28. openwrite/static/style/themes/light.css +28 -0
  29. openwrite/static/style/themes/obscura.css +27 -0
  30. openwrite/static/style/themes/stickynote.css +50 -0
  31. openwrite/static/style/themes/terminal.css +37 -0
  32. openwrite/static/style/themes/warmnight.css +73 -0
  33. openwrite/static/style/themes/windows98.css +132 -0
  34. openwrite/templates/admin.html +47 -0
  35. openwrite/templates/blog.html +16 -0
  36. openwrite/templates/blog_footer.html +6 -0
  37. openwrite/templates/blog_header.html +45 -0
  38. openwrite/templates/blog_preview.html +44 -0
  39. openwrite/templates/changepw.html +19 -0
  40. openwrite/templates/create.html +46 -0
  41. openwrite/templates/dashboard.html +45 -0
  42. openwrite/templates/discover.html +11 -0
  43. openwrite/templates/edit.html +190 -0
  44. openwrite/templates/footer.html +16 -0
  45. openwrite/templates/header.html +61 -0
  46. openwrite/templates/import.html +142 -0
  47. openwrite/templates/index.html +9 -0
  48. openwrite/templates/instances.html +14 -0
  49. openwrite/templates/login.html +18 -0
  50. openwrite/templates/new_post.html +207 -0
  51. openwrite/templates/post.html +53 -0
  52. openwrite/templates/preview.html +37 -0
  53. openwrite/templates/register.html +42 -0
  54. openwrite/templates/settings.html +63 -0
  55. openwrite/templates/stats.html +242 -0
  56. openwrite/utils/create_db.py +15 -0
  57. openwrite/utils/db.py +30 -0
  58. openwrite/utils/helpers.py +216 -0
  59. openwrite/utils/i18n.json +229 -0
  60. openwrite/utils/models.py +87 -0
  61. openwrite/utils/translations.py +6 -0
  62. openwrite-0.10.1.dist-info/METADATA +684 -0
  63. openwrite-0.10.1.dist-info/RECORD +67 -0
  64. openwrite-0.10.1.dist-info/WHEEL +5 -0
  65. openwrite-0.10.1.dist-info/entry_points.txt +2 -0
  66. openwrite-0.10.1.dist-info/licenses/LICENSE +661 -0
  67. openwrite-0.10.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,77 @@
1
+ from flask import Blueprint, render_template, redirect, request, session, g
2
+ from openwrite.utils.models import User
3
+ import bcrypt
4
+ import re
5
+ import requests
6
+ import json
7
+
8
+ auth_bp = Blueprint("auth", __name__)
9
+
10
+ @auth_bp.route("/register", methods=['GET', 'POST'])
11
+ def register():
12
+ if g.user is not None:
13
+ return redirect("/dashboard")
14
+
15
+ if not g.register_enabled:
16
+ return redirect("/")
17
+
18
+ if request.method == "GET":
19
+ return render_template('register.html')
20
+
21
+ form_username = request.form.get('username')
22
+ if not re.match(r"^[a-zA-Z0-9](?:[a-zA-Z0-9_-]{1,28}[a-zA-Z0-9])?$", form_username):
23
+ return render_template('register.html', error=g.trans['wrong_username'])
24
+ form_password = request.form.get('password')
25
+ form_password2 = request.form.get('password2')
26
+
27
+ if form_password != form_password2:
28
+ return render_template("register.html", error=g.trans['password_dont_match'])
29
+
30
+ user = g.db.query(User).filter_by(username=form_username).first()
31
+ if user:
32
+ return render_template('register.html', error=g.trans["user_exists"])
33
+
34
+ if request.form.get("frc-captcha-response") is None or request.form.get("frc-captcha-response") == ".ACTIVATED":
35
+ return render_template('register.html', error=g.trans['invalid_captcha'])
36
+
37
+ captcha_data = {'response': request.form.get("frc-captcha-response"), 'sitekey': g.fcaptcha_sitekey}
38
+
39
+ resp = requests.post("https://global.frcapi.com/api/v2/captcha/siteverify", json=captcha_data, headers={"X-API-Key": g.fcaptcha_apikey})
40
+
41
+ if json.loads(resp.text)['success'] != True:
42
+ return render_template("register.html", error=resp.text)
43
+
44
+ try:
45
+ hashed = bcrypt.hashpw(form_password.encode('utf-8'), bcrypt.gensalt())
46
+ new_user = User(username=form_username, email="", password_hash=hashed.decode('utf-8'), verified=0, admin=0)
47
+ g.db.add(new_user)
48
+ g.db.commit()
49
+ return render_template("register.html", message=g.trans["registered"])
50
+ except Exception:
51
+ g.db.rollback()
52
+ return render_template('register.html', error=g.trans["error"])
53
+
54
+ @auth_bp.route("/login", methods=['GET', 'POST'])
55
+ def login():
56
+ if g.user is not None:
57
+ return redirect("/dashboard")
58
+
59
+ if request.method == "GET":
60
+ return render_template('login.html')
61
+
62
+ form_username = request.form.get('username')
63
+ form_password = request.form.get('password')
64
+
65
+ user = g.db.query(User).filter_by(username=form_username).first()
66
+ if user and bcrypt.checkpw(form_password.encode('utf-8'), user.password_hash.encode('utf-8')):
67
+ session["userid"] = user.id
68
+ session["admin"] = user.admin
69
+ return redirect("/dashboard")
70
+ else:
71
+ return render_template('login.html', error=g.trans["invalid_credentials"])
72
+
73
+ @auth_bp.route("/logout")
74
+ def logout():
75
+ session.clear()
76
+ return redirect("/")
77
+
@@ -0,0 +1,205 @@
1
+ from flask import Blueprint, render_template, redirect, request, g, Response, abort
2
+ from openwrite.utils.models import Blog, Post, User, View, Like
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
+ from bs4 import BeautifulSoup
8
+ from datetime import timezone, datetime
9
+
10
+ blog_bp = Blueprint("blog", __name__)
11
+
12
+ @blog_bp.route("/b/<blog>")
13
+ def show_blog(blog):
14
+ if g.mode == "single":
15
+ return redirect("/")
16
+ blog = g.db.query(Blog).filter_by(name=blog).first()
17
+ if blog is None:
18
+ return redirect("/")
19
+
20
+ if blog.access == "domain":
21
+ return redirect(f"https://{blog.name}.{os.getenv('DOMAIN')}/")
22
+
23
+ posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
24
+ blog.url = f"/b/{blog.name}"
25
+
26
+ return render_template("blog.html", blog=blog, posts=posts)
27
+
28
+ @blog_bp.route("/", subdomain="<blog>")
29
+ def show_subblog(blog):
30
+ if g.mode == "single":
31
+ return redirect("/")
32
+ blog = g.db.query(Blog).filter_by(name=blog).first()
33
+ if blog is None:
34
+ return redirect(f"https://{os.getenv('DOMAIN')}/")
35
+
36
+ if blog.access == "path":
37
+ return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog.name}")
38
+
39
+ posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
40
+
41
+ return render_template("blog.html", blog=blog, posts=posts)
42
+
43
+ @blog_bp.route("/b/<blog>/<post>")
44
+ def show_post(blog, post):
45
+ if g.mode == "single":
46
+ return redirect("/")
47
+ blog = g.db.query(Blog).filter_by(name=blog).first()
48
+ if blog is None:
49
+ return redirect("/")
50
+
51
+ if post == "rss":
52
+ return _generate_rss(blog)
53
+
54
+ if blog.access == "domain":
55
+ return redirect(f"https://{blog.name}.{os.getenv('DOMAIN')}/{post}")
56
+
57
+ one_post = g.db.query(Post).filter(Post.blog == blog.id, Post.link == post).first()
58
+ if not one_post:
59
+ return redirect("/")
60
+
61
+ post_author = g.db.query(User).filter_by(id=blog.owner).first()
62
+ one_post.authorname = post_author.username
63
+ blog.url = f"/b/{blog.name}"
64
+
65
+ ip = anonymize(get_ip())
66
+ v = g.db.query(View).filter(View.blog == blog.id, View.post == one_post.id, View.hash == ip).count()
67
+ user_agent = request.headers.get('User-Agent')
68
+ if v < 1:
69
+ now = datetime.now(timezone.utc).replace(microsecond=0)
70
+ new_view = View(blog=blog.id, post=one_post.id, hash=ip, date=now, agent=user_agent)
71
+ g.db.add(new_view)
72
+ g.db.commit()
73
+ likes = g.db.query(Like).filter(Like.blog == blog.id, Like.post == one_post.id).count()
74
+ one_post.likes = likes
75
+ liked = g.db.query(Like).filter(Like.blog == blog.id, Like.post == one_post.id, Like.hash == anonymize(get_ip())).count()
76
+ one_post.liked = liked
77
+
78
+ user = g.db.query(User).filter_by(id=g.user) if g.user else None
79
+ return render_template("post.html", blog=blog, post=one_post, user=user, views=v, likes=likes)
80
+
81
+ @blog_bp.route("/<post>", subdomain="<blog>")
82
+ def show_subpost(blog, post):
83
+ if g.mode == "single":
84
+ return redirect("/")
85
+ blog = g.db.query(Blog).filter_by(name=blog).first()
86
+ if blog is None:
87
+ return redirect(f"https://{os.getenv('DOMAIN')}/")
88
+
89
+ if post == "rss":
90
+ return _generate_rss(blog)
91
+
92
+ if blog.access == "path":
93
+ return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog.name}/{post}")
94
+
95
+ one_post = g.db.query(Post).filter(Post.blog == blog.id, Post.link == post).first()
96
+ if not one_post:
97
+ return redirect("/")
98
+
99
+ post_author = g.db.query(User).filter_by(id=blog.owner).first()
100
+ one_post.authorname = post_author.username
101
+ blog.url = f"https://{blog.name}.{os.getenv('DOMAIN')}"
102
+
103
+ ip = anonymize(get_ip())
104
+ v = g.db.query(View).filter(View.blog == blog.id, View.post == one_post.id, View.hash == ip).count()
105
+ user_agent = request.headers.get('User-Agent')
106
+ if v < 1:
107
+ now = datetime.now(timezone.utc).replace(microsecond=0)
108
+ new_view = View(blog=blog.id, post=one_post.id, hash=ip, date=now, agent=user_agent)
109
+ g.db.add(new_view)
110
+ g.db.commit()
111
+
112
+ likes = g.db.query(Like).filter(Like.blog == blog.id, Like.post == one_post.id).count()
113
+ one_post.likes = likes
114
+ liked = g.db.query(Like).filter(Like.blog == blog.id, Like.post == one_post.id, Like.hash == anonymize(get_ip())).count()
115
+ one_post.liked = liked
116
+
117
+ user = g.db.query(User).filter_by(id=g.user) if g.user else None
118
+ return render_template("post.html", blog=blog, post=one_post, user=user, views=v)
119
+
120
+ @blog_bp.route("/p/<post>")
121
+ def single_showpost(post):
122
+ if g.mode == "multi":
123
+ return redirect("/")
124
+
125
+ blog = g.db.query(Blog).filter_by(id=1).first()
126
+ one_post = g.db.query(Post).filter(Post.blog == 1, Post.link == post).first()
127
+ if not one_post:
128
+ return redirect("/")
129
+
130
+ post_author = g.db.query(User).filter_by(id=1).first()
131
+ one_post.authorname = post_author.username
132
+
133
+ ip = anonymize(get_ip())
134
+ v = g.db.query(View).filter(View.blog == 1, View.post == one_post.id, View.hash == ip).count()
135
+ user_agent = request.headers.get('User-Agent')
136
+ if v < 1:
137
+ now = datetime.now(timezone.utc).replace(microsecond=0)
138
+ new_view = View(blog=1, post=one_post.id, hash=ip, date=now, agent=user_agent)
139
+ g.db.add(new_view)
140
+ g.db.commit()
141
+
142
+ likes = g.db.query(Like).filter(Like.blog == 1, Like.post == one_post.id).count()
143
+ one_post.likes = likes
144
+ liked = g.db.query(Like).filter(Like.blog == 1, Like.post == one_post.id, Like.hash == anonymize(get_ip())).count()
145
+ one_post.liked = liked
146
+
147
+ user = g.db.query(User).filter_by(id=g.user) if g.user else None
148
+ return render_template("post.html", blog=blog, post=one_post, user=user, views=v)
149
+
150
+ @blog_bp.route("/rss")
151
+ def single_rss():
152
+ if g.mode == "multi":
153
+ return redirect("/")
154
+
155
+ blog = g.db.query(Blog).first()
156
+ return _generate_rss(blog)
157
+
158
+ @blog_bp.route("/like", methods=["POST"])
159
+ def like():
160
+ data = request.get_json()
161
+ if not data:
162
+ abort(400)
163
+
164
+ blog_id = data.get("blog")
165
+ post_id = data.get("post")
166
+ post = g.db.query(Post).filter(Post.blog == blog_id, Post.id == post_id).count()
167
+ if post < 1:
168
+ resp = {"status": "no_post"}
169
+ return resp, 404
170
+ ip = anonymize(get_ip())
171
+
172
+ l = g.db.query(Like).filter(Like.blog == blog_id, Like.post == post_id, Like.hash == ip).first()
173
+ if l:
174
+ g.db.delete(l)
175
+ g.db.commit()
176
+ resp = {"status": "deleted"}
177
+ status = 204
178
+ else:
179
+ now = datetime.now(timezone.utc).replace(microsecond=0)
180
+ like = Like(blog=blog_id, post=post_id, hash=ip, date=now)
181
+ g.db.add(like)
182
+ g.db.commit()
183
+ resp = {"status": "ok"}
184
+ status = 201
185
+ return resp, status
186
+
187
+
188
+ def _generate_rss(blog):
189
+ posts = g.db.query(Post).filter_by(blog=blog.id).all()
190
+ fg = FeedGenerator()
191
+ fg.title(blog.title)
192
+ fg.link(href=f"https://{os.getenv('DOMAIN')}/b/{blog.name}", rel="alternate")
193
+ fg.description(blog.description_html)
194
+
195
+ for p in posts:
196
+ soup = BeautifulSoup(p.content_html, "html.parser")
197
+ fe = fg.add_entry()
198
+ fe.title(p.title)
199
+ fe.link(href=f"https://{os.getenv('DOMAIN')}/b/{blog.name}/{p.link}")
200
+ fe.description(soup.get_text())
201
+ fe.published(p.date.replace(tzinfo=timezone.utc))
202
+
203
+ return Response(fg.rss_str(pretty=True).decode("utf-8"), mimetype="application/rss+xml")
204
+
205
+
@@ -0,0 +1,408 @@
1
+ from flask import Blueprint, render_template, redirect, request, g, abort
2
+ from openwrite.utils.models import Blog, Post, User, View
3
+ from openwrite.utils.helpers import sanitize_html, gen_link, safe_css, send_activity, is_html, get_themes
4
+ import requests
5
+ from sqlalchemy import desc
6
+ from datetime import datetime, timezone, timedelta
7
+ from zoneinfo import ZoneInfo
8
+ from cryptography.hazmat.primitives.asymmetric import rsa
9
+ from cryptography.hazmat.primitives import serialization
10
+ import json
11
+ import bcrypt
12
+ import markdown
13
+ from bs4 import BeautifulSoup
14
+ from user_agents import parse
15
+
16
+ dashboard_bp = Blueprint("dashboard", __name__)
17
+
18
+ @dashboard_bp.route("/dashboard")
19
+ def dashboard():
20
+ if g.user is None:
21
+ return redirect("/login")
22
+
23
+ user_blogs = g.db.query(Blog).filter_by(owner=g.user).all()
24
+ user = g.db.query(User).filter_by(id=g.user)
25
+
26
+ return render_template("dashboard.html", blogs=user_blogs, user=user)
27
+
28
+ @dashboard_bp.route("/dashboard/create", methods=['GET', 'POST'])
29
+ def create_blog():
30
+ if g.mode == "single":
31
+ return redirect("/")
32
+ if g.user is None:
33
+ return redirect("/login")
34
+
35
+ count = g.db.query(Blog).filter_by(owner=g.user).count()
36
+ if count >= int(g.blog_limit):
37
+ return render_template("create.html", error="Blog limit reached!")
38
+
39
+ if request.method == "GET":
40
+ return render_template("create.html")
41
+
42
+ form_name = request.form.get("name")
43
+ form_url = gen_link(request.form.get("url"))
44
+ if len(form_name) > 30:
45
+ return render_template("create.html", error="Title too long! Max 30 characters.")
46
+ if len(form_url) > 30:
47
+ return render_template("create.html", error="URL too long! Max 30 characters.")
48
+ blog = g.db.query(Blog).filter_by(name=form_url).first()
49
+ if blog:
50
+ return render_template("create.html", error="This URL already exists!")
51
+
52
+ form_index = request.form.get("index") or "off"
53
+ form_access = request.form.get("access")
54
+
55
+ key = rsa.generate_private_key(
56
+ public_exponent=65537,
57
+ key_size=2048
58
+ )
59
+
60
+ private_pem = key.private_bytes(
61
+ encoding=serialization.Encoding.PEM,
62
+ format=serialization.PrivateFormat.PKCS8,
63
+ encryption_algorithm=serialization.NoEncryption()
64
+ ).decode()
65
+
66
+ public_pem = key.public_key().public_bytes(
67
+ encoding=serialization.Encoding.PEM,
68
+ format=serialization.PublicFormat.SubjectPublicKeyInfo
69
+ ).decode()
70
+
71
+ now = datetime.now(timezone.utc).replace(microsecond=0)
72
+ try:
73
+ new_blog = Blog(
74
+ owner=g.user,
75
+ title=form_name,
76
+ name=form_url,
77
+ index=form_index,
78
+ access=form_access,
79
+ description_raw=f"![hello](https://openwrite.b-cdn.net/hello.jpg =500x258)\n\n# Hello there! 👋\n\nYou can edit your blog description in [dashboard](https://{g.main_domain}/dashboard/edit/{form_url})",
80
+ description_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 description in <a href=\"https://{g.main_domain}/dashboard/edit/{form_url}\">dashboard</a></p>",
81
+ css="",
82
+ pub_key=public_pem,
83
+ priv_key=private_pem,
84
+ theme="default",
85
+ created=now
86
+ )
87
+ g.db.add(new_blog)
88
+ g.db.commit()
89
+ return redirect("/dashboard")
90
+ except Exception:
91
+ return render_template("create.html", error=g.trans['error'])
92
+
93
+ @dashboard_bp.route("/dashboard/delete/<name>")
94
+ def delete_blog(name):
95
+ if g.mode == "single":
96
+ return redirect("/")
97
+ if g.user is None:
98
+ return redirect("/login")
99
+
100
+ blog = g.db.query(Blog).filter_by(name=name).first()
101
+ if blog is None or blog.owner != g.user:
102
+ return redirect("/dashboard")
103
+
104
+ g.db.delete(blog)
105
+ g.db.commit()
106
+ return redirect("/dashboard")
107
+
108
+ @dashboard_bp.route("/dashboard/edit/<name>", methods=['GET', 'POST'])
109
+ def edit_blog(name):
110
+ if g.user is None:
111
+ return redirect("/login")
112
+
113
+ blog = g.db.query(Blog).filter_by(name=name).first()
114
+ if blog is None or blog.owner != g.user:
115
+ return redirect("/dashboard")
116
+
117
+ posts = g.db.query(Post).filter_by(blog=blog.id).all()
118
+ for p in posts:
119
+ v = g.db.query(View).filter(View.post == p.id, View.blog == blog.id).count()
120
+ p.views = v
121
+
122
+ themes = get_themes()
123
+
124
+ if request.method == "GET":
125
+ return render_template("edit.html", blog=blog, posts=posts, themes=themes)
126
+
127
+ now = datetime.now(timezone.utc).replace(microsecond=0)
128
+ blog.description_raw = request.form.get("description_raw")
129
+ blog.description_html = sanitize_html(request.form.get("description_html"))
130
+ if len(request.form.get("title")) > 30:
131
+ return render_template("edit.html", blog=blog, posts=posts, themes=themes, error="Title too long! Max 30 characters.")
132
+ blog.css = safe_css(request.form.get("css"))
133
+ blog.updated = now
134
+ blog.title = request.form.get("title")
135
+ selected_theme = request.form.get("theme")
136
+ themes_plus = themes
137
+ themes_plus.append("default")
138
+ if selected_theme not in themes_plus:
139
+ print(f"{selected_theme} in {themes}?")
140
+ return render_template("edit.html", blog=blog, posts=posts, themes=themes, error="Wrong theme!")
141
+ blog.theme = selected_theme
142
+ g.db.commit()
143
+
144
+ return render_template("edit.html", blog=blog, posts=posts, themes=themes)
145
+
146
+ @dashboard_bp.route("/dashboard/post/<name>", methods=['GET', 'POST'])
147
+ def new_post(name):
148
+ if g.user is None:
149
+ return redirect("/login")
150
+
151
+ blog = g.db.query(Blog).filter_by(name=name).first()
152
+ if blog is None or blog.owner != g.user:
153
+ return redirect("/dashboard")
154
+
155
+ if request.method == "GET":
156
+ return render_template("new_post.html", blog=blog)
157
+
158
+ u = g.db.query(User).filter_by(id=g.user).first()
159
+ title = request.form.get('title')
160
+ if len(title) > 120:
161
+ return render_template("new_post.html", blog=blog, error="Title too long! Max 120 characters.")
162
+
163
+ link = gen_link(title)
164
+ if link == "rss":
165
+ link = "p_rss"
166
+ dupes = g.db.query(Post).filter(Post.link.startswith(link), Post.blog == blog.id).count()
167
+ if dupes > 0:
168
+ link += f"-{dupes + 1}"
169
+
170
+ now = datetime.now(timezone.utc).replace(microsecond=0)
171
+ date = now
172
+
173
+ post = Post(
174
+ blog=blog.id,
175
+ title=title,
176
+ content_raw=request.form.get('content_raw'),
177
+ content_html=sanitize_html(request.form.get('content')),
178
+ author=request.form.get('author'),
179
+ link=link,
180
+ date=now,
181
+ feed=request.form.get('feed')
182
+ )
183
+ g.db.add(post)
184
+ g.db.commit()
185
+
186
+ if blog.access == "domain":
187
+ url = f"https://{blog.name}.openwrite.io/{link}"
188
+ else:
189
+ url = f"https://openwrite.io/b/{blog.name}/{link}"
190
+
191
+ followers = []
192
+ if blog.followers is not None:
193
+ followers = json.loads(blog.followers)
194
+
195
+ for actor in followers:
196
+ now = datetime.utcnow().isoformat() + "Z"
197
+ activity = {
198
+ "@context": "https://www.w3.org/ns/activitystreams",
199
+ "id": f"{url}",
200
+ "type": "Create",
201
+ "actor": f"https://{g.main_domain}/activity/{blog.name}",
202
+ "cc": followers,
203
+ "object": {
204
+ "id": f"{url}",
205
+ "type": "Note",
206
+ "published": now,
207
+ "attributedTo": f"https://{g.main_domain}/activity/{blog.name}",
208
+ "content": f"<p>{title}</p><a href=\"{url}\">{url}</a>",
209
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
210
+ "cc": followers
211
+ },
212
+ "to": ["https://www.w3.org/ns/activitystreams#Public"]
213
+ }
214
+ send_activity(
215
+ activity,
216
+ blog.priv_key,
217
+ f"https://{g.main_domain}/activity/{blog.name}",
218
+ f"{actor}/inbox"
219
+ )
220
+
221
+ return redirect(f"/dashboard/edit/{blog.name}")
222
+
223
+ @dashboard_bp.route("/dashboard/preview", methods=['POST'])
224
+ def preview():
225
+ if g.user is None:
226
+ return redirect("/login")
227
+
228
+ u = g.db.query(User).filter_by(id=g.user).first()
229
+ data = {
230
+ 'title': request.form.get('title'),
231
+ 'content': sanitize_html(request.form.get('content')),
232
+ 'author': request.form.get('author'),
233
+ 'blog_title': request.form.get('blog_title'),
234
+ 'blog_name': request.form.get('blog_name'),
235
+ 'theme': request.form.get('theme'),
236
+ 'date': request.form.get('date'),
237
+ 'author_name': u.username
238
+ }
239
+
240
+ return render_template("preview.html", data=data)
241
+
242
+ @dashboard_bp.route("/dashboard/blog_preview", methods=['POST'])
243
+ def blog_preview():
244
+ if g.user is None:
245
+ return redirect("/login")
246
+
247
+ u = g.db.query(User).filter_by(id=g.user).first()
248
+ data = {
249
+ 'title': request.form.get('title'),
250
+ 'content': sanitize_html(request.form.get('content')),
251
+ 'theme': request.form.get('theme')
252
+ }
253
+
254
+ return render_template("blog_preview.html", data=data)
255
+
256
+ @dashboard_bp.route("/dashboard/edit/<blog>/<post>", methods=['GET', 'POST'])
257
+ def edit_post(blog, post):
258
+ if g.user is None:
259
+ return redirect("/login")
260
+
261
+ blog_obj = g.db.query(Blog).filter_by(name=blog).first()
262
+ if blog_obj is None or blog_obj.owner != g.user:
263
+ return redirect("/dashboard")
264
+
265
+ e_post = g.db.query(Post).filter_by(link=post).first()
266
+ if request.method == "GET":
267
+ return render_template("new_post.html", blog=blog_obj, post=e_post)
268
+
269
+ p = g.db.query(Post).filter_by(link=post, blog=blog_obj.id).first()
270
+ if not p:
271
+ return redirect("/dashboard")
272
+
273
+ title = request.form.get("title")
274
+ if len(title) > 120:
275
+ return render_template("new_post.html", blog=blog_obj, error="Title too long! Max 120 characters.")
276
+ link = gen_link(title)
277
+ if link == "rss":
278
+ link = "p_rss"
279
+ dupes = g.db.query(Post).filter(Post.link.startswith(link), Post.blog == blog_obj.id).count()
280
+ if dupes > 0 and link != post:
281
+ link += f"-{dupes + 1}"
282
+
283
+ p.title = title
284
+ now = datetime.now(timezone.utc).replace(microsecond=0)
285
+ p.content_raw = request.form.get("content_raw")
286
+ p.content_html = sanitize_html(request.form.get("content"))
287
+ p.author = request.form.get("author")
288
+ p.feed = request.form.get("feed")
289
+ p.link = link
290
+ p.updated = now
291
+ g.db.commit()
292
+ return redirect(f"/dashboard/edit/{blog_obj.name}")
293
+
294
+ @dashboard_bp.route("/dashboard/edit/<blog>/<post>/delete")
295
+ def delete_post(blog, post):
296
+ if g.user is None:
297
+ return redirect("/login")
298
+
299
+ blog_obj = g.db.query(Blog).filter_by(name=blog).first()
300
+ if blog_obj is None or blog_obj.owner != g.user:
301
+ return redirect("/dashboard")
302
+
303
+ p = g.db.query(Post).filter_by(link=post, blog=blog_obj.id).first()
304
+ if p:
305
+ g.db.delete(p)
306
+ g.db.commit()
307
+
308
+ return redirect(f"/dashboard/edit/{blog_obj.name}")
309
+
310
+
311
+ @dashboard_bp.route("/dashboard/changepw", methods=['GET', 'POST'])
312
+ def changepw():
313
+ if g.user is None:
314
+ return redirect("/login")
315
+
316
+ if request.method == "GET":
317
+ return render_template("changepw.html")
318
+
319
+ old_pw = request.form.get("current_pass")
320
+ new_pass = request.form.get("new_pass")
321
+ new_pass2 = request.form.get("new_pass2")
322
+ user = g.db.query(User).filter_by(id=g.user).first()
323
+ if user and bcrypt.checkpw(old_pw.encode('utf-8'), user.password_hash.encode('utf-8')):
324
+ if new_pass != new_pass2:
325
+ return render_template("changepw.html", error=g.trans['passwords_dont_match'])
326
+ hashed = bcrypt.hashpw(new_pass.encode('utf-8'), bcrypt.gensalt())
327
+ user.password_hash = hashed
328
+ return redirect("/dashboard")
329
+
330
+ else:
331
+ return render_template("changepw.html", error=g.trans['invalid_password'])
332
+
333
+
334
+ @dashboard_bp.route("/dashboard/import", methods=['GET', 'POST'])
335
+ def migrate():
336
+ if g.user is None:
337
+ return redirect("/login")
338
+
339
+ blogs = g.db.query(Blog).filter_by(owner=g.user).all()
340
+ if request.method == "GET":
341
+ return render_template("import.html", blogs=blogs)
342
+
343
+ data = request.get_json()
344
+ posts = data.get("posts")
345
+ blog_d = data.get("blog")
346
+
347
+ blog = g.db.query(Blog).filter_by(name=blog_d).first()
348
+ if blog.owner != g.user:
349
+ return redirect("/dashboard")
350
+
351
+ for post in posts:
352
+ title = post['title']
353
+ content = post['content']
354
+ date = datetime.strptime(post['date'], "%a, %d %b %Y %H:%M:%S %Z")
355
+
356
+ if is_html(content):
357
+ soup = BeautifulSoup(content, "html.parser")
358
+ text_content = soup.get_text()
359
+ html_content = content
360
+ else:
361
+ html_content = markdown.markdown(content)
362
+ text_content = content
363
+ new_post = Post(blog=blog.id, content_raw=text_content, content_html=html_content, author='0', feed='0', date=date, title=title, link=gen_link(title))
364
+ g.db.add(new_post)
365
+ g.db.commit()
366
+
367
+ return "ok", 200
368
+
369
+ @dashboard_bp.route("/dashboard/stats/<blog>")
370
+ def stats(blog):
371
+ if g.user is None:
372
+ return redirect("/login")
373
+
374
+ blog = g.db.query(Blog).filter_by(name=blog).first()
375
+ if blog.owner != g.user:
376
+ return redirect("/dashboard")
377
+
378
+ posts = g.db.query(Post).filter_by(blog=blog.id).all()
379
+
380
+ return render_template("stats.html", blog=blog, posts=posts)
381
+
382
+ @dashboard_bp.route("/dashboard/get_stats/<blog>/<post>/<limit>")
383
+ def get_stats(blog, post, limit):
384
+ if g.user is None:
385
+ abort(403)
386
+
387
+ b = g.db.query(Blog).filter_by(id=blog).first()
388
+ if g.user != b.owner:
389
+ abort(403)
390
+
391
+ if int(limit) not in (24, 168, 720, 2160):
392
+ abort(400)
393
+ time_threshold = datetime.now() - timedelta(hours=int(limit))
394
+ views_obj = {}
395
+ views_obj["views"] = []
396
+ start_from = g.db.query(View).filter(View.blog == blog, View.post == post, View.date < time_threshold).count()
397
+ views_obj["start_from"] = start_from
398
+ views = g.db.query(View).filter(View.blog == blog, View.post == post, View.date >= time_threshold).all()
399
+ for v in views:
400
+ os = "Unknown"
401
+ browser = "Unknown"
402
+ if v.agent not in (None, "null", "NULL"):
403
+ ua = parse(v.agent)
404
+ os = ua.os.family
405
+ browser = ua.browser.family
406
+ views_obj["views"].append([v.date, os, browser])
407
+
408
+ return views_obj