diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..2f75dcc --- /dev/null +++ b/alembic.ini @@ -0,0 +1,74 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat alembic/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = sqlite:///dev-db.sqlite + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/README b/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..ecfcb0d --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,81 @@ + +from helpers.database import Base +from models import notification_model +from models import calendar_model +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata + + +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/ed2401b502eb_initial_migration.py b/alembic/versions/ed2401b502eb_initial_migration.py new file mode 100644 index 0000000..96bd5de --- /dev/null +++ b/alembic/versions/ed2401b502eb_initial_migration.py @@ -0,0 +1,44 @@ +"""Initial Migration + +Revision ID: ed2401b502eb +Revises: +Create Date: 2019-05-28 22:52:34.307606 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ed2401b502eb' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('calendars', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('calendar_id', sa.Integer(), nullable=False), + sa.Column('channel_id', sa.String(), nullable=True), + sa.Column('resource_id', sa.String(), nullable=True), + sa.Column('firebase_token', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('notification', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('time', sa.String(), nullable=True), + sa.Column('results', sa.String(), nullable=True), + sa.Column('subscriber_info', sa.String(), nullable=True), + sa.Column('platform', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('notification') + op.drop_table('calendars') + # ### end Alembic commands ### diff --git a/app.py b/app.py index b73d829..0f06974 100644 --- a/app.py +++ b/app.py @@ -22,7 +22,7 @@ def index(): @app.route("/notifications", methods=['POST', 'GET']) def calendar_notifications(): - PushNotification().send_notifications_to_subscribers() + # PushNotification().send_notifications_to_subscribers() return PushNotification.send_notifications(PushNotification) @app.route("/channels", methods=['POST', 'GET']) diff --git a/config.py b/config.py index abfc2e1..db41900 100644 --- a/config.py +++ b/config.py @@ -14,19 +14,23 @@ class DevelopmentConfig(Config): DEBUG = True NOTIFICATION_URL = os.getenv('DEV_NOTIFICATION_URL') CONVERGE_MRM_URL = os.getenv('DEV_CONVERGE_MRM_URL') - REDIS_DATABASE_URI = os.getenv('DEV_REDIS_URL') + SQLALCHEMY_DATABASE_URI = ( + 'sqlite:///' + os.path.join(basedir, 'dev-db.sqlite')) class ProductionConfig(Config): NOTIFICATION_URL = os.getenv('NOTIFICATION_URL') CONVERGE_MRM_URL = os.getenv('CONVERGE_MRM_URL') - REDIS_DATABASE_URI = os.getenv('PROD_REDIS_URL') + SQLALCHEMY_DATABASE_URI = ( + 'sqlite:///' + os.path.join(basedir, 'data.sqlite')) + class TestingConfig(Config): DEBUG = True NOTIFICATION_URL = os.getenv('DEV_NOTIFICATION_URL') CONVERGE_MRM_URL = os.getenv('DEV_CONVERGE_MRM_URL') - REDIS_DATABASE_URI = os.getenv('TEST_REDIS_URL') + SQLALCHEMY_DATABASE_URI = ( + 'sqlite:///' + os.path.join(basedir, 'test.sqlite')) config = { diff --git a/helpers/calendar.py b/helpers/calendar.py index 010a279..78af1c0 100644 --- a/helpers/calendar.py +++ b/helpers/calendar.py @@ -1,14 +1,11 @@ -from helpers.database import db +# from helpers.database import db -def update_calendar(calendar, calendar_key, room): - if not room['firebaseToken']: - room['firebaseToken'] = '' - if not calendar: - key = len(db.keys('*Calendar*')) + 1 - calendar_key = 'Calendar:' + str(key) - elif 'firebase_token' not in calendar.keys() or calendar['firebase_token'] == room['firebaseToken']: - return None - - db.hmset(calendar_key, {'calendar_id': room['calendarId'], 'firebase_token': room['firebaseToken']}) - db.persist(calendar_key) \ No newline at end of file +# def update_calendar(calendar, calendar_key, room): +# if not room['firebaseToken']: +# room['firebaseToken'] = '' +# if not calendar: +# key = len(db.keys('*Calendar*')) + 1 +# db.hmset('Calendar:' + str(key), {'calendar_id': room['calendarId'], 'firebase_token': room['firebaseToken']}) +# if 'firebase_token' in calendar.keys() and calendar['firebase_token'] != room['firebaseToken']: +# db.hmset(calendar_key, {'calendar_id': room['calendarId'], 'firebase_token': room['firebaseToken']}) diff --git a/helpers/database.py b/helpers/database.py index 8319e0b..7df92e5 100644 --- a/helpers/database.py +++ b/helpers/database.py @@ -1,10 +1,18 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import scoped_session, sessionmaker from config import config import os import sys -import redis sys.path.append(os.getcwd()) config_name = os.getenv('APP_SETTINGS') -database_uri = config.get(config_name).REDIS_DATABASE_URI -db = redis.from_url(database_uri, charset="utf-8", decode_responses=True) +database_uri = config.get(config_name).SQLALCHEMY_DATABASE_URI +engine = create_engine(database_uri, convert_unicode=True) +db_session = scoped_session(sessionmaker(autocommit=False, + autoflush=False, + bind=engine)) + +Base = declarative_base() +Base.query = db_session.query_property() diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/calendar_model.py b/models/calendar_model.py new file mode 100644 index 0000000..35ab8c3 --- /dev/null +++ b/models/calendar_model.py @@ -0,0 +1,13 @@ +from helpers.database import Base +from utilities.utility import Utility +from sqlalchemy import (Column, String, Integer, Sequence) + + +class Calendar(Base, Utility): + __tablename__ = 'calendars' + id = Column(Integer, Sequence('calendars_id_seq', start=1, increment=1), + primary_key=True) + calendar_id = Column(Integer, nullable=False) + channel_id = Column(String, nullable=True) + resource_id = Column(String, nullable=True) + firebase_token = Column(String, nullable=True) diff --git a/models/notification_model.py b/models/notification_model.py new file mode 100644 index 0000000..30de191 --- /dev/null +++ b/models/notification_model.py @@ -0,0 +1,12 @@ +from helpers.database import Base +from utilities.utility import Utility +from sqlalchemy import (Column, String, Integer, Sequence) + + +class Notification(Base, Utility): + __tablename__ = 'notification' + id = Column(Integer, primary_key=True) + time = Column(String) + results = Column(String) + subscriber_info = Column(String) + platform = Column(String) diff --git a/requirements.txt b/requirements.txt index 0781626..6d71b85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,58 @@ +alembic==1.0.10 +asn1crypto==0.24.0 +atomicwrites==1.3.0 +attrs==19.1.0 +autopep8==1.4.4 +certifi==2019.3.9 +cffi==1.12.3 +chardet==3.0.4 +Click==7.0 +coverage==4.5.3 +cryptography==2.6.1 +entrypoints==0.3 flake8==3.7.5 Flask==0.12.2 Flask-Cors==3.0.4 Flask-JSON==0.3.2 +Flask-Migrate==2.5.2 Flask-Script==2.0.6 +Flask-SQLAlchemy==2.4.0 google-api-python-client==1.6.7 +http-ece==1.1.0 +httplib2==0.12.3 +idna==2.8 +importlib-metadata==0.15 +itsdangerous==1.1.0 +Jinja2==2.10.1 +Mako==1.0.10 MarkupSafe==1.1.0 +mccabe==0.6.1 +more-itertools==7.0.0 +ndg-httpsclient==0.5.1 oauth2client==4.1.3 +pep8==1.7.1 +pluggy==0.12.0 +py==1.8.0 +py-vapid==1.5.0 +pyasn1==0.4.5 +pyasn1-modules==0.2.5 +pycodestyle==2.5.0 +pycparser==2.19 pyfcm==1.4.5 -virtualenv==15.2.0 -pywebpush==1.8.0 +pyflakes==2.1.1 +pyOpenSSL==19.0.0 pytest==4.3.0 -redis -ndg-httpsclient -pyopenssl +python-dateutil==2.8.0 +python-editor==1.0.4 +pywebpush==1.8.0 +redis==3.2.1 +requests==2.22.0 +requests-toolbelt==0.9.1 +rsa==4.0 +six==1.12.0 +SQLAlchemy==1.3.4 +uritemplate==3.0.0 +urllib3==1.25.3 +virtualenv==15.2.0 +Werkzeug==0.15.4 +zipp==0.5.1 diff --git a/service/push_notification.py b/service/push_notification.py index 4487500..fe1838b 100644 --- a/service/push_notification.py +++ b/service/push_notification.py @@ -3,17 +3,18 @@ import os import json import ast +import datetime -from helpers.database import db +from models.calendar_model import Calendar +from models.notification_model import Notification from flask import jsonify, request, render_template from pyfcm import FCMNotification from pywebpush import webpush, WebPushException from config import config from helpers.credentials import Credentials -from utilities.utility import stop_channel, save_to_db +from utilities.utility import update_entity_fields, save_to_db, stop_channel from apiclient import errors -from helpers.calendar import update_calendar config_name = os.getenv('APP_SETTINGS') @@ -37,11 +38,12 @@ def get_supported_platforms(self): return supported_platforms def update_firebase_token(self, calendar_id, firebase_token): - for key in db.keys('*Calendar*'): - calendar = db.hgetall(key) - if calendar['calendar_id'] == calendar_id: - calendar['firebase_token'] = firebase_token - db.hmset(key, calendar) + exact_calendar = Calendar.query.filter_by( + calendar_id=calendar_id).first() + update_entity_fields(exact_calendar, + calendar_id=calendar_id, + firebase_token=firebase_token) + exact_calendar.save() return "OK" def refresh(self): @@ -54,17 +56,23 @@ def refresh(self): selected_calendar = {} selected_calendar_key = '' for room in rooms: - for key in db.keys('*Calendar*'): - calendar = db.hgetall(key) - if calendar['calendar_id'] == room['calendarId']: - selected_calendar = calendar - selected_calendar_key = key - break - update_calendar(selected_calendar, selected_calendar_key, room) + exact_calendar = Calendar.query.filter_by( + calendar_id=room['calendarId']).first() + if not exact_calendar: + Calendar(calendar_id=room['calendarId'], + firebase_token=room['firebaseToken']).save() + continue + + if exact_calendar.firebase_token != room['firebaseToken']: + update_entity_fields(exact_calendar, + calendar_id=room['calendarId'], + firebase_token=room['firebaseToken']) + exact_calendar.save() data = { "message": "Calendars saved successfully" } + response = jsonify(data) return response @@ -77,70 +85,49 @@ def create_channels(self): } service = Credentials.set_api_credentials(self) calendar = {} - calendars = [] + calendars = Calendar.query.all() channels = [] - for key in db.keys('*Calendar*'): - calendar = db.hgetall(key) - calendar['key'] = key - calendars.append(calendar) for calendar in calendars: request_body['id'] = str(uuid.uuid4()) - if not 'channel_id' in calendar.keys(): - calendar['channel_id'] = '' - if not 'resource_id' in calendar.keys(): - calendar['resource_id'] = '' - stop_channel(service, calendar['channel_id'], calendar['resource_id']) + stop_channel( + service, calendar.channel_id, calendar.resource_id) try: channel = service.events().watch( - calendarId=calendar['calendar_id'], + calendarId=calendar.calendar_id, body=request_body).execute() except errors.HttpError as error: print('An error occurred', error) continue - db.hmset(calendar['key'], {'channel_id': channel['id'], 'resource_id': channel['resourceId']}) + update_entity_fields(calendar, channel_id=channel['id'], + resource_id=channel['resourceId']) + calendar.save() channels.append(channel) response = jsonify(channels) return response def send_notifications(self): - selected_calendar = {} - results = {} - for key in db.keys('*Calendar*'): - calendar = db.hgetall(key) - if 'resource_id' in calendar and calendar['resource_id'] == request.headers['X-Goog-Resource-Id']: - selected_calendar = calendar - break - if 'firebase_token' in selected_calendar.keys(): - results = push_service.notify_single_device( - registration_id=selected_calendar['firebase_token'], - message_body="success") + exact_calendar = Calendar.query.filter_by( + resource_id=request.headers['X-Goog-Resource-Id']).first() + + if exact_calendar.firebase_token: + # results = push_service.notify_single_device( + # registration_id=exact_calendar.firebase_token, + # message_body="success") result = {} - result['results'] = results['results'] - result['subscriber_info'] = selected_calendar['firebase_token'] + result['results'] = "results['results']" + result['subscriber_info'] = "selected_calendar['firebase_token']" result['platform'] = 'android' - save_to_db(result) + result['time'] = datetime.datetime.now().replace( + second=0, microsecond=0) - # Send an update to the backend API - if 'calendar_id' in selected_calendar.keys(): - notify_api_mutation = ( - { - 'query': - """ - mutation { - mrmNotification ( calendarId: \"%s\" ) { - message - } - } - """ % selected_calendar['calendar_id'] - } - ) - headers = {'Authorization': 'Bearer %s' % api_token} - requests.post(url=url, json=notify_api_mutation, headers=headers) - return jsonify(results) + Notification(results=result['results'], + subscriber_info=result['subscriber_info'], + platform=result['platform'], time=result['time']).save() + return jsonify(result) data = { "message": "Notification received but no registered device" @@ -177,9 +164,11 @@ def send_rest_notification(self, subscriber_url, calendar_id): def send_graphql_notification(self, subscriber_url, calendar_id): calendar_id = str(calendar_id) - notification_mutation = "mutation{mrmNotification(calendarId:\"" + calendar_id + "\"){message}}" + notification_mutation = "mutation{mrmNotification(calendarId:\"" + \ + calendar_id + "\"){message}}" try: - result = requests.post(url=subscriber_url, json={'query': notification_mutation}) + result = requests.post(url=subscriber_url, json={ + 'query': notification_mutation}) save_to_db(result) except Exception as e: print(e) @@ -194,36 +183,16 @@ def send_android_notification(self, firebase_token, calendar_id): result['platform'] = 'android' save_to_db(result) - def send_notifications_to_subscribers(self): - calendar = {} - calendar_id = '' - for key in db.keys('*Calendar*'): - each_calendar = db.hgetall(key) - if 'resource_id' in each_calendar and each_calendar['resource_id'] == request.headers['X-Goog-Resource-Id']: - calendar = each_calendar - calendar_id = each_calendar['calendar_id'] - break - subscribers = [] - for subscriber_key in db.keys('*Subscriber*'): - each_subscriber = db.hmget(subscriber_key, 'calendars')[0] - each_subscriber = ast.literal_eval(each_subscriber) - matching_subscribers = list(filter(lambda x: x == calendar_id, each_subscriber)) - if len(matching_subscribers): - subscribers.append(db.hgetall(subscriber_key)) - supported_platforms = self.get_supported_platforms() - for subscriber in subscribers: - platform = subscriber['platform'] - return supported_platforms[platform](subscriber, calendar_id) - def subscribe(self, subscriber_info): suported_platforms = self.get_supported_platforms().keys() if not subscriber_info["platform"] in suported_platforms: return "We currently do not support this platform" if subscriber_info["platform"] == "web": - subscriber_info["subscription_info"] = json.dumps(subscriber_info["subscription_info"]) + subscriber_info["subscription_info"] = json.dumps( + subscriber_info["subscription_info"]) subscriber_calendar_ids = subscriber_info.get("calendars") calendar_ids = subscriber_calendar_ids - if not subscriber_calendar_ids: + if not subscriber_calendar_ids: calendar_ids = [] for key in db.keys('*Calendar*'): calendar = db.hgetall(key) @@ -231,7 +200,8 @@ def subscribe(self, subscriber_info): subscriber_key = str(uuid.uuid4()) subscriber_info["subscriber_key"] = subscriber_key - subscriber_details = {'platform': subscriber_info["platform"], 'subscription_info': subscriber_info["subscription_info"], "subscribed": "True", "subscriber_key": subscriber_key} + subscriber_details = {'platform': subscriber_info["platform"], 'subscription_info': subscriber_info[ + "subscription_info"], "subscribed": "True", "subscriber_key": subscriber_key} key = len(db.keys('*Subscriber*')) + 1 db.hmset('Subscriber:' + str(key), subscriber_details) calendars = [] @@ -243,12 +213,14 @@ def subscribe(self, subscriber_info): calendar = each_calendar subscibers_list = [] if 'subscribers_list' in calendar.keys(): - subscibers_list = calendar['subscribers_list'].strip('"') + subscibers_list = calendar['subscribers_list'].strip( + '"') subscibers_list = ast.literal_eval(subscibers_list) subscibers_list.append(subscriber_details) else: subscibers_list.append(subscriber_details) - db.hmset(calendar_key, {'subscribers_list': str(subscibers_list)}) + db.hmset(calendar_key, { + 'subscribers_list': str(subscibers_list)}) calendar = db.hgetall(calendar_key) calendars.append(calendar) break @@ -256,10 +228,8 @@ def subscribe(self, subscriber_info): return subscriber_info def get_notifications(self): - notifications = [] - for key in db.keys('*Notification*'): - notification = db.hgetall(key) - notifications.append(notification) + notifications = Notification.query.all() + print(notifications) return render_template( 'log.html', result=notifications diff --git a/utilities/utility.py b/utilities/utility.py index ac618da..01c92fb 100644 --- a/utilities/utility.py +++ b/utilities/utility.py @@ -2,7 +2,7 @@ from apiclient import errors from flask import render_template -from helpers.database import db +from helpers.database import db_session def stop_channel(service, channel_id, resource_id): @@ -24,15 +24,24 @@ def stop_channel(service, channel_id, resource_id): def save_to_db(*args): """ Function to save to database.""" - results = (args) - result = results[0] - key = len(db.keys('*Notification*')) + 1 - result['time'] = datetime.datetime.now().replace( - second=0, microsecond=0 - ) - notification_details = {'time': str(result['time']), - 'results': str(result['results']), - 'subscriber_info': str(result['subscriber_info']), - 'platform': str(result['platform']) - } - db.hmset('Notification:' + str(key), notification_details) + pass + + +def update_entity_fields(entity, **kwargs): + """ + Function to update an entities fields + :param kwargs + :param entity + """ + keys = kwargs.keys() + for key in keys: + exec("entity.{0} = kwargs['{0}']".format(key)) + return entity + + +class Utility(object): + + def save(self): + """Function for saving new objects""" + db_session.add(self) + db_session.commit()