openwrite 0.10.5__tar.gz → 0.11.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.
Files changed (79) hide show
  1. {openwrite-0.10.5/openwrite.egg-info → openwrite-0.11.0}/PKG-INFO +4 -3
  2. {openwrite-0.10.5 → openwrite-0.11.0}/README.md +3 -2
  3. openwrite-0.11.0/openwrite/__init__.py +273 -0
  4. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/cli.py +10 -61
  5. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/gemini_multi.py +0 -2
  6. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/gemini_single.py +0 -2
  7. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/routes/admin.py +33 -3
  8. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/routes/auth.py +1 -1
  9. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/routes/dashboard.py +3 -5
  10. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/routes/main.py +4 -1
  11. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/routes/upload.py +1 -1
  12. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/admin.html +7 -6
  13. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/blog.html +1 -1
  14. openwrite-0.11.0/openwrite/templates/changepw.html +19 -0
  15. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/create.html +7 -7
  16. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/dashboard.html +10 -10
  17. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/discover.html +2 -2
  18. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/edit.html +15 -16
  19. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/footer.html +4 -4
  20. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/header.html +6 -6
  21. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/import.html +7 -7
  22. openwrite-0.11.0/openwrite/templates/index.html +17 -0
  23. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/instances.html +3 -3
  24. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/login.html +2 -2
  25. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/new_post.html +9 -9
  26. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/register.html +5 -5
  27. openwrite-0.11.0/openwrite/templates/settings.html +96 -0
  28. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/stats.html +4 -4
  29. openwrite-0.11.0/openwrite/templates/translations.html +87 -0
  30. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/utils/i18n.json +3 -5
  31. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/utils/models.py +7 -0
  32. {openwrite-0.10.5 → openwrite-0.11.0/openwrite.egg-info}/PKG-INFO +4 -3
  33. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite.egg-info/SOURCES.txt +2 -2
  34. {openwrite-0.10.5 → openwrite-0.11.0}/pyproject.toml +1 -1
  35. openwrite-0.10.5/openwrite/__init__.py +0 -134
  36. openwrite-0.10.5/openwrite/templates/changepw.html +0 -19
  37. openwrite-0.10.5/openwrite/templates/index.html +0 -9
  38. openwrite-0.10.5/openwrite/templates/settings.html +0 -63
  39. openwrite-0.10.5/openwrite/utils/translations.py +0 -6
  40. {openwrite-0.10.5 → openwrite-0.11.0}/LICENSE +0 -0
  41. {openwrite-0.10.5 → openwrite-0.11.0}/MANIFEST.in +0 -0
  42. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/db/__init__.py +0 -0
  43. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/db/base.py +0 -0
  44. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/routes/blog.py +0 -0
  45. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/routes/federation.py +0 -0
  46. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/IBMPlexSans-Regular.ttf +0 -0
  47. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/Merriweather.ttf +0 -0
  48. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/avatar.png +0 -0
  49. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/favicon.ico +0 -0
  50. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/logo.png +0 -0
  51. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/script/chart.js +0 -0
  52. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/script/highlight.min.js +0 -0
  53. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/script/openwrite.js +0 -0
  54. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/script/papaparse.min.js +0 -0
  55. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/script/showdown.min.js +0 -0
  56. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/script/showdown.min.js.map +0 -0
  57. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/script/site.compat.min.js +0 -0
  58. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/script/site.min.js +0 -0
  59. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/style/hl.css +0 -0
  60. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/style/style.css +0 -0
  61. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/style/themes/light.css +0 -0
  62. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/style/themes/obscura.css +0 -0
  63. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/style/themes/stickynote.css +0 -0
  64. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/style/themes/terminal.css +0 -0
  65. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/style/themes/warmnight.css +0 -0
  66. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/style/themes/windows98.css +0 -0
  67. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/blog_footer.html +0 -0
  68. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/blog_header.html +0 -0
  69. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/blog_preview.html +0 -0
  70. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/post.html +0 -0
  71. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/preview.html +0 -0
  72. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/utils/create_db.py +0 -0
  73. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/utils/db.py +0 -0
  74. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/utils/helpers.py +0 -0
  75. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite.egg-info/dependency_links.txt +0 -0
  76. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite.egg-info/entry_points.txt +0 -0
  77. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite.egg-info/requires.txt +0 -0
  78. {openwrite-0.10.5 → openwrite-0.11.0}/openwrite.egg-info/top_level.txt +0 -0
  79. {openwrite-0.10.5 → openwrite-0.11.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openwrite
3
- Version: 0.10.5
3
+ Version: 0.11.0
4
4
  License: GNU AFFERO GENERAL PUBLIC LICENSE
5
5
  Version 3, 19 November 2007
6
6
 
@@ -718,6 +718,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
718
718
  - Blog themes
719
719
  - Gemini protocol
720
720
  - Statistics per post
721
+ - Multilanguage (currently English and Polish)
721
722
 
722
723
 
723
724
  ## Installation
@@ -734,7 +735,7 @@ pip install openwrite
734
735
  openwrite init
735
736
  ```
736
737
 
737
- 3. Answer few questions about your instance to generate .env, database and create all necessary directories.
738
+ 3. Answer few questions about your instance to generate .env and create all necessary directories.
738
739
  4. Run:
739
740
 
740
741
  ```
@@ -749,7 +750,7 @@ openwrite run -d
749
750
 
750
751
  to run it in background.
751
752
 
752
- 5. Enjoy!
753
+ 5. Default user is `admin` with password `openwrite`. You can (and should!) change it in your dashboard. Enjoy!
753
754
 
754
755
 
755
756
  #### Docker
@@ -29,6 +29,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
29
29
  - Blog themes
30
30
  - Gemini protocol
31
31
  - Statistics per post
32
+ - Multilanguage (currently English and Polish)
32
33
 
33
34
 
34
35
  ## Installation
@@ -45,7 +46,7 @@ pip install openwrite
45
46
  openwrite init
46
47
  ```
47
48
 
48
- 3. Answer few questions about your instance to generate .env, database and create all necessary directories.
49
+ 3. Answer few questions about your instance to generate .env and create all necessary directories.
49
50
  4. Run:
50
51
 
51
52
  ```
@@ -60,7 +61,7 @@ openwrite run -d
60
61
 
61
62
  to run it in background.
62
63
 
63
- 5. Enjoy!
64
+ 5. Default user is `admin` with password `openwrite`. You can (and should!) change it in your dashboard. Enjoy!
64
65
 
65
66
 
66
67
  #### Docker
@@ -0,0 +1,273 @@
1
+ from flask import Flask, g
2
+ from dotenv import load_dotenv
3
+ from werkzeug.middleware.proxy_fix import ProxyFix
4
+ from werkzeug.exceptions import HTTPException
5
+ import os
6
+ from .utils.helpers import generate_nonce, get_ip
7
+ import time
8
+ from .utils.models import Settings, Home
9
+ from sqlalchemy import distinct
10
+ import bcrypt
11
+ import json
12
+ from cryptography.hazmat.primitives.asymmetric import rsa
13
+ from cryptography.hazmat.primitives import serialization
14
+ from datetime import datetime, timezone
15
+
16
+ start_time = time.time()
17
+
18
+ def create_app(test_config=None):
19
+ if test_config is None:
20
+ load_dotenv()
21
+ db_type = os.getenv("DB_TYPE", "sqlite")
22
+ db_path = os.getenv("DB_PATH", "db.sqlite")
23
+ else:
24
+ db_type = test_config.get("DB_TYPE", "sqlite")
25
+ db_path = test_config.get("DB_PATH", "data.db")
26
+ load_dotenv(test_config.get("env"), override=True)
27
+
28
+ if not os.path.exists(".initialized") or test_config is not None:
29
+ mode = os.getenv("MODE")
30
+ domain = os.getenv("DOMAIN")
31
+ f_abs_path = os.path.abspath(__file__)
32
+ 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
36
+ from .utils.db import init_engine, SessionLocal
37
+ init_engine(db_type, db_path)
38
+ from .utils.db import SessionLocal
39
+ admin_password = os.urandom(16).hex()
40
+ hashed = bcrypt.hashpw("openwrite".encode("utf-8"), bcrypt.gensalt())
41
+ admin_user = User(username="admin", email="", password_hash=hashed.decode("utf-8"), verified=1, admin=1)
42
+ SessionLocal.add(admin_user)
43
+ SessionLocal.commit()
44
+ SessionLocal.close()
45
+
46
+ if mode == "single":
47
+ key = rsa.generate_private_key(
48
+ public_exponent=65537,
49
+ key_size=2048
50
+ )
51
+
52
+ private_pem = key.private_bytes(
53
+ encoding=serialization.Encoding.PEM,
54
+ format=serialization.PrivateFormat.PKCS8,
55
+ encryption_algorithm=serialization.NoEncryption()
56
+ ).decode()
57
+
58
+ public_pem = key.public_key().public_bytes(
59
+ encoding=serialization.Encoding.PEM,
60
+ format=serialization.PublicFormat.SubjectPublicKeyInfo
61
+ ).decode()
62
+
63
+ now = datetime.now(timezone.utc).replace(microsecond=0)
64
+
65
+ new_blog = Blog(
66
+ owner=1,
67
+ name="default",
68
+ title=domain,
69
+ index="on",
70
+ access="domain",
71
+ description_raw=f"![hello](https://openwrite.b-cdn.net/hello.jpg =500x258)\n\n# Hello there! 👋\n\nYou can edit your blog description in [dashboard](http://{domain}/dashboard/edit/default)",
72
+ 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=\"http://{domain}/dashboard/edit/default\">dashboard</a></p>",
73
+ css="",
74
+ pub_key=public_pem,
75
+ priv_key=private_pem,
76
+ theme="default",
77
+ created=now
78
+ )
79
+ SessionLocal.add(new_blog)
80
+
81
+ hometext_pl = """
82
+ <h1 class="centered"><span style="color: #f2d2c8">cicha</span> przestrzeń na <span style="color: #5a36bf">głośne</span> myśli.</h1>
83
+
84
+ <article class="main-content" style="text-align: center">
85
+ <p><strong>Openwrite</strong> to lekka, otwartoźródłowa platforma blogowa stworzona z myślą o wolności pisania – bez reklam, trackerów i zbędnego balastu.</p><br>
86
+
87
+ - Pisz w Markdownie<br>
88
+ - Zainstaluj w kilka minut<br>
89
+ - Publikuj do sieci, przez Gemini lub na Mastodonie – jak chcesz<br>
90
+
91
+ <br>
92
+ <p>Bez analityki. Bez ukrytych skryptów.<br>
93
+ Tylko Ty, Twoje myśli i miejsce, w którym możesz je wyrazić.</p>
94
+ <p>Wybierz motyw. Dodaj obrazki. Śledź wyświetlenia.<br>
95
+ Zbuduj bloga po swojemu – minimalistycznego lub z dodatkami.</p>
96
+ <p><strong>To Twoje słowa. Twój blog. Twoje zasady.</strong><br><br>
97
+ Zacznij pisać — i odzyskaj głos.</p>
98
+ </p>
99
+ </article>
100
+ <div class="action-links">
101
+ <a class="btn purple" href="/register">Zarejestruj się tutaj</a>
102
+ <span class="or-separator">lub</span>
103
+ <a class="btn empty" href="https://main.openwrite.io/hosting">Samodzielny hosting</a>
104
+ </div>
105
+ """
106
+
107
+ hometext_en = """
108
+ <h1 class="centered"><span style="color: #f2d2c8">quiet</span> space for <span style="color: #5a36bf">loud</span> thoughts.</h1>
109
+
110
+ <article class="main-content" style="text-align: center">
111
+ <p><strong>Openwrite</strong> is a lightweight, open-source blogging platform built for people who just want to write — without ads, trackers, or bloated nonsense.</p><br>
112
+
113
+ - Write in Markdown<br>
114
+ - Self-host in minutes<br>
115
+ - Publish to the open web, Gemini, or Mastodon — your choice<br>
116
+
117
+ <br>
118
+ <p>There’s no analytics. No hidden scripts. No engagement traps.<br>
119
+ Just you, your thoughts, and an honest place to share them.</p>
120
+ <p>Choose a theme. Add images. Track views.<br>
121
+ Build your blog your way — minimal or feature-rich.</p>
122
+ <p><strong>It’s your voice. Your blog. Your rules.</strong><br><br>
123
+ Start writing — and own your words.</p>
124
+ </article>
125
+ <div class="action-links">
126
+ <a class="btn purple" href="/register">Register Here</a>
127
+ <span class="or-separator">or</span>
128
+ <a class="btn empty" href="https://main.openwrite.io/hosting">Self Host</a>
129
+ </div>
130
+ """
131
+
132
+ with open(f"{file_cwd}/utils/i18n.json", "r", encoding='utf-8') as f:
133
+ data = json.load(f)
134
+
135
+ for lang, entries in data.items():
136
+ for key, value in entries.items():
137
+ SessionLocal.add(Home(language=lang, name=key, type="translation", content=value))
138
+
139
+ new_setting = Settings(name="logo", value="/static/logo.png")
140
+ new_home_pl = Home(language="pl", name="hometext", type="text", content=hometext_pl)
141
+ new_home_en = Home(language="en", name="hometext", type="text", content=hometext_en)
142
+ SessionLocal.add(new_setting)
143
+ SessionLocal.add(new_home_pl)
144
+ SessionLocal.add(new_home_en)
145
+ SessionLocal.commit()
146
+
147
+ if test_config is None:
148
+ with open(".initialized", "w") as f:
149
+ f.write("ok")
150
+
151
+ app = Flask(__name__, template_folder="templates", subdomain_matching=True, static_url_path='/static')
152
+ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
153
+ app.secret_key = os.getenv("SECRET_KEY")
154
+ app.config['SERVER_NAME'] = os.getenv("DOMAIN")
155
+
156
+ from .routes.auth import auth_bp
157
+ from .routes.dashboard import dashboard_bp
158
+ from .routes.blog import blog_bp
159
+ from .routes.admin import admin_bp
160
+ from .routes.main import main_bp
161
+ from .routes.upload import upload_bp
162
+ from .routes.federation import federation_bp
163
+ from .utils.db import init_engine, SessionLocal
164
+ from flask_cors import CORS
165
+
166
+ CORS(app)
167
+
168
+ init_engine(db_type, db_path)
169
+ from .utils.db import SessionLocal
170
+
171
+ app.register_blueprint(auth_bp)
172
+ app.register_blueprint(dashboard_bp)
173
+ app.register_blueprint(blog_bp)
174
+ app.register_blueprint(admin_bp)
175
+ app.register_blueprint(main_bp)
176
+ app.register_blueprint(upload_bp)
177
+ app.register_blueprint(federation_bp)
178
+
179
+ def _(key: str):
180
+ return g.trans.get(key, key)
181
+
182
+ app.jinja_env.globals['_'] = _
183
+
184
+ @app.before_request
185
+ def before():
186
+ from flask import request, session
187
+ from openwrite.utils.models import Home, Settings
188
+ g.db = SessionLocal
189
+ t_query = g.db.query(distinct(Home.language)).all()
190
+ translations = [row[0] for row in t_query]
191
+ lang = request.cookies.get("lang")
192
+ if not lang or lang not in translations:
193
+ accept = request.headers.get("Accept-Language", "")
194
+ for part in accept.split(","):
195
+ code = part.split("-")[0].strip().lower()
196
+ if code in translations:
197
+ lang = code
198
+ break
199
+ if lang not in translations:
200
+ lang = "en"
201
+
202
+ trans = g.db.query(Home).filter(Home.type == "translation", Home.language == lang).all()
203
+
204
+ g.mode = os.getenv("MODE")
205
+ f_abs_path = os.path.abspath(__file__)
206
+ g.mainpath = "/".join(f_abs_path.split("/")[:-1])
207
+ g.trans = {t.name: t.content for t in trans if len(t.content) > 1}
208
+ g.alltrans = translations
209
+ g.lang = lang
210
+ g.main_domain = os.getenv("DOMAIN")
211
+ g.blog_limit = os.getenv("BLOG_LIMIT")
212
+ g.register_enabled = os.getenv("SELF_REGISTER", "no") == "yes"
213
+ g.upload_enabled = os.getenv("MEDIA_UPLOAD", "no") == "yes"
214
+ g.captcha = os.getenv("CAPTCHA_ENABLED", "no") == "yes"
215
+ g.fcaptcha_sitekey = os.getenv("FRIENDLY_CAPTCHA_SITEKEY", "key")
216
+ g.fcaptcha_apikey = os.getenv("FRIENDLY_CAPTCHA_APIKEY", "api_key")
217
+ g.staticdir = app.static_folder
218
+ g.staticurl = app.static_url_path
219
+ g.settings = g.db.query(Settings).all()
220
+
221
+ if session.get("userid") is not None:
222
+ g.user = session.get("userid")
223
+ g.isadmin = session.get("admin")
224
+ else:
225
+ g.user = None
226
+
227
+ g.nonce = generate_nonce()
228
+
229
+ @app.context_processor
230
+ def inject_globals():
231
+ from openwrite.utils.models import Home
232
+ langs = g.db.query(Home).filter_by(name="lang_name").all()
233
+ lang_obj = {}
234
+ for l in langs:
235
+ lang_obj[l.language] = {"name": l.content}
236
+ return {
237
+ 'current_lang': g.lang,
238
+ 'available_languages': lang_obj
239
+ }
240
+
241
+ @app.after_request
242
+ def after(response):
243
+ nonce = g.nonce
244
+ #response.headers["Content-Security-Policy"] = (
245
+ # f"default-src 'none'; "
246
+ # f"script-src 'self' 'nonce-{nonce}' https://cdn.jsdelivr.net http://{g.main_domain}"
247
+ # f"style-src 'self'; "
248
+ # f"style-src-elem 'self' http://{g.main_domain}; "
249
+ # f"style-src-attr 'unsafe-inline';"
250
+ # f"script-src-attr 'unsafe-inline';"
251
+ # f"img-src 'self' data: http://{g.main_domain}; "
252
+ # f"font-src 'self'; "
253
+ # f"connect-src 'self'; "
254
+ # f"base-uri 'none'; "
255
+ # f"form-action 'self'; "
256
+ # f"frame-ancestors 'none';"
257
+ # f"frame-src https://global.frcapi.com ;"
258
+ #)
259
+ return response
260
+
261
+ @app.errorhandler(Exception)
262
+ def handle_exception(e):
263
+ if isinstance(e, HTTPException):
264
+ return e
265
+
266
+ app.logger.exception(f"Unhandled exception! {e}")
267
+ return "Internal Server Error", 500
268
+
269
+ @app.teardown_appcontext
270
+ def shutdown_session(exception=None):
271
+ g.db.remove()
272
+
273
+ return app
@@ -13,6 +13,8 @@ from cryptography.hazmat.primitives.asymmetric import rsa
13
13
  from cryptography.hazmat.primitives import serialization
14
14
  from datetime import datetime, timezone
15
15
  import bcrypt
16
+ import json
17
+ import shutil
16
18
 
17
19
  load_dotenv()
18
20
  f_abs_path = os.path.abspath(__file__)
@@ -87,9 +89,9 @@ def init():
87
89
  os.makedirs(f"{cwd}/gemini/.certs", exist_ok=True)
88
90
  subprocess.run(["openssl", "req", "-x509", "-newkey", "rsa:4096", "-keyout", f"{cwd}/gemini/.certs/key.pem", "-out", f"{cwd}/gemini/.certs/cert.pem", "-days", "365", "-nodes", "-subj", f"/CN={domain}"], check=True)
89
91
  if mode == 1:
90
- os.rename(f"{lib_cwd}/gemini_multi.py", f"{cwd}/gemini/mod/10_openwrite.py")
92
+ shutil.copyfile(f"{lib_cwd}/gemini_multi.py", f"{cwd}/gemini/mod/10_openwrite.py")
91
93
  elif mode == 2:
92
- os.rename(f"{lib_cwd}/gemini_single.py", f"{cwd}/gemini/mod/10_openwrite.py")
94
+ shutil.copyfile(f"{lib_cwd}/gemini_single.py", f"{cwd}/gemini/mod/10_openwrite.py")
93
95
 
94
96
  logs_enabled = click.confirm("Enable logging?", default=True)
95
97
  if logs_enabled:
@@ -138,70 +140,15 @@ def init():
138
140
  gem.write(f"modules = {cwd}/gemini/mod\n")
139
141
  f.write(f"LOGS={'yes' if logs_enabled else 'no'}\n")
140
142
  if logs_enabled:
141
- f.write(f"LOGS_DIR={logs_dir}")
143
+ f.write(f"LOGS_DIR={logs_dir}\n")
142
144
  f.write(f"CAPTCHA_ENABLED={'yes' if captcha_enabled else 'no'}\n")
143
145
  if captcha_enabled:
144
146
  f.write(f"FRIENDLY_CAPTCHA_SITEKEY={captcha_sitekey}\n")
145
147
  f.write(f"FRIENDLY_CAPTCHA_APIKEY={captcha_apikey}\n")
146
148
 
147
149
  click.echo("[+] .env file created")
148
- click.echo("[*] Initializing database..")
149
- init_db(dbtype, dbpath)
150
- click.echo("[+] Database initialized")
151
- click.echo("[*] Adding admin user...")
152
- from .utils.models import User, Blog, Post, Settings
153
- from .utils.db import init_engine, SessionLocal
154
- init_engine(dbtype, dbpath)
155
- from .utils.db import SessionLocal
156
- admin_password = os.urandom(16).hex()
157
- hashed = bcrypt.hashpw(admin_password.encode("utf-8"), bcrypt.gensalt())
158
- admin_user = User(username="admin", email="", password_hash=hashed.decode("utf-8"), verified=1, admin=1)
159
- SessionLocal.add(admin_user)
160
- SessionLocal.commit()
161
- SessionLocal.close()
162
- click.echo(f"[+] Admin user added! Your credentials:\n\nLogin: admin\nPassword: {admin_password}")
163
150
  if logs_enabled and not os.path.exists(logs_dir):
164
151
  os.makedirs(logs_dir, exist_ok=True)
165
-
166
- if mode == 2:
167
- key = rsa.generate_private_key(
168
- public_exponent=65537,
169
- key_size=2048
170
- )
171
-
172
- private_pem = key.private_bytes(
173
- encoding=serialization.Encoding.PEM,
174
- format=serialization.PrivateFormat.PKCS8,
175
- encryption_algorithm=serialization.NoEncryption()
176
- ).decode()
177
-
178
- public_pem = key.public_key().public_bytes(
179
- encoding=serialization.Encoding.PEM,
180
- format=serialization.PublicFormat.SubjectPublicKeyInfo
181
- ).decode()
182
-
183
- now = datetime.now(timezone.utc)
184
-
185
- new_blog = Blog(
186
- owner=1,
187
- name="default",
188
- title=domain,
189
- index="on",
190
- access="domain",
191
- description_raw=f"![hello](https://openwrite.b-cdn.net/hello.jpg =500x258)\n\n# Hello there! 👋\n\nYou can edit your blog description in [dashboard](http://{domain}/dashboard/edit/default)",
192
- 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=\"http://{domain}/dashboard/edit/default\">dashboard</a></p>",
193
- css="",
194
- pub_key=public_pem,
195
- priv_key=private_pem,
196
- theme="default",
197
- created=now
198
- )
199
-
200
- new_setting = Settings(name="logo", value="/static/logo.png")
201
-
202
- SessionLocal.add(new_blog)
203
- SessionLocal.add(new_setting)
204
- SessionLocal.commit()
205
152
 
206
153
  @cli.command()
207
154
  @click.option("-d", "--daemon", is_flag=True, help="Run in background (daemon)")
@@ -267,10 +214,12 @@ def run(daemon):
267
214
 
268
215
  try:
269
216
  gunicorn_proc.wait()
270
- gemini_proc.wait()
217
+ if gemini:
218
+ gemini_proc.wait()
271
219
  except KeyboardInterrupt:
272
220
  gunicorn_proc.terminate()
273
- gemini_proc.terminate()
221
+ if gemini:
222
+ gemini_proc.terminate()
274
223
 
275
224
  @cli.command()
276
225
  def debugrun():
@@ -320,7 +269,7 @@ def stop(service):
320
269
  click.echo("[*] Searching Gunicorn processes")
321
270
  try:
322
271
  out = subprocess.check_output(["pgrep", "-f", "gunicorn.*openwrite:create_app"], text=True)
323
- out_gemini = subprocess.check_output(["pgrep", "-f", "gemini\.py"], text=True)
272
+ out_gemini = subprocess.check_output(["pgrep", "-f", "gmcapsuled"], text=True)
324
273
  pids = out.strip().splitlines()
325
274
  pids_gemini = out_gemini.strip().splitlines()
326
275
  for pid in pids:
@@ -113,5 +113,3 @@ def init(capsule):
113
113
  capsule.add("/b/*",
114
114
  blog_index,
115
115
  protocol='gemini')
116
-
117
-
@@ -80,5 +80,3 @@ def init(capsule):
80
80
  capsule.add("/p/*",
81
81
  blog_post,
82
82
  protocol='gemini')
83
-
84
-
@@ -5,6 +5,7 @@ import bcrypt
5
5
  from werkzeug.utils import secure_filename
6
6
  from PIL import Image
7
7
  import os
8
+ from collections import defaultdict
8
9
 
9
10
  admin_bp = Blueprint("admin", __name__)
10
11
 
@@ -164,7 +165,36 @@ def admin_settings():
164
165
  logo_q.value = f"{g.staticurl}/{filename}"
165
166
  g.db.commit()
166
167
  settings_dict['logo'] = f"{g.staticurl}/{filename}"
167
- for h in hometext:
168
- h.content = hometexts[h.language]
168
+ g.db.query(Home).filter_by(name="hometext").delete()
169
+ for h in hometexts.keys():
170
+ g.db.add(Home(name="hometext", language=h, type="text", content=hometexts[h]))
169
171
  g.db.commit()
170
- return render_template("settings.html", settings=settings_dict, hometext=hometext)
172
+ new_hometexts = g.db.query(Home).filter_by(name="hometext").all()
173
+ return render_template("settings.html", settings=settings_dict, hometext=new_hometexts)
174
+
175
+ @admin_bp.route("/admin/translations", methods=['GET', 'POST'])
176
+ def translations():
177
+ if request.method == "GET":
178
+ trans = g.db.query(Home).filter_by(type="translation").all()
179
+ grouped = defaultdict(dict)
180
+ for t in trans:
181
+ grouped[t.name][t.language] = t.content
182
+ return render_template("translations.html", trans=grouped, langs=g.alltrans)
183
+
184
+ trans_array = []
185
+ for key, val in request.form.items():
186
+ if key.startswith("val_"):
187
+ _, name, lang = key.split("___", 2)
188
+ trans_array.append({
189
+ "name": name,
190
+ "language": lang,
191
+ "content": val
192
+ })
193
+
194
+ g.db.query(Home).filter(Home.type == "translation").delete()
195
+
196
+ for tr in trans_array:
197
+ g.db.add(Home(name=tr['name'], language=tr['language'], content=tr['content'], type="translation"))
198
+
199
+ g.db.commit()
200
+ return redirect("/admin/translations")
@@ -25,7 +25,7 @@ def register():
25
25
  form_password2 = request.form.get('password2')
26
26
 
27
27
  if form_password != form_password2:
28
- return render_template("register.html", error=g.trans['password_dont_match'])
28
+ return render_template("register.html", error=g.trans['passwords_dont_match'])
29
29
 
30
30
  user = g.db.query(User).filter_by(username=form_username).first()
31
31
  if user:
@@ -123,6 +123,7 @@ def edit_blog(name):
123
123
  p.views = v
124
124
 
125
125
  themes = get_themes()
126
+ themes.append("default")
126
127
 
127
128
  if request.method == "GET":
128
129
  return render_template("edit.html", blog=blog, posts=posts, themes=themes)
@@ -136,10 +137,7 @@ def edit_blog(name):
136
137
  blog.updated = now
137
138
  blog.title = request.form.get("title")
138
139
  selected_theme = request.form.get("theme")
139
- themes_plus = themes
140
- themes_plus.append("default")
141
- if selected_theme not in themes_plus:
142
- print(f"{selected_theme} in {themes}?")
140
+ if selected_theme not in themes:
143
141
  return render_template("edit.html", blog=blog, posts=posts, themes=themes, error="Wrong theme!")
144
142
  blog.theme = selected_theme
145
143
  g.db.commit()
@@ -192,7 +190,7 @@ def new_post(name):
192
190
  url = f"https://openwrite.io/b/{blog.name}/{link}"
193
191
 
194
192
  followers = []
195
- if blog.followers is not None:
193
+ if blog.followers not in (None, "null", "NULL"):
196
194
  followers = json.loads(blog.followers)
197
195
 
198
196
  for actor in followers:
@@ -14,8 +14,11 @@ main_bp = Blueprint("main", __name__)
14
14
  @main_bp.route("/")
15
15
  def index():
16
16
  if g.mode == "multi":
17
+ langs = []
17
18
  home = g.db.query(Home).filter_by(name="hometext").all()
18
- return render_template('index.html', home=home)
19
+ for h in home:
20
+ langs.append(h.language)
21
+ return render_template('index.html', home=home, langs=langs)
19
22
 
20
23
  elif g.mode == "single":
21
24
  blog = g.db.query(Blog).filter_by(name="default").first()
@@ -47,7 +47,7 @@ def upload_image():
47
47
 
48
48
  m = hashlib.md5()
49
49
  m.update(filename.encode() + user.username.encode())
50
- filename = f"{user.username}_{m.hexdigest()}.{extension}"
50
+ filename = f"{m.hexdigest()}.{extension}"
51
51
 
52
52
  if STORAGE_BACKEND == "bunny":
53
53
  url = f"https://storage.bunnycdn.com/{BUNNY_ZONE}/{filename}"
@@ -4,8 +4,9 @@
4
4
  {% endif %}
5
5
  <h1 class="centered">Actions</h1>
6
6
  <div style="display: flex; justify-content: center; gap: 1rem;">
7
- <a style="width: 5rem;" class="btn purple" href="/admin/add_user">{{ g.trans['add_user'] }}</a>
7
+ <a style="width: 5rem;" class="btn purple" href="/admin/add_user">{{ _('add_user') }}</a>
8
8
  <a style="width: 5rem;" class="btn purple" href="/admin/settings">Settings</a>
9
+ <a style="width: 5rem;" class="btn purple" href="/admin/translations">Translations</a>
9
10
  </div>
10
11
  <h1 class="centered">Blogs</h1>
11
12
  <table class="table">
@@ -14,7 +15,7 @@
14
15
  </thead>
15
16
  <tbody>
16
17
  {% for blog in blogs %}
17
- <tr><td>{{ blog.title }}</td><td>{{ blog.name }}</td><td>{{ blog.ownername }}</td><td><a class="action" href="/admin/delete_blog/{{ blog.name }}" onclick="return confirm('{{ g.trans['are_you_sure_blog'] }} {{ blog.name }}?\n{{ g.trans['this_action_cannot_be_reverted'] }}.')">Delete</a></td></tr>
18
+ <tr><td>{{ blog.title }}</td><td>{{ blog.name }}</td><td>{{ blog.ownername }}</td><td><a class="action" href="/admin/delete_blog/{{ blog.name }}" onclick="return confirm('{{ _('are_you_sure_blog') }} {{ blog.name }}?\n{{ _('this_action_cannot_be_reverted') }}.')">Delete</a></td></tr>
18
19
  {% endfor %}
19
20
  </tbody>
20
21
  </table>
@@ -27,15 +28,15 @@
27
28
  <tbody>
28
29
  {% for user in users %}
29
30
  <tr><td>{{ user.username }}</td>
30
- <td>{% if user.admin > 0 %}{{ g.trans['yes'] }}{% else %}{{ g.trans['no'] }}{% endif %}</td>
31
+ <td>{% if user.admin > 0 %}{{ _('yes') }}{% else %}{{ _('no') }}{% endif %}</td>
31
32
  <td>
32
- <a class="action" href="/admin/delete_user/{{ user.username }}" onclick="return confirm('{{ g.trans['are_you_sure_user'] }} {{ user.username }}?\n{{ g.trans['this_action_cannot_be_reverted'] }}.')">Delete</a>
33
+ <a class="action" href="/admin/delete_user/{{ user.username }}" onclick="return confirm('{{ _('are_you_sure_user') }} {{ user.username }}?\n{{ _('this_action_cannot_be_reverted') }}.')">Delete</a>
33
34
  {% if user.admin > 0 %}
34
35
  {% if user.username != "admin" %}
35
- <a class="action" onclick="return confirm('{{ g.trans['are_you_sure_remove_admin'] }} {{ user.username }}?')" href="/admin/remove_admin/{{ user.username }}">{{ g.trans['remove_admin'] }}</a>
36
+ <a class="action" onclick="return confirm('{{ _('are_you_sure_remove_admin') }} {{ user.username }}?')" href="/admin/remove_admin/{{ user.username }}">{{ _('remove_admin') }}</a>
36
37
  {% endif %}
37
38
  {% else %}
38
- <a class="action" onclick="return confirm('{{ g.trans['are_you_sure_make_admin'] }} {{ user.username }}?')" href="/admin/make_admin/{{ user.username }}">{{ g.trans['add_admin'] }}</a>
39
+ <a class="action" onclick="return confirm('{{ _('are_you_sure_make_admin') }} {{ user.username }}?')" href="/admin/make_admin/{{ user.username }}">{{ _('add_admin') }}</a>
39
40
  {% endif %}
40
41
  </td>
41
42
  </tr>
@@ -4,7 +4,7 @@
4
4
  </article>
5
5
  {% if posts | count > 0 %}
6
6
  <hr>
7
- <h3><b>{{ g.trans['posts'] }}</b></h3>
7
+ <h3><b>{{ _('posts') }}</b></h3>
8
8
  <article class="post-content" id="posts">
9
9
  {% for post in posts %}
10
10
  <div class="postlist_item">
@@ -0,0 +1,19 @@
1
+ {% include 'header.html' %}
2
+
3
+ <h1 class="centered">{{ _('change_password') }}</h1>
4
+
5
+ <article class="main-content">
6
+ {% if error %}
7
+ <div class="error">{{ error }}</div>
8
+ {% endif %}
9
+ <form method="post" action="/dashboard/changepw">
10
+ <span class="form-field">{{ _('current_password') }}:</span>
11
+ <input type="password" name="current_pass">
12
+ <span class="form-field">{{ _('new_password') }}:</span>
13
+ <input type="password" name="new_pass">
14
+ <span class="form-field">{{ _('password2') }}:</span>
15
+ <input type="password" name="new_pass2">
16
+ <input type="submit" name="sub" value="{{ _('change_password') }}" class="btn purple" style="margin-top: 1rem;">
17
+ </form>
18
+ </article>
19
+ {% include 'footer.html' %}