From dc33b5567aeaf25982d20ed3d6b47e3ff4c0875c Mon Sep 17 00:00:00 2001 From: Mattia Succurro Date: Sun, 27 Oct 2019 11:30:14 +0100 Subject: [PATCH] Adding feed to public API --- CHANGELOG.md | 3 ++- README.md | 3 ++- app/__init__.py | 37 +++++++++++++++++++++++++-- app/api.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++++ app/utils.py | 11 +++++--- 5 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 app/api.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 82fcb82..e3652b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 0.7-dev -* Biggest change: unpacking modules. The single `app.py` file has become an `app` package, with submodules `models.py`, `utils.py`, `filters.py`, `website.py` and `ajax.py`. +* Biggest change: unpacking modules. The single `app.py` file has become an `app` package, with submodules `models.py`, `utils.py`, `filters.py`, `website.py` and `ajax.py`. There is also a new module `api.py`. * Now `/about/` shows Python and Flask versions. * Now the error 404 handler returns HTTP 404. * Added user followers and following lists, accessible via `/+/followers` and `/+/following` and from the profile info box, linked to the followers/following number. @@ -11,6 +11,7 @@ * Added the capability to change password. * Corrected a bug into `pwdhash`: it accepted an argument, but pulled data from the form instead of processing it. Now it uses the argument. * Schema changes: added column `telegram` to `UserProfile` table. To update schema, execute the script `migrate_0_6_to_0_7.py` +* Adding public API. Each of the API endpoints take a mandatory query string argument: the access token, generated by a separate endpoint at `/get_access_token` and stored into the client. All API routes start with `/api/V1`. Added endpoints `feed`. ## 0.6.0 diff --git a/README.md b/README.md index 8139d41..06fc58b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A simple social network, inspired by the now dead Google-Plus. -To run the app, run the file "run_example.py" +To run the app, do "flask run" in the package's parent directory. Based on Tweepee example of [peewee](https://github.com/coleifer/peewee/). @@ -13,6 +13,7 @@ Based on Tweepee example of [peewee](https://github.com/coleifer/peewee/). * Timeline feed * Add info to your profile * In-site notifications +* Public API * SQLite-based app ## Requirements diff --git a/app/__init__.py b/app/__init__.py index d7cb66d..bfde0f2 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -16,7 +16,6 @@ from flask import ( Flask, abort, flash, g, jsonify, redirect, render_template, request, send_from_directory, session, url_for, __version__ as flask_version) import hashlib -from peewee import * import datetime, time, re, os, sys, string, json, html from functools import wraps from flask_login import LoginManager @@ -76,12 +75,46 @@ def robots_txt(): def uploads(id, type='jpg'): return send_from_directory(UPLOAD_DIRECTORY, id + '.' + type) +@app.route('/get_access_token', methods=['POST']) +def send_access_token(): + try: + try: + user = User.get( + (User.username == request.form['username']) & + (User.password == pwdhash(request.form['password']))) + except User.DoesNotExist: + return jsonify({ + 'message': 'Invalid username or password', + 'login_correct': False, + 'status': 'ok' + }) + if user.is_disabled == 1: + user.is_disabled = 0 + elif user.is_disabled == 2: + return jsonify({ + 'message': 'Your account has been disabled by violating our Terms.', + 'login_correct': False, + 'status': 'ok' + }) + return jsonify({ + 'token': generate_access_token(user), + 'login_correct': True, + 'status': 'ok' + }) + except Exception: + sys.excepthook(*sys.exc_info()) + return jsonify({ + 'message': 'An unknown error has occurred.', + 'status': 'fail' + }) + from .website import bp app.register_blueprint(bp) from .ajax import bp app.register_blueprint(bp) - +from .api import bp +app.register_blueprint(bp) diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..f689a88 --- /dev/null +++ b/app/api.py @@ -0,0 +1,67 @@ +from flask import Blueprint, jsonify, request +import sys, datetime +from functools import wraps +from .models import User, Message +from .utils import check_access_token, Visibility + +bp = Blueprint('api', __name__, url_prefix='/api/V1') + +def get_message_info(message): + return { + 'id': message.id, + 'user': { + 'id': message.user.id, + 'username': message.user.username, + }, + 'text': message.text, + 'privacy': message.privacy, + 'pub_date': message.pub_date.timestamp() + } + +def validate_access(func): + @wraps(func) + def wrapper(*args, **kwargs): + access_token = request.args.get('access_token') + if access_token is None: + return jsonify({ + 'message': 'missing access_token', + 'status': 'fail' + }) + user = check_access_token(access_token) + if user is None: + return jsonify({ + 'message': 'invalid access_token', + 'status': 'fail' + }) + try: + result = func(user, *args, **kwargs) + assert isinstance(result, dict) + except Exception: + sys.excepthook(*sys.exc_info()) + return jsonify({ + 'message': str(sys.exc_info()[1]), + 'status': 'fail' + }) + result['status'] = 'ok' + return jsonify(result) + return wrapper + +@bp.route('/feed') +@validate_access +def feed(self): + timeline_media = [] + date = request.args.get('offset') + if date is None: + date = datetime.datetime.now() + else: + date = datetime.datetime.fromtimestamp(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: + timeline_media.append(get_message_info(message)) + return {'timeline_media': timeline_media} diff --git a/app/utils.py b/app/utils.py index 8f88ac3..f014d69 100644 --- a/app/utils.py +++ b/app/utils.py @@ -178,13 +178,16 @@ def generate_access_token(user): h.update(str(user.password).encode('utf-8')) return str(user.id) + ':' + h.hexdigest()[:32] -def check_access_token(user, token): +def check_access_token(token): uid, hh = token.split(':') - if uid != user.get_id(): - return False + try: + user = User[uid] + except User.DoesNotExist: + return h = hashlib.sha256(get_secret_key()) h.update(b':') h.update(str(user.id).encode('utf-8')) h.update(b':') h.update(str(user.password).encode('utf-8')) - return h.hexdigest()[:32] == hh + if h.hexdigest()[:32] == hh: + return user