from flask import Flask, request, json, Blueprint, current_app, render_template, jsonify, request, g from ownchatbot.db import get_db, clear_fulfilled_rewards from ownchatbot.owncast_com import send_chat, send_private_chat from ownchatbot.user_handlers import add_user_to_points, change_name, get_users_points, remove_duplicates, get_email_code, set_email_code, award_chat_points, user_in_points, get_all_users_with_user_id from ownchatbot.bot_messages import do_reward, help_message, porps from ownchatbot.reward_handlers import all_active_goals, all_active_votes, all_active_rewards, save_alerts from ownchatbot.donation_handlers import accept_donation, accept_kofi_sub import json import random import hmac import hashlib ocb = Blueprint('webhooks', __name__) def format(rawjson): # Make data legible formatted_data = json.dumps(rawjson, indent=4) return formatted_data @ocb.route('/ocbHook', methods=['POST']) def chat_hook(): prefix = current_app.config['PREFIX'] data = request.json db = get_db() if data['type'] in ['CHAT', 'NAME_CHANGED', 'USER_JOINED']: # Check if the viewer is in the chatbot database user_id = data['eventData']['user']['id'] authed = data['eventData']['user']['authenticated'] display_name = data['eventData']['user']['displayName'] if add_user_to_points(db, user_id, display_name, authed): current_app.logger.debug(f'Added/updated {user_id} database.') current_app.logger.debug(f'{display_name}/{user_id}: {data["eventData"]}') # Log all chat messages if data['type'] == 'STREAM_STARTED': current_app.logger.info('Starting a new stream.') if clear_fulfilled_rewards(): current_app.logger.info('Cleared fulfilled rewards.') if data['type'] == 'USER_JOINED': # Do username house cleaning when a viewer joins if data['eventData']['user']['authenticated']: remove_duplicates(db, user_id, display_name) elif data['type'] == 'FEDIVERSE_ENGAGEMENT_FOLLOW': alerts_dict = current_app.config['ALERTS'] data = request.json current_app.logger.debug(f'\n\n_______________\n/followHook triggered!\n_______________') alerts_dict['follower'] = data['eventData']['name'] save_alerts(alerts_dict) return jsonify({'status': 'success'}), 200 elif data['type'] == 'NAME_CHANGE': user_id = data['eventData']['user']['id'] new_name = data['eventData']['newName'] change_name(db, user_id, new_name) if data['eventData']['user']['authenticated']: remove_duplicates(db, user_id, new_name) elif data['type'] == 'CHAT': # If a chat message, sort out what command it is user_id = data['eventData']['user']['id'] display_name = data['eventData']['user']['displayName'] current_app.logger.info(f'{display_name}/{user_id}: {data["eventData"]["rawBody"]}') # Log all chat messages lowercase_msg = data['eventData']['rawBody'].lower() # Convert body to lower case to match reward case if lowercase_msg.startswith(f'{prefix}help'): # Send the help message help_message(user_id) elif lowercase_msg.startswith(f'{prefix}points'): # Get the viewer's current points points = get_users_points(db, user_id) if points is None: send_private_chat(user_id, f'{display_name}, couldn\'t get your points, for some highly technical reason.') else: send_private_chat(user_id, f'{display_name}, you have {porps(points)}.') elif lowercase_msg.startswith(f'{prefix}reg_mail'): # Generate a code to verify users account for email registration if current_app.config['KOFI_SETTINGS']['integration'] or current_app.config['GB_SETTINGS']['integration']: mail_reg_code = get_email_code(db, user_id) if mail_reg_code: # If the viewer already has a code waiting send_private_chat(user_id, f'{display_name}, your code is {mail_reg_code}. Enter it into the form on the Stream Rewards Info page, with your email address, to enable donation perks!') else: # if not mail_reg_code = random.randint(100000, 999999) if set_email_code(db, user_id, mail_reg_code): send_private_chat(user_id, f'{display_name}, your code is {mail_reg_code}. Enter it into the form on the Stream Rewards Info page, with your email address, to enable donation perks!') else: send_chat(f'{display_name}, donation integration is not enabled on this stream.') elif lowercase_msg.startswith(f'{prefix}rewards'): # Send rewards list if current_app.config['REWARDS']: rewards_msg = f'Currently active rewards:' for reward, details in current_app.config['REWARDS'].items(): if details.get('categories'): if not (set(details['categories']) & set(current_app.config['ACTIVE_CAT'])): # If there are no common categories, continue continue if 'type' in details and details['type'] == 'goal': rewards_msg = f'{rewards_msg}
* {prefix}{reward} goal at {details["target"]} contributed {porps(details["target"])}.' elif 'type' in details and details['type'] == 'vote': rewards_msg = f'{rewards_msg}
* {prefix}{reward} vote for {details["price"]} {porps(details["price"])}.' else: rewards_msg = f'{rewards_msg}
* {prefix}{reward} for {details["price"]} {porps(details["price"])}.' if 'info' in details: rewards_msg = f'{rewards_msg}
{details["info"]}' else: rewards_msg = f'{rewards_msg}' else: rewards_msg = 'There are currently no active rewards.' send_private_chat(user_id, rewards_msg) elif lowercase_msg.startswith(f'{prefix}'): # Send to handle rewards do_reward(lowercase_msg, user_id) return data @ocb.route('/kofiHook', methods=["POST"]) def kofi_hook(): current_app.logger.info(f'----------------------------------------------------------------------------') current_app.logger.info(f'Kofi request') if request.content_type == 'application/x-www-form-urlencoded': raw_data = request.form.get('data') # Get the kofi data if current_app.config['KOFI_SETTINGS']['integration']: if raw_data: raw_data = json.loads(raw_data) is_authed = raw_data['verification_token'] if is_authed == current_app.config['KOFI_SETTINGS']['token']: type = raw_data['type'] is_public = raw_data['is_public'] new_sub = raw_data['is_first_subscription_payment'] message = raw_data['message'] shop_items = raw_data['shop_items'] from_name = raw_data['from_name'] email = raw_data['email'] amount = raw_data['amount'] sub_payment = raw_data['is_subscription_payment'] first_sub = raw_data['is_first_subscription_payment'] tier_name = raw_data['tier_name'] if type == 'Shop Order': current_app.logger.info(f'{from_name} purchased {format(shop_items)}\nMessage: {message}\n') if type == 'Donation': donation_info = [is_public, from_name, email, amount, message] donation_points = current_app.config['KOFI_SETTINGS']['donation_points'] if accept_donation(donation_info, donation_points, 'Kofi'): current_app.logger.info('Donation processed.') if type == 'Subscription': if current_app.config['KOFI_SETTINGS']['subs']: # Check that subscriptions are enabled if first_sub: if tier_name: current_app.logger.info(f'{from_name} <{email}> subscribed as a {tier_name} tier member.') else: current_app.logger.info(f'{from_name} <{email}> subscribed.') else: if tier_name: current_app.logger.info(f'{from_name} <{email}> renewed their {tier_name} tier membership.') else: current_app.logger.info(f'{from_name} <{email}> renewed their membership.') sub_info = [is_public, from_name, email, amount, message, first_sub, tier_name] sub_points = current_app.config['KOFI_SETTINGS']['sub_points'] if accept_kofi_sub(sub_info, sub_points): current_app.logger.info('Subscription processed.') else: current_app.logger.info(f'Kofi membership received, but subscriptions are not enabled. Doing nothing.') return jsonify({'status': 'success'}), 200 else: current_app.logger.info(f'Token invalid. Rejecting.') return jsonify({'status': 'unauthorized'}), 401 else: return jsonify({'status': 'Failed. No data'}), 400 return jsonify({'status': 'success'}), 200 else: current_app.logger.error(f'Kofi donation recieved, but Kofi integration is turned off. Doing nothing.') return jsonify({'status': 'Failed. Not currently accepting Kofi donations.'}), 400 else: return jsonify({'status': 'Failed. Invalid content type'}), 400 def sign_payload(payload, secret): # For TESTING purposes test_payload = hmac.new( secret.encode(), payload.encode(), hashlib.sha256 ).hexdigest() current_app.logger.info(f'\n\nTest encoded payload output: {test_payload}\n\n') return test_payload # Not currently in use by GB, but it was. Maybe in the future? # Leaving it here just in case. def verify_gbhook_signature(payload, signature, secret): current_app.logger.info(f'\n\nRecieved Signature: {signature}\n\n') expected_signature = hmac.new( secret.encode(), payload.encode(), hashlib.sha256 ).hexdigest() do_sig_check = hmac.compare_digest(signature, expected_signature) current_app.logger.debug(f'\n\nExpected Signature: {expected_signature}\nResult: {do_sig_check}\n\n') return do_sig_check @ocb.route('/gbHook', methods=['POST']) def gb_hook(): current_app.logger.info(f'----------------------------------------------------------------------------') current_app.logger.info(f'GiveButter request') headers = request.headers current_app.logger.debug('Headers:') for header, value in headers.items(): current_app.logger.debug(f'> {header}: {value}') signature = request.headers.get('Signature') gb_secret = current_app.config['GB_SETTINGS']['secret'] raw_data = request.get_data(as_text=True) current_app.logger.debug(f'\n\n{raw_data}\n\n') event = request.json if signature == gb_secret: if current_app.config['GB_SETTINGS']['integration']: try: event_type = event['event'] if event_type == 'transaction.succeeded': data = event['data'] custom_fields = data["custom_fields"] if custom_fields: for field in custom_fields: # Get anonymous donation custom field, if it's there' if field['title'].lower() == 'anonymous': if field['value'].lower() == "yes": is_public = False current_app.logger.info(f'Donation is anonymous.') else: is_public = True current_app.logger.info(f'Donation is not anonymous.') else: # If it's not there, set to anonymous is_public = False current_app.logger.info(f'No anoymous donation custom field found. Assuming anonymous') else: # No custom fields found, set to anonymous is_public = False current_app.logger.info(f'No custom fields found. Assuming anonymous') from_name = f'{data["first_name"]} {data["last_name"]}' email = data['email'] amount = data['amount'] current_app.logger.debug(f'\nFrom: {from_name}\nEmail: {email}\nAmount: {amount}\nAnonymous: {is_public}\n\n') donation_info = [is_public, from_name, email, amount, ''] donation_points = current_app.config['GB_SETTINGS']['donation_points'] points_for_donations = current_app.config['GB_SETTINGS']['donations'] if points_for_donations: # Are we giving points for donations? if accept_donation(donation_info, donation_points, 'GiveButter'): current_app.logger.info(f'Donation processed.') else: current_app.logger.info(f'Points for donations is disabled. Doing nothing.') else: current_app.logger.info(f'Unhandled event type: {event_type}') except Exception as pgberror: current_app.logger.error(f'General exception processing gbhook: {pgberror}') else: current_app.logger.error(f'GiveButter donation recieved, but GiveButter integration is turned off. Doing nothing.') return jsonify({'status': 'Failed. Not currently accepting GiveButter donations.'}), 400 return jsonify({'status': 'Success'}), 200 # If signature matched else: return jsonify({'status': 'Signature invalid'}), 401 # If signature didn't match @ocb.route('/checkFollows') # Polled by follower.html template to check for new followers def check_follows(): alerts_dict = current_app.config['ALERTS'] follower = {'name': alerts_dict['follower']} if follower['name']: current_app.logger.info(f'New follower: \"{follower["name"]}\"') alerts_dict['follower'] = '' save_alerts(alerts_dict) return jsonify(follower) else: current_app.logger.debug(f'No new followers') return jsonify(None) @ocb.route('/checkGoals') # Polled by rgoal.html template to check for if a goal has been passed def check_goals(): alerts_dict = current_app.config['ALERTS'] rgoals = {'name': alerts_dict['g_name']} if rgoals['name']: current_app.logger.debug(rgoals) alerts_dict['g_name'] = '' alerts_dict['g_reward'] = '' save_alerts(alerts_dict) return jsonify(rgoals) else: current_app.logger.debug(f'No new goals reached') return jsonify(None) @ocb.route('/checkMilestones') # Polled by rmilestone.html template to check if a milestone has been passed def check_milestones(): alerts_dict = current_app.config['ALERTS'] rmilestones = {'name': alerts_dict['m_name']} if rmilestones['name']: current_app.logger.info(rmilestones) alerts_dict['m_name'] = '' alerts_dict['m_reward'] = '' save_alerts(alerts_dict) return jsonify(rmilestones) else: current_app.logger.debug(f'No new milestones passed') return jsonify(None) @ocb.route('/checkGBs') # Polled by rgbdonation.html template to check for new givebutter donations def check_gbs(): alerts_dict = current_app.config['ALERTS'] rgbs = {'name': alerts_dict['gb_name']} if rgbs['name']: current_app.logger.debug(rgbs) alerts_dict['gb_name'] = '' save_alerts(alerts_dict) return jsonify(rgbs) else: current_app.logger.debug(f'No GiveButter donation') return jsonify(None)