mirror of
https://git.wownero.com/wownero/tippero.git
synced 2024-08-15 00:33:14 +00:00
462 lines
15 KiB
Python
462 lines
15 KiB
Python
#!/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.user import User
|
|
from tipbot.link import Link
|
|
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)))
|
|
|
|
def GetBookIndex(cmd,base_arg_count):
|
|
active_books=GetActiveBooks()
|
|
if len(active_books) == 0:
|
|
return None, 'There is no open book'
|
|
|
|
if GetParam(cmd,base_arg_count+1):
|
|
name = GetParam(cmd,1)
|
|
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
|
|
else:
|
|
if len(active_books) > 1:
|
|
return None, 'There are several open books: %s' % ", ".join(active_books.values())
|
|
book_index = long(active_books.keys()[0])
|
|
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
|
|
|
|
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))
|
|
a = GetAccount(bettor)
|
|
p.hincrby('balances',a,units)
|
|
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()
|
|
|
|
res0, res1 = GetBookIndex(cmd,0)
|
|
if res0 == None:
|
|
link.send(res1)
|
|
return
|
|
book_index = res0
|
|
parm_offset = res1
|
|
|
|
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()
|
|
|
|
res0, res1 = GetBookIndex(cmd,0)
|
|
if res0 == None:
|
|
link.send(res1)
|
|
return
|
|
book_index = res0
|
|
parm_offset = res1
|
|
|
|
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
|
|
|
|
for book_index in sorted(active_books.keys()):
|
|
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')
|
|
outcomes_with_bets = []
|
|
for o in outcomes:
|
|
ou = long(redis_hget(tname+":bets",o) or 0)
|
|
if ou > 0:
|
|
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))
|
|
if redis_hget(tname,'closed'):
|
|
msg = msg + " - closed"
|
|
elif redis_hexists(tname,'closing_time'):
|
|
try:
|
|
closing_time=float(redis_hget(tname,'closing_time'))
|
|
msg = msg + ' - closing in %s' % (TimeToString(closing_time-time.time()))
|
|
except Exception,e:
|
|
log_error('Failed to get closing time: %s' % (str(e)))
|
|
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
|
|
if outcome:
|
|
link.send('%s has %s on %s in book #%d (%s)' % (NickFromIdentity(identity), AmountToString(units), outcome, book_index, name))
|
|
|
|
def Bet(link,cmd):
|
|
identity=link.identity()
|
|
|
|
SweepClosingTimes()
|
|
|
|
res0, res1 = GetBookIndex(cmd,2)
|
|
if res0 == None:
|
|
link.send(res1)
|
|
return
|
|
book_index = res0
|
|
parm_offset = res1
|
|
|
|
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:
|
|
units = StringToUnits(amount)
|
|
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:
|
|
link.send("%s is not a valid outcome for %s, try one of: %s" % (outcome, book_name, ", ".join(outcomes)))
|
|
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))
|
|
account = GetAccount(link)
|
|
try:
|
|
p = redis_pipeline()
|
|
p.hincrby("balances",account,-units)
|
|
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()
|
|
total_bet=long(redis_hget(tname,identity+":units"))
|
|
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()
|
|
|
|
res0, res1 = GetBookIndex(cmd,1)
|
|
if res0 == None:
|
|
link.send(res1)
|
|
return
|
|
book_index = res0
|
|
parm_offset = res1
|
|
|
|
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:
|
|
a = GetAccount(bettor)
|
|
owinunits = long(total_units_bet * (1-config.bookie_fee) * ounits / total_units_bet_by_winners)
|
|
if owinunits<ounits:
|
|
owinunits=ounits
|
|
resultmsg.append("%s wins %s" % (NickFromIdentity(bettor), AmountToString(owinunits)))
|
|
p.hincrby("balances",a,owinunits)
|
|
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)")
|
|
link.send_private("Minimum bet %s, maximum bet %s" % (config.bookie_min_bet, config.bookie_max_bet))
|
|
|
|
|
|
|
|
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',
|
|
'parms': '[<event name> | #<id>]',
|
|
'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',
|
|
'parms': '[<event name> | #<id>]',
|
|
'function': Close,
|
|
'admin': True,
|
|
'registered': True,
|
|
'help': "close a book to new bets"
|
|
})
|
|
RegisterCommand({
|
|
'module': __name__,
|
|
'name': 'schedule_close',
|
|
'parms': '[<event name> | #<id>] <minutes>',
|
|
'function': ScheduleClose,
|
|
'admin': True,
|
|
'registered': True,
|
|
'help': "schedule closing a book to new bets in X minutes"
|
|
})
|
|
RegisterCommand({
|
|
'module': __name__,
|
|
'name': 'result',
|
|
'parms': '[<event name> | #<id>] <outcome>',
|
|
'function': Result,
|
|
'admin': True,
|
|
'registered': True,
|
|
'help': "declare the result of a running book, paying winners"
|
|
})
|
|
RegisterCommand({
|
|
'module': __name__,
|
|
'name': 'bet',
|
|
'parms': '[<event name> | #<id>] <outcome> <amount>',
|
|
'function': Bet,
|
|
'registered': True,
|
|
'help': "bet some %s on a particular outcome" % coinspecs.name
|
|
})
|