From ad508e1dc5f1196fdc9f6d4877456af1f41c9432 Mon Sep 17 00:00:00 2001
From: Sandro Lutz <code@temparus.ch>
Date: Mon, 13 Jul 2020 10:21:43 +0200
Subject: [PATCH] Add basics for telegram bot

---
 Dockerfile                                    |  9 ++--
 Dockerfile.development                        |  6 +++
 README.md                                     |  3 ++
 app/__init__.py                               |  8 ++++
 app/app_context.py                            |  7 ++++
 app/bouncer/views.py                          | 34 +++++++--------
 app/controllers/__init__.py                   |  2 +-
 .../{free_spot.py => free_workplaces.py}      | 34 +++++++--------
 app/controllers/lock.py                       |  2 +-
 app/controllers/record.py                     | 12 +++---
 app/controllers/reservation.py                | 12 +++---
 app/controllers/user.py                       |  2 +-
 app/exceptions.py                             |  4 +-
 app/models/reservation.py                     |  2 +-
 app/static/css/style.css                      |  2 +-
 app/templates/base_authenticated.html         |  2 +-
 app/templates/bouncer/home_anonymous.html     |  4 +-
 app/templates/bouncer/home_authenticated.html |  2 +-
 app/templates/login/register_success.html     |  2 +-
 bot/__init__.py                               |  1 +
 bot/bot.py                                    | 25 +++++++++++
 bot/commands/free_workplaces.py               | 10 +++++
 bot/commands/help.py                          | 40 ++++++++++++++++++
 entrypoint.sh                                 |  2 +-
 instance/config.example.py                    |  4 +-
 manage.sh                                     | 42 +++++++++++++++++--
 requirements.in                               |  1 +
 requirements.txt                              |  9 +++-
 run_dev.py                                    | 11 ++++-
 run_prod.py                                   | 10 ++++-
 30 files changed, 234 insertions(+), 70 deletions(-)
 create mode 100644 app/app_context.py
 rename app/controllers/{free_spot.py => free_workplaces.py} (56%)
 create mode 100644 bot/__init__.py
 create mode 100644 bot/bot.py
 create mode 100644 bot/commands/free_workplaces.py
 create mode 100644 bot/commands/help.py
 mode change 100644 => 100755 entrypoint.sh

diff --git a/Dockerfile b/Dockerfile
index 9066f4d..f479679 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,23 +8,26 @@ EXPOSE 8080
 
 # Install bjoern and dependencies for install (we need to keep libev)
 RUN apk add --no-cache --virtual .deps \
-        musl-dev python-dev gcc git && \
+        musl-dev gcc git && \
     apk add --no-cache libev-dev && \
     apk add --no-cache libffi-dev libressl-dev && \
     pip install bjoern
 
 # Copy files to /bastlibouncer directory, install requirements
 COPY ./ /bastlibouncer
+
 RUN pip install -r /bastlibouncer/requirements.txt
 
 # Cleanup dependencies
 RUN apk del .deps
 
 # Update permissions for entrypoint
-RUN chmod 755 entrypoint.sh
+RUN chmod 755 /bastlibouncer/entrypoint.sh
 
 # Switch user
 USER bastlibouncer
 
+ENTRYPOINT [ "/bastlibouncer/entrypoint.sh" ]
+
 # Start application
-CMD [ "./entrypoint.sh" ]
+CMD [ "python3", "run_prod.py" ]
diff --git a/Dockerfile.development b/Dockerfile.development
index 38ed091..51a20ef 100644
--- a/Dockerfile.development
+++ b/Dockerfile.development
@@ -8,6 +8,12 @@ EXPOSE 5000
 
 # Copy files to /bastlibouncer directory, install requirements
 COPY . /bastlibouncer
+
+# Install dependencies for install
+RUN apk add --no-cache --virtual .deps \
+        musl-dev gcc git && \
+    apk add --no-cache libffi-dev libressl-dev
+
 RUN pip install pip-tools && \
     pip install -r /bastlibouncer/requirements.txt
 
