kasownik/web/webapp/api.py

237 lines
8.6 KiB
Python

# Copyright (c) 2015, Sergiusz Bazanski <q3k@q3k.org>
# Copyright (c) 2015, Remigiusz Marcinkiewicz <enleth@enleth.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import hmac
import json
import datetime
from functools import wraps
from sqlalchemy import and_
from flask import request, abort, Response
from webapp import models, app, mc
class APIError(Exception):
def __init__(self, message, code=500):
self.message = message
self.code = code
def _public_api_method(path):
"""A decorator that adds a public, GET based method at /api/<path>.json.
The resulting data is JSON-serialized."""
def decorator2(original):
@wraps(original)
def wrapper_json(*args, **kwargs):
try:
content = original(*args, **kwargs)
status = "ok"
code = 200
except APIError as e:
content = e.message
code = e.code
status = "error"
except Exception as e:
raise
content = "Internal server error."
code = 500
status = "error"
last_transfer = models.Transfer.query.order_by(models.Transfer.date.desc()).first()
modified = str(last_transfer.date)
r = {}
r["status"] = status
r["content"] = content
r["modified"] = modified
return Response(json.dumps(r), mimetype="application/json"), code
return app.route("/api/" + path + ".json", methods=["GET"])(wrapper_json)
return decorator2
def _private_api_method(path):
"""A decorator that adds a private, HMACed, POST based method at /api/path.
The JSON-decoded POSTbody is stored as request.decoded.
The resulting data is also JSON-encoded.
It also that ensures that the request is authorized if 'private' is True.
If so, it also adds a request.api_member object that points to a member if an
API key should be limited to that member (for example, when handing over
keys to normal members)."""
def decorator(original):
@wraps(original)
def wrapper(*args, **kwargs):
if request.data.count(",") != 1:
abort(400)
message64, mac64 = request.data.split(",")
try:
message = message64.decode("base64")
mac = mac64.decode("base64")
except:
abort(400)
for key in models.APIKey.query.all():
mac_verify = hmac.new(key.secret.encode("utf-8"))
mac_verify.update(message)
if mac_verify.digest() == mac:
break
else:
abort(403)
if key.member:
request.api_member = key.member
else:
request.api_member = None
try:
if request.data:
request.decoded = json.loads(request.data.decode("base64"))
else:
request.decoded = {}
except Exception as e:
print request.data
print e
abort(400)
return json.dumps(original(*args, **kwargs))
return app.route("/api/" + path, methods=["POST"])(wrapper)
return decorator
@_private_api_method("list_members")
def api_members():
if request.api_member:
abort(403)
members = [member.username for member in models.Member.query.all()]
return members
@_private_api_method("get_member_info")
def api_member():
mid = request.decoded["member"]
if request.api_member and request.api_member.username != mid:
abort(403)
member = models.Member.query.filter_by(username=mid).join(models.Member.transfers).\
join(models.MemberTransfer.transfer).first()
mts = member.transfers
response = {}
response["paid"] = []
for mt in mts:
t = {}
t["year"] = mt.year
t["month"] = mt.month
transfer = {}
transfer["uid"] = mt.transfer.uid
transfer["amount"] = mt.transfer.amount
transfer["title"] = mt.transfer.title
transfer["account"] = mt.transfer.account_from
transfer["from"] = mt.transfer.name_from
t["transfer"] = transfer
response["paid"].append(t)
response["months_due"] = member.get_months_due()
response["membership"] = member.type
return response
def _stats_for_month(year, month):
cache_key = 'kasownik-stats_for_month-{}-{}'.format(year, month)
cache_data = mc.get(cache_key)
if cache_data:
cache_data = json.loads(cache_data)
return cache_data[0], cache_data[1]
# TODO: export this to the config
money_required = 4217+615+615
money_paid = 0
mts = models.MemberTransfer.query.filter_by(year=year, month=month).\
join(models.MemberTransfer.transfer).all()
for mt in mts:
amount_all = mt.transfer.amount
amount = amount_all / len(mt.transfer.member_transfers)
money_paid += amount
mc.set(cache_key, json.dumps([money_required, money_paid/100]))
return money_required, money_paid/100
@_public_api_method("month/<year>/<month>")
def api_month(year=None, month=None):
money_required, money_paid = _stats_for_month(year, month)
return dict(required=money_required, paid=money_paid)
@_public_api_method("mana")
def api_manamana(year=None, month=None):
"""To-odee doo-dee-doo!"""
now = datetime.datetime.now()
money_required, money_paid = _stats_for_month(now.year, now.month)
return dict(required=money_required, paid=money_paid)
@_public_api_method("judgement/<membername>")
def api_judgement(membername):
member = models.Member.query.filter_by(username=membername).first()
if not member:
raise APIError("No such member.", 404)
judgement = member.get_status()['judgement']
return judgement
@_public_api_method("months_due/<membername>")
def api_months_due(membername):
cache_key = 'kasownik-months_due-{}'.format(membername)
cache_data = mc.get(cache_key)
if cache_data:
return cache_data
member = models.Member.query.filter_by(username=membername).first()
if not member:
raise APIError("No such member.", 404)
year, month = member.get_last_paid()
if not year:
raise APIError("Member never paid.", 402)
if year and member.active == False and member.username == 'b_rt':
raise APIError("Stoned.",420)
if year and member.active == False:
raise APIError("No longer a member.", 410)
due = member.get_months_due()
#now = datetime.datetime.now()
#then_timestamp = year * 12 + (month-1)
#now_timestamp = now.year * 12 + (now.month-1)
mc.set(cache_key, due)
return due
@_public_api_method("cashflow/<int:year>/<int:month>")
def api_cashflow(year, month):
cache_key = 'kasownik-cashflow-{}-{}'.format(year, month)
cache_data = mc.get(cache_key)
if cache_data:
amount_in = cache_data
else:
start = datetime.date(year=year, month=month, day=1)
month += 1
if month > 12:
month = 1
year += 1
end = datetime.date(year=year, month=month, day=1)
transfers = models.Transfer.query.filter(and_(models.Transfer.date >= start, models.Transfer.date < end, models.Transfer.ignore == False)).all()
amount_in = sum(t.amount for t in transfers)
mc.set(cache_key, amount_in)
return {"in": amount_in/100, "out": -1}