def __create_like_notification(self, article_info): notification_table = self.dynamodb.Table(os.environ['NOTIFICATION_TABLE_NAME']) notification_id = '-'.join([settings.LIKE_NOTIFICATION_TYPE, article_info['user_id'], article_info['article_id']]) notification = notification_table.get_item(Key={'notification_id': notification_id}).get('Item') liked_count = self.__get_article_likes_count() if notification: notification_table.update_item( Key={ 'notification_id': notification_id }, UpdateExpression="set sort_key = :sort_key, article_title = :article_title, liked_count = :liked_count", ExpressionAttributeValues={ ':sort_key': TimeUtil.generate_sort_key(), ':article_title': article_info['title'], ':liked_count': liked_count } ) else: notification_table.put_item(Item={ 'notification_id': notification_id, 'user_id': article_info['user_id'], 'article_id': article_info['article_id'], 'article_title': article_info['title'], 'sort_key': TimeUtil.generate_sort_key(), 'type': settings.LIKE_NOTIFICATION_TYPE, 'liked_count': liked_count, 'created_at': int(time.time()) } )
def __notify_purchaser(self, article_info, user_id, transaction_status): notification_table = self.dynamodb.Table( os.environ['NOTIFICATION_TABLE_NAME']) notification_table.put_item( Item={ 'notification_id': self.__get_randomhash(), 'user_id': user_id, 'acted_user_id': user_id, 'article_id': article_info['article_id'], 'article_user_id': article_info['user_id'], 'article_title': article_info['title'], 'sort_key': TimeUtil.generate_sort_key(), 'type': settings.ARTICLE_PURCHASE_TYPE if transaction_status == 'done' else settings.ARTICLE_PURCHASE_ERROR_TYPE, 'price': int(article_info['price']), 'created_at': int(time.time()) })
def exec_main_proc(self): sort_key = TimeUtil.generate_sort_key() article_id = self.__generate_article_id(sort_key) params = json.loads(self.event.get('body')) try: self.__create_article_info(params, sort_key, article_id) try: self.__create_article_content(params, article_id) except Exception as err: logging.fatal(err) traceback.print_exc() finally: return { 'statusCode': 200, 'body': json.dumps({'article_id': article_id}) } except ClientError as e: if e.response['Error'][ 'Code'] == 'ConditionalCheckFailedException': return { 'statusCode': 400, 'body': json.dumps({'message': 'Already exists'}) } else: raise
def __create_tip_info(self, transaction_hash, tip_value, burn_transaction, article_info): tip_table = self.dynamodb.Table(os.environ['TIP_TABLE_NAME']) sort_key = TimeUtil.generate_sort_key() user_id = self.event['requestContext']['authorizer']['claims'][ 'cognito:username'] epoch = int(time.time()) tip_info = { 'user_id': user_id, 'to_user_id': article_info['user_id'], 'tip_value': tip_value, 'article_id': self.params['article_id'], 'article_title': article_info['title'], 'transaction': transaction_hash, 'burn_transaction': burn_transaction, 'uncompleted': 1, 'sort_key': sort_key, 'target_date': time.strftime('%Y-%m-%d', time.gmtime(epoch)), 'created_at': epoch } tip_table.put_item(Item=tip_info, ConditionExpression='attribute_not_exists(user_id)')
def __create_article_liked_user(self, article_liked_user_table): epoch = int(time.time()) article_liked_user = { 'article_id': self.event['pathParameters']['article_id'], 'user_id': self.event['requestContext']['authorizer']['claims']['cognito:username'], 'article_user_id': self.__get_article_user_id(self.event['pathParameters']['article_id']), 'created_at': epoch, 'target_date': time.strftime('%Y-%m-%d', time.gmtime(epoch)), 'sort_key': TimeUtil.generate_sort_key() } article_liked_user_table.put_item( Item=article_liked_user, ConditionExpression='attribute_not_exists(article_id)' )
def __create_article_history_and_update_sort_key(self): # update sort_key article_history_table = self.dynamodb.Table( os.environ['ARTICLE_HISTORY_TABLE_NAME']) article_histories = article_history_table.query( KeyConditionExpression=Key('article_id').eq( self.params['article_id']))['Items'] if len(article_histories) == 0: sort_key = TimeUtil.generate_sort_key() article_info_table = self.dynamodb.Table( os.environ['ARTICLE_INFO_TABLE_NAME']) article_info_table.update_item( Key={ 'article_id': self.params['article_id'], }, UpdateExpression= 'set sort_key = :sort_key, published_at = :published_at', ExpressionAttributeValues={ ':sort_key': sort_key, ':published_at': int(time.time()) }) # create article_history article_content_table = self.dynamodb.Table( os.environ['ARTICLE_CONTENT_TABLE_NAME']) article_content = article_content_table.get_item( Key={ 'article_id': self.params['article_id'] }).get('Item') Item = { 'article_id': article_content['article_id'], 'title': article_content['title'], 'body': article_content['body'], 'created_at': int(time.time()) } # 金額や有料記事本文が含まれている場合一緒に更新する if self.params.get('price') is not None and self.params.get( 'paid_body') is not None: Item.update({ 'price': self.params.get('price'), 'paid_body': self.params.get('paid_body') }) article_history_table.put_item(Item=Item)
def __create_comment_notification(self, article_info, comment_id, user_id): notification_table = self.dynamodb.Table( os.environ['NOTIFICATION_TABLE_NAME']) notification_id = '-'.join([ settings.COMMENT_NOTIFICATION_TYPE, article_info['user_id'], comment_id ]) notification_table.put_item( Item={ 'notification_id': notification_id, 'user_id': article_info['user_id'], 'article_id': article_info['article_id'], 'article_title': article_info['title'], 'acted_user_id': user_id, 'sort_key': TimeUtil.generate_sort_key(), 'type': settings.COMMENT_NOTIFICATION_TYPE, 'created_at': int(time.time()) })
def __notification(self, user_id, announce_url): notification_table = self.dynamodb.Table( os.environ['NOTIFICATION_TABLE_NAME']) notification_id = self.__get_randomhash() notification_table.put_item( Item={ 'notification_id': notification_id, 'user_id': user_id, 'sort_key': TimeUtil.generate_sort_key(), 'type': settings.CSVDOWNLOAD_NOTIFICATION_TYPE, 'created_at': int(time.time()), 'announce_body': '全トークン履歴のcsvのダウンロード準備が完了しました。本通知をクリックしてダウンロードしてください。', 'announce_url': announce_url }) self.__update_unread_notification_manager(user_id)
def exec_main_proc(self): sort_key = TimeUtil.generate_sort_key() user_id = self.event['requestContext']['authorizer']['claims'][ 'cognito:username'] comment_id = self.__generate_comment_id(sort_key) comment_table = self.dynamodb.Table(os.environ['COMMENT_TABLE_NAME']) comment = { 'comment_id': comment_id, 'article_id': self.params['article_id'], 'text': TextSanitizer.sanitize_text(self.params['text']), 'user_id': user_id, 'sort_key': sort_key, 'created_at': int(time.time()) } comment_table.put_item( Item=comment, ConditionExpression='attribute_not_exists(comment_id)') # 優先度が低いため通知処理は失敗しても握り潰して200を返す(ログは出して検知できるようにする) try: article_info_table = self.dynamodb.Table( os.environ['ARTICLE_INFO_TABLE_NAME']) article_info = article_info_table.get_item( Key={'article_id': self.params['article_id']})['Item'] if self.__is_notifiable_comment(article_info, user_id): self.__create_comment_notification(article_info, comment_id, user_id) self.__update_unread_notification_manager(article_info) except Exception as err: logging.fatal(err) traceback.print_exc() finally: return { 'statusCode': 200, 'body': json.dumps({'comment_id': comment_id}) }
def notify_article_comment(dynamodb, article_info, comment, target_user_id, comment_type): if comment_type not in settings.COMMENT_NOTIFICATION_TYPES: raise ValueError('Invalid comment type ' + comment_type) notification_table = dynamodb.Table( os.environ['NOTIFICATION_TABLE_NAME']) notification_id = '-'.join( [comment_type, target_user_id, comment['comment_id']]) notification_table.put_item( Item={ 'notification_id': notification_id, 'user_id': target_user_id, 'article_id': article_info['article_id'], 'article_user_id': article_info['user_id'], 'article_title': article_info['title'], 'acted_user_id': comment['user_id'], 'sort_key': TimeUtil.generate_sort_key(), 'type': comment_type, 'created_at': int(time.time()) })
def exec_main_proc(self): # get article info article_info_table = self.dynamodb.Table( os.environ['ARTICLE_INFO_TABLE_NAME']) article_info = article_info_table.get_item( Key={ 'article_id': self.params['article_id'] }).get('Item') # does not purchase same user's article user_id = self.event['requestContext']['authorizer']['claims'][ 'cognito:username'] if article_info['user_id'] == user_id: raise ValidationError('Can not purchase own article') # purchase article paid_articles_table = self.dynamodb.Table( os.environ['PAID_ARTICLES_TABLE_NAME']) paid_status_table = self.dynamodb.Table( os.environ['PAID_STATUS_TABLE_NAME']) article_user_eth_address = self.__get_user_private_eth_address( article_info['user_id']) user_eth_address = self.event['requestContext']['authorizer'][ 'claims']['custom:private_eth_address'] price = self.params['price'] auth = AWSRequestsAuth( aws_access_key=os.environ['PRIVATE_CHAIN_AWS_ACCESS_KEY'], aws_secret_access_key=os. environ['PRIVATE_CHAIN_AWS_SECRET_ACCESS_KEY'], aws_host=os.environ['PRIVATE_CHAIN_EXECUTE_API_HOST'], aws_region='ap-northeast-1', aws_service='execute-api') headers = {'content-type': 'application/json'} sort_key = TimeUtil.generate_sort_key() # 多重リクエストによる不必要なレコード生成を防ぐためにpaid_statusレコードを生成 self.__create_paid_status(paid_status_table, user_id) # 購入のトランザクション処理 purchase_transaction = self.__create_purchase_transaction( auth, headers, user_eth_address, article_user_eth_address, price) # 購入記事データを作成 self.__create_paid_article(paid_articles_table, article_info, purchase_transaction, sort_key) # プライベートチェーンへのポーリングを行いトランザクションの承認状態を取得 transaction_status = self.__polling_to_private_chain( purchase_transaction, auth, headers) # トランザクションの承認状態をpaid_artilcleとpaid_statusに格納 self.__update_transaction_status(article_info, paid_articles_table, transaction_status, sort_key, paid_status_table, user_id) # 購入のトランザクションが成功した時のみバーンのトランザクションを発行する if transaction_status == 'done': try: # 購入に成功した場合、著者の未読通知フラグをTrueにする self.__update_unread_notification_manager( article_info['user_id']) # 著者へ通知を作成 self.__notify_author(article_info, user_id) # バーンのトランザクション処理 burn_transaction = self.__burn_transaction( price, user_eth_address, auth, headers) # バーンのトランザクションを購入テーブルに格納 self.__add_burn_transaction_to_paid_article( burn_transaction, paid_articles_table, article_info, sort_key) except Exception as err: logging.fatal(err) traceback.print_exc() # 記事購入者へは購入処理中の場合以外で通知を作成 if transaction_status == 'done' or transaction_status == 'fail': self.__update_unread_notification_manager(user_id) self.__notify_purchaser(article_info, user_id, transaction_status) return { 'statusCode': 200, 'body': json.dumps({'status': transaction_status}) }
def exec_main_proc(self): ################ # get parameter ################ sort_key = TimeUtil.generate_sort_key() from_user_eth_address = self.event['requestContext']['authorizer'][ 'claims']['custom:private_eth_address'] user_id = self.event['requestContext']['authorizer']['claims'][ 'cognito:username'] allowance = PrivateChainUtil.get_allowance(from_user_eth_address) transaction_count = PrivateChainUtil.get_transaction_count( from_user_eth_address) ################ # validation ################ # validate raw_transaction # init_approve_signed_transaction if int(allowance, 16) != 0: # allowance が設定されている場合は必須 if self.params.get('init_approve_signed_transaction') is None: raise ValidationError( 'init_approve_signed_transaction is invalid.') # data init_approve_data = PrivateChainUtil.get_data_from_raw_transaction( self.params['init_approve_signed_transaction'], transaction_count) PrivateChainUtil.validate_erc20_approve_data(init_approve_data) if int(init_approve_data[72:], 16) != 0: raise ValidationError('Value of init_approve is invalid.') transaction_count = PrivateChainUtil.increment_transaction_count( transaction_count) # approve_signed_transaction approve_data = PrivateChainUtil.get_data_from_raw_transaction( self.params['approve_signed_transaction'], transaction_count) PrivateChainUtil.validate_erc20_approve_data(approve_data) # 日次の限度額を超えていた場合は例外 sum_price = self.__get_token_send_value_today(user_id) if Decimal(os.environ['DAILY_LIMIT_TOKEN_SEND_VALUE'] ) < sum_price + Decimal(int(approve_data[72:], 16)): raise ValidationError('Token withdrawal limit has been exceeded.') transaction_count = PrivateChainUtil.increment_transaction_count( transaction_count) # relay_signed_transaction relay_data = PrivateChainUtil.get_data_from_raw_transaction( self.params['relay_signed_transaction'], transaction_count) PrivateChainUtil.validate_erc20_relay_data(relay_data) # approve と relay の value が同一であること approve_value = int(approve_data[72:], 16) relay_value = int(relay_data[72:], 16) if approve_value != relay_value: raise ValidationError('approve and relay values do not match.') ####################### # send_raw_transaction ####################### # 既に approve されている場合(allowance の戻り値が 0 ではない場合)、該当の approve を削除する(0 で更新) if int(allowance, 16) != 0: PrivateChainUtil.send_raw_transaction( self.params.get('init_approve_signed_transaction')) # approve 実施 approve_transaction_hash = PrivateChainUtil.send_raw_transaction( self.params.get('approve_signed_transaction')) self.__create_send_info_with_approve_transaction_hash( sort_key, user_id, approve_transaction_hash, relay_value) # token_send_table への書き込み完了後に出金関連の例外が発生した場合は、token_send_table のステータスを fail に更新する try: # relay 実施 relay_transaction_hash = PrivateChainUtil.send_raw_transaction( self.params.get('relay_signed_transaction')) self.__update_send_info_with_relay_transaction_hash( sort_key, user_id, relay_transaction_hash) # transaction の完了を確認 is_completed = PrivateChainUtil.is_transaction_completed( relay_transaction_hash) except SendTransactionError as e: # ステータスを fail に更新し中断 self.__update_send_info_with_send_status(sort_key, user_id, 'fail') raise e except ReceiptError: # send_value の値が残高を超えた場合や、処理最小・最大値の範囲に収まっていない場合に ReceiptError が発生するため # ValidationError として処理を中断する # ステータスを fail に更新 self.__update_send_info_with_send_status(sort_key, user_id, 'fail') raise ValidationError('send_value') # transaction が完了していた場合、ステータスを done に更新 if is_completed: self.__update_send_info_with_send_status(sort_key, user_id, 'done') return { 'statusCode': 200, 'body': json.dumps({'is_completed': is_completed}) }
def test_generate_sort_key(self): sort_key = TimeUtil.generate_sort_key() self.assertEqual(len(str(sort_key)), 16) self.assertTrue(type(sort_key) is int)
def exec_main_proc(self): ################ # get parameter ################ # get article info article_info_table = self.dynamodb.Table( os.environ['ARTICLE_INFO_TABLE_NAME']) article_info = article_info_table.get_item( Key={ 'article_id': self.params['article_id'] }).get('Item') # purchase article paid_articles_table = self.dynamodb.Table( os.environ['PAID_ARTICLES_TABLE_NAME']) paid_status_table = self.dynamodb.Table( os.environ['PAID_STATUS_TABLE_NAME']) # eth_address article_user_eth_address = UserUtil.get_private_eth_address( self.cognito, article_info['user_id']) user_eth_address = self.event['requestContext']['authorizer'][ 'claims']['custom:private_eth_address'] # transaction_count transaction_count = PrivateChainUtil.get_transaction_count( user_eth_address) ################ # validation ################ # does not purchase same user's article user_id = self.event['requestContext']['authorizer']['claims'][ 'cognito:username'] if article_info['user_id'] == user_id: raise ValidationError('Can not purchase own article') # validate raw_transaction # purchase purchase_data = PrivateChainUtil.get_data_from_raw_transaction( self.params['purchase_signed_transaction'], transaction_count) PrivateChainUtil.validate_erc20_transfer_data( purchase_data, article_user_eth_address) # burn transaction_count = PrivateChainUtil.increment_transaction_count( transaction_count) burn_data = PrivateChainUtil.get_data_from_raw_transaction( self.params['burn_signed_transaction'], transaction_count) PrivateChainUtil.validate_erc20_transfer_data( burn_data, '0x' + os.environ['BURN_ADDRESS']) # burn 量が正しいこと purchase_value = int(purchase_data[72:], 16) burn_value = int(burn_data[72:], 16) calc_burn_value = int(Decimal(purchase_value) / Decimal(9)) if burn_value != calc_burn_value: raise ValidationError('burn_value is invalid.') # purchase_value が記事で指定されている金額に基づいた量が設定されていること DBUtil.validate_latest_price(self.dynamodb, self.params['article_id'], purchase_value) ################ # purchase ################ sort_key = TimeUtil.generate_sort_key() # 多重リクエストによる不必要なレコード生成を防ぐためにpaid_statusレコードを生成 self.__create_paid_status(paid_status_table, user_id) # 購入のトランザクション処理 purchase_transaction = PrivateChainUtil.send_raw_transaction( self.params['purchase_signed_transaction']) # 購入記事データを作成 self.__create_paid_article(paid_articles_table, article_info, purchase_transaction, sort_key) # プライベートチェーンへのポーリングを行いトランザクションの承認状態を取得 transaction_status = self.__polling_to_private_chain( purchase_transaction) # トランザクションの承認状態をpaid_articleとpaid_statusに格納 self.__update_transaction_status(article_info, paid_articles_table, transaction_status, sort_key, paid_status_table, user_id) # 購入のトランザクションが成功した時のみバーンのトランザクションを発行する if transaction_status == 'done': try: # 購入に成功した場合、著者の未読通知フラグをTrueにする self.__update_unread_notification_manager( article_info['user_id']) # 著者へ通知を作成 self.__notify_author(article_info, user_id) # バーンのトランザクション処理 burn_transaction = PrivateChainUtil.send_raw_transaction( self.params['burn_signed_transaction']) # バーンのトランザクションを購入テーブルに格納 self.__add_burn_transaction_to_paid_article( burn_transaction, paid_articles_table, article_info, sort_key) except Exception as err: logging.fatal(err) traceback.print_exc() # 記事購入者へは購入処理中の場合以外で通知を作成 if transaction_status == 'done' or transaction_status == 'fail': self.__update_unread_notification_manager(user_id) self.__notify_purchaser(article_info, user_id, transaction_status) return { 'statusCode': 200, 'body': json.dumps({'status': transaction_status}) }
def exec_main_proc(self): from_user_eth_address = self.event['requestContext']['authorizer'][ 'claims']['custom:private_eth_address'] recipient_eth_address = self.params['recipient_eth_address'] send_value = self.params['send_value'] sort_key = TimeUtil.generate_sort_key() user_id = self.event['requestContext']['authorizer']['claims'][ 'cognito:username'] # 日次の限度額を超えていた場合は例外 sum_price = self.__get_token_send_value_today(user_id) if Decimal(os.environ['DAILY_LIMIT_TOKEN_SEND_VALUE'] ) < sum_price + Decimal(send_value): raise ValidationError('Token withdrawal limit has been exceeded.') # allowance を取得 allowance = self.__get_allowance(from_user_eth_address) # transaction_count を取得 transaction_count = self.__get_transaction_count(from_user_eth_address) # 既に approve されている場合(allowance の戻り値が "0x0" ではない場合)、該当の approve を削除する(0 で更新) if allowance != '0x0': self.__approve(from_user_eth_address, 0, transaction_count) transaction_count = self.__increment_transaction_count( transaction_count) # approve 実施 approve_transaction_hash = self.__approve(from_user_eth_address, send_value, transaction_count) transaction_count = self.__increment_transaction_count( transaction_count) self.__create_send_info_with_approve_transaction_hash( sort_key, user_id, approve_transaction_hash) # token_send_table への書き込み完了後に出金関連の例外が発生した場合は、token_send_table のステータスを fail に更新する try: # relay 実施 relay_transaction_hash = self.__relay(from_user_eth_address, recipient_eth_address, send_value, transaction_count) self.__update_send_info_with_relay_transaction_hash( sort_key, user_id, relay_transaction_hash) # transaction の完了を確認 is_completed = PrivateChainUtil.is_transaction_completed( relay_transaction_hash) except SendTransactionError as e: # ステータスを fail に更新し中断 self.__update_send_info_with_send_status(sort_key, user_id, 'fail') raise e except ReceiptError: # send_value の値が残高を超えた場合や、処理最小・最大値の範囲に収まっていない場合に ReceiptError が発生するため # ValidationError として処理を中断する # ステータスを fail に更新 self.__update_send_info_with_send_status(sort_key, user_id, 'fail') raise ValidationError('send_value') # transaction が完了していた場合、ステータスを done に更新 if is_completed: self.__update_send_info_with_send_status(sort_key, user_id, 'done') return { 'statusCode': 200, 'body': json.dumps({'is_completed': is_completed}) }