diff --git a/README.md b/README.md
index c469e46..a1cefff 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,9 @@ This is an attendance manager/tracker to ensure COVID-19 restrictions.
 
 **IMPORTANT**: DO NOT CHANGE already commited migrations files!
 
+All files related to data management and flask are in `./app`.
+The files for the telegram bot are in `./bot`.
+
 Use the script `manage.sh` for local development.
 
 ```shell
diff --git a/app/__init__.py b/app/__init__.py
index 4a689dc..6cbeca6 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -4,17 +4,25 @@ from flask_migrate import Migrate
 from flask_bootstrap import Bootstrap
 from flask_wtf.csrf import CSRFProtect
 from flask_mail import Mail
+from .app_context import AppContext
 
 # global variable initialization
 db = SQLAlchemy()
 mail = Mail()
 csrf = CSRFProtect()
+appctx = AppContext()
+
+def app_context():
+    return appctx.app_context()
 
 def create_app():
     # initialize flask app and configure
     app = Flask(__name__, instance_relative_config=True)
     app.config.from_pyfile('config.py')
 
+    # initialize AppContext for usage outside the flask application
+    appctx.init_app(app)
+
     # initialize Mail
     mail.init_app(app)
 
diff --git a/app/app_context.py b/app/app_context.py
new file mode 100644
index 0000000..2f36294
--- /dev/null
+++ b/app/app_context.py
@@ -0,0 +1,7 @@
+class AppContext(object):
+
+    def init_app(self, new_app):
+        self.app = new_app
+
+    def app_context(self):
+        return self.app.app_context()
diff --git a/app/bouncer/views.py b/app/bouncer/views.py
index befea76..ed08ef8 100644
--- a/app/bouncer/views.py
+++ b/app/bouncer/views.py
@@ -2,12 +2,12 @@ from flask import flash, redirect, render_template, url_for, request, abort, mak
 
 from . import bouncer_bp
 from ..auth import login_required, is_authenticated, get_authenticated_user
-from ..controllers import FreeSpotController, UserController, ReservationController, RecordController
-from ..exceptions import NoFreeSpotError
+from ..controllers import FreeWorkplacesController, UserController, ReservationController, RecordController
+from ..exceptions import NoFreeWorkplaceError
 
 @bouncer_bp.route('/', methods=['GET', 'POST'])
 def home():
-    free_spots = FreeSpotController.get_free_spots()
+    free_workplaces = FreeWorkplacesController.get_free_workplaces()
 
     if is_authenticated():
         user = get_authenticated_user()
@@ -20,23 +20,23 @@ def home():
                     UserController.start_record(user)
                     flash('You are allowed to enter.')
                     return redirect(url_for('bouncer.home'))
-                except NoFreeSpotError:
-                    flash('No free spots available! Try again later.', 'error')
+                except NoFreeWorkplaceError:
+                    flash('No free workspaces available! Try again later.', 'error')
             if request.form['action'] == 'reserve':
                 try:
                     UserController.reserve(user)
                     flash('Your reservation has been recorded. Your request for entry will be granted immediately as long as your reservation is valid.')
                     return redirect(url_for('bouncer.home'))
-                except NoFreeSpotError:
-                    flash('No free spots available! Try again later.', 'error')
+                except NoFreeWorkplaceError:
+                    flash('No free workspaces available! Try again later.', 'error')
 
-        return make_response(render_template('bouncer/home_authenticated.html', title='Home', free_spots=free_spots, user=user, reservation=reservation, record=record))
-    return make_response(render_template('bouncer/home_anonymous.html', title='Home', free_spots=free_spots))
+        return make_response(render_template('bouncer/home_authenticated.html', title='Home', free_workplaces=free_workplaces, user=user, reservation=reservation, record=record))
+    return make_response(render_template('bouncer/home_anonymous.html', title='Home', free_workplaces=free_workplaces))
 
 @bouncer_bp.route('/confirm/leave', methods=['GET', 'POST'])
 @login_required
 def confirm_leave():
-    free_spots = FreeSpotController.get_free_spots()
+    free_workplaces = FreeWorkplacesController.get_free_workplaces()
     user = get_authenticated_user()
     record = RecordController.get_active_record(user)
 
@@ -54,12 +54,12 @@ def confirm_leave():
             flash('You have been checked out successfully.')
             return redirect(url_for('bouncer.home'))
 
-    return make_response(render_template('bouncer/confirm_leave.html', title='Confirm: leave', user=user, free_spots=free_spots))
+    return make_response(render_template('bouncer/confirm_leave.html', title='Confirm: leave', user=user, free_workplaces=free_workplaces))
 
 @bouncer_bp.route('/confirm/enter', methods=['GET', 'POST'])
 @login_required
 def confirm_enter():
-    free_spots = FreeSpotController.get_free_spots()
+    free_workplaces = FreeWorkplacesController.get_free_workplaces()
     user = get_authenticated_user()
 
     if not user.is_confirmed:
@@ -76,17 +76,17 @@ def confirm_enter():
                 UserController.start_record(user)
                 flash('You are cleared for entry.')
                 return redirect(url_for('bouncer.home'))
-            except NoFreeSpotError:
-                flash ('No free spots available! Try again later.', 'error')
+            except NoFreeWorkplaceError:
+                flash ('No free workplaces available! Try again later.', 'error')
                 return redirect(url_for('bouncer.home'))
 
-    return make_response(render_template('bouncer/confirm_enter.html', title='Confirm: enter', user=user, free_spots=free_spots))
+    return make_response(render_template('bouncer/confirm_enter.html', title='Confirm: enter', user=user, free_workplaces=free_workplaces))
 
 
 @bouncer_bp.route('/confirm/cancel', methods=['GET', 'POST'])
 @login_required
 def confirm_cancel():
-    free_spots = FreeSpotController.get_free_spots()
+    free_workplaces = FreeWorkplacesController.get_free_workplaces()
     user = get_authenticated_user()
     reservation = ReservationController.get_active_reservation(user)
 
@@ -104,4 +104,4 @@ def confirm_cancel():
             flash('Your reservation has been cancelled.')
             return redirect(url_for('bouncer.home'))
 
-    return make_response(render_template('bouncer/confirm_cancel.html', title='Confirm: cancel reservation', user=user, free_spots=free_spots))
+    return make_response(render_template('bouncer/confirm_cancel.html', title='Confirm: cancel reservation', user=user, free_workplaces=free_workplaces))
diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py
index fb7217f..3ad6ce7 100644
--- a/app/controllers/__init__.py
+++ b/app/controllers/__init__.py
@@ -1,4 +1,4 @@
 from .record import RecordController
-from .free_spot import FreeSpotController
+from .free_workplaces import FreeWorkplacesController
 from .reservation import ReservationController
 from .user import UserController
diff --git a/app/controllers/free_spot.py b/app/controllers/free_workplaces.py
similarity index 56%
rename from app/controllers/free_spot.py
rename to app/controllers/free_workplaces.py
index bbd65ee..a6bd49a 100644
--- a/app/controllers/free_spot.py
+++ b/app/controllers/free_workplaces.py
@@ -5,35 +5,35 @@ from flask import current_app
 from app.models import Record, Reservation
 from app import db
 from .lock import Lock
-from ..exceptions import NoFreeSpotError
+from ..exceptions import NoFreeWorkplaceError
 
-class FreeSpotController():
-    spot_lock = Lock.spot_lock
+class FreeWorkplacesController():
+    workplaces_lock = Lock.workplaces_lock
 
 
     @classmethod
-    def has_free_spots(cls):
-        with cls.spot_lock:
-            return cls._has_free_spots()
+    def has_free_workplaces(cls):
+        with cls.workplaces_lock:
+            return cls._has_free_workplaces()
 
 
     @classmethod
-    def get_free_spots(cls):
-        with cls.spot_lock:
-            return cls._get_free_spots()
+    def get_free_workplaces(cls):
+        with cls.workplaces_lock:
+            return cls._get_free_workplaces()
 
 
     @classmethod
-    def check_free_spots(cls):
-        if not cls._has_free_spots:
-            raise NoFreeSpotError
+    def check_free_workplaces(cls):
+        if not cls._has_free_workplaces:
+            raise NoFreeWorkplaceError
 
 
     # ---------- PRIVATE METHODS BELOW ----------
 
     @classmethod
-    def _get_free_spots(cls):
-        total_spots = current_app.config.get('TOTAL_SPOTS')
+    def _get_free_workplaces(cls):
+        total_workplaces = current_app.config.get('TOTAL_WORKPLACES')
         now = datetime.now()
 
         active_records_count = db.session.query(func.count(Record._id)) \
@@ -48,8 +48,8 @@ class FreeSpotController():
 
 
         # check total of active records and valid reservations.
-        return total_spots - active_records_count - valid_reservations_count
+        return total_workplaces - active_records_count - valid_reservations_count
 
     @classmethod
-    def _has_free_spots(cls):
-        return cls._get_free_spots() > 0
\ No newline at end of file
+    def _has_free_workplaces(cls):
+        return cls._get_free_workplaces() > 0
\ No newline at end of file
diff --git a/app/controllers/lock.py b/app/controllers/lock.py
index 8f492dc..ec6fda5 100644
--- a/app/controllers/lock.py
+++ b/app/controllers/lock.py
@@ -1,4 +1,4 @@
 import threading
 
 class Lock():
-    spot_lock = threading.Lock()
+    workplaces_lock = threading.Lock()
diff --git a/app/controllers/record.py b/app/controllers/record.py
index 5970202..311de12 100644
--- a/app/controllers/record.py
+++ b/app/controllers/record.py
@@ -4,20 +4,20 @@ from sqlalchemy import DateTime, cast, func, or_
 from app import db
 from app.models import Record
 from .lock import Lock
-from .free_spot import FreeSpotController
+from .free_workplaces import FreeWorkplacesController
 from ..exceptions import ReservationExpiredError, UserNotConfirmedError
 
 class RecordController():
-    spot_lock = Lock.spot_lock
+    workplaces_lock = Lock.workplaces_lock
 
 
     @classmethod
     def create(cls, user):
-        with cls.spot_lock:
+        with cls.workplaces_lock:
             if not user.is_confirmed:
                 raise UserNotConfirmedError
 
-            FreeSpotController.check_free_spots()
+            FreeWorkplacesController.check_free_workplaces()
             record = Record()
             record.user = user
             record.time_start = datetime.now()
@@ -29,7 +29,7 @@ class RecordController():
 
     @classmethod
     def create_from_reservation(cls, reservation):
-        with cls.spot_lock:
+        with cls.workplaces_lock:
             if not reservation.user.is_confirmed:
                 raise UserNotConfirmedError
 
@@ -64,6 +64,6 @@ class RecordController():
 
     @classmethod
     def terminate(cls, record):
-        with cls.spot_lock:
+        with cls.workplaces_lock:
             record.time_end = datetime.now()
             db.session.commit()
diff --git a/app/controllers/reservation.py b/app/controllers/reservation.py
index 1047050..f6fd438 100644
--- a/app/controllers/reservation.py
+++ b/app/controllers/reservation.py
@@ -5,17 +5,17 @@ from flask import current_app
 from app import db
 from app.models import Reservation
 from .lock import Lock
-from .free_spot import FreeSpotController
-from ..exceptions import ReservationExpiredError, NoFreeSpotError, UserNotConfirmedError
+from .free_workplaces import FreeWorkplacesController
+from ..exceptions import ReservationExpiredError, NoFreeWorkplaceError, UserNotConfirmedError
 
 class ReservationController():
-    spot_lock = Lock.spot_lock
+    workplaces_lock = Lock.workplaces_lock
 
 
     @classmethod
     def create(cls, user):
-        with cls.spot_lock:
-            FreeSpotController.check_free_spots()
+        with cls.workplaces_lock:
+            FreeWorkplacesController.check_free_workplaces()
 
             if not user.is_confirmed:
                 raise UserNotConfirmedError
@@ -45,6 +45,6 @@ class ReservationController():
 
     @classmethod
     def cancel(cls, reservation):
-        with cls.spot_lock:
+        with cls.workplaces_lock:
             reservation.time_end = datetime.now()
             db.session.commit()
diff --git a/app/controllers/user.py b/app/controllers/user.py
index a98d07e..8f3ca6e 100644
--- a/app/controllers/user.py
+++ b/app/controllers/user.py
@@ -11,7 +11,7 @@ from .reservation import ReservationController
 from .record import RecordController
 from ..exceptions import ActiveReservationExistsError, \
     ActiveRecordExistsError, NoActiveRecordError, ReservationExpiredError, \
-    NoFreeSpotError, UserRegistrationInvalidDataError
+    NoFreeWorkplaceError, UserRegistrationInvalidDataError
 
 
 class UserController():
diff --git a/app/exceptions.py b/app/exceptions.py
index 40cf14d..2d1a555 100644
--- a/app/exceptions.py
+++ b/app/exceptions.py
@@ -5,8 +5,8 @@ class Error(Exception):
     pass
 
 
-class NoFreeSpotError(Error):
-    """Raised when no free spots are available"""
+class NoFreeWorkplaceError(Error):
+    """Raised when no free workplaces are available"""
     pass
 
 
diff --git a/app/models/reservation.py b/app/models/reservation.py
index 6c994b9..855280e 100644
--- a/app/models/reservation.py
+++ b/app/models/reservation.py
@@ -3,7 +3,7 @@ from datetime import datetime, timedelta
 
 class Reservation(db.Model):
     """
-    Spot reservation entry.
+    Workplace reservation entry.
     """
 
     __tablename__ = 'reservations'
diff --git a/app/static/css/style.css b/app/static/css/style.css
index d6c7acd..d95a679 100644
--- a/app/static/css/style.css
+++ b/app/static/css/style.css
@@ -86,7 +86,7 @@ p.copyright {
 .error {
   text-align: center;
 }
-.free_spots {
+.free_workplaces {
   font-size: 1.2em;
   font-weight: bold;
   text-decoration: underline;
diff --git a/app/templates/base_authenticated.html b/app/templates/base_authenticated.html
index f10bdb3..4cc3223 100644
--- a/app/templates/base_authenticated.html
+++ b/app/templates/base_authenticated.html
@@ -8,7 +8,7 @@
   <div class="center-narrow">
     <h1>Bastli Bouncer</h1>
     <br/>
-    <p>We currently have <span class="free_spots">{{ free_spots }} free spots</span> in our workshop.</p>
+    <p>We currently have <span class="free_workplaces">{{ free_workplaces }} free workplaces</span> in our workshop.</p>
     <p>
       Welcome, {{ user.name }} ({{ user.email }}).
       <a class="btn btn-default" role="button" href="{{ url_for('login.logout') }}">Logout</a>
diff --git a/app/templates/bouncer/home_anonymous.html b/app/templates/bouncer/home_anonymous.html
index f6bc104..3baf77c 100644
--- a/app/templates/bouncer/home_anonymous.html
+++ b/app/templates/bouncer/home_anonymous.html
@@ -8,9 +8,9 @@
   <div class="center-narrow">
     <h1>Bastli Bouncer</h1>
     <br/>
-    <p>We currently have <span class="free_spots">{{ free_spots }} free spots</span> in our workshop.</p>
+    <p>We currently have <span class="free_workplaces">{{ free_workplaces }} free workplaces</span> in our workshop.</p>
     <br/>
-    <p>Register and reserve your spot here before you come by! Thank you.</p>
+    <p>Register and reserve your workplace here before you come by! Thank you.</p>
     <br/>
     <p>
       <a class="btn btn-default" role="button" href="{{ url_for('login.login') }}">Login</a>
diff --git a/app/templates/bouncer/home_authenticated.html b/app/templates/bouncer/home_authenticated.html
index 28b527f..ee0441b 100644
--- a/app/templates/bouncer/home_authenticated.html
+++ b/app/templates/bouncer/home_authenticated.html
@@ -17,7 +17,7 @@
       <form action="" method="post">
         <input type="hidden" name="action" value="reserve">
         <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
-        <input class="btn btn-primary" type="submit" value="Reserve a spot">
+        <input class="btn btn-primary" type="submit" value="Reserve a workplace">
       </form>
        or 
        <form action="" method="post">
diff --git a/app/templates/login/register_success.html b/app/templates/login/register_success.html
index d4fe5bb..c558d47 100644
--- a/app/templates/login/register_success.html
+++ b/app/templates/login/register_success.html
@@ -10,7 +10,7 @@
     <br/>
     <h2>Registration successful!</h2>
     <br>
-    <p>Please check the inbox of your email address and follow the insttructions.</p>
+    <p>Please check the inbox of your email address and follow the instructions.</p>
     <p>
       <a class="btn btn-default" role="button" href="{{ url_for('login.login') }}">Login</a>
       <a class="btn btn-default" role="button" href="{{ url_for('bouncer.home') }}">Home</a>
diff --git a/bot/__init__.py b/bot/__init__.py
new file mode 100644
index 0000000..e288ec0
--- /dev/null
+++ b/bot/__init__.py
@@ -0,0 +1 @@
+from .bot import create_bot
diff --git a/bot/bot.py b/bot/bot.py
new file mode 100644
index 0000000..e9fd8f2
--- /dev/null
+++ b/bot/bot.py
@@ -0,0 +1,25 @@
+from telegram.ext import Updater, CommandHandler, MessageHandler, Filters
+
+from .commands.help import help_command, start_command
+from .commands.free_workplaces import freeworkplaces_command
+
+# def echo(update, context):
+#     """Echo the user message."""
+#     update.message.reply_text(update.message.text)
+
+
+def create_bot(token):
+    updater = Updater(token, use_context=True)
+
+    # Get the dispatcher to register handlers
+    dp = updater.dispatcher
+
+    # on different commands - answer in Telegram
+    dp.add_handler(CommandHandler("start", start_command))
+    dp.add_handler(CommandHandler("help", help_command))
+    dp.add_handler(CommandHandler("freeworkplaces", freeworkplaces_command))
+
+    # on noncommand i.e message - echo the message on Telegram
+    # dp.add_handler(MessageHandler(Filters.text, echo))
+
+    return updater
diff --git a/bot/commands/free_workplaces.py b/bot/commands/free_workplaces.py
new file mode 100644
index 0000000..9613dc7
--- /dev/null
+++ b/bot/commands/free_workplaces.py
@@ -0,0 +1,10 @@
+from telegram import ParseMode
+from app.controllers import FreeWorkplacesController
+from app import app_context
+
+def freeworkplaces_command(update, context):
+    """Send a message when the command /freeworkplaces is issued."""
+    with app_context():
+        free_workplaces = FreeWorkplacesController.get_free_workplaces()
+    response  = '*' + str(free_workplaces) + '* workplaces are currently available.'
+    update.message.reply_text(response, parse_mode=ParseMode.MARKDOWN)
diff --git a/bot/commands/help.py b/bot/commands/help.py
new file mode 100644
index 0000000..8870bf5
--- /dev/null
+++ b/bot/commands/help.py
@@ -0,0 +1,40 @@
+from telegram import ParseMode
+
+def start_command(update, context):
+    """Send a message when the command /start is issued."""
+    start_text  = 'Hi!\n\n'
+    start_text += 'I\'m the Bastli bouncer bot.\n'
+    start_text += 'My job is to ensure that the workshop doesn\'t get crowded and to maintain the user list.\n\n'
+    start_text += 'Please follow the rules below:\n\n'
+    start_text += '*Rule 1*\n'
+    start_text += 'Register yourself with /register [email] [name]\n\n'
+    start_text += '*Rule 2*\n'
+    start_text += 'Check for free workplaces and reserve one before you come here.\n\n'
+    start_text += '*Rule 3*\n'
+    start_text += 'Send /enter when you enter the workshop\n\n'
+    start_text += '*Rule 4*\n'
+    start_text += 'Send /leave when you leave the workshop\n\n\n'
+    start_text += 'See /help for more information about the commands.'
+    update.message.reply_text(start_text, parse_mode=ParseMode.MARKDOWN)
+
+
+def help_command(update, context):
+    """Send a message when the command /help is issued."""
+    help_text  = '*Bastli Bouncer bot usage:*\n\n'
+    help_text += '/freeworkplaces\n'
+    help_text += '    _Get available workplaces._\n'
+    help_text += '/register [email] [name]\n'
+    help_text += '    _Create an account._\n'
+    help_text += '/resetpassword\n'
+    help_text += '    _Request password reset._\n'
+    help_text += '/status\n'
+    help_text += '    _Get status about own user._\n'
+    help_text += '/reserve\n'
+    help_text += '    _Reserve a workplace._\n'
+    help_text += '/cancel\n'
+    help_text += '    _Cancel a workplace reservation._\n'
+    help_text += '/enter\n'
+    help_text += '    _Request for entry._\n'
+    help_text += '/leave\n'
+    help_text += '    _Free the workspace._\n'
+    update.message.reply_text(help_text, parse_mode=ParseMode.MARKDOWN)
diff --git a/entrypoint.sh b/entrypoint.sh
old mode 100644
new mode 100755
index df26477..80d2ef1
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -11,4 +11,4 @@ while true; do
 done
 unset FLASK_APP
 
-python3 run_prod.py
+"${@}"
diff --git a/instance/config.example.py b/instance/config.example.py
index 0085ab9..853c69a 100644
--- a/instance/config.example.py
+++ b/instance/config.example.py
@@ -1,7 +1,7 @@
 # Example Configuration File
 from datetime import timedelta
 
-TOTAL_SPOTS = 3
+TOTAL_WORKPLACES = 3
 RESERVATION_DURATION = timedelta(hours=1)
 RECORD_TIMEOUT = timedelta(hours=24)
 USER_TOKEN_TIMEOUT = timedelta(hours=12)
@@ -15,6 +15,8 @@ MAIL_USERNAME = None
 MAIL_PASSWORD = None
 MAIL_DEFAULT_SENDER = 'Bastli Bouncer <noreply@bastli.ethz.ch>'
 
+TELEGRAM_BOT_TOKEN = '<telegram-bot-token>'
+
 DEBUG = False
 SQLALCHEMY_TRACK_MODIFICATIONS = False
 
diff --git a/manage.sh b/manage.sh
index 7fe794e..fd64728 100755
--- a/manage.sh
+++ b/manage.sh
@@ -6,20 +6,52 @@ else
     SUDO_COMMAND="sudo"
 fi
 
+PROD_DOCKER_RUN_COMMAND="${SUDO_COMMAND} docker run -it \
+    -p 8080:8080 \
+    --network bastli-bouncer_backend \
+    -v ${PWD}:/bastlibouncer \
+    -v ${PWD}/instance/config.dev.py:/bastlibouncer/instance/config.py \
+    -v /etc/localtime:/etc/localtime \
+    bastlibouncer"
+
 BASE_DOCKER_RUN_COMMAND="${SUDO_COMMAND} docker run -it \
     -p 5000:5000 \
     --network bastli-bouncer_backend \
     -v ${PWD}:/bastlibouncer \
     -v ${PWD}/instance/config.dev.py:/bastlibouncer/instance/config.py \
-    -v /etc/localtime:/etc/localtime
+    -v /etc/localtime:/etc/localtime \
     bastlibouncer-dev"
 
 case $1 in
     build)
-        $SUDO_COMMAND docker build -t bastlibouncer-dev -f Dockerfile.development .
+        case $2 in
+            dev)
+                $SUDO_COMMAND docker build -t bastlibouncer-dev -f Dockerfile.development .
+                ;;
+            prod)
+                $SUDO_COMMAND docker build -t bastlibouncer -f Dockerfile .
+                ;;
+            *)
+                echo "Unknown variant for command \"build\"."
+                echo "Available variants: prod, dev"
+                exit 1
+                ;;
+        esac
         ;;
     run)
-        $BASE_DOCKER_RUN_COMMAND flask run --host=0.0.0.0
+        case $2 in
+            dev)
+                $BASE_DOCKER_RUN_COMMAND flask run --host=0.0.0.0
+                ;;
+            prod)
+                $PROD_DOCKER_RUN_COMMAND
+                ;;
+            *)
+                echo "Unknown variant for command \"run\"."
+                echo "Available variants: prod, dev"
+                exit 1
+                ;;
+        esac
         ;;
     services)
         case $2 in
@@ -34,6 +66,7 @@ case $1 in
                 ;;
             *)
                 echo "Unknown sub-command for command \"services\"."
+                echo "Available sub-commands: start, restart, stop"
                 exit 1
                 ;;
         esac
@@ -53,7 +86,8 @@ case $1 in
         echo "  manage.sh [COMMAND]"
         echo ""
         echo "COMMAND:"
-        echo "  build                          Build docker image for local development."
+        echo "  build [dev|prod]               Build docker image for given environment."
+        echo "  run [dev|prod]                 Run docker container for given environment."
         echo "  services [start|restart|stop]  Start/stop service dependencies."
         echo "  makemigrations                 Create new migration files."
         echo "  migrate                        Apply migrations to local database."
diff --git a/requirements.in b/requirements.in
index ff5bbfa..a6e5db0 100644
--- a/requirements.in
+++ b/requirements.in
@@ -14,3 +14,4 @@ py3-validate-email
 pymysql
 
 # telegram bot
+python-telegram-bot
diff --git a/requirements.txt b/requirements.txt
index c2f9e58..0fb569b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,7 +6,11 @@
 #
 alembic==1.4.2            # via flask-migrate
 blinker==1.4              # via flask-mail
+certifi==2020.6.20        # via python-telegram-bot
+cffi==1.14.0              # via cryptography
 click==7.1.2              # via flask
+cryptography==2.9.2       # via python-telegram-bot
+decorator==4.4.2          # via python-telegram-bot
 dnspython==1.16.0         # via py3-validate-email
 dominate==2.5.1           # via flask-bootstrap
 filelock==3.0.12          # via py3-validate-email
@@ -22,11 +26,14 @@ jinja2==2.11.2            # via flask
 mako==1.1.3               # via alembic
 markupsafe==1.1.1         # via jinja2, mako, wtforms
 py3-validate-email==0.2.9  # via -r requirements.in
+pycparser==2.20           # via cffi
 pymysql==0.9.3            # via -r requirements.in
 python-dateutil==2.8.1    # via alembic
 python-editor==1.0.4      # via alembic
-six==1.15.0               # via python-dateutil
+python-telegram-bot==12.8  # via -r requirements.in
+six==1.15.0               # via cryptography, python-dateutil
 sqlalchemy==1.3.18        # via alembic, flask-sqlalchemy
+tornado==6.0.4            # via python-telegram-bot
 visitor==0.1.3            # via flask-bootstrap
 werkzeug==1.0.1           # via flask
 wtforms==2.3.1            # via flask-wtf
diff --git a/run_dev.py b/run_dev.py
index 12ca988..db591fd 100644
--- a/run_dev.py
+++ b/run_dev.py
@@ -1,6 +1,15 @@
 from app import create_app
+from bot import create_bot
 
 app = create_app()
+bot = create_bot(app.config.get('TELEGRAM_BOT_TOKEN'))
 
 if __name__ == '__main__':
-    app.run(host='0.0.0.0')
+    print('Starting telegram bot...', flush=True)
+    bot.start_polling()
+    print('Starting flask dev server on port 5000...', flush=True)
+    try:
+        app.run(host='0.0.0.0')
+    except BaseException as e:
+        print(e)
+    bot.stop()
diff --git a/run_prod.py b/run_prod.py
index fbe7b0f..38317f2 100644
--- a/run_prod.py
+++ b/run_prod.py
@@ -2,10 +2,18 @@
 # [bjoern](https://github.com/jonashaag/bjoern) required.
 
 from app import create_app
+from bot import create_bot
 import bjoern
 
 app = create_app()
+bot = create_bot(app.config.get('TELEGRAM_BOT_TOKEN'))
 
 if __name__ == '__main__':
+    print('Starting telegram bot...', flush=True)
+    bot.start_polling()
     print('Starting bjoern on port 8080...', flush=True)
-    bjoern.run(app, '0.0.0.0', 8080)
+    try:
+        bjoern.run(app, '0.0.0.0', 8080)
+    except BaseException as e:
+        print(e)
+    bot.stop()
-- 
GitLab