2021-06-07 07:10:18 -07:00
# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Mango Markets](https://mango.markets/) support is available at:
# [Docs](https://docs.mango.markets/)
# [Discord](https://discord.gg/67jySBhxrg)
# [Twitter](https://twitter.com/mangomarkets)
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
import abc
import csv
import logging
import os . path
import requests
import typing
from urllib . parse import unquote
from . liquidationevent import LiquidationEvent
# # 🥭 Notification
#
# This file contains code to send arbitrary notifications.
#
# # 🥭 NotificationTarget class
#
# This base class is the root of the different notification mechanisms.
#
# Derived classes should override `send_notification()` to implement their own sending logic.
#
# Derived classes should not override `send()` since that is the interface outside classes call and it's used to ensure `NotificationTarget`s don't throw an exception when sending.
#
class NotificationTarget ( metaclass = abc . ABCMeta ) :
def __init__ ( self ) :
self . logger : logging . Logger = logging . getLogger ( self . __class__ . __name__ )
def send ( self , item : typing . Any ) - > None :
try :
self . send_notification ( item )
except Exception as exception :
self . logger . error ( f " Error sending { item } - { self } - { exception } " )
@abc.abstractmethod
def send_notification ( self , item : typing . Any ) - > None :
raise NotImplementedError ( " NotificationTarget.send() is not implemented on the base type. " )
def __repr__ ( self ) - > str :
return f " { self } "
# # 🥭 TelegramNotificationTarget class
#
# The `TelegramNotificationTarget` sends messages to Telegram.
#
# The format for the telegram notification is:
# 1. The word 'telegram'
# 2. A colon ':'
# 3. The chat ID
# 4. An '@' symbol
# 5. The bot token
#
# For example:
# ```
# telegram:<CHAT-ID>@<BOT-TOKEN>
# ```
#
# The [Telegram instructions to create a bot](https://core.telegram.org/bots#creating-a-new-bot)
# show you how to create the bot token.
class TelegramNotificationTarget ( NotificationTarget ) :
def __init__ ( self , address ) :
super ( ) . __init__ ( )
chat_id , bot_id = address . split ( " @ " , 1 )
self . chat_id = chat_id
self . bot_id = bot_id
def send_notification ( self , item : typing . Any ) - > None :
payload = { " disable_notification " : True , " chat_id " : self . chat_id , " text " : str ( item ) }
url = f " https://api.telegram.org/bot { self . bot_id } /sendMessage "
headers = { " Content-Type " : " application/json " }
requests . post ( url , json = payload , headers = headers )
def __str__ ( self ) - > str :
return f " Telegram chat ID: { self . chat_id } "
# # 🥭 DiscordNotificationTarget class
#
# The `DiscordNotificationTarget` sends messages to Discord.
#
class DiscordNotificationTarget ( NotificationTarget ) :
def __init__ ( self , address ) :
super ( ) . __init__ ( )
self . address = address
def send_notification ( self , item : typing . Any ) - > None :
payload = {
" content " : str ( item )
}
url = self . address
headers = { " Content-Type " : " application/json " }
requests . post ( url , json = payload , headers = headers )
def __str__ ( self ) - > str :
return " Discord webhook "
# # 🥭 MailjetNotificationTarget class
#
# The `MailjetNotificationTarget` sends an email through [Mailjet](https://mailjet.com).
#
# In order to pass everything in to the notifier as a single string (needed to stop
# command-line parameters form getting messy), `MailjetNotificationTarget` requires a
# compound string, separated by colons.
# ```
# mailjet:<MAILJET-API-KEY>:<MAILJET-API-SECRET>:FROM-NAME:FROM-ADDRESS:TO-NAME:TO-ADDRESS
#
# ```
# Individual components are URL-encoded (so, for example, spaces are replaces with %20,
# colons are replaced with %3A).
#
# * `<MAILJET-API-KEY>` and `<MAILJET-API-SECRET>` are from your [Mailjet](https://mailjet.com) account.
# * `FROM-NAME` and `TO-NAME` are just text fields that are used as descriptors in the email messages.
# * `FROM-ADDRESS` is the address the email appears to come from. This must be validated with [Mailjet](https://mailjet.com).
# * `TO-ADDRESS` is the destination address - the email account to which the email is being sent.
#
# Mailjet provides a client library, but really we don't need or want more dependencies. This`
# code just replicates the `curl` way of doing things:
# ```
# curl -s \
# -X POST \
# --user "$MJ_APIKEY_PUBLIC:$MJ_APIKEY_PRIVATE" \
# https://api.mailjet.com/v3.1/send \
# -H 'Content-Type: application/json' \
# -d '{
# "SandboxMode":"true",
# "Messages":[
# {
# "From":[
# {
# "Email":"pilot@mailjet.com",
# "Name":"Your Mailjet Pilot"
# }
# ],
# "HTMLPart":"<h3>Dear passenger, welcome to Mailjet!</h3><br />May the delivery force be with you!",
# "Subject":"Your email flight plan!",
# "TextPart":"Dear passenger, welcome to Mailjet! May the delivery force be with you!",
# "To":[
# {
# "Email":"passenger@mailjet.com",
# "Name":"Passenger 1"
# }
# ]
# }
# ]
# }'
# ```
class MailjetNotificationTarget ( NotificationTarget ) :
def __init__ ( self , encoded_parameters ) :
super ( ) . __init__ ( )
self . address = " https://api.mailjet.com/v3.1/send "
api_key , api_secret , subject , from_name , from_address , to_name , to_address = encoded_parameters . split ( " : " )
self . api_key : str = unquote ( api_key )
self . api_secret : str = unquote ( api_secret )
self . subject : str = unquote ( subject )
self . from_name : str = unquote ( from_name )
self . from_address : str = unquote ( from_address )
self . to_name : str = unquote ( to_name )
self . to_address : str = unquote ( to_address )
def send_notification ( self , item : typing . Any ) - > None :
payload = {
" Messages " : [
{
" From " : {
" Email " : self . from_address ,
" Name " : self . from_name
} ,
" Subject " : self . subject ,
" TextPart " : str ( item ) ,
" To " : [
{
" Email " : self . to_address ,
" Name " : self . to_name
}
]
}
]
}
url = self . address
headers = { " Content-Type " : " application/json " }
requests . post ( url , json = payload , headers = headers , auth = ( self . api_key , self . api_secret ) )
def __str__ ( self ) - > str :
return f " Mailjet notifications to ' { self . to_name } ' ' { self . to_address } ' with subject ' { self . subject } ' "
# # 🥭 CsvFileNotificationTarget class
#
# Outputs a liquidation event to CSV. Nothing is written if the item is not a
# `LiquidationEvent`.
#
# Headers for the CSV file should be:
# ```
# "Timestamp","Liquidator Name","Group","Succeeded","Signature","Wallet","Margin Account","Token Changes"
# ```
# Token changes are listed as pairs of value plus symbol, so each token change adds two
# columns to the output. Token changes may arrive in different orders, so ordering of token
# changes is not guaranteed to be consistent from transaction to transaction.
#
class CsvFileNotificationTarget ( NotificationTarget ) :
def __init__ ( self , filename ) :
super ( ) . __init__ ( )
self . filename = filename
def send_notification ( self , item : typing . Any ) - > None :
if isinstance ( item , LiquidationEvent ) :
event : LiquidationEvent = item
if not os . path . isfile ( self . filename ) or os . path . getsize ( self . filename ) == 0 :
with open ( self . filename , " w " ) as empty_file :
empty_file . write (
' " Timestamp " , " Liquidator Name " , " Group " , " Succeeded " , " Signature " , " Wallet " , " Margin Account " , " Token Changes " \n ' )
with open ( self . filename , " a " ) as csvfile :
result = " Succeeded " if event . succeeded else " Failed "
row_data = [ event . timestamp , event . liquidator_name , event . group_name , result ,
2021-08-30 07:42:11 -07:00
event . signatures , event . wallet_address , event . account_address ]
2021-06-07 07:10:18 -07:00
for change in event . changes :
row_data + = [ f " { change . value : .8f } " , change . token . name ]
file_writer = csv . writer ( csvfile , quoting = csv . QUOTE_MINIMAL )
file_writer . writerow ( row_data )
def __str__ ( self ) - > str :
return f " CSV notifications to file { self . filename } "
# # 🥭 FilteringNotificationTarget class
#
# This class takes a `NotificationTarget` and a filter function, and only calls the
# `NotificationTarget` if the filter function returns `True` for the notification item.
#
class FilteringNotificationTarget ( NotificationTarget ) :
def __init__ ( self , inner_notifier : NotificationTarget , filter_func : typing . Callable [ [ typing . Any ] , bool ] ) :
super ( ) . __init__ ( )
self . inner_notifier : NotificationTarget = inner_notifier
self . filter_func = filter_func
def send_notification ( self , item : typing . Any ) - > None :
if self . filter_func ( item ) :
self . inner_notifier . send_notification ( item )
def __str__ ( self ) - > str :
return f " Filtering notification target for ' { self . inner_notifier } ' "
# # 🥭 NotificationHandler class
#
# A bridge between the worlds of notifications and logging. This allows any
# `NotificationTarget` to be plugged in to the `logging` subsystem to receive log messages
# and notify however it chooses.
#
class NotificationHandler ( logging . StreamHandler ) :
def __init__ ( self , target : NotificationTarget ) :
logging . StreamHandler . __init__ ( self )
self . target = target
def emit ( self , record ) :
2021-07-19 10:29:55 -07:00
# Don't send error logging from solanaweb3
if record . name == " solanaweb3.rpc.httprpc.HTTPClient " :
return
2021-06-07 07:10:18 -07:00
message = self . format ( record )
self . target . send_notification ( message )
# # 🥭 parse_subscription_target() function
#
# `parse_subscription_target()` takes a parameter as a string and returns a notification
# target.
#
# This is most likely used when parsing command-line arguments - this function can be used
# in the `type` parameter of an `add_argument()` call.
#
def parse_subscription_target ( target ) :
protocol , destination = target . split ( " : " , 1 )
if protocol == " telegram " :
return TelegramNotificationTarget ( destination )
elif protocol == " discord " :
return DiscordNotificationTarget ( destination )
elif protocol == " mailjet " :
return MailjetNotificationTarget ( destination )
elif protocol == " csvfile " :
return CsvFileNotificationTarget ( destination )
else :
raise Exception ( f " Unknown protocol: { protocol } " )