From 29cf1532f750b7984b68af73583454f1f9fca0df Mon Sep 17 00:00:00 2001 From: Mattia Succurro Date: Fri, 22 Nov 2019 18:20:32 +0100 Subject: [PATCH 01/15] Adding explore endpoint and fixing bugs --- CHANGELOG.md | 10 ++++++- app/__init__.py | 6 +++- app/api.py | 71 ++++++++++++++++++++++++++++++++++++++---------- app/utils.py | 2 +- app/website.py | 17 +----------- favicon.ico | Bin 0 -> 1150 bytes 6 files changed, 73 insertions(+), 33 deletions(-) create mode 100644 favicon.ico diff --git a/CHANGELOG.md b/CHANGELOG.md index cae4de4..2e8a425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.9-dev + +* Added `create_account` endpoint to API. This endpoint does not require an access token. +* Added `has_more` field to feed endpoints (`feed`, `explore` and `profile_feed`). +* Added `/favicon.ico`. +* Added `explore` endpoint. +* Fixed some bugs when creating mentions and using offsets in feeds. + ## 0.8.0 * Added the admin dashboard, accessible from `/admin/` via basic auth. Only users with admin right can access it. Added endpoints `admin.reports` and `admin.reports_detail`. @@ -12,7 +20,7 @@ * Added `relationships_follow`, `relationships_unfollow`, `username_availability`, `edit_profile`, `request_edit` and `confirm_edit` endpoints to API. * Added `url` utility to model `Upload`. * Changed default `robots.txt`, adding report and admin-related lines. -* Released official [Android client](https://github.com/sakuragasaki46/coriplusapp/releases/tag/v0.8.0) +* Released official [Android client](https://github.com/sakuragasaki46/coriplusapp/releases/tag/v0.8.0). ## 0.7.1-dev diff --git a/app/__init__.py b/app/__init__.py index 835fcbc..b8b5c06 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -23,7 +23,7 @@ import datetime, time, re, os, sys, string, json, html from functools import wraps from flask_login import LoginManager -__version__ = '0.8.0' +__version__ = '0.9-dev' # we want to support Python 3 only. # Python 2 has too many caveats. @@ -69,6 +69,10 @@ def _inject_user(userid): @app.errorhandler(404) def error_404(body): return render_template('404.html'), 404 + +@app.route('/favicon.ico') +def favicon_ico(): + return send_from_directory(os.getcwd(), 'favicon.ico') @app.route('/robots.txt') def robots_txt(): diff --git a/app/api.py b/app/api.py index 0cb8719..c74efa3 100644 --- a/app/api.py +++ b/app/api.py @@ -1,11 +1,11 @@ from flask import Blueprint, jsonify, request -import sys, os, datetime, re +import sys, os, datetime, re, uuid from functools import wraps from peewee import IntegrityError from .models import User, UserProfile, Message, Upload, Relationship, database, \ 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 + create_mentions, is_username, generate_access_token, pwdhash bp = Blueprint('api', __name__, url_prefix='/api/V1') @@ -64,17 +64,33 @@ def feed(self): if date is None: date = datetime.datetime.now() else: - date = datetime.datetime.fromtimestamp(date) + date = datetime.datetime.fromtimestamp(float(date)) query = Visibility(Message .select() .where(((Message.user << self.following()) | (Message.user == self)) & (Message.pub_date < date)) - .order_by(Message.pub_date.desc()) - .limit(20)) - for message in query: + .order_by(Message.pub_date.desc())) + for message in query.paginate(1): timeline_media.append(get_message_info(message)) - return {'timeline_media': timeline_media} + return {'timeline_media': timeline_media, 'has_more': query.count() > len(timeline_media)} + +@bp.route('/explore') +@validate_access +def explore(self): + timeline_media = [] + date = request.args.get('offset') + if date is None: + date = datetime.datetime.now() + else: + date = datetime.datetime.fromtimestamp(float(date)) + query = Visibility(Message + .select() + .where(Message.pub_date < date) + .order_by(Message.pub_date.desc()), True) + for message in query.paginate(1): + timeline_media.append(get_message_info(message)) + return {'timeline_media': timeline_media, 'has_more': query.count() > len(timeline_media)} @bp.route('/create', methods=['POST']) @validate_access @@ -88,7 +104,7 @@ def create(self): pub_date=datetime.datetime.now(), privacy=privacy) # This API does not support files. Use create2 instead. - create_mentions(self, text) + create_mentions(self, text, privacy) return {} @bp.route('/create2', methods=['POST']) @@ -110,7 +126,7 @@ def create2(self): message=message ) file.save(os.path.join(UPLOAD_DIRECTORY, str(upload.id) + '.' + ext)) - create_mentions(self, text) + create_mentions(self, text, privacy) return {} def get_relationship_info(self, other): @@ -169,16 +185,15 @@ def profile_feed(self, userid): if date is None: date = datetime.datetime.now() else: - date = datetime.datetime.fromtimestamp(date) + date = datetime.datetime.fromtimestamp(float(date)) query = Visibility(Message .select() .where((Message.user == user) & (Message.pub_date < date)) - .order_by(Message.pub_date.desc()) - .limit(20)) - for message in query: + .order_by(Message.pub_date.desc())) + for message in query.paginate(1): timeline_media.append(get_message_info(message)) - return {'timeline_media': timeline_media} + return {'timeline_media': timeline_media, 'has_more': query.count() > len(timeline_media)} @bp.route('/relationships//follow', methods=['POST']) @validate_access @@ -305,3 +320,31 @@ def save_edit(self, id): data = request.get_json(True) Message.update(text=data['text'], privacy=data['privacy']).where(Message.id == id).execute() return {} + +# no validate access for this endpoint! +@bp.route('/create_account', methods=['POST']) +def create_account(): + try: + data = request.get_json(True) + try: + birthday = datetime.datetime.fromisoformat(data['birthday']) + except ValueError: + raise ValueError('invalid date format') + username = data['username'].lower() + if not is_username(username): + raise ValueError('invalid username') + with database.atomic(): + user = User.create( + username=username, + full_name=data.get('full_name') or username, + password=pwdhash(data['password']), + email=data['email'], + birthday=birthday, + join_date=datetime.datetime.now()) + UserProfile.create( + user=user + ) + + return jsonify({'access_token': generate_access_token(user), 'status': 'ok'}) + except Exception as e: + return jsonify({'message': str(e), 'status': 'fail'}) diff --git a/app/utils.py b/app/utils.py index f459bf1..e39e476 100644 --- a/app/utils.py +++ b/app/utils.py @@ -198,7 +198,7 @@ def check_access_token(token): if h.hexdigest()[:32] == hh: return user -def create_mentions(cur_user, text): +def create_mentions(cur_user, text, privacy): # create mentions mention_usernames = set() for mo in re.finditer(r'\+([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)', text): diff --git a/app/website.py b/app/website.py index 9d7d13d..f5867e3 100644 --- a/app/website.py +++ b/app/website.py @@ -188,22 +188,7 @@ def create(): message=message ) file.save(UPLOAD_DIRECTORY + str(upload.id) + '.' + ext) - # create mentions - mention_usernames = set() - for mo in re.finditer(r'\+([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)', text): - mention_usernames.add(mo.group(1)) - # to avoid self mention - mention_usernames.difference_update({user.username}) - for u in mention_usernames: - try: - mention_user = User.get(User.username == u) - if privacy in (MSGPRV_PUBLIC, MSGPRV_UNLISTED) or \ - (privacy == MSGPRV_FRIENDS and - mention_user.is_following(user) and - user.is_following(mention_user)): - push_notification('mention', mention_user, user=user.id) - except User.DoesNotExist: - pass + create_mentions(user, text, privacy) flash('Your message has been posted successfully') return redirect(url_for('website.user_detail', username=user.username)) return render_template('create.html') diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..89a340743bced95ba012163e32d0c525a1eed6f9 GIT binary patch literal 1150 zcmbu7Q4WJJ3`0F>2jFYp``ppE{{^~)=&4>QLX4o|#MnuJN{_gfMR9*?eM&D$pIAW! zQ+&Wdv?kSR5h2=E<{r+Y9}&H4liSyQ=2?$3Z=bn;2fSeIrR&%mJ+2~s6TO$$o{t)T zmbuqxk2v^kaNMr?@qu^zc-fPq=Q(SpooQT4J|ex6|0kH6Ue00V^)KiTjykX1KkJcP b2D%xRqJN{~dfun{_j81Q Date: Mon, 25 Nov 2019 09:39:33 +0100 Subject: [PATCH 02/15] Adding notifications and +1's to messages --- CHANGELOG.md | 5 +- app/ajax.py | 30 +++++++++- app/api.py | 87 ++++++++++++++++++++++++++++- app/models.py | 26 ++++++++- app/static/lib.js | 18 ++++++ app/templates/includes/message.html | 10 +++- 6 files changed, 168 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e8a425..4e027d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,13 @@ ## 0.9-dev +* 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. +* Added notifications support for API. * Added `create_account` endpoint to API. This endpoint does not require an access token. +* Added `explore`, `notifications_count`, `notifications` and `notifications_seen` endpoints. * Added `has_more` field to feed endpoints (`feed`, `explore` and `profile_feed`). +* Added `join_date` field into `user` object of `profile_info` endpoint, for more profile transparency. * Added `/favicon.ico`. -* Added `explore` endpoint. * Fixed some bugs when creating mentions and using offsets in feeds. ## 0.8.0 diff --git a/app/ajax.py b/app/ajax.py index 63dc532..d2c0be3 100644 --- a/app/ajax.py +++ b/app/ajax.py @@ -5,8 +5,9 @@ Warning: this is not the public API. ''' from flask import Blueprint, jsonify -from .models import User +from .models import User, Message, MessageUpvote from .utils import locations, get_current_user, is_username +import datetime bp = Blueprint('ajax', __name__, url_prefix='/ajax') @@ -35,3 +36,30 @@ def location_search(name): if value.lower().startswith(name.lower()): results.append({'value': key, 'display': value}) return jsonify({'results': results}) + +@bp.route('/score//toggle', methods=['POST']) +def score_toggle(id): + user = get_current_user() + message = Message[id] + upvoted_by_self = (MessageUpvote + .select() + .where((MessageUpvote.message == message) & (MessageUpvote.user == user)) + .exists()) + if upvoted_by_self: + (MessageUpvote + .delete() + .where( + (MessageUpvote.message == message) & + (MessageUpvote.user == user)) + .execute() + ) + else: + MessageUpvote.create( + message=message, + user=user, + created_date=datetime.datetime.now() + ) + return jsonify({ + "score": message.score, + "status": "ok" + }) diff --git a/app/api.py b/app/api.py index c74efa3..6ee0fd3 100644 --- a/app/api.py +++ b/app/api.py @@ -2,7 +2,8 @@ from flask import Blueprint, jsonify, request import sys, os, datetime, re, uuid from functools import wraps from peewee import IntegrityError -from .models import User, UserProfile, Message, Upload, Relationship, database, \ +from .models import User, UserProfile, Message, Upload, Relationship, Notification, \ + MessageUpvote, database, \ 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 @@ -25,7 +26,9 @@ def get_message_info(message): 'text': message.text, 'privacy': message.privacy, 'pub_date': message.pub_date.timestamp(), - 'media': media + 'media': media, + 'score': len(message.upvotes), + 'upvoted_by_self': message.upvoted_by_self(), } def validate_access(func): @@ -162,6 +165,7 @@ def profile_info(self, userid): "generation": profile.year, "instagram": profile.instagram, "facebook": profile.facebook, + "join_date": user.join_date.timestamp(), "relationships": get_relationship_info(self, user), "messages_count": len(user.messages), "followers_count": len(user.followers()), @@ -348,3 +352,82 @@ def create_account(): return jsonify({'access_token': generate_access_token(user), 'status': 'ok'}) except Exception as e: return jsonify({'message': str(e), 'status': 'fail'}) + +def get_notification_info(notification): + obj = { + "id": notification.id, + "type": notification.type, + "timestamp": notification.pub_date.timestamp(), + "seen": notification.seen + } + obj.update(json.loads(notification.detail)) + return obj + +@bp.route('/notifications/count') +@validate_access +def notifications_count(self): + count = len(Notification + .select() + .where((Notification.target == self) & (Notification.seen == 0))) + return { + 'count': count + } + +@bp.route('/notifications') +@validate_access +def notifications(self): + items = [] + query = (Notification + .select() + .where(Notification.target == self) + .order_by(Notification.pub_date.desc()) + .limit(100)) + unseen_count = len(Notification + .select() + .where((Notification.target == self) & (Notification.seen == 0))) + for notification in query: + items.append(get_notification_info(query)) + return { + "notifications": { + "items": items, + "unseen_count": unseen_count + } + } + +@bp.route('/notifications/seen', methods=['POST']) +@validate_access +def notifications_seen(self): + data = request.get_json(True) + (Notification + .update(seen=1) + .where((Notification.target == self) & (Notification.pub_date < data['offset'])) + .execute()) + return {} + +@bp.route('/score/message//add', methods=['POST']) +@validate_access +def score_message_add(self, id): + message = Message[id] + MessageUpvote.create( + message=message, + user=self, + created_date=datetime.datetime.now() + ) + return { + 'score': len(message.upvotes) + } + +@bp.route('/score/message//remove', methods=['POST']) +@validate_access +def score_message_remove(self, id): + message = Message[id] + (MessageUpvote + .delete() + .where( + (MessageUpvote.message == message) & + (MessageUpvote.user == self)) + .execute() + ) + return { + 'score': len(message.upvotes) + } diff --git a/app/models.py b/app/models.py index ec37798..4d2cc8c 100644 --- a/app/models.py +++ b/app/models.py @@ -162,6 +162,17 @@ class Message(BaseModel): return user.is_following(cur_user) and cur_user.is_following(user) else: return False + @property + def score(self): + return self.upvotes.count() + def upvoted_by_self(self): + from .utils import get_current_user + user = get_current_user() + return (MessageUpvote + .select() + .where((MessageUpvote.message == self) & (MessageUpvote.user == user)) + .exists() + ) # this model contains two foreign keys to user -- it essentially allows us to # model a "many-to-many" relationship between users. by querying and joining @@ -226,6 +237,8 @@ report_reasons = [ (REPORT_REASON_FIREARMS, "Sale or promotion of firearms"), (REPORT_REASON_DRUGS, "Sale or promotion of drugs"), (REPORT_REASON_UNDERAGE, "This user is less than 13 years old"), + (REPORT_REASON_LEAK, "Leak of sensitive information"), + (REPORT_REASON_DMCA, "Copyright violation") ] REPORT_STATUS_DELIVERED = 0 @@ -251,10 +264,21 @@ class Report(BaseModel): except DoesNotExist: return +# New in 0.9. +class MessageUpvote(BaseModel): + message = ForeignKeyField(Message, backref='upvotes') + user = ForeignKeyField(User) + created_date = DateTimeField() + + class Meta: + indexes = ( + (('message', 'user'), True), + ) + def create_tables(): with database: database.create_tables([ User, UserAdminship, UserProfile, Message, Relationship, - Upload, Notification, Report]) + Upload, Notification, Report, MessageUpvote]) if not os.path.isdir(UPLOAD_DIRECTORY): os.makedirs(UPLOAD_DIRECTORY) diff --git a/app/static/lib.js b/app/static/lib.js index 538218a..cc78bea 100644 --- a/app/static/lib.js +++ b/app/static/lib.js @@ -98,3 +98,21 @@ function showHideMessageOptions(id){ options.style.display = 'block'; } } + +function toggleUpvote(id){ + var msgElem = document.getElementById(id); + var upvoteLink = msgElem.getElementsByClassName('message-upvote')[0]; + var scoreCounter = msgElem.getElementsByClassName('message-score')[0]; + var xhr = new XMLHttpRequest(); + xhr.open("POST", "/ajax/score/" + id + "/toggle", true); + xhr.onreadystatechange = function(){ + if(xhr.readyState == XMLHttpRequest.DONE){ + if(xhr.status == 200){ + console.log('liked #' + id); + var data = JSON.parse(xhr.responseText); + scoreCounter.innerHTML = data.score; + } + } + }; + xhr.send(); +} diff --git a/app/templates/includes/message.html b/app/templates/includes/message.html index 8095ab2..aa10503 100644 --- a/app/templates/includes/message.html +++ b/app/templates/includes/message.html @@ -5,12 +5,16 @@ {% endif %}

