openwrite 0.14.0__tar.gz → 0.16.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openwrite-0.14.0/openwrite.egg-info → openwrite-0.16.0}/PKG-INFO +7 -3
- {openwrite-0.14.0 → openwrite-0.16.0}/README.md +6 -2
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/__init__.py +12 -11
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/cli.py +1 -1
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/gemini_multi.py +21 -1
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/routes/auth.py +8 -3
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/routes/blog.py +162 -58
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/routes/dashboard.py +83 -9
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/routes/main.py +29 -6
- openwrite-0.16.0/openwrite/static/favicon.png +0 -0
- openwrite-0.16.0/openwrite/static/feed/.gitkeep +0 -0
- openwrite-0.16.0/openwrite/static/feed/74.xml +12 -0
- openwrite-0.16.0/openwrite/static/feed/a.xml +2 -0
- openwrite-0.16.0/openwrite/static/feed/anim3sh1t.xml +12 -0
- openwrite-0.16.0/openwrite/static/feed/b.xml +2 -0
- openwrite-0.16.0/openwrite/static/feed/corrode.xml +4 -0
- openwrite-0.16.0/openwrite/static/feed/dirtysanchez.xml +6 -0
- openwrite-0.16.0/openwrite/static/feed/eikerammen.xml +165 -0
- openwrite-0.16.0/openwrite/static/feed/fedi.xml +9 -0
- openwrite-0.16.0/openwrite/static/feed/friskt.xml +2 -0
- openwrite-0.16.0/openwrite/static/feed/gordi.xml +6 -0
- openwrite-0.16.0/openwrite/static/feed/igilq.xml +226 -0
- openwrite-0.16.0/openwrite/static/feed/io.xml +50 -0
- openwrite-0.16.0/openwrite/static/feed/isyuricunha.xml +4 -0
- openwrite-0.16.0/openwrite/static/feed/kenny.xml +2 -0
- openwrite-0.16.0/openwrite/static/feed/m0bi.xml +6 -0
- openwrite-0.16.0/openwrite/static/feed/main.xml +236 -0
- openwrite-0.16.0/openwrite/static/feed/mywritese.xml +2 -0
- openwrite-0.16.0/openwrite/static/feed/nay-thoughs.xml +2 -0
- openwrite-0.16.0/openwrite/static/feed/nintendoflesz.xml +7 -0
- openwrite-0.16.0/openwrite/static/feed/phronima.xml +68 -0
- openwrite-0.16.0/openwrite/static/feed/ram.xml +75 -0
- openwrite-0.16.0/openwrite/static/feed/samah.xml +2 -0
- openwrite-0.16.0/openwrite/static/feed/themes-test.xml +18 -0
- openwrite-0.16.0/openwrite/static/feed/totylkoja.xml +78 -0
- openwrite-0.16.0/openwrite/static/feed/vegafjord.xml +48 -0
- openwrite-0.16.0/openwrite/static/feed/vijayvaradharaj.xml +2 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/style/blog_design.css +74 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/style/default.css +55 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/style/style.css +5 -1
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/style/themes/cyberpunk.css +69 -0
- openwrite-0.16.0/openwrite/static/style/themes/light.css +111 -0
- openwrite-0.16.0/openwrite/static/style/themes/obscura.css +82 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/style/themes/paper.css +73 -0
- openwrite-0.16.0/openwrite/static/style/themes/sepia.css +344 -0
- openwrite-0.16.0/openwrite/static/style/themes/stickynote.css +117 -0
- openwrite-0.16.0/openwrite/static/style/themes/synthwave.css +389 -0
- openwrite-0.16.0/openwrite/static/style/themes/terminal.css +126 -0
- openwrite-0.16.0/openwrite/static/style/themes/warmnight.css +141 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/style/themes/windows98.css +81 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/blog.html +3 -1
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/dashboard.html +1 -1
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/edit.html +6 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/footer.html +1 -1
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/new_page.html +1 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/new_post.html +183 -5
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/post.html +37 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/stats.html +26 -0
- openwrite-0.16.0/openwrite/utils/create_db.py +28 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/utils/helpers.py +3 -2
- openwrite-0.16.0/openwrite/utils/migrations.py +136 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/utils/models.py +59 -1
- openwrite-0.16.0/openwrite/utils/tags.py +242 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/utils/worker.py +8 -3
- {openwrite-0.14.0 → openwrite-0.16.0/openwrite.egg-info}/PKG-INFO +7 -3
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite.egg-info/SOURCES.txt +32 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/pyproject.toml +1 -1
- openwrite-0.14.0/openwrite/static/style/themes/light.css +0 -58
- openwrite-0.14.0/openwrite/static/style/themes/obscura.css +0 -27
- openwrite-0.14.0/openwrite/static/style/themes/stickynote.css +0 -60
- openwrite-0.14.0/openwrite/static/style/themes/terminal.css +0 -46
- openwrite-0.14.0/openwrite/static/style/themes/warmnight.css +0 -79
- openwrite-0.14.0/openwrite/utils/create_db.py +0 -15
- {openwrite-0.14.0 → openwrite-0.16.0}/LICENSE +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/MANIFEST.in +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/db/__init__.py +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/db/base.py +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/gemini_single.py +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/routes/admin.py +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/routes/federation.py +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/routes/upload.py +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/IBMPlexSans-Regular.ttf +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/Merriweather.ttf +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/avatar.png +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/emoji.json +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/favicon.ico +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/logo.png +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/chart.js +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/highlight.min.js +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/nativeEmoji.js +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/openwrite.js +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/papaparse.min.js +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/showdown.min.js +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/showdown.min.js.map +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/site.compat.min.js +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/script/site.min.js +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/style/hl.css +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/static/style/nativeEmoji.min.css +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/admin.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/blog_footer.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/blog_header.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/blog_preview.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/changepw.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/create.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/discover.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/header.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/import.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/index.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/instances.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/login.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/page_preview.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/preview.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/register.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/settings.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/templates/translations.html +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/utils/db.py +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/utils/i18n.json +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite/utils/valkey_client.py +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite.egg-info/dependency_links.txt +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite.egg-info/entry_points.txt +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite.egg-info/requires.txt +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/openwrite.egg-info/top_level.txt +0 -0
- {openwrite-0.14.0 → openwrite-0.16.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openwrite
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.0
|
|
4
4
|
License: GNU AFFERO GENERAL PUBLIC LICENSE
|
|
5
5
|
Version 3, 19 November 2007
|
|
6
6
|
|
|
@@ -689,13 +689,13 @@ Requires-Dist: redis
|
|
|
689
689
|
Dynamic: license-file
|
|
690
690
|
|
|
691
691
|

|
|
692
|
+
|
|
692
693
|
openwrite is a minimalist blogging platform built for writing freely, hosting independently, and publishing without noise.
|
|
693
694
|
|
|
694
695
|

|
|
695
696
|

|
|
696
697
|
[](LICENSE)
|
|
697
698
|

|
|
698
|
-

|
|
699
699
|
|
|
700
700
|
---
|
|
701
701
|
|
|
@@ -715,7 +715,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
|
|
|
715
715
|
- Lightweight
|
|
716
716
|
- No tracking, only data collected is anonymized(hashed) IP for post view counting and likes
|
|
717
717
|
- Custom CSS per blog
|
|
718
|
-
- Federation using ActivityPub protocol
|
|
718
|
+
- Federation using ActivityPub protocol
|
|
719
719
|
- Likes system
|
|
720
720
|
- Posts importing from xml/csv
|
|
721
721
|
- Blog themes
|
|
@@ -724,6 +724,10 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
|
|
|
724
724
|
- Multilanguage (currently English and Polish)
|
|
725
725
|
- Custom favicon per blog
|
|
726
726
|
- Subpages per blog
|
|
727
|
+
- Valkey support
|
|
728
|
+
- Post tags, filtering
|
|
729
|
+
- Optional comments per post
|
|
730
|
+
- Auto save during typing
|
|
727
731
|
|
|
728
732
|
|
|
729
733
|
## Installation
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|

|
|
2
|
+
|
|
2
3
|
openwrite is a minimalist blogging platform built for writing freely, hosting independently, and publishing without noise.
|
|
3
4
|
|
|
4
5
|

|
|
5
6
|

|
|
6
7
|
[](LICENSE)
|
|
7
8
|

|
|
8
|
-

|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -25,7 +25,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
|
|
|
25
25
|
- Lightweight
|
|
26
26
|
- No tracking, only data collected is anonymized(hashed) IP for post view counting and likes
|
|
27
27
|
- Custom CSS per blog
|
|
28
|
-
- Federation using ActivityPub protocol
|
|
28
|
+
- Federation using ActivityPub protocol
|
|
29
29
|
- Likes system
|
|
30
30
|
- Posts importing from xml/csv
|
|
31
31
|
- Blog themes
|
|
@@ -34,6 +34,10 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
|
|
|
34
34
|
- Multilanguage (currently English and Polish)
|
|
35
35
|
- Custom favicon per blog
|
|
36
36
|
- Subpages per blog
|
|
37
|
+
- Valkey support
|
|
38
|
+
- Post tags, filtering
|
|
39
|
+
- Optional comments per post
|
|
40
|
+
- Auto save during typing
|
|
37
41
|
|
|
38
42
|
|
|
39
43
|
## Installation
|
|
@@ -25,17 +25,21 @@ def create_app(test_config=None):
|
|
|
25
25
|
db_path = test_config.get("DB_PATH", "data.db")
|
|
26
26
|
load_dotenv(test_config.get("env"), override=True)
|
|
27
27
|
|
|
28
|
+
# Always run database migrations to ensure schema is up to date
|
|
29
|
+
from .utils.create_db import init_db
|
|
30
|
+
init_db(db_type, db_path)
|
|
31
|
+
|
|
32
|
+
# Initialize database engine and session
|
|
33
|
+
from .utils.models import User, Blog, Post, Settings, Home, Page
|
|
34
|
+
from .utils.db import init_engine, SessionLocal
|
|
35
|
+
init_engine(db_type, db_path)
|
|
36
|
+
from .utils.db import SessionLocal
|
|
37
|
+
|
|
28
38
|
if not os.path.exists(".initialized") or test_config is not None:
|
|
29
39
|
mode = os.getenv("MODE")
|
|
30
40
|
domain = os.getenv("DOMAIN")
|
|
31
41
|
f_abs_path = os.path.abspath(__file__)
|
|
32
42
|
file_cwd = "/".join(f_abs_path.split("/")[:-1])
|
|
33
|
-
from .utils.create_db import init_db
|
|
34
|
-
init_db(db_type, db_path)
|
|
35
|
-
from .utils.models import User, Blog, Post, Settings, Home, Page
|
|
36
|
-
from .utils.db import init_engine, SessionLocal
|
|
37
|
-
init_engine(db_type, db_path)
|
|
38
|
-
from .utils.db import SessionLocal
|
|
39
43
|
admin_password = os.urandom(16).hex()
|
|
40
44
|
hashed = bcrypt.hashpw("openwrite".encode("utf-8"), bcrypt.gensalt())
|
|
41
45
|
admin_user = User(username="admin", email="", password_hash=hashed.decode("utf-8"), verified=1, admin=1)
|
|
@@ -163,6 +167,7 @@ def create_app(test_config=None):
|
|
|
163
167
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
|
|
164
168
|
app.secret_key = os.getenv("SECRET_KEY")
|
|
165
169
|
app.config['SERVER_NAME'] = os.getenv("DOMAIN")
|
|
170
|
+
app.config['SESSION_COOKIE_DOMAIN'] = '.' + os.getenv("DOMAIN", "").split(':')[0]
|
|
166
171
|
|
|
167
172
|
from .routes.auth import auth_bp
|
|
168
173
|
from .routes.dashboard import dashboard_bp
|
|
@@ -171,14 +176,10 @@ def create_app(test_config=None):
|
|
|
171
176
|
from .routes.main import main_bp
|
|
172
177
|
from .routes.upload import upload_bp
|
|
173
178
|
from .routes.federation import federation_bp
|
|
174
|
-
from .utils.db import init_engine, SessionLocal
|
|
175
179
|
from flask_cors import CORS
|
|
176
180
|
|
|
177
181
|
CORS(app)
|
|
178
182
|
|
|
179
|
-
init_engine(db_type, db_path)
|
|
180
|
-
from .utils.db import SessionLocal
|
|
181
|
-
|
|
182
183
|
app.register_blueprint(auth_bp)
|
|
183
184
|
app.register_blueprint(dashboard_bp)
|
|
184
185
|
app.register_blueprint(blog_bp)
|
|
@@ -212,7 +213,7 @@ def create_app(test_config=None):
|
|
|
212
213
|
|
|
213
214
|
trans = g.db.query(Home).filter(Home.type == "translation", Home.language == lang).all()
|
|
214
215
|
|
|
215
|
-
g.version = "0.
|
|
216
|
+
g.version = "0.15.0"
|
|
216
217
|
g.mode = os.getenv("MODE")
|
|
217
218
|
f_abs_path = os.path.abspath(__file__)
|
|
218
219
|
g.mainpath = "/".join(f_abs_path.split("/")[:-1])
|
|
@@ -152,8 +152,8 @@ def init():
|
|
|
152
152
|
if captcha_enabled:
|
|
153
153
|
f.write(f"FRIENDLY_CAPTCHA_SITEKEY={captcha_sitekey}\n")
|
|
154
154
|
f.write(f"FRIENDLY_CAPTCHA_APIKEY={captcha_apikey}\n")
|
|
155
|
+
f.write(f"VALKEY_ENABLED={'yes' if valkey_enabled else 'no'}\n")
|
|
155
156
|
if valkey_enabled:
|
|
156
|
-
f.write(f"VALKEY_ENABLED={'yes' if valkey_enabled else 'no'}\n")
|
|
157
157
|
f.write(f"VALKEY_HOST={valkey_host}\n")
|
|
158
158
|
f.write(f"VALKEY_PORT={valkey_port}\n")
|
|
159
159
|
f.write(f"VALKEY_PASSWORD={valkey_password}\n")
|
|
@@ -22,8 +22,10 @@ def root(req):
|
|
|
22
22
|
global session
|
|
23
23
|
resp = f"```\n{openwrite_logo}\n```"
|
|
24
24
|
resp += "\nopenwrite.io is a minimalist blogging platform built for writing freely, hosting independently, and publishing without noise. To create your own blog, visit:\n"
|
|
25
|
-
resp += "=> https://openwrite.io openwrite.io\n"
|
|
25
|
+
resp += "=> https://openwrite.io https://openwrite.io\n"
|
|
26
26
|
resp += "In here you can read posts published.\n"
|
|
27
|
+
resp += "If you want to list all blogs (that did not disable indexing) find them in the link below:\n"
|
|
28
|
+
resp += "=> /blogs All Blogs list"
|
|
27
29
|
resp += "\n## 🗞️ Latest posts:\n"
|
|
28
30
|
try:
|
|
29
31
|
posts = (session.query(Post).filter(Post.feed == "1", Post.isdraft == "0").order_by(Post.id.desc()).limit(10).all())
|
|
@@ -36,6 +38,20 @@ def root(req):
|
|
|
36
38
|
finally:
|
|
37
39
|
session.close()
|
|
38
40
|
|
|
41
|
+
def all_blogs(req):
|
|
42
|
+
global session
|
|
43
|
+
blogs = session.query(Blog).filter_by(index='on').order_by(Blog.title).all()
|
|
44
|
+
resp = "Here you will find a list of all blogs in openwrite that did not disable their indexing:\n"
|
|
45
|
+
resp += "\n"
|
|
46
|
+
for b in blogs:
|
|
47
|
+
posts = session.query(Post).filter_by(blog=b.id).count()
|
|
48
|
+
last = session.query(Post).filter_by(blog=b.id).order_by(Post.date.desc()).first()
|
|
49
|
+
resp += f"=> /b/{b.name} {b.title} \n"
|
|
50
|
+
resp += f"Posts: {posts}\n"
|
|
51
|
+
resp += f"Last posted: {last.date if last is not None else 'never'}\n\n"
|
|
52
|
+
|
|
53
|
+
return resp
|
|
54
|
+
|
|
39
55
|
def blog_index(req):
|
|
40
56
|
global session
|
|
41
57
|
path = req.path
|
|
@@ -134,3 +150,7 @@ def init(capsule):
|
|
|
134
150
|
capsule.add("/b/*",
|
|
135
151
|
blog_index,
|
|
136
152
|
protocol='gemini')
|
|
153
|
+
|
|
154
|
+
capsule.add("/blogs",
|
|
155
|
+
all_blogs,
|
|
156
|
+
protocol='gemini')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from flask import Blueprint, render_template, redirect, request, session, g
|
|
1
|
+
from flask import Blueprint, render_template, redirect, request, session, g, current_app
|
|
2
2
|
from openwrite.utils.models import User
|
|
3
3
|
import bcrypt
|
|
4
4
|
import re
|
|
@@ -19,7 +19,7 @@ def register():
|
|
|
19
19
|
return render_template('register.html')
|
|
20
20
|
|
|
21
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])
|
|
22
|
+
if not re.match(r"^[a-zA-Z0-9](?:[a-zA-Z0-9_-]{1,28}[a-zA-Z0-9])+$", form_username):
|
|
23
23
|
return render_template('register.html', error=g.trans['wrong_username'])
|
|
24
24
|
form_password = request.form.get('password')
|
|
25
25
|
form_password2 = request.form.get('password2')
|
|
@@ -74,5 +74,10 @@ def login():
|
|
|
74
74
|
@auth_bp.route("/logout")
|
|
75
75
|
def logout():
|
|
76
76
|
session.clear()
|
|
77
|
-
|
|
77
|
+
response = redirect("/")
|
|
78
|
+
cookie_name = current_app.config.get('SESSION_COOKIE_NAME', 'session')
|
|
79
|
+
cookie_domain = current_app.config.get('SESSION_COOKIE_DOMAIN')
|
|
80
|
+
response.delete_cookie(cookie_name, path='/', domain=cookie_domain)
|
|
81
|
+
response.delete_cookie(cookie_name, path='/')
|
|
82
|
+
return response
|
|
78
83
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
from flask import Blueprint, render_template, redirect, request, g, Response, abort
|
|
2
|
-
from openwrite.utils.models import Blog, Post, User, View, Like, Page
|
|
1
|
+
from flask import Blueprint, render_template, redirect, request, g, Response, abort, send_file, jsonify
|
|
2
|
+
from openwrite.utils.models import Blog, Post, User, View, Like, Page, PostDraft, Comment
|
|
3
|
+
from openwrite.utils.tags import get_post_tags, parse_tags_from_posts_syntax, get_posts_by_tags
|
|
3
4
|
from openwrite.utils.helpers import gen_link, sanitize_html, anonymize, get_ip
|
|
4
5
|
from openwrite.utils.valkey_client import valkey_client
|
|
5
6
|
from feedgen.feed import FeedGenerator
|
|
@@ -12,6 +13,26 @@ from datetime import timezone, datetime
|
|
|
12
13
|
|
|
13
14
|
blog_bp = Blueprint("blog", __name__)
|
|
14
15
|
|
|
16
|
+
def process_posts_content(page, blog_id, blog_url, domain):
|
|
17
|
+
posts_matches = re.findall(r'{posts[^}]*}', page.content_raw)
|
|
18
|
+
if not posts_matches:
|
|
19
|
+
return [], page.content_html
|
|
20
|
+
|
|
21
|
+
posts_syntax = posts_matches[0]
|
|
22
|
+
tag_names = parse_tags_from_posts_syntax(posts_syntax)
|
|
23
|
+
posts = get_posts_by_tags(g.db, blog_id, tag_names)
|
|
24
|
+
|
|
25
|
+
posts_html = ""
|
|
26
|
+
for post in posts:
|
|
27
|
+
post_url = f"{blog_url}/{post.link}"
|
|
28
|
+
posts_html += f'<div class="postlist_item"><i class="datetime">{post.date}</i> - <a href="{post_url}">{post.title}</a></div>'
|
|
29
|
+
|
|
30
|
+
processed_content = page.content_html
|
|
31
|
+
for match in posts_matches:
|
|
32
|
+
processed_content = processed_content.replace(match, posts_html)
|
|
33
|
+
|
|
34
|
+
return posts, processed_content
|
|
35
|
+
|
|
15
36
|
def create_activity_object(post, blog, url, full_content=True):
|
|
16
37
|
"""Create ActivityPub object (Article or Note) based on post content length"""
|
|
17
38
|
dt = datetime.strptime(str(post.date), "%Y-%m-%d %H:%M:%S")
|
|
@@ -139,11 +160,9 @@ def show_blog(blog):
|
|
|
139
160
|
pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
|
|
140
161
|
blog_obj.url = f"/b/{blog_obj.name}"
|
|
141
162
|
homepage = g.db.query(Page).filter(Page.blog == blog_obj.id, Page.url == "").first()
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return render_template("blog.html", blog=blog_obj, page=homepage, pages=pages)
|
|
163
|
+
|
|
164
|
+
posts, processed_content = process_posts_content(homepage, blog_obj.id, blog_obj.url, g.main_domain)
|
|
165
|
+
return render_template("blog.html", blog=blog_obj, page=homepage, posts=posts, pages=pages, processed_content=processed_content)
|
|
147
166
|
|
|
148
167
|
@blog_bp.route("/", subdomain="<blog>")
|
|
149
168
|
def show_subblog(blog):
|
|
@@ -167,11 +186,9 @@ def show_subblog(blog):
|
|
|
167
186
|
blog_obj.url = f"https://{blog_obj.name}.{os.getenv('DOMAIN')}"
|
|
168
187
|
pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
|
|
169
188
|
homepage = g.db.query(Page).filter(Page.blog == blog_obj.id, Page.url == "").first()
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return render_template("blog.html", blog=blog_obj, page=homepage, pages=pages)
|
|
189
|
+
|
|
190
|
+
posts, processed_content = process_posts_content(homepage, blog_obj.id, blog_obj.url, g.main_domain)
|
|
191
|
+
return render_template("blog.html", blog=blog_obj, page=homepage, posts=posts, pages=pages, processed_content=processed_content)
|
|
175
192
|
|
|
176
193
|
@blog_bp.route("/b/<blog>/<post>")
|
|
177
194
|
def show_post(blog, post):
|
|
@@ -182,7 +199,7 @@ def show_post(blog, post):
|
|
|
182
199
|
return redirect("/")
|
|
183
200
|
|
|
184
201
|
if post == "rss":
|
|
185
|
-
return
|
|
202
|
+
return serve_rss(blog_obj)
|
|
186
203
|
|
|
187
204
|
if blog_obj.access == "domain":
|
|
188
205
|
return redirect(f"https://{blog_obj.name}.{os.getenv('DOMAIN')}/{post}")
|
|
@@ -196,11 +213,8 @@ def show_post(blog, post):
|
|
|
196
213
|
if not page:
|
|
197
214
|
return redirect("/")
|
|
198
215
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages)
|
|
202
|
-
|
|
203
|
-
return render_template("blog.html", blog=blog_obj, page=page, pages=pages)
|
|
216
|
+
posts, processed_content = process_posts_content(page, blog_obj.id, blog_obj.url, g.main_domain)
|
|
217
|
+
return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages, processed_content=processed_content)
|
|
204
218
|
|
|
205
219
|
if is_activitypub_request():
|
|
206
220
|
url = f"https://{g.main_domain}/b/{blog}"
|
|
@@ -240,8 +254,11 @@ def show_post(blog, post):
|
|
|
240
254
|
one_post.likes = likes
|
|
241
255
|
one_post.liked = liked
|
|
242
256
|
|
|
257
|
+
post_tags = get_post_tags(g.db, one_post)
|
|
258
|
+
comments = g.db.query(Comment).filter_by(blog_id=blog_obj.id, post_id=one_post.id).order_by(Comment.created_at.asc()).all() if one_post.comments_allowed == "1" else []
|
|
259
|
+
|
|
243
260
|
user = g.db.query(User).filter_by(id=g.user) if g.user else None
|
|
244
|
-
return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages)
|
|
261
|
+
return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages, post_tags=post_tags, comments=comments)
|
|
245
262
|
|
|
246
263
|
@blog_bp.route("/<post>", subdomain="<blog>")
|
|
247
264
|
def show_subpost(blog, post):
|
|
@@ -252,7 +269,7 @@ def show_subpost(blog, post):
|
|
|
252
269
|
return redirect(f"https://{os.getenv('DOMAIN')}/")
|
|
253
270
|
|
|
254
271
|
if post == "rss":
|
|
255
|
-
return
|
|
272
|
+
return serve_rss(blog_obj)
|
|
256
273
|
|
|
257
274
|
if blog_obj.access == "path":
|
|
258
275
|
return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog_obj.name}/{post}")
|
|
@@ -267,11 +284,8 @@ def show_subpost(blog, post):
|
|
|
267
284
|
if not page:
|
|
268
285
|
return redirect("/")
|
|
269
286
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages)
|
|
273
|
-
|
|
274
|
-
return render_template("blog.html", blog=blog_obj, page=page, pages=pages)
|
|
287
|
+
posts, processed_content = process_posts_content(page, blog_obj.id, blog_obj.url, g.main_domain)
|
|
288
|
+
return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages, processed_content=processed_content)
|
|
275
289
|
|
|
276
290
|
if is_activitypub_request():
|
|
277
291
|
url = f"https://{blog}.{g.main_domain}"
|
|
@@ -310,9 +324,12 @@ def show_subpost(blog, post):
|
|
|
310
324
|
|
|
311
325
|
one_post.likes = likes
|
|
312
326
|
one_post.liked = liked
|
|
327
|
+
|
|
328
|
+
post_tags = get_post_tags(g.db, one_post)
|
|
329
|
+
comments = g.db.query(Comment).filter_by(blog_id=blog_obj.id, post_id=one_post.id).order_by(Comment.created_at.asc()).all() if one_post.comments_allowed == "1" else []
|
|
313
330
|
|
|
314
331
|
user = g.db.query(User).filter_by(id=g.user) if g.user else None
|
|
315
|
-
return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages)
|
|
332
|
+
return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages, post_tags=post_tags, comments=comments)
|
|
316
333
|
|
|
317
334
|
@blog_bp.route("/.well-known/webfinger")
|
|
318
335
|
def webfinger():
|
|
@@ -382,12 +399,8 @@ def single_showpost(post):
|
|
|
382
399
|
if not page:
|
|
383
400
|
return redirect("/")
|
|
384
401
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages)
|
|
389
|
-
|
|
390
|
-
return render_template("blog.html", blog=blog, page=page, pages=pages)
|
|
402
|
+
posts, processed_content = process_posts_content(page, blog.id, blog.url, g.main_domain)
|
|
403
|
+
return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages, processed_content=processed_content)
|
|
391
404
|
|
|
392
405
|
if is_activitypub_request():
|
|
393
406
|
url = f"https://{g.main_domain}"
|
|
@@ -426,8 +439,11 @@ def single_showpost(post):
|
|
|
426
439
|
one_post.likes = likes
|
|
427
440
|
one_post.liked = liked
|
|
428
441
|
|
|
442
|
+
post_tags = get_post_tags(g.db, one_post)
|
|
443
|
+
comments = g.db.query(Comment).filter_by(blog_id=blog.id, post_id=one_post.id).order_by(Comment.created_at.asc()).all() if one_post.comments_allowed == "1" else []
|
|
444
|
+
|
|
429
445
|
user = g.db.query(User).filter_by(id=g.user) if g.user else None
|
|
430
|
-
return render_template("post.html", blog=blog, post=one_post, user=user, views=v, likes=likes, pages=pages)
|
|
446
|
+
return render_template("post.html", blog=blog, post=one_post, user=user, views=v, likes=likes, pages=pages, post_tags=post_tags, comments=comments)
|
|
431
447
|
|
|
432
448
|
@blog_bp.route("/rss")
|
|
433
449
|
def single_rss():
|
|
@@ -435,7 +451,7 @@ def single_rss():
|
|
|
435
451
|
return redirect("/")
|
|
436
452
|
|
|
437
453
|
blog = g.db.query(Blog).first()
|
|
438
|
-
return
|
|
454
|
+
return serve_rss(blog)
|
|
439
455
|
|
|
440
456
|
@blog_bp.route("/like", methods=["POST"])
|
|
441
457
|
def like():
|
|
@@ -483,29 +499,117 @@ def like():
|
|
|
483
499
|
status = 201
|
|
484
500
|
|
|
485
501
|
return resp, status
|
|
486
|
-
|
|
487
502
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def serve_rss(blog):
|
|
506
|
+
"""
|
|
507
|
+
Here we just serve static RSS files :)
|
|
508
|
+
"""
|
|
509
|
+
return send_file(f"{g.mainpath}/static/feed/{blog.name}.xml")
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
@blog_bp.route("/comment", methods=['POST'])
|
|
513
|
+
def add_comment():
|
|
514
|
+
blog_id = request.form.get('blog_id', type=int)
|
|
515
|
+
post_id = request.form.get('post_id', type=int)
|
|
516
|
+
author_name = request.form.get('author_name', '').strip()[:50]
|
|
517
|
+
content = request.form.get('content', '').strip()[:1000]
|
|
518
|
+
redirect_to = request.referrer or '/'
|
|
519
|
+
|
|
520
|
+
if not blog_id or not post_id:
|
|
521
|
+
abort(400)
|
|
522
|
+
|
|
523
|
+
post = g.db.query(Post).filter_by(id=post_id, blog=blog_id).first()
|
|
524
|
+
if not post or post.comments_allowed != '1':
|
|
525
|
+
abort(403)
|
|
526
|
+
|
|
527
|
+
if not author_name or not content:
|
|
528
|
+
return redirect(redirect_to)
|
|
529
|
+
|
|
530
|
+
now = datetime.now(timezone.utc).replace(microsecond=0)
|
|
531
|
+
comment = Comment(blog_id=blog_id, post_id=post_id, author_name=author_name, content=content, created_at=now)
|
|
532
|
+
g.db.add(comment)
|
|
533
|
+
g.db.commit()
|
|
534
|
+
return redirect(redirect_to)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@blog_bp.route("/comment/<int:comment_id>/delete", methods=['POST'])
|
|
538
|
+
def delete_comment(comment_id):
|
|
539
|
+
if g.user is None:
|
|
540
|
+
abort(403)
|
|
541
|
+
|
|
542
|
+
comment = g.db.query(Comment).filter_by(id=comment_id).first()
|
|
543
|
+
if not comment:
|
|
544
|
+
abort(404)
|
|
545
|
+
|
|
546
|
+
blog = g.db.query(Blog).filter_by(id=comment.blog_id).first()
|
|
547
|
+
if not blog or blog.owner != g.user:
|
|
548
|
+
abort(403)
|
|
549
|
+
|
|
550
|
+
redirect_to = request.referrer or '/'
|
|
551
|
+
g.db.delete(comment)
|
|
552
|
+
g.db.commit()
|
|
553
|
+
return redirect(redirect_to)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
@blog_bp.route("/dashboard/autosave/<blog_name>", methods=['POST'])
|
|
557
|
+
def autosave_post(blog_name):
|
|
558
|
+
if g.user is None:
|
|
559
|
+
abort(403)
|
|
560
|
+
|
|
561
|
+
blog = g.db.query(Blog).filter_by(name=blog_name).first()
|
|
562
|
+
if blog is None or blog.owner != g.user:
|
|
563
|
+
abort(403)
|
|
564
|
+
|
|
565
|
+
data = request.get_json()
|
|
566
|
+
if not data:
|
|
567
|
+
abort(400)
|
|
568
|
+
|
|
569
|
+
title = data.get('title', '')[:128]
|
|
570
|
+
content_raw = data.get('content_raw', '')
|
|
571
|
+
post_id = data.get('post_id')
|
|
572
|
+
session_key = data.get('session_key') if post_id is None else None
|
|
573
|
+
|
|
574
|
+
now = datetime.now(timezone.utc).replace(microsecond=0)
|
|
575
|
+
draft = PostDraft(post_id=post_id, blog_id=blog.id, session_key=session_key, title=title, content_raw=content_raw, saved_at=now)
|
|
576
|
+
g.db.add(draft)
|
|
577
|
+
g.db.flush()
|
|
578
|
+
|
|
579
|
+
if post_id is not None:
|
|
580
|
+
existing = g.db.query(PostDraft).filter_by(post_id=post_id, blog_id=blog.id).order_by(PostDraft.saved_at.desc()).all()
|
|
496
581
|
else:
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
582
|
+
existing = g.db.query(PostDraft).filter_by(session_key=session_key, blog_id=blog.id).order_by(PostDraft.saved_at.desc()).all()
|
|
583
|
+
|
|
584
|
+
for old in existing[20:]:
|
|
585
|
+
g.db.delete(old)
|
|
586
|
+
|
|
587
|
+
g.db.commit()
|
|
588
|
+
return {'status': 'ok'}, 200
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
@blog_bp.route("/dashboard/autosaves/<blog_name>", methods=['GET'])
|
|
592
|
+
def get_autosaves(blog_name):
|
|
593
|
+
if g.user is None:
|
|
594
|
+
abort(403)
|
|
595
|
+
|
|
596
|
+
blog = g.db.query(Blog).filter_by(name=blog_name).first()
|
|
597
|
+
if blog is None or blog.owner != g.user:
|
|
598
|
+
abort(403)
|
|
599
|
+
|
|
600
|
+
post_id = request.args.get('post_id', type=int)
|
|
601
|
+
session_key = request.args.get('session_key')
|
|
602
|
+
|
|
603
|
+
if post_id is not None:
|
|
604
|
+
drafts = g.db.query(PostDraft).filter_by(post_id=post_id, blog_id=blog.id).order_by(PostDraft.saved_at.desc()).limit(20).all()
|
|
605
|
+
elif session_key:
|
|
606
|
+
drafts = g.db.query(PostDraft).filter_by(session_key=session_key, blog_id=blog.id).order_by(PostDraft.saved_at.desc()).limit(20).all()
|
|
607
|
+
else:
|
|
608
|
+
drafts = []
|
|
609
|
+
|
|
610
|
+
return jsonify([{
|
|
611
|
+
'id': d.id,
|
|
612
|
+
'title': d.title,
|
|
613
|
+
'content_raw': d.content_raw,
|
|
614
|
+
'saved_at': d.saved_at.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
615
|
+
} for d in drafts])
|