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,284 @@
1
+ from flask import Blueprint, render_template, redirect, g, jsonify, request, abort, Response
2
+ from openwrite.utils.models import Blog, User, Like, Post
3
+ from openwrite.utils.helpers import verify_http_signature, send_activity, anonymize
4
+ import json
5
+ import requests
6
+ from datetime import datetime, timezone
7
+
8
+ federation_bp = Blueprint("federation", __name__)
9
+
10
+ @federation_bp.route("/.well-known/webfinger")
11
+ def webfinger():
12
+ resource = request.args.get("resource")
13
+ if not resource or not resource.startswith("acct:"):
14
+ abort(400)
15
+
16
+ blogname = resource.split(":")[1].split("@")[0]
17
+ b_count = g.db.query(Blog).filter_by(name=blogname).count()
18
+ if not b_count or b_count < 1:
19
+ abort(404)
20
+ data = {
21
+ "subject": f"acct:{blogname}@{g.main_domain}",
22
+ "links": [{
23
+ "rel": "self",
24
+ "type": "application/activity+json",
25
+ "href": f"https://{g.main_domain}/activity/{blogname}"
26
+ }]
27
+ }
28
+
29
+ return Response(
30
+ response=json.dumps(data),
31
+ status=200,
32
+ content_type="application/jrd+json"
33
+ )
34
+
35
+ @federation_bp.route("/activity/<blog>")
36
+ def activity(blog):
37
+ b = g.db.query(Blog).filter_by(name=blog).first()
38
+ if not b:
39
+ abort(404)
40
+
41
+ if b.access == "domain":
42
+ url = f"https://{blog}.{g.main_domain}"
43
+ else:
44
+ url = f"https://{g.main_domain}/b/{blog}"
45
+
46
+ published = b.created
47
+ dt = datetime.strptime(str(published), "%Y-%m-%d %H:%M:%S")
48
+ dt = dt.replace(tzinfo=timezone.utc)
49
+ iso = dt.isoformat(timespec="seconds").replace("+00:00", "Z")
50
+
51
+ actor = {
52
+ "@context": [
53
+ "https://www.w3.org/ns/activitystreams",
54
+ "https://w3id.org/security/v1"
55
+ ],
56
+ "id": f"https://{g.main_domain}/activity/{blog}",
57
+ "type": "Person",
58
+ "preferredUsername": blog,
59
+ "name": blog,
60
+ "summary": f"{blog} - Blog on {g.main_domain}",
61
+ "inbox": f"https://{g.main_domain}/inbox/{blog}",
62
+ "outbox": f"https://{g.main_domain}/outbox/{blog}",
63
+ "followers": f"https://{g.main_domain}/followers/{blog}",
64
+ "published": iso,
65
+ "manuallyApprovesFollowers": False,
66
+ "discoverable": True,
67
+ "indexable": True,
68
+ "url": url,
69
+ "publicKey": {
70
+ "id": f"https://{g.main_domain}/activity/{blog}#main-key",
71
+ "owner": f"https://{g.main_domain}/activity/{blog}",
72
+ "publicKeyPem": b.pub_key
73
+ },
74
+ "icon": {
75
+ "type": "Image",
76
+ "mediaType": "image/png",
77
+ "url": f"https://{g.main_domain}/static/avatar.png"
78
+ }
79
+ }
80
+
81
+ return Response(json.dumps(actor), content_type="application/activity+json")
82
+
83
+ @federation_bp.route("/inbox/<blog>", methods=["POST"])
84
+ def inbox(blog):
85
+ b = g.db.query(Blog).filter_by(name=blog).first()
86
+ if not b:
87
+ abort(404)
88
+
89
+ data = request.get_json()
90
+ if not data:
91
+ return "Bad Request", 400
92
+
93
+ #print(f"""
94
+ # Data received:
95
+
96
+ # {data}
97
+ #""")
98
+
99
+ body = request.get_data(as_text=True)
100
+ sign = verify_http_signature(request.headers, body, blog)
101
+ if not sign:
102
+ return "Bad signature", 400
103
+ if data.get("type") == "Follow":
104
+ actor = data.get("actor")
105
+ object_ = data.get("object")
106
+ id_ = data.get("id")
107
+
108
+ if object_ != f"https://{g.main_domain}/activity/{blog}":
109
+ return "Invalid target", 400
110
+
111
+ followers = []
112
+ if b.followers not in (None, "null", "NULL"):
113
+ followers = json.loads(b.followers)
114
+ if actor not in followers:
115
+ followers.append(actor)
116
+ b.followers = json.dumps(followers)
117
+ g.db.commit()
118
+
119
+ activity = {
120
+ "@context": "https://www.w3.org/ns/activitystreams",
121
+ "id": f"https://{g.main_domain}/activity/{blog}#accept-{id_.split('/')[-1]}",
122
+ "type": "Accept",
123
+ "actor": f"https://{g.main_domain}/activity/{blog}",
124
+ "object": data,
125
+ "to": [f"{actor}"]
126
+ }
127
+
128
+ actor_doc = requests.get(actor, headers={"Accept": "application/activity+json"}).json()
129
+ inbox = actor_doc.get("endpoints", {}).get("sharedInbox", actor)
130
+
131
+ from_ = f"https://{g.main_domain}/activity/{blog}"
132
+ send_activity(activity, b.priv_key, from_, f"{actor}/inbox")
133
+
134
+ return "", 202
135
+
136
+ elif data.get("type") == "Undo":
137
+ actor = data.get("actor")
138
+ object_ = data.get("object")
139
+
140
+ if object_['type'] == "Follow":
141
+ if object_['object'] != f"https://{g.main_domain}/activity/{blog}":
142
+ return "Invalid target", 400
143
+
144
+ followers = []
145
+ if b.followers not in (None, "null", "NULL"):
146
+ followers = json.loads(b.followers)
147
+ if actor in followers:
148
+ followers = followers.remove(actor)
149
+ b.followers = json.dumps(followers)
150
+ g.db.commit()
151
+
152
+ elif object_['type'] == "Like":
153
+ post_name = object_['object'].split("/")[-1]
154
+ if object_['object'].split("/")[2] == g.main_domain:
155
+ blog_name = object_['object'].split("/")[-2]
156
+ else:
157
+ blog_name = object_['object'].split("/")[2].split('.')[0]
158
+ blog = g.db.query(Blog).filter_by(name=blog_name).first()
159
+ blog_id = blog.id
160
+ post = g.db.query(Post).filter(Post.blog == blog_id, Post.link == post_name).first()
161
+ post_id = post.id
162
+
163
+ hashed = anonymize(actor)
164
+ like = g.db.query(Like).filter(Like.blog == blog_id, Like.post == post_id, Like.hash == hashed).first()
165
+ g.db.delete(like)
166
+ g.db.commit()
167
+
168
+ return "", 202
169
+
170
+ elif data.get("type") == "Like":
171
+ object_ = data.get("object")
172
+ actor = data.get("actor")
173
+ post_name = object_.split("/")[-1]
174
+ if object_.split("/")[2] == g.main_domain:
175
+ blog_name = object_.split("/")[-2]
176
+ else:
177
+ blog_name = object_.split("/")[2].split('.')[0]
178
+ blog = g.db.query(Blog).filter_by(name=blog_name).first()
179
+ blog_id = blog.id
180
+ post = g.db.query(Post).filter(Post.blog == blog_id, Post.link == post_name).first()
181
+ post_id = post.id
182
+
183
+ hashed = anonymize(actor)
184
+ like = Like(hash=hashed, blog=blog_id, post=post_id)
185
+ g.db.add(like)
186
+ g.db.commit()
187
+
188
+ return "", 202
189
+
190
+ @federation_bp.route("/outbox/<blog>")
191
+ def outbox(blog):
192
+ page = request.args.get("page")
193
+ b = g.db.query(Blog).filter_by(name=blog).first()
194
+ if not b:
195
+ abort(404)
196
+
197
+ p = g.db.query(Post).filter_by(blog=b.id)
198
+ total = p.count()
199
+ posts = p.all()
200
+ first_outbox = {
201
+ "@context": "https://www.w3.org/ns/activitystreams",
202
+ "id": f"https://openwrite.io/outbox/{blog}",
203
+ "type": "OrderedCollection",
204
+ "totalItems": total,
205
+ "first": f"https://openwrite.io/outbox/{blog}?page=1"
206
+ }
207
+
208
+ if page not in ("true", "1"):
209
+ return Response(json.dumps(first_outbox), content_type="application/activity+json")
210
+
211
+
212
+ orderedPosts = []
213
+ if b.access == "path":
214
+ url = f"https://{g.main_domain}/b/{blog}"
215
+ else:
216
+ url = f"https://{blog}.{g.main_domain}"
217
+ for post in posts:
218
+ dt = datetime.strptime(str(post.date), "%Y-%m-%d %H:%M:%S")
219
+ dt = dt.replace(tzinfo=timezone.utc)
220
+ iso = dt.isoformat(timespec="seconds").replace("+00:00", "Z")
221
+ orderedPosts.append({
222
+ "id": f"https://{g.main_domain}/activity/{blog}#{post.link}",
223
+ "type": "Create",
224
+ "actor": f"https://{g.main_domain}/activity/{blog}",
225
+ "object": {
226
+ "id": f"{url}/{post.link}",
227
+ "type": "Note",
228
+ "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"]
234
+ })
235
+
236
+ outbox = {
237
+ "@context": "https://www.w3.org/ns/activitystreams",
238
+ "id": f"https://openwrite.io/outbox/{blog}?page={page}",
239
+ "type": "OrderedCollectionPage",
240
+ "partOf": f"https://{g.main_domain}/outbox/{blog}",
241
+ "orderedItems": orderedPosts
242
+ }
243
+
244
+ return Response(json.dumps(outbox), content_type="application/activity+json")
245
+
246
+ @federation_bp.route("/followers/<blog>")
247
+ def followers(blog):
248
+ page = request.args.get("page")
249
+ b = g.db.query(Blog).filter_by(name=blog).first()
250
+ if not b:
251
+ abort(404)
252
+
253
+ followers = []
254
+ if b.followers not in (None, "null", "NULL"):
255
+ followers = json.loads(b.followers)
256
+
257
+ if page not in ("true", "1"):
258
+ data = {
259
+ "@context": "https://www.w3.org/ns/activitystreams",
260
+ "id": f"https://{g.main_domain}/followers/{blog}",
261
+ "type": "OrderedCollection",
262
+ "totalItems": len(followers),
263
+ "first": {
264
+ "@context": "https://www.w3.org/ns/activitystreams",
265
+ "id": f"https://{g.main_domain}/followers/{blog}?page=true",
266
+ "type": "OrderedCollectionPage",
267
+ "partOf": f"https://{g.main_domain}.followers/{blog}",
268
+ "totalItems": len(followers),
269
+ "orderedItems": followers
270
+ }
271
+ }
272
+
273
+ return Response(json.dumps(data), content_type="application/activity+json")
274
+
275
+ data = {
276
+ "@context": "https://www.w3.org/ns/activitystreams",
277
+ "id": f"https://{g.main_domain}/followers/{blog}?page={page}",
278
+ "type": "OrderedCollectionPage",
279
+ "totalItems": len(followers),
280
+ "partOf": f"https://{g.main_domain}/followers/{blog}",
281
+ "orderedItems": followers
282
+ }
283
+
284
+ return Response(json.dumps(data), content_type="application/activity+json")
@@ -0,0 +1,96 @@
1
+ from flask import Blueprint, render_template, redirect, request, make_response, g
2
+ import os
3
+ import json
4
+ import datetime
5
+ import requests
6
+ from openwrite.utils.models import Post, Blog, Home, Settings
7
+ import time
8
+ from openwrite import start_time
9
+
10
+ from sqlalchemy import desc
11
+
12
+ main_bp = Blueprint("main", __name__)
13
+
14
+ @main_bp.route("/")
15
+ def index():
16
+ if g.mode == "multi":
17
+ home = g.db.query(Home).filter_by(name="hometext").all()
18
+ return render_template('index.html', home=home)
19
+
20
+ elif g.mode == "single":
21
+ blog = g.db.query(Blog).filter_by(name="default").first()
22
+ posts = g.db.query(Post).filter_by(blog=blog.id).all()
23
+ return render_template('blog.html', blog=blog, posts=posts)
24
+
25
+ @main_bp.route("/set-lang/<lang_code>")
26
+ def set_lang(lang_code):
27
+ if lang_code not in g.alltrans:
28
+ return redirect("/")
29
+ resp = make_response(redirect(request.referrer or "/"))
30
+ resp.set_cookie("lang", lang_code, max_age=60*60*24*365)
31
+ return resp
32
+
33
+ @main_bp.route("/instances")
34
+ def instances():
35
+ if g.mode == "single":
36
+ return redirect("/")
37
+ instances = ["https://openwrite.io"]
38
+ instances_data = []
39
+
40
+ for i in instances:
41
+ try:
42
+ response = requests.get(f"{i}/.well-known/openwrite")
43
+ response.raise_for_status()
44
+ data = response.json()
45
+ uptime = str(datetime.timedelta(seconds=data['uptime']))
46
+ instances_data.append({
47
+ "name": data['name'],
48
+ "url": i,
49
+ "users": data['users'],
50
+ "uptime": uptime,
51
+ "version": data['version'],
52
+ "blogs": data['blogs'],
53
+ "register": data['register'],
54
+ "media": data['media']
55
+ })
56
+ except Exception:
57
+ continue
58
+
59
+ return render_template("instances.html", instances=instances_data)
60
+
61
+ @main_bp.route("/discover")
62
+ def discover():
63
+ if g.mode == "single":
64
+ return redirect("/")
65
+ posts = g.db.query(Post).filter_by(feed=1).order_by(desc(Post.id)).all()
66
+ for p in posts:
67
+ b = g.db.query(Blog).filter_by(id=p.blog).first()
68
+ if not b:
69
+ continue
70
+ if b.access == "path":
71
+ url = f"https://{os.getenv('DOMAIN')}/b/{b.name}/{p.link}"
72
+ elif b.access == "domain":
73
+ url = f"https://{b.name}.{os.getenv('DOMAIN')}/{p.link}"
74
+ else:
75
+ url = "#"
76
+ p.url = url
77
+ p.blogname = b.title
78
+
79
+ return render_template("discover.html", posts=posts)
80
+
81
+ @main_bp.route('/.well-known/openwrite')
82
+ def show_instance():
83
+ from openwrite.utils.models import Blog, User
84
+ import time
85
+ blog_count = g.db.query(Blog).count()
86
+ user_count = g.db.query(User).count()
87
+ return {
88
+ "version": os.getenv("VERSION"),
89
+ "blogs": blog_count,
90
+ "users": user_count,
91
+ "uptime": int(time.time() - int(start_time)),
92
+ "name": os.getenv("DOMAIN"),
93
+ "register": os.getenv("SELF_REGISTER"),
94
+ "media": os.getenv("MEDIA_UPLOAD")
95
+ }
96
+
@@ -0,0 +1,81 @@
1
+ from flask import Blueprint, request, jsonify, g, send_from_directory, abort
2
+ from openwrite.utils.models import User
3
+ from werkzeug.utils import secure_filename
4
+ from PIL import Image
5
+ import os
6
+ import hashlib
7
+ import requests
8
+
9
+ upload_bp = Blueprint("upload", __name__)
10
+
11
+ UPLOAD_ENABLED = os.getenv("MEDIA_UPLOAD", "no") == "yes"
12
+ STORAGE_BACKEND = os.getenv("UPLOAD_STORAGE", "local")
13
+ BUNNY_API_KEY = os.getenv("BUNNY_API_KEY")
14
+ BUNNY_ZONE = os.getenv("BUNNY_STORAGE_ZONE")
15
+ BUNNY_URL = os.getenv("BUNNY_STORAGE_URL")
16
+ LOCAL_UPLOAD_DIR = os.getenv("UPLOAD_PATH", "uploads")
17
+ ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp"}
18
+
19
+ @upload_bp.route("/upload_image", methods=["POST"])
20
+ def upload_image():
21
+ if not UPLOAD_ENABLED:
22
+ return jsonify({"error": g.trans['uploads_disabled']}), 403
23
+
24
+ if 'file' not in request.files:
25
+ return jsonify({"error": g.trans['no_file']}), 400
26
+
27
+ if g.user is None:
28
+ return jsonify({"error": "unauthorized"}), 403
29
+
30
+ file = request.files['file']
31
+ filename = file.filename
32
+ if "." not in filename:
33
+ return jsonify({"error": g.trans['invalid_filename']}), 400
34
+
35
+ extension = filename.rsplit('.', 1)[1].lower()
36
+ if extension not in ALLOWED_EXTENSIONS:
37
+ return jsonify({"error": g.trans['unsupported_filetype']}), 400
38
+
39
+ try:
40
+ img = Image.open(file.stream)
41
+ img.verify()
42
+ except Exception:
43
+ return jsonify({"error": g.trans['not_image']}), 400
44
+
45
+ file.stream.seek(0)
46
+ user = g.db.query(User).filter_by(id=g.user).first()
47
+
48
+ m = hashlib.md5()
49
+ m.update(filename.encode() + user.username.encode())
50
+ filename = f"{user.username}_{m.hexdigest()}.{extension}"
51
+
52
+ if STORAGE_BACKEND == "bunny":
53
+ url = f"https://storage.bunnycdn.com/{BUNNY_ZONE}/{filename}"
54
+ headers = {
55
+ "AccessKey": BUNNY_API_KEY,
56
+ "Content-Type": "application/octet-stream",
57
+ "accept": "application/json"
58
+ }
59
+ response = requests.put(url, headers=headers, data=file)
60
+ if response.ok:
61
+ return jsonify({"url": f"{BUNNY_URL}{filename}"})
62
+ else:
63
+ return jsonify({"error": g.trans['upload_failed'], "detail": response.text}), 500
64
+
65
+ elif STORAGE_BACKEND == "local":
66
+ os.makedirs(LOCAL_UPLOAD_DIR, exist_ok=True)
67
+ filepath = os.path.join(LOCAL_UPLOAD_DIR, filename)
68
+ file.save(filepath)
69
+ return jsonify({"url": f"/uploads/{filename}"})
70
+
71
+ return jsonify({"error": g.trans['upload_failed']}), 500
72
+
73
+ @upload_bp.route("/uploads/<file>")
74
+ def get_file(file):
75
+ filename = secure_filename(file)
76
+ filepath = os.path.join(LOCAL_UPLOAD_DIR, filename)
77
+
78
+ if not os.path.isfile(filepath):
79
+ abort(404)
80
+
81
+ return send_from_directory(LOCAL_UPLOAD_DIR, filename)
Binary file
Binary file
Binary file
Binary file
Binary file