def purchase_api(): '''process the given tx_hash and return the payed-for goods''' #TODO: at some point we should try to listen in on incoming tx_hashes # for our account(s). this should hasten the process of reedeming offers. payload = request.get_json(silent=True) try: user_id = extract_header(request) tx_hash = payload.get('tx_hash', None) if None in (user_id, tx_hash): raise InvalidUsage('invalid param') except Exception as e: print('exception: %s' % e) raise InvalidUsage('bad-request') try: # process the tx_hash, provided its not already being processed by another server lock = redis_lock.Lock(app.redis, 'redeem:%s' % tx_hash) if lock.acquire(blocking=False): success, goods = process_order(user_id, tx_hash) if not success: raise InvalidUsage('cant redeem with tx_hash:%s' % tx_hash) increment_metric('offers_redeemed') return jsonify(status='ok', goods=goods) else: return jsonify(status='error', reason='already processing tx_hash') finally: lock.release()
def send_p2p_push(user_id, amount, tx_dict): """sends a push to the given userid to inform of p2p tx""" push_id = generate_push_id() push_type = 'engage-recent' from kinappserver.models import get_user_push_data os_type, token, push_env = get_user_push_data(user_id) if token: if os_type == OS_ANDROID: increment_metric('p2p-tx-push-gcm') print('sending p2p-tx push message to GCM user %s' % user_id) push_send_gcm( token, gcm_payload(push_type, push_id, { 'title': '', 'body': "A friend just sent you %sKIN!" % amount }), push_env) else: increment_metric('p2p-tx-push-ios') print('sending p2p-tx push message to APNS user %s' % user_id) push_send_apns( token, apns_payload("", "A friend just sent you %sKIN!" % amount, 'p2p_received', push_id, 'default', {'tx': tx_dict}), push_env) else: print('not sending p2p-tx push to user_id %s: no token' % user_id) return
def quest_answers(): '''receive the results for a tasks and pay the user for them''' payload = request.get_json(silent=True) try: user_id = extract_header(request) task_id = payload.get('id', None) address = payload.get('address', None) results = payload.get('results', None) send_push = payload.get('send_push', True) if None in (user_id, task_id, address, results): raise InvalidUsage('bad-request') #TODO more input checks here except Exception as e: raise InvalidUsage('bad-request') if not store_task_results(user_id, task_id, results): # should never happen: the client sent the results too soon print('rejecting user %s task %s results' % (user_id, task_id)) increment_metric('premature_task_results') return jsonify(status='error', reason='cooldown_enforced'), status.HTTP_400_BAD_REQUEST try: memo = utils.KINIT_MEMO_PREFIX + str(uuid4( ))[:utils. ORDER_ID_LENGTH] # generate a memo string and send it to the client reward_store_and_push(address, task_id, send_push, user_id, memo) except Exception as e: print('exception: %s' % e) print('failed to reward task %s at address %s' % (task_id, address)) increment_metric('task_completed') return jsonify(status='ok', memo=str(memo))
def send_please_upgrade_push_2_inner(user_id): """sends a push to the given userid to please upgrade""" push_id = generate_push_id() push_type = 'please_upgrade' from kinappserver.models import get_user_push_data os_type, token, push_env = get_user_push_data(user_id) if token: if os_type == OS_ANDROID: increment_metric('pleaseupgrade-android') print('sending please-upgrade push message to GCM user %s' % user_id) push_send_gcm( token, gcm_payload( 'engage-recent', push_id, { 'title': '', 'body': "Your current version of Kinit is no longer supported. Please download the newest version from Google Play" }), push_env) else: increment_metric('pleaseupgrade-ios') print('sending please-upgrade push message to APNS user %s' % user_id) push_send_apns( token, apns_payload( "", "Your current version of Kinit is no longer supported. Please download the newest version from the App Store", push_type, push_id), push_env) else: print('not sending please-upgrade push to user_id %s: no token' % user_id) return
def send_kin_with_payment_service(public_address, amount, memo=None): """send kins to an address using the payment service""" # sanity: if public_address in (None, ''): log.error('cant send kin to address: %s' % public_address) return False, None if amount is None or amount < 1: log.error('cant send kin amount: %s' % amount) return False, None print('sending kin to address: %s' % public_address) headers = { 'X-REQUEST-ID': str(random.randint(1, 1000000)) } # doesn't actually matter payment_payload = { 'id': memo, 'amount': amount, 'app_id': 'kit', 'recipient_address': public_address, 'callback': "%s/payments/callback" % config.API_SERVER_URL } try: res = requests.post('%s/payments' % config.PAYMENT_SERVICE_URL, headers=headers, json=payment_payload) res.raise_for_status() except Exception as e: increment_metric('send_kin_error') print( 'caught exception sending %s kin to address %s using the payment service' % (amount, public_address)) print(e)
def register_api(): ''' register a user to the system called once by every client until 200OK is received from the server. the payload may contain a optional push token. ''' payload = request.get_json(silent=True) try: # add redis lock here? user_id = payload.get('user_id', None) os = payload.get('os', None) device_model = payload.get('device_model', None) token = payload.get('token', None) time_zone = payload.get('time_zone', None) device_id = payload.get('device_id', None) app_ver = payload.get('app_ver', None) #TODO more input check on the values if None in ( user_id, os, device_model, time_zone, app_ver ): # token is optional, device-id is required but may be None raise InvalidUsage('bad-request') if os not in ('iOS', 'android'): raise InvalidUsage('bad-request') user_id = UUID(user_id) # throws exception on invalid uuid except Exception as e: raise InvalidUsage('bad-request') else: try: create_user(user_id, os, device_model, token, time_zone, device_id, app_ver) except InvalidUsage as e: raise InvalidUsage('duplicate-userid') else: print('created user with user_id %s' % (user_id)) increment_metric('user_registered') return jsonify(status='ok')
def release_unclaimed_api(): '''endpoint used to get the current balance''' if not config.DEBUG: limit_to_local_host() released = release_unclaimed_goods() increment_metric('unclaimed_released', released) return jsonify(status='ok', released=released)
def release_unclaimed_api(): """endpoint used to release goods that were booked but never redeemed""" if not config.DEBUG: limit_to_localhost() released = release_unclaimed_goods() increment_metric('unclaimed_released', released) return jsonify(status='ok', released=released)
def process_order(user_id, tx_hash): '''release the goods to the user, provided that they've been payed for''' # extract the tx_data from the blockchain goods = [] res, tx_data = stellar.extract_tx_payment_data(tx_hash) if not res: print('could not extract tx_data for tx_hash: %s' % tx_hash) return False, None # get the order from the db using the memo in the tx order = get_order_by_order_id(tx_data['memo']) if not order: print('cant match tx order_id to any active orders') return False, None # ensure the tx matches the order if tx_data['to_address'] != order.address: print('tx address does not match offer address') return False, None if int(tx_data['amount']) != order.kin_amount: print('tx amount does not match offer amount') return False, None # tx matched! docuemnt the tx in the db with a tx object create_tx(tx_hash, user_id, order.address, True, order.kin_amount, {'offer_id': str(order.offer_id)}) # prevent doomsday scenario: ensure that the user's expected balance matches his actual balance expected_balance = expected_user_kin_balance(user_id) actual_balance = get_current_user_kin_balance(user_id) if actual_balance is None: print('doomsday: cant get user balance, so skipping check') increment_metric('doomsday-skipped') elif abs(expected_balance - actual_balance) > 100: print( 'doomsday detected and rejected: userid: %s, expected %s, actual: %s' % (user_id, expected_balance, actual_balance)) increment_metric('doomsday-rejected') return False, None else: print( 'doomsday checked and passed: userid: %s, expected %s, actual: %s' % (user_id, expected_balance, actual_balance)) # get the allocated goods res, good = finalize_good(order.order_id, tx_hash) if res: goods.append(good) else: print('failed to finalize the good for tx_hash: %s' % tx_hash) # delete the order try: delete_order(order.order_id) except Exception as e: print('failed to delete order %s' % order.order_id) return True, goods
def extract_tx_payment_data(tx_hash): """ensures that the given tx_hash is a valid payment tx, and return a dict with the memo, amount and to_address""" if tx_hash is None: raise InvalidUsage('invlid params') # get the tx_hash data. this might take a second, # so retry while 'Resource Missing' is recevied count = 0 tx_data = None while (count < config.STELLAR_TIMEOUT_SEC): try: tx_data = app.kin_sdk.get_transaction_data(tx_hash) except kin.ResourceNotFoundError as e: count = count + 1 sleep(1) else: break if tx_data is None: print('could not get tx_data for tx_hash: %s. waited %s seconds' % (tx_hash, count)) increment_metric('tx_data_timeout') return False, {} if len(tx_data.operations) != 1: print('too many ops') return False, {} # get the first (and only) op: op = tx_data.operations[0] # verify op type if op['type'] != 'payment': print('unexpected type: %s' % op['type']) return False, {} # verify asset params if op['asset_code'] != ASSET_NAME and op['asset_issuer'] != \ config.STELLAR_KIN_ISSUER_ADDRESS and op['asset_type'] != 'credit_alphanum4': print('unexpected asset-code/issuer/asset_type') return False, {} # verify memo type if tx_data['memo_type'] != 'text': print('unexpected memo type') return False, {} # assemble the result dict data = {} data['memo'] = tx_data.get('memo', None) data['amount'] = op.get('amount', None) data['to_address'] = op.get('to_address', None) return True, data
def create_account(public_address, initial_xlm_amount): """create an account for the given public address""" #TODO all repeating logic? print('creating account with balance:%s' % initial_xlm_amount) try: return app.kin_sdk.create_account(public_address, starting_balance=initial_xlm_amount) except Exception as e: increment_metric('create_account_error') print('caught exception creating account for address %s' % (public_address)) print(e)
def compensate_user_api(): """internal endpoint used to manually compensate users for missing txs""" if not config.DEBUG: limit_to_localhost() payload = request.get_json(silent=True) user_id = payload.get('user_id', None) kin_amount = int(payload.get('kin_amount', None)) task_id = payload.get('task_id', None) memo = utils.generate_memo(is_manual=True) if None in (user_id, kin_amount, task_id): raise InvalidUsage('invalid param') public_address = get_address_by_userid(user_id) if not public_address: log.error('cant compensate user %s - no public address' % user_id) return jsonify(status='error', reason='no_public_address') user_tx_task_ids = [ tx.tx_info.get('task_id', '-1') for tx in list_user_transactions(user_id) ] if task_id in user_tx_task_ids: print( 'refusing to compensate user %s for task %s - already received funds!' % (user_id, task_id)) return jsonify(status='error', reason='already_compensated') print('calling send_kin: %s, %s' % (public_address, kin_amount)) try: tx_hash = send_kin(public_address, kin_amount, memo) create_tx(tx_hash, user_id, public_address, False, kin_amount, { 'task_id': task_id, 'memo': memo }) except Exception as e: print('error attempting to compensate user %s for task %s' % (user_id, task_id)) print(e) return jsonify(status='error', reason='internal_error') else: print('compensated user %s with %s kins for task_id %s' % (user_id, kin_amount, task_id)) # also send push to the user task_title = get_task_details(task_id)['title'] send_compensated_push(user_id, kin_amount, task_title) increment_metric('manual-compensation') return jsonify(status='ok', tx_hash=tx_hash)
def onboard_user(): '''creates a wallet for the user and deposits some xlms there''' # input sanity try: user_id = extract_header(request) public_address = request.get_json(silent=True).get( 'public_address', None) if None in (public_address, user_id): raise InvalidUsage('bad-request') except Exception as e: raise InvalidUsage('bad-request') # ensure the user exists but does not have an account: onboarded = is_onboarded(user_id) if onboarded is True: raise InvalidUsage('user already has an account') elif onboarded is None: raise InvalidUsage('no such user exists') else: # create an account, provided none is already being created lock = redis_lock.Lock(app.redis, 'address:%s' % public_address) if lock.acquire(blocking=False): try: print('creating account with address %s and amount %s' % (public_address, config.STELLAR_INITIAL_ACCOUNT_BALANCE)) tx_id = create_account(public_address, config.STELLAR_INITIAL_ACCOUNT_BALANCE) if (tx_id): set_onboarded(user_id, True) else: raise InternalError('failed to create account at %s' % public_address) except Exception as e: print('exception trying to create account:%s' % e) raise InternalError('unable to create account') else: print('created account %s with txid %s' % (public_address, tx_id)) finally: lock.release() else: raise InvalidUsage( 'already creating account for user_id: %s and address: %s' % (user_id, public_address)) increment_metric('user_onboarded') return jsonify(status='ok')
def send_country_not_supported(user_id): """sends a push to the given userid to tell them their country isnt supported""" # add cooldown with redis to this function. if not (app.redis.set('countrynot:%s' % str(user_id), '', ex=COUNTRY_NOT_SUPPORTED_PUSH_COOLDOWN_SECONDS, nx=True)): # returns None if already exists return push_id = generate_push_id() push_type = 'country_not_supported' from kinappserver.models import get_user_push_data os_type, token, push_env = get_user_push_data(user_id) if token: if os_type == OS_ANDROID: increment_metric('country_not_supported-android') # return # not supported yet print('sending country_not_supported push message to GCM user %s' % user_id) push_send_gcm( token, gcm_payload( push_type, push_id, { 'title': 'Oh no!', 'body': "Kinit is currently not available in your country. We are continuing to grow, so check back again soon." }), push_env) else: increment_metric('country_not_supported-ios') print( 'sending country_not_supported push message to APNS user %s' % user_id) push_send_apns( token, apns_payload( "Oh no!", "Kinit is currently not available in your country. We are continuing to grow, so check back again soon.", push_type, push_id), push_env) else: print( 'not sending country_not_supported push to user_id %s: no token' % user_id) return
def book_offer_api(): '''books an offer by a user''' payload = request.get_json(silent=True) try: user_id = extract_header(request) offer_id = payload.get('id', None) if None in (user_id, offer_id): raise InvalidUsage('invalid payload') except Exception as e: raise InvalidUsage('bad-request') order_id, error_code = create_order(user_id, offer_id) if order_id: increment_metric('offers_booked') return jsonify(status='ok', order_id=order_id) else: return jsonify( status='error', reason=errors_to_string(error_code)), status.HTTP_400_BAD_REQUEST
def skip_wait_endpoint(): """sets the next task's timestamp to the past for the given user""" limit_to_acl() limit_to_password() try: payload = request.get_json(silent=True) user_id = payload.get('user_id', None) cat_id = payload.get('cat_id', None) next_ts = payload.get('next_ts', 1) # optional if user_id is None: raise InvalidUsage('bad-request') except Exception as e: print(e) raise InvalidUsage('bad-request') else: store_next_task_results_ts(user_id, 'fake_task_id', next_ts, cat_id) increment_metric('skip-wait') return jsonify(status='ok')
def send_please_upgrade_push(user_id): """sends a push to the given userid to please upgrade""" # add cooldown with redis to this function. if not (app.redis.set('plsupgr:%s' % str(user_id), '', ex=PLEASE_UPGRADE_COOLDOWN_SECONDS, nx=True)): # returns None if already exists return push_id = generate_push_id() push_type = 'please_upgrade' from kinappserver.models import get_user_push_data os_type, token, push_env = get_user_push_data(user_id) if token: if os_type == OS_ANDROID: increment_metric('pleaseupgrade-android') #return # not supported yet print('sending please-upgrade push message to GCM user %s' % user_id) push_send_gcm( token, gcm_payload( push_type, push_id, { 'title': '', 'body': "Please upgrade the app to get the next task" }), push_env) else: increment_metric('pleaseupgrade-ios') print('sending please-upgrade push message to APNS user %s' % user_id) push_send_apns( token, apns_payload("", "Please upgrade the app to get the next task", push_type, push_id), push_env) else: print('not sending please-upgrade push to user_id %s: no token' % user_id) return
def send_kin(public_address, amount, memo=None): """send kins to an address""" # sanity: if public_address in (None, ''): log.error('cant send kin to address: %s' % public_address) return False, None if amount is None or amount < 1: log.error('cant send kin amount: %s' % amount) return False, None print('sending kin to address: %s' % public_address) #TODO REMOVE from stellar_base.asset import Asset try: kin_asset = Asset(ASSET_NAME, config.STELLAR_KIN_ISSUER_ADDRESS) return app.kin_sdk._send_asset(kin_asset, public_address, amount, memo) except Exception as e: increment_metric('send_kin_error') print('caught exception sending %s kin to address %s' % (amount, public_address)) print(e)
def order_gift_cards(merchant_code, merchant_template_id, denomination, num_of_cards=1): """this function orders a set of gift cards""" card_ids = [] # get the creds object creds = get_bh_creds() if not creds or not creds['token']: print('no bh creds object/auth token') return False else: token = creds['token'] order_id = egift_start_order_api(token, merchant_code, merchant_template_id) if not order_id: log.error('failed to create order') return False for i in range(num_of_cards): card_id = egift_add_card_api(token, order_id, denomination) if not card_id: log.error('failed to create card') return False else: print('order_gift_cards: added a bh card with id: %s' % card_id) card_ids.append(card_id) returned_order_id = egift_complete_order_api(token, order_id, creds['account_id'], creds['digital_signature']) if not returned_order_id: log.error('failed to complete order_id %s' % order_id) # at this point, the card isn't ready yet. we need to monitor the order # until it is processed. in the meanwhile, lets store it in the db. for card_id in card_ids: create_bh_card(card_id, order_id, merchant_code, denomination) increment_metric('bh_card_ordered') return True
def limit_to_acl(return_bool=False): """aborts unauthorized requests for sensitive APIs (nginx specific). allow on DEBUG the optional 'return_bool' flag governs whether the function aborts the request (default) or just returns a boolean. """ source_ip = request.headers.get('X-Forwarded-For', None) if not source_ip: print('missing expected header') if return_bool: return False increment_metric('not-in-acl') abort(403) if not is_in_acl(source_ip): if return_bool: return False print('%s is not in ACL, rejecting' % source_ip) increment_metric('not-in-acl') abort(403) if return_bool: return True
def track_orders(): """go over all the unprocessed orders and determine whether they were processed. if so, retrieve the redemption code for each of the cards in the now-processed orders. returns the number of yet unprocessed orders or -1 if the token was bad / sth went wrong """ creds = get_bh_creds() token = creds['token'] if not token: print('track_orders: no bh auth token') return -1 orders_dict = list_unprocessed_orders() unprocessed_orders = 0 for order_id in orders_dict.keys(): time.sleep(0.1) # dont choke their api servers status = get_order_status_api(token, order_id) print('received status:%s for order_id:%s' % (status, order_id)) if status == 'incomplete': print('ignoring order_id %s with status: incomplete' % order_id) continue if status == 'cancelled': print('ignoring order_id %s with status: cancelled' % order_id) continue if status == 'processed': # order was processed, so add all related cards to our db print('detected a processed order: %s. getting card codes for this order...' % order_id) for card_id in orders_dict[order_id]: card = egift_get_card_api(token, card_id) if card is None: print('could not retrieve info about cardid %s' % card_id) increment_metric('bh_card_processing_failure') continue print('card info: %s' % card) code = card['redemption_details']['activation_account_number'] # the actual redemption code is called 'activation_account_number' pin = card['redemption_details'].get('security_code', None) # sometimes there's a PIN code - so add it if its there if pin: code = code + ' PIN:%s' % pin merchant_code = card['merchant_code'] card_id = card['id'] order_id = card['order']['id'] if create_good(merchant_code_to_offer_id(merchant_code, card_id, order_id), 'code', code, extra_info={'bh_card_id': card_id}): print('created good with code %s for card_id %s' % (code, card_id)) increment_metric('bh_card_processed') else: log.error('failed to convert bh card_id %s to good' % card_id) increment_metric('bh_card_processing_failure') set_processed_orders(orders_dict[order_id]) else: unprocessed_orders = unprocessed_orders + 1 return unprocessed_orders
def store_task_results(user_id, task_id, results): """store the results provided by the user""" # reject hackers trying to send task results too soon try: # store the results try: user_task_results = UserTaskResults() user_task_results.user_id = user_id user_task_results.task_id = task_id user_task_results.results = results db.session.add(user_task_results) db.session.commit() except Exception as e: db.session.rollback() # this code handles the unlikely event that a user already had task results for this task, so rather # than INSERT, we UPDATE. log.info('store_task_results - failed to insert results. attempting to update instead. error:%s' % e) previous_task_results = UserTaskResults.query.filter_by(user_id=user_id).filter_by(task_id=task_id).first() previous_task_results.results = results db.session.add(previous_task_results) db.session.commit() log.info('store_task_results: overwritten user_id %s task %s results' % (user_id, task_id)) increment_metric('overwrite-task-results') # write down the completed task-id from kinappserver.models import UserAppData user_app_data = UserAppData.query.filter_by(user_id=user_id).first() if user_app_data is None: log.error('cant retrieve user app data for user:%s' % user_id) raise InternalError('cant retrieve user app data for user:%s' % user_id) cat_id = get_cat_id_for_task_id(task_id) if not cat_id: log.error('cant find cat_id for task_id %s' % task_id) raise InternalError('cant find cat_id for task_id %s' % task_id) if cat_id in user_app_data.completed_tasks_dict: if task_id not in user_app_data.completed_tasks_dict[cat_id]: user_app_data.completed_tasks_dict[cat_id].append(task_id) else: user_app_data.completed_tasks_dict[cat_id] = [task_id] commit_json_changed_to_orm(user_app_data, ['completed_tasks_dict']) log.info('wrote user_app_data.completed_tasks for userid: %s' % user_id) # calculate the next valid submission time, and store it: delay_days = None # calculate the next task's valid submission time, and store it: # this takes into account the delay_days field on the next task. # note: even if we end up skipping the next task (for example, truex for iOS), # we should still use the original delay days value (as done here). try: delay_days = get_next_task_delay_days(user_id, task_id) # throws exception if no such task exists print('next task delay:%s (user_id: %s, current task: %s)' % (delay_days, user_id, task_id)) except Exception as e: log.error('cant find task_delay for next task_id of %s' % task_id) if delay_days is None or int(delay_days) == 0: shifted_ts = arrow.utcnow().timestamp log.info('setting next task time to now (delay_days is: %s)' % delay_days) else: shift_seconds = calculate_timeshift(user_id, delay_days) shifted_ts = arrow.utcnow().shift(seconds=shift_seconds).timestamp log.info('setting next task time to %s seconds in the future' % shift_seconds) log.info('next valid submission time for user %s, (previous task id %s) in shifted_ts: %s' % (user_id, task_id, shifted_ts)) store_next_task_results_ts(user_id, task_id, shifted_ts) return True except Exception as e: log.error('exception in store_task_results: %s', e) raise InvalidUsage('cant store_task_results')