2015-01-12 21:52:19 +00:00
#!/bin/python
#
# Cryptonote tipbot - bookie commands
# Copyright 2015 moneromooo
#
# The Cryptonote tipbot is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
import sys
import os
import redis
import string
import random
import tipbot . config as config
from tipbot . log import log_error , log_warn , log_info , log_log
import tipbot . coinspecs as coinspecs
from tipbot . utils import *
from tipbot . command_manager import *
from tipbot . redisdb import *
from tipbot . betutils import *
def GetActiveBooks ( ) :
return redis_hgetall ( ' bookie:active ' )
def SweepClosingTimes ( ) :
books = GetActiveBooks ( )
if not books :
return
now = time . time ( )
for book_index in books . keys ( ) :
book_index = long ( book_index )
tname = " bookie: %d " % book_index
if redis_hexists ( tname , ' closing_time ' ) :
closing_time = float ( redis_hget ( tname , ' closing_time ' ) )
if closing_time < = now and not redis_hget ( tname , ' closed ' ) :
book_name = redis_hget ( tname , ' name ' )
redis_hset ( tname , ' closed ' , 1 )
log_info ( ' Closing book # %d ( %s ) as scheduled, dt %s ' % ( book_index , book_name , TimeToString ( now - closing_time ) ) )
def Bookie ( link , cmd ) :
identity = link . identity ( )
name = GetParam ( cmd , 1 )
if not name :
link . send ( ' usage: !bookie <name> <outcome1> <outcome2> [<outcome3>...] ' )
return
outcomes = cmd [ 2 : ]
if len ( outcomes ) < 2 :
link . send ( ' usage: !bookie <name> <outcome1> <outcome2> [<outcome3>...] ' )
return
book_index = long ( redis_get ( ' bookie:last_book ' ) or 0 )
book_index + = 1
tname = " bookie: %d " % book_index
log_info ( ' %s opens book # %d for %s , with outcomes %s ' % ( identity , book_index , name , str ( outcomes ) ) )
try :
p = redis_pipeline ( )
p . hset ( tname , ' name ' , name )
for o in outcomes :
p . sadd ( tname + ' :outcomes ' , o )
p . hset ( ' bookie:active ' , book_index , name )
redis_set ( ' bookie:last_book ' , book_index )
p . execute ( )
except Exception , e :
log_error ( ' Bookie: Failed to register book for %s with outcomes %s : %s ' % ( name , str ( outcomes ) , str ( e ) ) )
link . send ( ' Failed to create book ' )
return
link . send ( ' %s opens book # %d for %s , with outcomes: %s ' % ( link . user . nick , book_index , name , " , " . join ( outcomes ) ) )
2015-01-31 18:59:30 +00:00
def GetBookIndex ( cmd , base_arg_count ) :
2015-01-12 21:52:19 +00:00
active_books = GetActiveBooks ( )
if len ( active_books ) == 0 :
2015-01-31 18:59:30 +00:00
return None , ' There is no open book '
2015-01-12 21:52:19 +00:00
2015-01-31 18:59:30 +00:00
if GetParam ( cmd , base_arg_count + 1 ) :
name = GetParam ( cmd , 1 )
2015-01-31 19:12:32 +00:00
if name [ 0 ] == ' # ' :
if not name [ 1 : ] in active_books . keys ( ) :
return None , ' Book %s not found ' % name
book_index = long ( active_books . keys ( ) [ active_books . keys ( ) . index ( name [ 1 : ] ) ] )
parm_offset = 1
else :
if not name in active_books . values ( ) :
return None , ' Book %s not found ' % name
if active_books . values ( ) . count ( name ) > 1 :
return None , ' There are several books named %s , use its #N id instead ' % name
book_index = long ( active_books . keys ( ) [ active_books . values ( ) . index ( name ) ] )
parm_offset = 1
2015-01-12 21:52:19 +00:00
else :
if len ( active_books ) > 1 :
2015-01-31 18:59:30 +00:00
return None , ' There are several open books: %s ' % " , " . join ( active_books . values ( ) )
2015-01-12 21:52:19 +00:00
book_index = long ( active_books . keys ( ) [ 0 ] )
2015-01-31 18:59:30 +00:00
parm_offset = 0
return long ( book_index ) , parm_offset
def Cancel ( link , cmd ) :
identity = link . identity ( )
SweepClosingTimes ( )
res0 , res1 = GetBookIndex ( cmd , 0 )
if res0 == None :
link . send ( res1 )
return
book_index = res0
parm_offset = res1
2015-01-12 21:52:19 +00:00
tname = ' bookie: %d ' % book_index
book_name = redis_hget ( tname , ' name ' )
log_info ( ' Cancelling book %d ( %s ) ' % ( book_index , book_name ) )
try :
p = redis_pipeline ( )
bettors = redis_smembers ( tname + ' :bettors ' )
refundmsg = [ ]
for bettor in bettors :
units = long ( redis_hget ( tname , bettor + " :units " ) )
log_info ( ' Refunding %s to %s ' % ( AmountToString ( units ) , bettor ) )
2015-01-31 23:40:13 +00:00
a = GetAccount ( bettor )
p . hincrby ( ' balances ' , a , units )
2015-01-12 21:52:19 +00:00
p . hincrby ( ' earmarked ' , ' bookie ' , - units )
refundmsg . append ( ' %s to %s ' % ( AmountToString ( units ) , NickFromIdentity ( bettor ) ) )
p . hdel ( ' bookie:active ' , book_index )
p . execute ( )
if len ( refundmsg ) == 0 :
link . send ( ' Book %s cancelled, nobody had bet ' % book_name )
else :
link . send ( ' Book %s cancelled, refunding %s ' % ( book_name , " , " . join ( refundmsg ) ) )
except Exception , e :
log_error ( ' Cancel: Failed to cancel book: %s ' % str ( e ) )
link . send ( ' Failed to cancel book %s ' % book_name )
return
def Close ( link , cmd ) :
identity = link . identity ( )
SweepClosingTimes ( )
2015-01-31 18:59:30 +00:00
res0 , res1 = GetBookIndex ( cmd , 0 )
if res0 == None :
link . send ( res1 )
2015-01-12 21:52:19 +00:00
return
2015-01-31 18:59:30 +00:00
book_index = res0
parm_offset = res1
2015-01-12 21:52:19 +00:00
tname = " bookie: %d " % book_index
book_name = redis_hget ( tname , ' name ' )
log_info ( ' Closing book %d ' % book_index )
try :
redis_hset ( tname , ' closed ' , 1 )
except Exception , e :
log_error ( ' Failed to close book: %s ' % str ( e ) )
link . send ( ' An error occured ' )
return
link . send ( ' %s closed book # %d ( %s ) to new bets ' % ( link . user . nick , book_index , book_name ) )
def ScheduleClose ( link , cmd ) :
identity = link . identity ( )
SweepClosingTimes ( )
2015-01-31 18:59:30 +00:00
res0 , res1 = GetBookIndex ( cmd , 0 )
if res0 == None :
link . send ( res1 )
2015-01-12 21:52:19 +00:00
return
2015-01-31 18:59:30 +00:00
book_index = res0
parm_offset = res1
2015-01-12 21:52:19 +00:00
tname = " bookie: %d " % book_index
book_name = redis_hget ( tname , ' name ' )
try :
minutes = float ( GetParam ( cmd , 1 + parm_offset ) )
except Exception , e :
log_error ( ' error getting minutes: %s ' % str ( e ) )
link . send ( ' usage: schedule_close [<event name>] <minutes> ' )
return
if minutes < 0 :
log_error ( ' error: negative minutes: %f ' % minutes )
link . send ( ' minutes to closing must not be negative ' )
return
try :
redis_hset ( tname , ' closing_time ' , time . time ( ) + minutes * 60 )
except Exception , e :
log_error ( ' error setting closing time: %s ' % str ( e ) )
link . send ( ' Failed to schedule closing time ' )
return
link . send ( ' Book # %d ( %s ) will be closed to new bets in %s ' % ( book_index , book_name , TimeToString ( minutes * 60 ) ) )
def Book ( link , cmd ) :
identity = link . identity ( )
SweepClosingTimes ( )
active_books = GetActiveBooks ( )
if len ( active_books ) == 0 :
link . send ( ' The book is empty ' )
return
2015-01-31 19:12:32 +00:00
for book_index in sorted ( active_books . keys ( ) ) :
2015-01-12 21:52:19 +00:00
book_index = long ( book_index )
tname = ' bookie: %s ' % book_index
try :
name = redis_hget ( tname , ' name ' )
outcomes = redis_smembers ( tname + ' :outcomes ' )
outcome = redis_hget ( tname , identity + " :outcome " )
units = redis_hget ( tname , identity + " :units " )
except Exception , e :
log_error ( ' Book: Failed to retrieve book %d : %s ' % ( book_index , str ( e ) ) )
link . send ( ' An error occured ' )
return
outcomes = redis_smembers ( tname + ' :outcomes ' )
2015-01-31 18:22:14 +00:00
outcomes_with_bets = [ ]
2015-01-12 21:52:19 +00:00
for o in outcomes :
ou = long ( redis_hget ( tname + " :bets " , o ) or 0 )
if ou > 0 :
2015-01-31 18:22:14 +00:00
outcomes_with_bets . append ( o + " ( %s ) " % AmountToString ( ou ) )
else :
outcomes_with_bets . append ( o )
msg = ' Book # %d ( %s ): %s ' % ( book_index , name , " , " . join ( outcomes_with_bets ) )
2015-01-12 21:52:19 +00:00
if redis_hget ( tname , ' closed ' ) :
2015-01-31 18:22:14 +00:00
msg = msg + " - closed "
2015-01-12 21:52:19 +00:00
elif redis_hexists ( tname , ' closing_time ' ) :
try :
closing_time = float ( redis_hget ( tname , ' closing_time ' ) )
2015-01-31 18:22:14 +00:00
msg = msg + ' - closing in %s ' % ( TimeToString ( closing_time - time . time ( ) ) )
2015-01-12 21:52:19 +00:00
except Exception , e :
log_error ( ' Failed to get closing time: %s ' % ( str ( e ) ) )
2015-01-31 18:22:14 +00:00
link . send ( msg )
for book_index in active_books . keys ( ) :
book_index = long ( book_index )
tname = ' bookie: %s ' % book_index
try :
name = redis_hget ( tname , ' name ' )
outcome = redis_hget ( tname , identity + " :outcome " )
units = redis_hget ( tname , identity + " :units " )
except Exception , e :
log_error ( ' Book: Failed to retrieve book %d : %s ' % ( book_index , str ( e ) ) )
link . send ( ' An error occured ' )
return
2015-01-12 21:52:19 +00:00
if outcome :
2015-01-31 18:22:14 +00:00
link . send ( ' %s has %s on %s in book # %d ( %s ) ' % ( NickFromIdentity ( identity ) , AmountToString ( units ) , outcome , book_index , name ) )
2015-01-12 21:52:19 +00:00
def Bet ( link , cmd ) :
identity = link . identity ( )
SweepClosingTimes ( )
2015-01-31 18:59:30 +00:00
res0 , res1 = GetBookIndex ( cmd , 2 )
if res0 == None :
link . send ( res1 )
2015-01-12 21:52:19 +00:00
return
2015-01-31 18:59:30 +00:00
book_index = res0
parm_offset = res1
2015-01-12 21:52:19 +00:00
tname = " bookie: %d " % book_index
book_name = redis_hget ( tname , ' name ' )
if redis_hget ( tname , ' closed ' ) :
link . send ( ' The %s book is closed to new bets ' % book_name )
return
outcome = GetParam ( cmd , 1 + parm_offset )
amount = GetParam ( cmd , 2 + parm_offset )
if not outcome or not amount :
link . send ( ' usage: !bet [<event name>] <outcome> <amount> ' )
return
try :
2015-02-02 12:22:20 +00:00
units = StringToUnits ( amount )
2015-01-12 21:52:19 +00:00
except Exception , e :
link . send ( ' usage: !bet [<event name>] <outcome> <amount> ' )
return
if units < = 0 :
link . send ( " Invalid amount " )
return
valid , reason = IsBetValid ( link , amount , config . bookie_min_bet , config . bookie_max_bet , 0 , 0 , 0 )
if not valid :
log_info ( " Bookie: %s ' s bet refused: %s " % ( identity , reason ) )
link . send ( " %s : %s " % ( link . user . nick , reason ) )
return
outcomes = redis_smembers ( tname + ' :outcomes ' )
if not outcome in outcomes :
2015-01-31 19:14:48 +00:00
link . send ( " %s is not a valid outcome for %s , try one of: %s " % ( outcome , book_name , " , " . join ( outcomes ) ) )
2015-01-12 21:52:19 +00:00
return
if redis_hexists ( tname , identity + " :outcome " ) :
previous_outcome = redis_hget ( tname , identity + " :outcome " )
if previous_outcome != outcome :
link . send ( " %s : you can only bet on one outcome per book, and you already bet on %s " % ( NickFromIdentity ( identity ) , previous_outcome ) )
return
log_info ( ' %s wants to bet %s on %s ' % ( identity , AmountToString ( units ) , outcome ) )
try :
log_info ( ' Bet: %s betting %s on outcome %s ' % ( identity , AmountToString ( units ) , outcome ) )
2015-01-31 23:40:13 +00:00
account = GetAccount ( link )
2015-01-12 21:52:19 +00:00
try :
p = redis_pipeline ( )
2015-01-31 23:40:13 +00:00
p . hincrby ( " balances " , account , - units )
2015-01-12 21:52:19 +00:00
p . hincrby ( " earmarked " , " bookie " , units )
p . hincrby ( tname + " :bets " , outcome , units )
p . hincrby ( tname , " bets " , units )
p . hset ( tname , identity + " :outcome " , outcome )
p . hincrby ( tname , identity + " :units " , units )
p . sadd ( tname + " :bettors " , identity )
p . execute ( )
2015-01-29 20:17:25 +00:00
total_bet = long ( redis_hget ( tname , identity + " :units " ) )
2015-01-12 21:52:19 +00:00
if total_bet == units :
link . send ( " %s has bet %s on %s for %s " % ( NickFromIdentity ( identity ) , AmountToString ( units ) , outcome , book_name ) )
else :
link . send ( " %s has bet another %s on %s for %s , for a total of %s " % ( NickFromIdentity ( identity ) , AmountToString ( units ) , outcome , book_name , AmountToString ( total_bet ) ) )
except Exception , e :
log_error ( " Bet: Error updating redis: %s " % str ( e ) )
link . send ( " An error occured " )
return
except Exception , e :
log_error ( ' Bet: exception: %s ' % str ( e ) )
link . send ( " An error has occured " )
def Result ( link , cmd ) :
identity = link . identity ( )
SweepClosingTimes ( )
2015-01-31 18:59:30 +00:00
res0 , res1 = GetBookIndex ( cmd , 1 )
if res0 == None :
link . send ( res1 )
2015-01-12 21:52:19 +00:00
return
2015-01-31 18:59:30 +00:00
book_index = res0
parm_offset = res1
2015-01-12 21:52:19 +00:00
tname = " bookie: %d " % book_index
book_name = redis_hget ( tname , ' name ' )
outcome = GetParam ( cmd , 1 + parm_offset )
if not outcome :
link . send ( ' usage: !result [<event name>] <outcome> ' )
return
outcomes = redis_smembers ( tname + ' :outcomes ' )
if not outcome in outcomes :
link . send ( " %s is not a valid outcome for %s , try one of: %s " % ( outcome , book_name , " , " . join ( outcomes ) ) )
return
log_info ( ' %s calls %s on book %d ' % ( identity , outcome , book_index ) )
try :
p = redis_pipeline ( )
total_units_bet = long ( redis_hget ( tname , " bets " ) or 0 )
total_units_bet_by_winners = long ( redis_hget ( tname + " :bets " , outcome ) or 0 )
resultmsg = [ ]
bettors = redis_smembers ( tname + ' :bettors ' )
p . hincrby ( " earmarked " , " bookie " , - total_units_bet )
for bettor in bettors :
o = redis_hget ( tname , bettor + " :outcome " )
ounits = long ( redis_hget ( tname , bettor + " :units " ) )
if o == outcome :
2015-01-31 23:40:13 +00:00
a = GetAccount ( bettor )
2015-01-12 21:52:19 +00:00
owinunits = long ( total_units_bet * ( 1 - config . bookie_fee ) * ounits / total_units_bet_by_winners )
2015-01-27 22:13:11 +00:00
if owinunits < ounits :
2015-02-01 11:36:11 +00:00
owinunits = ounits
2015-01-12 21:52:19 +00:00
resultmsg . append ( " %s wins %s " % ( NickFromIdentity ( bettor ) , AmountToString ( owinunits ) ) )
2015-01-31 23:40:13 +00:00
p . hincrby ( " balances " , a , owinunits )
2015-01-12 21:52:19 +00:00
else :
resultmsg . append ( " %s loses %s " % ( NickFromIdentity ( bettor ) , AmountToString ( ounits ) ) )
p . hdel ( ' bookie:active ' , book_index )
p . execute ( )
if len ( bettors ) == 0 :
resultmsg = [ " nobody had bet " ]
log_info ( ' Book outcome is %s - %s ' % ( outcome , " , " . join ( resultmsg ) ) )
link . send ( ' Book # %d ( %s ) outcome is %s - %s ' % ( book_index , book_name , outcome , " , " . join ( resultmsg ) ) )
except Exception , e :
log_error ( ' Result: Failed to process result: %s ' % str ( e ) )
link . send ( ' An error occured ' )
return
def Help ( link ) :
link . send_private ( " The bookie module allows you to bet on particular events " )
link . send_private ( " Basic usage: !bet outcome amount " )
link . send_private ( " Administrators can setup a book over any particular event " )
link . send_private ( " Anyone can then bet on one of the available outcomes until the book " )
link . send_private ( " closes to new bets (a player can bet only bet on one outcome per book) " )
link . send_private ( " After the event result is in, winners share the total amount bet " )
link . send_private ( " (minus bookie fee) pro rata to their original bet amount " )
link . send_private ( " Once placed, a bet may not be cancelled (unless the book itself " )
link . send_private ( " is cancelled, in which case every bettor gets a full refund) " )
2015-01-31 19:15:14 +00:00
link . send_private ( " Minimum bet %s , maximum bet %s " % ( config . bookie_min_bet , config . bookie_max_bet ) )
2015-01-12 21:52:19 +00:00
RegisterModule ( {
' name ' : __name__ ,
' help ' : Help ,
} )
RegisterCommand ( {
' module ' : __name__ ,
' name ' : ' bookie ' ,
' parms ' : ' <event name> <outcome1> <outcome2> [<outcome3>...] ' ,
' function ' : Bookie ,
' admin ' : True ,
' registered ' : True ,
' help ' : " start a bookie game - bookie fee %.1f %% " % ( float ( config . bookie_fee ) * 100 )
} )
RegisterCommand ( {
' module ' : __name__ ,
' name ' : ' cancel ' ,
2015-01-31 19:12:32 +00:00
' parms ' : ' [<event name> | #<id>] ' ,
2015-01-12 21:52:19 +00:00
' function ' : Cancel ,
' admin ' : True ,
' registered ' : True ,
' help ' : " cancels a running book, refunding everyone who bet on it "
} )
RegisterCommand ( {
' module ' : __name__ ,
' name ' : ' book ' ,
' function ' : Book ,
' registered ' : True ,
' help ' : " shows current book "
} )
RegisterCommand ( {
' module ' : __name__ ,
' name ' : ' close ' ,
2015-01-31 19:12:32 +00:00
' parms ' : ' [<event name> | #<id>] ' ,
2015-01-12 21:52:19 +00:00
' function ' : Close ,
' admin ' : True ,
' registered ' : True ,
' help ' : " close a book to new bets "
} )
RegisterCommand ( {
' module ' : __name__ ,
' name ' : ' schedule_close ' ,
2015-01-31 19:12:32 +00:00
' parms ' : ' [<event name> | #<id>] <minutes> ' ,
2015-01-12 21:52:19 +00:00
' function ' : ScheduleClose ,
' admin ' : True ,
' registered ' : True ,
' help ' : " schedule closing a book to new bets in X minutes "
} )
RegisterCommand ( {
' module ' : __name__ ,
' name ' : ' result ' ,
2015-01-31 19:12:32 +00:00
' parms ' : ' [<event name> | #<id>] <outcome> ' ,
2015-01-12 21:52:19 +00:00
' function ' : Result ,
' admin ' : True ,
' registered ' : True ,
' help ' : " declare the result of a running book, paying winners "
} )
RegisterCommand ( {
' module ' : __name__ ,
' name ' : ' bet ' ,
2015-01-31 19:12:32 +00:00
' parms ' : ' [<event name> | #<id>] <outcome> <amount> ' ,
2015-01-12 21:52:19 +00:00
' function ' : Bet ,
' registered ' : True ,
' help ' : " bet some %s on a particular outcome " % coinspecs . name
} )