openwrite 0.13.4__tar.gz → 0.15.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.13.4/openwrite.egg-info → openwrite-0.15.0}/PKG-INFO +5 -2
- {openwrite-0.13.4 → openwrite-0.15.0}/README.md +3 -1
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/__init__.py +11 -11
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/cli.py +28 -6
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/gemini_multi.py +21 -1
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/routes/auth.py +1 -1
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/routes/blog.py +133 -93
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/routes/dashboard.py +119 -69
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/routes/main.py +29 -6
- openwrite-0.15.0/openwrite/static/favicon.png +0 -0
- openwrite-0.15.0/openwrite/static/feed/.gitkeep +0 -0
- openwrite-0.15.0/openwrite/static/feed/74.xml +12 -0
- openwrite-0.15.0/openwrite/static/feed/a.xml +2 -0
- openwrite-0.15.0/openwrite/static/feed/anim3sh1t.xml +12 -0
- openwrite-0.15.0/openwrite/static/feed/b.xml +2 -0
- openwrite-0.15.0/openwrite/static/feed/corrode.xml +4 -0
- openwrite-0.15.0/openwrite/static/feed/dirtysanchez.xml +6 -0
- openwrite-0.15.0/openwrite/static/feed/eikerammen.xml +165 -0
- openwrite-0.15.0/openwrite/static/feed/fedi.xml +9 -0
- openwrite-0.15.0/openwrite/static/feed/friskt.xml +2 -0
- openwrite-0.15.0/openwrite/static/feed/gordi.xml +6 -0
- openwrite-0.15.0/openwrite/static/feed/igilq.xml +226 -0
- openwrite-0.15.0/openwrite/static/feed/io.xml +50 -0
- openwrite-0.15.0/openwrite/static/feed/isyuricunha.xml +4 -0
- openwrite-0.15.0/openwrite/static/feed/kenny.xml +2 -0
- openwrite-0.15.0/openwrite/static/feed/m0bi.xml +6 -0
- openwrite-0.15.0/openwrite/static/feed/main.xml +236 -0
- openwrite-0.15.0/openwrite/static/feed/mywritese.xml +2 -0
- openwrite-0.15.0/openwrite/static/feed/nay-thoughs.xml +2 -0
- openwrite-0.15.0/openwrite/static/feed/nintendoflesz.xml +7 -0
- openwrite-0.15.0/openwrite/static/feed/phronima.xml +68 -0
- openwrite-0.15.0/openwrite/static/feed/ram.xml +75 -0
- openwrite-0.15.0/openwrite/static/feed/samah.xml +2 -0
- openwrite-0.15.0/openwrite/static/feed/themes-test.xml +18 -0
- openwrite-0.15.0/openwrite/static/feed/totylkoja.xml +78 -0
- openwrite-0.15.0/openwrite/static/feed/vegafjord.xml +48 -0
- openwrite-0.15.0/openwrite/static/feed/vijayvaradharaj.xml +2 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/style.css +400 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/blog.html +3 -1
- openwrite-0.15.0/openwrite/templates/dashboard.html +68 -0
- openwrite-0.15.0/openwrite/templates/discover.html +23 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/edit.html +44 -37
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/footer.html +1 -1
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/new_page.html +1 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/new_post.html +54 -2
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/post.html +9 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/stats.html +26 -0
- openwrite-0.15.0/openwrite/utils/create_db.py +28 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/utils/helpers.py +3 -2
- openwrite-0.15.0/openwrite/utils/i18n.json +1 -0
- openwrite-0.15.0/openwrite/utils/migrations.py +136 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/utils/models.py +26 -1
- openwrite-0.15.0/openwrite/utils/tags.py +242 -0
- openwrite-0.15.0/openwrite/utils/valkey_client.py +161 -0
- openwrite-0.15.0/openwrite/utils/worker.py +134 -0
- {openwrite-0.13.4 → openwrite-0.15.0/openwrite.egg-info}/PKG-INFO +5 -2
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite.egg-info/SOURCES.txt +33 -1
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite.egg-info/requires.txt +1 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/pyproject.toml +3 -2
- openwrite-0.13.4/openwrite/templates/dashboard.html +0 -48
- openwrite-0.13.4/openwrite/templates/discover.html +0 -11
- openwrite-0.13.4/openwrite/utils/create_db.py +0 -15
- openwrite-0.13.4/openwrite/utils/i18n.json +0 -1
- {openwrite-0.13.4 → openwrite-0.15.0}/LICENSE +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/MANIFEST.in +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/db/__init__.py +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/db/base.py +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/gemini_single.py +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/routes/admin.py +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/routes/federation.py +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/routes/upload.py +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/IBMPlexSans-Regular.ttf +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/Merriweather.ttf +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/avatar.png +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/emoji.json +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/favicon.ico +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/logo.png +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/chart.js +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/highlight.min.js +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/nativeEmoji.js +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/openwrite.js +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/papaparse.min.js +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/showdown.min.js +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/showdown.min.js.map +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/site.compat.min.js +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/script/site.min.js +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/blog_design.css +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/default.css +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/hl.css +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/nativeEmoji.min.css +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/themes/cyberpunk.css +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/themes/light.css +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/themes/obscura.css +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/themes/paper.css +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/themes/stickynote.css +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/themes/terminal.css +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/themes/warmnight.css +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/static/style/themes/windows98.css +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/admin.html +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/blog_footer.html +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/blog_header.html +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/blog_preview.html +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/changepw.html +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/create.html +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/header.html +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/import.html +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/index.html +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/instances.html +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/login.html +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/page_preview.html +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/preview.html +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/register.html +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/settings.html +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/templates/translations.html +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite/utils/db.py +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite.egg-info/dependency_links.txt +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite.egg-info/entry_points.txt +0 -0
- {openwrite-0.13.4 → openwrite-0.15.0}/openwrite.egg-info/top_level.txt +0 -0
- {openwrite-0.13.4 → openwrite-0.15.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.15.0
|
|
4
4
|
License: GNU AFFERO GENERAL PUBLIC LICENSE
|
|
5
5
|
Version 3, 19 November 2007
|
|
6
6
|
|
|
@@ -685,6 +685,7 @@ Requires-Dist: markdown
|
|
|
685
685
|
Requires-Dist: md2gemini
|
|
686
686
|
Requires-Dist: gmcapsule
|
|
687
687
|
Requires-Dist: user-agents
|
|
688
|
+
Requires-Dist: redis
|
|
688
689
|
Dynamic: license-file
|
|
689
690
|
|
|
690
691
|

|
|
@@ -714,7 +715,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
|
|
|
714
715
|
- Lightweight
|
|
715
716
|
- No tracking, only data collected is anonymized(hashed) IP for post view counting and likes
|
|
716
717
|
- Custom CSS per blog
|
|
717
|
-
- Federation using ActivityPub protocol
|
|
718
|
+
- Federation using ActivityPub protocol
|
|
718
719
|
- Likes system
|
|
719
720
|
- Posts importing from xml/csv
|
|
720
721
|
- Blog themes
|
|
@@ -723,6 +724,8 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
|
|
|
723
724
|
- Multilanguage (currently English and Polish)
|
|
724
725
|
- Custom favicon per blog
|
|
725
726
|
- Subpages per blog
|
|
727
|
+
- Valkey support
|
|
728
|
+
- Post tags, filtering
|
|
726
729
|
|
|
727
730
|
|
|
728
731
|
## Installation
|
|
@@ -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,8 @@ 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
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
## 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)
|
|
@@ -171,14 +175,10 @@ def create_app(test_config=None):
|
|
|
171
175
|
from .routes.main import main_bp
|
|
172
176
|
from .routes.upload import upload_bp
|
|
173
177
|
from .routes.federation import federation_bp
|
|
174
|
-
from .utils.db import init_engine, SessionLocal
|
|
175
178
|
from flask_cors import CORS
|
|
176
179
|
|
|
177
180
|
CORS(app)
|
|
178
181
|
|
|
179
|
-
init_engine(db_type, db_path)
|
|
180
|
-
from .utils.db import SessionLocal
|
|
181
|
-
|
|
182
182
|
app.register_blueprint(auth_bp)
|
|
183
183
|
app.register_blueprint(dashboard_bp)
|
|
184
184
|
app.register_blueprint(blog_bp)
|
|
@@ -212,7 +212,7 @@ def create_app(test_config=None):
|
|
|
212
212
|
|
|
213
213
|
trans = g.db.query(Home).filter(Home.type == "translation", Home.language == lang).all()
|
|
214
214
|
|
|
215
|
-
g.version = "0.
|
|
215
|
+
g.version = "0.15.0"
|
|
216
216
|
g.mode = os.getenv("MODE")
|
|
217
217
|
f_abs_path = os.path.abspath(__file__)
|
|
218
218
|
g.mainpath = "/".join(f_abs_path.split("/")[:-1])
|
|
@@ -100,6 +100,13 @@ def init():
|
|
|
100
100
|
if captcha_enabled:
|
|
101
101
|
captcha_sitekey = click.prompt("Friendly captcha sitekey")
|
|
102
102
|
captcha_apikey = click.prompt("Friendly captcha API key")
|
|
103
|
+
valkey_enabled = click.confirm("Enable valkey (queue for likes and views)?", default=False)
|
|
104
|
+
if valkey_enabled:
|
|
105
|
+
valkey_host = click.prompt("Valkey host", default="localhost")
|
|
106
|
+
valkey_port = click.prompt("Valkey port", default="6379")
|
|
107
|
+
valkey_password = click.prompt("Valkey password(optional)", default="")
|
|
108
|
+
valkey_db = click.prompt("Valkey database", default="0")
|
|
109
|
+
valkey_interval = click.prompt("How often should valkey refresh data to DB? (minutes)", default="5")
|
|
103
110
|
|
|
104
111
|
|
|
105
112
|
|
|
@@ -145,6 +152,13 @@ def init():
|
|
|
145
152
|
if captcha_enabled:
|
|
146
153
|
f.write(f"FRIENDLY_CAPTCHA_SITEKEY={captcha_sitekey}\n")
|
|
147
154
|
f.write(f"FRIENDLY_CAPTCHA_APIKEY={captcha_apikey}\n")
|
|
155
|
+
if valkey_enabled:
|
|
156
|
+
f.write(f"VALKEY_ENABLED={'yes' if valkey_enabled else 'no'}\n")
|
|
157
|
+
f.write(f"VALKEY_HOST={valkey_host}\n")
|
|
158
|
+
f.write(f"VALKEY_PORT={valkey_port}\n")
|
|
159
|
+
f.write(f"VALKEY_PASSWORD={valkey_password}\n")
|
|
160
|
+
f.write(f"VALKEY_DB={valkey_db}\n")
|
|
161
|
+
f.write(f"VALKEY_INTERVAL={valkey_interval}\n")
|
|
148
162
|
|
|
149
163
|
click.echo("[+] .env file created")
|
|
150
164
|
if logs_enabled and not os.path.exists(logs_dir):
|
|
@@ -185,7 +199,12 @@ def run(daemon):
|
|
|
185
199
|
"gmcapsuled", "-c", f"{cwd}/gemini.ini"
|
|
186
200
|
]
|
|
187
201
|
|
|
202
|
+
worker_cmd = [
|
|
203
|
+
sys.executable, "-m", "openwrite.utils.worker"
|
|
204
|
+
]
|
|
205
|
+
|
|
188
206
|
gemini_log = open(os.path.join(logs_dir, "gemini.log"), "a") if logs_enabled else subprocess.PIPE
|
|
207
|
+
worker_log = open(os.path.join(logs_dir, "worker.log"), "a") if logs_enabled else subprocess.PIPE
|
|
189
208
|
|
|
190
209
|
if gemini_proxy:
|
|
191
210
|
gemini_cmd = gemini_cmd + ["proxy"]
|
|
@@ -193,33 +212,37 @@ def run(daemon):
|
|
|
193
212
|
if logs_enabled:
|
|
194
213
|
gunicorn_cmd = gunicorn_cmd + gunicorn_logs
|
|
195
214
|
|
|
196
|
-
|
|
197
215
|
if daemon:
|
|
198
216
|
click.echo(f"[+] Openwrite listening on {ip}:{port}")
|
|
199
217
|
subprocess.Popen(gunicorn_cmd + ["--daemon"])
|
|
200
218
|
|
|
201
219
|
if gemini:
|
|
202
220
|
click.echo(f"[+] Gemini listening on {ip}:{gemini_port}")
|
|
203
|
-
|
|
204
221
|
subprocess.Popen(gemini_cmd, stdout=gemini_log, stderr=subprocess.STDOUT)
|
|
205
222
|
|
|
223
|
+
click.echo("[+] Starting worker process")
|
|
224
|
+
subprocess.Popen(worker_cmd, stdout=worker_log, stderr=subprocess.STDOUT)
|
|
225
|
+
|
|
206
226
|
else:
|
|
207
|
-
gunicorn_proc = subprocess.Popen(
|
|
208
|
-
gunicorn_cmd
|
|
209
|
-
)
|
|
227
|
+
gunicorn_proc = subprocess.Popen(gunicorn_cmd)
|
|
210
228
|
|
|
211
229
|
if gemini:
|
|
212
230
|
click.echo(f"[+] Gemini started on {ip}:{gemini_port}")
|
|
213
231
|
gemini_proc = subprocess.Popen(gemini_cmd, stdout=gemini_log)
|
|
214
232
|
|
|
233
|
+
click.echo("[+] Starting worker process")
|
|
234
|
+
worker_proc = subprocess.Popen(worker_cmd, stdout=worker_log)
|
|
235
|
+
|
|
215
236
|
try:
|
|
216
237
|
gunicorn_proc.wait()
|
|
217
238
|
if gemini:
|
|
218
239
|
gemini_proc.wait()
|
|
240
|
+
worker_proc.wait()
|
|
219
241
|
except KeyboardInterrupt:
|
|
220
242
|
gunicorn_proc.terminate()
|
|
221
243
|
if gemini:
|
|
222
244
|
gemini_proc.terminate()
|
|
245
|
+
worker_proc.terminate()
|
|
223
246
|
|
|
224
247
|
@cli.command()
|
|
225
248
|
def debugrun():
|
|
@@ -286,4 +309,3 @@ def stop(service):
|
|
|
286
309
|
|
|
287
310
|
if __name__ == "__main__":
|
|
288
311
|
cli()
|
|
289
|
-
|
|
@@ -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')
|
|
@@ -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')
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
from flask import Blueprint, render_template, redirect, request, g, Response, abort
|
|
1
|
+
from flask import Blueprint, render_template, redirect, request, g, Response, abort, send_file
|
|
2
2
|
from openwrite.utils.models import Blog, Post, User, View, Like, Page
|
|
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
|
|
5
|
+
from openwrite.utils.valkey_client import valkey_client
|
|
4
6
|
from feedgen.feed import FeedGenerator
|
|
5
7
|
from sqlalchemy import desc
|
|
6
8
|
import os
|
|
@@ -11,6 +13,26 @@ from datetime import timezone, datetime
|
|
|
11
13
|
|
|
12
14
|
blog_bp = Blueprint("blog", __name__)
|
|
13
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
|
+
|
|
14
36
|
def create_activity_object(post, blog, url, full_content=True):
|
|
15
37
|
"""Create ActivityPub object (Article or Note) based on post content length"""
|
|
16
38
|
dt = datetime.strptime(str(post.date), "%Y-%m-%d %H:%M:%S")
|
|
@@ -138,11 +160,9 @@ def show_blog(blog):
|
|
|
138
160
|
pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
|
|
139
161
|
blog_obj.url = f"/b/{blog_obj.name}"
|
|
140
162
|
homepage = g.db.query(Page).filter(Page.blog == blog_obj.id, Page.url == "").first()
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
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)
|
|
146
166
|
|
|
147
167
|
@blog_bp.route("/", subdomain="<blog>")
|
|
148
168
|
def show_subblog(blog):
|
|
@@ -166,11 +186,9 @@ def show_subblog(blog):
|
|
|
166
186
|
blog_obj.url = f"https://{blog_obj.name}.{os.getenv('DOMAIN')}"
|
|
167
187
|
pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
|
|
168
188
|
homepage = g.db.query(Page).filter(Page.blog == blog_obj.id, Page.url == "").first()
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
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)
|
|
174
192
|
|
|
175
193
|
@blog_bp.route("/b/<blog>/<post>")
|
|
176
194
|
def show_post(blog, post):
|
|
@@ -181,7 +199,7 @@ def show_post(blog, post):
|
|
|
181
199
|
return redirect("/")
|
|
182
200
|
|
|
183
201
|
if post == "rss":
|
|
184
|
-
return
|
|
202
|
+
return serve_rss(blog_obj)
|
|
185
203
|
|
|
186
204
|
if blog_obj.access == "domain":
|
|
187
205
|
return redirect(f"https://{blog_obj.name}.{os.getenv('DOMAIN')}/{post}")
|
|
@@ -195,11 +213,8 @@ def show_post(blog, post):
|
|
|
195
213
|
if not page:
|
|
196
214
|
return redirect("/")
|
|
197
215
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages)
|
|
201
|
-
|
|
202
|
-
return render_template("blog.html", blog=blog_obj, page=page, pages=pages)
|
|
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)
|
|
203
218
|
|
|
204
219
|
if is_activitypub_request():
|
|
205
220
|
url = f"https://{g.main_domain}/b/{blog}"
|
|
@@ -215,22 +230,34 @@ def show_post(blog, post):
|
|
|
215
230
|
blog_obj.url = f"/b/{blog_obj.name}"
|
|
216
231
|
|
|
217
232
|
user_agent = request.headers.get('User-Agent')
|
|
233
|
+
ip = anonymize(get_ip())
|
|
218
234
|
|
|
219
235
|
if 'Bot' not in user_agent:
|
|
220
|
-
ip = anonymize(get_ip())
|
|
221
236
|
v = g.db.query(View).filter(View.blog == blog_obj.id, View.post == one_post.id, View.hash == ip).count()
|
|
222
237
|
if v < 1:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
238
|
+
if valkey_client.is_enabled():
|
|
239
|
+
valkey_client.queue_view(blog_obj.id, one_post.id, ip, user_agent)
|
|
240
|
+
else:
|
|
241
|
+
now = datetime.now(timezone.utc).replace(microsecond=0)
|
|
242
|
+
new_view = View(blog=blog_obj.id, post=one_post.id, hash=ip, date=now, agent=user_agent)
|
|
243
|
+
g.db.add(new_view)
|
|
244
|
+
g.db.commit()
|
|
245
|
+
|
|
246
|
+
likes_db = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id).count()
|
|
247
|
+
likes_queued = valkey_client.count_queued_likes(blog_obj.id, one_post.id) if valkey_client.is_enabled() else 0
|
|
248
|
+
likes = likes_db + likes_queued
|
|
249
|
+
|
|
250
|
+
liked_db = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id, Like.hash == ip).count()
|
|
251
|
+
liked_queued = valkey_client.check_queued_like(blog_obj.id, one_post.id, ip) if valkey_client.is_enabled() else False
|
|
252
|
+
liked = liked_db or liked_queued
|
|
253
|
+
|
|
254
|
+
one_post.likes = likes
|
|
255
|
+
one_post.liked = liked
|
|
256
|
+
|
|
257
|
+
post_tags = get_post_tags(g.db, one_post)
|
|
231
258
|
|
|
232
259
|
user = g.db.query(User).filter_by(id=g.user) if g.user else None
|
|
233
|
-
return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages)
|
|
260
|
+
return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages, post_tags=post_tags)
|
|
234
261
|
|
|
235
262
|
@blog_bp.route("/<post>", subdomain="<blog>")
|
|
236
263
|
def show_subpost(blog, post):
|
|
@@ -241,7 +268,7 @@ def show_subpost(blog, post):
|
|
|
241
268
|
return redirect(f"https://{os.getenv('DOMAIN')}/")
|
|
242
269
|
|
|
243
270
|
if post == "rss":
|
|
244
|
-
return
|
|
271
|
+
return serve_rss(blog_obj)
|
|
245
272
|
|
|
246
273
|
if blog_obj.access == "path":
|
|
247
274
|
return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog_obj.name}/{post}")
|
|
@@ -256,11 +283,8 @@ def show_subpost(blog, post):
|
|
|
256
283
|
if not page:
|
|
257
284
|
return redirect("/")
|
|
258
285
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages)
|
|
262
|
-
|
|
263
|
-
return render_template("blog.html", blog=blog_obj, page=page, pages=pages)
|
|
286
|
+
posts, processed_content = process_posts_content(page, blog_obj.id, blog_obj.url, g.main_domain)
|
|
287
|
+
return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages, processed_content=processed_content)
|
|
264
288
|
|
|
265
289
|
if is_activitypub_request():
|
|
266
290
|
url = f"https://{blog}.{g.main_domain}"
|
|
@@ -275,24 +299,35 @@ def show_subpost(blog, post):
|
|
|
275
299
|
one_post.authorname = post_author.username
|
|
276
300
|
blog_obj.url = f"https://{blog_obj.name}.{os.getenv('DOMAIN')}"
|
|
277
301
|
|
|
278
|
-
user_agent = request.headers.get('User-Agent'
|
|
302
|
+
user_agent = request.headers.get('User-Agent')
|
|
303
|
+
ip = anonymize(get_ip())
|
|
304
|
+
|
|
279
305
|
if 'Bot' not in user_agent:
|
|
280
|
-
ip = anonymize(get_ip())
|
|
281
306
|
v = g.db.query(View).filter(View.blog == blog_obj.id, View.post == one_post.id, View.hash == ip).count()
|
|
282
|
-
user_agent = request.headers.get('User-Agent')
|
|
283
307
|
if v < 1:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
308
|
+
if valkey_client.is_enabled():
|
|
309
|
+
valkey_client.queue_view(blog_obj.id, one_post.id, ip, user_agent)
|
|
310
|
+
else:
|
|
311
|
+
now = datetime.now(timezone.utc).replace(microsecond=0)
|
|
312
|
+
new_view = View(blog=blog_obj.id, post=one_post.id, hash=ip, date=now, agent=user_agent)
|
|
313
|
+
g.db.add(new_view)
|
|
314
|
+
g.db.commit()
|
|
315
|
+
|
|
316
|
+
likes_db = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id).count()
|
|
317
|
+
likes_queued = valkey_client.count_queued_likes(blog_obj.id, one_post.id) if valkey_client.is_enabled() else 0
|
|
318
|
+
likes = likes_db + likes_queued
|
|
319
|
+
|
|
320
|
+
liked_db = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id, Like.hash == ip).count()
|
|
321
|
+
liked_queued = valkey_client.check_queued_like(blog_obj.id, one_post.id, ip) if valkey_client.is_enabled() else False
|
|
322
|
+
liked = liked_db or liked_queued
|
|
288
323
|
|
|
289
|
-
likes = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id).count()
|
|
290
324
|
one_post.likes = likes
|
|
291
|
-
liked = g.db.query(Like).filter(Like.blog == blog_obj.id, Like.post == one_post.id, Like.hash == anonymize(get_ip())).count()
|
|
292
325
|
one_post.liked = liked
|
|
326
|
+
|
|
327
|
+
post_tags = get_post_tags(g.db, one_post)
|
|
293
328
|
|
|
294
329
|
user = g.db.query(User).filter_by(id=g.user) if g.user else None
|
|
295
|
-
return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, pages=pages)
|
|
330
|
+
return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages, post_tags=post_tags)
|
|
296
331
|
|
|
297
332
|
@blog_bp.route("/.well-known/webfinger")
|
|
298
333
|
def webfinger():
|
|
@@ -362,12 +397,8 @@ def single_showpost(post):
|
|
|
362
397
|
if not page:
|
|
363
398
|
return redirect("/")
|
|
364
399
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages)
|
|
369
|
-
|
|
370
|
-
return render_template("blog.html", blog=blog, page=page, pages=pages)
|
|
400
|
+
posts, processed_content = process_posts_content(page, blog.id, blog.url, g.main_domain)
|
|
401
|
+
return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages, processed_content=processed_content)
|
|
371
402
|
|
|
372
403
|
if is_activitypub_request():
|
|
373
404
|
url = f"https://{g.main_domain}"
|
|
@@ -382,23 +413,34 @@ def single_showpost(post):
|
|
|
382
413
|
one_post.authorname = post_author.username
|
|
383
414
|
|
|
384
415
|
user_agent = request.headers.get('User-Agent', '')
|
|
416
|
+
ip = anonymize(get_ip())
|
|
417
|
+
|
|
385
418
|
if 'Bot' not in user_agent:
|
|
386
|
-
|
|
387
|
-
v = g.db.query(View).filter(View.blog == 1, View.post == one_post.id, View.hash == ip).count()
|
|
388
|
-
user_agent = request.headers.get('User-Agent')
|
|
419
|
+
v = g.db.query(View).filter(View.blog == blog.id, View.post == one_post.id, View.hash == ip).count()
|
|
389
420
|
if v < 1:
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
421
|
+
if valkey_client.is_enabled():
|
|
422
|
+
valkey_client.queue_view(blog.id, one_post.id, ip, user_agent)
|
|
423
|
+
else:
|
|
424
|
+
now = datetime.now(timezone.utc).replace(microsecond=0)
|
|
425
|
+
new_view = View(blog=blog.id, post=one_post.id, hash=ip, date=now, agent=user_agent)
|
|
426
|
+
g.db.add(new_view)
|
|
427
|
+
g.db.commit()
|
|
428
|
+
|
|
429
|
+
likes_db = g.db.query(Like).filter(Like.blog == blog.id, Like.post == one_post.id).count()
|
|
430
|
+
likes_queued = valkey_client.count_queued_likes(blog.id, one_post.id) if valkey_client.is_enabled() else 0
|
|
431
|
+
likes = likes_db + likes_queued
|
|
432
|
+
|
|
433
|
+
liked_db = g.db.query(Like).filter(Like.blog == blog.id, Like.post == one_post.id, Like.hash == ip).count()
|
|
434
|
+
liked_queued = valkey_client.check_queued_like(blog.id, one_post.id, ip) if valkey_client.is_enabled() else False
|
|
435
|
+
liked = liked_db or liked_queued
|
|
394
436
|
|
|
395
|
-
likes = g.db.query(Like).filter(Like.blog == 1, Like.post == one_post.id).count()
|
|
396
437
|
one_post.likes = likes
|
|
397
|
-
liked = g.db.query(Like).filter(Like.blog == 1, Like.post == one_post.id, Like.hash == anonymize(get_ip())).count()
|
|
398
438
|
one_post.liked = liked
|
|
399
439
|
|
|
440
|
+
post_tags = get_post_tags(g.db, one_post)
|
|
441
|
+
|
|
400
442
|
user = g.db.query(User).filter_by(id=g.user) if g.user else None
|
|
401
|
-
return render_template("post.html", blog=blog, post=one_post, user=user, views=v, pages=pages)
|
|
443
|
+
return render_template("post.html", blog=blog, post=one_post, user=user, views=v, likes=likes, pages=pages, post_tags=post_tags)
|
|
402
444
|
|
|
403
445
|
@blog_bp.route("/rss")
|
|
404
446
|
def single_rss():
|
|
@@ -406,7 +448,7 @@ def single_rss():
|
|
|
406
448
|
return redirect("/")
|
|
407
449
|
|
|
408
450
|
blog = g.db.query(Blog).first()
|
|
409
|
-
return
|
|
451
|
+
return serve_rss(blog)
|
|
410
452
|
|
|
411
453
|
@blog_bp.route("/like", methods=["POST"])
|
|
412
454
|
def like():
|
|
@@ -420,47 +462,45 @@ def like():
|
|
|
420
462
|
if post < 1:
|
|
421
463
|
resp = {"status": "no_post"}
|
|
422
464
|
return resp, 404
|
|
465
|
+
|
|
423
466
|
ip = anonymize(get_ip())
|
|
424
467
|
|
|
425
|
-
|
|
426
|
-
if
|
|
427
|
-
|
|
428
|
-
|
|
468
|
+
liked_db = g.db.query(Like).filter(Like.blog == blog_id, Like.post == post_id, Like.hash == ip).first()
|
|
469
|
+
liked_queued = valkey_client.check_queued_like(blog_id, post_id, ip) if valkey_client.is_enabled() else False
|
|
470
|
+
|
|
471
|
+
if liked_db:
|
|
472
|
+
if liked_db and valkey_client.is_enabled():
|
|
473
|
+
valkey_client.queue_like(blog_id, post_id, ip, 'remove')
|
|
474
|
+
elif liked_db and not valkey_client.is_enabled():
|
|
475
|
+
g.db.delete(liked_db)
|
|
476
|
+
g.db.commit()
|
|
477
|
+
|
|
478
|
+
resp = {"status": "deleted"}
|
|
479
|
+
status = 204
|
|
480
|
+
|
|
481
|
+
elif liked_queued and valkey_client.is_enabled():
|
|
482
|
+
valkey_client.queue_like(blog_id, post_id, ip, 'remove')
|
|
483
|
+
|
|
429
484
|
resp = {"status": "deleted"}
|
|
430
485
|
status = 204
|
|
431
486
|
else:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
487
|
+
if valkey_client.is_enabled():
|
|
488
|
+
valkey_client.queue_like(blog_id, post_id, ip, 'add')
|
|
489
|
+
else:
|
|
490
|
+
now = datetime.now(timezone.utc).replace(microsecond=0)
|
|
491
|
+
like = Like(blog=blog_id, post=post_id, hash=ip, date=now)
|
|
492
|
+
g.db.add(like)
|
|
493
|
+
g.db.commit()
|
|
494
|
+
|
|
436
495
|
resp = {"status": "ok"}
|
|
437
496
|
status = 201
|
|
438
|
-
return resp, status
|
|
439
497
|
|
|
498
|
+
return resp, status
|
|
440
499
|
|
|
441
|
-
def _generate_rss(blog):
|
|
442
|
-
posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").all()
|
|
443
|
-
fg = FeedGenerator()
|
|
444
|
-
fg.title(blog.title)
|
|
445
|
-
|
|
446
|
-
if blog.access == "domain":
|
|
447
|
-
blog_url = f"https://{blog.name}.{g.main_domain}"
|
|
448
|
-
post_base_url = blog_url
|
|
449
|
-
else:
|
|
450
|
-
blog_url = f"https://{g.main_domain}/b/{blog.name}"
|
|
451
|
-
post_base_url = blog_url
|
|
452
|
-
|
|
453
|
-
fg.link(href=blog_url, rel="alternate")
|
|
454
|
-
fg.description(blog.description_html)
|
|
455
|
-
|
|
456
|
-
for p in posts:
|
|
457
|
-
soup = BeautifulSoup(p.content_html, "html.parser")
|
|
458
|
-
fe = fg.add_entry()
|
|
459
|
-
fe.title(p.title)
|
|
460
|
-
fe.link(href=f"{post_base_url}/{p.link}")
|
|
461
|
-
fe.description(soup.get_text())
|
|
462
|
-
fe.published(p.date.replace(tzinfo=timezone.utc))
|
|
463
|
-
|
|
464
|
-
return Response(fg.rss_str(pretty=True).decode("utf-8"), mimetype="application/rss+xml")
|
|
465
500
|
|
|
466
501
|
|
|
502
|
+
def serve_rss(blog):
|
|
503
|
+
"""
|
|
504
|
+
Here we just serve static RSS files :)
|
|
505
|
+
"""
|
|
506
|
+
return send_file(f"{g.mainpath}/static/feed/{blog.name}.xml")
|