{{ site_name }}

@@ -36,7 +36,8 @@ diff --git a/app/templates/explore.html b/app/templates/explore.html index ed3aab2..65d1846 100644 --- a/app/templates/explore.html +++ b/app/templates/explore.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block body %}

Explore

-
    +
      {% for message in message_list %}
    • {% include "includes/message.html" %}
    • {% endfor %} diff --git a/app/templates/feed.html b/app/templates/feed.html index 4a57123..88b05ac 100644 --- a/app/templates/feed.html +++ b/app/templates/feed.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block body %}

      Your Timeline

      -
        +
          {% for message in message_list %}
        • {% include "includes/message.html" %}
        • {% endfor %} diff --git a/app/templates/includes/infobox_profile.html b/app/templates/includes/infobox_profile.html index f20df41..2cec0b7 100644 --- a/app/templates/includes/infobox_profile.html +++ b/app/templates/includes/infobox_profile.html @@ -30,6 +30,6 @@ {{ user.following()|count }} following

          {% if user == current_user %} -

          Edit profile

          +

          {{ inline_svg('edit', 18) }} Edit profile

          {% endif %} diff --git a/app/templates/user_detail.html b/app/templates/user_detail.html index 5a5c42f..9b5e7a5 100644 --- a/app/templates/user_detail.html +++ b/app/templates/user_detail.html @@ -18,7 +18,7 @@ Create a message {% endif %} {% endif %} -
            +
              {% for message in message_list %}
            • {% include "includes/message.html" %}
            • {% endfor %} diff --git a/app/utils.py b/app/utils.py index e39e476..dfd2518 100644 --- a/app/utils.py +++ b/app/utils.py @@ -5,7 +5,7 @@ A list of utilities used across modules. import datetime, re, base64, hashlib, string, sys, json from .models import User, Message, Notification, MSGPRV_PUBLIC, MSGPRV_UNLISTED, \ MSGPRV_FRIENDS, MSGPRV_ONLYME -from flask import abort, render_template, request, session +from flask import Markup, abort, render_template, request, session _forbidden_extensions = 'com net org txt'.split() _username_characters = frozenset(string.ascii_letters + string.digits + '_') @@ -82,7 +82,7 @@ class Visibility(object): def get_locations(): data = {} - with open('locations.txt') as f: + with open('locations.txt', encoding='utf-8') as f: for line in f: line = line.rstrip() if line.startswith('#'): @@ -215,3 +215,14 @@ def create_mentions(cur_user, text, privacy): push_notification('mention', mention_user, user=user.id) except User.DoesNotExist: pass + +# New in 0.9 +def inline_svg(name, width=None): + try: + with open('icons/' + name + '-24px.svg') as f: + data = f.read() + if isinstance(width, int): + data = re.sub(r'( (?:height|width)=")\d+(")', lambda x:x.group(1) + str(width) + x.group(2), data) + return Markup(data) + except OSError: + return '' diff --git a/icons/edit-24px.svg b/icons/edit-24px.svg new file mode 100644 index 0000000..a6f23ff --- /dev/null +++ b/icons/edit-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/exit_to_app-24px.svg b/icons/exit_to_app-24px.svg new file mode 100644 index 0000000..2f0decb --- /dev/null +++ b/icons/exit_to_app-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/explore-24px.svg b/icons/explore-24px.svg new file mode 100644 index 0000000..9e72b8b --- /dev/null +++ b/icons/explore-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/notifications-24px.svg b/icons/notifications-24px.svg new file mode 100644 index 0000000..6d5dfe6 --- /dev/null +++ b/icons/notifications-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/person-24px.svg b/icons/person-24px.svg new file mode 100644 index 0000000..58b25d9 --- /dev/null +++ b/icons/person-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/person_add-24px.svg b/icons/person_add-24px.svg new file mode 100644 index 0000000..40736bb --- /dev/null +++ b/icons/person_add-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/shuffle-24px.svg b/icons/shuffle-24px.svg new file mode 100644 index 0000000..a3efe19 --- /dev/null +++ b/icons/shuffle-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file From 5ba9f1d7d5c382c06ddf9e0f88e7edb2df0c5204 Mon Sep 17 00:00:00 2001 From: Mattia Succurro Date: Sun, 16 Jun 2024 11:22:54 +0200 Subject: [PATCH 04/15] CSS changes --- app/models.py | 2 +- app/static/style.css | 1 + app/templates/base.html | 12 ++++++------ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/models.py b/app/models.py index 865513b..4cdb2b5 100644 --- a/app/models.py +++ b/app/models.py @@ -156,7 +156,7 @@ class Message(BaseModel): # even if unlisted return not is_public_timeline elif privacy == MSGPRV_FRIENDS: - if cur_user.is_anonymous: + if not cur_user or cur_user.is_anonymous: return False return user.is_following(cur_user) and cur_user.is_following(user) else: diff --git a/app/static/style.css b/app/static/style.css index aaef997..87152a8 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -13,6 +13,7 @@ a:hover{text-decoration:underline} .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} diff --git a/app/templates/base.html b/app/templates/base.html index dae5889..b2f79bc 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -12,18 +12,18 @@

              {{ site_name }}

              From 71619dba2b8168781b616aae18584a858e41b585 Mon Sep 17 00:00:00 2001 From: Mattia Succurro Date: Sun, 16 Jun 2024 11:31:56 +0200 Subject: [PATCH 05/15] Fix imports --- .gitignore | 9 +++++++++ app/filters.py | 2 +- app/utils.py | 3 ++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index c9522f1..d320dae 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,12 @@ uploads/ **~ **/.*.swp **/__pycache__/ +venv +.env +.venv +env +data/ +conf/ +config/ +\#*\# +.\#* diff --git a/app/filters.py b/app/filters.py index cbb9dbb..04d929a 100644 --- a/app/filters.py +++ b/app/filters.py @@ -2,7 +2,7 @@ Filter functions used in the website templates. ''' -from flask import Markup +from markupsafe import Markup import html, datetime, re, time from .utils import tokenize, inline_svg as _inline_svg from . import app diff --git a/app/utils.py b/app/utils.py index dfd2518..5259b80 100644 --- a/app/utils.py +++ b/app/utils.py @@ -5,7 +5,8 @@ A list of utilities used across modules. import datetime, re, base64, hashlib, string, sys, json from .models import User, Message, Notification, MSGPRV_PUBLIC, MSGPRV_UNLISTED, \ MSGPRV_FRIENDS, MSGPRV_ONLYME -from flask import Markup, abort, render_template, request, session +from flask import abort, render_template, request, session +from markupsafe import Markup _forbidden_extensions = 'com net org txt'.split() _username_characters = frozenset(string.ascii_letters + string.digits + '_') From b874b989bfad7242884cf8ab3dc3262adce0132f Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Thu, 6 Nov 2025 05:48:17 +0100 Subject: [PATCH 06/15] 0.9.0 --- app/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index 3b8ab12..0ef203e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -23,7 +23,7 @@ import datetime, time, re, os, sys, string, json, html from functools import wraps from flask_login import LoginManager -__version__ = '0.9-dev' +__version__ = '0.9.0' # we want to support Python 3 only. # Python 2 has too many caveats. From 71f7bd1a3b501014eab6a08b24037a02e9d9ecdc Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Thu, 6 Nov 2025 06:33:31 +0100 Subject: [PATCH 07/15] refactor code layout, move config to .env, add pyproject.toml --- .gitignore | 7 +++- CHANGELOG.md | 9 ++++- config.py | 3 -- pyproject.toml | 21 ++++++++++ run_example.py | 16 -------- {app => src/coriplus}/__init__.py | 37 +++++++++++------- {app => src/coriplus}/__main__.py | 0 {app => src/coriplus}/admin.py | 0 {app => src/coriplus}/ajax.py | 0 {app => src/coriplus}/api.py | 7 +++- {app => src/coriplus}/filters.py | 0 {app => src/coriplus}/models.py | 0 {app => src/coriplus}/reports.py | 0 {app => src/coriplus}/static/lib.js | 0 {app => src/coriplus}/static/style.css | 0 {app => src/coriplus}/templates/404.html | 0 {app => src/coriplus}/templates/about.html | 0 .../coriplus}/templates/admin_base.html | 0 .../coriplus}/templates/admin_home.html | 0 .../templates/admin_report_detail.html | 0 .../coriplus}/templates/admin_reports.html | 0 {app => src/coriplus}/templates/base.html | 0 .../coriplus}/templates/change_password.html | 0 .../coriplus}/templates/confirm_delete.html | 0 {app => src/coriplus}/templates/create.html | 0 {app => src/coriplus}/templates/edit.html | 0 .../coriplus}/templates/edit_profile.html | 0 {app => src/coriplus}/templates/explore.html | 0 {app => src/coriplus}/templates/feed.html | 0 {app => src/coriplus}/templates/homepage.html | 0 .../templates/includes/infobox_profile.html | 0 .../templates/includes/location_selector.html | 0 .../coriplus}/templates/includes/message.html | 0 .../templates/includes/notification.html | 0 .../templates/includes/pagination.html | 0 .../templates/includes/reported_message.html | 0 {app => src/coriplus}/templates/join.html | 0 {app => src/coriplus}/templates/login.html | 0 .../coriplus}/templates/notifications.html | 0 {app => src/coriplus}/templates/privacy.html | 0 .../coriplus}/templates/report_base.html | 0 .../coriplus}/templates/report_done.html | 0 .../coriplus}/templates/report_message.html | 0 .../coriplus}/templates/report_user.html | 0 {app => src/coriplus}/templates/terms.html | 0 .../coriplus}/templates/user_detail.html | 0 .../coriplus}/templates/user_list.html | 0 {app => src/coriplus}/utils.py | 0 {app => src/coriplus}/website.py | 5 ++- favicon.ico => src/favicon.ico | Bin robots.txt => src/robots.txt | 0 51 files changed, 64 insertions(+), 41 deletions(-) delete mode 100644 config.py create mode 100644 pyproject.toml delete mode 100644 run_example.py rename {app => src/coriplus}/__init__.py (77%) rename {app => src/coriplus}/__main__.py (100%) rename {app => src/coriplus}/admin.py (100%) rename {app => src/coriplus}/ajax.py (100%) rename {app => src/coriplus}/api.py (99%) rename {app => src/coriplus}/filters.py (100%) rename {app => src/coriplus}/models.py (100%) rename {app => src/coriplus}/reports.py (100%) rename {app => src/coriplus}/static/lib.js (100%) rename {app => src/coriplus}/static/style.css (100%) rename {app => src/coriplus}/templates/404.html (100%) rename {app => src/coriplus}/templates/about.html (100%) rename {app => src/coriplus}/templates/admin_base.html (100%) rename {app => src/coriplus}/templates/admin_home.html (100%) rename {app => src/coriplus}/templates/admin_report_detail.html (100%) rename {app => src/coriplus}/templates/admin_reports.html (100%) rename {app => src/coriplus}/templates/base.html (100%) rename {app => src/coriplus}/templates/change_password.html (100%) rename {app => src/coriplus}/templates/confirm_delete.html (100%) rename {app => src/coriplus}/templates/create.html (100%) rename {app => src/coriplus}/templates/edit.html (100%) rename {app => src/coriplus}/templates/edit_profile.html (100%) rename {app => src/coriplus}/templates/explore.html (100%) rename {app => src/coriplus}/templates/feed.html (100%) rename {app => src/coriplus}/templates/homepage.html (100%) rename {app => src/coriplus}/templates/includes/infobox_profile.html (100%) rename {app => src/coriplus}/templates/includes/location_selector.html (100%) rename {app => src/coriplus}/templates/includes/message.html (100%) rename {app => src/coriplus}/templates/includes/notification.html (100%) rename {app => src/coriplus}/templates/includes/pagination.html (100%) rename {app => src/coriplus}/templates/includes/reported_message.html (100%) rename {app => src/coriplus}/templates/join.html (100%) rename {app => src/coriplus}/templates/login.html (100%) rename {app => src/coriplus}/templates/notifications.html (100%) rename {app => src/coriplus}/templates/privacy.html (100%) rename {app => src/coriplus}/templates/report_base.html (100%) rename {app => src/coriplus}/templates/report_done.html (100%) rename {app => src/coriplus}/templates/report_message.html (100%) rename {app => src/coriplus}/templates/report_user.html (100%) rename {app => src/coriplus}/templates/terms.html (100%) rename {app => src/coriplus}/templates/user_detail.html (100%) rename {app => src/coriplus}/templates/user_list.html (100%) rename {app => src/coriplus}/utils.py (100%) rename {app => src/coriplus}/website.py (99%) rename favicon.ico => src/favicon.ico (100%) rename robots.txt => src/robots.txt (100%) diff --git a/.gitignore b/.gitignore index d320dae..546be68 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,8 @@ __pycache__/ uploads/ *.pyc **~ -**/.*.swp -**/__pycache__/ +.*.swp +__pycache__/ venv .env .venv @@ -15,3 +15,6 @@ conf/ config/ \#*\# .\#* +node_modules/ +alembic.ini +**.egg-info \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 105a0ad..ff728bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog -## 0.9-dev +## 0.10.0 ++ Codebase refactor (with breaking changes!) ++ Move ALL config to .env (config.py is NO MORE supported) ++ Config SITE_NAME replaced with APP_NAME + +## 0.9.0 * 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. @@ -26,7 +31,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-dev +## 0.7.1 * Adding `messages_count`, `followers_count` and `following_count` to `profile_info` API endpoint (forgot to release). diff --git a/config.py b/config.py deleted file mode 100644 index a6e2b64..0000000 --- a/config.py +++ /dev/null @@ -1,3 +0,0 @@ -DEBUG = True -SECRET_KEY = 'hin6bab8ge25*r=x&+5$0kn=-#log$pt^#@vrqjld!^2ci@g*b' -SITE_NAME = 'Cori+' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f4d5b53 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "sakuragasaki46_coriplus" +authors = [ + { name = "Sakuragasaki46" } +] +dynamic = ["version"] +dependencies = [ + "Python-Dotenv>=1.0.0", + "Flask", + "Flask-Login", + "Peewee" +] +requires-python = ">=3.10" +classifiers = [ + "Private :: X" +] + +[tool.setuptools.dynamic] +version = { attr = "coriplus.__version__" } + + diff --git a/run_example.py b/run_example.py deleted file mode 100644 index 4ab76a9..0000000 --- a/run_example.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python - -import sys -sys.path.insert(0, '../..') - -import argparse -parser = argparse.ArgumentParser() -parser.add_argument('-p', '--port', type=int, default=5000, - help='An alternative port where to run the server.') - -from app import app, create_tables - -if __name__ == '__main__': - args = parser.parse_args() - create_tables() - app.run(port=args.port) diff --git a/app/__init__.py b/src/coriplus/__init__.py similarity index 77% rename from app/__init__.py rename to src/coriplus/__init__.py index 0ef203e..4f01715 100644 --- a/app/__init__.py +++ b/src/coriplus/__init__.py @@ -16,24 +16,31 @@ For other, see `app.utils`. ''' from flask import ( - 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 + Flask, g, jsonify, render_template, request, + send_from_directory, __version__ as flask_version) +import os, sys from flask_login import LoginManager +import dotenv +import logging -__version__ = '0.9.0' +__version__ = '0.10.0-dev44' -# we want to support Python 3 only. +# we want to support Python 3.10+ only. # Python 2 has too many caveats. -if sys.version_info[0] < 3: - raise RuntimeError('Python 3 required') +# Python <=3.9 has harder type support. +if sys.version_info[0:2] < (3, 10): + raise RuntimeError('Python 3.10+ required') -os.chdir(os.path.dirname(os.path.dirname(__file__))) +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__) app = Flask(__name__) -app.config.from_pyfile('../config.py') +app.secret_key = os.environ['SECRET_KEY'] login_manager = LoginManager(app) @@ -53,7 +60,7 @@ def before_request(): try: g.db.connect() except OperationalError: - sys.stderr.write('database connected twice.\n') + logger.error('database connected twice.\n') @app.after_request def after_request(response): @@ -63,7 +70,7 @@ def after_request(response): @app.context_processor def _inject_variables(): return { - 'site_name': app.config['SITE_NAME'], + 'site_name': os.environ.get('APP_NAME', 'Cori+'), 'locations': locations, 'inline_svg': inline_svg } @@ -78,11 +85,11 @@ def error_404(body): @app.route('/favicon.ico') def favicon_ico(): - return send_from_directory(os.getcwd(), 'favicon.ico') + return send_from_directory(BASEDIR, 'src/favicon.ico') @app.route('/robots.txt') def robots_txt(): - return send_from_directory(os.getcwd(), 'robots.txt') + return send_from_directory(BASEDIR, 'src/robots.txt') @app.route('/uploads/.') def uploads(id, type='jpg'): diff --git a/app/__main__.py b/src/coriplus/__main__.py similarity index 100% rename from app/__main__.py rename to src/coriplus/__main__.py diff --git a/app/admin.py b/src/coriplus/admin.py similarity index 100% rename from app/admin.py rename to src/coriplus/admin.py diff --git a/app/ajax.py b/src/coriplus/ajax.py similarity index 100% rename from app/ajax.py rename to src/coriplus/ajax.py diff --git a/app/api.py b/src/coriplus/api.py similarity index 99% rename from app/api.py rename to src/coriplus/api.py index 6ee0fd3..5f58c2c 100644 --- a/app/api.py +++ b/src/coriplus/api.py @@ -7,6 +7,9 @@ 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') @@ -16,7 +19,7 @@ def get_message_info(message): except IndexError: media = None if media: - print(media) + logger.debug(media) return { 'id': message.id, 'user': { @@ -122,7 +125,7 @@ def create2(self): privacy=privacy) file = request.files.get('file') if file: - print('Uploading', file.filename) + logger.info('Uploading', file.filename) ext = file.filename.split('.')[-1] upload = Upload.create( type=ext, diff --git a/app/filters.py b/src/coriplus/filters.py similarity index 100% rename from app/filters.py rename to src/coriplus/filters.py diff --git a/app/models.py b/src/coriplus/models.py similarity index 100% rename from app/models.py rename to src/coriplus/models.py diff --git a/app/reports.py b/src/coriplus/reports.py similarity index 100% rename from app/reports.py rename to src/coriplus/reports.py diff --git a/app/static/lib.js b/src/coriplus/static/lib.js similarity index 100% rename from app/static/lib.js rename to src/coriplus/static/lib.js diff --git a/app/static/style.css b/src/coriplus/static/style.css similarity index 100% rename from app/static/style.css rename to src/coriplus/static/style.css diff --git a/app/templates/404.html b/src/coriplus/templates/404.html similarity index 100% rename from app/templates/404.html rename to src/coriplus/templates/404.html diff --git a/app/templates/about.html b/src/coriplus/templates/about.html similarity index 100% rename from app/templates/about.html rename to src/coriplus/templates/about.html diff --git a/app/templates/admin_base.html b/src/coriplus/templates/admin_base.html similarity index 100% rename from app/templates/admin_base.html rename to src/coriplus/templates/admin_base.html diff --git a/app/templates/admin_home.html b/src/coriplus/templates/admin_home.html similarity index 100% rename from app/templates/admin_home.html rename to src/coriplus/templates/admin_home.html diff --git a/app/templates/admin_report_detail.html b/src/coriplus/templates/admin_report_detail.html similarity index 100% rename from app/templates/admin_report_detail.html rename to src/coriplus/templates/admin_report_detail.html diff --git a/app/templates/admin_reports.html b/src/coriplus/templates/admin_reports.html similarity index 100% rename from app/templates/admin_reports.html rename to src/coriplus/templates/admin_reports.html diff --git a/app/templates/base.html b/src/coriplus/templates/base.html similarity index 100% rename from app/templates/base.html rename to src/coriplus/templates/base.html diff --git a/app/templates/change_password.html b/src/coriplus/templates/change_password.html similarity index 100% rename from app/templates/change_password.html rename to src/coriplus/templates/change_password.html diff --git a/app/templates/confirm_delete.html b/src/coriplus/templates/confirm_delete.html similarity index 100% rename from app/templates/confirm_delete.html rename to src/coriplus/templates/confirm_delete.html diff --git a/app/templates/create.html b/src/coriplus/templates/create.html similarity index 100% rename from app/templates/create.html rename to src/coriplus/templates/create.html diff --git a/app/templates/edit.html b/src/coriplus/templates/edit.html similarity index 100% rename from app/templates/edit.html rename to src/coriplus/templates/edit.html diff --git a/app/templates/edit_profile.html b/src/coriplus/templates/edit_profile.html similarity index 100% rename from app/templates/edit_profile.html rename to src/coriplus/templates/edit_profile.html diff --git a/app/templates/explore.html b/src/coriplus/templates/explore.html similarity index 100% rename from app/templates/explore.html rename to src/coriplus/templates/explore.html diff --git a/app/templates/feed.html b/src/coriplus/templates/feed.html similarity index 100% rename from app/templates/feed.html rename to src/coriplus/templates/feed.html diff --git a/app/templates/homepage.html b/src/coriplus/templates/homepage.html similarity index 100% rename from app/templates/homepage.html rename to src/coriplus/templates/homepage.html diff --git a/app/templates/includes/infobox_profile.html b/src/coriplus/templates/includes/infobox_profile.html similarity index 100% rename from app/templates/includes/infobox_profile.html rename to src/coriplus/templates/includes/infobox_profile.html diff --git a/app/templates/includes/location_selector.html b/src/coriplus/templates/includes/location_selector.html similarity index 100% rename from app/templates/includes/location_selector.html rename to src/coriplus/templates/includes/location_selector.html diff --git a/app/templates/includes/message.html b/src/coriplus/templates/includes/message.html similarity index 100% rename from app/templates/includes/message.html rename to src/coriplus/templates/includes/message.html diff --git a/app/templates/includes/notification.html b/src/coriplus/templates/includes/notification.html similarity index 100% rename from app/templates/includes/notification.html rename to src/coriplus/templates/includes/notification.html diff --git a/app/templates/includes/pagination.html b/src/coriplus/templates/includes/pagination.html similarity index 100% rename from app/templates/includes/pagination.html rename to src/coriplus/templates/includes/pagination.html diff --git a/app/templates/includes/reported_message.html b/src/coriplus/templates/includes/reported_message.html similarity index 100% rename from app/templates/includes/reported_message.html rename to src/coriplus/templates/includes/reported_message.html diff --git a/app/templates/join.html b/src/coriplus/templates/join.html similarity index 100% rename from app/templates/join.html rename to src/coriplus/templates/join.html diff --git a/app/templates/login.html b/src/coriplus/templates/login.html similarity index 100% rename from app/templates/login.html rename to src/coriplus/templates/login.html diff --git a/app/templates/notifications.html b/src/coriplus/templates/notifications.html similarity index 100% rename from app/templates/notifications.html rename to src/coriplus/templates/notifications.html diff --git a/app/templates/privacy.html b/src/coriplus/templates/privacy.html similarity index 100% rename from app/templates/privacy.html rename to src/coriplus/templates/privacy.html diff --git a/app/templates/report_base.html b/src/coriplus/templates/report_base.html similarity index 100% rename from app/templates/report_base.html rename to src/coriplus/templates/report_base.html diff --git a/app/templates/report_done.html b/src/coriplus/templates/report_done.html similarity index 100% rename from app/templates/report_done.html rename to src/coriplus/templates/report_done.html diff --git a/app/templates/report_message.html b/src/coriplus/templates/report_message.html similarity index 100% rename from app/templates/report_message.html rename to src/coriplus/templates/report_message.html diff --git a/app/templates/report_user.html b/src/coriplus/templates/report_user.html similarity index 100% rename from app/templates/report_user.html rename to src/coriplus/templates/report_user.html diff --git a/app/templates/terms.html b/src/coriplus/templates/terms.html similarity index 100% rename from app/templates/terms.html rename to src/coriplus/templates/terms.html diff --git a/app/templates/user_detail.html b/src/coriplus/templates/user_detail.html similarity index 100% rename from app/templates/user_detail.html rename to src/coriplus/templates/user_detail.html diff --git a/app/templates/user_list.html b/src/coriplus/templates/user_list.html similarity index 100% rename from app/templates/user_list.html rename to src/coriplus/templates/user_list.html diff --git a/app/utils.py b/src/coriplus/utils.py similarity index 100% rename from app/utils.py rename to src/coriplus/utils.py diff --git a/app/website.py b/src/coriplus/website.py similarity index 99% rename from app/website.py rename to src/coriplus/website.py index f5867e3..df43b86 100644 --- a/app/website.py +++ b/src/coriplus/website.py @@ -9,6 +9,9 @@ from sys import version as python_version from flask import Blueprint, abort, flash, redirect, render_template, request, url_for, __version__ as flask_version from flask_login import login_required, login_user, logout_user import json +import logging + +logger = logging.getLogger(__name__) bp = Blueprint('website', __name__) @@ -181,7 +184,7 @@ def create(): privacy=privacy) file = request.files.get('file') if file: - print('Uploading', file.filename) + logger.info('Uploading', file.filename) ext = file.filename.split('.')[-1] upload = Upload.create( type=ext, diff --git a/favicon.ico b/src/favicon.ico similarity index 100% rename from favicon.ico rename to src/favicon.ico diff --git a/robots.txt b/src/robots.txt similarity index 100% rename from robots.txt rename to src/robots.txt From be24a37f5cd911c4e10e7aa4bcf27ecbe519982f Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Thu, 6 Nov 2025 06:36:02 +0100 Subject: [PATCH 08/15] move old migrations away from project root --- migrate_0_4_to_0_5.py => src/old_migrations/migrate_0_4_to_0_5.py | 0 migrate_0_6_to_0_7.py => src/old_migrations/migrate_0_6_to_0_7.py | 0 migrate_0_7_to_0_8.py => src/old_migrations/migrate_0_7_to_0_8.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename migrate_0_4_to_0_5.py => src/old_migrations/migrate_0_4_to_0_5.py (100%) rename migrate_0_6_to_0_7.py => src/old_migrations/migrate_0_6_to_0_7.py (100%) rename migrate_0_7_to_0_8.py => src/old_migrations/migrate_0_7_to_0_8.py (100%) diff --git a/migrate_0_4_to_0_5.py b/src/old_migrations/migrate_0_4_to_0_5.py similarity index 100% rename from migrate_0_4_to_0_5.py rename to src/old_migrations/migrate_0_4_to_0_5.py diff --git a/migrate_0_6_to_0_7.py b/src/old_migrations/migrate_0_6_to_0_7.py similarity index 100% rename from migrate_0_6_to_0_7.py rename to src/old_migrations/migrate_0_6_to_0_7.py diff --git a/migrate_0_7_to_0_8.py b/src/old_migrations/migrate_0_7_to_0_8.py similarity index 100% rename from migrate_0_7_to_0_8.py rename to src/old_migrations/migrate_0_7_to_0_8.py From c46dce5e3bef094dc726483232ee02049cf6edd9 Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Thu, 6 Nov 2025 07:25:07 +0100 Subject: [PATCH 09/15] add CSRF token --- CHANGELOG.md | 1 + genmig.sh | 6 ++++++ pyproject.toml | 4 +++- src/coriplus/__init__.py | 8 +++++++- src/coriplus/models.py | 3 ++- src/coriplus/templates/about.html | 9 ++++++--- src/coriplus/templates/admin_report_detail.html | 1 + src/coriplus/templates/base.html | 2 +- src/coriplus/templates/change_password.html | 1 + src/coriplus/templates/confirm_delete.html | 9 ++++----- src/coriplus/templates/create.html | 1 + src/coriplus/templates/edit.html | 1 + src/coriplus/templates/edit_profile.html | 1 + src/coriplus/templates/login.html | 1 + src/coriplus/templates/report_message.html | 1 + src/coriplus/templates/report_user.html | 1 + 16 files changed, 38 insertions(+), 12 deletions(-) create mode 100755 genmig.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index ff728bc..24a2232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ + Codebase refactor (with breaking changes!) + Move ALL config to .env (config.py is NO MORE supported) + Config SITE_NAME replaced with APP_NAME ++ Add CSRF token and flask_WTF ## 0.9.0 diff --git a/genmig.sh b/genmig.sh new file mode 100755 index 0000000..bc624ce --- /dev/null +++ b/genmig.sh @@ -0,0 +1,6 @@ +#!/usr/bin/bash +# GENERATE MIGRATIONS + +source venv/bin/activate && \ +source .env && \ +pw_migrate create --auto --auto-source=coriplus.models --directory=src/migrations --database="$DATABASE_URL" "$@" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f4d5b53..72b1800 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,9 @@ dependencies = [ "Python-Dotenv>=1.0.0", "Flask", "Flask-Login", - "Peewee" + "Peewee", + "Flask-WTF", + "peewee-migrate" ] requires-python = ">=3.10" classifiers = [ diff --git a/src/coriplus/__init__.py b/src/coriplus/__init__.py index 4f01715..d829da9 100644 --- a/src/coriplus/__init__.py +++ b/src/coriplus/__init__.py @@ -20,6 +20,7 @@ from flask import ( send_from_directory, __version__ as flask_version) import os, sys from flask_login import LoginManager +from flask_wtf import CSRFProtect import dotenv import logging @@ -44,6 +45,8 @@ app.secret_key = os.environ['SECRET_KEY'] login_manager = LoginManager(app) +CSRFProtect(app) + from .models import * from .utils import * @@ -64,7 +67,10 @@ def before_request(): @app.after_request def after_request(response): - g.db.close() + try: + g.db.close() + except Exception: + logger.error('database closed twice') return response @app.context_processor diff --git a/src/coriplus/models.py b/src/coriplus/models.py index 4cdb2b5..0c9c68e 100644 --- a/src/coriplus/models.py +++ b/src/coriplus/models.py @@ -13,11 +13,12 @@ The tables are: from flask import request from peewee import * +from playhouse.db_url import connect import os # here should go `from .utils import get_current_user`, but it will cause # import errors. It's instead imported at function level. -database = SqliteDatabase('coriplus.sqlite') +database = connect(os.environ['DATABASE_URL']) class BaseModel(Model): class Meta: diff --git a/src/coriplus/templates/about.html b/src/coriplus/templates/about.html index e5691a8..b337caa 100644 --- a/src/coriplus/templates/about.html +++ b/src/coriplus/templates/about.html @@ -3,9 +3,12 @@ {% block body %}

              About {{ site_name }}

              -

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

              -

              Copyright © 2019 Sakuragasaki46.

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

              Copyright © 2019, 2025 Sakuragasaki46.

              License

              Permission is hereby granted, free of charge, to any person obtaining diff --git a/src/coriplus/templates/admin_report_detail.html b/src/coriplus/templates/admin_report_detail.html index d445d64..8f5d2c6 100644 --- a/src/coriplus/templates/admin_report_detail.html +++ b/src/coriplus/templates/admin_report_detail.html @@ -21,6 +21,7 @@ {% include "includes/reported_message.html" %} {% endif %}

              +
              diff --git a/src/coriplus/templates/base.html b/src/coriplus/templates/base.html index b2f79bc..adba498 100644 --- a/src/coriplus/templates/base.html +++ b/src/coriplus/templates/base.html @@ -34,7 +34,7 @@ {% block body %}{% endblock %} {% endblock %} diff --git a/src/coriplus/templates/edit.html b/src/coriplus/templates/edit.html index 5a8f907..316dc7e 100644 --- a/src/coriplus/templates/edit.html +++ b/src/coriplus/templates/edit.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% block body %} +

              Edit

              @@ -15,4 +16,5 @@
              +
              {% endblock %} diff --git a/src/coriplus/templates/edit_profile.html b/src/coriplus/templates/edit_profile.html index 319a9c2..66ecd28 100644 --- a/src/coriplus/templates/edit_profile.html +++ b/src/coriplus/templates/edit_profile.html @@ -1,6 +1,7 @@ {% extends "base.html" %} {% block body %} +

              Edit Profile

              @@ -33,4 +34,5 @@
              +
              {% endblock %} diff --git a/src/coriplus/templates/explore.html b/src/coriplus/templates/explore.html index 65d1846..e86a8f2 100644 --- a/src/coriplus/templates/explore.html +++ b/src/coriplus/templates/explore.html @@ -1,9 +1,10 @@ {% extends "base.html" %} +{% from "macros/message.html" import feed_message with context %} {% block body %}

              Explore

                {% for message in message_list %} -
              • {% include "includes/message.html" %}
              • + {{ feed_message(message) }} {% endfor %}
              {% include "includes/pagination.html" %} diff --git a/src/coriplus/templates/feed.html b/src/coriplus/templates/feed.html index 88b05ac..beb6607 100644 --- a/src/coriplus/templates/feed.html +++ b/src/coriplus/templates/feed.html @@ -1,9 +1,10 @@ {% extends "base.html" %} +{% from "macros/message.html" import feed_message with context %} {% block body %}

              Your Timeline

                {% for message in message_list %} -
              • {% include "includes/message.html" %}
              • + {{ feed_message(message) }} {% endfor %}
              {% include "includes/pagination.html" %} diff --git a/src/coriplus/templates/homepage.html b/src/coriplus/templates/homepage.html index 106bf9a..fdad92f 100644 --- a/src/coriplus/templates/homepage.html +++ b/src/coriplus/templates/homepage.html @@ -1,7 +1,9 @@ {% extends "base.html" %} {% block body %} +

              Hello

              {{ site_name }} is made by people like you.
              Log in or register to see more.

              +
              {% endblock %} diff --git a/src/coriplus/templates/join.html b/src/coriplus/templates/join.html index 607387f..a45b511 100644 --- a/src/coriplus/templates/join.html +++ b/src/coriplus/templates/join.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% block body %} +

              Join {{ site_name }}

              @@ -32,4 +33,5 @@
              +
              {% endblock %} diff --git a/src/coriplus/templates/login.html b/src/coriplus/templates/login.html index dee8219..21a5e93 100644 --- a/src/coriplus/templates/login.html +++ b/src/coriplus/templates/login.html @@ -1,9 +1,10 @@ {% extends "base.html" %} {% block body %}

              Login

              - {% if error %}

              Error: {{ error }}{% endif %} + {% if error %}

              Error: {{ error }}

              {% endif %} +
              - +
              Username or email:
              @@ -19,4 +20,5 @@
              +
              {% endblock %} diff --git a/src/coriplus/templates/macros/message.html b/src/coriplus/templates/macros/message.html new file mode 100644 index 0000000..b4ef93a --- /dev/null +++ b/src/coriplus/templates/macros/message.html @@ -0,0 +1,35 @@ +{% macro feed_message(message) %} +
            • +

              {{ message.text|enrich }}

              +{% if message.uploads %} +
              + +
              +{% endif %} + + +
            • +{% endmacro %} \ No newline at end of file diff --git a/src/coriplus/templates/notifications.html b/src/coriplus/templates/notifications.html index 04b61d1..33ee7bb 100644 --- a/src/coriplus/templates/notifications.html +++ b/src/coriplus/templates/notifications.html @@ -3,7 +3,7 @@

              Notifications

                {% for notification in notification_list %} -
              • {% include "includes/notification.html" %}
              • +
              • {% include "includes/notification.html" %}
              • {% endfor %}
              {% include "includes/pagination.html" %} diff --git a/src/coriplus/templates/privacy.html b/src/coriplus/templates/privacy.html index a7b8570..df4248a 100644 --- a/src/coriplus/templates/privacy.html +++ b/src/coriplus/templates/privacy.html @@ -1,47 +1,54 @@ {% extends "base.html" %} {% block body %} -

              Privacy Policy

              +
              +

              Privacy Policy

              -

              At {{ site_name }}, accessible from {{ request.host }}, one of our main priorities is the privacy of our visitors. This Privacy Policy document contains types of information that is collected and recorded by {{ site_name }} and how we use it.

              +

              At {{ site_name }}, accessible from {{ request.host }}, one of our main priorities is the privacy of our visitors. This Privacy Policy document contains types of information that is collected and recorded by {{ site_name }} and how we use it.

              -

              If you have additional questions or require more information about our Privacy Policy, do not hesitate to contact us through email at sakuragasaki46@gmail.com

              +

              If you have additional questions or require more information about our Privacy Policy, do not hesitate to contact us through email at sakuragasaki46@gmail.com

              -

              Log Files

              +

              Log Files

              -

              {{ site_name }} follows a standard procedure of using log files. These files log visitors when they visit websites. All hosting companies do this and a part of hosting services' analytics. The information collected by log files include internet protocol (IP) addresses, browser type, Internet Service Provider (ISP), date and time stamp, referring/exit pages, and possibly the number of clicks. These are not linked to any information that is personally identifiable. The purpose of the information is for analyzing trends, administering the site, tracking users' movement on the website, and gathering demographic information.

              +

              {{ site_name }} follows a standard procedure of using log files. These files log visitors when they visit websites. All hosting companies do this and a part of hosting services' analytics. The information collected by log files include internet protocol (IP) addresses, browser type, Internet Service Provider (ISP), date and time stamp, referring/exit pages, and possibly the number of clicks. These are not linked to any information that is personally identifiable. The purpose of the information is for analyzing trends, administering the site, tracking users' movement on the website, and gathering demographic information.

              -

              Cookies and Web Beacons

              +

              Cookies and Web Beacons

              -

              Like any other website, {{ site_name }} uses 'cookies'. These cookies are used to store information including visitors' preferences, and the pages on the website that the visitor accessed or visited. The information is used to optimize the users' experience by customizing our web page content based on visitors' browser type and/or other information.

              +

              Like any other website, {{ site_name }} uses 'cookies'. These cookies are used to store information including visitors' preferences, and the pages on the website that the visitor accessed or visited. The information is used to optimize the users' experience by customizing our web page content based on visitors' browser type and/or other information.

              +

              You can choose to disable cookies through your individual browser options. This, however, can and will hurt Your usage of {{ site_name }}

              + +

              Privacy Policies

              +

              You may consult this list to find the Privacy Policy for each of the advertising partners of {{ site_name }}. Our Privacy Policy was created with the help of the Privacy Policy Generator and the Generate Privacy Policy Generator.

              -

              Privacy Policies

              +

              Third-party ad servers or ad networks uses technologies like cookies, JavaScript, or Web Beacons that are used in their respective advertisements and links that appear on {{ site_name }}, which are sent directly to users' browser. They automatically receive your IP address when this occurs. These technologies are used to measure the effectiveness of their advertising campaigns and/or to personalize the advertising content that you see on websites that you visit.

              -

              You may consult this list to find the Privacy Policy for each of the advertising partners of {{ site_name }}. Our Privacy Policy was created with the help of the Privacy Policy Generator and the Generate Privacy Policy Generator.

              +

              Note that {{ site_name }} has no access to or control over these cookies that are used by third-party advertisers.

              -

              Third-party ad servers or ad networks uses technologies like cookies, JavaScript, or Web Beacons that are used in their respective advertisements and links that appear on {{ site_name }}, which are sent directly to users' browser. They automatically receive your IP address when this occurs. These technologies are used to measure the effectiveness of their advertising campaigns and/or to personalize the advertising content that you see on websites that you visit.

              +

              Third Party Privacy Policies

              -

              Note that {{ site_name }} has no access to or control over these cookies that are used by third-party advertisers.

              +

              {{ site_name }}'s Privacy Policy does not apply to other advertisers or websites. Thus, we are advising you to consult the respective Privacy Policies of these third-party ad servers for more detailed information. It may include their practices and instructions about how to opt-out of certain options. You may find a complete list of these Privacy Policies and their links here: Privacy Policy Links.

              -

              Third Party Privacy Policies

              +

              Legal Basis

              -

              {{ site_name }}'s Privacy Policy does not apply to other advertisers or websites. Thus, we are advising you to consult the respective Privacy Policies of these third-party ad servers for more detailed information. It may include their practices and instructions about how to opt-out of certain options. You may find a complete list of these Privacy Policies and their links here: Privacy Policy Links.

              +

              Legal Basis for treatment is Legitimate Interest, except:

              +
                +
              • Transactional information, such as username, email and essential cookies, are treated according to Providing a Service.
              • +
              -

              You can choose to disable cookies through your individual browser options. To know more detailed information about cookie management with specific web browsers, it can be found at the browsers' respective websites. What Are Cookies?

              +

              Children's Information

              -

              Children's Information

              +

              Another part of our priority is adding protection for children while using the internet. We encourage parents and guardians to observe, participate in, monitor, guide and/or exercise total control on their online activity.

              -

              Another part of our priority is adding protection for children while using the internet. We encourage parents and guardians to observe, participate in, and/or monitor and guide their online activity.

              +

              {{ site_name }} does not knowingly collect any Personal Identifiable Information from children under the age of 13. If you think that your child provided this kind of information on our website, we strongly encourage you to contact us immediately and we will do our best efforts to promptly remove such information from our records.

              -

              {{ site_name }} does not knowingly collect any Personal Identifiable Information from children under the age of 13. If you think that your child provided this kind of information on our website, we strongly encourage you to contact us immediately and we will do our best efforts to promptly remove such information from our records.

              +

              Online Privacy Policy Only

              -

              Online Privacy Policy Only

              +

              This Privacy Policy applies only to our online activities and is valid for visitors to our website with regards to the information that they shared and/or collect in {{ site_name }}. This policy is not applicable to any information collected via channels other than this website.

              -

              This Privacy Policy applies only to our online activities and is valid for visitors to our website with regards to the information that they shared and/or collect in {{ site_name }}. This policy is not applicable to any information collected offline or via channels other than this website.

              +

              Consent

              -

              Consent

              - -

              By using our website, you hereby consent to our Privacy Policy and agree to its Terms and Conditions.

              +

              By using our website, you hereby consent irrevocably to our Privacy Policy and agree to its Terms and Conditions.

              +
              {% endblock %} diff --git a/src/coriplus/templates/terms.html b/src/coriplus/templates/terms.html index 203e44e..ab8ca7f 100644 --- a/src/coriplus/templates/terms.html +++ b/src/coriplus/templates/terms.html @@ -1,7 +1,9 @@ {% extends "base.html" %} {% block body %} +

              Terms of Service

              - +

              [decline to state]

              +
              {% endblock %} diff --git a/src/coriplus/templates/user_detail.html b/src/coriplus/templates/user_detail.html index 9b5e7a5..6324f1e 100644 --- a/src/coriplus/templates/user_detail.html +++ b/src/coriplus/templates/user_detail.html @@ -1,15 +1,18 @@ {% extends "base.html" %} +{% from "macros/message.html" import feed_message with context %} {% block body %} {% include "includes/infobox_profile.html" %}

              Messages from {{ user.username }}

              {% if not current_user.is_anonymous %} {% if user.username != current_user.username %} {% if current_user|is_following(user) %} -
              + +
              {% else %} -
              + +
              {% endif %} @@ -20,7 +23,7 @@ {% endif %}
                {% for message in message_list %} -
              • {% include "includes/message.html" %}
              • + {{ feed_message(message) }} {% endfor %}
              {% include "includes/pagination.html" %} diff --git a/src/coriplus/utils.py b/src/coriplus/utils.py index 5259b80..7a98d5b 100644 --- a/src/coriplus/utils.py +++ b/src/coriplus/utils.py @@ -218,12 +218,6 @@ def create_mentions(cur_user, text, privacy): pass # New in 0.9 -def inline_svg(name, width=None): - try: - with open('icons/' + name + '-24px.svg') as f: - data = f.read() - if isinstance(width, int): - data = re.sub(r'( (?:height|width)=")\d+(")', lambda x:x.group(1) + str(width) + x.group(2), data) - return Markup(data) - except OSError: - return '' +# changed in 0.10 +def inline_svg(name): + return Markup('{}').format(name) From 9071f5ff7a9f3a7a84a38bedaaf431032c87e55f Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Wed, 12 Nov 2025 10:34:57 +0100 Subject: [PATCH 12/15] change credential access for /admin/, style changes, fix and deprecate get_current_user() --- src/coriplus/__init__.py | 2 +- src/coriplus/admin.py | 7 +++---- src/coriplus/static/style.css | 11 +++++++---- .../templates/includes/infobox_profile.html | 16 ++-------------- src/coriplus/utils.py | 9 +++++---- src/coriplus/website.py | 9 ++++++--- 6 files changed, 24 insertions(+), 30 deletions(-) diff --git a/src/coriplus/__init__.py b/src/coriplus/__init__.py index 98b4d85..9543c60 100644 --- a/src/coriplus/__init__.py +++ b/src/coriplus/__init__.py @@ -24,7 +24,7 @@ from flask_wtf import CSRFProtect import dotenv import logging -__version__ = '0.10.0-dev44' +__version__ = '0.10.0-dev45' # we want to support Python 3.10+ only. # Python 2 has too many caveats. diff --git a/src/coriplus/admin.py b/src/coriplus/admin.py index a78cf1b..d8c8fb0 100644 --- a/src/coriplus/admin.py +++ b/src/coriplus/admin.py @@ -13,17 +13,16 @@ from functools import wraps bp = Blueprint('admin', __name__, url_prefix='/admin') -def _check_auth(username, password) -> bool: +def _check_auth(username) -> bool: try: - return User.select().where((User.username == username) & (User.password == pwdhash(password)) & (User.is_admin) - ).exists() + return User.get((User.username == username)).is_admin except User.DoesNotExist: return False def admin_required(f): @wraps(f) def wrapped_view(**kwargs): - if not _check_auth(current_user.username, current_user.password): + if not _check_auth(current_user.username): abort(403) return f(**kwargs) return wrapped_view diff --git a/src/coriplus/static/style.css b/src/coriplus/static/style.css index c238285..cb05c09 100644 --- a/src/coriplus/static/style.css +++ b/src/coriplus/static/style.css @@ -2,6 +2,9 @@ --accent: #f0372e; --link: #3399ff; } +* { + box-sizing: border-box; +} body, button, input, select, textarea { font-family: Inter, Roboto, sans-serif; line-height: 1.6; @@ -38,13 +41,13 @@ a:hover{text-decoration:underline} #site-name {text-align: center;flex: 1} .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} +.infobox{width: 50%; float: right;} +@media (max-width:639px) { + .infobox{width: 100%;} } .weak{opacity:.5} .field_desc{display:block} -ul.timeline{padding:0;margin:auto;max-width:960px} +ul.timeline{padding:0;margin:auto;max-width:960px;clear: both} ul.timeline > li{list-style:none;} .message-visual img{max-width:100%;margin:auto} .message-options-showhide::before{content:'\2026'} diff --git a/src/coriplus/templates/includes/infobox_profile.html b/src/coriplus/templates/includes/infobox_profile.html index 2cec0b7..d1b1494 100644 --- a/src/coriplus/templates/includes/infobox_profile.html +++ b/src/coriplus/templates/includes/infobox_profile.html @@ -1,27 +1,15 @@ {% set profile = user.profile %} -
              +

              {{ profile.full_name }}

              {{ profile.biography|enrich }}

              {% if profile.location %}

              Location: {{ profile.location|locationdata }}

              {% endif %} - {% if profile.year %} -

              Year: {{ profile.year }}

              - {% endif %} {% if profile.website %} {% set website = profile.website %} {% set website = website if website.startswith(('http://', 'https://')) else 'http://' + website %}

              Website: {{ profile.website|urlize }}

              {% endif %} - {% if profile.instagram %} -

              Instagram: {{ profile.instagram }}

              - {% endif %} - {% if profile.facebook %} -

              Facebook: {{ profile.facebook }}

              - {% endif %} - {% if profile.telegram %} -

              Telegram: {{ profile.telegram }}

              - {% endif %}

              {{ user.messages|count }} messages - @@ -30,6 +18,6 @@ {{ user.following()|count }} following

              {% if user == current_user %} -

              {{ inline_svg('edit', 18) }} Edit profile

              +

              {{ inline_svg('edit') }} Edit profile

              {% endif %}
              diff --git a/src/coriplus/utils.py b/src/coriplus/utils.py index 7a98d5b..1db7414 100644 --- a/src/coriplus/utils.py +++ b/src/coriplus/utils.py @@ -3,6 +3,8 @@ A list of utilities used across modules. ''' import datetime, re, base64, hashlib, string, sys, json + +from flask_login import current_user from .models import User, Message, Notification, MSGPRV_PUBLIC, MSGPRV_UNLISTED, \ MSGPRV_FRIENDS, MSGPRV_ONLYME from flask import abort, render_template, request, session @@ -102,15 +104,14 @@ except OSError: # get the user from the session # changed in 0.5 to comply with flask_login +# DEPRECATED in 0.10; use current_user instead def get_current_user(): # new in 0.7; need a different method to get current user id if request.path.startswith('/api/'): # assume token validation is already done return User[request.args['access_token'].split(':')[0]] - else: - user_id = session.get('user_id') - if user_id: - return User[user_id] + elif current_user.is_authenticated: + return current_user def push_notification(type, target, **kwargs): try: diff --git a/src/coriplus/website.py b/src/coriplus/website.py index df43b86..9612c66 100644 --- a/src/coriplus/website.py +++ b/src/coriplus/website.py @@ -7,7 +7,7 @@ from .models import * from . import __version__ as app_version from sys import version as python_version from flask import Blueprint, abort, flash, redirect, render_template, request, url_for, __version__ as flask_version -from flask_login import login_required, login_user, logout_user +from flask_login import current_user, login_required, login_user, logout_user import json import logging @@ -17,7 +17,7 @@ bp = Blueprint('website', __name__) @bp.route('/') def homepage(): - if get_current_user(): + if current_user and current_user.is_authenticated: return private_timeline() else: return render_template('homepage.html') @@ -26,7 +26,7 @@ def private_timeline(): # the private timeline (aka feed) exemplifies the use of a subquery -- we are asking for # messages where the person who created the message is someone the current # user is following. these messages are then ordered newest-first. - user = get_current_user() + user = current_user messages = Visibility(Message .select() .where((Message.user << user.following()) @@ -83,6 +83,9 @@ def register(): @bp.route('/login/', methods=['GET', 'POST']) def login(): + if current_user and current_user.is_authenticated: + flash('You are already logged in') + return redirect(request.args.get('next', '/')) if request.method == 'POST' and request.form['username']: try: username = request.form['username'] From 536e49d1b9314604e2846d0740a377c3979c3512 Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Wed, 12 Nov 2025 11:02:53 +0100 Subject: [PATCH 13/15] schema changes --- genmig.sh | 3 +- src/coriplus/models.py | 19 +++-- .../templates/includes/infobox_profile.html | 15 ++-- src/coriplus/website.py | 7 +- .../002_move_columns_from_userprofile.py | 81 +++++++++++++++++++ 5 files changed, 104 insertions(+), 21 deletions(-) create mode 100644 src/migrations/002_move_columns_from_userprofile.py diff --git a/genmig.sh b/genmig.sh index a29c9fb..c141519 100755 --- a/genmig.sh +++ b/genmig.sh @@ -6,4 +6,5 @@ source .env && \ case "$1" in ("+") pw_migrate create --auto --auto-source=coriplus.models --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;; ("@") pw_migrate migrate --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;; -esac \ No newline at end of file + (\\) pw_migrate rollback --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;; +esac diff --git a/src/coriplus/models.py b/src/coriplus/models.py index 5a2ae50..f47fa65 100644 --- a/src/coriplus/models.py +++ b/src/coriplus/models.py @@ -29,19 +29,25 @@ class BaseModel(Model): # A user. The user is separated from its page. class User(BaseModel): # The unique username. - username = CharField(unique=True) + username = CharField(30, unique=True) # The user's full name (here for better search since 0.8) - full_name = TextField() + full_name = CharField(80) # The password hash. - password = CharField() + password = CharField(256) # An email address. - email = CharField() + email = CharField(256) # 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): @@ -110,15 +116,12 @@ 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): ''' diff --git a/src/coriplus/templates/includes/infobox_profile.html b/src/coriplus/templates/includes/infobox_profile.html index d1b1494..bc08a20 100644 --- a/src/coriplus/templates/includes/infobox_profile.html +++ b/src/coriplus/templates/includes/infobox_profile.html @@ -1,14 +1,11 @@ -{% set profile = user.profile %} +
              -

              {{ profile.full_name }}

              -

              {{ profile.biography|enrich }}

              - {% if profile.location %} -

              Location: {{ profile.location|locationdata }}

              - {% endif %} - {% if profile.website %} - {% set website = profile.website %} +

              {{ user.full_name }}

              +

              {{ user.biography|enrich }}

              + {% if user.website %} + {% set website = user.website %} {% set website = website if website.startswith(('http://', 'https://')) else 'http://' + website %} -

              Website: {{ profile.website|urlize }}

              +

              Website: {{ website|urlize }}

              {% endif %}

              {{ user.messages|count }} messages diff --git a/src/coriplus/website.py b/src/coriplus/website.py index 9612c66..14b944c 100644 --- a/src/coriplus/website.py +++ b/src/coriplus/website.py @@ -137,11 +137,12 @@ def user_follow(username): from_user=cur_user, to_user=user, created_date=datetime.datetime.now()) + push_notification('follow', user, user=cur_user.id) + flash('You are now following %s' % user.username) except IntegrityError: - pass + flash(f'Error following {user.username}') + - flash('You are following %s' % user.username) - push_notification('follow', user, user=cur_user.id) return redirect(url_for('website.user_detail', username=user.username)) @bp.route('/+/unfollow/', methods=['POST']) diff --git a/src/migrations/002_move_columns_from_userprofile.py b/src/migrations/002_move_columns_from_userprofile.py new file mode 100644 index 0000000..0b2d004 --- /dev/null +++ b/src/migrations/002_move_columns_from_userprofile.py @@ -0,0 +1,81 @@ +"""Peewee migrations -- 002_move_columns_from_userprofile.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + migrator.add_fields( + 'user', + + biography=pw.CharField(max_length=256, default=""), + website=pw.TextField(null=True)) + + migrator.change_fields('user', username=pw.CharField(max_length=30, unique=True)) + + migrator.change_fields('user', full_name=pw.CharField(max_length=80)) + + migrator.change_fields('user', password=pw.CharField(max_length=256)) + + migrator.change_fields('user', email=pw.CharField(max_length=256)) + + migrator.sql(""" + UPDATE "user" SET biography = (SELECT p.biography FROM userprofile p WHERE p.user_id = id LIMIT 1), + website = (SELECT p.website FROM userprofile p WHERE p.user_id = id LIMIT 1); + """) + + migrator.remove_fields('userprofile', 'year', 'instagram', 'facebook', 'telegram') + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.add_fields( + 'userprofile', + + year=pw.IntegerField(null=True), + instagram=pw.TextField(null=True), + facebook=pw.TextField(null=True), + telegram=pw.TextField(null=True)) + + migrator.remove_fields('user', 'biography', 'website') + + migrator.change_fields('user', username=pw.CharField(max_length=255, unique=True)) + + migrator.change_fields('user', full_name=pw.TextField()) + + migrator.change_fields('user', password=pw.CharField(max_length=255)) + + migrator.change_fields('user', email=pw.CharField(max_length=255)) From 8369035693c15eb304fa54da34d4e61f0e7e05bb Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Wed, 26 Nov 2025 16:50:42 +0100 Subject: [PATCH 14/15] implement permanent deletion, make user profile migration reversible --- .gitignore | 3 ++- CHANGELOG.md | 3 +++ src/coriplus/__init__.py | 4 ++-- src/coriplus/models.py | 2 ++ src/coriplus/templates/confirm_delete.html | 2 +- src/coriplus/website.py | 9 ++++++--- src/migrations/002_move_columns_from_userprofile.py | 5 +++++ 7 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 546be68..75b7704 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ config/ .\#* node_modules/ alembic.ini -**.egg-info \ No newline at end of file +**.egg-info +.vscode \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 24a2232..6bf0e72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,12 @@ ## 0.10.0 + Codebase refactor (with breaking changes!) ++ Dropped support for Python<=3.9 + 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 ## 0.9.0 diff --git a/src/coriplus/__init__.py b/src/coriplus/__init__.py index 9543c60..305ea7b 100644 --- a/src/coriplus/__init__.py +++ b/src/coriplus/__init__.py @@ -24,7 +24,7 @@ from flask_wtf import CSRFProtect import dotenv import logging -__version__ = '0.10.0-dev45' +__version__ = '0.10.0-dev47' # we want to support Python 3.10+ only. # Python 2 has too many caveats. @@ -63,7 +63,7 @@ def before_request(): try: g.db.connect() except OperationalError: - logger.error('database connected twice.\n') + logger.error('database connected twice') @app.after_request def after_request(response): diff --git a/src/coriplus/models.py b/src/coriplus/models.py index f47fa65..07d52b7 100644 --- a/src/coriplus/models.py +++ b/src/coriplus/models.py @@ -23,6 +23,8 @@ from . import BASEDIR database = connect(os.environ['DATABASE_URL']) class BaseModel(Model): + id = AutoField(primary_key=True) + class Meta: database = database diff --git a/src/coriplus/templates/confirm_delete.html b/src/coriplus/templates/confirm_delete.html index 3d89e16..7343dad 100644 --- a/src/coriplus/templates/confirm_delete.html +++ b/src/coriplus/templates/confirm_delete.html @@ -16,7 +16,7 @@

            - +
            diff --git a/src/coriplus/website.py b/src/coriplus/website.py index 14b944c..ab9ab53 100644 --- a/src/coriplus/website.py +++ b/src/coriplus/website.py @@ -239,12 +239,15 @@ def edit(id): @bp.route('/delete/', methods=['GET', 'POST']) def confirm_delete(id): - user = get_current_user() - message = get_object_or_404(Message, Message.id == id) + user: User = current_user + message: Message = get_object_or_404(Message, Message.id == id) if message.user != user: abort(404) if request.method == 'POST': - abort(501, 'CSRF-Token missing.') + if message.user == user: + message.delete_instance() + flash('Your message has been deleted forever') + return redirect(request.args.get('next', '/')) return render_template('confirm_delete.html', message=message) # Workaround for problems related to invalid data. diff --git a/src/migrations/002_move_columns_from_userprofile.py b/src/migrations/002_move_columns_from_userprofile.py index 0b2d004..d8637f7 100644 --- a/src/migrations/002_move_columns_from_userprofile.py +++ b/src/migrations/002_move_columns_from_userprofile.py @@ -70,6 +70,11 @@ def rollback(migrator: Migrator, database: pw.Database, *, fake=False): facebook=pw.TextField(null=True), telegram=pw.TextField(null=True)) + migrator.sql(""" + UPDATE "userprofile" SET biography = (SELECT p.biography FROM user p WHERE p.user_id = id LIMIT 1), + website = (SELECT p.website FROM user p WHERE p.user_id = id LIMIT 1); + """) + migrator.remove_fields('user', 'biography', 'website') migrator.change_fields('user', username=pw.CharField(max_length=255, unique=True)) From b29fa7522613f09ebc2762129dd4ca25f546ee7d Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Fri, 19 Dec 2025 11:10:32 +0100 Subject: [PATCH 15/15] add csrf_token to JavaScript actions --- CHANGELOG.md | 2 ++ src/coriplus/__init__.py | 2 +- src/coriplus/ajax.py | 5 +++-- src/coriplus/static/lib.js | 12 ++++++++++-- src/coriplus/templates/base.html | 1 + 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bf0e72..cfb7a8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,13 @@ ## 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 diff --git a/src/coriplus/__init__.py b/src/coriplus/__init__.py index 305ea7b..09cecf6 100644 --- a/src/coriplus/__init__.py +++ b/src/coriplus/__init__.py @@ -24,7 +24,7 @@ from flask_wtf import CSRFProtect import dotenv import logging -__version__ = '0.10.0-dev47' +__version__ = '0.10.0-dev50' # we want to support Python 3.10+ only. # Python 2 has too many caveats. diff --git a/src/coriplus/ajax.py b/src/coriplus/ajax.py index d2c0be3..cad9c74 100644 --- a/src/coriplus/ajax.py +++ b/src/coriplus/ajax.py @@ -5,8 +5,9 @@ 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, get_current_user, is_username +from .utils import locations, is_username import datetime bp = Blueprint('ajax', __name__, url_prefix='/ajax') @@ -39,7 +40,7 @@ def location_search(name): @bp.route('/score//toggle', methods=['POST']) def score_toggle(id): - user = get_current_user() + user = current_user message = Message[id] upvoted_by_self = (MessageUpvote .select() diff --git a/src/coriplus/static/lib.js b/src/coriplus/static/lib.js index cc78bea..11a2316 100644 --- a/src/coriplus/static/lib.js +++ b/src/coriplus/static/lib.js @@ -99,12 +99,20 @@ 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){ @@ -114,5 +122,5 @@ function toggleUpvote(id){ } } }; - xhr.send(); + xhr.send(body); } diff --git a/src/coriplus/templates/base.html b/src/coriplus/templates/base.html index 415711b..60667ea 100644 --- a/src/coriplus/templates/base.html +++ b/src/coriplus/templates/base.html @@ -6,6 +6,7 @@ +