tippero/tipbot/utils.py
moneromooo cea60feeae Create accounts on linking if they do not exist
This can happen if someone tries to link to another account
before doing any other action that creates an account.
2015-03-19 20:56:03 +00:00

417 lines
13 KiB
Python

#!/bin/python
#
# Cryptonote tipbot - utility functions
# Copyright 2014,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 redis
import hashlib
import json
import httplib
import time
import threading
import math
import string
from decimal import *
import tipbot.config as config
import tipbot.coinspecs as coinspecs
from tipbot.log import log_error, log_warn, log_info, log_log
from tipbot.link import Link
from tipbot.redisdb import *
registered_networks=dict()
networks=[]
cached_tipbot_balance=None
cached_tipbot_unlocked_balance=None
cached_tipbot_balance_timestamp=None
core_lock = threading.RLock()
def GetPassword(name):
try:
f = open('tipbot-password.txt', 'r')
for p in f:
p = p.strip("\r\n")
parts=p.split(':')
if parts[0]==name:
return parts[1]
except Exception,e:
log_error('could not fetch password: %s' % str(e))
raise
return "xxx"
finally:
f.close()
def IsParamPresent(parms,idx):
return len(parms) > idx
def GetParam(parms,idx):
if IsParamPresent(parms,idx):
return parms[idx]
return None
def GetPaymentID(link):
salt="2u3g55bkwrui32fi3g4bGR$j5g4ugnujb-"+coinspecs.name+"-";
p = hashlib.sha256(salt+link.identity()).hexdigest();
try:
redis_hset("paymentid",p,link.identity())
except Exception,e:
log_error('GetPaymentID: failed to set payment ID for %s to redis: %s' % (link.identity(),str(e)))
return p
def GetIdentityFromPaymentID(p):
if not redis_hexists("paymentid",p):
log_log('PaymentID %s not found' % p)
return None
identity = redis_hget("paymentid",p)
log_log('PaymentID %s => %s' % (p, str(identity)))
# HACK - grandfathering pre-network payment IDs
if identity.index(':') == -1:
log_warn('Pre-network payment ID found, assuming freenode')
identity = "freenode:"+identity
return identity
def IsValidAddress(address):
if len(address) < coinspecs.address_length[0] or len(address) > coinspecs.address_length[1]:
return False
for prefix in coinspecs.address_prefix:
if address.startswith(prefix):
return True
return False
def IsValidPaymentID(payment_id):
if len(payment_id)!=64:
return False
for char in payment_id:
if char not in string.hexdigits:
return False
return True
# Code taken from the Python documentation
def moneyfmt(value, places=2, curr='', sep=',', dp='.',
pos='', neg='-', trailneg=''):
"""Convert Decimal to a money formatted string.
places: required number of places after the decimal point
curr: optional currency symbol before the sign (may be blank)
sep: optional grouping separator (comma, period, space, or blank)
dp: decimal point indicator (comma or period)
only specify as blank when places is zero
pos: optional sign for positive numbers: '+', space or blank
neg: optional sign for negative numbers: '-', '(', space or blank
trailneg:optional trailing minus indicator: '-', ')', space or blank
>>> d = Decimal('-1234567.8901')
>>> moneyfmt(d, curr='$')
'-$1,234,567.89'
>>> moneyfmt(d, places=0, sep='.', dp='', neg='', trailneg='-')
'1.234.568-'
>>> moneyfmt(d, curr='$', neg='(', trailneg=')')
'($1,234,567.89)'
>>> moneyfmt(Decimal(123456789), sep=' ')
'123 456 789.00'
>>> moneyfmt(Decimal('-0.02'), neg='<', trailneg='>')
'<0.02>'
"""
q = Decimal(10) ** -places # 2 places --> '0.01'
sign, digits, exp = value.quantize(q).as_tuple()
result = []
digits = map(str, digits)
build, next = result.append, digits.pop
if sign:
build(trailneg)
for i in range(places):
build(next() if digits else '0')
build(dp)
if not digits:
build('0')
i = 0
while digits:
build(next())
i += 1
if i == 3 and digits:
i = 0
build(sep)
build(curr)
build(neg if sign else pos)
s = ''.join(reversed(result))
if dp in s:
s=s.strip('0').rstrip(dp)
if s=="" or s[0]==dp:
s="0"+s
return s
def AmountToString(amount):
if amount == None:
amount = 0
lamount=long(amount)
samount = None
if lamount == 0:
samount = "0 %s" % coinspecs.name
else:
places=long(0.5+math.log10(coinspecs.atomic_units))
for den in coinspecs.denominations:
if lamount < den[0]:
samount = moneyfmt(Decimal(lamount)/Decimal(den[1]),places=places) + " " + den[2]
break
if not samount:
samount = moneyfmt(Decimal(lamount)/Decimal(coinspecs.atomic_units),places=places) + " " + coinspecs.name
return samount
def TimeToString(seconds):
seconds=float(seconds)
if seconds < 1e-3:
return "%.2f microseconds" % (seconds*1e6)
if seconds < 1:
return "%.2f milliseconds" % (seconds*1e3)
if seconds < 60:
return "%.2f seconds" % (seconds)
if seconds < 3600:
return "%.2f minutes" % (seconds / 60)
if seconds < 3600 * 24:
return "%.2f hours" % (seconds / 3600)
if seconds < 3600 * 24 * 30.5:
return "%.2f days" % (seconds / (3600*24))
if seconds < 3600 * 24 * 365.25:
return "%.2f months" % (seconds / (3600*24*30.5))
if seconds < 3600 * 24 * 365.25 * 100:
return "%.2f years" % (seconds / (3600*24*365.25))
if seconds < 3600 * 24 * 365.25 * 1000:
return "%.2f centuries" % (seconds / (3600*24*365.25 * 100))
if seconds < 3600 * 24 * 365.25 * 1000000:
return "%.2f millenia" % (seconds / (3600*24*365.25 * 100))
return "like, forever, dude"
def StringToUnits(s):
try:
return long(Decimal(s)*long(coinspecs.atomic_units))
except Exception,e:
log_error('Failed to convert %s to units: %s' % (s,str(e)))
raise
def SendJSONRPCCommand(host,port,method,params):
try:
http = httplib.HTTPConnection(host,port,timeout=20)
except Exception,e:
log_error('SendJSONRPCCommand: Error connecting to %s:%u: %s' % (host, port, str(e)))
raise
d = dict(id="0",jsonrpc="2.0",method=method,params=params)
try:
j = json.dumps(d).encode()
except Exception,e:
log_error('SendJSONRPCCommand: Failed to encode JSON: %s' % str(e))
http.close()
raise
log_log('SendJSONRPCCommand: Sending json as body: %s' % j)
headers = None
try:
http.request("POST","/json_rpc",body=j)
except Exception,e:
log_error('SendJSONRPCCommand: Failed to post request: %s' % str(e))
http.close()
raise
response = http.getresponse()
if response.status != 200:
log_error('SendJSONRPCCommand: Error, received reply status %s' % str(response.status))
http.close()
raise RuntimeError("Error "+response.status)
s = response.read()
log_log('SendJSONRPCCommand: Received reply status %s: %s' % (response.status, str(s).replace('\r\n',' ').replace('\n',' ')))
try:
j = json.loads(s)
except Exception,e:
log_error('SendJSONRPCCommand: Failed to decode JSON: %s' % str(e))
http.close()
raise
http.close()
return j
def SendHTMLCommand(host,port,method):
try:
http = httplib.HTTPConnection(host,port,timeout=20)
except Exception,e:
log_error('SendHTMLCommand: Error connecting to %s:%u: %s' % (host, port, str(e)))
raise
headers = None
try:
http.request("POST","/"+method)
except Exception,e:
log_error('SendHTMLCommand: Failed to post request: %s' % str(e))
http.close()
raise
response = http.getresponse()
if response.status != 200:
log_error('SendHTMLCommand: Error, received reply status %s' % str(response.status))
http.close()
raise RuntimeError("Error "+response.status)
s = response.read()
log_log('SendHTMLCommand: Received reply status %s: %s' % (response.status,s.replace('\r\n',' ').replace('\n',' ')))
try:
j = json.loads(s)
except Exception,e:
log_error('SendHTMLCommand: Failed to decode JSON: %s' % str(e))
http.close()
raise
http.close()
return j
def SendWalletJSONRPCCommand(method,params):
return SendJSONRPCCommand(config.wallet_host,config.wallet_port,method,params)
def SendDaemonJSONRPCCommand(method,params):
return SendJSONRPCCommand(config.daemon_host,config.daemon_port,method,params)
def SendDaemonHTMLCommand(method):
return SendHTMLCommand(config.daemon_host,config.daemon_port,method)
def RetrieveTipbotBalance(force_refresh=False):
global cached_tipbot_balance, cached_tipbot_unlocked_balance, cached_tipbot_balance_timestamp
if not force_refresh and cached_tipbot_balance_timestamp and time.time()-cached_tipbot_balance_timestamp < config.tipbot_balance_cache_time:
return cached_tipbot_balance, cached_tipbot_unlocked_balance
j = SendWalletJSONRPCCommand("getbalance",None)
if not "result" in j:
log_error('RetrieveTipbotBalance: result not found in reply')
raise RuntimeError("")
return
result = j["result"]
if not "balance" in result:
log_error('RetrieveTipbotBalance: balance not found in result')
raise RuntimeError("")
return
if not "unlocked_balance" in result:
log_error('RetrieveTipbotBalance: unlocked_balance not found in result')
raise RuntimeError("")
return
balance = result["balance"]
unlocked_balance = result["unlocked_balance"]
log_log('RetrieveTipbotBalance: balance: %s' % str(balance))
log_log('RetrieveTipbotBalance: unlocked_balance: %s' % str(unlocked_balance))
pending = long(balance)-long(unlocked_balance)
if pending < 0:
log_error('RetrieveTipbotBalance: Negative pending balance! balance %s, unlocked %s' % (str(balance),str(unlocked_balance)))
raise RuntimeError("")
return
cached_tipbot_balance_timestamp=time.time()
cached_tipbot_balance=balance
cached_tipbot_unlocked_balance=unlocked_balance
return balance, unlocked_balance
def GetAccount(link_or_identity):
if isinstance(link_or_identity,Link):
identity=link_or_identity.identity()
else:
identity=link_or_identity
account = redis_hget('accounts',identity)
if account == None:
log_info('No account found for %s, creating new one' % identity)
next_account_id = long(redis_get('next_account_id') or 0)
account = next_account_id
if redis_hexists('accounts',account):
raise RuntimeError('Next account ID already exists (%d)', account)
redis_hset('accounts',identity,account)
next_account_id += 1
redis_set('next_account_id',next_account_id)
return account
def RetrieveBalance(link):
try:
account = GetAccount(link)
balance = redis_hget("balances",account) or 0
confirming = redis_hget("confirming_payments",account) or 0
return long(balance), long(confirming)
except Exception, e:
log_error('RetrieveBalance: exception: %s' % str(e))
raise
def LinkCore(link,other_identity):
try:
identity=link.identity()
if identity==other_identity:
return True, "same-identity"
links=redis_hget('links',identity)
if links:
if other_identity in links.split(chr(0)):
return True, "already"
links=links+chr(0)+other_identity
else:
links=other_identity
redis_hset('links',identity,links)
links=redis_hget('links',other_identity)
if links:
if identity in links.split(chr(0)):
# we have both
account=GetAccount(identity)
other_account=GetAccount(other_identity)
if account==other_account:
log_info('%s and %s already have the same account: %s' % (identity,other_identity,account))
return True, "same-account"
balance=long(redis_hget('balances',account))
log_info('Linking accounts %s (%s) and %s (%s)' % (account,identity,other_account,other_identity))
p=redis_pipeline()
p.hincrby('balances',other_account,balance)
p.hincrby('balances',account,-balance)
accounts=redis_hgetall('accounts')
for a in accounts:
if accounts[a]==account:
log_info('Changing %s\'s account from %s to %s' % (a,account,other_account))
p.hset('accounts',a,other_account)
p.execute()
return True, "linked"
except Exception,e:
log_error('Error linking %s and %s: %s' % (identity,other_identity,str(e)))
return False, "error"
return True, "ok"
def IdentityFromString(link,s):
if s.find(':') == -1:
network = link.network
nick=s
else:
parts=s.split(':')
network_name=parts[0]
network=GetNetworkByName(network_name)
if not network:
log_error('unknown network: %s' % network_name)
raise RuntimeError('Unknown network: %s' % network_name)
nick=parts[1]
return network.name+':'+network.canonicalize(nick)
def NickFromIdentity(identity):
return identity.split(':')[1]
def RegisterNetwork(name,type):
registered_networks[name]=type
def AddNetwork(network):
networks.append(network)
def GetNetworkByName(name):
for network in networks:
if network.name==name:
return network
return None
def GetNetworkByType(type):
for network in networks:
if isinstance(network,type):
return network
return None
def Lock():
return core_lock.acquire()
def Unlock():
core_lock.release()
return True