diff --git a/.gitignore b/.gitignore index 75b7704..d320dae 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,8 @@ __pycache__/ uploads/ *.pyc **~ -.*.swp -__pycache__/ +**/.*.swp +**/__pycache__/ venv .env .venv @@ -15,7 +15,3 @@ conf/ config/ \#*\# .\#* -node_modules/ -alembic.ini -**.egg-info -.vscode \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index cfb7a8a..105a0ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,6 @@ # Changelog -## 0.10.0 -+ Codebase refactor (with breaking changes!) -+ Dropped support for Python<=3.9 -+ Switched database to PostgreSQL -+ Move ALL config to .env (config.py is NO MORE supported) -+ Config SITE_NAME replaced with APP_NAME -+ Add CSRF token and flask_WTF -+ Schema changes: biography and website moved to `User`; `UserProfile` table deprecated (and useless fields removed) -+ Posts can now be permanently deleted -+ Miscellaneous style changes - -## 0.9.0 +## 0.9-dev * Website redesign: added some material icons, implemented via a `inline_svg` function, injected by default in templates and defined in `utils.py`. * Added positive feedback mechanism: now you can +1 a message. So, `score_message_add` and `score_message_remove` API endpoints were added, and `MessageUpvote` table was created. @@ -37,7 +26,7 @@ * Changed default `robots.txt`, adding report and admin-related lines. * Released official [Android client](https://github.com/sakuragasaki46/coriplusapp/releases/tag/v0.8.0). -## 0.7.1 +## 0.7.1-dev * Adding `messages_count`, `followers_count` and `following_count` to `profile_info` API endpoint (forgot to release). diff --git a/README.md b/README.md index 3ee8c96..b96fffc 100644 --- a/README.md +++ b/README.md @@ -16,19 +16,10 @@ This is the server. For the client, see [coriplusapp](https://github.com/sakurag * Add info to your profile * In-site notifications * Public API -* SQLite (or PostgreSQL)-based app +* SQLite-based app ## Requirements -* **Python 3.10+** with **pip**. -* **Flask** web framework. +* **Python 3** only. We don't want to support Python 2. +* **Flask** web framework (also required extension **Flask-Login**). * **Peewee** ORM. -* A \*nix-based OS. - -## Installation - -* Install dependencies: `pip install .` -* Set the `DATABASE_URL` (must be SQLite or PostgreSQL) -* Run the migrations: `sh ./genmig.sh @` -* i forgor - diff --git a/src/coriplus/__init__.py b/app/__init__.py similarity index 73% rename from src/coriplus/__init__.py rename to app/__init__.py index 09cecf6..0ef203e 100644 --- a/src/coriplus/__init__.py +++ b/app/__init__.py @@ -16,37 +16,27 @@ For other, see `app.utils`. ''' from flask import ( - Flask, g, jsonify, render_template, request, - send_from_directory, __version__ as flask_version) -import os, sys + Flask, abort, flash, g, jsonify, redirect, render_template, request, + send_from_directory, session, url_for, __version__ as flask_version) +import hashlib +import datetime, time, re, os, sys, string, json, html +from functools import wraps from flask_login import LoginManager -from flask_wtf import CSRFProtect -import dotenv -import logging -__version__ = '0.10.0-dev50' +__version__ = '0.9.0' -# we want to support Python 3.10+ only. +# we want to support Python 3 only. # Python 2 has too many caveats. -# Python <=3.9 has harder type support. -if sys.version_info[0:2] < (3, 10): - raise RuntimeError('Python 3.10+ required') +if sys.version_info[0] < 3: + raise RuntimeError('Python 3 required') -BASEDIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) -os.chdir(BASEDIR) - -dotenv.load_dotenv() - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) +os.chdir(os.path.dirname(os.path.dirname(__file__))) app = Flask(__name__) -app.secret_key = os.environ['SECRET_KEY'] +app.config.from_pyfile('../config.py') login_manager = LoginManager(app) -CSRFProtect(app) - from .models import * from .utils import * @@ -63,20 +53,17 @@ def before_request(): try: g.db.connect() except OperationalError: - logger.error('database connected twice') + sys.stderr.write('database connected twice.\n') @app.after_request def after_request(response): - try: - g.db.close() - except Exception: - logger.error('database closed twice') + g.db.close() return response @app.context_processor def _inject_variables(): return { - 'site_name': os.environ.get('APP_NAME', 'Cori+'), + 'site_name': app.config['SITE_NAME'], 'locations': locations, 'inline_svg': inline_svg } @@ -85,21 +72,17 @@ def _inject_variables(): def _inject_user(userid): return User[userid] -@app.errorhandler(403) -def error_403(body): - return render_template('403.html'), 403 - @app.errorhandler(404) def error_404(body): return render_template('404.html'), 404 @app.route('/favicon.ico') def favicon_ico(): - return send_from_directory(BASEDIR, 'src/favicon.ico') + return send_from_directory(os.getcwd(), 'favicon.ico') @app.route('/robots.txt') def robots_txt(): - return send_from_directory(BASEDIR, 'src/robots.txt') + return send_from_directory(os.getcwd(), 'robots.txt') @app.route('/uploads/.') def uploads(id, type='jpg'): diff --git a/src/coriplus/__main__.py b/app/__main__.py similarity index 100% rename from src/coriplus/__main__.py rename to app/__main__.py diff --git a/src/coriplus/admin.py b/app/admin.py similarity index 80% rename from src/coriplus/admin.py rename to app/admin.py index d8c8fb0..b8fb4b9 100644 --- a/src/coriplus/admin.py +++ b/app/admin.py @@ -4,8 +4,7 @@ Management of reports and the entire site. New in 0.8. ''' -from flask import Blueprint, abort, redirect, render_template, request, url_for -from flask_login import current_user +from flask import Blueprint, redirect, render_template, request, url_for from .models import User, Message, Report, report_reasons, REPORT_STATUS_ACCEPTED, \ REPORT_MEDIA_USER, REPORT_MEDIA_MESSAGE from .utils import pwdhash, object_list @@ -13,17 +12,21 @@ from functools import wraps bp = Blueprint('admin', __name__, url_prefix='/admin') -def _check_auth(username) -> bool: +def check_auth(username, password): try: - return User.get((User.username == username)).is_admin + return User.get((User.username == username) & (User.password == pwdhash(password)) + ).is_admin except User.DoesNotExist: return False def admin_required(f): @wraps(f) def wrapped_view(**kwargs): - if not _check_auth(current_user.username): - abort(403) + auth = request.authorization + if not (auth and check_auth(auth.username, auth.password)): + return ('Unauthorized', 401, { + 'WWW-Authenticate': 'Basic realm="Login Required"' + }) return f(**kwargs) return wrapped_view diff --git a/src/coriplus/ajax.py b/app/ajax.py similarity index 94% rename from src/coriplus/ajax.py rename to app/ajax.py index cad9c74..d2c0be3 100644 --- a/src/coriplus/ajax.py +++ b/app/ajax.py @@ -5,9 +5,8 @@ Warning: this is not the public API. ''' from flask import Blueprint, jsonify -from flask_login import current_user from .models import User, Message, MessageUpvote -from .utils import locations, is_username +from .utils import locations, get_current_user, is_username import datetime bp = Blueprint('ajax', __name__, url_prefix='/ajax') @@ -40,7 +39,7 @@ def location_search(name): @bp.route('/score//toggle', methods=['POST']) def score_toggle(id): - user = current_user + user = get_current_user() message = Message[id] upvoted_by_self = (MessageUpvote .select() diff --git a/src/coriplus/api.py b/app/api.py similarity index 99% rename from src/coriplus/api.py rename to app/api.py index 5f58c2c..6ee0fd3 100644 --- a/src/coriplus/api.py +++ b/app/api.py @@ -7,9 +7,6 @@ from .models import User, UserProfile, Message, Upload, Relationship, Notificati MSGPRV_PUBLIC, MSGPRV_UNLISTED, MSGPRV_FRIENDS, MSGPRV_ONLYME, UPLOAD_DIRECTORY from .utils import check_access_token, Visibility, push_notification, unpush_notification, \ create_mentions, is_username, generate_access_token, pwdhash -import logging - -logger = logging.getLogger(__name__) bp = Blueprint('api', __name__, url_prefix='/api/V1') @@ -19,7 +16,7 @@ def get_message_info(message): except IndexError: media = None if media: - logger.debug(media) + print(media) return { 'id': message.id, 'user': { @@ -125,7 +122,7 @@ def create2(self): privacy=privacy) file = request.files.get('file') if file: - logger.info('Uploading', file.filename) + print('Uploading', file.filename) ext = file.filename.split('.')[-1] upload = Upload.create( type=ext, diff --git a/src/coriplus/filters.py b/app/filters.py similarity index 100% rename from src/coriplus/filters.py rename to app/filters.py diff --git a/src/coriplus/models.py b/app/models.py similarity index 94% rename from src/coriplus/models.py rename to app/models.py index 07d52b7..4cdb2b5 100644 --- a/src/coriplus/models.py +++ b/app/models.py @@ -13,43 +13,32 @@ The tables are: from flask import request from peewee import * -from playhouse.db_url import connect import os - -from . import BASEDIR # here should go `from .utils import get_current_user`, but it will cause # import errors. It's instead imported at function level. -database = connect(os.environ['DATABASE_URL']) +database = SqliteDatabase('coriplus.sqlite') class BaseModel(Model): - id = AutoField(primary_key=True) - class Meta: database = database # A user. The user is separated from its page. class User(BaseModel): # The unique username. - username = CharField(30, unique=True) + username = CharField(unique=True) # The user's full name (here for better search since 0.8) - full_name = CharField(80) + full_name = TextField() # The password hash. - password = CharField(256) + password = CharField() # An email address. - email = CharField(256) + email = CharField() # The date of birth (required because of Terms of Service) birthday = DateField() # The date joined join_date = DateTimeField() # A disabled flag. 0 = active, 1 = disabled by user, 2 = banned is_disabled = IntegerField(default=0) - # Short description of user. - biography = CharField(256, default='') - # Personal website. - website = TextField(null=True) - - # Helpers for flask_login def get_id(self): @@ -118,12 +107,15 @@ class UserAdminship(BaseModel): # User profile. # Additional info for identifying users. # New in 0.6 -# Deprecated in 0.10 and merged with User class UserProfile(BaseModel): user = ForeignKeyField(User, primary_key=True) biography = TextField(default='') location = IntegerField(null=True) + year = IntegerField(null=True) website = TextField(null=True) + instagram = TextField(null=True) + facebook = TextField(null=True) + telegram = TextField(null=True) @property def full_name(self): ''' @@ -197,7 +189,7 @@ class Relationship(BaseModel): ) -UPLOAD_DIRECTORY = os.path.join(BASEDIR, 'uploads') +UPLOAD_DIRECTORY = os.path.join(os.path.split(os.path.dirname(__file__))[0], 'uploads') class Upload(BaseModel): # the extension of the media diff --git a/src/coriplus/reports.py b/app/reports.py similarity index 100% rename from src/coriplus/reports.py rename to app/reports.py diff --git a/src/coriplus/static/lib.js b/app/static/lib.js similarity index 91% rename from src/coriplus/static/lib.js rename to app/static/lib.js index 11a2316..cc78bea 100644 --- a/src/coriplus/static/lib.js +++ b/app/static/lib.js @@ -99,20 +99,12 @@ function showHideMessageOptions(id){ } } -function getCsrfToken () { - var csrf_token = document.querySelector('meta[name="csrf_token"]'); - return csrf_token?.getAttribute('content'); -} - function toggleUpvote(id){ var msgElem = document.getElementById(id); - //var upvoteLink = msgElem.getElementsByClassName('message-upvote')[0]; + var upvoteLink = msgElem.getElementsByClassName('message-upvote')[0]; var scoreCounter = msgElem.getElementsByClassName('message-score')[0]; - var body = "csrf_token=" + getCsrfToken(); var xhr = new XMLHttpRequest(); xhr.open("POST", "/ajax/score/" + id + "/toggle", true); - xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); - // TODO add csrf token somewhere xhr.onreadystatechange = function(){ if(xhr.readyState == XMLHttpRequest.DONE){ if(xhr.status == 200){ @@ -122,5 +114,5 @@ function toggleUpvote(id){ } } }; - xhr.send(body); + xhr.send(); } diff --git a/app/static/style.css b/app/static/style.css new file mode 100644 index 0000000..87152a8 --- /dev/null +++ b/app/static/style.css @@ -0,0 +1,38 @@ +body,button,input,select,textarea{font-family:Roboto,'Segoe UI',Arial,Helvetica,sans-serif} +body{margin:0} +a{text-decoration:none} +a:hover{text-decoration:underline} +@media (max-width:640px){ + .mobile-collapse{display:none} +} +.header{padding:12px;color:white;background-color:#ff3018;box-shadow:0 0 3px 3px #ccc} +.content{padding:12px} +.header a{color:white} +.header a svg{fill:white} +.content a{color:#3399ff} +.content a svg{fill:#3399ff} +.content a.plus{color:#ff3018} +.metanav{float:right} +.metanav-divider{width:1px;background:white;display:inline-block} +.header h1{margin:0;display:inline-block} +.flash{background-color:#ff9;border:yellow 1px solid} +.infobox{padding:12px;border:#ccc 1px solid} +@media (min-width:640px) { + .infobox{float:right;width:320px} +} +.weak{opacity:.5} +.field_desc{display:block} +ul.timeline{padding:0;margin:auto;max-width:960px} +ul.timeline > li{list-style:none;border-bottom:#808080 1px solid} +.message-visual img{max-width:100%;margin:auto} +.message-options-showhide::before{content:'\2026'} +.message-options{display:none} +.create_text{width:100%;height:8em} +.biography_text{height:4em} +.before-toggle:not(:checked) + input{display:none} +.follow_button,input[type="submit"]{background-color:#ff3018;color:white;border-radius:3px;border:1px solid #ff3018} +.follow_button.following{background-color:transparent;color:#ff3018;border-color:#ff3018} +.copyright{font-size:smaller;text-align:center;color:#808080} +.copyright a:link,.copyright a:visited{color:#31559e} +.copyright ul{list-style:none;padding:0} +.copyright ul > li{padding:0 3px} diff --git a/app/templates/404.html b/app/templates/404.html new file mode 100644 index 0000000..d434c9a --- /dev/null +++ b/app/templates/404.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% block body %} +

Not Found

+ +

Back to homepage.

+{% endblock %} \ No newline at end of file diff --git a/app/templates/about.html b/app/templates/about.html new file mode 100644 index 0000000..e5691a8 --- /dev/null +++ b/app/templates/about.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} + +{% block body %} +

About {{ site_name }}

+ +

{{ site_name }} {{ version }} – Python {{ python_version }} – + Flask {{ flask_version }}

+

Copyright © 2019 Sakuragasaki46.

+ +

License

+

Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ +

Source code for this site: + https://github.com/sakuragasaki46/coriplus/ + +{% endblock %} diff --git a/src/coriplus/templates/admin_base.html b/app/templates/admin_base.html similarity index 90% rename from src/coriplus/templates/admin_base.html rename to app/templates/admin_base.html index c20c146..e59e813 100644 --- a/src/coriplus/templates/admin_base.html +++ b/app/templates/admin_base.html @@ -7,19 +7,16 @@

-

{{ site_name }}: Admin

-
- {% for message in get_flashed_messages() %}
{{ message }}
{% endfor %} -
{% block body %}{% endblock %} -