def decorated_func(*args, **kwargs): try: return func(*args, **kwargs) except HTTPException as e: return jsonify({ 'status': e.code, 'message': HTTP_STATUS_CODES[e.code] }), e.code except DebuggableException as e: app = get_app() app.bot.notify_error(e) e.print_info(app.logger) return jsonify({ 'status': 500, 'message': HTTP_STATUS_CODES[500] }), 500 except Exception as e: # noinspection PyBroadException try: get_app().bot.notify_error(e) except Exception: pass traceback.print_tb(e.__traceback__) print(e, flush=True, file=sys.stderr) return jsonify({ 'status': 500, 'message': HTTP_STATUS_CODES[500] }), 500
def put_subscribe(): class PostData(TypedDict): old_endpoint: str endpoint: str keys: Dict[str, str] if not current_user.is_role('admin'): return api_utils.response_unauthorized() post_data: PostData = request.get_json() old_endpoint = post_data['old_endpoint'] endpoint = post_data['endpoint'] keys = post_data['keys'] app = get_app() user: WebUser = app.user_manager.get_user( UserId(old_endpoint, web_constants.PROVIDER_ID)) # FIXME: Change internal ID of user and keys # FIXME: Change admin subscriptions as well? Need to verify this # FIXME: This code is not really done, but until we can send out daily menus in a consistent manner it'll # have to be this way return api_utils.response_bad_request()
def get_administrators(self) -> 'List[User]': identifier = self.get_identifier() return [ self.get_user(user) for user in get_app().admin_ids if user.provider == identifier ]
def get_login_google(): app = get_app() if google_client is None: init_google_client(app) if google_client is False: return redirect('/login/not_available') google_provider_cfg = get_google_provider_cfg() authorization_endpoint = google_provider_cfg['authorization_endpoint'] state = {} if 'next' in request.args: next_url = request.args.get('next') parsed_next_url = urlparse(next_url) # Prevent changing the scheme or host if parsed_next_url.scheme != '' or parsed_next_url.netloc != '': return abort(400) state['next'] = parsed_next_url.geturl() request_uri = google_client.prepare_request_uri( authorization_endpoint, redirect_uri=url_for('.get_login_google_callback', _external=True), scope=['openid', 'email', 'profile'], state=quote(json.dumps(state)) ) return redirect(request_uri)
def handle_facebook_webhook(): try: app = get_app() data = request.get_json() if data and data['object'] == 'page': for entry in data['entry']: entry: dict if 'messaging' not in entry: continue for event in entry['messaging']: sender = event["sender"]["id"] # recipient = event["recipient"]["id"] user_manager = app.user_manager user: FacebookUser = user_manager.get_user(UserId(sender, fb_constants.PROVIDER_ID), event=event) if not isinstance(user, FacebookUser): # FIXME: Rather have a check that when the user supports "read" markers, we mark as read raise RuntimeError('Expected Facebook User') app.task_executor.submit(_do_handle_facebook_webhook, event, user, app._get_current_object()) return 'ok', 200 print(pprint.pformat(data, indent=2), flush=True) return abort(400) except DebuggableException as e: app = get_app() app.bot.notify_error(e) e.print_info(app.logger) except Exception as e: try: get_app().bot.notify_error(e) except Exception: pass traceback.print_tb(e.__traceback__) print(e, flush=True, file=sys.stderr) return 'ok', 200
def get_locale(self) -> 'Optional[str]': stored_value = super().get_locale() if not stored_value: return get_app( ).bot_interfaces['facebook']['api_interface'].lookup_locale( self._id) return stored_value
def mark_message_seen(self): return get_app( ).bot_interfaces['facebook']['api_interface'].post_send_api({ 'recipient': { 'id': self._id }, 'sender_action': 'mark_seen' })
def initialise(self): import komidabot.facebook.postbacks as postbacks app = get_app() if app.config.get('TESTING') or app.config.get('DISABLED'): return data = postbacks.generate_postback_data(True, app.config.get('PRODUCTION')) app.bot_interfaces['facebook']['api_interface'].post_profile_api(data)
def handle_facebook_verification(): if request.args.get("hub.mode") == "subscribe" and request.args.get("hub.challenge"): if request.args.get('hub.verify_token', '') == get_app().config['VERIFY_TOKEN']: print("Verified") return escape(request.args.get('hub.challenge', '')) else: print("Wrong token") return "Error, wrong validation token" else: return abort(401)
def send_message( self, message: 'messages.Message') -> 'messages.MessageSendResult': result = self.get_message_handler().send_message(self, message) app = get_app() if app.config.get('VERBOSE'): print('Sending message to user {} got result {}'.format( self.id, result), flush=True) return result
def menu_update(context, bot: 'Komidabot'): with context(): if get_app().config.get('DISABLED'): return try: today = datetime.datetime.today().date() week_start = today + datetime.timedelta(days=-today.weekday()) dates = [week_start + datetime.timedelta(days=i) for i in range(today.weekday(), 5)] if today.weekday() >= 3: dates += [week_start + datetime.timedelta(days=7 + i) for i in range(5)] update_menus(dates=dates) except DebuggableException as e: bot.notify_error(e) e.print_info(get_app().logger) except Exception as e: bot.notify_error(e) get_app().logger.exception(e)
def decorated_func(*args, **kwargs): if get_app().config['TESTING']: # Skip validating signature if we're testing return func(*args, **kwargs) advertised = request.headers.get("X-Hub-Signature") if advertised is None: return False advertised = advertised.replace("sha1=", "", 1) data = request.get_data() received = hmac.new( key=get_app().config['APP_SECRET'].encode('raw_unicode_escape'), msg=data, digestmod=hashlib.sha1 ).hexdigest() if hmac.compare_digest(advertised, received): return func(*args, **kwargs) return abort(401)
def post_pass_thread_control(self, data: dict): response = self.session.post(BASE_ENDPOINT + API_VERSION + PASS_THREAD_CONTROL_API, params=self.base_parameters, headers=self.headers_post, data=json.dumps(data)) app = get_app() if app.config.get('VERBOSE'): print('Received {} for request {}'.format(response.status_code, response.request.body), flush=True) print(response.content, flush=True) # response.raise_for_status() # return True return response.status_code == 200
def post_send_api(self, data: dict) -> messages.MessageSendResult: response = self.session.post(BASE_ENDPOINT + API_VERSION + SEND_API, params=self.base_parameters, headers=self.headers_post, data=json.dumps(data)) data = json.loads(response.content) app = get_app() if app.config.get('VERBOSE'): print('Received {} for request {}'.format(response.status_code, response.request.body), flush=True) print(response.content, flush=True) if response.status_code == 200: return messages.MessageSendResult.SUCCESS if 500 <= response.status_code < 600: return messages.MessageSendResult.EXTERNAL_ERROR if response.status_code == 400: code = data['error']['code'] subcode = data['error']['error_subcode'] # https://developers.facebook.com/docs/messenger-platform/reference/send-api/error-codes if code == 1200: # Temporary send message failure. Please try again later. return messages.MessageSendResult.EXTERNAL_ERROR if code == 100: if subcode == 2018001: # No matching user found return messages.MessageSendResult.GONE if code == 10: if subcode == 2018065: # This message is sent outside of allowed window. return messages.MessageSendResult.UNREACHABLE if subcode == 2018108: # This Person Cannot Receive Messages: This person isn't receiving messages from you right now. return messages.MessageSendResult.UNREACHABLE if subcode == 2018278: # TODO: Get official description from FB once available # Sent after March 4th to indicate the subscription message was denied return messages.MessageSendResult.UNREACHABLE if code == 551: if subcode == 1545041: # This person isn't available right now. return messages.MessageSendResult.UNREACHABLE return messages.MessageSendResult.ERROR # TODO: Further specify
def _send_text_message( user_id: users.UserId, message: messages.TextMessage) -> messages.MessageSendResult: data = { 'recipient': { 'id': user_id.id }, 'message': { 'text': message.text }, 'messaging_type': TYPE_REPLY if triggers.SenderAspect in message.trigger else TYPE_SUBSCRIPTION, } return get_app( ).bot_interfaces['facebook']['api_interface'].post_send_api(data)
def dispatch_daily_menus(trigger: triggers.SubscriptionTrigger): from komidabot.subscriptions.daily_menu import CHANNEL_ID as DAILY_MENU_ID # limiter = Limiter(20) # Limit to 20 messages per second date = trigger.date or datetime.datetime.now().date() day = Day(date.isoweekday()) app = get_app() verbose = app.config.get('VERBOSE') if verbose: print('Sending out subscription for {} ({})'.format(date, day.name), flush=True) message = messages.SubscriptionMenuMessage(trigger, date, app.translator) app.subscription_manager.deliver_message(DAILY_MENU_ID, message)
def _send_notification(subscription_information, data) -> messages.MessageSendResult: app = get_app() try: response = webpush( subscription_info=subscription_information, data=json.dumps(data), vapid_private_key=app.config['VAPID_PRIVATE_KEY'], vapid_claims=copy.deepcopy(VAPID_CLAIMS)) if app.config.get('VERBOSE'): print('Received {} for push {}'.format( response.status_code, subscription_information['endpoint']), flush=True) print(response.content, flush=True) return messages.MessageSendResult.SUCCESS except WebPushException as e: response = e.response if app.config.get('VERBOSE'): print('Received {} for push {}'.format( response.status_code, subscription_information['endpoint']), flush=True) print(response.content, flush=True) if 500 <= response.status_code < 600: return messages.MessageSendResult.EXTERNAL_ERROR if response.status_code == 429: # Too many requests, rate limited pass # TODO: Handle rate-limiting if response.status_code == 400: # Invalid request return messages.MessageSendResult.ERROR if response.status_code == 404: # Subscription not found return messages.MessageSendResult.GONE if response.status_code == 410: # Subscription has been removed return messages.MessageSendResult.GONE if response.status_code == 413: # Payload too large return messages.MessageSendResult.ERROR return messages.MessageSendResult.ERROR
def _send_template_message( user_id: users.UserId, message: 'TemplateMessage') -> messages.MessageSendResult: data = { 'recipient': { 'id': user_id.id }, 'message': { 'attachment': { 'type': 'template', 'payload': message.payload } }, 'messaging_type': TYPE_REPLY if triggers.SenderAspect in message.trigger else TYPE_SUBSCRIPTION, } return get_app( ).bot_interfaces['facebook']['api_interface'].post_send_api(data)
class Channel(subscriptions.SubscriptionChannel): def get_subscribed_users(self, /, query: Union[Query, Dict] = None) -> 'List[User]': if not isinstance(query, Query): query = self.get_query_from(query) assert isinstance(query, Query), 'query must be SubscriptionQuery' if query.campus is not None: raise NotImplementedError( 'Cannot query by (day, campus) right now') app = get_app() user_manager = app.user_manager users = models.AppUser.find_subscribed_users_by_day(query.day) return [ user for user in (user_manager.get_user(user) for user in users) if self.user_supported(user) ]
def _send_menu_message( user: users.User, message: messages.MenuMessage) -> messages.MessageSendResult: text = komidabot.menu.get_menu_text(message.menu, message.translator, user.get_locale()) if text is None: return messages.MessageSendResult.ERROR data = { 'recipient': { 'id': user.get_internal_id() }, 'message': { 'text': text }, 'messaging_type': TYPE_REPLY if triggers.SenderAspect in message.trigger else TYPE_SUBSCRIPTION, } return get_app( ).bot_interfaces['facebook']['api_interface'].post_send_api(data)
def get_login_google_callback(): app = get_app() if google_client is None: init_google_client(app) if google_client is False: return redirect('/login/not_available') code = request.args.get('code') state = json.loads(unquote(request.args.get('state'))) next_url = state.get('next', '/') google_provider_cfg = get_google_provider_cfg() token_endpoint = google_provider_cfg['token_endpoint'] token_url, headers, body = google_client.prepare_token_request( token_endpoint, authorization_response=request.url, redirect_url=request.base_url, code=code ) token_response = requests.post( token_url, headers=headers, data=body, auth=(app.config.get('AUTH_GOOGLE_CLIENT_ID'), app.config.get('AUTH_GOOGLE_CLIENT_SECRET')), ) try: google_client.parse_request_body_response(json.dumps(token_response.json())) except InvalidGrantError: # Invalid grant, let's try the login flow again if next_url != '/': return redirect(next_url) return redirect('/login/internal_error') except OAuth2Error: return redirect('/login/internal_error') userinfo_endpoint = google_provider_cfg['userinfo_endpoint'] uri, headers, body = google_client.add_token(userinfo_endpoint) userinfo_response = requests.get(uri, headers=headers, data=body) # You want to make sure their email is verified. # The user authenticated with Google, authorized your # app, and now you've verified their email through Google! if userinfo_response.json().get('email_verified'): unique_id = userinfo_response.json()['sub'] users_email = userinfo_response.json()['email'] picture = userinfo_response.json()['picture'] users_name = userinfo_response.json()['given_name'] else: return redirect('/login/not_verified') user = RegisteredUser.find_by_provider_id('google', unique_id) if not user: if app_config.is_registrations_enabled(): user = RegisteredUser.create('google', unique_id, users_name, users_email, picture) db.session.commit() else: return redirect('/login/login_closed') if not user.is_active: return redirect('/login/not_active') login_user(user) return redirect(next_url)
def daily_menu(context, bot: 'Komidabot'): with context(): if get_app().config.get('DISABLED'): return bot.trigger_received(triggers.SubscriptionTrigger())
def is_admin(self): user_id = self.id return user_id in get_app().admin_ids
def trigger_received(self, trigger: triggers.Trigger): with self.lock: # TODO: Maybe only lock on critical sections? app = get_app() verbose = app.config.get('VERBOSE') if verbose: print('Komidabot received a trigger: {}'.format(type(trigger).__name__), flush=True) print(repr(trigger), flush=True) if isinstance(trigger, triggers.SubscriptionTrigger): dispatch_daily_menus(trigger) return if triggers.AtAdminAspect in trigger: return # Don't process messages targeted at the admin locale = None message_handled = False # XXX: Disabled once more because responses aren't reliably in the language the user expects it to be # if triggers.LocaleAspect in trigger and trigger[triggers.LocaleAspect].confidence > 0.9: # locale = trigger[triggers.LocaleAspect].locale if triggers.SenderAspect in trigger: sender = trigger[triggers.SenderAspect].sender campuses = Campus.get_all() # This ensures that when a user is marked as reachable in case they were unreachable at some point # TODO: We no longer mark users as reachable, need to think over the proper course of action # if sender.mark_reachable(): # db.session.commit() if locale is None: locale = sender.get_locale() if triggers.NewUserAspect in trigger: sender.send_message(messages.TextMessage(trigger, localisation.REPLY_NEW_USER(locale))) msg = localisation.REPLY_INSTRUCTIONS(locale).format( campuses=', '.join([campus.short_name.lower() for campus in campuses if campus.active]) ) sender.send_message(messages.TextMessage(trigger, msg)) sender.set_is_notified_new_site(True) db.session.commit() message_handled = True # TODO: Is this really how we want to handle input? # Maybe we can add an IntentAspect, where the intent is the desired action the bot should take # next? Ex. intents: admin message, get help, get menu, set preference (language, subscriptions) if isinstance(trigger, triggers.TextTrigger): text = trigger.text split = text.lower().split(' ') if sender.is_admin(): if split[0] == 'setup': if app.config.get('PRODUCTION'): sender.send_message(messages.TextMessage(trigger, 'Not running setup on production')) return recreate_db() create_standard_values() import_dump(app.config['DUMP_FILE']) sender.send_message(messages.TextMessage(trigger, 'Setup done')) return elif split[0] == 'update': sender.send_message(messages.TextMessage(trigger, 'Updating menus...')) update_menus(*split[1:]) sender.send_message(messages.TextMessage(trigger, 'Done updating menus...')) return elif split[0] == 'psid': # TODO: Deprecated? sender.send_message(messages.TextMessage(trigger, 'Your ID is {}'.format(sender.id.id))) return # TODO: Allow users to send more manual commands # See also the note prefacing the containing block if not message_handled and split[0] == 'help': msg = localisation.REPLY_INSTRUCTIONS(locale).format( campuses=', '.join([campus.short_name.lower() for campus in campuses if campus.active]) ) sender.send_message(messages.TextMessage(trigger, msg)) return if app.config.get('COVID19_DISABLED'): sender.send_message(messages.TextMessage(trigger, localisation.COVID19_UNAVAILABLE(locale))) return requested_dates = [] default_date = False if triggers.DatetimeAspect in trigger: date_times = trigger[triggers.DatetimeAspect] # TODO: Date parsing needs improving requested_dates, invalid_date = nlp_dates.extract_days(date_times) if invalid_date: sender.send_message(messages.TextMessage(trigger, localisation.REPLY_INVALID_DATE(locale))) return if len(requested_dates) > 1: sender.send_message(messages.TextMessage(trigger, localisation.REPLY_TOO_MANY_DAYS(locale))) return elif len(requested_dates) == 1: date = requested_dates[0] else: default_date = True date = datetime.datetime.now().date() # TODO: How about getting the menu for the next day after a certain time of day? # Only if we're returning the default day day = Day(date.isoweekday()) if day == Day.SATURDAY or day == Day.SUNDAY: sender.send_message(messages.TextMessage(trigger, localisation.REPLY_WEEKEND(locale))) return requested_campuses = [] default_campus = False if isinstance(trigger, triggers.TextTrigger): text = trigger.text.lower() for campus in campuses: if not campus.active: continue for kw in campus.get_keywords(): if text.count(kw) > 0: requested_campuses.append(campus) break # Prevent the same campus from being added multiple times if len(requested_campuses) > 1: sender.send_message(messages.TextMessage(trigger, localisation.REPLY_TOO_MANY_CAMPUSES(locale))) return elif len(requested_campuses) == 1: campus = requested_campuses[0] else: default_campus = True campus = sender.get_campus_for_day(date) if campus is None: # User has no campus for the specified day campus = Campus.get_by_short_name('cmi') if not campus.active: sender.send_message(messages.TextMessage(trigger, localisation.REPLY_CAMPUS_INACTIVE(locale) .format(campus=campus.name))) return if message_handled and default_campus and default_date: if isinstance(trigger, triggers.TextTrigger): for word in ['menu', 'lunch', 'eten']: if word in trigger.text: break else: return else: return # if default_date and default_campus: # if isinstance(trigger, triggers.TextTrigger): # sender.send_message(messages.TextMessage(trigger, # localisation.REPLY_NO_DATE_OR_CAMPUS(locale))) # msg = localisation.REPLY_INSTRUCTIONS(locale).format( # campuses=', '.join([campus.short_name for campus in campuses]) # ) # sender.send_message(messages.TextMessage(trigger, msg)) # return # # # User did not send a text message, so we'll continue anyway if not default_campus: sender.set_campus_for_day(campus, date) db.session.commit() if sender.get_is_notified_new_site() is False and sender.is_feature_active('new_site_notifications'): if sender.send_message(messages.TextMessage(trigger, localisation.MESSAGE_NEW_SITE(locale))) \ == messages.MessageSendResult.SUCCESS: sender.set_is_notified_new_site(True) db.session.commit() closed = ClosingDays.find_is_closed(campus, date) if closed: translation = closed.translatable.get_translation(locale, app.translator) sender.send_message(messages.TextMessage(trigger, localisation.REPLY_CAMPUS_CLOSED(locale) .format(campus=campus.name, date=str(date), reason=translation.translation))) return # menu = komidabot.menu.prepare_menu_text(campus, date, app.translator, locale) menu = Menu.get_menu(campus, date) if menu is None: sender.send_message(messages.TextMessage(trigger, localisation.REPLY_NO_MENU(locale) .format(campus=campus.name, date=str(date)))) else: # sender.send_message(messages.TextMessage(trigger, menu)) sender.send_message(messages.MenuMessage(trigger, menu, app.translator))
def _do_handle_facebook_webhook(event, user: FacebookUser, app): time.sleep(0.1) # Yield with app.app_context(): trigger = triggers.Trigger(aspects=[triggers.SenderAspect(user)]) needs_commit = False if user.get_db_user() is None: trigger.add_aspect(triggers.NewUserAspect()) print('Adding new user to the database {}'.format(user.id), flush=True) user.add_to_db() needs_commit = True bot: Bot = app.bot locale = user.get_locale() try: print('Handling message in new path for {}'.format(user.id), flush=True) # print(pprint.pformat(event, indent=2), flush=True) if 'message' in event: message = event['message'] user.mark_message_seen() # print(pprint.pformat(message, indent=2), flush=True) # TODO: Is this the preferred way to differentiate inputs? # What about messages that include attachments or other things? # TODO: This now works with aspects rather than inheritance, so in theory this could be done if 'text' in message: message_text = message['text'] trigger = triggers.TextTrigger.extend(trigger, message_text) if '@admin' in message_text: trigger.add_aspect(triggers.AtAdminAspect()) if 'nlp' in message: if 'detected_locales' in message['nlp'] and len(message['nlp']['detected_locales']) > 0: # Get the locale that has the highest confidence locale_entry = max(message['nlp']['detected_locales'], key=lambda x: x['confidence']) trigger.add_aspect(triggers.LocaleAspect(locale_entry['locale'], locale_entry['confidence'])) locale = locale_entry['locale'] if 'entities' in message['nlp']: entities = message['nlp']['entities'] if 'datetime' in entities: for entity in entities['datetime']: if 'value' in entity: # Specific date given, vs. date range # FIXME: Do we want to add range datetimes? trigger.add_aspect(triggers.DatetimeAspect(entity['value'], entity['grain'])) if user.is_admin() and message_text == 'sub': # Simulate subscription instead trigger = triggers.SubscriptionTrigger.extend(trigger) if app.config.get('DISABLED'): if not user.is_admin(): if triggers.AtAdminAspect not in trigger: user.send_message(TextMessage(trigger, localisation.DOWN_FOR_MAINTENANCE(locale))) return # sender_obj.send_text_message('Note: The bot is currently disabled') elif 'postback' in event: # print(pprint.pformat(event, indent=2), flush=True) user.mark_message_seen() if app.config.get('DISABLED'): if not user.is_admin(): if triggers.AtAdminAspect not in trigger: user.send_message(TextMessage(trigger, localisation.DOWN_FOR_MAINTENANCE(locale))) return postback: dict = event['postback'] payload = postback.get('payload') try: data: dict = json.loads(payload) except json.JSONDecodeError: raise trigger = triggers.PostbackTrigger.extend(trigger, data['name'], data['args'], data['kwargs']) # TODO: This will be cleaner if we work with intents (see komidabot.py) postback_obj = postbacks.lookup_postback(trigger.name) if postback_obj: trigger = postback_obj.call_postback(trigger, *trigger.args, **trigger.kwargs) if trigger is None: return # Indicates the trigger was processed # TODO: Again, this will be cleaner if we work with intents (see komidabot.py) else: get_app().bot.message_admins(TextMessage(triggers.Trigger(), 'Unknown postback type received!')) user.send_message(TextMessage(trigger, localisation.ERROR_POSTBACK(locale))) return elif 'request_thread_control' in event: request_thread_control: dict = event['request_thread_control'] requested_owner_app_id = request_thread_control['requested_owner_app_id'] metadata = request_thread_control['metadata'] if requested_owner_app_id == 263902037430900: # Page Inbox app id # We'll allow the request app.bot_interfaces['facebook']['api_interface'].post_pass_thread_control({ 'recipient': {'id': user.id.id}, 'target_app_id': requested_owner_app_id, 'metadata': metadata }) return elif 'pass_thread_control' in event: return # Right now we don't need to handle this one else: print(pprint.pformat(event, indent=2), flush=True) get_app().bot.message_admins(TextMessage(triggers.Trigger(), 'Unknown message type received!')) return bot.trigger_received(trigger) if needs_commit: db.session.commit() except DebuggableException as e: app = get_app() app.bot.notify_error(e) e.print_info(app.logger) except Exception as e: try: app.logger.error('Error while handling event:\n{}'.format(pprint.pformat(event, indent=2))) get_app().bot.notify_error(e) except Exception: pass user.send_message(TextMessage(trigger, localisation.INTERNAL_ERROR(locale))) app.logger.exception(e)
def handle_web_push_subscription(): try: app = get_app() data = request.get_json() print(pprint.pformat(data, indent=2), flush=True) if data and 'subscription' in data: subscription = data['subscription'] if 'endpoint' not in subscription: return abort(400) if 'keys' not in subscription: return abort(400) endpoint = subscription['endpoint'] keys = subscription['keys'] needs_commit = False user_manager = app.user_manager user: WebUser = user_manager.get_user(UserId(endpoint, web_constants.PROVIDER_ID)) if user.get_db_user() is None: print('Adding new subscription to the database {}'.format(user.id), flush=True) user.add_to_db() user.set_data({ 'keys': keys }) needs_commit = True if 'days' in subscription: days = subscription['days'] if len(days) != 5: return abort(400) for i in range(5): if i >= 5: break day = models.week_days[i] campus_id = days[i] campus = user.get_campus_for_day(day) if campus_id is None: if user.disable_subscription_for_day(day): needs_commit = True elif campus is None or campus.id != campus_id: campus = models.Campus.get_by_id(campus_id) if campus is None: continue user.set_campus_for_day(campus, day) needs_commit = True if needs_commit: db.session.commit() return '{}', 200 return abort(400) except DebuggableException as e: app = get_app() app.bot.notify_error(e) e.print_info(app.logger) return abort(500) except Exception as e: try: get_app().bot.notify_error(e) except Exception: pass traceback.print_tb(e.__traceback__) print(e, flush=True, file=sys.stderr) return abort(500)