Merge branch 'main' into frontend
This commit is contained in:
commit
cf6f1714b4
77
.drone.yml
77
.drone.yml
@ -1,77 +0,0 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: linux-amd64
|
||||
|
||||
platform:
|
||||
arch: amd64
|
||||
os: linux
|
||||
|
||||
steps:
|
||||
- name: generate_tag
|
||||
image: golang
|
||||
commands:
|
||||
- echo -n "${DRONE_BRANCH//\//-}-${DRONE_COMMIT_SHA:0:8}, latest" > .tags
|
||||
|
||||
|
||||
- name: publish_api
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username:
|
||||
from_secret: username
|
||||
password:
|
||||
from_secret: password
|
||||
registry:
|
||||
from_secret: registry
|
||||
repo:
|
||||
from_secret: repo_api
|
||||
dockerfile: api/Dockerfile
|
||||
|
||||
- name: publish_frontend
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username:
|
||||
from_secret: username
|
||||
password:
|
||||
from_secret: password
|
||||
registry:
|
||||
from_secret: registry
|
||||
repo:
|
||||
from_secret: repo_frontend
|
||||
dockerfile: frontend/Dockerfile
|
||||
|
||||
- name: publish_bot
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username:
|
||||
from_secret: username
|
||||
password:
|
||||
from_secret: password
|
||||
registry:
|
||||
from_secret: registry
|
||||
repo:
|
||||
from_secret: repo_bot
|
||||
dockerfile: telegram_bot/Dockerfile
|
||||
|
||||
- name: deploy
|
||||
image: appleboy/drone-ssh
|
||||
network_mode: host
|
||||
environment:
|
||||
REPO:
|
||||
from_secret: repo
|
||||
IP:
|
||||
from_secret: deploy_ip
|
||||
NET:
|
||||
from_secret: deploy_net
|
||||
NAME:
|
||||
from_secret: deploy_name
|
||||
PLUGIN_HOST:
|
||||
from_secret: ssh_host
|
||||
PLUGIN_USERNAME:
|
||||
from_secret: ssh_user
|
||||
PLUGIN_PASSWORD:
|
||||
from_secret: ssh_password
|
||||
PLUGIN_SCRIPT: /opt/docker/TelegramAktienBot/deploy.sh
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
13
.env.example
13
.env.example
@ -1,13 +0,0 @@
|
||||
# MYSQL Database
|
||||
MYSQL_HOST=
|
||||
MYSQL_PORT=
|
||||
MYSQL_DATABASE=
|
||||
MYSQL_USER=
|
||||
MYSQL_PASSWORD=
|
||||
|
||||
# Telegram bot api key
|
||||
BOT_API_KEY=""
|
||||
NEWS_API_KEY=""
|
||||
|
||||
# Flask secret key
|
||||
SECRET_KEY=
|
35
.github/dependabot.yml
vendored
Normal file
35
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# API
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/api"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
assignees:
|
||||
- "H4CK3R-01"
|
||||
open-pull-requests-limit: 100
|
||||
|
||||
# Bot
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/telegram_bot"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
assignees:
|
||||
- "NormalParameter"
|
||||
- "Rripped"
|
||||
- "FlorianKellermann"
|
||||
open-pull-requests-limit: 100
|
||||
|
||||
# Frontend
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/frontend"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
assignees:
|
||||
- "kevinpauer"
|
||||
open-pull-requests-limit: 100
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,5 @@
|
||||
.idea/*
|
||||
.env
|
||||
.env.example
|
||||
env
|
||||
Lib
|
||||
Include
|
||||
|
87
.woodpecker/pipeline.yml
Normal file
87
.woodpecker/pipeline.yml
Normal file
@ -0,0 +1,87 @@
|
||||
pipeline:
|
||||
generate_docker_tag:
|
||||
image: golang
|
||||
commands:
|
||||
- echo -n "${CI_COMMIT_BRANCH//\//-}-${CI_COMMIT_SHA:0:8}, latest" > .tags
|
||||
when:
|
||||
path: [ "frontend/**", "telegram_bot/**", "api/**" ]
|
||||
event: push
|
||||
|
||||
|
||||
# -------------------------------------- API --------------------------------------
|
||||
build_api:
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
repo:
|
||||
from_secret: repo_api
|
||||
username:
|
||||
from_secret: username
|
||||
password:
|
||||
from_secret: password
|
||||
registry:
|
||||
from_secret: registry
|
||||
dockerfile: api/Dockerfile
|
||||
platforms: linux/amd64
|
||||
when:
|
||||
path: "api/**"
|
||||
event: push
|
||||
|
||||
|
||||
# -------------------------------------- Bot --------------------------------------
|
||||
build_bot:
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
repo:
|
||||
from_secret: repo_bot
|
||||
username:
|
||||
from_secret: username
|
||||
password:
|
||||
from_secret: password
|
||||
registry:
|
||||
from_secret: registry
|
||||
dockerfile: telegram_bot/Dockerfile
|
||||
platforms: linux/amd64
|
||||
when:
|
||||
path: "telegram_bot/**"
|
||||
event: push
|
||||
|
||||
|
||||
# -------------------------------------- Frontend --------------------------------------
|
||||
build_frontend:
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
repo:
|
||||
from_secret: repo_frontend
|
||||
username:
|
||||
from_secret: username
|
||||
password:
|
||||
from_secret: password
|
||||
registry:
|
||||
from_secret: registry
|
||||
dockerfile: frontend/Dockerfile
|
||||
platforms: linux/amd64
|
||||
when:
|
||||
path: "frontend/**"
|
||||
event: push
|
||||
|
||||
|
||||
# -------------------------------------- Deploy --------------------------------------
|
||||
deploy:
|
||||
image: appleboy/drone-ssh
|
||||
network_mode: host
|
||||
settings:
|
||||
host:
|
||||
from_secret: ssh_host
|
||||
username:
|
||||
from_secret: ssh_user
|
||||
password:
|
||||
from_secret: ssh_password
|
||||
script:
|
||||
- cd /root/docker/aktienbot
|
||||
- docker-compose pull
|
||||
- docker-compose -p "aktienbot" up -d
|
||||
when:
|
||||
path: [ "frontend/**", "telegram_bot/**", "api/**" ]
|
||||
event: push
|
||||
|
||||
branches: main
|
28
README.md
28
README.md
@ -1,4 +1,4 @@
|
||||
[![Build Status](https://drone.flokaiser.com/api/badges/H4CK3R-01/TelegramAktienBot/status.svg?ref=main)](https://drone.flokaiser.com/H4CK3R-01/TelegramAktienBot)
|
||||
[![Build Status](https://woodpecker.flokaiser.com/api/badges/WebEngineering2/TelegramAktienBot/status.svg)](https://woodpecker.flokaiser.com/WebEngineering2/TelegramAktienBot/)
|
||||
# TelegramAktienBot
|
||||
WebEngineering2 Projekt: Aktien und News Bot für Telegram
|
||||
|
||||
@ -15,12 +15,22 @@ WebEngineering2 Projekt: Aktien und News Bot für Telegram
|
||||
- Update auf Anfrage
|
||||
|
||||
## Dokumentation
|
||||
- Postman-API -> docs/postman.json
|
||||
- Datenbank -> database/*
|
||||
-> README.md in /documentation
|
||||
|
||||
## Local setup for telegram bot
|
||||
0. optional: build virtual env by ``python -m venv venv``
|
||||
``env/Scripts/activate``
|
||||
2. create .env and set API keys etc. (use .env.example as a layout)
|
||||
3. install required libs via ``pip install -r ./telegram_bot/requirements.txt``
|
||||
4. run bot.py via ``python ./telegram_bot/bot.py``
|
||||
## Team
|
||||
* Florian Kaiser
|
||||
* Florian Kellermann
|
||||
* Linus Eickhoff
|
||||
* Kevin Pauer
|
||||
|
||||
## Nützliche Tools
|
||||
- Portainer (https://gruppe1.testsites.info/portainer/) \
|
||||
*Container Management System*
|
||||
- phpMyAdmin (https://gruppe1.testsites.info/phpmyadmin/) \
|
||||
*Administration von MySQL-Datenbanken*
|
||||
- goaccess (https://gruppe1.testsites.info/goaccess/) \
|
||||
*Webanalyseanwendung*
|
||||
- Uptimekuma (https://uptimekuma.flokaiser.com/status/aktienbot) \
|
||||
*Monitoring*
|
||||
- Woodpecker (https://woodpecker.flokaiser.com/WebEngineering2/TelegramAktienBot) \
|
||||
*Continuous Integration platform*
|
||||
|
21
api/.env.example
Normal file
21
api/.env.example
Normal file
@ -0,0 +1,21 @@
|
||||
# MYSQL Database
|
||||
MYSQL_HOST=
|
||||
MYSQL_PORT=
|
||||
MYSQL_DATABASE=
|
||||
MYSQL_USER=
|
||||
MYSQL_PASSWORD=
|
||||
|
||||
# Flask secret key
|
||||
SECRET_KEY=
|
||||
|
||||
# Users
|
||||
BOT_EMAIL=
|
||||
BOT_USERNAME=
|
||||
BOT_PASSWORD=
|
||||
|
||||
ADMIN_EMAIL=
|
||||
ADMIN_USERNAME=
|
||||
ADMIN_PASSWORD=
|
||||
|
||||
# API URL (used for load_share_price.py and generate_sample_transactions.py)
|
||||
API_URL=
|
@ -1,19 +1,28 @@
|
||||
FROM python:3.10-alpine
|
||||
FROM python:3.10-slim
|
||||
|
||||
# Change the working directory to the root of the project
|
||||
WORKDIR /srv/flask_app
|
||||
RUN apk add nginx build-base libffi-dev curl uwsgi
|
||||
|
||||
# Install dependencies
|
||||
RUN apt update && apt install -y python3 python3-pip curl nginx && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install the dependencies
|
||||
COPY api/requirements.txt /srv/flask_app/
|
||||
|
||||
RUN pip install -r requirements.txt --src /usr/local/src --no-warn-script-location
|
||||
|
||||
# Copy the source code to the working directory
|
||||
COPY api /srv/flask_app
|
||||
COPY api/deploy/nginx.conf /etc/nginx
|
||||
|
||||
# Change file permissions
|
||||
RUN chmod +x ./deploy/start.sh
|
||||
RUN chmod +x ./deploy/healthcheck.sh
|
||||
|
||||
# Set healthcheck
|
||||
HEALTHCHECK --interval=15s --timeout=2s CMD ["./deploy/healthcheck.sh"]
|
||||
|
||||
# Expose webserver port
|
||||
EXPOSE 80
|
||||
|
||||
# Run the app
|
||||
CMD ["./deploy/start.sh"]
|
||||
|
51
api/README.md
Normal file
51
api/README.md
Normal file
@ -0,0 +1,51 @@
|
||||
# API
|
||||
|
||||
Aktienbot API
|
||||
|
||||
## Development
|
||||
1. Create virtual environment `python -m venv venv env/Scripts/activate`
|
||||
2. Install requirements `pip install -r api/requirements.txt`
|
||||
3. Set environment variables (see list below)
|
||||
1. Use `.env`-file in `api` directory like `.env.example`
|
||||
2. Or set variables using `export` or `set` commands. (Windows `set`, Linux `export`)
|
||||
4. Run api `python api/app.py`
|
||||
|
||||
|
||||
## Testing
|
||||
1. Create virtual environment `python -m venv venv env/Scripts/activate`
|
||||
2. Install requirements `pip install -r api/requirements.txt`
|
||||
3. Set environment variables (see list below)
|
||||
1. Use `.env`-file in `api` directory like `.env.example`
|
||||
2. Or set variables using `export` or `set` commands. (Windows `set`, Linux `export`)
|
||||
4. Change directory: `cd api/`
|
||||
5. Run tests: `python -m pytest -v --cov-report term-missing --cov=app`
|
||||
|
||||
## Environment variables
|
||||
```
|
||||
# Flask secret key
|
||||
SECRET_KEY=
|
||||
|
||||
# MYSQL Connection
|
||||
MYSQL_USER=
|
||||
MYSQL_PASSWORD=
|
||||
MYSQL_HOST=
|
||||
MYSQL_PORT=
|
||||
MYSQL_DATABASE=
|
||||
```
|
||||
|
||||
## Docker
|
||||
```
|
||||
docker run -d \
|
||||
--name aktienbot_api \
|
||||
--hostname aktienbot_api \
|
||||
--publish 80:80 \
|
||||
--env "SECRET_KEY=" \
|
||||
--env "MYSQL_USER=" \
|
||||
--env "MYSQL_PASSWORD=" \
|
||||
--env "MYSQL_HOST=" \
|
||||
--env "MYSQL_PORT=" \
|
||||
--env "MYSQL_DATABASE=" \
|
||||
--restart unless-stopped \
|
||||
registry.flokaiser.com/aktienbot/api:latest
|
||||
```
|
||||
or load environment variables from file by using `--env-file <filename>`
|
@ -1,33 +0,0 @@
|
||||
import os
|
||||
|
||||
from apiflask import APIBlueprint
|
||||
from flask import jsonify
|
||||
|
||||
from db import db
|
||||
from helper_functions import get_user_id_from_username, get_username_or_abort_401
|
||||
from models import Transaction
|
||||
from auth import auth
|
||||
|
||||
portfolio_blueprint = APIBlueprint('portfolio', __name__, url_prefix='/api')
|
||||
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
|
||||
|
||||
|
||||
@portfolio_blueprint.route('/portfolio', methods=['GET'])
|
||||
@portfolio_blueprint.output(200)
|
||||
@portfolio_blueprint.auth_required(auth)
|
||||
@portfolio_blueprint.doc(summary="Returns portfolio", description="Returns all shares of current user")
|
||||
def get_portfolio():
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
return_portfolio = {}
|
||||
transactions = db.session.query(Transaction).filter_by(user_id=get_user_id_from_username(username)).all()
|
||||
|
||||
if transactions is not None:
|
||||
for row in transactions:
|
||||
if row.symbol in return_portfolio:
|
||||
return_portfolio[row.symbol]['count'] += row.count
|
||||
return_portfolio[row.symbol]['last_transaction'] = row.time
|
||||
else:
|
||||
return_portfolio[row.symbol] = {"count": row.count, "last_transaction": row.time}
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully loaded symbols", "data": return_portfolio})
|
@ -1,83 +0,0 @@
|
||||
import os
|
||||
|
||||
from apiflask import APIBlueprint, abort
|
||||
from flask import jsonify
|
||||
|
||||
from auth import auth
|
||||
from db import db
|
||||
from helper_functions import get_user_id_from_username, get_username_or_abort_401
|
||||
from models import Share
|
||||
from scheme import SymbolSchema, SymbolResponseSchema, DeleteSuccessfulSchema
|
||||
|
||||
shares_blueprint = APIBlueprint('share', __name__, url_prefix='/api')
|
||||
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
|
||||
|
||||
|
||||
@shares_blueprint.route('/share', methods=['POST'])
|
||||
@shares_blueprint.output(SymbolResponseSchema(many=True), 200)
|
||||
@shares_blueprint.input(schema=SymbolSchema)
|
||||
@shares_blueprint.auth_required(auth)
|
||||
@shares_blueprint.doc(summary="Add new symbol", description="Adds new symbol for current user")
|
||||
def add_symbol(data):
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
check_if_symbol_data_exists(data)
|
||||
|
||||
symbol = data['symbol']
|
||||
|
||||
check_share = db.session.query(Share).filter_by(symbol=symbol, user_id=get_user_id_from_username(username)).first()
|
||||
if check_share is None:
|
||||
# Keyword doesn't exist yet for this user
|
||||
new_symbol = Share(
|
||||
user_id=get_user_id_from_username(username),
|
||||
symbol=symbol
|
||||
)
|
||||
db.session.add(new_symbol)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully added symbol", "data": new_symbol.as_dict()})
|
||||
else:
|
||||
return jsonify({"status": 500, "text": "Symbol already exist for this user"})
|
||||
|
||||
|
||||
@shares_blueprint.route('/share', methods=['DELETE'])
|
||||
@shares_blueprint.output(DeleteSuccessfulSchema, 200)
|
||||
@shares_blueprint.input(schema=SymbolSchema)
|
||||
@shares_blueprint.auth_required(auth)
|
||||
@shares_blueprint.doc(summary="Removes existing symbol", description="Removes existing symbol for current user")
|
||||
def remove_symbol(data):
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
check_if_symbol_data_exists(data)
|
||||
|
||||
symbol = data['symbol']
|
||||
|
||||
db.session.query(Share).filter_by(symbol=symbol, user_id=get_user_id_from_username(username)).delete()
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully removed symbol", "data": {}})
|
||||
|
||||
|
||||
@shares_blueprint.route('/shares', methods=['GET'])
|
||||
@shares_blueprint.output(SymbolResponseSchema(many=True), 200)
|
||||
@shares_blueprint.auth_required(auth)
|
||||
@shares_blueprint.doc(summary="Returns all symbols", description="Returns all symbols for current user")
|
||||
def get_symbol():
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
return_symbols = []
|
||||
symbols = db.session.query(Share).filter_by(user_id=get_user_id_from_username(username)).all()
|
||||
|
||||
if symbols is not None:
|
||||
for row in symbols:
|
||||
return_symbols.append(row.as_dict())
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully loaded symbols", "data": return_symbols})
|
||||
|
||||
|
||||
def check_if_symbol_data_exists(data):
|
||||
if "symbol" not in data:
|
||||
abort(400, message="Symbol missing")
|
||||
|
||||
if data['symbol'] == "" or data['symbol'] is None:
|
||||
abort(400, message="Symbol missing")
|
@ -1,80 +0,0 @@
|
||||
import os
|
||||
import datetime
|
||||
|
||||
from apiflask import abort, APIBlueprint
|
||||
from flask import jsonify
|
||||
|
||||
from db import db
|
||||
from helper_functions import get_user_id_from_username, get_username_or_abort_401
|
||||
from models import Transaction
|
||||
from scheme import TransactionSchema
|
||||
from auth import auth
|
||||
|
||||
transaction_blueprint = APIBlueprint('transaction', __name__, url_prefix='/api')
|
||||
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
|
||||
|
||||
|
||||
@transaction_blueprint.route('/transaction', methods=['POST'])
|
||||
@transaction_blueprint.output((), 200)
|
||||
@transaction_blueprint.input(schema=TransactionSchema)
|
||||
@transaction_blueprint.auth_required(auth)
|
||||
@transaction_blueprint.doc(summary="Adds new transaction", description="Adds new transaction for current user")
|
||||
def add_transaction(data):
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
check_if_transaction_data_exists(data)
|
||||
|
||||
new_transaction = Transaction(
|
||||
user_id=get_user_id_from_username(username),
|
||||
symbol=data['symbol'],
|
||||
time=datetime.datetime.strptime(data['time'], '%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||
count=data['count'],
|
||||
price=data['price']
|
||||
)
|
||||
db.session.add(new_transaction)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully added transaction", "data": new_transaction.as_dict()})
|
||||
|
||||
|
||||
@transaction_blueprint.route('/transactions', methods=['GET'])
|
||||
@transaction_blueprint.output(TransactionSchema(), 200)
|
||||
@transaction_blueprint.auth_required(auth)
|
||||
@transaction_blueprint.doc(summary="Returns all transactions", description="Returns all transactions for current user")
|
||||
def get_transaction():
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
return_transactions = []
|
||||
transactions = db.session.query(Transaction).filter_by(user_id=get_user_id_from_username(username)).all()
|
||||
|
||||
if transactions is not None:
|
||||
for row in transactions:
|
||||
return_transactions.append(row.as_dict())
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully loaded transactions", "data": return_transactions})
|
||||
|
||||
|
||||
def check_if_transaction_data_exists(data):
|
||||
if "symbol" not in data:
|
||||
abort(400, message="Symbol missing")
|
||||
|
||||
if data['symbol'] == "" or data['symbol'] is None:
|
||||
abort(400, message="Symbol missing")
|
||||
|
||||
if "time" not in data:
|
||||
abort(400, message="Time missing")
|
||||
|
||||
if data['time'] == "" or data['time'] is None:
|
||||
abort(400, message="Time missing")
|
||||
|
||||
if "count" not in data:
|
||||
abort(400, message="Count missing")
|
||||
|
||||
if data['count'] == "" or data['count'] is None:
|
||||
abort(400, message="Count missing")
|
||||
|
||||
if "price" not in data:
|
||||
abort(400, message="Price missing")
|
||||
|
||||
if data['price'] == "" or data['price'] is None:
|
||||
abort(400, message="Price missing")
|
@ -1,199 +0,0 @@
|
||||
import datetime
|
||||
import os
|
||||
|
||||
import jwt
|
||||
from apiflask import APIBlueprint, abort
|
||||
from flask import jsonify
|
||||
|
||||
from db import db
|
||||
from helper_functions import check_password, hash_password, get_username_or_abort_401, abort_if_no_admin
|
||||
from models import User
|
||||
from scheme import UsersSchema, TokenSchema, LoginDataSchema, AdminDataSchema, DeleteUserSchema
|
||||
from auth import auth
|
||||
|
||||
users_blueprint = APIBlueprint('users', __name__, url_prefix='/api')
|
||||
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
|
||||
|
||||
|
||||
@users_blueprint.route('/users', methods=['GET'])
|
||||
@users_blueprint.output(UsersSchema(many=True), 200)
|
||||
@users_blueprint.auth_required(auth)
|
||||
@users_blueprint.doc(summary="Get all users", description="Returns all existing users as array")
|
||||
def users():
|
||||
abort_if_no_admin()
|
||||
|
||||
res = []
|
||||
for i in User.query.all():
|
||||
res.append(i.as_dict())
|
||||
|
||||
return jsonify({"status": 200, "data": res})
|
||||
|
||||
|
||||
@users_blueprint.route('/user', methods=['GET'])
|
||||
@users_blueprint.output(UsersSchema(), 200)
|
||||
@users_blueprint.auth_required(auth)
|
||||
@users_blueprint.doc(summary="Get current user", description="Returns current user")
|
||||
def user():
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
res = db.session.query(User).filter_by(username=username).first().as_dict()
|
||||
|
||||
return jsonify({"status": 200, "data": res})
|
||||
|
||||
|
||||
@users_blueprint.route('/user/login', methods=['POST'])
|
||||
@users_blueprint.output(TokenSchema(), 200)
|
||||
@users_blueprint.input(schema=LoginDataSchema)
|
||||
@users_blueprint.doc(summary="Login", description="Returns jwt token if username and password match, otherwise returns error")
|
||||
def login(data):
|
||||
check_if_user_data_exists(data)
|
||||
|
||||
username = data['username']
|
||||
password = data['password']
|
||||
|
||||
query_user = db.session.query(User).filter_by(username=username).first()
|
||||
|
||||
if query_user is None: # Username doesn't exist
|
||||
abort(500, message="Unable to login")
|
||||
|
||||
if not check_password(query_user.password, password): # Password incorrect
|
||||
abort(500, message="Unable to login")
|
||||
|
||||
token = jwt.encode({'username': query_user.username, 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=45)}, os.getenv('SECRET_KEY'), "HS256")
|
||||
return jsonify({"status": 200, "text": "Successfully logged in", "data": {"token": token}})
|
||||
|
||||
|
||||
@users_blueprint.route('/user/register', methods=['POST'])
|
||||
@users_blueprint.output(UsersSchema(), 200)
|
||||
@users_blueprint.input(schema=LoginDataSchema)
|
||||
@users_blueprint.doc(summary="Register", description="Registers user")
|
||||
def register(data):
|
||||
check_if_user_data_exists(data)
|
||||
|
||||
username = data['username']
|
||||
password = data['password']
|
||||
|
||||
query_user = db.session.query(User).filter_by(username=username).first()
|
||||
|
||||
if query_user is not None: # Username already exist
|
||||
abort(500, message="Username already exist")
|
||||
|
||||
new_user = User(
|
||||
username=username,
|
||||
password=hash_password(password),
|
||||
admin=False
|
||||
)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully registered user", "data": new_user.as_dict()})
|
||||
|
||||
|
||||
@users_blueprint.route('/user', methods=['PUT'])
|
||||
@users_blueprint.output({}, 200)
|
||||
@users_blueprint.input(schema=LoginDataSchema)
|
||||
@users_blueprint.auth_required(auth)
|
||||
@users_blueprint.doc(summary="Update user", description="Changes password and/or username of current user")
|
||||
def update_user(data):
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
check_if_user_data_exists(data)
|
||||
|
||||
new_username = data['username']
|
||||
new_password = data['password']
|
||||
|
||||
query_user = db.session.query(User).filter_by(username=username).first()
|
||||
|
||||
if query_user is None: # Username doesn't exist
|
||||
abort(500, message="Unable to login")
|
||||
|
||||
if new_password is not None:
|
||||
query_user.password = hash_password(new_password)
|
||||
if new_username is not None:
|
||||
query_user.username = new_username
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully updated user", "data": {}})
|
||||
|
||||
|
||||
@users_blueprint.route('/user/setAdmin', methods=['PUT'])
|
||||
@users_blueprint.output({}, 200)
|
||||
@users_blueprint.input(schema=AdminDataSchema)
|
||||
@users_blueprint.auth_required(auth)
|
||||
@users_blueprint.doc(summary="Set user admin state", description="Set admin state of specified user")
|
||||
def set_admin(data):
|
||||
abort_if_no_admin() # Only admin users can do this
|
||||
|
||||
check_if_admin_data_exists(data)
|
||||
|
||||
username = data['username']
|
||||
admin = data['admin']
|
||||
|
||||
query_user = db.session.query(User).filter_by(username=username).first()
|
||||
|
||||
if query_user is None: # Username doesn't exist
|
||||
abort(500, message="Unable to login")
|
||||
|
||||
query_user.admin = admin
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully updated users admin rights", "data": {}})
|
||||
|
||||
|
||||
@users_blueprint.route('/user', methods=['DELETE'])
|
||||
@users_blueprint.output({}, 200)
|
||||
@users_blueprint.input(schema=DeleteUserSchema)
|
||||
@users_blueprint.auth_required(auth)
|
||||
@users_blueprint.doc(summary="Delete user", description="Deletes user by username")
|
||||
def delete_user(data):
|
||||
check_if_delete_data_exists(data)
|
||||
|
||||
username = data['username']
|
||||
|
||||
if username == get_username_or_abort_401(): # Username is same as current user
|
||||
db.session.query(User).filter_by(username=username).delete()
|
||||
db.session.commit()
|
||||
else: # Delete different user than my user -> only admin users
|
||||
abort_if_no_admin()
|
||||
|
||||
db.session.query(User).filter_by(username=username).delete()
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully removed user", "data": {}})
|
||||
|
||||
|
||||
def check_if_user_data_exists(data):
|
||||
if "username" not in data:
|
||||
abort(400, message="Username missing")
|
||||
|
||||
if data['username'] == "" or data['username'] is None:
|
||||
abort(400, message="Username missing")
|
||||
|
||||
if "password" not in data:
|
||||
abort(400, message="Password missing")
|
||||
|
||||
if data['password'] == "" or data['password'] is None:
|
||||
abort(400, message="Password missing")
|
||||
|
||||
|
||||
def check_if_admin_data_exists(data):
|
||||
if "username" not in data:
|
||||
abort(400, message="Username missing")
|
||||
|
||||
if data['username'] == "" or data['username'] is None:
|
||||
abort(400, message="Username missing")
|
||||
|
||||
if "admin" not in data:
|
||||
abort(400, message="Admin state missing")
|
||||
|
||||
if data['admin'] == "" or data['admin'] is None:
|
||||
abort(400, message="Admin state missing")
|
||||
|
||||
|
||||
def check_if_delete_data_exists(data):
|
||||
if "username" not in data:
|
||||
abort(400, message="Username missing")
|
||||
|
||||
if data['username'] == "" or data['username'] is None:
|
||||
abort(400, message="Username missing")
|
51
api/app.py
51
api/app.py
@ -1,44 +1,11 @@
|
||||
from apiflask import APIFlask
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from flask_cors import CORS
|
||||
from app import create_app
|
||||
|
||||
from models import *
|
||||
from api_blueprint_keyword import keyword_blueprint
|
||||
from api_blueprint_shares import shares_blueprint
|
||||
from api_blueprint_user import users_blueprint
|
||||
from api_blueprint_transactions import transaction_blueprint
|
||||
from api_blueprint_portfolio import portfolio_blueprint
|
||||
|
||||
|
||||
def create_app():
|
||||
load_dotenv()
|
||||
|
||||
# Create Flask app load app.config
|
||||
application = APIFlask(__name__, docs_path='/api/docs')
|
||||
application.config.from_object("config.ConfigClass")
|
||||
|
||||
CORS(application)
|
||||
|
||||
application.app_context().push()
|
||||
|
||||
db.init_app(application)
|
||||
|
||||
# Create all tables
|
||||
db.create_all()
|
||||
|
||||
# api blueprints
|
||||
application.register_blueprint(keyword_blueprint)
|
||||
application.register_blueprint(shares_blueprint)
|
||||
application.register_blueprint(transaction_blueprint)
|
||||
application.register_blueprint(portfolio_blueprint)
|
||||
application.register_blueprint(users_blueprint)
|
||||
|
||||
return application
|
||||
|
||||
|
||||
app = create_app()
|
||||
|
||||
# Start development web server
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
# Create an application instance that web servers can use.
|
||||
application = create_app('config/flask.cfg')
|
||||
application.run()
|
||||
|
73
api/app/__init__.py
Normal file
73
api/app/__init__.py
Normal file
@ -0,0 +1,73 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
from flask import current_app
|
||||
from apiflask import APIFlask
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from flask_cors import CORS
|
||||
|
||||
from app.blueprints.keyword import keyword_blueprint
|
||||
from app.blueprints.portfolio import portfolio_blueprint
|
||||
from app.blueprints.shares import shares_blueprint
|
||||
from app.blueprints.share_price import share_price_blueprint
|
||||
from app.blueprints.transactions import transaction_blueprint
|
||||
from app.blueprints.telegram import telegram_blueprint
|
||||
from app.blueprints.user import users_blueprint
|
||||
from app.helper_functions import hash_password
|
||||
from app.models import *
|
||||
|
||||
|
||||
def create_app(config_filename=None):
|
||||
load_dotenv()
|
||||
|
||||
# Create Flask app load app.config
|
||||
application = APIFlask(__name__, openapi_blueprint_url_prefix='/api')
|
||||
application.config.from_pyfile(config_filename)
|
||||
|
||||
CORS(application, resources={r"*": {"origins": "*"}})
|
||||
|
||||
db.init_app(application)
|
||||
|
||||
# api blueprints
|
||||
application.register_blueprint(keyword_blueprint)
|
||||
application.register_blueprint(shares_blueprint)
|
||||
application.register_blueprint(share_price_blueprint)
|
||||
application.register_blueprint(transaction_blueprint)
|
||||
application.register_blueprint(portfolio_blueprint)
|
||||
application.register_blueprint(users_blueprint)
|
||||
application.register_blueprint(telegram_blueprint)
|
||||
|
||||
@application.before_first_request
|
||||
def init_database():
|
||||
db.create_all()
|
||||
|
||||
if current_app.config['BOT_EMAIL'] is not None and current_app.config['BOT_USERNAME'] is not None and current_app.config['BOT_PASSWORD'] is not None:
|
||||
if db.session.query(User).filter_by(email=current_app.config['BOT_EMAIL']).first() is None: # Check if user already exist
|
||||
bot = User(
|
||||
email=current_app.config['BOT_EMAIL'],
|
||||
username=current_app.config['BOT_USERNAME'],
|
||||
password=hash_password(current_app.config['BOT_PASSWORD']),
|
||||
admin=False
|
||||
)
|
||||
db.session.add(bot)
|
||||
db.session.commit()
|
||||
|
||||
if current_app.config['ADMIN_EMAIL'] is not None and current_app.config['ADMIN_USERNAME'] is not None and current_app.config['ADMIN_PASSWORD'] is not None:
|
||||
if db.session.query(User).filter_by(email=current_app.config['ADMIN_EMAIL']).first() is None: # Check if user already exist
|
||||
admin = User(
|
||||
email=current_app.config['ADMIN_EMAIL'],
|
||||
username=current_app.config['ADMIN_USERNAME'],
|
||||
password=hash_password(current_app.config['ADMIN_PASSWORD']),
|
||||
admin=True
|
||||
)
|
||||
db.session.add(admin)
|
||||
db.session.commit()
|
||||
|
||||
return application
|
||||
|
||||
|
||||
app = create_app("config/flask.cfg")
|
29
api/app/auth.py
Normal file
29
api/app/auth.py
Normal file
@ -0,0 +1,29 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
from flask import current_app
|
||||
|
||||
import jwt
|
||||
from apiflask import HTTPTokenAuth
|
||||
|
||||
auth = HTTPTokenAuth()
|
||||
|
||||
|
||||
@auth.verify_token
|
||||
def verify_token(token):
|
||||
if token is None:
|
||||
return False
|
||||
|
||||
# We decided to append the user id to the bearer token using ":" as separator to select an specific user
|
||||
# To validate the token we can remove the user id since we only validate the token and not the user id
|
||||
if ':' in token:
|
||||
token = token.split(":")[0]
|
||||
|
||||
try:
|
||||
jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=["HS256"])
|
||||
return True
|
||||
except jwt.PyJWTError:
|
||||
return False
|
5
api/app/blueprints/__init__.py
Normal file
5
api/app/blueprints/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
@ -1,13 +1,18 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
import os
|
||||
|
||||
from apiflask import APIBlueprint, abort
|
||||
from flask import jsonify
|
||||
|
||||
from db import db
|
||||
from helper_functions import get_user_id_from_username, get_username_or_abort_401
|
||||
from auth import auth
|
||||
from scheme import KeywordResponseSchema, KeywordSchema, DeleteSuccessfulSchema
|
||||
from models import Keyword
|
||||
from app.db import database as db
|
||||
from app.helper_functions import make_response, get_email_or_abort_401
|
||||
from app.auth import auth
|
||||
from app.schema import KeywordResponseSchema, KeywordSchema, DeleteSuccessfulSchema
|
||||
from app.models import Keyword
|
||||
|
||||
keyword_blueprint = APIBlueprint('keyword', __name__, url_prefix='/api')
|
||||
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
|
||||
@ -19,23 +24,25 @@ __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file
|
||||
@keyword_blueprint.auth_required(auth)
|
||||
@keyword_blueprint.doc(summary="Add new keyword", description="Adds new keyword for current user")
|
||||
def add_keyword(data):
|
||||
username = get_username_or_abort_401()
|
||||
email = get_email_or_abort_401()
|
||||
|
||||
check_if_keyword_data_exists(data)
|
||||
if not check_if_keyword_data_exists(data):
|
||||
abort(400, message="Keyword missing")
|
||||
|
||||
key = data['keyword']
|
||||
|
||||
check_keyword = db.session.query(Keyword).filter_by(keyword=key, user_id=get_user_id_from_username(username)).first()
|
||||
# Check if keyword already exists
|
||||
check_keyword = db.session.query(Keyword).filter_by(keyword=key, email=email).first()
|
||||
if check_keyword is None:
|
||||
# Keyword doesn't exist yet for this user
|
||||
# Keyword doesn't exist yet for this user -> add it
|
||||
new_keyword = Keyword(
|
||||
user_id=get_user_id_from_username(username),
|
||||
email=email,
|
||||
keyword=key
|
||||
)
|
||||
db.session.add(new_keyword)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully added keyword", "data": new_keyword.as_dict()})
|
||||
return make_response(new_keyword.as_dict(), 200, "Successfully added keyword")
|
||||
else:
|
||||
abort(500, message="Keyword already exist for this user")
|
||||
|
||||
@ -46,16 +53,22 @@ def add_keyword(data):
|
||||
@keyword_blueprint.auth_required(auth)
|
||||
@keyword_blueprint.doc(summary="Removes existing keyword", description="Removes existing keyword for current user")
|
||||
def remove_keyword(data):
|
||||
username = get_username_or_abort_401()
|
||||
email = get_email_or_abort_401()
|
||||
|
||||
check_if_keyword_data_exists(data)
|
||||
# Check if request data is valid
|
||||
if not check_if_keyword_data_exists(data):
|
||||
abort(400, message="Keyword missing")
|
||||
|
||||
key = data['keyword']
|
||||
# Check if keyword exists
|
||||
check_keyword = db.session.query(Keyword).filter_by(keyword=data['keyword'], email=email).first()
|
||||
if check_keyword is None:
|
||||
return abort(500, "Keyword doesn't exist for this user")
|
||||
else:
|
||||
# Keyword exists -> delete it
|
||||
db.session.query(Keyword).filter_by(keyword=data['keyword'], email=email).delete()
|
||||
db.session.commit()
|
||||
|
||||
db.session.query(Keyword).filter_by(keyword=key, user_id=get_user_id_from_username(username)).delete()
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully removed keyword", "data": {}})
|
||||
return make_response({}, 200, "Successfully removed keyword")
|
||||
|
||||
|
||||
@keyword_blueprint.route('/keywords', methods=['GET'])
|
||||
@ -63,21 +76,25 @@ def remove_keyword(data):
|
||||
@keyword_blueprint.auth_required(auth)
|
||||
@keyword_blueprint.doc(summary="Returns all keywords", description="Returns all keywords for current user")
|
||||
def get_keywords():
|
||||
username = get_username_or_abort_401()
|
||||
email = get_email_or_abort_401()
|
||||
|
||||
return_keywords = []
|
||||
keywords = db.session.query(Keyword).filter_by(user_id=get_user_id_from_username(username)).all()
|
||||
keywords = db.session.query(Keyword).filter_by(email=email).all()
|
||||
|
||||
# If no keywords exist for this user -> return empty list
|
||||
# Otherwise iterate over all keywords, convert them to json and add them to the return list
|
||||
if keywords is not None:
|
||||
for row in keywords:
|
||||
return_keywords.append(row.as_dict())
|
||||
|
||||
return jsonify({"status": 200, "text": "Successfully loaded keywords", "data": return_keywords})
|
||||
return make_response(return_keywords, 200, "Successfully loaded keywords")
|
||||
|
||||
|
||||
def check_if_keyword_data_exists(data):
|
||||
if "keyword" not in data:
|
||||
abort(400, message="Keyword missing")
|
||||
return False
|
||||
|
||||
if data['keyword'] == "" or data['keyword'] is None:
|
||||
abort(400, message="Keyword missing")
|
||||
return False
|
||||
|
||||
return True
|
52
api/app/blueprints/portfolio.py
Normal file
52
api/app/blueprints/portfolio.py
Normal file
@ -0,0 +1,52 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.1"
|
||||
|
||||
import os
|
||||
|
||||
from apiflask import APIBlueprint
|
||||
from app.auth import auth
|
||||
from app.db import database as db
|
||||
from app.helper_functions import make_response, get_email_or_abort_401
|
||||
from app.models import SharePrice
|
||||
from app.schema import PortfolioResponseSchema
|
||||
|
||||
portfolio_blueprint = APIBlueprint('portfolio', __name__, url_prefix='/api')
|
||||
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
|
||||
|
||||
|
||||
@portfolio_blueprint.route('/portfolio', methods=['GET'])
|
||||
@portfolio_blueprint.output(PortfolioResponseSchema(many=True), 200)
|
||||
@portfolio_blueprint.auth_required(auth)
|
||||
@portfolio_blueprint.doc(summary="Returns portfolio", description="Returns all shares of current user")
|
||||
def get_portfolio():
|
||||
email = get_email_or_abort_401()
|
||||
|
||||
return_portfolio = []
|
||||
|
||||
# Get all transactions of current user
|
||||
transactions = db.session.execute("SELECT isin, comment, SUM(count), SUM(price), MAX(time) FROM `transactions` WHERE email = '" + email + "' GROUP BY isin, comment;").all()
|
||||
|
||||
# If there are no transactions, return empty portfolio
|
||||
# Otherwise calculate portfolio
|
||||
if transactions is not None:
|
||||
for row in transactions:
|
||||
data = {
|
||||
"isin": row[0],
|
||||
"comment": row[1],
|
||||
"count": row[2],
|
||||
# "calculated_price": row[3],
|
||||
"last_transaction": row[4],
|
||||
'current_price': 0
|
||||
}
|
||||
|
||||
# Add current share value to portfolio
|
||||
query_share_price = db.session.query(SharePrice).filter_by(isin=row[0]).order_by(SharePrice.date.desc()).first()
|
||||
if query_share_price is not None:
|
||||
data['current_price'] = query_share_price.as_dict()['price']
|
||||
|
||||
return_portfolio.append(data)
|
||||
|
||||
return make_response(return_portfolio, 200, "Successfully loaded symbols")
|
92
api/app/blueprints/share_price.py
Normal file
92
api/app/blueprints/share_price.py
Normal file
@ -0,0 +1,92 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.1"
|
||||
|
||||
import datetime
|
||||
import os
|
||||
|
||||
from apiflask import APIBlueprint, abort
|
||||
from app.auth import auth
|
||||
from app.db import database as db
|
||||
from app.helper_functions import make_response
|
||||
from app.models import SharePrice
|
||||
from app.schema import SymbolPriceSchema
|
||||
|
||||
share_price_blueprint = APIBlueprint('share_price', __name__, url_prefix='/api')
|
||||
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
|
||||
|
||||
|
||||
@share_price_blueprint.route('/symbols', methods=['GET'])
|
||||
@share_price_blueprint.output({}, 200)
|
||||
@share_price_blueprint.auth_required(auth)
|
||||
@share_price_blueprint.doc(summary="Returns all transaction symbols", description="Returns all transaction symbols for all users")
|
||||
def get_transaction_symbols():
|
||||
# Get all transaction symbols
|
||||
symbols = db.session.execute("SELECT isin FROM `transactions` GROUP BY isin;").all()
|
||||
|
||||
return_symbols = []
|
||||
for s in symbols:
|
||||
return_symbols.append(s[0])
|
||||
|
||||
return make_response(return_symbols, 200, "Successfully loaded symbols")
|
||||
|
||||
|
||||
@share_price_blueprint.route('/symbol', methods=['POST'])
|
||||
@share_price_blueprint.output({}, 200)
|
||||
@share_price_blueprint.input(schema=SymbolPriceSchema)
|
||||
@share_price_blueprint.auth_required(auth)
|
||||
@share_price_blueprint.doc(summary="Adds new price for isin", description="Adds new price to database")
|
||||
def add_symbol_price(data):
|
||||
# Check if required data is available
|
||||
if not check_if_isin_data_exists(data):
|
||||
abort(400, message="ISIN missing")
|
||||
|
||||
if not check_if_price_data_exists(data):
|
||||
abort(400, message="Price missing")
|
||||
|
||||
if not check_if_time_data_exists(data):
|
||||
abort(400, message="Time missing")
|
||||
|
||||
# Add share price
|
||||
share_price = SharePrice(
|
||||
isin=data['isin'],
|
||||
price=data['price'],
|
||||
date=datetime.datetime.strptime(data['time'], '%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||
)
|
||||
|
||||
db.session.add(share_price)
|
||||
db.session.commit()
|
||||
|
||||
return make_response(share_price.as_dict(), 200, "Successfully added price")
|
||||
|
||||
|
||||
def check_if_isin_data_exists(data):
|
||||
if 'isin' not in data:
|
||||
return False
|
||||
|
||||
if data['isin'] == "" or data['isin'] is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_if_price_data_exists(data):
|
||||
if 'price' not in data:
|
||||
return False
|
||||
|
||||
if data['price'] == "" or data['price'] is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_if_time_data_exists(data):
|
||||
if 'time' not in data:
|
||||
return False
|
||||
|
||||
if data['time'] == "" or data['time'] is None:
|
||||
return False
|
||||
|
||||
return True
|
112
api/app/blueprints/shares.py
Normal file
112
api/app/blueprints/shares.py
Normal file
@ -0,0 +1,112 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.1"
|
||||
|
||||
import os
|
||||
|
||||
from apiflask import APIBlueprint, abort
|
||||
from app.auth import auth
|
||||
from app.db import database as db
|
||||
from app.helper_functions import make_response, get_email_or_abort_401
|
||||
from app.models import Share
|
||||
from app.schema import SymbolSchema, SymbolResponseSchema, DeleteSuccessfulSchema, SymbolRemoveSchema
|
||||
|
||||
shares_blueprint = APIBlueprint('share', __name__, url_prefix='/api')
|
||||
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
|
||||
|
||||
|
||||
@shares_blueprint.route('/share', methods=['POST'])
|
||||
@shares_blueprint.output(SymbolResponseSchema(many=True), 200)
|
||||
@shares_blueprint.input(schema=SymbolSchema)
|
||||
@shares_blueprint.auth_required(auth)
|
||||
@shares_blueprint.doc(summary="Add new symbol", description="Adds new symbol for current user")
|
||||
def add_symbol(data):
|
||||
email = get_email_or_abort_401()
|
||||
|
||||
# Check if required data is available
|
||||
if not check_if_isin_data_exists(data):
|
||||
abort(400, message="ISIN missing")
|
||||
|
||||
if not check_if_comment_data_exists(data):
|
||||
abort(400, message="Comment missing")
|
||||
|
||||
# Check if share already exists
|
||||
check_share = db.session.query(Share).filter_by(isin=data['isin'], email=email).first()
|
||||
if check_share is None:
|
||||
# Keyword doesn't exist yet for this user -> add it
|
||||
new_symbol = Share(
|
||||
email=email,
|
||||
isin=data['isin'],
|
||||
comment=data['comment']
|
||||
)
|
||||
db.session.add(new_symbol)
|
||||
db.session.commit()
|
||||
|
||||
return make_response(new_symbol.as_dict(), 200, "Successfully added symbol")
|
||||
else:
|
||||
abort(500, "Symbol already exist for this user")
|
||||
|
||||
|
||||
@shares_blueprint.route('/share', methods=['DELETE'])
|
||||
@shares_blueprint.output(DeleteSuccessfulSchema, 200)
|
||||
@shares_blueprint.input(schema=SymbolRemoveSchema)
|
||||
@shares_blueprint.auth_required(auth)
|
||||
@shares_blueprint.doc(summary="Removes existing symbol", description="Removes existing symbol for current user")
|
||||
def remove_symbol(data):
|
||||
email = get_email_or_abort_401()
|
||||
|
||||
# Check if required data is available
|
||||
if not check_if_isin_data_exists(data):
|
||||
abort(400, message="ISIN missing")
|
||||
|
||||
# Check if share exists
|
||||
check_share = db.session.query(Share).filter_by(isin=data['isin'], email=email).first()
|
||||
if check_share is None:
|
||||
abort(500, "Symbol doesn't exist for this user")
|
||||
else:
|
||||
# Delete share
|
||||
db.session.query(Share).filter_by(isin=data['isin'], email=email).delete()
|
||||
db.session.commit()
|
||||
|
||||
return make_response({}, 200, "Successfully removed symbol")
|
||||
|
||||
|
||||
@shares_blueprint.route('/shares', methods=['GET'])
|
||||
@shares_blueprint.output(SymbolResponseSchema(many=True), 200)
|
||||
@shares_blueprint.auth_required(auth)
|
||||
@shares_blueprint.doc(summary="Returns all symbols", description="Returns all symbols for current user")
|
||||
def get_symbol():
|
||||
email = get_email_or_abort_401()
|
||||
|
||||
return_symbols = []
|
||||
symbols = db.session.query(Share).filter_by(email=email).all()
|
||||
|
||||
# If no shares exist for this user -> return empty list
|
||||
# Otherwise iterate over all shares, convert them to json and add them to the return list
|
||||
if symbols is not None:
|
||||
for row in symbols:
|
||||
return_symbols.append(row.as_dict())
|
||||
|
||||
return make_response(return_symbols, 200, "Successfully loaded symbols")
|
||||
|
||||
|
||||
def check_if_isin_data_exists(data):
|
||||
if "isin" not in data:
|
||||
return False
|
||||
|
||||
if data['isin'] == "" or data['isin'] is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_if_comment_data_exists(data):
|
||||
if "comment" not in data:
|
||||
return False
|
||||
|
||||
if data['comment'] == "" or data['comment'] is None:
|
||||
return False
|
||||
|
||||
return True
|
47
api/app/blueprints/telegram.py
Normal file
47
api/app/blueprints/telegram.py
Normal file
@ -0,0 +1,47 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
import os
|
||||
|
||||
from apiflask import APIBlueprint, abort
|
||||
from app.auth import auth
|
||||
from app.db import database as db
|
||||
from app.helper_functions import make_response, get_email_or_abort_401, get_user
|
||||
from app.schema import TelegramIdSchema, UsersSchema
|
||||
|
||||
telegram_blueprint = APIBlueprint('telegram', __name__, url_prefix='/api')
|
||||
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
|
||||
|
||||
|
||||
@telegram_blueprint.route('/telegram', methods=['POST'])
|
||||
@telegram_blueprint.output(UsersSchema(many=False), 200)
|
||||
@telegram_blueprint.input(schema=TelegramIdSchema)
|
||||
@telegram_blueprint.auth_required(auth)
|
||||
@telegram_blueprint.doc(summary="Connects telegram user id", description="Connects telegram user id to user account")
|
||||
def add_keyword(data):
|
||||
email = get_email_or_abort_401()
|
||||
|
||||
# Check if request data is valid
|
||||
if not check_if_telegram_user_id_data_exists(data):
|
||||
abort(400, message="User ID missing")
|
||||
|
||||
query_user = get_user(email)
|
||||
|
||||
# Change user id
|
||||
query_user.telegram_user_id = data['telegram_user_id']
|
||||
db.session.commit()
|
||||
|
||||
return make_response(query_user.as_dict(), 200, "Successfully connected telegram user")
|
||||
|
||||
|
||||
def check_if_telegram_user_id_data_exists(data):
|
||||
if "telegram_user_id" not in data:
|
||||
return False
|
||||
|
||||
if data['telegram_user_id'] == "" or data['telegram_user_id'] is None:
|
||||
return False
|
||||
|
||||
return True
|
127
api/app/blueprints/transactions.py
Normal file
127
api/app/blueprints/transactions.py
Normal file
@ -0,0 +1,127 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.1"
|
||||
|
||||
import datetime
|
||||
import os
|
||||
|
||||
from apiflask import abort, APIBlueprint
|
||||
from app.auth import auth
|
||||
from app.db import database as db
|
||||
from app.helper_functions import make_response, get_email_or_abort_401
|
||||
from app.models import Transaction
|
||||
from app.schema import TransactionSchema, TransactionResponseSchema
|
||||
|
||||
transaction_blueprint = APIBlueprint('transaction', __name__, url_prefix='/api')
|
||||
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
|
||||
|
||||
|
||||
@transaction_blueprint.route('/transaction', methods=['POST'])
|
||||
@transaction_blueprint.output(TransactionResponseSchema(), 200)
|
||||
@transaction_blueprint.input(schema=TransactionSchema)
|
||||
@transaction_blueprint.auth_required(auth)
|
||||
@transaction_blueprint.doc(summary="Adds new transaction", description="Adds new transaction for current user")
|
||||
def add_transaction(data):
|
||||
email = get_email_or_abort_401()
|
||||
|
||||
# Check if required data is available
|
||||
if not check_if_isin_data_exists(data):
|
||||
abort(400, "ISIN missing")
|
||||
|
||||
if not check_if_time_data_exists(data):
|
||||
abort(400, "Time missing")
|
||||
|
||||
if not check_if_comment_data_exists(data):
|
||||
abort(400, "Comment missing")
|
||||
|
||||
if not check_if_count_data_exists(data):
|
||||
abort(400, "Count missing")
|
||||
|
||||
if not check_if_price_data_exists(data):
|
||||
abort(400, "Price missing")
|
||||
|
||||
# Add transaction
|
||||
new_transaction = Transaction(
|
||||
email=email,
|
||||
isin=data['isin'],
|
||||
comment=data['comment'],
|
||||
time=datetime.datetime.strptime(data['time'], '%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||
count=data['count'],
|
||||
price=data['price']
|
||||
)
|
||||
db.session.add(new_transaction)
|
||||
db.session.commit()
|
||||
|
||||
return make_response(new_transaction.as_dict(), 200, "Successfully added transaction")
|
||||
|
||||
|
||||
@transaction_blueprint.route('/transactions', methods=['GET'])
|
||||
@transaction_blueprint.output(TransactionSchema(), 200)
|
||||
@transaction_blueprint.auth_required(auth)
|
||||
@transaction_blueprint.doc(summary="Returns all transactions", description="Returns all transactions for current user")
|
||||
def get_transaction():
|
||||
email = get_email_or_abort_401()
|
||||
|
||||
return_transactions = []
|
||||
|
||||
# Get all transactions
|
||||
transactions = db.session.query(Transaction).filter_by(email=email).all()
|
||||
|
||||
# Iterate over transactions and add them to return_transactions
|
||||
if transactions is not None:
|
||||
for row in transactions:
|
||||
return_transactions.append(row.as_dict())
|
||||
|
||||
return make_response(return_transactions, 200, "Successfully loaded transactions")
|
||||
|
||||
|
||||
def check_if_isin_data_exists(data):
|
||||
if "isin" not in data:
|
||||
return False
|
||||
|
||||
if data['isin'] == "" or data['isin'] is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_if_time_data_exists(data):
|
||||
if "time" not in data:
|
||||
return False
|
||||
|
||||
if data['time'] == "" or data['time'] is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_if_comment_data_exists(data):
|
||||
if "comment" not in data:
|
||||
return False
|
||||
|
||||
if data['comment'] == "" or data['comment'] is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_if_count_data_exists(data):
|
||||
if "count" not in data:
|
||||
return False
|
||||
|
||||
if data['count'] == "" or data['count'] is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_if_price_data_exists(data):
|
||||
if "price" not in data:
|
||||
return False
|
||||
|
||||
if data['price'] == "" or data['price'] is None:
|
||||
return False
|
||||
|
||||
return True
|
258
api/app/blueprints/user.py
Normal file
258
api/app/blueprints/user.py
Normal file
@ -0,0 +1,258 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
import datetime
|
||||
import os
|
||||
|
||||
import jwt
|
||||
from apiflask import APIBlueprint, abort
|
||||
from app.auth import auth
|
||||
from app.db import database as db
|
||||
from app.helper_functions import check_password, hash_password, abort_if_no_admin, make_response, get_email_or_abort_401, get_user
|
||||
from app.models import User
|
||||
from app.schema import UsersSchema, TokenSchema, LoginDataSchema, AdminDataSchema, DeleteUserSchema, RegisterDataSchema, UpdateUserDataSchema, CronDataSchema
|
||||
from flask import current_app
|
||||
|
||||
users_blueprint = APIBlueprint('users', __name__, url_prefix='/api')
|
||||
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
|
||||
|
||||
|
||||
@users_blueprint.route('/users', methods=['GET'])
|
||||
@users_blueprint.output(UsersSchema(many=True), 200)
|
||||
@users_blueprint.auth_required(auth)
|
||||
@users_blueprint.doc(summary="Get all users", description="Returns all existing users as array")
|
||||
def users():
|
||||
abort_if_no_admin()
|
||||
|
||||
res = []
|
||||
|
||||
# Query all users and convert them to dicts
|
||||
for i in User.query.all():
|
||||
res.append(i.as_dict())
|
||||
|
||||
return make_response(res, 200, "Successfully received all users")
|
||||
|
||||
|
||||
@users_blueprint.route('/user', methods=['GET'])
|
||||
@users_blueprint.output(UsersSchema(), 200)
|
||||
@users_blueprint.auth_required(auth)
|
||||
@users_blueprint.doc(summary="Get current user", description="Returns current user")
|
||||
def user():
|
||||
email = get_email_or_abort_401()
|
||||
|
||||
# Query current user
|
||||
query_user = get_user(email)
|
||||
|
||||
return make_response(query_user.as_dict(), 200, "Successfully received current user data")
|
||||
|
||||
|
||||
@users_blueprint.route('/user/login', methods=['POST'])
|
||||
@users_blueprint.output(TokenSchema(), 200)
|
||||
@users_blueprint.input(schema=LoginDataSchema)
|
||||
@users_blueprint.doc(summary="Login", description="Returns jwt token if username and password match, otherwise returns error")
|
||||
def login(data):
|
||||
# Check if required data is available
|
||||
if not check_if_password_data_exists(data):
|
||||
abort(400, "Password missing")
|
||||
|
||||
if not check_if_email_data_exists(data):
|
||||
abort(400, "Email missing")
|
||||
|
||||
# Query current user
|
||||
query_user = get_user(data['email'])
|
||||
|
||||
# Check if password matches
|
||||
if not check_password(query_user.password, data['password'].encode("utf-8")): # Password incorrect
|
||||
abort(500, message="Unable to login")
|
||||
|
||||
# Check if user is bot
|
||||
if query_user.email == current_app.config['BOT_EMAIL']:
|
||||
# Set bot token valid for 1 year
|
||||
token = jwt.encode({'email': query_user.email, 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=365)}, current_app.config['SECRET_KEY'], "HS256")
|
||||
else:
|
||||
# Set token valid for 1 day
|
||||
token = jwt.encode({'email': query_user.email, 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1)}, current_app.config['SECRET_KEY'], "HS256")
|
||||
|
||||
return make_response({"token": token}, 200, "Successfully logged in")
|
||||
|
||||
|
||||
@users_blueprint.route('/user/register', methods=['POST'])
|
||||
@users_blueprint.output(UsersSchema(), 200)
|
||||
@users_blueprint.input(schema=RegisterDataSchema)
|
||||
@users_blueprint.doc(summary="Register", description="Registers user")
|
||||
def register(data):
|
||||
# Check if required data is available
|
||||
if not check_if_email_data_exists(data):
|
||||
abort(400, "Email missing")
|
||||
|
||||
if not check_if_username_data_exists(data):
|
||||
abort(400, "Username missing")
|
||||
|
||||
if not check_if_password_data_exists(data):
|
||||
abort(400, "Password missing")
|
||||
|
||||
# Check if user already exists
|
||||
query_user = db.session.query(User).filter_by(email=data['email']).first()
|
||||
if query_user is not None:
|
||||
abort(500, message="Email already exist")
|
||||
|
||||
# Add user to database
|
||||
new_user = User(
|
||||
email=data['email'],
|
||||
username=data['username'],
|
||||
password=hash_password(data['password']),
|
||||
admin=False,
|
||||
cron="0 8 * * *"
|
||||
)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
|
||||
return make_response(new_user.as_dict(), 200, "Successfully registered user")
|
||||
|
||||
|
||||
@users_blueprint.route('/user', methods=['PUT'])
|
||||
@users_blueprint.output({}, 200)
|
||||
@users_blueprint.input(schema=UpdateUserDataSchema)
|
||||
@users_blueprint.auth_required(auth)
|
||||
@users_blueprint.doc(summary="Update user", description="Changes password and/or username of current user")
|
||||
def update_user(data):
|
||||
email = get_email_or_abort_401()
|
||||
|
||||
# Query current user
|
||||
query_user = get_user(email)
|
||||
|
||||
# Check if password data is available -> if, change password
|
||||
if check_if_password_data_exists(data):
|
||||
query_user.password = hash_password(data['password'])
|
||||
|
||||
# Check if username data is available -> if, change username
|
||||
if check_if_username_data_exists(data):
|
||||
query_user.username = data['username']
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return make_response({}, 200, "Successfully updated user")
|
||||
|
||||
|
||||
@users_blueprint.route('/user/setAdmin', methods=['PUT'])
|
||||
@users_blueprint.output({}, 200)
|
||||
@users_blueprint.input(schema=AdminDataSchema)
|
||||
@users_blueprint.auth_required(auth)
|
||||
@users_blueprint.doc(summary="Set user admin state", description="Set admin state of specified user")
|
||||
def set_admin(data):
|
||||
abort_if_no_admin() # Only admin users can do this
|
||||
|
||||
# Check if required data is available
|
||||
if not check_if_email_data_exists(data):
|
||||
abort(400, "Email missing")
|
||||
|
||||
if not check_if_admin_data_exists(data):
|
||||
abort(400, "Admin data missing")
|
||||
|
||||
# Get user by email
|
||||
query_user = get_user(data['email'])
|
||||
|
||||
# Update user admin state
|
||||
query_user.admin = data['admin']
|
||||
db.session.commit()
|
||||
|
||||
return make_response({}, 200, "Successfully updated users admin rights")
|
||||
|
||||
|
||||
@users_blueprint.route('/user/setCron', methods=['PUT'])
|
||||
@users_blueprint.output({}, 200)
|
||||
@users_blueprint.input(schema=CronDataSchema)
|
||||
@users_blueprint.auth_required(auth)
|
||||
@users_blueprint.doc(summary="Set update cron", description="Set update cron of specified user")
|
||||
def set_cron(data):
|
||||
email = get_email_or_abort_401()
|
||||
|
||||
# Check if required data is available
|
||||
if not check_if_cron_data_exists(data):
|
||||
abort(400, "Cron data missing")
|
||||
|
||||
# Update user cron
|
||||
get_user(email).cron = data['cron']
|
||||
db.session.commit()
|
||||
|
||||
return make_response({}, 200, "Successfully updated users cron")
|
||||
|
||||
|
||||
@users_blueprint.route('/user', methods=['DELETE'])
|
||||
@users_blueprint.output({}, 200)
|
||||
@users_blueprint.input(schema=DeleteUserSchema)
|
||||
@users_blueprint.auth_required(auth)
|
||||
@users_blueprint.doc(summary="Delete user", description="Deletes user by username")
|
||||
def delete_user(data):
|
||||
# Check if required data is available
|
||||
if not check_if_email_data_exists(data):
|
||||
abort(400, "Email missing")
|
||||
|
||||
# Check if email to delete is current user
|
||||
# -> if, delete user
|
||||
# -> if not, check if user is admin
|
||||
# -> if, delete user
|
||||
# -> else, abort
|
||||
if data['email'] == get_email_or_abort_401(): # Username is same as current user
|
||||
db.session.query(User).filter_by(email=data['email']).delete()
|
||||
db.session.commit()
|
||||
else:
|
||||
abort_if_no_admin()
|
||||
|
||||
db.session.query(User).filter_by(email=data['email']).delete()
|
||||
db.session.commit()
|
||||
|
||||
return make_response({}, 200, "Successfully removed user")
|
||||
|
||||
|
||||
def check_if_email_data_exists(data):
|
||||
if "email" not in data:
|
||||
return False
|
||||
|
||||
if data['email'] == "" or data['email'] is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_if_password_data_exists(data):
|
||||
if "password" not in data:
|
||||
return False
|
||||
|
||||
if data['password'] == "" or data['password'] is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_if_username_data_exists(data):
|
||||
if "username" not in data:
|
||||
return False
|
||||
|
||||
if data['username'] == "" or data['username'] is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_if_admin_data_exists(data):
|
||||
if "admin" not in data:
|
||||
return False
|
||||
|
||||
if data['admin'] == "" or data['admin'] is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_if_cron_data_exists(data):
|
||||
if "cron" not in data:
|
||||
return False
|
||||
|
||||
if data['cron'] == "" or data['cron'] is None:
|
||||
return False
|
||||
|
||||
return True
|
33
api/app/config/flask.cfg
Normal file
33
api/app/config/flask.cfg
Normal file
@ -0,0 +1,33 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
import os
|
||||
from app.schema import BaseResponseSchema
|
||||
|
||||
# Flask settings
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'secret string')
|
||||
|
||||
|
||||
# Flask-SQLAlchemy settings
|
||||
SQLALCHEMY_DATABASE_URI = "mysql+pymysql://" + os.getenv('MYSQL_USER') + ":" + os.getenv('MYSQL_PASSWORD') + "@" + os.getenv('MYSQL_HOST') + ":" + os.getenv('MYSQL_PORT', '3306') + "/" + os.getenv('MYSQL_NAME', 'aktienbot')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False # Avoids SQLAlchemy warning
|
||||
SQLALCHEMY_ENGINE_OPTIONS = {
|
||||
'pool_size': 10,
|
||||
'pool_recycle': 60,
|
||||
'pool_pre_ping': True
|
||||
}
|
||||
|
||||
# openapi/Swagger config
|
||||
BASE_RESPONSE_DATA_KEY = "data"
|
||||
BASE_RESPONSE_SCHEMA = BaseResponseSchema
|
||||
|
||||
BOT_EMAIL = os.getenv('BOT_EMAIL')
|
||||
BOT_USERNAME = os.getenv('BOT_USERNAME')
|
||||
BOT_PASSWORD = os.getenv('BOT_PASSWORD')
|
||||
|
||||
ADMIN_EMAIL = os.getenv('ADMIN_EMAIL')
|
||||
ADMIN_USERNAME = os.getenv('ADMIN_USERNAME')
|
||||
ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD')
|
32
api/app/config/flask_test.cfg
Normal file
32
api/app/config/flask_test.cfg
Normal file
@ -0,0 +1,32 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
import os
|
||||
from app.schema import BaseResponseSchema
|
||||
|
||||
# Flask settings
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'secret string')
|
||||
|
||||
|
||||
# Flask-SQLAlchemy settings
|
||||
SQLALCHEMY_DATABASE_URI = "mysql+pymysql://" + os.getenv('MYSQL_USER') + ":" + os.getenv('MYSQL_PASSWORD') + "@" + os.getenv('MYSQL_HOST') + ":" + os.getenv('MYSQL_PORT', '3306') + "/" + os.getenv('MYSQL_NAME', 'aktienbot_test')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False # Avoids SQLAlchemy warning
|
||||
SQLALCHEMY_ENGINE_OPTIONS = {
|
||||
'pool_size': 100,
|
||||
'pool_recycle': 240 # 4 minutes
|
||||
}
|
||||
|
||||
# openapi/Swagger config
|
||||
BASE_RESPONSE_DATA_KEY = "data"
|
||||
BASE_RESPONSE_SCHEMA = BaseResponseSchema
|
||||
|
||||
BOT_EMAIL = "bot1@example.com"
|
||||
BOT_USERNAME = "bot1"
|
||||
BOT_PASSWORD = "bot1"
|
||||
|
||||
ADMIN_EMAIL = "admin1@example.com"
|
||||
ADMIN_USERNAME = "admin1"
|
||||
ADMIN_PASSWORD = "admin1"
|
10
api/app/db.py
Normal file
10
api/app/db.py
Normal file
@ -0,0 +1,10 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
# database object
|
||||
database = SQLAlchemy()
|
136
api/app/helper_functions.py
Normal file
136
api/app/helper_functions.py
Normal file
@ -0,0 +1,136 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
import bcrypt
|
||||
import jwt
|
||||
from apiflask import abort
|
||||
from app.db import database as db
|
||||
from app.models import User
|
||||
from flask import current_app
|
||||
from flask import request, jsonify
|
||||
|
||||
|
||||
def hash_password(password):
|
||||
"""
|
||||
Hash plain password to save it in the database
|
||||
"""
|
||||
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
|
||||
|
||||
|
||||
def check_password(hashed_password, user_password):
|
||||
"""
|
||||
Check if the password is correct using the bcrypt checkpw function
|
||||
"""
|
||||
return bcrypt.checkpw(user_password, hashed_password)
|
||||
|
||||
|
||||
def get_email_from_token_data(token):
|
||||
"""
|
||||
Extract email from token data
|
||||
"""
|
||||
|
||||
# If token is not provided-> return None
|
||||
if token is None or len(token) < 2:
|
||||
return None
|
||||
else:
|
||||
# Token contains "Bearer " -> remove it
|
||||
token = token[1]
|
||||
|
||||
# Again: Check if token is not None
|
||||
# Don't know why, but sometimes the token is None
|
||||
if token is not None:
|
||||
|
||||
# We decided to append the user id to the bearer token using ":" as separator to select an specific user
|
||||
# If the token contains ":" -> It may be a bot token
|
||||
# If token valid -> return user email, not bot email
|
||||
if ':' in token:
|
||||
telegram_user_id = token.split(":")[1]
|
||||
token = token.split(":")[0]
|
||||
|
||||
try:
|
||||
# Only allow selecting users with telegram_user_id if current user is the bot user
|
||||
if jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=["HS256"])['email'] == current_app.config['BOT_EMAIL']:
|
||||
res = db.session.query(User).filter_by(telegram_user_id=telegram_user_id).first()
|
||||
|
||||
# Check if user id exists
|
||||
if res is not None:
|
||||
return res.as_dict()['email']
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
except jwt.PyJWTError:
|
||||
return None
|
||||
|
||||
else: # "Normal" token, extract username from token
|
||||
try:
|
||||
# Return email from token if token is valid
|
||||
return jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=["HS256"])['email']
|
||||
except jwt.PyJWTError:
|
||||
return None
|
||||
|
||||
|
||||
def get_token():
|
||||
"""
|
||||
Extract token from Authorization header
|
||||
"""
|
||||
|
||||
# Check if Authorization header is provided
|
||||
if 'Authorization' in request.headers:
|
||||
return request.headers['Authorization'].split(" ")
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_email_or_abort_401():
|
||||
"""
|
||||
Try to receive email from token data
|
||||
If email is not provided -> abort 401
|
||||
"""
|
||||
email = get_email_from_token_data(get_token())
|
||||
|
||||
if email is None: # If token not provided or invalid -> return 401 code
|
||||
abort(401, message="Unable to login")
|
||||
|
||||
return email
|
||||
|
||||
|
||||
def abort_if_no_admin():
|
||||
"""
|
||||
Check if user is admin
|
||||
If not -> abort 401
|
||||
"""
|
||||
if not is_user_admin():
|
||||
abort(401, message="Only admin users can access this")
|
||||
|
||||
|
||||
def is_user_admin():
|
||||
"""
|
||||
Return users admin status
|
||||
"""
|
||||
email = get_email_or_abort_401()
|
||||
|
||||
return db.session.query(User).filter_by(email=email).first().admin
|
||||
|
||||
|
||||
def make_response(data, status=200, text=""):
|
||||
"""
|
||||
Generate response object
|
||||
"""
|
||||
return jsonify({"status": status, "text": text, "data": data})
|
||||
|
||||
|
||||
def get_user(email):
|
||||
"""
|
||||
Get user from database
|
||||
"""
|
||||
query_user = db.session.query(User).filter_by(email=email).first()
|
||||
|
||||
# Check if user exists
|
||||
if query_user is None:
|
||||
abort(500, message="Can't find user")
|
||||
|
||||
return query_user
|
72
api/app/models.py
Normal file
72
api/app/models.py
Normal file
@ -0,0 +1,72 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.1"
|
||||
|
||||
from app.db import database as db
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = 'users'
|
||||
email = db.Column('email', db.String(255), primary_key=True, nullable=False, unique=True)
|
||||
password = db.Column('password', db.BINARY(60), nullable=False)
|
||||
username = db.Column('username', db.String(255), nullable=False, server_default='')
|
||||
telegram_user_id = db.Column('telegram_user_id', db.String(255), nullable=True, server_default='')
|
||||
admin = db.Column('admin', db.Boolean(), server_default='0') # 0 = False, 1 = True
|
||||
cron = db.Column('cron', db.String(20), server_default='0 8 * * *', nullable=False)
|
||||
|
||||
def as_dict(self):
|
||||
return {
|
||||
"email": self.email,
|
||||
"username": self.username,
|
||||
"telegram_user_id": self.telegram_user_id,
|
||||
"admin": self.admin,
|
||||
"cron": self.cron
|
||||
}
|
||||
|
||||
|
||||
class Transaction(db.Model):
|
||||
__tablename__ = 'transactions'
|
||||
t_id = db.Column('t_id', db.Integer(), nullable=False, unique=True, primary_key=True)
|
||||
email = db.Column('email', db.String(255), db.ForeignKey('users.email', ondelete='CASCADE'))
|
||||
isin = db.Column('isin', db.String(255))
|
||||
comment = db.Column('comment', db.String(255))
|
||||
time = db.Column('time', db.DateTime())
|
||||
count = db.Column('count', db.Integer())
|
||||
price = db.Column('price', db.Float())
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
|
||||
class Keyword(db.Model):
|
||||
__tablename__ = 'keywords'
|
||||
s_id = db.Column('s_id', db.Integer(), nullable=False, unique=True, primary_key=True)
|
||||
email = db.Column('email', db.String(255), db.ForeignKey('users.email', ondelete='CASCADE'))
|
||||
keyword = db.Column('keyword', db.String(255))
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
|
||||
class Share(db.Model):
|
||||
__tablename__ = 'shares'
|
||||
a_id = db.Column('a_id', db.Integer(), nullable=False, unique=True, primary_key=True)
|
||||
email = db.Column('email', db.String(255), db.ForeignKey('users.email', ondelete='CASCADE'))
|
||||
isin = db.Column('isin', db.String(255))
|
||||
comment = db.Column('comment', db.String(255))
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
|
||||
class SharePrice(db.Model):
|
||||
__tablename__ = 'share_price'
|
||||
id = db.Column('id', db.Integer(), nullable=False, unique=True, primary_key=True)
|
||||
isin = db.Column('isin', db.String(255))
|
||||
price = db.Column('price', db.Float())
|
||||
date = db.Column('date', db.DateTime())
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
137
api/app/schema.py
Normal file
137
api/app/schema.py
Normal file
@ -0,0 +1,137 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.1"
|
||||
|
||||
from apiflask import Schema
|
||||
from apiflask.fields import Integer, String, Boolean, Field, Float
|
||||
from marshmallow import validate
|
||||
from marshmallow.fields import Email
|
||||
|
||||
|
||||
class BaseResponseSchema(Schema):
|
||||
text = String()
|
||||
status = Integer()
|
||||
data = Field()
|
||||
|
||||
|
||||
class UsersSchema(Schema):
|
||||
admin = Boolean()
|
||||
password = String()
|
||||
username = String()
|
||||
telegram_user_id = String()
|
||||
email = Email()
|
||||
cron = String()
|
||||
|
||||
|
||||
class AdminDataSchema(Schema):
|
||||
email = Email()
|
||||
admin = Boolean()
|
||||
|
||||
|
||||
class CronDataSchema(Schema):
|
||||
cron = String()
|
||||
|
||||
|
||||
class TokenSchema(Schema):
|
||||
token = String()
|
||||
|
||||
|
||||
class LoginDataSchema(Schema):
|
||||
email = Email()
|
||||
password = String()
|
||||
|
||||
|
||||
class RegisterDataSchema(Schema):
|
||||
email = Email()
|
||||
username = String()
|
||||
password = String()
|
||||
|
||||
|
||||
class UpdateUserDataSchema(Schema):
|
||||
username = String(required=False)
|
||||
password = String(required=False)
|
||||
|
||||
|
||||
class DeleteUserSchema(Schema):
|
||||
email = Email()
|
||||
|
||||
|
||||
class ChangePasswordSchema(Schema):
|
||||
old_password = String()
|
||||
new_password = String()
|
||||
|
||||
|
||||
class ChangeUsernameSchema(Schema):
|
||||
new_username = String()
|
||||
|
||||
|
||||
class KeywordSchema(Schema):
|
||||
keyword = String()
|
||||
|
||||
|
||||
class SymbolSchema(Schema):
|
||||
isin = String()
|
||||
comment = String()
|
||||
|
||||
|
||||
class SymbolRemoveSchema(Schema):
|
||||
isin = String()
|
||||
|
||||
|
||||
class TransactionSchema(Schema):
|
||||
isin = String()
|
||||
comment = String()
|
||||
time = String(validate=validate.Regexp(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z"))
|
||||
count = Integer()
|
||||
price = Float()
|
||||
|
||||
|
||||
class SymbolPriceSchema(Schema):
|
||||
isin = String()
|
||||
time = String(validate=validate.Regexp(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z"))
|
||||
price = Float()
|
||||
|
||||
|
||||
class TelegramIdSchema(Schema):
|
||||
telegram_user_id = String()
|
||||
|
||||
|
||||
class DeleteSuccessfulSchema(Schema):
|
||||
pass
|
||||
|
||||
|
||||
class KeywordResponseSchema(Schema):
|
||||
keyword = String()
|
||||
s_id = Integer()
|
||||
email = Email()
|
||||
|
||||
|
||||
class SymbolResponseSchema(Schema):
|
||||
isin = String()
|
||||
comment = String()
|
||||
s_id = Integer()
|
||||
email = Email()
|
||||
|
||||
|
||||
class PortfolioShareResponseSchema(Schema):
|
||||
count = Integer()
|
||||
last_transaction = String()
|
||||
|
||||
|
||||
class TransactionResponseSchema(Schema):
|
||||
email = Email()
|
||||
isin = String()
|
||||
comment = String()
|
||||
time = String()
|
||||
count = Integer()
|
||||
price = Float()
|
||||
|
||||
|
||||
class PortfolioResponseSchema(Schema):
|
||||
isin = String()
|
||||
comment = String()
|
||||
last_transaction = String()
|
||||
count = Integer()
|
||||
# price = Float()
|
18
api/auth.py
18
api/auth.py
@ -1,18 +0,0 @@
|
||||
import os
|
||||
|
||||
import jwt
|
||||
from apiflask import HTTPTokenAuth
|
||||
|
||||
auth = HTTPTokenAuth()
|
||||
|
||||
|
||||
@auth.verify_token
|
||||
def verify_token(token):
|
||||
if token is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
jwt.decode(token, os.getenv('SECRET_KEY'), algorithms=["HS256"])
|
||||
return True
|
||||
except jwt.exceptions.DecodeError:
|
||||
return False
|
@ -1,52 +0,0 @@
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from scheme import BaseResponseSchema
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class ConfigClass(object):
|
||||
""" Flask application config """
|
||||
|
||||
# Flask settings
|
||||
SECRET_KEY = os.getenv('SECRET_KEY')
|
||||
|
||||
# Flask-SQLAlchemy settings
|
||||
SQLALCHEMY_DATABASE_URI = "mysql+pymysql://" + \
|
||||
os.getenv('MYSQL_USER') + ":" + \
|
||||
os.getenv('MYSQL_PASSWORD') + "@" + \
|
||||
os.getenv('MYSQL_HOST') + ":" + \
|
||||
(os.getenv("MYSQL_PORT") or str(3306)) + "/" + \
|
||||
os.getenv('MYSQL_DATABASE')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False # Avoids SQLAlchemy warning
|
||||
|
||||
# openapi/Swagger config
|
||||
SPEC_FORMAT = 'yaml'
|
||||
SERVERS = [
|
||||
{
|
||||
"name": "Production",
|
||||
"url": "https://aktienbot.flokaiser.com"
|
||||
},
|
||||
{
|
||||
"name": "Local",
|
||||
"url": "http://127.0.0.1:5000"
|
||||
}
|
||||
]
|
||||
INFO = {
|
||||
'description': 'Webengineering 2 | Telegram Aktienbot',
|
||||
'version': '0.0.1'
|
||||
# 'termsOfService': 'http://example.com',
|
||||
# 'contact': {
|
||||
# 'name': 'API Support',
|
||||
# 'url': 'http://www.example.com/support',
|
||||
# 'email': 'support@example.com'
|
||||
# },
|
||||
# 'license': {
|
||||
# 'name': 'Apache 2.0',
|
||||
# 'url': 'http://www.apache.org/licenses/LICENSE-2.0.html'
|
||||
# }
|
||||
}
|
||||
BASE_RESPONSE_DATA_KEY = "data"
|
||||
BASE_RESPONSE_SCHEMA = BaseResponseSchema
|
@ -1,2 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
curl -s http://localhost:80/ -o /dev/null || exit 1
|
||||
|
@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
nginx -g "daemon off;" &
|
||||
uwsgi --ini deploy/uwsgi.ini
|
31
api/generate_sample_transactions.py
Normal file
31
api/generate_sample_transactions.py
Normal file
@ -0,0 +1,31 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
import os
|
||||
import random
|
||||
|
||||
import faker
|
||||
import requests
|
||||
|
||||
username = ''
|
||||
password = ''
|
||||
|
||||
shares = ["TWTR", "GOOG", "AAPL", "MSFT", "AMZN", "FB", "NFLX", "TSLA", "BABA", "BA", "BAC", "C", "CAT", "CSCO", "CVX", "DIS", "DOW", "DUK", "GE", "HD", "IBM" "INTC", "JNJ", "JPM", "KO",
|
||||
"MCD", "MMM", "MRK", "NKE", "PFE", "PG", "T", "UNH", "UTX", "V", "VZ", "WMT", "XOM", "YHOO", "ZTS"]
|
||||
|
||||
fake = faker.Faker()
|
||||
|
||||
token = requests.post(os.getenv("API_URL") + '/user/login', json={"email": username, "password": password}).json()['data']['token']
|
||||
|
||||
for i in range(1, 10):
|
||||
payload = {
|
||||
"count": random.randint(1, 100),
|
||||
"price": random.random() * 100,
|
||||
"symbol": shares[random.randint(0, len(shares) - 1)],
|
||||
"time": fake.date_time().isoformat() + ".000Z"
|
||||
}
|
||||
|
||||
response = requests.post(os.getenv("API_URL") + '/transaction', json=payload, headers={'Authorization': 'Bearer ' + token})
|
@ -1,73 +0,0 @@
|
||||
import hashlib
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import jwt
|
||||
from apiflask import abort
|
||||
from flask import request
|
||||
|
||||
from db import db
|
||||
from models import User
|
||||
|
||||
|
||||
def hash_password(password):
|
||||
salt = uuid.uuid4().hex
|
||||
return hashlib.sha256(salt.encode() + password.encode()).hexdigest() + ':' + salt
|
||||
|
||||
|
||||
def check_password(hashed_password, user_password):
|
||||
password, salt = hashed_password.split(':')
|
||||
return password == hashlib.sha256(salt.encode() + user_password.encode()).hexdigest()
|
||||
|
||||
|
||||
def get_token():
|
||||
token = None
|
||||
if 'Authorization' in request.headers:
|
||||
token = request.headers['Authorization'].split(" ")[1]
|
||||
|
||||
return token
|
||||
|
||||
|
||||
def extract_token_data(token):
|
||||
if token is not None:
|
||||
try:
|
||||
return jwt.decode(token, os.getenv('SECRET_KEY'), algorithms=["HS256"])
|
||||
except jwt.exceptions.DecodeError:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_username_from_token_data(token_data):
|
||||
if token_data is not None:
|
||||
return token_data['username']
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_user_id_from_username(username):
|
||||
if username is not None:
|
||||
return db.session.query(User).filter_by(username=username).first().user_id
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_username_or_abort_401():
|
||||
# get username from jwt token
|
||||
username = get_username_from_token_data(extract_token_data(get_token()))
|
||||
|
||||
if username is None: # If token not provided or invalid -> return 401 code
|
||||
abort(401, message="Unable to login")
|
||||
|
||||
return username
|
||||
|
||||
|
||||
def abort_if_no_admin():
|
||||
if not is_user_admin():
|
||||
abort(401, message="Only admin users can access this")
|
||||
|
||||
|
||||
def is_user_admin():
|
||||
username = get_username_or_abort_401()
|
||||
|
||||
return db.session.query(User).filter_by(username=username).first().admin
|
50
api/load_share_price.py
Normal file
50
api/load_share_price.py
Normal file
@ -0,0 +1,50 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
import requests
|
||||
import yfinance
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
def thread_function(s):
|
||||
my_share_info = yfinance.Ticker(s)
|
||||
my_share_data = my_share_info.info
|
||||
|
||||
if my_share_data['regularMarketPrice'] is not None:
|
||||
payload = {
|
||||
"symbol": s,
|
||||
"price": float(my_share_data['regularMarketPrice']),
|
||||
"time": datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
||||
}
|
||||
|
||||
requests.post(os.getenv("API_URL") + '/symbol', json=payload, headers={'Authorization': 'Bearer ' + token})
|
||||
|
||||
|
||||
def split(a, n):
|
||||
k, m = divmod(len(a), n)
|
||||
return (a[i * k + min(i, m):(i + 1) * k + min(i + 1, m)] for i in range(n))
|
||||
|
||||
|
||||
load_dotenv()
|
||||
username = os.getenv('ADMIN_EMAIL')
|
||||
password = os.getenv('ADMIN_PASSWORD')
|
||||
|
||||
token = requests.post(os.getenv("API_URL") + '/user/login', json={"email": username, "password": password}).json()['data']['token']
|
||||
|
||||
response = requests.get(os.getenv("API_URL") + '/symbols', headers={'Authorization': 'Bearer ' + token}).json()['data']
|
||||
|
||||
symbols = split(response, int(len(response) / 5))
|
||||
for symbol_list in symbols:
|
||||
for symbol in symbol_list:
|
||||
x = threading.Thread(target=thread_function, args=(symbol,))
|
||||
x.start()
|
||||
|
||||
time.sleep(10)
|
@ -1,46 +0,0 @@
|
||||
from db import db
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = 'users'
|
||||
username = db.Column('username', db.String(255), nullable=False, unique=True)
|
||||
password = db.Column('password', db.String(255), nullable=False, server_default='')
|
||||
user_id = db.Column('user_id', db.Integer(), primary_key=True)
|
||||
telegram_name = db.Column('telegram_name', db.String(255), nullable=True, server_default='')
|
||||
admin = db.Column('admin', db.Boolean(), server_default='0')
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
|
||||
class Transaction(db.Model):
|
||||
__tablename__ = 'transactions'
|
||||
t_id = db.Column('t_id', db.Integer(), nullable=False, unique=True, primary_key=True)
|
||||
user_id = db.Column('user_id', db.Integer(), db.ForeignKey('users.user_id', ondelete='CASCADE'))
|
||||
symbol = db.Column('symbol', db.String(255))
|
||||
time = db.Column('time', db.DateTime())
|
||||
count = db.Column('count', db.Integer())
|
||||
price = db.Column('price', db.Float())
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
|
||||
class Keyword(db.Model):
|
||||
__tablename__ = 'keywords'
|
||||
s_id = db.Column('s_id', db.Integer(), nullable=False, unique=True, primary_key=True)
|
||||
user_id = db.Column('user_id', db.Integer(), db.ForeignKey('users.user_id', ondelete='CASCADE'))
|
||||
keyword = db.Column('keyword', db.String(255))
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
|
||||
class Share(db.Model):
|
||||
__tablename__ = 'shares'
|
||||
a_id = db.Column('a_id', db.Integer(), nullable=False, unique=True, primary_key=True)
|
||||
user_id = db.Column('user_id', db.Integer(), db.ForeignKey('users.user_id', ondelete='CASCADE'))
|
||||
symbol = db.Column('symbol', db.String(255))
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
@ -1,12 +1,16 @@
|
||||
Flask~=2.0.3
|
||||
python-dotenv==0.19.2
|
||||
requests==2.27.1
|
||||
Flask~=2.1.1
|
||||
python-dotenv==0.20.0
|
||||
uwsgi==2.0.20
|
||||
Flask_SQLAlchemy==2.5.1
|
||||
python-dotenv==0.19.2
|
||||
python-dotenv==0.20.0
|
||||
pymysql==1.0.2
|
||||
pyjwt==2.0.0
|
||||
pyjwt==2.3.0
|
||||
apiflask==0.12.0
|
||||
flask-swagger-ui==3.36.0
|
||||
flask-cors==3.0.10
|
||||
|
||||
bcrypt==3.2.0
|
||||
pytest~=7.1.2
|
||||
pytest-cov
|
||||
marshmallow~=3.15.0
|
||||
faker~=13.6.0
|
||||
yfinance~=0.1.70
|
||||
requests~=2.27.1
|
@ -1,80 +0,0 @@
|
||||
from apiflask import Schema
|
||||
from apiflask.fields import Integer, String, Boolean, Field, Float
|
||||
|
||||
|
||||
class BaseResponseSchema(Schema):
|
||||
text = String()
|
||||
status = Integer()
|
||||
data = Field()
|
||||
|
||||
|
||||
class UsersSchema(Schema):
|
||||
admin = Boolean()
|
||||
password = String()
|
||||
telegram_name = String()
|
||||
user_id = Integer()
|
||||
username = String()
|
||||
|
||||
|
||||
class AdminDataSchema(Schema):
|
||||
username = String()
|
||||
admin = Boolean()
|
||||
|
||||
|
||||
class TokenSchema(Schema):
|
||||
token = String()
|
||||
|
||||
|
||||
class LoginDataSchema(Schema):
|
||||
username = String()
|
||||
password = String()
|
||||
|
||||
|
||||
class DeleteUserSchema(Schema):
|
||||
username = String()
|
||||
|
||||
|
||||
class ChangePasswordSchema(Schema):
|
||||
old_password = String()
|
||||
new_password = String()
|
||||
|
||||
|
||||
class ChangeUsernameSchema(Schema):
|
||||
new_username = String()
|
||||
|
||||
|
||||
class KeywordSchema(Schema):
|
||||
keyword = String()
|
||||
|
||||
|
||||
class KeywordResponseSchema(Schema):
|
||||
keyword = String()
|
||||
s_id = Integer()
|
||||
user_id = Integer()
|
||||
|
||||
|
||||
class SymbolSchema(Schema):
|
||||
symbol = String()
|
||||
|
||||
|
||||
class SymbolResponseSchema(Schema):
|
||||
symbol = String()
|
||||
s_id = Integer()
|
||||
user_id = Integer()
|
||||
|
||||
|
||||
class PortfolioShareResponseSchema(Schema):
|
||||
count = Integer()
|
||||
last_transaction = String()
|
||||
|
||||
|
||||
class TransactionSchema(Schema):
|
||||
user_id = Integer()
|
||||
symbol = String()
|
||||
time = String()
|
||||
count = Integer()
|
||||
price = Float()
|
||||
|
||||
|
||||
class DeleteSuccessfulSchema(Schema):
|
||||
pass
|
111
api/tests/conftest.py
Normal file
111
api/tests/conftest.py
Normal file
@ -0,0 +1,111 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
import pytest
|
||||
from app import create_app, db
|
||||
from app.models import User, Transaction, Keyword, Share
|
||||
|
||||
from app.helper_functions import hash_password
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def new_user():
|
||||
user = User(
|
||||
email="user@example.com",
|
||||
username="user",
|
||||
password=hash_password("password"),
|
||||
admin=False
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def new_transaction():
|
||||
transaction = Transaction(
|
||||
email="user@example.com",
|
||||
symbol="DTEGY",
|
||||
time="2022-03-29T10:00:00.000Z",
|
||||
count=10,
|
||||
price=9.99
|
||||
)
|
||||
return transaction
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def new_keyword():
|
||||
keyword = Keyword(
|
||||
email="user@example.com",
|
||||
keyword="Elon Musk",
|
||||
)
|
||||
return keyword
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def new_share():
|
||||
share = Share(
|
||||
email="user@example.com",
|
||||
symbol="DTEGY",
|
||||
)
|
||||
return share
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def test_client():
|
||||
flask_app = create_app('config/flask_test.cfg')
|
||||
|
||||
# Create a test client using the Flask application configured for testing
|
||||
with flask_app.test_client() as testing_client:
|
||||
# Establish an application context
|
||||
with flask_app.app_context():
|
||||
yield testing_client # this is where the testing happens!
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def init_database(test_client):
|
||||
# Create the database and the database table
|
||||
db.create_all()
|
||||
|
||||
# Insert user data
|
||||
user1 = User(
|
||||
email="user1@example.com",
|
||||
username="user1",
|
||||
password=hash_password("password"),
|
||||
telegram_user_id="12345678",
|
||||
admin=False
|
||||
)
|
||||
user2 = User(
|
||||
email="user2@example.com",
|
||||
username="user2",
|
||||
password=hash_password("password"),
|
||||
telegram_user_id="87654321",
|
||||
admin=False
|
||||
)
|
||||
admin = User(
|
||||
email="admin1@example.com",
|
||||
username="admin1",
|
||||
password=hash_password("admin1"),
|
||||
telegram_user_id="00000000",
|
||||
admin=True
|
||||
)
|
||||
bot = User(
|
||||
email="bot1@example.com",
|
||||
username="bot1",
|
||||
password=hash_password("bot1"),
|
||||
telegram_user_id="00000000",
|
||||
admin=False
|
||||
)
|
||||
db.session.add(user1)
|
||||
db.session.add(user2)
|
||||
db.session.add(admin)
|
||||
db.session.add(bot)
|
||||
|
||||
# Commit the changes for the users
|
||||
db.session.commit()
|
||||
|
||||
yield # this is where the testing happens!
|
||||
|
||||
db.session.commit()
|
||||
db.drop_all()
|
5
api/tests/functional/__init__.py
Normal file
5
api/tests/functional/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
17
api/tests/functional/helper_functions.py
Normal file
17
api/tests/functional/helper_functions.py
Normal file
@ -0,0 +1,17 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
import json
|
||||
|
||||
|
||||
def get_token(test_client, email, password):
|
||||
response = test_client.post('/api/user/login', data=json.dumps(dict(email=email, password=password)), content_type='application/json')
|
||||
|
||||
if "data" in json.loads(response.data):
|
||||
if "token" in json.loads(response.data)["data"]:
|
||||
return json.loads(response.data)["data"]["token"]
|
||||
|
||||
return ""
|
188
api/tests/functional/test_keyword.py
Normal file
188
api/tests/functional/test_keyword.py
Normal file
@ -0,0 +1,188 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
"""
|
||||
This file (test_keyword.py) contains the functional tests for the `keyword` blueprint.
|
||||
"""
|
||||
import json
|
||||
from tests.functional.helper_functions import get_token
|
||||
|
||||
|
||||
def test_add_keyword_not_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/keyword'
|
||||
|
||||
User is not logged in
|
||||
"""
|
||||
response = test_client.post('/api/keyword')
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_add_keyword_user1_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/keyword'
|
||||
|
||||
User1 is logged in
|
||||
"""
|
||||
response = test_client.post('/api/keyword', data=json.dumps(dict(keyword="DTEGY")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully added keyword' in response.data
|
||||
|
||||
|
||||
def test_add_keyword_user1_logged_in_but_keyword_exist(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/keyword'
|
||||
|
||||
User1 is logged in
|
||||
Add keyword two times
|
||||
"""
|
||||
test_client.post('/api/keyword', data=json.dumps(dict(keyword="DTEGY")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
response = test_client.post('/api/keyword', data=json.dumps(dict(keyword="DTEGY")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 500
|
||||
assert b'Keyword already exist for this user' in response.data
|
||||
|
||||
|
||||
def test_add_keyword_user1_logged_in_but_keyword_missing(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/keyword'
|
||||
|
||||
User1 is logged in
|
||||
Keyword is missing in post data
|
||||
"""
|
||||
response = test_client.post('/api/keyword', data=json.dumps(dict()),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_add_keyword_user1_logged_in_but_keyword_empty(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/keyword'
|
||||
|
||||
User1 is logged in
|
||||
Keyword is empty in post data
|
||||
"""
|
||||
response = test_client.post('/api/keyword', data=json.dumps(dict(keyword="")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_get_keyword_not_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/keyword'
|
||||
|
||||
User is not logged in
|
||||
"""
|
||||
response = test_client.get('/api/keywords')
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_get_keyword_user1_logged_in_empty_response(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/keyword'
|
||||
|
||||
User1 is logged in
|
||||
Empty response
|
||||
"""
|
||||
response = test_client.get('/api/keywords', headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))})
|
||||
assert response.status_code == 200
|
||||
assert b'"data":[' in response.data
|
||||
|
||||
|
||||
def test_get_keyword_user1_logged_in_response_data(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/keyword'
|
||||
|
||||
User1 is logged in
|
||||
Create some keywords for user1
|
||||
"""
|
||||
test_client.post('/api/keyword', data=json.dumps(dict(keyword="DTEGY")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
response = test_client.get('/api/keywords', headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))})
|
||||
assert response.status_code == 200
|
||||
assert b'"data":[' in response.data
|
||||
|
||||
|
||||
def test_delete_keyword_not_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test DELETE '/api/keyword'
|
||||
|
||||
User is not logged in
|
||||
"""
|
||||
response = test_client.delete('/api/keyword')
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_delete_keyword_user1_logged_in_but_empty_keyword(test_client, init_database):
|
||||
"""
|
||||
Test DELETE '/api/keyword'
|
||||
|
||||
User1 is logged in
|
||||
Keyword empty in in delete data
|
||||
"""
|
||||
response = test_client.delete('/api/keyword', data=json.dumps(dict(keyword="")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_delete_keyword_user1_logged_in_but_missing_keyword(test_client, init_database):
|
||||
"""
|
||||
Test DELETE '/api/keyword'
|
||||
|
||||
User1 is logged in
|
||||
Keyword missing in in delete data
|
||||
"""
|
||||
response = test_client.delete('/api/keyword', data=json.dumps(dict()),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_delete_keyword_user1_logged_in_keyword_exists(test_client, init_database):
|
||||
"""
|
||||
Test DELETE '/api/keyword'
|
||||
|
||||
User1 is logged in
|
||||
Keyword exists
|
||||
"""
|
||||
test_client.post('/api/keyword', data=json.dumps(dict(keyword="DTEGY")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
response = test_client.delete('/api/keyword', data=json.dumps(dict(keyword="DTEGY")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully removed keyword' in response.data
|
||||
|
||||
|
||||
def test_delete_keyword_user1_logged_in_keyword_not_exists(test_client, init_database):
|
||||
"""
|
||||
Test DELETE '/api/keyword'
|
||||
|
||||
User1 is logged in
|
||||
Keyword doesn't exists
|
||||
"""
|
||||
response = test_client.delete('/api/keyword', data=json.dumps(dict(keyword="DTEGY")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 500
|
||||
assert b'Keyword doesn\'t exist for this user' in response.data
|
56
api/tests/functional/test_portfolio.py
Normal file
56
api/tests/functional/test_portfolio.py
Normal file
@ -0,0 +1,56 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
"""
|
||||
This file (test_portfolio.py) contains the functional tests for the `portfolio` blueprint.
|
||||
"""
|
||||
import json
|
||||
from tests.functional.helper_functions import get_token
|
||||
|
||||
|
||||
def test_get_portfolio_not_logged_in_empty_response(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/portfolio'
|
||||
|
||||
User is not logged in
|
||||
"""
|
||||
response = test_client.get('/api/portfolio')
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_get_portfolio_user1_logged_in_empty_response(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/portfolio'
|
||||
|
||||
User1 is logged in
|
||||
Empty response
|
||||
"""
|
||||
response = test_client.get('/api/portfolio',
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert b'"data":[]' in response.data
|
||||
assert b'Successfully loaded symbols' in response.data
|
||||
|
||||
|
||||
def test_get_portfolio_user1_logged_in_response_data(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/portfolio'
|
||||
|
||||
User1 is logged in
|
||||
Create transaction data
|
||||
"""
|
||||
test_client.post('/api/transaction', data=json.dumps(dict(count=5, price=9.99, symbol="DTEGY", time="2022-03-29T10:00:00.000Z")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
|
||||
response = test_client.get('/api/portfolio',
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert b'"data":[]' not in response.data
|
||||
assert b'"data":[' in response.data
|
188
api/tests/functional/test_share.py
Normal file
188
api/tests/functional/test_share.py
Normal file
@ -0,0 +1,188 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
"""
|
||||
This file (test_share.py) contains the functional tests for the `share` blueprint.
|
||||
"""
|
||||
import json
|
||||
from tests.functional.helper_functions import get_token
|
||||
|
||||
|
||||
def test_add_share_not_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/share'
|
||||
|
||||
User is not logged in
|
||||
"""
|
||||
response = test_client.post('/api/share')
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_add_share_user1_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/share'
|
||||
|
||||
User1 is logged in
|
||||
"""
|
||||
response = test_client.post('/api/share', data=json.dumps(dict(symbol="DTEGY")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully added symbol' in response.data
|
||||
|
||||
|
||||
def test_add_share_user1_logged_in_but_symbol_exist(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/share'
|
||||
|
||||
User1 is logged in
|
||||
Add symbol two times
|
||||
"""
|
||||
test_client.post('/api/share', data=json.dumps(dict(symbol="DTEGY")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
response = test_client.post('/api/share', data=json.dumps(dict(symbol="DTEGY")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 500
|
||||
assert b'Symbol already exist for this user' in response.data
|
||||
|
||||
|
||||
def test_add_share_user1_logged_in_but_symbol_missing(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/share'
|
||||
|
||||
User1 is logged in
|
||||
Symbol is missing in post data
|
||||
"""
|
||||
response = test_client.post('/api/share', data=json.dumps(dict()),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_add_share_user1_logged_in_but_symbol_empty(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/share'
|
||||
|
||||
User1 is logged in
|
||||
Symbol is empty in post data
|
||||
"""
|
||||
response = test_client.post('/api/share', data=json.dumps(dict(symbol="")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_get_share_not_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/share'
|
||||
|
||||
User is not logged in
|
||||
"""
|
||||
response = test_client.get('/api/shares')
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_get_share_user1_logged_in_empty_response(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/share'
|
||||
|
||||
User1 is logged in
|
||||
Empty response
|
||||
"""
|
||||
response = test_client.get('/api/shares', headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))})
|
||||
assert response.status_code == 200
|
||||
assert b'"data":[' in response.data
|
||||
|
||||
|
||||
def test_get_share_user1_logged_in_response_data(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/share'
|
||||
|
||||
User1 is logged in
|
||||
Create some symbols for user1
|
||||
"""
|
||||
test_client.post('/api/share', data=json.dumps(dict(symbol="DTEGY")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
response = test_client.get('/api/shares', headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))})
|
||||
assert response.status_code == 200
|
||||
assert b'"data":[' in response.data
|
||||
|
||||
|
||||
def test_delete_share_not_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test DELETE '/api/share'
|
||||
|
||||
User is not logged in
|
||||
"""
|
||||
response = test_client.delete('/api/share')
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_delete_share_user1_logged_in_but_empty_symbol(test_client, init_database):
|
||||
"""
|
||||
Test DELETE '/api/share'
|
||||
|
||||
User1 is logged in
|
||||
Symbol empty in in delete data
|
||||
"""
|
||||
response = test_client.delete('/api/share', data=json.dumps(dict(symbol="")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_delete_share_user1_logged_in_but_missing_symbol(test_client, init_database):
|
||||
"""
|
||||
Test DELETE '/api/share'
|
||||
|
||||
User1 is logged in
|
||||
Symbol missing in in delete data
|
||||
"""
|
||||
response = test_client.delete('/api/share', data=json.dumps(dict()),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_delete_share_user1_logged_in_symbol_exists(test_client, init_database):
|
||||
"""
|
||||
Test DELETE '/api/share'
|
||||
|
||||
User1 is logged in
|
||||
Symbol exists
|
||||
"""
|
||||
test_client.post('/api/share', data=json.dumps(dict(symbol="DTEGY")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
response = test_client.delete('/api/share', data=json.dumps(dict(symbol="DTEGY")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully removed symbol' in response.data
|
||||
|
||||
|
||||
def test_delete_share_user1_logged_in_symbol_not_exists(test_client, init_database):
|
||||
"""
|
||||
Test DELETE '/api/share'
|
||||
|
||||
User1 is logged in
|
||||
Symbol doesn't exists
|
||||
"""
|
||||
response = test_client.delete('/api/share', data=json.dumps(dict(symbol="DTEGY")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 500
|
||||
assert b'Symbol doesn\'t exist for this user' in response.data
|
63
api/tests/functional/test_telegram.py
Normal file
63
api/tests/functional/test_telegram.py
Normal file
@ -0,0 +1,63 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
"""
|
||||
This file (test_telegram.py) contains the functional tests for the `telegram` blueprint.
|
||||
"""
|
||||
import json
|
||||
from tests.functional.helper_functions import get_token
|
||||
|
||||
|
||||
def test_add_telegram_not_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/telegram'
|
||||
|
||||
User is not logged in
|
||||
"""
|
||||
response = test_client.post('/api/telegram')
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_add_telegram_user1_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/telegram'
|
||||
|
||||
User1 is logged in
|
||||
"""
|
||||
response = test_client.post('/api/telegram', data=json.dumps(dict(telegram_user_id="12345678")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully connected telegram user' in response.data
|
||||
|
||||
|
||||
def test_add_telegram_user1_logged_in_user_data_missing(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/telegram'
|
||||
|
||||
User1 is logged in
|
||||
telegram_user_id is missing
|
||||
"""
|
||||
response = test_client.post('/api/telegram', data=json.dumps(dict()),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_add_telegram_user1_logged_in_user_data_empty(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/telegram'
|
||||
|
||||
User1 is logged in
|
||||
telegram_user_id is empty
|
||||
"""
|
||||
response = test_client.post('/api/telegram', data=json.dumps(dict(telegram_user_id="")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
100
api/tests/functional/test_transaction.py
Normal file
100
api/tests/functional/test_transaction.py
Normal file
@ -0,0 +1,100 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
"""
|
||||
This file (test_transaction.py) contains the functional tests for the `transaction` blueprint.
|
||||
"""
|
||||
import json
|
||||
from tests.functional.helper_functions import get_token
|
||||
|
||||
|
||||
def test_add_transaction_not_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/transaction'
|
||||
|
||||
User is not logged in
|
||||
"""
|
||||
response = test_client.get('/api/portfolio')
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_add_transaction_user1_logged_in_missing_data(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/transaction'
|
||||
|
||||
User1 is logged in
|
||||
Data missing
|
||||
"""
|
||||
# symbol missing
|
||||
response = test_client.post('/api/transaction', data=json.dumps(dict(time="2022-03-29T10:00:00.000Z", count=10, price=9.99)),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
# time missing
|
||||
response = test_client.post('/api/transaction', data=json.dumps(dict(symbol="DTEGY", count=10, price=9.99)),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
# count missing
|
||||
response = test_client.post('/api/transaction', data=json.dumps(dict(symbol="DTEGY", time="2022-03-29T10:00:00.000Z", price=9.99)),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
# price missing
|
||||
response = test_client.post('/api/transaction', data=json.dumps(dict(symbol="DTEGY", time="2022-03-29T10:00:00.000Z", count=10)),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_get_transaction_not_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/transaction'
|
||||
|
||||
User is not logged in
|
||||
"""
|
||||
response = test_client.get('/api/transactions')
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_get_transaction_user1_logged_in_empty_response(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/transaction'
|
||||
|
||||
User1 is logged in
|
||||
"""
|
||||
response = test_client.get('/api/transactions',
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully loaded transactions' in response.data
|
||||
|
||||
|
||||
def test_get_transaction_user1_logged_in_response_data(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/transaction'
|
||||
|
||||
User1 is logged in
|
||||
Create transaction
|
||||
"""
|
||||
test_client.post('/api/transaction', data=json.dumps(dict(count=5, price=9.99, symbol="DTEGY", time="2022-03-29T10:00:00.000Z")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
|
||||
response = test_client.get('/api/transactions',
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully loaded transactions' in response.data
|
599
api/tests/functional/test_user.py
Normal file
599
api/tests/functional/test_user.py
Normal file
@ -0,0 +1,599 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
"""
|
||||
This file (test_user.py) contains the functional tests for the `users` blueprint.
|
||||
"""
|
||||
import json
|
||||
|
||||
from tests.functional.helper_functions import get_token
|
||||
|
||||
|
||||
def test_login_with_valid_data(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/user/login'
|
||||
|
||||
Valid data
|
||||
"""
|
||||
response = test_client.post('/api/user/login', data=json.dumps(dict(email="user1@example.com", password="password")), content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully logged in' in response.data
|
||||
|
||||
|
||||
def test_login_with_wrong_password(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/user/login'
|
||||
|
||||
Wrong password
|
||||
"""
|
||||
response = test_client.post('/api/user/login', data=json.dumps(dict(email="user2@example.com", password="password2")), content_type='application/json')
|
||||
assert response.status_code == 500
|
||||
assert b'Unable to login' in response.data
|
||||
|
||||
|
||||
def test_login_user_not_exist(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/user/login'
|
||||
|
||||
User doesn't exist
|
||||
"""
|
||||
response = test_client.post('/api/user/login', data=json.dumps(dict(email="notexistinguser@example.com", password="password")), content_type='application/json')
|
||||
assert response.status_code == 500
|
||||
assert b'Can\'t find user' in response.data
|
||||
|
||||
|
||||
def test_login_email_missing(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/user/login'
|
||||
|
||||
Email missing
|
||||
"""
|
||||
response = test_client.post('/api/user/login', data=json.dumps(dict(password="password")), content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_login_password_missing(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/user/login'
|
||||
|
||||
Password missing
|
||||
"""
|
||||
response = test_client.post('/api/user/login', data=json.dumps(dict(email="user1@example.com")), content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_register_valid_data(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/user/register'
|
||||
|
||||
Valid data
|
||||
"""
|
||||
response = test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully registered user' in response.data
|
||||
|
||||
|
||||
def test_register_user_exists_already(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/user/register'
|
||||
|
||||
User exists already
|
||||
"""
|
||||
test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json')
|
||||
response = test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json')
|
||||
assert response.status_code == 500
|
||||
assert b'Email already exist' in response.data
|
||||
|
||||
|
||||
def test_register_email_missing(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/user/register'
|
||||
|
||||
Email missing
|
||||
"""
|
||||
response = test_client.post('/api/user/register', data=json.dumps(dict(password="password", username="user3")), content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_register_email_empty(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/user/register'
|
||||
|
||||
Email empty
|
||||
"""
|
||||
response = test_client.post('/api/user/register', data=json.dumps(dict(password="password", username="user3", email="")), content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'Not a valid email address' in response.data
|
||||
|
||||
|
||||
def test_register_password_missing(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/user/register'
|
||||
|
||||
Password missing
|
||||
"""
|
||||
response = test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", username="user3")), content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_register_password_empty(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/user/register'
|
||||
|
||||
password empty
|
||||
"""
|
||||
response = test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", username="user3", password="")), content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_register_username_missing(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/user/register'
|
||||
|
||||
Username missing
|
||||
"""
|
||||
response = test_client.post('/api/user/register', data=json.dumps(dict(password="password", email="user3@example.com")), content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_register_username_empty(test_client, init_database):
|
||||
"""
|
||||
Test POST '/api/user/register'
|
||||
|
||||
Username empty
|
||||
"""
|
||||
response = test_client.post('/api/user/register', data=json.dumps(dict(password="password", username="", email="user3@example.com")), content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_delete_user_not_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test DELETE '/api/user'
|
||||
|
||||
User is not logged in
|
||||
"""
|
||||
test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json')
|
||||
response = test_client.delete('/api/user', data=json.dumps(dict(email="user3@example.com")), content_type='application/json')
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_delete_user_same_user_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test DELETE '/api/user'
|
||||
|
||||
User3 is logged in
|
||||
"""
|
||||
test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json')
|
||||
response = test_client.delete('/api/user',
|
||||
data=json.dumps(dict(email="user3@example.com")),
|
||||
content_type='application/json',
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user3@example.com", "password"))})
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully removed user' in response.data
|
||||
|
||||
|
||||
def test_delete_user_different_user_logged_in_no_admin(test_client, init_database):
|
||||
"""
|
||||
Test DELETE '/api/user'
|
||||
|
||||
Different user is logged in -> no admin
|
||||
"""
|
||||
test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json')
|
||||
response = test_client.delete('/api/user',
|
||||
data=json.dumps(dict(email="user3@example.com")),
|
||||
content_type='application/json',
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))})
|
||||
assert response.status_code == 401
|
||||
assert b'Only admin users can access this' in response.data
|
||||
|
||||
|
||||
def test_delete_user_different_user_logged_in_admin(test_client, init_database):
|
||||
"""
|
||||
Test DELETE '/api/user'
|
||||
|
||||
Different user is logged in -> admin
|
||||
"""
|
||||
test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json')
|
||||
response = test_client.delete('/api/user',
|
||||
data=json.dumps(dict(email="user3@example.com")),
|
||||
content_type='application/json',
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))})
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully removed user' in response.data
|
||||
|
||||
|
||||
def test_delete_user_email_missing(test_client, init_database):
|
||||
"""
|
||||
Test DELETE '/api/user'
|
||||
|
||||
Email missing
|
||||
"""
|
||||
test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json')
|
||||
response = test_client.delete('/api/user',
|
||||
data=json.dumps(dict()),
|
||||
content_type='application/json',
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user3@example.com", "password"))})
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_delete_user_email_empty(test_client, init_database):
|
||||
"""
|
||||
Test DELETE '/api/user'
|
||||
|
||||
Email empty
|
||||
"""
|
||||
test_client.post('/api/user/register', data=json.dumps(dict(email="user3@example.com", password="password", username="user3")), content_type='application/json')
|
||||
response = test_client.delete('/api/user',
|
||||
data=json.dumps(dict(email="")),
|
||||
content_type='application/json',
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user3@example.com", "password"))})
|
||||
assert response.status_code == 400
|
||||
assert b'Not a valid email address' in response.data
|
||||
|
||||
|
||||
def test_get_current_user_user_not_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/user'
|
||||
|
||||
User is not logged in
|
||||
"""
|
||||
response = test_client.get('/api/user')
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_get_current_user_user1_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/user'
|
||||
|
||||
User1 is logged in
|
||||
"""
|
||||
response = test_client.get('/api/user', headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))})
|
||||
assert response.status_code == 200
|
||||
assert b'user1' in response.data
|
||||
assert b'user2' not in response.data
|
||||
|
||||
|
||||
def test_get_current_user_bot_logged_in_user_exists(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/user'
|
||||
|
||||
Bot1 is logged in and requests user 12345678
|
||||
"""
|
||||
response = test_client.get('/api/user', headers={"Authorization": "Bearer {}".format(get_token(test_client, "bot1@example.com", "bot1") + ":12345678")})
|
||||
assert response.status_code == 200
|
||||
assert b'user1' in response.data
|
||||
assert b'bot' not in response.data
|
||||
|
||||
|
||||
def test_get_current_user_bot_logged_in_user_not_exists(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/user'
|
||||
|
||||
Bot1 is logged in and requests user 1234 (not existing)
|
||||
"""
|
||||
response = test_client.get('/api/user', headers={"Authorization": "Bearer {}".format(get_token(test_client, "bot1@example.com", "bot1") + ":1234")})
|
||||
assert response.status_code == 401
|
||||
assert b'Unable to login' in response.data
|
||||
|
||||
|
||||
def test_get_current_user_user1_logged_in_but_no_bot(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/user'
|
||||
|
||||
User1 is logged in and requests user 1234 (not existing)
|
||||
Fails because user1 is not a bot
|
||||
"""
|
||||
response = test_client.get('/api/user', headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password") + ":12345678")})
|
||||
assert response.status_code == 401
|
||||
assert b'Unable to login' in response.data
|
||||
|
||||
|
||||
def test_get_current_user_invalid_token(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/user'
|
||||
|
||||
Invalid Bearer token
|
||||
"""
|
||||
response = test_client.get('/api/user', headers={"Authorization": "Bearer {}".format("invalidtoken:12345678")})
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_update_user_not_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user'
|
||||
|
||||
User is not logged in
|
||||
"""
|
||||
response = test_client.put('/api/user')
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_update_user1_logged_in_password_username(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user'
|
||||
|
||||
User1 is logged in
|
||||
Change Username and Password
|
||||
"""
|
||||
test_client.put('/api/user', data=json.dumps(dict(username="user4", password="password")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
response = test_client.get('/api/user',
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert b'user4' in response.data
|
||||
|
||||
|
||||
def test_update_user1_logged_in_password(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user'
|
||||
|
||||
User1 is logged in
|
||||
Change Password
|
||||
"""
|
||||
response = test_client.put('/api/user', data=json.dumps(dict(password="password123")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))})
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully updated user' in response.data
|
||||
|
||||
|
||||
def test_update_user1_logged_in_username(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user'
|
||||
|
||||
User1 is logged in
|
||||
Change Username
|
||||
"""
|
||||
response = test_client.put('/api/user', data=json.dumps(dict(username="user1")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))})
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully updated user' in response.data
|
||||
|
||||
|
||||
def test_set_admin_user_not_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user/setAdmin'
|
||||
|
||||
User is not logged in
|
||||
"""
|
||||
response = test_client.put('/api/user/setAdmin', data=json.dumps(dict(email="user1@example.com", admin=True)), content_type='application/json')
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_set_admin_user1_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user/setAdmin'
|
||||
|
||||
User1 is logged in (no admin)
|
||||
"""
|
||||
response = test_client.put('/api/user/setAdmin', data=json.dumps(dict(email="user1@example.com", admin=True)),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 401
|
||||
assert b'Only admin users can access this' in response.data
|
||||
|
||||
|
||||
def test_set_admin_admin1_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user/setAdmin'
|
||||
|
||||
Admin1 is logged in (admin)
|
||||
"""
|
||||
response = test_client.put('/api/user/setAdmin', data=json.dumps(dict(email="user1@example.com", admin=True)),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully updated users admin rights' in response.data
|
||||
|
||||
|
||||
def test_set_admin_admin1_logged_in_user_not_exist(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user/setAdmin'
|
||||
|
||||
Admin1 is logged in (admin)
|
||||
notexistinguser@example.com does not exist
|
||||
"""
|
||||
response = test_client.put('/api/user/setAdmin', data=json.dumps(dict(email="notexistinguser@example.com", admin=True)),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 500
|
||||
assert b'Can\'t find user' in response.data
|
||||
|
||||
|
||||
def test_set_admin_admin1_logged_in_email_missing(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user/setAdmin'
|
||||
|
||||
Admin1 is logged in (admin)
|
||||
email missing
|
||||
"""
|
||||
response = test_client.put('/api/user/setAdmin', data=json.dumps(dict(admin=True)),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_set_admin_admin1_logged_in_email_empty(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user/setAdmin'
|
||||
|
||||
Admin1 is logged in (admin)
|
||||
email missing
|
||||
"""
|
||||
response = test_client.put('/api/user/setAdmin', data=json.dumps(dict(email="", admin=True)),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'Not a valid email address.' in response.data
|
||||
|
||||
|
||||
def test_set_admin_admin1_logged_in_admin_missing(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user/setAdmin'
|
||||
|
||||
Admin1 is logged in (admin)
|
||||
admin data missing
|
||||
"""
|
||||
response = test_client.put('/api/user/setAdmin', data=json.dumps(dict(email="user1@example.com")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'missing' in response.data
|
||||
|
||||
|
||||
def test_set_admin_admin1_logged_in_admin_empty(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user/setAdmin'
|
||||
|
||||
Admin1 is logged in (admin)
|
||||
admin data missing
|
||||
"""
|
||||
response = test_client.put('/api/user/setAdmin', data=json.dumps(dict(email="user1@example.com", admin=None)),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert b'Field may not be null' in response.data
|
||||
|
||||
|
||||
def test_set_cron_user_not_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user/setCron'
|
||||
|
||||
User is not logged in
|
||||
"""
|
||||
response = test_client.put('/api/user/setCron')
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_set_cron_user1_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user/setCron'
|
||||
|
||||
User1 is logged in
|
||||
"""
|
||||
response = test_client.put('/api/user/setCron', data=json.dumps(dict(cron="* * * * *")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully updated users cron' in response.data
|
||||
|
||||
|
||||
def test_set_empty_cron_user1_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user/setCron'
|
||||
|
||||
User1 is logged in
|
||||
Interval is empty
|
||||
"""
|
||||
response = test_client.put('/api/user/setCron', data=json.dumps(dict(cron="")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_set_cron_bot_logged_in_user_exists(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user/setCron'
|
||||
|
||||
Bot1 is logged in and requests user 12345678
|
||||
"""
|
||||
response = test_client.put('/api/user/setCron', data=json.dumps(dict(cron="* * * * *")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "bot1@example.com", "bot1") + ":12345678")},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully updated users cron' in response.data
|
||||
|
||||
|
||||
def test_set_cron_bot_logged_in_user_not_exists(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user/setCron'
|
||||
|
||||
Bot1 is logged in and requests user 1234 (not existing)
|
||||
"""
|
||||
response = test_client.put('/api/user/setCron', data=json.dumps(dict(cron="* * * * *")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "bot1@example.com", "bot1") + ":1234")},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 401
|
||||
assert b'Unable to login' in response.data
|
||||
|
||||
|
||||
def test_set_cron_user1_logged_in_but_no_bot(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user/setCron'
|
||||
|
||||
User1 is logged in and requests user 1234 (not existing)
|
||||
Fails because user1 is not a bot
|
||||
"""
|
||||
response = test_client.put('/api/user/setCron', data=json.dumps(dict(cron="* * * * *")),
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password") + ":12345678")},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 401
|
||||
assert b'Unable to login' in response.data
|
||||
|
||||
|
||||
def test_set_cron_invalid_token(test_client, init_database):
|
||||
"""
|
||||
Test PUT '/api/user/setCron'
|
||||
|
||||
Invalid Bearer token
|
||||
"""
|
||||
response = test_client.put('/api/user/setCron', headers={"Authorization": "Bearer {}".format("invalidtoken:12345678")})
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_get_users_not_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/users'
|
||||
|
||||
User is not logged in
|
||||
"""
|
||||
response = test_client.get('/api/users')
|
||||
assert response.status_code == 401
|
||||
assert b'Unauthorized' in response.data
|
||||
|
||||
|
||||
def test_get_users_user1_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/users'
|
||||
|
||||
User1 is logged in (not admin)
|
||||
"""
|
||||
response = test_client.get('/api/users',
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "user1@example.com", "password"))},
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 401
|
||||
assert b'Only admin users can access this' in response.data
|
||||
|
||||
|
||||
def test_get_users_admin1_logged_in(test_client, init_database):
|
||||
"""
|
||||
Test GET '/api/users'
|
||||
|
||||
Admin1 is logged in (admin)
|
||||
"""
|
||||
response = test_client.get('/api/users',
|
||||
headers={"Authorization": "Bearer {}".format(get_token(test_client, "admin1@example.com", "admin1"))},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert b'Successfully received all users' in response.data
|
0
api/tests/pytest.ini
Normal file
0
api/tests/pytest.ini
Normal file
5
api/tests/unit/__init__.py
Normal file
5
api/tests/unit/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
17
api/tests/unit/test_auth.py
Normal file
17
api/tests/unit/test_auth.py
Normal file
@ -0,0 +1,17 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
"""
|
||||
This file (test_helper_functions.py) contains the unit tests for the helper_functions.py file.
|
||||
"""
|
||||
from app.auth import *
|
||||
|
||||
|
||||
def test_verify_token():
|
||||
"""
|
||||
Test verify_token function
|
||||
"""
|
||||
assert verify_token(None) is False
|
33
api/tests/unit/test_helper_functions.py
Normal file
33
api/tests/unit/test_helper_functions.py
Normal file
@ -0,0 +1,33 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
"""
|
||||
This file (test_helper_functions.py) contains the unit tests for the helper_functions.py file.
|
||||
"""
|
||||
from app.helper_functions import *
|
||||
|
||||
|
||||
def test_hash_password():
|
||||
"""
|
||||
Test hash_password function
|
||||
"""
|
||||
assert hash_password("password") != "password"
|
||||
|
||||
|
||||
def test_check_password():
|
||||
"""
|
||||
Test check_password function
|
||||
"""
|
||||
hashed = hash_password("password")
|
||||
assert check_password(hashed, "password".encode("utf-8")) is True
|
||||
assert check_password(hashed, "password1".encode("utf-8")) is False
|
||||
|
||||
|
||||
def test_get_email_from_token():
|
||||
"""
|
||||
Test get_email_from_token function
|
||||
"""
|
||||
assert get_email_from_token_data(None) is None
|
122
api/tests/unit/test_models.py
Normal file
122
api/tests/unit/test_models.py
Normal file
@ -0,0 +1,122 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
"""
|
||||
This file (test_models.py) contains the unit tests for the models.py file.
|
||||
"""
|
||||
from app.helper_functions import hash_password
|
||||
from app.models import User, Transaction, Keyword, Share
|
||||
|
||||
|
||||
def test_new_user():
|
||||
"""
|
||||
GIVEN a User model
|
||||
WHEN a new User is created
|
||||
THEN check the email, password, username, telegram_user_id and admin fields are defined correctly
|
||||
"""
|
||||
user = User(
|
||||
email="user@example.com",
|
||||
username="user",
|
||||
password=hash_password("password"),
|
||||
admin=False,
|
||||
cron="0 8 * * *",
|
||||
)
|
||||
assert user.email == 'user@example.com'
|
||||
assert user.password != 'password'
|
||||
assert user.username == "user"
|
||||
assert user.telegram_user_id is None
|
||||
assert user.admin is False
|
||||
assert user.as_dict() == {'cron': '0 8 * * *', 'email': 'user@example.com', 'username': 'user', 'telegram_user_id': None, 'admin': False}
|
||||
|
||||
|
||||
def test_new_user_with_fixture(new_user):
|
||||
"""
|
||||
GIVEN a User model
|
||||
WHEN a new User is created
|
||||
THEN check the email and password_hashed fields are defined correctly
|
||||
"""
|
||||
assert new_user.email == 'user@example.com'
|
||||
assert new_user.password != 'password'
|
||||
|
||||
|
||||
def test_new_transaction():
|
||||
"""
|
||||
GIVEN a Transaction model
|
||||
WHEN a new Transaction is created
|
||||
THEN check the email, symbol, time count and price fields are defined correctly
|
||||
"""
|
||||
transaction = Transaction(
|
||||
email="user@example.com",
|
||||
symbol="DTEGY",
|
||||
time="2022-03-29T10:00:00.000Z",
|
||||
count=10,
|
||||
price=9.99
|
||||
)
|
||||
assert transaction.email == 'user@example.com'
|
||||
assert transaction.symbol == "DTEGY"
|
||||
assert transaction.count == 10
|
||||
assert transaction.price == 9.99
|
||||
assert transaction.as_dict() == {'t_id': None, 'email': 'user@example.com', 'symbol': 'DTEGY', 'time': '2022-03-29T10:00:00.000Z', 'count': 10, 'price': 9.99}
|
||||
|
||||
|
||||
def test_new_transaction_with_fixture(new_transaction):
|
||||
"""
|
||||
GIVEN a User model
|
||||
WHEN a new User is created
|
||||
THEN check the email and password_hashed fields are defined correctly
|
||||
"""
|
||||
assert new_transaction.email == 'user@example.com'
|
||||
assert new_transaction.symbol == 'DTEGY'
|
||||
|
||||
|
||||
def test_new_keyword():
|
||||
"""
|
||||
GIVEN a Transaction model
|
||||
WHEN a new Transaction is created
|
||||
THEN check the email, symbol, time count and price fields are defined correctly
|
||||
"""
|
||||
keyword = Keyword(
|
||||
email="user@example.com",
|
||||
keyword="Elon Musk"
|
||||
)
|
||||
assert keyword.email == 'user@example.com'
|
||||
assert keyword.keyword == "Elon Musk"
|
||||
assert keyword.as_dict() == {'s_id': None, 'email': 'user@example.com', 'keyword': 'Elon Musk'}
|
||||
|
||||
|
||||
def test_new_keyword_with_fixture(new_keyword):
|
||||
"""
|
||||
GIVEN a User model
|
||||
WHEN a new User is created
|
||||
THEN check the email and password_hashed fields are defined correctly
|
||||
"""
|
||||
assert new_keyword.email == 'user@example.com'
|
||||
assert new_keyword.keyword == 'Elon Musk'
|
||||
|
||||
|
||||
def test_new_share():
|
||||
"""
|
||||
GIVEN a Transaction model
|
||||
WHEN a new Transaction is created
|
||||
THEN check the email, symbol, time count and price fields are defined correctly
|
||||
"""
|
||||
share = Share(
|
||||
email="user@example.com",
|
||||
symbol="DTEGY"
|
||||
)
|
||||
assert share.email == 'user@example.com'
|
||||
assert share.symbol == "DTEGY"
|
||||
assert share.as_dict() == {'a_id': None, 'email': 'user@example.com', 'symbol': 'DTEGY'}
|
||||
|
||||
|
||||
def test_new_share_with_fixture(new_share):
|
||||
"""
|
||||
GIVEN a User model
|
||||
WHEN a new User is created
|
||||
THEN check the email and password_hashed fields are defined correctly
|
||||
"""
|
||||
assert new_share.email == 'user@example.com'
|
||||
assert new_share.symbol == 'DTEGY'
|
46
api/tests/unit/test_transaction.py
Normal file
46
api/tests/unit/test_transaction.py
Normal file
@ -0,0 +1,46 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
"""
|
||||
This file (test_transaction.py) contains the unit tests for the blueprints/transaction.py file.
|
||||
"""
|
||||
from app.blueprints.transactions import *
|
||||
|
||||
|
||||
def test_check_if_symbol_data_exists():
|
||||
"""
|
||||
Test check_if_symbol_data_exists function
|
||||
"""
|
||||
assert check_if_symbol_data_exists(dict(symbol='DTEGY')) is True
|
||||
assert check_if_symbol_data_exists(dict(symbol='')) is False
|
||||
assert check_if_symbol_data_exists(dict()) is False
|
||||
|
||||
|
||||
def test_check_if_time_data_exists():
|
||||
"""
|
||||
Test check_if_time_data_exists function
|
||||
"""
|
||||
assert check_if_time_data_exists(dict(time='2022-03-29T10:00:00.000Z')) is True
|
||||
assert check_if_time_data_exists(dict(time='')) is False
|
||||
assert check_if_time_data_exists(dict()) is False
|
||||
|
||||
|
||||
def test_check_if_count_data_exists():
|
||||
"""
|
||||
Test check_if_count_data_exists function
|
||||
"""
|
||||
assert check_if_count_data_exists(dict(count=10)) is True
|
||||
assert check_if_count_data_exists(dict(count=None)) is False
|
||||
assert check_if_count_data_exists(dict()) is False
|
||||
|
||||
|
||||
def test_check_if_price_data_exists():
|
||||
"""
|
||||
Test check_if_price_data_exists function
|
||||
"""
|
||||
assert check_if_price_data_exists(dict(price=9.99)) is True
|
||||
assert check_if_price_data_exists(dict(price=None)) is False
|
||||
assert check_if_price_data_exists(dict()) is False
|
55
api/tests/unit/test_user.py
Normal file
55
api/tests/unit/test_user.py
Normal file
@ -0,0 +1,55 @@
|
||||
__author__ = "Florian Kaiser"
|
||||
__copyright__ = "Copyright 2022, Project Aktienbot"
|
||||
__credits__ = ["Florian Kaiser", "Florian Kellermann", "Linus Eickhof", "Kevin Pauer"]
|
||||
__license__ = "GPL 3.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
"""
|
||||
This file (test_helper_functions.py) contains the unit tests for the helper_functions.py file.
|
||||
"""
|
||||
from app.blueprints.user import *
|
||||
|
||||
|
||||
def test_check_if_email_data_exists():
|
||||
"""
|
||||
Test check_if_email_data_exists function
|
||||
"""
|
||||
assert check_if_email_data_exists(dict(email='user@example.com')) is True
|
||||
assert check_if_email_data_exists(dict(email='')) is False
|
||||
assert check_if_email_data_exists(dict()) is False
|
||||
|
||||
|
||||
def test_check_if_password_data_exists():
|
||||
"""
|
||||
Test check_if_password_data_exists function
|
||||
"""
|
||||
assert check_if_password_data_exists(dict(password='password')) is True
|
||||
assert check_if_password_data_exists(dict(password='')) is False
|
||||
assert check_if_password_data_exists(dict()) is False
|
||||
|
||||
|
||||
def test_check_if_username_data_exists():
|
||||
"""
|
||||
Test check_if_username_data_exists function
|
||||
"""
|
||||
assert check_if_username_data_exists(dict(username='user')) is True
|
||||
assert check_if_username_data_exists(dict(username='')) is False
|
||||
assert check_if_username_data_exists(dict()) is False
|
||||
|
||||
|
||||
def test_check_if_admin_data_exists():
|
||||
"""
|
||||
Test check_if_admin_data_exists function
|
||||
"""
|
||||
assert check_if_admin_data_exists(dict(admin=True)) is True
|
||||
assert check_if_admin_data_exists(dict(admin=None)) is False
|
||||
assert check_if_admin_data_exists(dict()) is False
|
||||
|
||||
|
||||
def test_check_if_cron_data_exists():
|
||||
"""
|
||||
Test check_if_cron_data_exists function
|
||||
"""
|
||||
assert check_if_cron_data_exists(dict(cron="* * * * *")) is True
|
||||
assert check_if_cron_data_exists(dict(cron="")) is False
|
||||
assert check_if_cron_data_exists(dict()) is False
|
3
deploy/README.md
Normal file
3
deploy/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Deploy
|
||||
|
||||
Files that are used for deployment.
|
16
deploy/aktienbot/.env.api
Normal file
16
deploy/aktienbot/.env.api
Normal file
@ -0,0 +1,16 @@
|
||||
BOT_API_KEY=
|
||||
SECRET_KEY=
|
||||
|
||||
MYSQL_USER=
|
||||
MYSQL_PASSWORD=
|
||||
MYSQL_HOST=
|
||||
MYSQL_PORT=
|
||||
MYSQL_DATABASE=
|
||||
|
||||
BOT_EMAIL=
|
||||
BOT_USERNAME=
|
||||
BOT_PASSWORD=
|
||||
|
||||
ADMIN_EMAIL=
|
||||
ADMIN_USERNAME=
|
||||
ADMIN_PASSWORD=
|
6
deploy/aktienbot/.env.bot
Normal file
6
deploy/aktienbot/.env.bot
Normal file
@ -0,0 +1,6 @@
|
||||
BOT_API_KEY=
|
||||
NEWS_API_KEY=
|
||||
SECRET_KEY=
|
||||
|
||||
BOT_EMAIL=
|
||||
BOT_PASSWORD=
|
62
deploy/aktienbot/docker-compose.yml
Normal file
62
deploy/aktienbot/docker-compose.yml
Normal file
@ -0,0 +1,62 @@
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
aktienbot_fe:
|
||||
image: registry.flokaiser.com/aktienbot/frontend
|
||||
labels:
|
||||
traefik.enable: 'true'
|
||||
traefik.http.routers.aktienbot_fe.rule: Host(`gruppe1.testsites.info`) && !PathPrefix(`/api`) && !PathPrefix(`/phpmyadmin`) && !PathPrefix(`/portainer`)
|
||||
traefik.http.routers.aktienbot_fe.middlewares: secHeaders@file
|
||||
traefik.http.routers.aktienbot_fe.priority: 40
|
||||
traefik.http.routers.aktienbot_fe.tls: true
|
||||
traefik.http.routers.aktienbot_fe.tls.certresolver: myresolver
|
||||
|
||||
aktienbot_api:
|
||||
image: registry.flokaiser.com/aktienbot/api
|
||||
labels:
|
||||
traefik.enable: 'true'
|
||||
traefik.http.routers.aktienbot_api.rule: Host(`gruppe1.testsites.info`) && PathPrefix(`/api`)
|
||||
traefik.http.routers.aktienbot_api.middlewares: secHeaders@file
|
||||
traefik.http.routers.aktienbot_api.priority: 50
|
||||
traefik.http.routers.aktienbot_api.tls: true
|
||||
traefik.http.routers.aktienbot_api.tls.certresolver: myresolver
|
||||
depends_on:
|
||||
- mariadb
|
||||
env_file:
|
||||
- ${PWD}/.env.api
|
||||
|
||||
aktienbot_bot:
|
||||
image: registry.flokaiser.com/aktienbot/bot
|
||||
env_file:
|
||||
- ${PWD}/.env.bot
|
||||
|
||||
mariadb:
|
||||
image: mariadb
|
||||
volumes:
|
||||
- mariadb_data:/var/lib/mysql
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=sBvKtMY7ej9*dETatTtk#uRd5f*5wJYovfdDJDa&
|
||||
|
||||
phpmyadmin:
|
||||
image: phpmyadmin
|
||||
environment:
|
||||
- PMA_HOST=mariadb
|
||||
- PMA_ABSOLUTE_URI=https://gruppe1.testsites.info/phpmyadmin/
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.phpmyadmin.rule: Host(`gruppe1.testsites.info`) && PathPrefix(`/phpmyadmin`)
|
||||
traefik.http.routers.phpmyadmin.middlewares: secHeaders@file,strip_phpmyadmin
|
||||
traefik.http.routers.phpmyadmin.priority: 50
|
||||
traefik.http.routers.phpmyadmin.tls: true
|
||||
traefik.http.routers.phpmyadmin.tls.certresolver: myresolver
|
||||
|
||||
traefik.http.middlewares.strip_phpmyadmin.stripprefix.prefixes: /phpmyadmin
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: net
|
||||
|
||||
volumes:
|
||||
portainer_data:
|
||||
mariadb_data:
|
3
deploy/base/.env
Normal file
3
deploy/base/.env
Normal file
@ -0,0 +1,3 @@
|
||||
WATCHTOWER_SCHEDULE=0 5 3 * * *
|
||||
WATCHTOWER_ROLLING_RESTART=true
|
||||
WATCHTOWER_CLEANUP=true
|
0
deploy/base/acme.json
Normal file
0
deploy/base/acme.json
Normal file
83
deploy/base/docker-compose.yml
Normal file
83
deploy/base/docker-compose.yml
Normal file
@ -0,0 +1,83 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ${PWD}/traefik.toml:/etc/traefik/traefik.toml
|
||||
- ${PWD}/traefik-dynamic.toml:/etc/traefik/traefik-dynamic.toml
|
||||
- ${PWD}/acme.json:/etc/traefik/acme.json
|
||||
- ${PWD}/access.log:/etc/traefik/access.log
|
||||
- ${PWD}/users:/etc/traefik/users
|
||||
|
||||
goaccess:
|
||||
image: allinurl/goaccess
|
||||
command:
|
||||
- --no-global-config
|
||||
- --config-file=/srv/data/goaccess.conf
|
||||
- --num-tests=0
|
||||
volumes:
|
||||
- ${PWD}/access.log:/srv/logs/access.log:ro
|
||||
- ${PWD}/goaccess.conf:/srv/data/goaccess.conf
|
||||
- goaccess_data:/srv/data
|
||||
- goaccess_report:/srv/report
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.goaccess.rule: Host(`gruppe1.testsites.info`) && PathPrefix(`/goaccess/ws`)
|
||||
traefik.http.routers.goaccess.priority: 55
|
||||
traefik.http.routers.goaccess.middlewares: strip_goaccess,goaccess_auth,secHeaders@file
|
||||
traefik.http.routers.goaccess.tls: true
|
||||
traefik.http.routers.goaccess.tls.certresolver: myresolver
|
||||
|
||||
nginx:
|
||||
image: nginx
|
||||
volumes:
|
||||
- goaccess_report:/usr/share/nginx/html
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.goaccess_web.rule: Host(`gruppe1.testsites.info`) && PathPrefix(`/goaccess`)
|
||||
traefik.http.routers.goaccess_web.priority: 50
|
||||
traefik.http.routers.goaccess_web.middlewares: strip_goaccess,goaccess_auth,secHeaders@file
|
||||
traefik.http.routers.goaccess_web.tls: true
|
||||
traefik.http.routers.goaccess_web.tls.certresolver: myresolver
|
||||
|
||||
traefik.http.middlewares.strip_goaccess.stripprefix.prefixes: /goaccess
|
||||
traefik.http.middlewares.goaccess_auth.basicauth.usersfile: /etc/traefik/users
|
||||
|
||||
portainer:
|
||||
image: portainer/portainer-ce
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.portainer.rule: Host(`gruppe1.testsites.info`) && PathPrefix(`/portainer`)
|
||||
traefik.http.routers.portainer.priority: 50
|
||||
traefik.http.services.portainer.loadbalancer.server.port: 9000
|
||||
traefik.http.routers.portainer.middlewares: strip_portainer,secHeaders@file
|
||||
traefik.http.routers.portainer.tls: true
|
||||
traefik.http.routers.portainer.tls.certresolver: myresolver
|
||||
|
||||
traefik.http.middlewares.strip_portainer.stripprefix.prefixes: /portainer
|
||||
volumes:
|
||||
- portainer_data:/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
env_file:
|
||||
- ${PWD}/.env
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: net
|
||||
|
||||
volumes:
|
||||
portainer_data:
|
||||
goaccess_report:
|
||||
goaccess_data:
|
5
deploy/base/goaccess.conf
Normal file
5
deploy/base/goaccess.conf
Normal file
@ -0,0 +1,5 @@
|
||||
log-format COMMON
|
||||
log-file /srv/logs/access.log
|
||||
output /srv/report/index.html
|
||||
real-time-html true
|
||||
ws-url wss://gruppe1.testsites.info:443/goaccess/ws
|
22
deploy/base/traefik-dynamic.toml
Normal file
22
deploy/base/traefik-dynamic.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[tls.options]
|
||||
[tls.options.default]
|
||||
minVersion = "VersionTLS12"
|
||||
cipherSuites = [
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
|
||||
"TLS_AES_128_GCM_SHA256",
|
||||
"TLS_AES_256_GCM_SHA384",
|
||||
"TLS_CHACHA20_POLY1305_SHA256"
|
||||
]
|
||||
curvePreferences = [ "CurveP521", "CurveP384" ]
|
||||
sniStrict = true
|
||||
|
||||
[http.middlewares.secHeaders.headers]
|
||||
browserXssFilter = true
|
||||
contentTypeNosniff = true
|
||||
frameDeny = true
|
||||
stsIncludeSubdomains = true
|
||||
stsPreload = true
|
||||
stsSeconds = 31_536_000
|
||||
customFrameOptionsValue = "SAMEORIGIN"
|
44
deploy/base/traefik.toml
Normal file
44
deploy/base/traefik.toml
Normal file
@ -0,0 +1,44 @@
|
||||
[log]
|
||||
level = "INFO"
|
||||
|
||||
[accessLog]
|
||||
filePath = "/etc/traefik/access.log"
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.web]
|
||||
address = ":80"
|
||||
|
||||
[entryPoints.web.forwardedHeaders]
|
||||
insecure = true
|
||||
|
||||
[entryPoints.web.http]
|
||||
[entryPoints.web.http.redirections]
|
||||
[entryPoints.web.http.redirections.entryPoint]
|
||||
to = "web-secure"
|
||||
scheme = "https"
|
||||
|
||||
[entryPoints.web-secure]
|
||||
address = ":443"
|
||||
|
||||
[entryPoints.web-secure.forwardedHeaders]
|
||||
insecure = true
|
||||
|
||||
[entryPoints.websecure.http]
|
||||
middlewares = ["secHeaders@file"]
|
||||
|
||||
[api]
|
||||
dashboard = true
|
||||
insecure = true
|
||||
|
||||
[providers.docker]
|
||||
watch = true
|
||||
exposedByDefault = false
|
||||
|
||||
[providers.file]
|
||||
filename = "/etc/traefik/traefik-dynamic.toml"
|
||||
|
||||
[certificatesResolvers.myresolver.acme]
|
||||
email = "inf20155@lehre.dhbw-stuttgart.de"
|
||||
storage = "/etc/traefik/acme.json"
|
||||
[certificatesResolvers.myresolver.acme.httpChallenge]
|
||||
entryPoint = "web"
|
1
deploy/base/users
Normal file
1
deploy/base/users
Normal file
@ -0,0 +1 @@
|
||||
# Create user by using ```htpasswd -nb user password``` and append output to this file.
|
31
documentation/README.md
Normal file
31
documentation/README.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Dokumentation
|
||||
|
||||
## Swagger Documentation
|
||||
Visit https://gruppe1.testsites.info/api/docs
|
||||
|
||||
## API
|
||||
- `api/openapi.json`
|
||||
- OpenAPI-Dokumentation
|
||||
- von APIFlask generiert
|
||||
- kann in Postman importiert werden
|
||||
- `api/postman.json`
|
||||
- Postman Collection
|
||||
|
||||
## Database
|
||||
- `database/structure_database.pdf`
|
||||
- Relationales Modell der Datenbank
|
||||
- `database/structure_database.uxf`
|
||||
- Relationales Modell der Datenbank
|
||||
- Source Datei des Modells
|
||||
|
||||
## Requirements
|
||||
- `requirements/2022-03-15 Anforderungen an Projekt.pdf`
|
||||
- Anforderungen an dieses Projekt
|
||||
|
||||
## Role-model
|
||||
Function|Admin|No Admin|
|
||||
|---|---|---|
|
||||
|Delete user|yes|only current user|
|
||||
|Update users|yes|only current user|
|
||||
|Set admin state|yes|no|
|
||||
|Show all users|yes|no|
|
1404
documentation/api/openapi.json
Normal file
1404
documentation/api/openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
||||
This is the database folder
|
Binary file not shown.
BIN
documentation/database/structure_database.png
Normal file
BIN
documentation/database/structure_database.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
@ -1,105 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<diagram program="umlet" version="14.3.0">
|
||||
<zoom_level>10</zoom_level>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>580</x>
|
||||
<y>320</y>
|
||||
<w>210</w>
|
||||
<h>80</h>
|
||||
</coordinates>
|
||||
<panel_attributes>Stichwort
|
||||
<diagram program="umletino" version="14.3.0"><zoom_level>10</zoom_level><help_text>Database V0.1</help_text><element><id>UMLClass</id><coordinates><x>705</x><y>360</y><w>210</w><h>80</h></coordinates><panel_attributes>keywords
|
||||
--
|
||||
PK: s_id
|
||||
FK: username
|
||||
Stichwort</panel_attributes>
|
||||
<additional_attributes/>
|
||||
</element>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>580</x>
|
||||
<y>100</y>
|
||||
<w>210</w>
|
||||
<h>130</h>
|
||||
</coordinates>
|
||||
<panel_attributes>User
|
||||
PK: s_id int(11)
|
||||
FK: email varchar(255)
|
||||
keyword varchar(255)</panel_attributes><additional_attributes></additional_attributes></element><element><id>UMLClass</id><coordinates><x>425</x><y>240</y><w>210</w><h>130</h></coordinates><panel_attributes>users
|
||||
--
|
||||
PK: username
|
||||
password
|
||||
user_id
|
||||
telegramname
|
||||
Rolle
|
||||
</panel_attributes>
|
||||
<additional_attributes/>
|
||||
</element>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>930</x>
|
||||
<y>80</y>
|
||||
<w>210</w>
|
||||
<h>90</h>
|
||||
</coordinates>
|
||||
<panel_attributes>Aktie
|
||||
PK: email varchar(255)
|
||||
password varchar(255)
|
||||
username varchar(255)
|
||||
telegram_user_id varchar(255)
|
||||
admin tinyint(1)
|
||||
cron varchar(20)
|
||||
</panel_attributes><additional_attributes></additional_attributes></element><element><id>UMLClass</id><coordinates><x>705</x><y>150</y><w>210</w><h>90</h></coordinates><panel_attributes>shares
|
||||
--
|
||||
PK: a_id
|
||||
FK: username
|
||||
Aktiensymbol
|
||||
</panel_attributes>
|
||||
<additional_attributes/>
|
||||
</element>
|
||||
<element>
|
||||
<id>UMLClass</id>
|
||||
<coordinates>
|
||||
<x>200</x>
|
||||
<y>320</y>
|
||||
<w>210</w>
|
||||
<h>130</h>
|
||||
</coordinates>
|
||||
<panel_attributes>Transaktion
|
||||
PK: a_id int(11)
|
||||
FK: email varchar(255)
|
||||
isin varchar(255)
|
||||
comment varchar(255)</panel_attributes><additional_attributes></additional_attributes></element><element><id>UMLClass</id><coordinates><x>135</x><y>150</y><w>210</w><h>130</h></coordinates><panel_attributes>transaction
|
||||
--
|
||||
PK: t_id
|
||||
FK: username
|
||||
FK: Aktiensymbol
|
||||
Zeitpunkt
|
||||
Anzahl(+/-)
|
||||
Preis(+/-)
|
||||
</panel_attributes>
|
||||
<additional_attributes/>
|
||||
</element>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>780</x>
|
||||
<y>120</y>
|
||||
<w>170</w>
|
||||
<h>30</h>
|
||||
</coordinates>
|
||||
<panel_attributes>lt=<-</panel_attributes>
|
||||
<additional_attributes>150.0;10.0;10.0;10.0</additional_attributes>
|
||||
</element>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>400</x>
|
||||
<y>120</y>
|
||||
<w>200</w>
|
||||
<h>260</h>
|
||||
</coordinates>
|
||||
<panel_attributes>lt=<-</panel_attributes>
|
||||
<additional_attributes>10.0;240.0;180.0;10.0</additional_attributes>
|
||||
</element>
|
||||
<element>
|
||||
<id>Relation</id>
|
||||
<coordinates>
|
||||
<x>510</x>
|
||||
<y>200</y>
|
||||
<w>90</w>
|
||||
<h>190</h>
|
||||
</coordinates>
|
||||
<panel_attributes>lt=<-</panel_attributes>
|
||||
<additional_attributes>70.0;170.0;10.0;10.0</additional_attributes>
|
||||
</element>
|
||||
</diagram>
|
||||
PK: t_id int(11)
|
||||
FK: email varchar(255)
|
||||
isin varchar(255)
|
||||
comment varchar(255)
|
||||
time datetime
|
||||
count int(11)
|
||||
price float
|
||||
</panel_attributes><additional_attributes></additional_attributes></element><element><id>Relation</id><coordinates><x>335</x><y>190</y><w>110</w><h>100</h></coordinates><panel_attributes>lt=<<-
|
||||
group=1</panel_attributes><additional_attributes>10;10;60;10;60;80;90;80</additional_attributes></element><element><id>Relation</id><coordinates><x>625</x><y>190</y><w>100</w><h>100</h></coordinates><panel_attributes>lt=<<-</panel_attributes><additional_attributes>80;10;50;10;50;80;10;80</additional_attributes></element><element><id>Relation</id><coordinates><x>625</x><y>260</y><w>100</w><h>160</h></coordinates><panel_attributes>lt=<<-</panel_attributes><additional_attributes>80;140;50;140;50;10;10;10</additional_attributes></element><element><id>UMLClass</id><coordinates><x>140</x><y>410</y><w>100</w><h>30</h></coordinates><panel_attributes>Database V0.1</panel_attributes><additional_attributes></additional_attributes></element><element><id>UMLClass</id><coordinates><x>705</x><y>150</y><w>210</w><h>90</h></coordinates><panel_attributes>shares
|
||||
--
|
||||
PK: a_id int(11)
|
||||
FK: email varchar(255)
|
||||
isin varchar(255)
|
||||
comment varchar(255)</panel_attributes><additional_attributes></additional_attributes></element><element><id>UMLClass</id><coordinates><x>140</x><y>300</y><w>210</w><h>90</h></coordinates><panel_attributes>share_price
|
||||
--
|
||||
PK: id int(11)
|
||||
isin varchar(255)
|
||||
price float
|
||||
date datetime</panel_attributes><additional_attributes></additional_attributes></element></diagram>
|
@ -1,19 +1,29 @@
|
||||
FROM node:latest as build
|
||||
|
||||
# Change to the project directory
|
||||
WORKDIR /usr/local/app
|
||||
|
||||
# Copy the project files to the container
|
||||
COPY frontend /usr/local/app/
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
RUN ls /usr/local/app/dist
|
||||
|
||||
|
||||
FROM nginx:latest
|
||||
|
||||
# Copy the project files to the container
|
||||
COPY --from=build /usr/local/app/dist/aktienbot /usr/share/nginx/html
|
||||
|
||||
# Copy configuration files
|
||||
COPY frontend/deploy/nginx.conf /etc/nginx
|
||||
COPY frontend/deploy deploy/
|
||||
|
||||
# Change file permissions
|
||||
RUN chmod +x ./deploy/healthcheck.sh
|
||||
|
||||
# set healthcheck
|
||||
HEALTHCHECK --interval=15s --timeout=2s CMD ["./deploy/healthcheck.sh"]
|
||||
|
||||
# Expose webserver port
|
||||
EXPOSE 80
|
||||
|
@ -25,3 +25,4 @@ Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To u
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
||||
|
||||
|
@ -1,2 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
curl -s http://localhost:80/ -o /dev/null || exit 1
|
||||
|
34
frontend/deploy/nginx.conf
Normal file
34
frontend/deploy/nginx.conf
Normal file
@ -0,0 +1,34 @@
|
||||
events {
|
||||
worker_connections 1024; ## Default: 1024
|
||||
}
|
||||
|
||||
http {
|
||||
## use mime types
|
||||
include /etc/nginx/mime.types;
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
## without this our .css are not loaded
|
||||
try_files $uri $uri/ /index.html?$query_string;
|
||||
}
|
||||
}
|
||||
|
||||
## enable gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 256;
|
||||
gzip_proxied any;
|
||||
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/javascript
|
||||
application/javascript
|
||||
application/x-javascript
|
||||
application/xml
|
||||
application/json
|
||||
application/ld+json;
|
||||
}
|
@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
const AUTH_API = 'https://gruppe1.testsites.info/api/user';
|
||||
|
||||
const httpOptions = {
|
||||
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
|
||||
};
|
||||
|
12
telegram_bot/.env.example
Normal file
12
telegram_bot/.env.example
Normal file
@ -0,0 +1,12 @@
|
||||
# Telegram bot api key
|
||||
BOT_API_KEY=
|
||||
|
||||
# News api key
|
||||
NEWS_API_KEY=
|
||||
|
||||
# Flask secret key
|
||||
SECRET_KEY=
|
||||
|
||||
# bot credentials
|
||||
BOT_EMAIL=
|
||||
BOT_PASSWORD=
|
@ -1,17 +1,17 @@
|
||||
FROM python:3.10-slim
|
||||
|
||||
# Change the working directory to the root of the project
|
||||
WORKDIR /srv/flask_app
|
||||
|
||||
# Install the dependencies
|
||||
COPY telegram_bot/requirements.txt /srv/flask_app/
|
||||
|
||||
RUN pip install -r requirements.txt --src /usr/local/src --no-warn-script-location
|
||||
|
||||
# Copy the source code to the working directory
|
||||
COPY telegram_bot /srv/flask_app
|
||||
|
||||
# Change file permissions
|
||||
RUN chmod +x ./deploy/start.sh
|
||||
RUN chmod +x ./deploy/healthcheck.sh
|
||||
|
||||
# HEALTHCHECK --interval=15s --timeout=2s CMD ["./deploy/healthcheck.sh"]
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["./deploy/start.sh"]
|
||||
# Run the app
|
||||
CMD ["./deploy/start.sh"]
|
33
telegram_bot/README.md
Normal file
33
telegram_bot/README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Telegram bot
|
||||
|
||||
Aktienbot telegram bot
|
||||
|
||||
## Development
|
||||
1. Create virtual environment `python -m venv venv env/Scripts/activate`
|
||||
2. Install requirements `pip install -r telegram_bot/requirements.txt`
|
||||
3. Set environment variables (see list below)
|
||||
1. Use `.env`-file in `api` directory like `.env.example`
|
||||
2. Or set variables using `export` or `set` commands. (Windows `set`, Linux `export`)
|
||||
4. Run api `python telegram_bot/bot.py`
|
||||
|
||||
## Environment variables
|
||||
```
|
||||
# Telegram bot api key
|
||||
BOT_API_KEY=
|
||||
|
||||
# News api key
|
||||
NEWS_API_KEY=
|
||||
```
|
||||
|
||||
## Docker
|
||||
```
|
||||
docker run -d \
|
||||
--name aktienbot_bot \
|
||||
--hostname aktienbot_bot \
|
||||
--publish 80:80 \
|
||||
--env "BOT_API_KEY=" \
|
||||
--env "NEWS_API_KEY=" \
|
||||
--restart unless-stopped \
|
||||
registry.flokaiser.com/aktienbot/bot:latest
|
||||
```
|
||||
or load environment variables from file by using `--env-file <filename>`
|
@ -1,7 +0,0 @@
|
||||
"""
|
||||
script for communicating with webservice to get data from database
|
||||
"""
|
||||
__author__ = "Florian Kellermann, Linus Eickhoff"
|
||||
__date__ = "16.03.2022"
|
||||
__version__ = "0.0.1"
|
||||
__license__ = "None"
|
0
telegram_bot/api_handling/__init__.py
Normal file
0
telegram_bot/api_handling/__init__.py
Normal file
374
telegram_bot/api_handling/api_handler.py
Normal file
374
telegram_bot/api_handling/api_handler.py
Normal file
@ -0,0 +1,374 @@
|
||||
"""
|
||||
script for communicating with webservice to get data from database
|
||||
"""
|
||||
__author__ = "Florian Kellermann, Linus Eickhoff"
|
||||
__date__ = "26.04.2022"
|
||||
__version__ = "1.0.1"
|
||||
__license__ = "None"
|
||||
|
||||
#side-dependencies: none
|
||||
#Work in Progress
|
||||
|
||||
import sys
|
||||
import os
|
||||
import requests as r
|
||||
from croniter import croniter # used for checking cron formatting
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv() # loads environment vars
|
||||
|
||||
# note: for more information about the api visit swagger documentation on https://gruppe1.testsites.info/api/docs#/
|
||||
|
||||
class API_Handler:
|
||||
"""class for interacting with the api webservice
|
||||
|
||||
Attributes:
|
||||
db_adress (string): adress of the database
|
||||
token (string): auth token for api
|
||||
|
||||
Methods:
|
||||
reauthorize(email, password): set new credentials
|
||||
get_user_keywords(user_id): gets the keywords of the user
|
||||
set_keyword(user_id, keyword): sets the keyword of the user
|
||||
delete_keyword(user_id, keyword): deletes the keyword of the user
|
||||
get_user_shares(user_id): gets the shares of the user
|
||||
set_share(user_id, symbol): sets the share of the user
|
||||
delete_share(user_id, symbol): deletes the share of the user
|
||||
get_user_transactions(user_id): gets the transactions of the user
|
||||
set_transaction(user_id, transaction): sets the transaction of the user
|
||||
get_user_portfolio(user_id): gets the portfolio of the user
|
||||
set_portfolio(user_id, portfolio): sets the portfolio of the user
|
||||
delete_portfolio(user_id, portfolio): deletes the portfolio of the user
|
||||
set_cron_interval(user_id, interval): sets the cron interval of the user
|
||||
set_admin(email, is_admin): sets the admin status of the user with the given email
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, db_adress, email, password):
|
||||
"""initializes the API_Handler class
|
||||
|
||||
Args:
|
||||
db_adress (string): adress of the database
|
||||
email (string): email of the user
|
||||
password (string): password of the user
|
||||
"""
|
||||
self.db_adress = db_adress
|
||||
|
||||
payload = {'email': email, 'password': password} # credentials for admin account that has all permissions to get and set data (in this case bot account)
|
||||
with r.Session() as s: # open session
|
||||
p = s.post(self.db_adress + "/user/login", json=payload) # login to webservice
|
||||
if p.status_code == 200:
|
||||
self.token = p.json()["data"]['token'] # store token for further authentication of requests
|
||||
else:
|
||||
print("Error: " + str(p.status_code) + " invalid credentials")
|
||||
self.token = None
|
||||
|
||||
|
||||
def reauthorize(self, email, password): # can be used if token expired
|
||||
"""set new credentials
|
||||
|
||||
Args:
|
||||
email (string): email of the user
|
||||
password (string): password of the user
|
||||
"""
|
||||
payload = {'email': email, 'password': password}
|
||||
with r.Session() as s:
|
||||
p = s.post(self.db_adress + "/user/login", json=payload)
|
||||
if p.status_code == 200:
|
||||
self.token = p.json()["data"]['token']
|
||||
return p.json()["data"]['token']
|
||||
else:
|
||||
self.token = None
|
||||
return None
|
||||
|
||||
|
||||
def get_user(self, user_id, max_retries=10): # max retries are used recursively if the request fails
|
||||
"""gets the shares of the user
|
||||
|
||||
Args:
|
||||
user_id (int): id of the user
|
||||
max_retries (int): max retries for the request
|
||||
|
||||
Returns:
|
||||
json: json of user infos
|
||||
"""
|
||||
if max_retries <= 0:
|
||||
return None
|
||||
|
||||
with r.Session() as s:
|
||||
headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)} # authorization is bot_token:user_id (user_id is the id of the user you want to get data from)
|
||||
req = s.get(self.db_adress + "/user", headers=headers)
|
||||
if(req.status_code == 200):
|
||||
return req.json()["data"]
|
||||
|
||||
else:
|
||||
return self.get_user(user_id, max_retries-1) # if request fails try again recursively
|
||||
|
||||
|
||||
def get_all_users(self, max_retries=10):
|
||||
"""gets all users
|
||||
|
||||
Args:
|
||||
max_retries (int): max retries for the request
|
||||
|
||||
Returns:
|
||||
list: list of users
|
||||
"""
|
||||
if max_retries <= 0:
|
||||
return None
|
||||
|
||||
with r.Session() as s:
|
||||
headers = {'Authorization': 'Bearer ' + self.token}
|
||||
req = s.get(self.db_adress + "/users", headers=headers)
|
||||
if(req.status_code == 200):
|
||||
return req.json()["data"]
|
||||
|
||||
else:
|
||||
return self.get_all_users(max_retries-1)
|
||||
|
||||
|
||||
def get_user_keywords(self, user_id, max_retries=10):
|
||||
"""gets the keywords of the user
|
||||
|
||||
Args:
|
||||
user_id (int): id of the user
|
||||
max_retries (int): max retries for the request
|
||||
|
||||
Returns:
|
||||
list: list of keywords
|
||||
"""
|
||||
if max_retries <= 0:
|
||||
return None
|
||||
|
||||
keywords = []
|
||||
with r.Session() as s:
|
||||
headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
|
||||
req = s.get(self.db_adress + "/keywords", headers=headers)
|
||||
if(req.status_code == 200):
|
||||
keywords_json = req.json()["data"]
|
||||
for keyword in keywords_json: # keywords_json is a list of dictionaries
|
||||
keywords.append(keyword["keyword"])
|
||||
|
||||
return keywords # will be empty if no keywords are set
|
||||
|
||||
else:
|
||||
return self.get_user_keywords(user_id, max_retries-1)
|
||||
|
||||
|
||||
|
||||
def set_keyword(self, user_id, keyword):
|
||||
"""sets the keyword of the user
|
||||
|
||||
Args:
|
||||
user_id (int): id of the user
|
||||
keyword (int): keyword of the user
|
||||
|
||||
Returns:
|
||||
int: status code
|
||||
"""
|
||||
with r.Session() as s:
|
||||
headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
|
||||
req = s.post(self.db_adress + "/keyword", json={"keyword": keyword}, headers=headers)
|
||||
|
||||
return req.status_code
|
||||
|
||||
|
||||
def delete_keyword(self, user_id, keyword):
|
||||
"""deletes the keyword of the user
|
||||
|
||||
Args:
|
||||
user_id (int): id of the user
|
||||
keyword (string): keyword of the user
|
||||
|
||||
Returns:
|
||||
int: status code
|
||||
"""
|
||||
with r.Session() as s:
|
||||
headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
|
||||
req = s.delete(self.db_adress + "/keyword", json={"keyword": keyword}, headers=headers)
|
||||
|
||||
return req.status_code
|
||||
|
||||
|
||||
def get_user_shares(self, user_id, max_retries=10):
|
||||
"""gets the shares of the user
|
||||
|
||||
Args:
|
||||
user_id (int): id of the user
|
||||
max_retries (int): max retries for the request
|
||||
|
||||
Returns:
|
||||
list: list of shares
|
||||
"""
|
||||
if max_retries <= 0:
|
||||
return None
|
||||
|
||||
with r.Session() as s:
|
||||
headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
|
||||
req = s.get(self.db_adress + "/shares", headers=headers)
|
||||
if(req.status_code == 200):
|
||||
shares_json = req.json()["data"]
|
||||
shares = []
|
||||
for share in shares_json:
|
||||
shares.append(share["isin"]) # we only want the isin of the shares
|
||||
|
||||
return shares
|
||||
|
||||
else:
|
||||
return self.get_user_shares(user_id, max_retries-1)
|
||||
|
||||
|
||||
def set_share(self, user_id, isin, comment):
|
||||
"""sets the share of the user
|
||||
|
||||
Args:
|
||||
user_id (int): id of the user
|
||||
isin (string): isin of the share
|
||||
comment (string): comment of the share
|
||||
|
||||
Returns:
|
||||
int: status code
|
||||
"""
|
||||
with r.Session() as s:
|
||||
headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
|
||||
req = s.post(self.db_adress + "/share", json={"comment": comment, "isin": isin}, headers=headers) # set share by setting comment and isin, comment can be the real name of the share e.g. "Apple Inc."
|
||||
return req.status_code
|
||||
|
||||
|
||||
def delete_share(self, user_id, isin):
|
||||
"""deletes the share of the user
|
||||
|
||||
Args:
|
||||
user_id (int): id of the user
|
||||
symbol (string): symbol of the share
|
||||
|
||||
Returns:
|
||||
int: status code
|
||||
"""
|
||||
with r.Session() as s:
|
||||
headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
|
||||
req = s.delete(self.db_adress + "/share", json={"isin": isin}, headers=headers) # to delete a share only the isin is needed because it is unique, shares are not transactions!
|
||||
return req.status_code
|
||||
|
||||
|
||||
def get_user_transactions(self, user_id, max_retries=10):
|
||||
"""gets the transactions of the user
|
||||
|
||||
Args:
|
||||
user_id (int): id of the user
|
||||
max_retries (int): max retries for the request
|
||||
|
||||
Returns:
|
||||
dict: dictionary of transactions
|
||||
"""
|
||||
if max_retries <= 0:
|
||||
return None
|
||||
|
||||
with r.Session() as s:
|
||||
headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
|
||||
req = s.get(self.db_adress + "/transactions", headers=headers)
|
||||
|
||||
if req.status_code == 200:
|
||||
transactions_dict = req.json()["data"]
|
||||
return transactions_dict
|
||||
else:
|
||||
return self.get_user_transactions(user_id, max_retries-1)
|
||||
|
||||
|
||||
def set_transaction(self, user_id, comment, isin, count, price, time):
|
||||
"""sets the transaction of the user
|
||||
|
||||
Args:
|
||||
user_id (int): id of the user
|
||||
comment (string): comment of the transaction
|
||||
isin (string): isin of the transaction
|
||||
count (float): count of the transaction
|
||||
price (float): price of the transaction
|
||||
time (string): time of the transaction formatted like e.g. "2011-10-05T14:48:00.000Z"
|
||||
|
||||
Returns:
|
||||
int: status code
|
||||
"""
|
||||
with r.Session() as s:
|
||||
time = time[:-3] + "Z" # remove last character and add Z to make it a valid date for db
|
||||
headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
|
||||
transaction = {"comment": str(comment), "count": float(count), "isin": str(isin), "price": float(price), "time": str(time)} # set transaction as JSON with all the attributes needed according to Swagger docs
|
||||
req = s.post(self.db_adress + "/transaction", json=transaction, headers=headers)
|
||||
return req.status_code
|
||||
|
||||
|
||||
def get_user_portfolio(self, user_id, max_retries=10):
|
||||
"""gets the portfolio of the user
|
||||
|
||||
Args:
|
||||
user_id (int): id of the user
|
||||
max_retries (int): max retries for the request
|
||||
|
||||
Returns:
|
||||
dict: dictionary of portfolio
|
||||
"""
|
||||
if max_retries <= 0:
|
||||
return None
|
||||
|
||||
with r.Session() as s:
|
||||
headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
|
||||
req = s.get(self.db_adress + "/portfolio", headers=headers) # get portfolio as JSON
|
||||
if req.status_code == 200:
|
||||
portfolio_dict = req.json()["data"] # get the data of the JSON
|
||||
return portfolio_dict
|
||||
else:
|
||||
return self.get_user_portfolio(user_id, max_retries-1)
|
||||
|
||||
def set_cron_interval(self, user_id, cron_interval):
|
||||
"""sets the cron interval of the user
|
||||
|
||||
Args:
|
||||
user_id (int): id of the user
|
||||
cron_interval (String): Update interval in cron format => see https://crontab.guru/ for formatting
|
||||
|
||||
Returns:
|
||||
int: status code
|
||||
"""
|
||||
if not croniter.is_valid(cron_interval): # check if cron_interval is in valid format
|
||||
print("Error: Invalid cron format")
|
||||
return -1 # return error code -1 if invalid cron format
|
||||
|
||||
with r.Session() as s:
|
||||
headers = {'Authorization': 'Bearer ' + self.token + ":" + str(user_id)}
|
||||
req = s.put(self.db_adress + "/user/setCron", json={"cron": str(cron_interval)}, headers=headers) # put not post (see swagger docs)
|
||||
return req.status_code
|
||||
|
||||
|
||||
def set_admin(self, email, is_admin):
|
||||
"""sets the admin of the user
|
||||
|
||||
Args:
|
||||
email (string): email of the user
|
||||
is_admin (bool): "true" if user should be Admin, "false" if not
|
||||
|
||||
Returns:
|
||||
int: status code
|
||||
"""
|
||||
with r.Session() as s:
|
||||
headers = {'Authorization': 'Bearer ' + self.token} # only bot token is needed, user is chosen by email
|
||||
req = s.put(self.db_adress + "/user/setAdmin", json={"admin": is_admin,"email": str(email)}, headers=headers)
|
||||
return req.status_code
|
||||
|
||||
|
||||
if __name__ == "__main__": # editable, just for basic on the go testing of new functions
|
||||
|
||||
print("This is a module for the telegram bot. It is not intended to be run directly.")
|
||||
handler = API_Handler("https://gruppe1.testsites.info/api", str(os.getenv("BOT_EMAIL")), str(os.getenv("BOT_PASSWORD"))) # get creds from env
|
||||
print(handler.token)
|
||||
keywords = handler.get_user_keywords(user_id = 1709356058) #user_id here is currently mine (Linus)
|
||||
print(keywords)
|
||||
shares = handler.get_user_portfolio(user_id = 1709356058)
|
||||
print("set cron with status: "+ str(handler.set_cron_interval(user_id = 1709356058, cron_interval = "0 0 * * *")))
|
||||
user = handler.get_user(user_id = 1709356058)
|
||||
print(user)
|
||||
all_users = handler.get_all_users()
|
||||
admin_status = handler.set_admin("test@test.com", "true")
|
||||
print(admin_status)
|
||||
print(all_users)
|
||||
print(shares)
|
||||
sys.exit(1)
|
@ -3,8 +3,8 @@
|
||||
script for telegram bot and its functions
|
||||
"""
|
||||
__author__ = "Florian Kellermann, Linus Eickhoff"
|
||||
__date__ = "11.03.2022"
|
||||
__version__ = "0.0.4"
|
||||
__date__ = "26.04.2022"
|
||||
__version__ = "1.2.2"
|
||||
__license__ = "None"
|
||||
|
||||
# side-dependencies: none
|
||||
@ -17,51 +17,31 @@ __license__ = "None"
|
||||
import os
|
||||
|
||||
import telebot
|
||||
import time
|
||||
import sys
|
||||
import logging
|
||||
import json
|
||||
import re
|
||||
|
||||
import news.news_fetcher as news
|
||||
import shares.share_fetcher as share_fetcher
|
||||
import datetime as dt
|
||||
|
||||
from telebot import types
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from api_handling.api_handler import API_Handler
|
||||
|
||||
load_dotenv()
|
||||
|
||||
bot_version = "0.2.1"
|
||||
user_list = []
|
||||
load_dotenv(dotenv_path='.env') # load environment variables
|
||||
|
||||
class User: # Currently saving users in this class to test functionality -> later database
|
||||
def __init__(self, p_user_id, p_user_name, p_chat_id):
|
||||
|
||||
""" Initialize a new user
|
||||
:type self:
|
||||
:param self: for class
|
||||
|
||||
:type p_user_id: int
|
||||
:param p_user_id: telegram user id
|
||||
|
||||
:type p_user_name: str
|
||||
:param p_user_name: first name of user
|
||||
|
||||
:type p_chat_id: int
|
||||
:param p_chat_id: telegram chat id
|
||||
|
||||
:raises:
|
||||
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
self.user_id = int(p_user_id)
|
||||
self.chat_id = int(p_chat_id)
|
||||
self.user_name = str(p_user_name)
|
||||
bot_version = "1.0.1" # version of bot
|
||||
|
||||
#create api handler
|
||||
api_handler = API_Handler("https://gruppe1.testsites.info/api", str(os.getenv("BOT_EMAIL")), str(os.getenv("BOT_PASSWORD"))) # get creds from env vars.
|
||||
print("Webserver Token: " + str(api_handler.token))
|
||||
|
||||
bot = telebot.TeleBot(os.getenv('BOT_API_KEY'))
|
||||
|
||||
@bot.message_handler(commands=['start']) # /start -> saving as new user and sending welcome
|
||||
@bot.message_handler(commands=['start', 'Start']) # /start -> saving as new user and sending welcome
|
||||
def send_start(message):
|
||||
|
||||
""" Description
|
||||
@ -72,18 +52,10 @@ def send_start(message):
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
new_user = User(int(message.from_user.id), message.from_user.first_name, int(message.chat.id))
|
||||
existing_already = False
|
||||
for known_user in user_list:
|
||||
if known_user.user_id == new_user.user_id:
|
||||
existing_already = True
|
||||
if existing_already == False:
|
||||
user_list.append(new_user)
|
||||
|
||||
bot.reply_to(message, "Welcome to this share bot project. Type /help to get information on what this bot can do")
|
||||
|
||||
|
||||
@bot.message_handler(commands=['version'])
|
||||
@bot.message_handler(commands=['version', 'Version'])
|
||||
def send_version(message):
|
||||
|
||||
""" Sending programm version
|
||||
@ -94,11 +66,11 @@ def send_version(message):
|
||||
|
||||
:rtype:none
|
||||
"""
|
||||
bot.reply_to(message, bot_version)
|
||||
bot.reply_to(message, "the current bot version is " + bot_version)
|
||||
|
||||
|
||||
@bot.message_handler(commands=['help']) # /help -> sending all functions
|
||||
def send_welcome(message):
|
||||
@bot.message_handler(commands=['help', 'Help']) # /help -> sending all functions
|
||||
def send_help(message):
|
||||
|
||||
""" Send all functions
|
||||
:type message: message object bot
|
||||
@ -108,10 +80,10 @@ def send_welcome(message):
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
bot.reply_to(message, "/id or /auth for authentication. /update to get updates on your shares. /users to see all users. /news to get current use for your keywords. /share to get price of specific share. For further details see aktienbot.flokaiser.com")
|
||||
bot.reply_to(message, "/id or /auth get your user id\n/update get updates on your shares.\n/setAdmin set admin rights of user (ADMIN)\n/users see all users. (ADMIN)\n/me get my user info\n/news get top article for each keyword.\n/allnews get all news (last 7 days)\n/keywords get all your keywords\n/addkeyword add a keyword\n/removekeyword remove a keyword\n/share get price of specific share\n/portfolio see own portfolio\n/newtransaction add new transaction\n/interval get update interval\n/setinterval set update interval\n_For further details see https://gruppe1.testsites.info _", parse_mode='MARKDOWN')
|
||||
|
||||
|
||||
@bot.message_handler(commands=['users'])
|
||||
@bot.message_handler(commands=['users', 'Users']) # /users -> sending all users
|
||||
def send_all_users(message):
|
||||
|
||||
""" Send all users, only possible for admins
|
||||
@ -122,19 +94,97 @@ def send_all_users(message):
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
print('Debug: users command')
|
||||
user_id = int(message.from_user.id)
|
||||
|
||||
user_id = int(message.from_user.id)
|
||||
user_data = api_handler.get_user(user_id)
|
||||
if(user_data["admin"] == False): # check if user has admin rights
|
||||
bot.reply_to(message, "You have to be an admin to use this command")
|
||||
return
|
||||
|
||||
# tbd check if user is admin
|
||||
user_list = api_handler.get_all_users()
|
||||
user_count = len(user_list)
|
||||
bot.send_message(chat_id=user_id, text="There are " + str(user_count) + " users in the database:")
|
||||
|
||||
answer = 'Current number of users: ' + str(len(user_list))
|
||||
bot.send_message(chat_id = user_id, text=answer)
|
||||
for known_user in user_list:
|
||||
answer = str(known_user.user_id) + ' : ' + known_user.user_name
|
||||
bot.send_message(chat_id=user_id, text=answer)
|
||||
for user in user_list:
|
||||
|
||||
username = user['username']
|
||||
email = user['email']
|
||||
id = user['telegram_user_id']
|
||||
cron = user['cron']
|
||||
admin = user['admin']
|
||||
|
||||
bot.send_message(chat_id=user_id, text=f'Username: {username}\nEmail: {email}\nID: {id}\nCron: {cron}\nAdmin: {admin}') # format user data into readable message text
|
||||
|
||||
|
||||
@bot.message_handler(commands=['setAdmin', 'SetAdmin', 'setadmin', 'Setadmin']) # set admin rights to user TBD: not working!!
|
||||
def set_admin(message):
|
||||
|
||||
@bot.message_handler(commands=['id', 'auth']) # /auth or /id -> Authentication with user_id over web tool
|
||||
""" Set admin rights to user
|
||||
:type message: message object bot
|
||||
:param message: message that was reacted to, in this case always containing '/setAdmin'
|
||||
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
user_id = int(message.from_user.id)
|
||||
user_data = api_handler.get_user(user_id)
|
||||
|
||||
if(user_data["admin"] == False): # check if user has admin rights
|
||||
bot.reply_to(message, "You have to be an admin to use this command")
|
||||
return
|
||||
|
||||
bot.send_message(chat_id=user_id, text='send email and true if this account should have admin rights, else false\n in format: <email>,<is_admin>') # request email and admin rights to change to
|
||||
bot.register_next_step_handler(message, set_admin_step)
|
||||
|
||||
def set_admin_step(message):
|
||||
str_message = str(message.text)
|
||||
args_message = str_message.split(',') # split message into email and admin rights
|
||||
|
||||
if len(args_message) != 2: # make sure 2 args (email,is_admin) are given
|
||||
|
||||
bot.reply_to(message, "exactly 2 arguments (<email>,<is_admin>) required, try again")
|
||||
return
|
||||
|
||||
email = args_message[0]
|
||||
is_admin = False # default: False
|
||||
|
||||
if args_message[1].lower() == "true": # if user types true, set is_admin to true
|
||||
is_admin = True
|
||||
|
||||
status = api_handler.set_admin(email, is_admin) # set admin in db
|
||||
|
||||
if(status == 200):
|
||||
bot.reply_to(message, "Admin rights set")
|
||||
|
||||
else:
|
||||
bot.reply_to(message, f"Admin rights could not be set ({status})")
|
||||
|
||||
|
||||
@bot.message_handler(commands=['me', 'Me']) # /me -> sending user info
|
||||
def send_user(message):
|
||||
""" Send user data
|
||||
:type message: message object bot
|
||||
:param message: message that was reacted to, in this case always containing '/me'
|
||||
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
user_id = int(message.from_user.id)
|
||||
user_data = api_handler.get_user(user_id)
|
||||
if not user_data or user_data == None: # true if user is not registered
|
||||
bot.reply_to(message, "This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info")
|
||||
return
|
||||
username = user_data['username']
|
||||
email = user_data['email']
|
||||
user_id = user_data['telegram_user_id']
|
||||
cron = user_data['cron']
|
||||
admin = user_data['admin']
|
||||
bot.reply_to(message, f'Username: {username}\nEmail: {email}\nID: {user_id}\nCron: {cron}\nAdmin: {admin}') # format user data into readable message text
|
||||
|
||||
|
||||
@bot.message_handler(commands=['id', 'auth', 'Id', 'Auth']) # /auth or /id -> Authentication with user_id over web tool
|
||||
def send_id(message):
|
||||
|
||||
""" Send user id for authentication with browser
|
||||
@ -145,49 +195,72 @@ def send_id(message):
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
answer = 'Your ID/Authentication Code is: [' + str(message.from_user.id) + ']. Enter this code in the settings on aktienbot.flokaiser.com to get updates on your shares.'
|
||||
answer = 'Your ID/Authentication Code is: [' + str(message.from_user.id) + ']. Enter this code in the settings on https://gruppe1.testsites.info to get updates on your shares.'
|
||||
bot.reply_to(message, answer)
|
||||
|
||||
|
||||
@bot.message_handler(commands=['update'])
|
||||
def send_update(message):
|
||||
|
||||
""" Send update on shares
|
||||
#function that can be used to ensure that the bot is online and running
|
||||
@bot.message_handler(commands=['status', 'Status'])
|
||||
def send_status(message):
|
||||
|
||||
""" Sends status to user
|
||||
:type message: message object bot
|
||||
:param message: message that was reacted to, in this case always containing '/help'
|
||||
:param message: message that was reacted to, if no other command handler gets called
|
||||
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
user_id = int(message.from_user.id)
|
||||
bot.reply_to(message, "bot is running")
|
||||
|
||||
|
||||
@bot.message_handler(commands=['update', 'Update']) # /update -> update shares
|
||||
def update_for_user(message):
|
||||
|
||||
#Can be deleted when getting from database
|
||||
dirname = os.path.dirname(__file__)
|
||||
json_path = os.path.join(dirname, 'shares/shares_example.json')
|
||||
p_user_id = int(message.from_user.id)
|
||||
p_my_handler = api_handler
|
||||
|
||||
with open(json_path) as json_file:
|
||||
json_share_data = json.load(json_file)
|
||||
int_share_count = int(json_share_data['share_count'])
|
||||
str_username = str(json_share_data['user'])
|
||||
bot.send_message(chat_id=user_id, text=f'Hello {str_username}. Here is the update on your currently owned shares:')
|
||||
|
||||
|
||||
for i in range(int_share_count):
|
||||
|
||||
my_share = json_share_data['shares'][i]
|
||||
my_share_symbol = str(my_share['symbol'])
|
||||
my_share_amount = float(my_share['amount_bought'])
|
||||
my_share_buy_price = float(my_share['price_bought'])
|
||||
my_share_course = float(share_fetcher.get_share_price(my_share_symbol))
|
||||
|
||||
|
||||
my_update_message = f'Symbol: {my_share_symbol}\nPrice: {my_share_course}\nBought for: {my_share_buy_price}\n\
|
||||
Amount owned: {my_share_amount}\nWin/Lose: {(my_share_amount*my_share_course) - (my_share_amount*my_share_buy_price)}'
|
||||
bot.send_message(chat_id=user_id, text=my_update_message)
|
||||
share_symbols = []
|
||||
share_amounts = []
|
||||
share_courses = []
|
||||
|
||||
my_portfolio = p_my_handler.get_user_portfolio(p_user_id)
|
||||
|
||||
for element in my_portfolio:
|
||||
if element["count"] != '' and element["isin"]!= '':
|
||||
print(element["count"], element["isin"])
|
||||
share_symbols.append(element["isin"])
|
||||
share_amounts.append(element["count"])
|
||||
share_courses.append(element["current_price"])
|
||||
|
||||
my_user = p_my_handler.get_user(p_user_id)
|
||||
send_to_user("Hello %s this is your share update:"%str(my_user["username"]), pUser_id=p_user_id)
|
||||
|
||||
if len(share_symbols) != 0:
|
||||
for i in range(len(share_symbols)):
|
||||
my_update_message = f'Symbol: {share_symbols[i]}\nCurrent Price per Share: {share_courses[i]}\nAmount owned: {share_amounts[i]}\nTotal Investment: {float(share_courses[i]) * float(share_amounts[i])}'
|
||||
send_to_user(my_update_message, pUser_id=p_user_id)
|
||||
else:
|
||||
send_to_user("No shares found for your account. Check https://gruppe1.testsites.info to change your settings and add shares.", pUser_id=p_user_id)
|
||||
|
||||
|
||||
def send_to_user(pText, pUser_id):
|
||||
|
||||
""" Send message to user
|
||||
:type pText: string
|
||||
:param pText: Text to send to user
|
||||
|
||||
:type pUser_id: int
|
||||
:param pUser_id: user to send to. per default me (Florian Kellermann)
|
||||
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
bot.send_message(chat_id=pUser_id, text=pText)
|
||||
|
||||
|
||||
@bot.message_handler(commands=['share'])
|
||||
@bot.message_handler(commands=['share', 'Share']) # /share -> get share price
|
||||
def send_share_update(message):
|
||||
|
||||
""" Send price of a specific share
|
||||
@ -207,13 +280,50 @@ def send_share_update(message):
|
||||
|
||||
def send_share_price(message):
|
||||
str_share_price = share_fetcher.get_share_price(str(message.text))
|
||||
bot.reply_to(message, str_share_price)
|
||||
bot.reply_to(message, str_share_price) # add dollar symbol etc.
|
||||
|
||||
|
||||
@bot.message_handler(commands=['news'])
|
||||
def send_news(message):
|
||||
@bot.message_handler(commands=['allnews', 'Allnews']) # /allnews -> get all news
|
||||
def send_all_news(message):
|
||||
|
||||
""" Get news for keywords of user
|
||||
:type message: message object bot
|
||||
:param message: message that was reacted to, in this case always containing '/allnews'
|
||||
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
|
||||
user_id = int(message.from_user.id)
|
||||
keywords = api_handler.get_user_keywords(user_id) # get keywords of user
|
||||
|
||||
if keywords == None: # true if user is not registered
|
||||
bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info')
|
||||
return
|
||||
|
||||
if not keywords: # true if user is registered but does not have any keywords
|
||||
bot.send_message(chat_id=user_id, text='You have no keywords. Please add some keywords with /news')
|
||||
return
|
||||
|
||||
keywords_search = ' OR '.join(keywords) # concat all keywords with OR -> NewsAPI can understand OR, AND, NOT etc.
|
||||
now = dt.datetime.now().date() # get current date
|
||||
from_date = now - dt.timedelta(days=7) # get date 7 days ago -> limit age of news to 7 days old max
|
||||
from_date_formatted = dt.datetime.strftime(from_date, '%Y-%m-%d')
|
||||
news_list = news.get_all_news_by_keyword(keywords_search, from_date_formatted)["articles"] # array of JSON article objects
|
||||
|
||||
if news_list: # true if news_list is not empty
|
||||
for article in news_list:
|
||||
formatted_article = news.format_article(article)
|
||||
bot.send_message(chat_id=user_id, text=formatted_article, parse_mode="MARKDOWN") # Markdown allows to write bold text with * etc.
|
||||
else:
|
||||
bot.send_message(chat_id=user_id, text='No news found for your keywords.')
|
||||
|
||||
|
||||
@bot.message_handler(commands=['news', 'News']) # /news -> get news for specific keyword
|
||||
def send_news(message):
|
||||
""" Get news for keywords of user
|
||||
|
||||
:type message: message object bot
|
||||
:param message: message that was reacted to, in this case always containing '/news'
|
||||
|
||||
@ -221,21 +331,222 @@ def send_news(message):
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
|
||||
keyword = "bitcoin"
|
||||
user_id = int(message.from_user.id)
|
||||
#Get Information for user with this id
|
||||
keywords = api_handler.get_user_keywords(user_id) # get keywords of user
|
||||
|
||||
articles = news.get_top_news_by_keyword(keyword) #tbd: get keyword from db
|
||||
try:
|
||||
formatted_article = news.format_article(articles["articles"][0])
|
||||
except IndexError:
|
||||
bot.send_message(chat_id=user_id, text=f"no news currently available for keyword: {keyword}")
|
||||
if keywords == None: # true if user is not registered
|
||||
bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info')
|
||||
return
|
||||
bot.send_message(chat_id=user_id, text=f"_keyword: {keyword}_\n\n" + formatted_article, parse_mode="MARKDOWN")
|
||||
|
||||
if not keywords: # true if user is registered but does not have any keywords
|
||||
bot.send_message(chat_id=user_id, text='You have no keywords. Please add some keywords with /addkeyword')
|
||||
return
|
||||
|
||||
if keywords:
|
||||
for keyword in keywords:
|
||||
top_news = news.get_top_news_by_keyword(keyword)["articles"]
|
||||
if top_news == None: # true if request to NewsAPI failed
|
||||
bot.send_message(chat_id=user_id, text='News Server did not respond correctly. Try again later.')
|
||||
|
||||
if not top_news: # true if no news found for keyword (empty list)
|
||||
bot.send_message(chat_id=user_id, text=f'No news found for keyword: *{keyword}*', parse_mode="MARKDOWN")
|
||||
|
||||
else:
|
||||
formatted_article = news.format_article(top_news[0]) # only format and send most popular news
|
||||
bot.send_message(chat_id=user_id, text=f"_keyword: {keyword}_\n\n" + formatted_article, parse_mode="MARKDOWN")
|
||||
|
||||
|
||||
@bot.message_handler(func=lambda message: True) # Returning that command is unkown for any other statement
|
||||
@bot.message_handler(commands=['addkeyword', 'Addkeyword']) # /addkeyword -> add keyword to user
|
||||
def add_keyword(message):
|
||||
""" Add keyword to user
|
||||
:type message: message object bot
|
||||
:param message: message that was reacted to, in this case always '/addkeyword'
|
||||
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
user_id = int(message.from_user.id)
|
||||
bot.send_message(chat_id=user_id, text='Type keyword to add:')
|
||||
bot.register_next_step_handler(message, store_keyword) # wait for user to send keyword, then call store_keyword function
|
||||
|
||||
def store_keyword(message):
|
||||
user_id = int(message.from_user.id)
|
||||
keyword = str(message.text).lower() # lower to ensure Bitcoin and bitcoin is not stored as individual keywords
|
||||
status = api_handler.set_keyword(user_id, keyword) # set keyword in database
|
||||
if status == 200: # statuscode 200 means keyword was added successfully without errors
|
||||
bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" added.') # duplicate keywords are denied by Database, so no need to check for that here
|
||||
else:
|
||||
bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" could not be stored. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info (statuscode {status})')
|
||||
|
||||
|
||||
@bot.message_handler(commands=['removekeyword', 'Removekeyword']) # /removekeyword -> remove keyword from user
|
||||
def remove_keyword(message):
|
||||
""" Remove keyword from user
|
||||
:type message: message object bot
|
||||
:param message: message that was reacted to, in this case always '/removekeyword'
|
||||
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
user_id = int(message.from_user.id)
|
||||
bot.send_message(chat_id=user_id, text='Type keyword to remove:')
|
||||
bot.register_next_step_handler(message, remove_keyword_step) # wait for user to send keyword to remove, then call remove_keyword_step function
|
||||
|
||||
def remove_keyword_step(message):
|
||||
user_id = int(message.from_user.id)
|
||||
keyword = str(message.text).lower()
|
||||
status = api_handler.delete_keyword(user_id, keyword)
|
||||
if status == 200: # statuscode 200 means keyword was removed successfully without errors
|
||||
bot.send_message(chat_id=user_id, text=f'Keyword "{keyword}" removed.') # checking if keyword to remove is in database are handled in database, not here
|
||||
else:
|
||||
bot.send_message(chat_id=user_id, text=f'Failed deleting keyword "{keyword}". (statuscode {status})')
|
||||
|
||||
|
||||
@bot.message_handler(commands=['keywords', 'Keywords']) # /keywords -> get keywords of user
|
||||
def send_keywords(message):
|
||||
""" Send keywords of user
|
||||
:type message: message object bot
|
||||
:param message: message that was reacted to, in this case always '/keywords'
|
||||
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
user_id = int(message.from_user.id)
|
||||
keywords = api_handler.get_user_keywords(user_id) # get keywords of user
|
||||
if keywords == None: # true if user is not registered
|
||||
bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info')
|
||||
return
|
||||
if not keywords: # true if user is registered but does not have any keywords
|
||||
bot.send_message(chat_id=user_id, text='No keywords set for this account. Add keywords by using /addkeyword')
|
||||
return
|
||||
else: # send keyword list
|
||||
keywords_str = ', '.join(keywords)
|
||||
bot.send_message(chat_id=user_id, text=f'Your keywords are: _{keywords_str}_', parse_mode="MARKDOWN")
|
||||
|
||||
|
||||
@bot.message_handler(commands=['portfolio', 'Portfolio'])
|
||||
def send_portfolio(message):
|
||||
""" Send portfolio of user
|
||||
:type message: message object bot
|
||||
:param message: message that was reacted to, in this case always '/portfolio'
|
||||
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
user_id = int(message.from_user.id)
|
||||
portfolio = api_handler.get_user_portfolio(user_id) # get portfolio of user as json
|
||||
if portfolio == None: # true if user is not registered
|
||||
bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info')
|
||||
return
|
||||
if not portfolio: # true if user is registered but does not have any stocks in portfolio
|
||||
bot.send_message(chat_id=user_id, text='You do not have any stocks in your portfolio.')
|
||||
return
|
||||
else: # send portfolio
|
||||
for stock in portfolio:
|
||||
comment = str(stock["comment"]) # comment may be written name of stock, comment is made by user when adding an stock to portfolio
|
||||
count = "{:.2f}".format(float(stock["count"])) # round count to 2 decimal places
|
||||
isin = str(stock["isin"])
|
||||
worth = "{:.2f}".format(float(stock["current_price"]) * float(stock["count"])) # round current_price to 2 decimal places
|
||||
bot.send_message(chat_id=user_id, text=f'*{comment}*\n_{isin}_\namount: {count}\nworth: ${worth}', parse_mode="MARKDOWN") # formatted message in markdown
|
||||
|
||||
|
||||
@bot.message_handler(commands=['newtransaction', 'Newtransaction']) #tbd not working rn may be deleted in future
|
||||
def set_new_transaction(message):
|
||||
""" Set new transaction for user
|
||||
:type message: message object bot
|
||||
:param message: message that was reacted to, in this case always '/newtransaction'
|
||||
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
user_id = int(message.from_user.id)
|
||||
bot.send_message(chat_id=user_id, text='Type "<name of stock>,<isin>,<amount>,<price_per_stock_usd>" (time of transaction will be set to now):')
|
||||
bot.register_next_step_handler(message, set_new_transaction_step)
|
||||
|
||||
def set_new_transaction_step(message):
|
||||
user_id = int(message.from_user.id)
|
||||
|
||||
if not re.match(r"[A-Za-z0-9]+,[A-Za-z0-9]+,[0-9]+(.[0-9]+)?,[0-9]+(.[0-9]+)?", message.text):
|
||||
bot.send_message(chat_id=user_id, text='Invalid format \n(e.g. Apple,US0378331005,53.2,120.4).\n Try again with /newtransaction.')
|
||||
return
|
||||
|
||||
transaction_data = str(message.text).split(',')
|
||||
desc = str(transaction_data[0])
|
||||
isin = str(transaction_data[1])
|
||||
amount = float(transaction_data[2])
|
||||
price = float(transaction_data[3])
|
||||
time = dt.datetime.now().isoformat()
|
||||
print("\n\n\n\n\n")
|
||||
print(f"{isin},{amount},{price},{time}")
|
||||
status = api_handler.set_transaction(user_id, desc, isin, amount, price, time)
|
||||
|
||||
if status == 200:
|
||||
bot.send_message(chat_id=user_id, text='Transaction succesfully added.')
|
||||
else:
|
||||
bot.send_message(chat_id=user_id, text=f'Failed adding transaction. (statuscode {status})')
|
||||
|
||||
|
||||
@bot.message_handler(commands=['interval', 'Interval'])
|
||||
def send_interval(message):
|
||||
""" send interval for user
|
||||
:type message: message object bot
|
||||
:param message: message that was reacted to, in this case always '/interval'
|
||||
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
user_id = int(message.from_user.id)
|
||||
user_data = api_handler.get_user(user_id) # get cron interval of user (stored in user data)
|
||||
if user_data == None: # true if user is not registered in DB
|
||||
bot.send_message(chat_id=user_id, text='This didn\'t work. Make sure to connect your telegram id (/id) on https://gruppe1.testsites.info and set an interval with /setinterval')
|
||||
return
|
||||
else: # send interval
|
||||
interval = str(user_data['cron']) # get cron from user data
|
||||
if interval == 'None': # true if user has no cron set
|
||||
bot.send_message(chat_id=user_id, text='You do not have an interval set. Set one with /setinterval')
|
||||
return
|
||||
formatted_interval = str(interval).replace(' ', '_') # replace spaces with underscores to add to url of crontab.guru
|
||||
bot.send_message(chat_id=user_id, text=f'Your update interval: {interval} (https://crontab.guru/#{formatted_interval})')
|
||||
|
||||
|
||||
@bot.message_handler(commands=['setinterval', 'Setinterval'])
|
||||
def set_new_interval(message):
|
||||
""" Set new interval for user
|
||||
:type message: message object bot
|
||||
:param message: message that was reacted to, in this case always '/setinterval'
|
||||
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
user_id = int(message.from_user.id)
|
||||
bot.send_message(chat_id=user_id, text='Type interval in cron format:\n(https://crontab.guru/)')
|
||||
bot.register_next_step_handler(message, set_new_interval_step) # executes function when user sends message
|
||||
|
||||
def set_new_interval_step(message):
|
||||
|
||||
user_id = int(message.from_user.id)
|
||||
interval = str(message.text)
|
||||
status = api_handler.set_cron_interval(user_id, interval) # send cron to db
|
||||
|
||||
if status == 200:
|
||||
bot.send_message(chat_id=user_id, text='Interval succesfully set.')
|
||||
return
|
||||
|
||||
if status == -1: # only -1 when interval is invalid, not a real statuscode, but used from api_handler.set_cron_interval to tell the crontab has the wrong format
|
||||
bot.send_message(chat_id=user_id, text='Invalid interval format. Try again with\n /setinterval.')
|
||||
return
|
||||
else:
|
||||
bot.send_message(chat_id=user_id, text=f'Failed setting interval. (statuscode {status})')
|
||||
|
||||
|
||||
@bot.message_handler(func=lambda message: True) # Returning that command is unknown for any other statement
|
||||
def echo_all(message):
|
||||
|
||||
""" Tell that command is not known if it is no known command
|
||||
@ -274,15 +585,12 @@ def query_text(inline_query):
|
||||
|
||||
def main_loop():
|
||||
|
||||
""" Get Information about bot status every 3 seconds
|
||||
""" Start bot
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
bot.infinity_polling()
|
||||
while 1:
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
|
193
telegram_bot/bot_updates.py
Normal file
193
telegram_bot/bot_updates.py
Normal file
@ -0,0 +1,193 @@
|
||||
"""
|
||||
script for regularly sending updates on shares and news based on user interval
|
||||
"""
|
||||
__author__ = "Florian Kellermann, Linus Eickhoff"
|
||||
__date__ = "26.04.2022"
|
||||
__version__ = "1.0.2"
|
||||
__license__ = "None"
|
||||
|
||||
from dotenv import load_dotenv
|
||||
import news.news_fetcher as news_fetcher
|
||||
import time
|
||||
import os
|
||||
from bot import bot
|
||||
import sys
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from api_handling.api_handler import API_Handler
|
||||
|
||||
|
||||
'''
|
||||
* * * * * code
|
||||
┬ ┬ ┬ ┬ ┬
|
||||
│ │ │ │ │
|
||||
│ │ │ │ └──── weekday (0->Monday, 7->Sunday)
|
||||
│ │ │ └────── Month (1-12)
|
||||
│ │ └──────── Day (1-31)
|
||||
│ └────────── Hour (0-23)
|
||||
└──────────── Minute (0-59)
|
||||
|
||||
example 0 8 * * * -> daily update at 8am
|
||||
'''
|
||||
user_ids = []
|
||||
user_crontab = []
|
||||
|
||||
load_dotenv(dotenv_path='.env')
|
||||
|
||||
def start_updater():
|
||||
""" starting function for regularly sending updates
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
|
||||
print("Bot updates started")
|
||||
|
||||
my_handler = API_Handler("https://gruppe1.testsites.info/api", str(os.getenv("BOT_EMAIL")), str(os.getenv("BOT_PASSWORD")))
|
||||
|
||||
|
||||
update_crontab(my_handler)
|
||||
|
||||
|
||||
def update_crontab(p_my_handler):
|
||||
""" Updating crontab lists every hour
|
||||
:type pCurrent_Time: time when starting crontab update
|
||||
:param pCurrent_Time: datetime
|
||||
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
|
||||
global user_crontab
|
||||
global user_ids
|
||||
|
||||
all_users = p_my_handler.get_all_users()
|
||||
|
||||
user_ids = []
|
||||
user_crontab = []
|
||||
|
||||
for element in all_users:
|
||||
if element["cron"] != '' and element["telegram_user_id"] != '':
|
||||
user_ids.append(int(element["telegram_user_id"]))
|
||||
user_crontab.append(str(element["cron"]))
|
||||
|
||||
print(user_ids)
|
||||
|
||||
update_based_on_crontab(user_ids, user_crontab, p_my_handler)
|
||||
|
||||
update_crontab(p_my_handler)
|
||||
|
||||
|
||||
def update_based_on_crontab(p_user_ids, p_user_crontab, p_my_handler):
|
||||
|
||||
""" Check all the crontab codes and add jobs to start in time
|
||||
:type p_user_ids: array
|
||||
:param p_user_ids: user id array of all users
|
||||
|
||||
:type p_user_crontab: array
|
||||
:param p_user_crontab: crontabs for all users equivalent to the user array
|
||||
|
||||
:type p_my_handler: Api_Handler
|
||||
:param p_my_handler: get database stuff
|
||||
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
|
||||
my_scheduler = BackgroundScheduler()
|
||||
|
||||
for i in range(len(p_user_ids)):
|
||||
cron_split = p_user_crontab[i].split(" ")
|
||||
print(cron_split[4], cron_split[1], cron_split[0], cron_split[3], cron_split[2])
|
||||
my_scheduler.add_job(update_for_user, 'cron', day_of_week = cron_split[4] , hour= cron_split[1] , minute = cron_split[0], month= cron_split[3] , day=cron_split[2], args=(p_user_ids[i], p_my_handler ))
|
||||
|
||||
my_scheduler.start()
|
||||
|
||||
time.sleep( 600 )
|
||||
my_scheduler.shutdown()
|
||||
|
||||
def update_for_user(p_user_id, p_my_handler):
|
||||
|
||||
""" Pull shares and send updates for specific user id
|
||||
:type p_user_id: integer
|
||||
:param p_user_id: user id of user that shall receive update
|
||||
|
||||
:type p_my_handler: Api_Handler
|
||||
:param p_my_handler: handle the api and pull from database
|
||||
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
share_symbols = []
|
||||
share_amounts = []
|
||||
share_courses = []
|
||||
|
||||
my_portfolio = p_my_handler.get_user_portfolio(p_user_id)
|
||||
|
||||
for element in my_portfolio:
|
||||
if element["count"] != '' and element["isin"]!= '':
|
||||
print(element["count"], element["isin"])
|
||||
share_symbols.append(element["isin"])
|
||||
share_amounts.append(element["count"])
|
||||
share_courses.append(element["current_price"])
|
||||
|
||||
my_user = p_my_handler.get_user(p_user_id)
|
||||
send_to_user("Hello %s this is your share update for today:"%str(my_user["username"]), pUser_id=p_user_id)
|
||||
|
||||
if len(share_symbols) != 0:
|
||||
for i in range(len(share_symbols)):
|
||||
my_update_message = f'Symbol: {share_symbols[i]}\nCurrent Price per Share: {share_courses[i]}\nAmount owned: {share_amounts[i]}\nTotal Investment: {float(share_courses[i]) * float(share_amounts[i])}'
|
||||
send_to_user(my_update_message, pUser_id=p_user_id)
|
||||
else:
|
||||
send_to_user("No shares found for your account. Check https://gruppe1.testsites.info to change your settings and add shares.", pUser_id=p_user_id)
|
||||
|
||||
keywords = p_my_handler.get_user_keywords(p_user_id) # get keywords as array
|
||||
|
||||
if(keywords): # if keywords exist and array is not empty
|
||||
send_to_user("If you haven't read yet: \nHere are some interesting news according to your keywords:", pUser_id=p_user_id)
|
||||
for keyword in keywords:
|
||||
news = news_fetcher.get_top_news_by_keyword(keyword)["articles"]
|
||||
|
||||
if not news: # if empty news array
|
||||
send_to_user(f"No news found for keyword _{keyword}_.", pUser_id=p_user_id, md_mode=True)
|
||||
|
||||
if news == None: # if news is none
|
||||
send_to_user(f"Server error for keyword _{keyword}_.", pUser_id=p_user_id, md_mode=True)
|
||||
else:
|
||||
news_formatted = news_fetcher.format_article(news[0]) # format for message, only use the most popular article
|
||||
send_to_user(f"_keyword: {keyword}_\n\n{news_formatted}", pUser_id=p_user_id, md_mode=True) # send news with related keyword in Markdown
|
||||
|
||||
|
||||
|
||||
def send_to_user(pText, pUser_id , md_mode = False):
|
||||
|
||||
""" Send message to user
|
||||
:type pText: string
|
||||
:param pText: Text to send to user
|
||||
|
||||
:type pUser_id: int
|
||||
:param pUser_id: user to send to. per default me (Florian Kellermann)
|
||||
|
||||
:type md_mode: boolean
|
||||
:param md_mode: if true, parse_mode is markdown
|
||||
|
||||
:raises: none
|
||||
|
||||
:rtype: none
|
||||
"""
|
||||
if md_mode:
|
||||
bot.send_message(chat_id=pUser_id, text=pText, parse_mode="MARKDOWN")
|
||||
else:
|
||||
bot.send_message(chat_id=pUser_id, text=pText)
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
start_updater()
|
||||
sys.exit(-1)
|
||||
except KeyboardInterrupt:
|
||||
print("Ending")
|
||||
sys.exit(-1)
|
@ -1,15 +0,0 @@
|
||||
"""
|
||||
script for database interaction
|
||||
"""
|
||||
__author__ = "Florian Kellermann, Linus Eickhoff"
|
||||
__date__ = "15.03.2022"
|
||||
__version__ = "0.0.1"
|
||||
__license__ = "None"
|
||||
|
||||
# get db_key from env
|
||||
|
||||
class DB_Handler:
|
||||
|
||||
def __init__(self, db_adress):
|
||||
# tbd
|
||||
return
|
@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
curl -s http://localhost:80/ -o /dev/null || exit 1
|
@ -1,2 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
python bot.py
|
||||
|
||||
python bot.py &
|
||||
python bot_updates.py
|
@ -2,28 +2,48 @@
|
||||
script for news fetching (by keywords)
|
||||
"""
|
||||
__author__ = "Florian Kellermann, Linus Eickhoff"
|
||||
__date__ = "15.03.2022"
|
||||
__version__ = "0.0.1"
|
||||
__date__ = "26.04.2022"
|
||||
__version__ = "1.0.0"
|
||||
__license__ = "None"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
|
||||
from newsapi import NewsApiClient
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
load_dotenv() # loads environment vars
|
||||
|
||||
# Init
|
||||
api_key = os.getenv('NEWS_API_KEY')
|
||||
newsapi = NewsApiClient(api_key=api_key)
|
||||
source_json = requests.get(f"https://newsapi.org/v2/top-headlines/sources?apiKey={api_key}&language=en").json()
|
||||
sources = source_json["sources"]
|
||||
str_sources = ",".join([source["id"] for source in sources])
|
||||
api_key = os.getenv('NEWS_API_KEY') # get API Key from .env file
|
||||
newsapi = NewsApiClient(api_key=api_key) # news api from https://newsapi.org/
|
||||
try:
|
||||
# get all available news sources (e.g BBC, New York Times, etc.)
|
||||
source_json = requests.get(f"https://newsapi.org/v2/top-headlines/sources?apiKey={api_key}&language=en").json()
|
||||
sources = source_json["sources"]
|
||||
str_sources = ",".join([source["id"] for source in sources])
|
||||
except KeyError:
|
||||
print("Error: Could not get sources")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_all_news_by_keyword(keyword, from_date="2000-01-01"):
|
||||
"""get all news to keyword
|
||||
Args:
|
||||
keyword (String): keyword for search
|
||||
from_date (String): min date for search
|
||||
|
||||
Returns:
|
||||
JSON/dict: dict containing articles
|
||||
"""
|
||||
top_headlines = newsapi.get_everything(q=keyword, sources=str_sources, language='en', from_param=from_date) # keywords can be combined with OR (e.g. keyword = "bitcoin OR ethereum")
|
||||
if(top_headlines["status"] == "ok"):
|
||||
return top_headlines
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_top_news_by_keyword(keyword):
|
||||
"""get top news to keyword
|
||||
Args:
|
||||
@ -31,9 +51,13 @@ def get_top_news_by_keyword(keyword):
|
||||
|
||||
Returns:
|
||||
JSON/dict: dict containing articles
|
||||
"""
|
||||
top_headlines = newsapi.get_top_headlines(q=keyword, sources=str_sources, language='en')
|
||||
return top_headlines
|
||||
"""
|
||||
top_headlines = newsapi.get_top_headlines(q=keyword, sources=str_sources, language='en') # get top headlines, measured by popularity from NewsApi
|
||||
if(top_headlines["status"] == "ok"):
|
||||
return top_headlines
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def format_article(article):
|
||||
"""format article for messaging (using markdown syntax)
|
||||
@ -47,14 +71,19 @@ def format_article(article):
|
||||
sourcename = article["source"]["name"]
|
||||
headline = article["title"]
|
||||
url = article["url"]
|
||||
formatted_article = f"_{sourcename}_\n*{headline}*\n\n{url}"
|
||||
formatted_article = f"_{sourcename}_\n*{headline}*\n\n{url}" # formatting in Markdown syntax
|
||||
|
||||
return formatted_article
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == '__main__': # only execute if script is called directly -> for simple testing
|
||||
|
||||
print("fetching top news by keyword business...")
|
||||
print("this is a module and should not be run directly")
|
||||
print("fetching top news by keyword bitcoin...")
|
||||
|
||||
articles = get_all_news_by_keyword("bitcoin")
|
||||
formatted_article = format_article(articles["articles"][0])
|
||||
print(formatted_article)
|
||||
articles = get_top_news_by_keyword("bitcoin")
|
||||
formatted_article = format_article(articles["articles"][0])
|
||||
print(formatted_article)
|
||||
print(formatted_article)
|
||||
sys.exit(1)
|
@ -1,6 +1,9 @@
|
||||
pyTelegramBotAPI~=4.4.0
|
||||
pyTelegramBotAPI~=4.5.0
|
||||
Markdown~=3.3.6
|
||||
yfinance~=0.1.70
|
||||
newsapi-python~=0.2.6
|
||||
python-dotenv~=0.19.2
|
||||
python-dotenv~=0.20.0
|
||||
requests~=2.27.1
|
||||
APScheduler~=3.9.1
|
||||
croniter~=1.3.4
|
||||
tzlocal==2.1
|
||||
|
@ -7,7 +7,6 @@ __version__ = "0.0.2"
|
||||
__license__ = "None"
|
||||
|
||||
import yfinance
|
||||
import json
|
||||
|
||||
def get_share_price(str_symbol):
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user