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.
- openwrite/__init__.py +134 -0
- openwrite/cli.py +340 -0
- openwrite/db/__init__.py +0 -0
- openwrite/db/base.py +3 -0
- openwrite/launch_gemini.py +8 -0
- openwrite/routes/admin.py +170 -0
- openwrite/routes/auth.py +77 -0
- openwrite/routes/blog.py +205 -0
- openwrite/routes/dashboard.py +408 -0
- openwrite/routes/federation.py +284 -0
- openwrite/routes/main.py +96 -0
- openwrite/routes/upload.py +81 -0
- openwrite/static/IBMPlexSans-Regular.ttf +0 -0
- openwrite/static/Merriweather.ttf +0 -0
- openwrite/static/avatar.png +0 -0
- openwrite/static/favicon.ico +0 -0
- openwrite/static/logo.png +0 -0
- openwrite/static/script/chart.js +14 -0
- openwrite/static/script/highlight.min.js +1244 -0
- openwrite/static/script/openwrite.js +88 -0
- openwrite/static/script/papaparse.min.js +7 -0
- openwrite/static/script/showdown.min.js +3 -0
- openwrite/static/script/showdown.min.js.map +1 -0
- openwrite/static/script/site.compat.min.js +7 -0
- openwrite/static/script/site.min.js +7 -0
- openwrite/static/style/hl.css +1 -0
- openwrite/static/style/style.css +611 -0
- openwrite/static/style/themes/light.css +28 -0
- openwrite/static/style/themes/obscura.css +27 -0
- openwrite/static/style/themes/stickynote.css +50 -0
- openwrite/static/style/themes/terminal.css +37 -0
- openwrite/static/style/themes/warmnight.css +73 -0
- openwrite/static/style/themes/windows98.css +132 -0
- openwrite/templates/admin.html +47 -0
- openwrite/templates/blog.html +16 -0
- openwrite/templates/blog_footer.html +6 -0
- openwrite/templates/blog_header.html +45 -0
- openwrite/templates/blog_preview.html +44 -0
- openwrite/templates/changepw.html +19 -0
- openwrite/templates/create.html +46 -0
- openwrite/templates/dashboard.html +45 -0
- openwrite/templates/discover.html +11 -0
- openwrite/templates/edit.html +190 -0
- openwrite/templates/footer.html +16 -0
- openwrite/templates/header.html +61 -0
- openwrite/templates/import.html +142 -0
- openwrite/templates/index.html +9 -0
- openwrite/templates/instances.html +14 -0
- openwrite/templates/login.html +18 -0
- openwrite/templates/new_post.html +207 -0
- openwrite/templates/post.html +53 -0
- openwrite/templates/preview.html +37 -0
- openwrite/templates/register.html +42 -0
- openwrite/templates/settings.html +63 -0
- openwrite/templates/stats.html +242 -0
- openwrite/utils/create_db.py +15 -0
- openwrite/utils/db.py +30 -0
- openwrite/utils/helpers.py +216 -0
- openwrite/utils/i18n.json +229 -0
- openwrite/utils/models.py +87 -0
- openwrite/utils/translations.py +6 -0
- openwrite-0.10.1.dist-info/METADATA +684 -0
- openwrite-0.10.1.dist-info/RECORD +67 -0
- openwrite-0.10.1.dist-info/WHEEL +5 -0
- openwrite-0.10.1.dist-info/entry_points.txt +2 -0
- openwrite-0.10.1.dist-info/licenses/LICENSE +661 -0
- openwrite-0.10.1.dist-info/top_level.txt +1 -0
openwrite/routes/auth.py
ADDED
|
@@ -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
|
+
|
openwrite/routes/blog.py
ADDED
|
@@ -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"\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
|