├── doc └── index.rst ├── src ├── logs │ └── dummy ├── ctb │ ├── __init__.py │ ├── ctb_log.py │ ├── ctb_db.py │ ├── ctb_exchange.py │ ├── ctb_misc.py │ ├── ctb_coin.py │ ├── ctb_stats.py │ └── ctb_user.py ├── tpl │ └── jinja2 │ │ ├── announcement.tpl │ │ ├── pending-tips-declined.tpl │ │ ├── command-disabled.tpl │ │ ├── no-coin-balances.tpl │ │ ├── redeem-cant-compute.tpl │ │ ├── rates-error.tpl │ │ ├── redeem-already-done.tpl │ │ ├── redeem-low-karma.tpl │ │ ├── no-pending-tips.tpl │ │ ├── not-on-reddit.tpl │ │ ├── address-invalid.tpl │ │ ├── history.tpl │ │ ├── not-registered.tpl │ │ ├── tip-already-pending.tpl │ │ ├── didnt-understand.tpl │ │ ├── redeem-low-funds.tpl │ │ ├── tip-sent.tpl │ │ ├── tip-below-minimum.tpl │ │ ├── tip-went-wrong.tpl │ │ ├── rates.tpl │ │ ├── redeem-confirmation.tpl │ │ ├── tip-low-balance.tpl │ │ ├── info.tpl │ │ ├── tip-received.tpl │ │ ├── tip-incoming.tpl │ │ ├── footer.tpl │ │ └── confirmation.tpl ├── conf-sample │ ├── logs.yml │ ├── misc.yml │ ├── fiat.yml │ ├── coins.yml │ ├── reddit.yml │ ├── exchanges.yml │ ├── db.yml │ ├── keywords.yml │ └── regex.yml ├── _start.sh ├── _add_coin.py ├── _update_stats.py ├── _backup_config.py ├── _backup_db.py ├── _backup_wallets.py └── cointipbot.py ├── .gitignore ├── altcointip.sql ├── README.md └── LICENSE /doc/index.rst: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/logs/dummy: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ctb/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tpl/jinja2/announcement.tpl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tpl/jinja2/pending-tips-declined.tpl: -------------------------------------------------------------------------------- 1 | Hello {{ user_from | replace('_', '\_') }}, your pending tips have been declined. 2 | 3 | {% include 'footer.tpl' %} 4 | -------------------------------------------------------------------------------- /src/tpl/jinja2/command-disabled.tpl: -------------------------------------------------------------------------------- 1 | Sorry {{ a.u_from.name | replace('_', '\_') }}, action type '{{ a.type }}' is currently disabled. Please try again later. 2 | 3 | {% include 'footer.tpl' %} 4 | -------------------------------------------------------------------------------- /src/tpl/jinja2/no-coin-balances.tpl: -------------------------------------------------------------------------------- 1 | {% set user_from = a.u_from.name %} 2 | Sorry {{ user_from | replace('_', '\_') }}, you don't have any coin balances enough for a tip. 3 | 4 | {% include 'footer.tpl' %} 5 | -------------------------------------------------------------------------------- /src/tpl/jinja2/redeem-cant-compute.tpl: -------------------------------------------------------------------------------- 1 | I'm sorry {{ a.u_from.name | replace('_', '\_') }}, I can't get the exchange rate for {{ ctb.conf.coins[a.coin].name }} at this time. Please try again later. 2 | 3 | {% include 'footer.tpl' %} 4 | -------------------------------------------------------------------------------- /src/tpl/jinja2/rates-error.tpl: -------------------------------------------------------------------------------- 1 | {% set user = a.u_from.name %} 2 | Hey {{ user | replace('_', '\_') }}, I couldn't get the rates for you because exchange **{{ exchange }}** didn't respond. Please try again later. 3 | 4 | {% include 'footer.tpl' %} 5 | -------------------------------------------------------------------------------- /src/tpl/jinja2/redeem-already-done.tpl: -------------------------------------------------------------------------------- 1 | I'm sorry {{ a.u_from.name | replace('_', '\_') }}, you've already redeemed your {{ coin }} karma. 2 | 3 | {% if coin %} 4 | Try redeeming a different coin. 5 | {% endif %} 6 | 7 | {% include 'footer.tpl' %} 8 | -------------------------------------------------------------------------------- /src/tpl/jinja2/redeem-low-karma.tpl: -------------------------------------------------------------------------------- 1 | I'm sorry {{ a.u_from.name | replace('_', '\_') }}, you total karma needs to be greater or equal to {{ ctb.conf.reddit.redeem.min_karma }} for this feature (you have {{ user_karma }}). 2 | 3 | {% include 'footer.tpl' %} 4 | -------------------------------------------------------------------------------- /src/tpl/jinja2/no-pending-tips.tpl: -------------------------------------------------------------------------------- 1 | I'm sorry {{ user_from | replace('_', '\_') }}, you don't have any pending tips. Perhaps they've been already confirmed, or already expired. Tips are auto-confirmed after you've registered. 2 | 3 | {% include 'footer.tpl' %} 4 | -------------------------------------------------------------------------------- /src/tpl/jinja2/not-on-reddit.tpl: -------------------------------------------------------------------------------- 1 | {% set user_from = a.u_from.name %} 2 | {% set user_to = a.u_to.name %} 3 | I'm sorry {{ user_from | replace('_', '\_') }}, your tip has failed because user **{{ user_to | replace('_', '\_') }}** is not on Reddit. 4 | 5 | {% include 'footer.tpl' %} 6 | -------------------------------------------------------------------------------- /src/tpl/jinja2/address-invalid.tpl: -------------------------------------------------------------------------------- 1 | {% set user_from = a.u_from.name %} 2 | {% set coin_name = a.coin.upper() %} 3 | {% set address = a.addr_to %} 4 | I'm sorry {{ user_from | replace('_', '\_') }}, __{{ coin_name }}__ address __{{ address | escape }}__ appears to be invalid (is there a typo?). 5 | 6 | {% include 'footer.tpl' %} 7 | -------------------------------------------------------------------------------- /src/tpl/jinja2/history.tpl: -------------------------------------------------------------------------------- 1 | {% set user = a.u_from.name %} 2 | 3 | Hello {{ user | replace('_', '\_') }}, here are your last {{ limit }} transactions. 4 | 5 | {{ "|".join(keys) }} 6 | {{ "|".join([":---"] * (keys|length)) }} 7 | {% for h in history %} 8 | {{ "|".join(h) }} 9 | {% endfor %} 10 | 11 | {% include 'footer.tpl' %} 12 | -------------------------------------------------------------------------------- /src/tpl/jinja2/not-registered.tpl: -------------------------------------------------------------------------------- 1 | {% set user_from = a.u_from.name %} 2 | {% set user_bot = ctb.conf.reddit.auth.user %} 3 | I'm sorry {{ user_from | replace('_', '\_') }}, we've never met. Please __[+register](http://www.reddit.com/message/compose?to={{ user_bot }}&subject=register&message=%2Bregister)__ first! 4 | 5 | {% include 'footer.tpl' %} 6 | -------------------------------------------------------------------------------- /src/tpl/jinja2/tip-already-pending.tpl: -------------------------------------------------------------------------------- 1 | {% set user_from = a.u_from.name %} 2 | {% set user_to = a.u_to.name %} 3 | {% set coin_name = ctb.conf.coins[a.coin].name %} 4 | I'm sorry {{ user_from | replace('_', '\_') }}, /u/{{ user_to }} already has a pending {{ coin_name }} tip from you. Please wait until it's accepted, declined, or expired. 5 | 6 | {% include 'footer.tpl' %} 7 | -------------------------------------------------------------------------------- /src/tpl/jinja2/didnt-understand.tpl: -------------------------------------------------------------------------------- 1 | {% if ctb and ctb.conf.reddit.help.enabled %} 2 | {% set help_link = "[verify syntax](%s)" % ctb.conf.reddit.help.url %} 3 | {% else %} 4 | {% set help_link = "verify syntax" %} 5 | {% endif %} 6 | Sorry {{ user_from | replace('_', '\_') }}, I didn't understand your {{ what }}. Please {{ help_link }} and try again. 7 | 8 | {% set user = user_from %} 9 | {% include 'footer.tpl' %} 10 | -------------------------------------------------------------------------------- /src/tpl/jinja2/redeem-low-funds.tpl: -------------------------------------------------------------------------------- 1 | {% set coinval_fmt = "%s%.6g" % (ctb.conf.coins[a.coin].symbol, a.coinval) %} 2 | {% set fiatval_fmt = "%s%.3f" % (ctb.conf.fiat[a.fiat].symbol, a.fiatval) %} 3 | I'm sorry {{ a.u_from.name | replace('_', '\_') }}, I don't have enough {{ ctb.conf.coins[a.coin].name }}s to give you for your karma (you would've gotten {{ coinval_fmt }} {{ ctb.conf.coins[a.coin].name }}s ({{ fiatval_fmt }})). Try again later, or pick a different coin. 4 | 5 | {% include 'footer.tpl' %} 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | # cointipbot 38 | *.log* 39 | pifkoin 40 | conf 41 | -------------------------------------------------------------------------------- /src/conf-sample/logs.yml: -------------------------------------------------------------------------------- 1 | levels: 2 | warning: 3 | enabled: true 4 | append: true 5 | format: '%(levelname)s %(asctime)s %(message)s' 6 | filename: logs/warning.log 7 | info: 8 | enabled: true 9 | append: true 10 | format: '%(levelname)s %(asctime)s %(message)s' 11 | filename: logs/info.log 12 | debug: 13 | enabled: false 14 | append: false 15 | format: '%(levelname)s %(asctime)s %(message)s' 16 | filename: logs/debug.log 17 | -------------------------------------------------------------------------------- /src/conf-sample/misc.yml: -------------------------------------------------------------------------------- 1 | times: 2 | sleep_seconds: 60 3 | expire_pending_hours: 48 4 | 5 | backup: 6 | encryptionpassphrase: 'ChangeAndRememberMe11' 7 | 8 | qr_service_url: 'https://chart.googleapis.com/chart?cht=qr&choe=UTF-8&chs=300x300&chl=' 9 | 10 | notify: 11 | enabled: false 12 | addr_from: 'user@example.com' 13 | addr_to: 'you@example.com' 14 | subject: 'ALTcointip error' 15 | smtp_host: 'smtp.gmail.com:587' 16 | smtp_tls: true 17 | smtp_username: 'user@example.com' 18 | smtp_password: 'examplePass' 19 | -------------------------------------------------------------------------------- /src/tpl/jinja2/tip-sent.tpl: -------------------------------------------------------------------------------- 1 | {% set user_from = a.u_from.name %} 2 | {% set user_to = a.u_to.name %} 3 | {% set coinval_fmt = "%s%.6g %s" % (ctb.conf.coins[a.coin].symbol, a.coinval, ctb.conf.coins[a.coin].name) %} 4 | {% set fiatval_fmt = "%s%.6g" % (ctb.conf.fiat[a.fiat].symbol, a.fiatval) %} 5 | Hey {{ user_from | replace('_', '\_') }}, you have successfully sent a __{{ coinval_fmt }} ({{ fiatval_fmt }})__ tip to /u/{{ user_to }}. 6 | 7 | wow 8 | 9 | much tip 10 | 11 | such generosity 12 | 13 | {% set user = user_from %} 14 | {% include 'footer.tpl' %} 15 | -------------------------------------------------------------------------------- /src/_start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | user=$1 4 | 5 | if [ ! -f ./cointipbot.py ] ; then 6 | # Output help message and exit 7 | echo "Usage: $0 [username]" 8 | echo "if [username] is specified, script will be started as user [username]" 9 | echo "$0 must be run from 'src' directory of ALTcointip" 10 | exit 1 11 | fi 12 | 13 | if [ -z "$user" ] ; then 14 | # Run as current user 15 | python -c 'import cointipbot; cb=cointipbot.CointipBot(); cb.main()' 16 | else 17 | # Run as $user 18 | sudo su - $user -c "cd `pwd` && python -c 'import cointipbot; cb=cointipbot.CointipBot(); cb.main()'" 19 | fi 20 | -------------------------------------------------------------------------------- /src/tpl/jinja2/tip-below-minimum.tpl: -------------------------------------------------------------------------------- 1 | {% set user_from = a.u_from.name %} 2 | {% set amount_fmt = "%.8g %s" % (a.coinval, a.coin.upper()) %} 3 | {% set min_fmt = "%.10g" % min_value %} 4 | {% set coin_name = ctb.conf.coins[a.coin].name %} 5 | I'm sorry {{ user_from | replace('_', '\_') }}, your tip/withdraw of __{{ amount_fmt }}__ is below minimum of __{{ min_fmt }}__. I cannot process very small transactions because of high network fee requirement. 6 | 7 | If you really need to withdraw this amount, try depositing some {{ coin_name }}s to meet the minimum limit, then withdrawing everything. 8 | 9 | {% include 'footer.tpl' %} 10 | -------------------------------------------------------------------------------- /src/tpl/jinja2/tip-went-wrong.tpl: -------------------------------------------------------------------------------- 1 | {% set user_from = a.u_from.name %} 2 | {% if a.u_to %} 3 | {% set user_to_fmt = "/u/%s" % a.u_to.name %} 4 | {% else %} 5 | {% set user_to_fmt = a.addr_to %} 6 | {% endif %} 7 | {% set coin_amount = a.coinval %} 8 | {% set coin_name = ctb.conf.coins[a.coin].name %} 9 | {% set coin_amount_fmt = " %.3f %s(s)" % (coin_amount, coin_name) %} 10 | Hey {{ user_from | replace('_', '\_') }}, something went wrong and your tip/withdraw of **{{ coin_amount_fmt }}** to **{{ user_to_fmt }}** may not have been processed. Developer has been notified, and will look into the issue as soon as possible. 11 | 12 | {% include 'footer.tpl' %} 13 | -------------------------------------------------------------------------------- /src/tpl/jinja2/rates.tpl: -------------------------------------------------------------------------------- 1 | {% set user = a.u_from.name %} 2 | 3 | Hello {{ user | replace('_', '\_') }}, here are the latest exchange rates. 4 | 5 | coin|average{% for e in exchanges %}{{ "|" + e }}{% endfor %} 6 | 7 | :---|---:{% for e in exchanges %}{{ "|---:" }}{% endfor %} 8 | {% for c in coins %} 9 | {{ "\n**%s (%s)**|%s%.4g^%s%.2f" % (ctb.conf.coins[c].name, c.upper(), ctb.conf.coins.btc.symbol, rates[c]['average'].btc, ctb.conf.fiat[fiat].symbol, rates[c]['average'][fiat]) }}{% for e in exchanges %} 10 | {% if rates[c][e].btc and rates[c][e][fiat] %}{{ "|%s%.4g^%s%.2f" % (ctb.conf.coins.btc.symbol, rates[c][e].btc, ctb.conf.fiat[fiat].symbol, rates[c][e][fiat]) }}{% else %}{{ "|-" }}{% endif %} 11 | {% endfor %} 12 | {% endfor %} 13 | 14 | 15 | {% include 'footer.tpl' %} 16 | -------------------------------------------------------------------------------- /src/tpl/jinja2/redeem-confirmation.tpl: -------------------------------------------------------------------------------- 1 | {% if a.coinval: %} 2 | {% if a.coinval < 1.0 %} 3 | {% set coin_amount = ( a.coinval * 1000.0 ) %} 4 | {% set amount_prefix_short = "m" %} 5 | {% set amount_prefix_long = "milli" %} 6 | {% elif a.coinval >= 1000.0 %} 7 | {% set coin_amount = ( a.coinval / 1000.0 ) %} 8 | {% set amount_prefix_short = "M" %} 9 | {% set amount_prefix_long = "Mega" %} 10 | {% else %} 11 | {% set coin_amount = a.coinval %} 12 | {% endif %} 13 | {% endif %} 14 | {% set coinval_fmt = "%s%s%.6g %s%s" % (amount_prefix_short, ctb.conf.coins[a.coin].symbol, coin_amount, amount_prefix_long, ctb.conf.coins[a.coin].name) %} 15 | {% set fiatval_fmt = "%s%.3f" % (ctb.conf.fiat[a.fiat].symbol, a.fiatval) %} 16 | Hey {{ a.u_from.name | replace('_', '\_') }}, you have received __{{ coinval_fmt }} ({{ fiatval_fmt }})__ for your karma. 17 | 18 | {% include 'footer.tpl' %} 19 | -------------------------------------------------------------------------------- /src/conf-sample/fiat.yml: -------------------------------------------------------------------------------- 1 | # USD 2 | usd: 3 | enabled: true 4 | unit: usd 5 | symbol: '$' 6 | name: US Dollar 7 | regex: 8 | units: '(\$|usd)' 9 | 10 | # CNY 11 | cny: 12 | enabled: true 13 | unit: cny 14 | symbol: '¥' 15 | name: Chinese Yuan 16 | regex: 17 | units: '(¥|cny)' 18 | 19 | # EUR 20 | eur: 21 | enabled: true 22 | unit: eur 23 | symbol: '€' 24 | name: Euro 25 | regex: 26 | units: '(€|eur)' 27 | 28 | # GBP 29 | gbp: 30 | enabled: true 31 | unit: gbp 32 | symbol: '£' 33 | name: British Pound 34 | regex: 35 | units: '(£|gbp)' 36 | 37 | # JPY 38 | jpy: 39 | enabled: true 40 | unit: jpy 41 | symbol: '¥' 42 | name: Japanese Yen 43 | regex: 44 | units: '(¥|jpy)' 45 | 46 | # RUB 47 | rub: 48 | enabled: true 49 | unit: rub 50 | symbol: 'R' 51 | name: Russian Ruble 52 | regex: 53 | units: '(r|rur)' 54 | -------------------------------------------------------------------------------- /src/ctb/ctb_log.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of ALTcointip. 3 | 4 | ALTcointip is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | ALTcointip is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with ALTcointip. If not, see . 16 | """ 17 | 18 | import logging 19 | 20 | class LevelFilter(logging.Filter): 21 | def __init__(self, level): 22 | self.level = level 23 | 24 | def filter(self, record): 25 | return record.levelno >= self.level 26 | -------------------------------------------------------------------------------- /src/tpl/jinja2/tip-low-balance.tpl: -------------------------------------------------------------------------------- 1 | {% set user_from = a.u_from.name %} 2 | {% set balance_fmt = "%.6g %s" % (balance, a.coin.upper()) %} 3 | I'm sorry {{ user_from | replace('_', '\_') }}, your {{ action_name }} balance of **{{ balance_fmt }}** is insufficient for this {{ action_name }}. 4 | {% if action_name == 'withdraw' %} 5 | {% set coin_name = ctb.conf.coins[a.coin].name %} 6 | {% set coin_confs = ctb.conf.coins[a.coin].minconf.withdraw %} 7 | {% set coin_fee_fmt = "%.6g" % ctb.conf.coins[a.coin].txfee %} 8 | 9 | Withdrawals are subject to network confirmations and network fees. {{ coin_name }} requires {{ coin_confs }} confirmations and a {{ coin_fee_fmt }} fee. 10 | 11 | If withdrawal balance above doesn't match your reported tip balance, try waiting for more network confirmations. 12 | 13 | >**Tip:** To withdraw everything, use the `ALL` keyword - `+withdraw ADDRESS ALL {{ coin_name }}` - and I'll automatically deduct the required network fee. 14 | {% endif %} 15 | 16 | {% include 'footer.tpl' %} 17 | -------------------------------------------------------------------------------- /src/tpl/jinja2/info.tpl: -------------------------------------------------------------------------------- 1 | {% set user_from = a.u_from.name %} 2 | {% set fiat_total_fmt = "%s%.2f" % (fiat_symbol, fiat_total) %} 3 | 4 | Hello {{ user_from | replace('_', '\_') }}! Here's your account info. 5 | 6 | coin|deposit address|balance 7 | :---|:---|---: 8 | {% for i in info %} 9 | {% set name_fmt = "%s (%s)" % (ctb.conf.coins[i.coin].name, i.coin.upper()) %} 10 | {% set address_fmt = "%s ^[[ex]](%s%s) ^[[qr]](%s%s)" % (i.address, ctb.conf.coins[i.coin].explorer.address, i.address, ctb.conf.misc.qr_service_url, i.address) %} 11 | {% set coin_bal_fmt = "%.6f" % i.balance %} 12 | {% if i.fiat_balance %} 13 | {% set fiat_bal_fmt = " ^%s%.2f" % ( i.fiat_symbol, i.fiat_balance ) %} 14 | {% else %} 15 | {% set fiat_bal_fmt = "" %} 16 | {% endif %} 17 | __{{ name_fmt }}__|{{ address_fmt }}|__{{ coin_bal_fmt }}{{ fiat_bal_fmt }}__ 18 | {% endfor %} 19 |  | |  20 | __TOTAL__| |__{{ fiat_total_fmt }}__ 21 | 22 | Use addresses above to deposit coins into your account. 23 | 24 | {% include 'footer.tpl' %} 25 | -------------------------------------------------------------------------------- /src/tpl/jinja2/tip-received.tpl: -------------------------------------------------------------------------------- 1 | {% set user_from = a.u_from.name %} 2 | {% set user_to = a.u_to.name %} 3 | {% if a.coinval: %} 4 | {% if a.coinval < 1.0 %} 5 | {% set coin_amount = ( a.coinval * 1000.0 ) %} 6 | {% set amount_prefix_short = "m" %} 7 | {% set amount_prefix_long = "milli" %} 8 | {% elif a.coinval >= 1000.0 %} 9 | {% set coin_amount = ( a.coinval / 1000.0 ) %} 10 | {% set amount_prefix_short = "M" %} 11 | {% set amount_prefix_long = "Mega" %} 12 | {% else %} 13 | {% set coin_amount = a.coinval %} 14 | {% endif %} 15 | {% endif %} 16 | {% set coinval_fmt = "%s%s%.6g %s%s" % (amount_prefix_short, ctb.conf.coins[a.coin].symbol, coin_amount, amount_prefix_long, ctb.conf.coins[a.coin].name) %} 17 | {% set fiatval_fmt = "%s%.3f" % (ctb.conf.fiat[a.fiat].symbol, a.fiatval) %} 18 | Hey {{ user_to | replace('_', '\_') }}, you have received a __{{ coinval_fmt }}{{ ' (%s)' % fiatval_fmt if a.fiatval }}__ tip from /u/{{ user_from }}. 19 | 20 | {% set user = user_to %} 21 | {% include 'footer.tpl' %} 22 | -------------------------------------------------------------------------------- /src/_add_coin.py: -------------------------------------------------------------------------------- 1 | # Here's how to add a new coin type to CointipBot 2 | 3 | # * Make sure CointipBot instance is NOT running 4 | # * Install and run coin daemon, make sure it's synced with network 5 | # * Configure and nable new coin in config.yml 6 | # * Then run this script, specifying coin (such as "python _add_coin.py btc") 7 | # * After this script has finished, you can reusme the tip bot normally 8 | 9 | import cointipbot, logging, sys 10 | from ctb import ctb_coin, ctb_misc 11 | 12 | if not len(sys.argv) == 2: 13 | print "Usage: %s COIN" % sys.argv[0] 14 | print "(COIN refers to ctb.conf[COIN], a hash location in coins.yml)" 15 | sys.exit(1) 16 | 17 | coin = sys.argv[1] 18 | 19 | logging.basicConfig() 20 | lg = logging.getLogger('cointipbot') 21 | lg.setLevel(logging.DEBUG) 22 | 23 | ctb = cointipbot.CointipBot(self_checks=False, init_reddit=False, init_coins=False, init_exchanges=False, init_db=True, init_logging=True) 24 | ctb.coins[coin] = ctb_coin.CtbCoin(_conf=ctb.conf.coins[coin]) 25 | ctb_misc.add_coin(coin, ctb.db, ctb.coins) 26 | -------------------------------------------------------------------------------- /src/conf-sample/coins.yml: -------------------------------------------------------------------------------- 1 | # Bitcoin 2 | btc: 3 | enabled: false 4 | unit: btc 5 | name: Bitcoin 6 | symbol: 'Ƀ' 7 | config_file: '~/.bitcoin/bitcoin.conf' 8 | minconf: 9 | givetip: 1 10 | withdraw: 144 11 | txmin: 12 | givetip: 0.00005 13 | withdraw: 0.0005 14 | txfee: 0.0001 15 | walletpassphrase: 'mypass1' 16 | explorer: 17 | address: 'http://blockchain.info/address/' 18 | transaction: 'http://blockchain.info/tx/' 19 | regex: 20 | address: '([1|3][1-9a-z]{20,40})' 21 | units: '(btc|bitcoin|bitcoins)' 22 | 23 | # Dogecoin 24 | dog: 25 | enabled: true 26 | unit: dog 27 | name: Dogecoin 28 | symbol: 'Ð' 29 | config_file: '~/.dogecoin/dogecoin.conf' 30 | config_rpcserver: '127.0.0.1' 31 | minconf: 32 | givetip: 4 33 | withdraw: 60 34 | txmin: 35 | givetip: 5 36 | withdraw: 1 37 | txfee: 1 38 | explorer: 39 | address: 'http://dogechain.info/address/' 40 | transaction: 'http://dogechain.info/tx/' 41 | regex: 42 | address: '(D[1-9a-z]{20,40})' 43 | units: '(doge|dogecoin|dogecoins|doges|Ð|dog)' 44 | -------------------------------------------------------------------------------- /src/conf-sample/reddit.yml: -------------------------------------------------------------------------------- 1 | auth: 2 | user: 'mybotuser' 3 | password: 'mybotpass' 4 | 5 | scan: 6 | batch_limit: 1000 7 | my_subreddits: false 8 | # these_subreddits: ["all"] 9 | 10 | help: 11 | enabled: true 12 | url: 'http://www.reddit.com/r/mysubreddit/wiki/index' 13 | 14 | contact: 15 | enabled: true 16 | url: 'http://www.reddit.com/message/compose/?to=YOUR_NAME' 17 | 18 | messages: 19 | verified: true 20 | pending: false 21 | expired: false 22 | declined: false 23 | sorry: true 24 | 25 | redeem: 26 | enabled: false 27 | unit: 'usd' 28 | multicoin: true 29 | min_karma: 150 30 | base: 0.1 31 | multiplier: 32 | link: 0.00005 33 | comment: 0.000005 34 | maximum: 0.5 35 | account: 'YOUR_ACCOUNT_NAME' 36 | 37 | banned_users: 38 | method: list 39 | list: ['mybotuser', 'bitcointip', 'altcointip'] 40 | subreddit: 'mysubreddit' 41 | 42 | stats: 43 | enabled: false 44 | userflair: false 45 | subreddit: 'mysubreddit' 46 | page: 'stats' 47 | url: 'http://www.reddit.com/r/mysubreddit/wiki/stats' 48 | page_tips: 'tips' 49 | url_tips: 'http://www.reddit.com/r/mysubreddit/wiki/tips' 50 | -------------------------------------------------------------------------------- /src/ctb/ctb_db.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of ALTcointip. 3 | 4 | ALTcointip is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | ALTcointip is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with ALTcointip. If not, see . 16 | """ 17 | 18 | from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, ForeignKey, Numeric, UnicodeText 19 | from sqlalchemy.pool import SingletonThreadPool 20 | 21 | class CointipBotDatabase: 22 | 23 | metadata = MetaData() 24 | 25 | def __init__(self, dsn_url): 26 | '''Pass a DSN URL conforming to the SQLAlchemy API''' 27 | self.dsn_url = dsn_url 28 | 29 | def connect(self): 30 | '''Return a connection object''' 31 | engine = create_engine(self.dsn_url, echo_pool=True, poolclass=SingletonThreadPool) 32 | self.metadata.create_all(engine) 33 | return engine 34 | -------------------------------------------------------------------------------- /src/_update_stats.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of ALTcointip. 3 | 4 | ALTcointip is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | ALTcointip is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with ALTcointip. If not, see . 16 | """ 17 | 18 | import cointipbot, logging 19 | from ctb import ctb_stats 20 | 21 | logging.basicConfig() 22 | lg = logging.getLogger('cointipbot') 23 | lg.setLevel(logging.DEBUG) 24 | 25 | ctb = cointipbot.CointipBot(self_checks=False, init_reddit=True, init_coins=False, init_exchanges=False, init_db=True, init_logging=False) 26 | 27 | # Update stats page 28 | result = ctb_stats.update_stats(ctb=ctb) 29 | lg.debug(result) 30 | 31 | # Update tips page 32 | result = ctb_stats.update_tips(ctb=ctb) 33 | lg.debug(result) 34 | 35 | # This isn't needed because it happens during the tip processing 36 | #result = ctb_stats.update_all_user_stats(ctb=ctb) 37 | #lg.debug(result) 38 | -------------------------------------------------------------------------------- /src/tpl/jinja2/tip-incoming.tpl: -------------------------------------------------------------------------------- 1 | {% set user_from = a.u_from.name %} 2 | {% set user_to = a.u_to.name %} 3 | {% set user_bot = ctb.conf.reddit.auth.user %} 4 | {% if a.coinval: %} 5 | {% if a.coinval < 1.0 %} 6 | {% set coin_amount = ( a.coinval * 1000.0 ) %} 7 | {% set amount_prefix_short = "m" %} 8 | {% set amount_prefix_long = "milli" %} 9 | {% elif a.coinval >= 1000.0 %} 10 | {% set coin_amount = ( a.coinval / 1000.0 ) %} 11 | {% set amount_prefix_short = "M" %} 12 | {% set amount_prefix_long = "Mega" %} 13 | {% else %} 14 | {% set coin_amount = a.coinval %} 15 | {% endif %} 16 | {% endif %} 17 | {% set coinval_fmt = "%s%s%.6g %s%s" % (amount_prefix_short, ctb.conf.coins[a.coin].symbol, coin_amount, amount_prefix_long, ctb.conf.coins[a.coin].name) %} 18 | {% set fiatval_fmt = "%s%.3f" % (ctb.conf.fiat[a.fiat].symbol, a.fiatval) %} 19 | {% set expire_days_fmt = "%.2g" % ( ctb.conf.misc.times.expire_pending_hours / 24.0 ) %} 20 | Hey {{ user_to | replace('_', '\_') }}, /u/{{ user_from }} sent you a __{{ coinval_fmt }} ({{ fiatval_fmt }})__ tip, reply with __[+accept](http://www.reddit.com/message/compose?to={{ user_bot }}&subject=accept&message=%2Baccept)__ to claim it. Reply with __[+decline](http://www.reddit.com/message/compose?to={{ user_bot }}&subject=decline&message=%2Bdecline)__ to decline it. __Pending tips expire in {{ expire_days_fmt }} days.__ 21 | 22 | {% set user = user_to %} 23 | {% include 'footer.tpl' %} 24 | -------------------------------------------------------------------------------- /src/_backup_config.py: -------------------------------------------------------------------------------- 1 | # Simple script to back up ALTcointip conf/ dir 2 | 3 | import sys, os, datetime 4 | from distutils.spawn import find_executable 5 | import cointipbot 6 | 7 | if not len(sys.argv) in [2, 3] or not os.access(sys.argv[1], os.W_OK): 8 | print "Usage: %s DIRECTORY [RSYNC-TO]" % sys.argv[0] 9 | print "(DIRECTORY must be writeable, RSYNC-TO is optional location to RSYNC the file to)" 10 | sys.exit(1) 11 | 12 | ctb = cointipbot.CointipBot(self_checks=False, init_reddit=False, init_coins=False, init_db=False, init_logging=False) 13 | 14 | if not find_executable('zip'): 15 | print "zip executable not found, please install zip" 16 | sys.exit(1) 17 | 18 | if hasattr(ctb.conf.misc.backup, 'encryptionpassphrase') and not find_executable('gpg'): 19 | print "encryptionpassphrase is specified but gpg executable not found, please install gpg" 20 | sys.exit(1) 21 | 22 | filename = "%s/conf_%s.zip" % (sys.argv[1], datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")) 23 | 24 | print "Backing up to %s..." % filename 25 | os.popen("zip -r %s conf/" % filename) 26 | 27 | try: 28 | print "Encrypting..." 29 | os.popen("gpg --batch --passphrase '%s' -c %s" % (ctb.conf.misc.backup.encryptionpassphrase, filename)) 30 | os.popen("rm -f %s" % filename) 31 | filename += '.gpg' 32 | except AttributeError: 33 | print "Not encrypting" 34 | 35 | if len(sys.argv) == 3: 36 | print "Calling rsync..." 37 | os.popen("rsync -urltv %s %s" % (filename, sys.argv[2])) 38 | -------------------------------------------------------------------------------- /src/tpl/jinja2/footer.tpl: -------------------------------------------------------------------------------- 1 | {% if not user and a %} 2 | {% set user = a.u_from.name %} 3 | {% endif %} 4 | {% if not user_bot and ctb %} 5 | {% set user_bot = ctb.conf.reddit.auth.user %} 6 | {% endif %} 7 | {% set compose_url = "http://www.reddit.com/message/compose?to=%s&subject=%s&message=%%2B%s" %} 8 | {% set i_url = compose_url % (user_bot, "info", "info") %} 9 | {% set h_url = compose_url % (user_bot, "history", "history") %} 10 | {% set w_url = compose_url % (user_bot, "withdraw", "withdraw%20ADDRESS%20AMOUNT%20COIN_NAME") %} 11 | {% if ctb and ctb.conf.reddit.help.enabled %} 12 | {% set help_link = " ^[[help]](%s)" % ctb.conf.reddit.help.url %} 13 | {% endif %} 14 | {% if ctb and ctb.conf.reddit.contact.enabled %} 15 | {% set contact_link = " ^[[contact]](%s)" % ctb.conf.reddit.contact.url %} 16 | {% endif %} 17 | {% if ctb and ctb.conf.reddit.stats.enabled %} 18 | {% set stats_user_link = " **^[[your_stats]](%s_%s)**" % (ctb.conf.reddit.stats.url, user) %} 19 | {% set stats_global_link = " ^[[global_stats]](%s)" % ctb.conf.reddit.stats.url %} 20 | {% endif %} 21 | ***** 22 | 23 | ^many ^links|  24 | :---|:--- 25 | {% if a and a.msg and a.msg.permalink %} 26 | ^Source ^comment|^[[link]]({{ a.msg.permalink }}) 27 | {% elif source_link %} 28 | ^Source ^comment|^[[link]]({{ source_link }}) 29 | {% endif %} 30 | ^Quick ^commands|**^[+info]({{ i_url }})** ^[+history]({{ h_url }}) ^[+withdraw]({{ w_url }}) 31 | ^Resources|{{ help_link }}{{ contact_link }}{{ stats_user_link }}{{ stats_global_link }} 32 | 33 | {% include 'announcement.tpl' %} 34 | -------------------------------------------------------------------------------- /src/_backup_db.py: -------------------------------------------------------------------------------- 1 | # Simple script to back up ALTcointip database 2 | 3 | import sys, os, datetime 4 | from distutils.spawn import find_executable 5 | import cointipbot 6 | 7 | if not len(sys.argv) in [2, 3] or not os.access(sys.argv[1], os.W_OK): 8 | print "Usage: %s DIRECTORY [RSYNC-TO]" % sys.argv[0] 9 | print "(DIRECTORY must be writeable, RSYNC-TO is optional location to RSYNC the file to)" 10 | sys.exit(1) 11 | 12 | ctb = cointipbot.CointipBot(self_checks=False, init_reddit=False, init_coins=False, init_db=True, init_logging=False) 13 | 14 | if not find_executable('gzip'): 15 | print "gzip executable not found, please install gzip" 16 | sys.exit(1) 17 | 18 | if hasattr(ctb.conf.misc.backup, 'encryptionpassphrase') and not find_executable('gpg'): 19 | print "encryptionpassphrase is specified but gpg executable not found, please install gpg" 20 | sys.exit(1) 21 | 22 | filename = "%s/%s_%s.sql.gz" % (sys.argv[1], ctb.conf.db.auth.dbname, datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")) 23 | 24 | print "Backing up to %s..." % filename 25 | os.popen("mysqldump -u %s -p%s -h %s -e --opt -c %s | gzip --best -c >%s" % (ctb.conf.db.auth.user, ctb.conf.db.auth.password, ctb.conf.db.auth.host, ctb.conf.db.auth.dbname, filename)) 26 | 27 | try: 28 | print "Encrypting..." 29 | os.popen("gpg --batch --passphrase '%s' -c %s" % (ctb.conf.misc.backup.encryptionpassphrase, filename)) 30 | os.popen("rm -f %s" % filename) 31 | filename += '.gpg' 32 | except AttributeError: 33 | print "Not encrypting" 34 | 35 | if len(sys.argv) == 3: 36 | print "Calling rsync..." 37 | os.popen("rsync -urltv %s %s" % (filename, sys.argv[2])) 38 | -------------------------------------------------------------------------------- /src/_backup_wallets.py: -------------------------------------------------------------------------------- 1 | # Simple script to back up active coin wallets 2 | 3 | import sys, os, datetime 4 | from distutils.spawn import find_executable 5 | import cointipbot 6 | 7 | if not len(sys.argv) in [2, 3] or not os.access(sys.argv[1], os.W_OK): 8 | print "Usage: %s DIRECTORY [RSYNC-TO]" % sys.argv[0] 9 | print "(DIRECTORY must be writeable, RSYNC-TO is optional location to RSYNC the file to)" 10 | sys.exit(1) 11 | 12 | ctb = cointipbot.CointipBot(self_checks=False, init_reddit=False, init_coins=True, init_db=False, init_logging=False) 13 | 14 | if not find_executable('gzip'): 15 | print "gzip executable not found, please install gzip" 16 | sys.exit(1) 17 | 18 | if hasattr(ctb.conf.misc.backup, 'encryptionpassphrase') and not find_executable('gpg'): 19 | print "encryptionpassphrase is specified but gpg executable not found, please install gpg" 20 | sys.exit(1) 21 | 22 | for c in ctb.coins: 23 | filename = "%s/wallet_%s_%s.dat" % (sys.argv[1], ctb.conf.coins[c].unit, datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")) 24 | 25 | print "Backing up %s wallet to %s..." % (ctb.conf.coins[c].name, filename) 26 | ctb.coins[c].conn.backupwallet(filename) 27 | 28 | print "Compressing..." 29 | os.popen("gzip --best %s" % filename) 30 | filename += '.gz' 31 | 32 | try: 33 | print "Encrypting..." 34 | os.popen("gpg --batch --passphrase '%s' -c %s" % (ctb.conf.misc.backup.encryptionpassphrase, filename)) 35 | os.popen("rm -f %s" % filename) 36 | filename += '.gpg' 37 | except AttributeError: 38 | print "Not encrypting" 39 | 40 | if len(sys.argv) == 3: 41 | print "Calling rsync..." 42 | os.popen("rsync -urltv %s %s" % (filename, sys.argv[2])) 43 | -------------------------------------------------------------------------------- /altcointip.sql: -------------------------------------------------------------------------------- 1 | SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; 2 | SET time_zone = "+00:00"; 3 | 4 | CREATE TABLE IF NOT EXISTS `t_action` ( 5 | `type` enum('givetip','withdraw','info','register','accept','decline','history','redeem','rates') NOT NULL, 6 | `state` enum('completed','pending','failed','declined','expired') NOT NULL, 7 | `created_utc` int(11) unsigned NOT NULL, 8 | `from_user` varchar(30) NOT NULL, 9 | `to_user` varchar(30) DEFAULT NULL, 10 | `to_addr` varchar(34) DEFAULT NULL, 11 | `coin_val` float unsigned DEFAULT NULL, 12 | `fiat_val` float unsigned DEFAULT NULL, 13 | `txid` varchar(64) DEFAULT NULL, 14 | `coin` varchar(3) DEFAULT NULL, 15 | `fiat` varchar(3) DEFAULT NULL, 16 | `subreddit` varchar(30) DEFAULT NULL, 17 | `msg_id` varchar(10) NOT NULL, 18 | `msg_link` varchar(200) DEFAULT NULL, 19 | PRIMARY KEY (`type`,`created_utc`,`msg_id`), 20 | UNIQUE KEY `msg_id` (`msg_id`) 21 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 22 | 23 | CREATE TABLE IF NOT EXISTS `t_addrs` ( 24 | `username` varchar(30) NOT NULL, 25 | `coin` varchar(3) NOT NULL, 26 | `address` varchar(34) NOT NULL, 27 | PRIMARY KEY (`username`,`coin`), 28 | UNIQUE KEY `address` (`address`) 29 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 30 | 31 | CREATE TABLE IF NOT EXISTS `t_users` ( 32 | `username` varchar(30) NOT NULL, 33 | `joindate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 34 | `giftamount` float DEFAULT '0', 35 | PRIMARY KEY (`username`) 36 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 37 | 38 | CREATE TABLE IF NOT EXISTS `t_values` ( 39 | `param0` varchar(64) NOT NULL, 40 | `value0` int(11) NOT NULL DEFAULT '0', 41 | PRIMARY KEY (`param0`) 42 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 43 | -------------------------------------------------------------------------------- /src/tpl/jinja2/confirmation.tpl: -------------------------------------------------------------------------------- 1 | {% set title_fmt = "^__[%s]__:" % title %} 2 | {% set user_from_fmt = " ^/u/%s" % a.u_from.name %} 3 | {% set arrow_fmt = " ^->" %} 4 | {% if a.u_to: %} 5 | {% set user_to_fmt = " ^/u/%s" % a.u_to.name %} 6 | {% if ctb.conf.reddit.stats.enabled: %} 7 | {% set stats_user_to_fmt = " ^^[[stats]](%s_%s)" % (ctb.conf.reddit.stats.url, a.u_to.name) %} 8 | {% endif %} 9 | {% endif %} 10 | {% if a.addr_to: %} 11 | {% set ex = ctb.conf.coins[a.coin].explorer %} 12 | {% set user_to_fmt = " ^[%s](%s%s)" % (a.addr_to, ex.address, a.addr_to) %} 13 | {% set arrow_fmt = " ^[->](%s%s)" % (ex.transaction, a.txid) %} 14 | {% endif %} 15 | {% if a.coinval: %} 16 | {% set coin_amount = a.coinval %} 17 | {% set coin_name = ctb.conf.coins[a.coin].name %} 18 | {% set coin_symbol = ctb.conf.coins[a.coin].symbol %} 19 | {% set coin_amount_fmt = " __^%s%.6f ^%s(s)__" % (coin_symbol, coin_amount, coin_name) %} 20 | {% endif %} 21 | {% if a.fiatval: %} 22 | {% set fiat_amount = a.fiatval %} 23 | {% set fiat_symbol = ctb.conf.fiat[a.fiat].symbol %} 24 | {% set fiat_amount_fmt = " ^__(%s%.6g)__" % (fiat_symbol, fiat_amount) %} 25 | {% endif %} 26 | {% if ctb.conf.reddit.stats.enabled: %} 27 | {% set stats_user_from_fmt = " ^^[[stats]](%s_%s)" % (ctb.conf.reddit.stats.url, a.u_from.name) %} 28 | {% set stats_link_fmt = " ^[[stats]](%s)" % ctb.conf.reddit.stats.url %} 29 | {% endif %} 30 | {% if ctb.conf.reddit.help.enabled: %} 31 | {% set help_link_fmt = " ^[[help]](%s)" % ctb.conf.reddit.help.url %} 32 | {% endif %} 33 | {{ title_fmt }}{{ user_from_fmt }}{{ stats_user_from_fmt }}{{ arrow_fmt }}{{ user_to_fmt }}{{ stats_user_to_fmt }}{{ coin_amount_fmt }}{{ fiat_amount_fmt }}{{ help_link_fmt }}{{ stats_link_fmt }} 34 | -------------------------------------------------------------------------------- /src/conf-sample/exchanges.yml: -------------------------------------------------------------------------------- 1 | blockchaininfo: 2 | enabled: false 3 | domain: 'blockchain.info' 4 | https: true 5 | urlpaths: ['/ticker'] 6 | jsonpaths: ['{THING_TO}.15m'] 7 | coinlist: ['btc'] 8 | fiatlist: ['usd','cny','jpy','sgd','hkd','cad','aud','nzd','gbp','dkk','sek','brl','chf','eur','rub','sll','pln','thb'] 9 | uppercase: true 10 | 11 | vircurex: 12 | enabled: false 13 | domain: 'vircurex.com' 14 | https: true 15 | urlpaths: ['/api/get_highest_bid.json?base={THING_FROM}&alt={THING_TO}', '/api/get_lowest_ask.json?base={THING_FROM}&alt={THING_TO}'] 16 | jsonpaths: ['value'] 17 | coinlist: ['anc','btc','dgc','dvc','frc','ftc','i0c','ixc','ltc','nmc','nvc','ppc','trc','xpm'] 18 | fiatlist: ['usd','eur'] 19 | uppercase: true 20 | 21 | btce: 22 | enabled: false 23 | domain: 'btc-e.com' 24 | https: true 25 | urlpaths: ['/api/2/{THING_FROM}_{THING_TO}/ticker'] 26 | jsonpaths: ['ticker.avg'] 27 | coinlist: ['btc','ltc','nmc','nvc','trc','ppc','ftc','xpm'] 28 | fiatlist: ['usd','eur','rur'] 29 | uppercase: false 30 | 31 | bitstamp: 32 | enabled: true 33 | domain: 'www.bitstamp.net' 34 | https: true 35 | urlpaths: ['/api/ticker/'] 36 | jsonpaths: ['ask'] 37 | coinlist: ['btc'] 38 | fiatlist: ['usd'] 39 | uppercase: false 40 | 41 | mtgox: 42 | enabled: false 43 | domain: 'data.mtgox.com' 44 | https: true 45 | urlpaths: ['/api/2/{THING_FROM}{THING_TO}/money/ticker_fast'] 46 | jsonpaths: ['data.buy.value'] 47 | coinlist: ['btc'] 48 | fiatlist: ['usd','eur','jpy','gbp','rub'] 49 | uppercase: true 50 | 51 | campbx: 52 | enabled: false 53 | domain: 'campbx.com' 54 | https: true 55 | urlpaths: ['/api/xticker.php'] 56 | jsonpaths: ['Last Trade'] 57 | coinlist: ['btc'] 58 | fiatlist: ['usd'] 59 | uppercase: false 60 | 61 | cryptsy: 62 | enabled: true 63 | domain: 'pubapi.cryptsy.com' 64 | https: false 65 | urlpaths: ['/api.php?method=marketdatav2'] 66 | jsonpaths: ['return.markets.DOGE/BTC.lasttradeprice'] 67 | coinlist: ['btc','dog'] 68 | fiatlist: [] 69 | uppercase: true 70 | 71 | bter: 72 | enabled: false 73 | domain: 'bter.com' 74 | https: true 75 | urlpaths: ['/api/1/ticker/{THING_FROM}_{THING_TO}'] 76 | jsonpaths: ['avg'] 77 | coinlist: ['btc','ltc','ppc','frc','ftc','cnc','bqc','btb','wdc'] 78 | fiatlist: ['cny'] 79 | uppercase: false 80 | 81 | cryptotrade: 82 | enabled: false 83 | domain: 'crypto-trade.com' 84 | https: true 85 | urlpaths: ['/api/1/ticker/{THING_FROM}_{THING_TO}'] 86 | jsonpaths: ['data.min_ask', 'data.max_bid'] 87 | coinlist: ['btc','ltc','nmc','xpm','ppc','trc','ftc','dvc','wdc','dgc'] 88 | fiatlist: ['usd','eur'] 89 | uppercase: false -------------------------------------------------------------------------------- /src/ctb/ctb_exchange.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of ALTcointip. 3 | 4 | ALTcointip is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | ALTcointip is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with ALTcointip. If not, see . 16 | """ 17 | 18 | import json, logging, urllib2, httplib 19 | 20 | lg = logging.getLogger('cointipbot') 21 | 22 | class CtbExchange(object): 23 | """ 24 | Exchange class for cointip bot 25 | """ 26 | 27 | conf = None 28 | 29 | def __init__(self, _conf = None): 30 | """ 31 | Initialize CtbExchange with given parameters. 32 | _conf is an exchange config dictionary defind in conf/exchanges.yml 33 | """ 34 | 35 | if not _conf or not hasattr(_conf, 'urlpaths') or not hasattr(_conf, 'jsonpaths') or not hasattr(_conf, 'coinlist') or not hasattr(_conf, 'fiatlist'): 36 | raise Exception("CtbExchange::__init__(): _conf is empty or invalid") 37 | 38 | self.conf = _conf 39 | 40 | # Convert coinlist and fiatlist values to lowercase 41 | self.conf.coinlist = map(lambda x:x.lower(), self.conf.coinlist) 42 | self.conf.fiatlist = map(lambda x:x.lower(), self.conf.fiatlist) 43 | 44 | lg.debug("CtbExchange::__init__(): initialized exchange %s" % self.conf.domain) 45 | 46 | def supports(self, _name = None): 47 | """ 48 | Return True if exchange supports given coin/fiat _name 49 | """ 50 | 51 | if not _name or not type(_name) in [str, unicode]: 52 | raise Exception("CtbExchange::supports(): _name is empty or wrong type") 53 | 54 | name = str(_name).lower() 55 | 56 | if name in self.conf.coinlist or name in self.conf.fiatlist: 57 | #lg.debug("CtbExchange::supports(%s): YES" % name) 58 | return True 59 | else: 60 | #lg.debug("CtbExchange::supports(%s): NO" % name) 61 | return False 62 | 63 | def supports_pair(self, _name1 = None, _name2 = None): 64 | """ 65 | Return true of exchange supports given coin/fiat pair 66 | """ 67 | 68 | return self.supports(_name=_name1) and self.supports(_name=_name2) 69 | 70 | def get_ticker_value(self, _name1 = None, _name2 = None): 71 | """ 72 | Return (float) ticker value for given pair 73 | """ 74 | 75 | if _name1 == _name2: 76 | return float(1) 77 | 78 | if not self.supports_pair(_name1=_name1, _name2=_name2): 79 | raise Exception("CtbExchange::get_ticker_value(%s, %s, %s): pair not supported" % (self.conf.domain, _name1, _name2)) 80 | 81 | results = [] 82 | for myurlpath in self.conf.urlpaths: 83 | for myjsonpath in self.conf.jsonpaths: 84 | 85 | toreplace = {'{THING_FROM}': _name1.upper() if self.conf.uppercase else _name1.lower(), '{THING_TO}': _name2.upper() if self.conf.uppercase else _name2.lower()} 86 | for t in toreplace: 87 | myurlpath = myurlpath.replace(t, toreplace[t]) 88 | myjsonpath = myjsonpath.replace(t, toreplace[t]) 89 | 90 | try: 91 | lg.debug("CtbExchange::get_ticker_value(%s, %s, %s): calling %s to get %s...", self.conf.domain, _name1, _name2, myurlpath, myjsonpath) 92 | if self.conf.https: 93 | connection = httplib.HTTPSConnection(self.conf.domain) 94 | connection.request("GET", myurlpath, {}, {}) 95 | else: 96 | connection = httplib.HTTPConnection(self.conf.domain) 97 | connection.request("GET", myurlpath) 98 | response = json.loads(connection.getresponse().read()) 99 | result = xpath_get(response, myjsonpath) 100 | lg.debug("CtbExchange::get_ticker_value(%s, %s, %s): result: %.6f", self.conf.domain, _name1, _name2, float(result)) 101 | results.append( float(result) ) 102 | 103 | except urllib2.URLError as e: 104 | lg.error("CtbExchange::get_ticker_value(%s, %s, %s): %s", self.conf.domain, _name1, _name2, e) 105 | return None 106 | except urllib2.HTTPError as e: 107 | lg.error("CtbExchange::get_ticker_value(%s, %s, %s): %s", self.conf.domain, _name1, _name2, e) 108 | return None 109 | except Exception as e: 110 | lg.error("CtbExchange::get_ticker_value(%s, %s, %s): %s", self.conf.domain, _name1, _name2, e) 111 | return None 112 | 113 | # Return average of all responses 114 | return ( sum(results) / float(len(results)) ) 115 | 116 | 117 | def xpath_get(mydict, path): 118 | elem = mydict 119 | try: 120 | for x in path.strip('.').split('.'): 121 | try: 122 | x = int(x) 123 | elem = elem[x] 124 | except ValueError: 125 | elem = elem.get(x) 126 | except: 127 | pass 128 | 129 | return elem 130 | -------------------------------------------------------------------------------- /src/ctb/ctb_misc.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of ALTcointip. 3 | 4 | ALTcointip is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | ALTcointip is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with ALTcointip. If not, see . 16 | """ 17 | 18 | import ctb_user 19 | 20 | import logging, time 21 | 22 | from requests.exceptions import HTTPError, ConnectionError, Timeout 23 | from praw.errors import ExceptionList, APIException, InvalidCaptcha, InvalidUser, RateLimitExceeded 24 | from socket import timeout 25 | 26 | lg = logging.getLogger('cointipbot') 27 | 28 | 29 | def praw_call(prawFunc, *extraArgs, **extraKwArgs): 30 | """ 31 | Call prawFunc() with extraArgs and extraKwArgs 32 | Retry if Reddit is down 33 | """ 34 | 35 | while True: 36 | 37 | try: 38 | res = prawFunc(*extraArgs, **extraKwArgs) 39 | return res 40 | 41 | except (HTTPError, ConnectionError, Timeout, RateLimitExceeded, timeout) as e: 42 | if str(e) == "403 Client Error: Forbidden": 43 | lg.warning("praw_call(): 403 forbidden") 44 | return False 45 | if str(e) == "404 Client Error: Not Found": 46 | lg.warning("praw_call(): 404 not found") 47 | return False 48 | lg.warning("praw_call(): Reddit is down (%s), sleeping...", e) 49 | time.sleep(30) 50 | pass 51 | except Exception as e: 52 | raise 53 | 54 | return True 55 | 56 | def reddit_get_parent_author(comment, reddit, ctb): 57 | """ 58 | Return author of comment's parent comment 59 | """ 60 | lg.debug("> reddit_get_parent_author()") 61 | 62 | while True: 63 | 64 | try: 65 | parentcomment = reddit.get_info(thing_id=comment.parent_id) 66 | if (parentcomment.author): 67 | lg.debug("< reddit_get_parent_author(%s) -> %s", comment.id, parentcomment.author.name) 68 | return parentcomment.author.name 69 | else: 70 | lg.warning("reddit_get_parent_author(%s): parent comment was deleted", comment.id) 71 | return None 72 | 73 | except IndexError as e: 74 | lg.warning("reddit_get_parent_author(): couldn't get author: %s", e) 75 | return None 76 | except (RateLimitExceeded, timeout) as e: 77 | lg.warning("reddit_get_parent_author(): Reddit is down (%s), sleeping...", e) 78 | time.sleep(ctb.conf.misc.times.sleep_seconds) 79 | pass 80 | except HTTPError as e: 81 | lg.warning("reddit_get_parent_author(): thread or comment not found (%s)", e) 82 | return None 83 | except Exception as e: 84 | raise 85 | 86 | lg.error("reddit_get_parent_author(): returning None (should not get here)") 87 | return None 88 | 89 | def get_value(conn, param0=None): 90 | """ 91 | Fetch a value from t_values table 92 | """ 93 | lg.debug("> get_value()") 94 | 95 | if param0 == None: 96 | raise Exception("get_value(): param0 == None") 97 | 98 | value = None 99 | sql = "SELECT value0 FROM t_values WHERE param0 = %s" 100 | 101 | try: 102 | 103 | mysqlrow = conn.execute(sql, (param0)).fetchone() 104 | if mysqlrow == None: 105 | lg.error("get_value(): query <%s> didn't return any rows", sql % (param0)) 106 | return None 107 | value = mysqlrow['value0'] 108 | 109 | except Exception, e: 110 | lg.error("get_value(): error executing query <%s>: %s", sql % (param0), e) 111 | raise 112 | 113 | lg.debug("< get_value() DONE (%s)", value) 114 | return value 115 | 116 | def set_value(conn, param0=None, value0=None): 117 | """ 118 | Set a value in t_values table 119 | """ 120 | lg.debug("> set_value(%s, %s)", param0, value0) 121 | 122 | if param0 == None or value0 == None: 123 | raise Exception("set_value(): param0 == None or value0 == None") 124 | sql = "REPLACE INTO t_values (param0, value0) VALUES (%s, %s)" 125 | 126 | try: 127 | 128 | mysqlexec = conn.execute(sql, (param0, value0)) 129 | if mysqlexec.rowcount <= 0: 130 | lg.error("set_value(): query <%s> didn't affect any rows", sql % (param0, value0)) 131 | return False 132 | 133 | except Exception, e: 134 | lg.error("set_value: error executing query <%s>: %s", sql % (param0, value0), e) 135 | raise 136 | 137 | lg.debug("< set_value() DONE") 138 | return True 139 | 140 | def add_coin(coin, db, coins): 141 | """ 142 | Add new coin address to each user 143 | """ 144 | lg.debug("> add_coin(%s)", coin) 145 | 146 | sql_select = "SELECT username FROM t_users WHERE username NOT IN (SELECT username FROM t_addrs WHERE coin = %s) ORDER BY username" 147 | sql_insert = "REPLACE INTO t_addrs (username, coin, address) VALUES (%s, %s, %s)" 148 | 149 | try: 150 | 151 | mysqlsel = db.execute(sql_select, (coin)) 152 | for m in mysqlsel: 153 | # Generate new coin address for user 154 | new_addr = coins[coin].getnewaddr(_user=m['username']) 155 | lg.info("add_coin(): got new address %s for %s", new_addr, m['username']) 156 | # Add new coin address to MySQL 157 | mysqlins = db.execute(sql_insert, (m['username'].lower(), coin, new_addr)) 158 | if mysqlins.rowcount <= 0: 159 | raise Exception("add_coin(%s): rowcount <= 0 when executing <%s>", coin, sql_insert % (m['username'].lower(), coin, new_addr)) 160 | time.sleep(1) 161 | 162 | except Exception, e: 163 | lg.error("add_coin(%s): error: %s", coin, e) 164 | raise 165 | 166 | lg.debug("< add_coin(%s) DONE", coin) 167 | return True 168 | 169 | class DotDict(object): 170 | def __init__(self, d): 171 | for a, b in d.items(): 172 | if isinstance(b, (list, tuple)): 173 | setattr(self, a, [DotDict(x) if isinstance(x, dict) else x for x in b]) 174 | else: 175 | setattr(self, a, DotDict(b) if isinstance(b, dict) else b) 176 | def __getitem__(self, val): 177 | return getattr(self, val) 178 | def has_key(self, key): 179 | return hasattr(self, key) 180 | -------------------------------------------------------------------------------- /src/conf-sample/db.yml: -------------------------------------------------------------------------------- 1 | auth: 2 | user: 'mysqluser' 3 | password: 'mysqlpass' 4 | host: localhost 5 | port: 3306 6 | dbname: mysqldb 7 | 8 | sql: 9 | globalstats: 10 | 01_total_tipped_usd: 11 | name: "Total Accepted Tips (USD)" 12 | desc: "Total value of all tips given and accepted in USD (default) fiat" 13 | type: line 14 | query: "SELECT SUM(fiat_val) AS total_usd, fiat FROM t_action WHERE type = 'givetip' AND state = 'completed' AND fiat = 'usd'" 15 | 01a_total_tipped_usd_by_coin: 16 | name: "Total Accepted Tips (USD) By Coin" 17 | desc: "Total value of all tips given and accepted in USD (default) fiat grouped by coin" 18 | type: table 19 | query: "SELECT coin, SUM(fiat_val) AS total_usd, fiat FROM t_action WHERE type = 'givetip' AND state = 'completed' AND fiat = 'usd' GROUP BY coin ORDER BY coin" 20 | 02_total_tips_expired_and_declined: 21 | name: "Total Expired and Declined Tips (USD)" 22 | desc: "Total value of all tips given that weren't accepted (expired or declined) in USD (default) fiat" 23 | type: line 24 | query: "SELECT SUM(fiat_val) AS total_usd, fiat FROM t_action WHERE type = 'givetip' AND state = 'expired' OR state = 'declined' AND fiat = 'usd'" 25 | 03_total_users_registered: 26 | name: "Total Users Registered" 27 | desc: "Number of registered users" 28 | type: line 29 | query: "SELECT COUNT(username) AS total_users FROM t_users" 30 | 04_total_tippers: 31 | name: "Total Tippers" 32 | desc: "Number of users who tipped at least once" 33 | type: line 34 | query: "SELECT COUNT(from_user) AS total_tippers FROM (SELECT DISTINCT from_user FROM t_action WHERE type = 'givetip') AS t_distinct_action" 35 | 05_total_tips: 36 | name: "Total Number of Tips" 37 | desc: "Total number of tips given" 38 | type: line 39 | query: "SELECT COUNT(msg_id) AS total_tips FROM t_action WHERE type = 'givetip' AND state = 'completed'" 40 | 05a_total_tips_by_coin: 41 | name: "Total Number of Tips (by coin)" 42 | desc: "Total number of tips given grouped by coin" 43 | type: table 44 | query: "SELECT coin, count(*) AS total_tips FROM t_action WHERE type = 'givetip' AND state = 'completed' GROUP BY coin ORDER BY coin" 45 | 05b_total_karma_redeemed: 46 | name: "Total Karma Redeemed (USD)" 47 | desc: "Total value of redeemed karma" 48 | type: line 49 | query: "SELECT SUM(fiat_val) AS total_usd, fiat FROM t_action WHERE type = 'redeem' AND state = 'completed' AND fiat = 'usd'" 50 | 06_top_10_tippers: 51 | name: "Top 10 Tippers" 52 | desc: "Top 10 all-time tippers as determined by total USD/EUR (fiat) value of their tips." 53 | type: table 54 | query: "SELECT from_user, SUM(fiat_val) AS total_fiat, fiat FROM t_action WHERE type = 'givetip' AND state = 'completed' AND fiat IN ('usd', 'eur') GROUP BY from_user ORDER BY total_fiat DESC LIMIT 10" 55 | 07_top_10_tips: 56 | name: "Top 10 Tips" 57 | desc: "Top 10 all-time tips as determined by their USD/EUR (fiat) value." 58 | type: table 59 | query: "SELECT from_user, to_user, coin_val, coin, fiat_val, fiat, msg_link FROM t_action WHERE type ='givetip' AND state = 'completed' AND fiat IN ('usd', 'eur') ORDER BY fiat_val DESC LIMIT 10" 60 | 07a_top_5_tips_btc: 61 | name: "Top 5 Tips (BTC)" 62 | desc: "Top 5 all-time BTC tips as determined by coin amount" 63 | type: table 64 | query: "SELECT from_user, to_user, coin_val, coin, fiat_val, fiat, msg_link FROM t_action WHERE type ='givetip' AND state = 'completed' AND coin = 'btc' ORDER BY coin_val DESC LIMIT 5" 65 | 07b_top_5_tips_ltc: 66 | name: "Top 5 Tips (LTC)" 67 | desc: "Top 5 all-time LTC tips as determined by coin amount" 68 | type: table 69 | query: "SELECT from_user, to_user, coin_val, coin, fiat_val, fiat, msg_link FROM t_action WHERE type ='givetip' AND state = 'completed' AND coin = 'ltc' ORDER BY coin_val DESC LIMIT 5" 70 | 07c_top_5_tips_ppc: 71 | name: "Top 5 Tips (PPC)" 72 | desc: "Top 5 all-time PPC tips as determined by coin amount" 73 | type: table 74 | query: "SELECT from_user, to_user, coin_val, coin, fiat_val, fiat, msg_link FROM t_action WHERE type ='givetip' AND state = 'completed' AND coin = 'ppc' ORDER BY coin_val DESC LIMIT 5" 75 | 07d_top_5_tips_nmc: 76 | name: "Top 5 Tips (NMC)" 77 | desc: "Top 5 all-time NMC tips as determined by coin amount" 78 | type: table 79 | query: "SELECT from_user, to_user, coin_val, coin, fiat_val, fiat, msg_link FROM t_action WHERE type ='givetip' AND state = 'completed' AND coin = 'nmc' ORDER BY coin_val DESC LIMIT 5" 80 | 07e_top_5_tips_xpm: 81 | name: "Top 5 Tips (XPM)" 82 | desc: "Top 5 all-time XPM tips as determined by coin amount" 83 | type: table 84 | query: "SELECT from_user, to_user, coin_val, coin, fiat_val, fiat, msg_link FROM t_action WHERE type ='givetip' AND state = 'completed' AND coin = 'xpm' ORDER BY coin_val DESC LIMIT 5" 85 | 07f_top_5_tips_mec: 86 | name: "Top 5 Tips (MEC)" 87 | desc: "Top 5 all-time MEC tips as determined by coin amount" 88 | type: table 89 | query: "SELECT from_user, to_user, coin_val, coin, fiat_val, fiat, msg_link FROM t_action WHERE type ='givetip' AND state = 'completed' AND coin = 'mec' ORDER BY coin_val DESC LIMIT 5" 90 | 08_top_10_receivers: 91 | name: "Top 10 Receivers" 92 | desc: "Top 10 all-time tip receivers as determined by total USD/EUR (fiat) value of their received tips." 93 | type: table 94 | query: "SELECT to_user, SUM(fiat_val) AS total_fiat, fiat FROM t_action WHERE type = 'givetip' AND state = 'completed' AND fiat IN ('usd', 'eur') AND to_user IS NOT NULL GROUP BY to_user ORDER BY total_fiat DESC LIMIT 10" 95 | userstats: 96 | users: "SELECT username FROM t_users WHERE username IN (SELECT from_user FROM t_action WHERE type = 'givetip') OR username in (SELECT to_user FROM t_action WHERE type = 'givetip') ORDER BY username" 97 | coins: 'SELECT DISTINCT coin FROM t_action WHERE coin IS NOT NULL ORDER BY coin' 98 | fiat: 'SELECT DISTINCT fiat FROM t_action WHERE fiat IS NOT NULL ORDER BY fiat' 99 | history: "SELECT from_user, to_user, created_utc, to_addr, coin_val, coin, fiat_val, fiat, state, subreddit, msg_link FROM t_action WHERE type='givetip' AND (from_user=%s OR to_user=%s) ORDER BY created_utc DESC" 100 | total_tipped_fiat: "SELECT SUM(fiat_val) AS total_fiat FROM t_action WHERE type='givetip' AND state='completed' AND from_user=%s AND fiat=%s" 101 | total_tipped_coin: "SELECT SUM(coin_val) AS total_coin FROM t_action WHERE type='givetip' AND state='completed' AND from_user=%s AND coin=%s" 102 | total_received_fiat: "SELECT SUM(fiat_val) AS total_fiat FROM t_action WHERE type='givetip' AND state='completed' AND to_user=%s AND fiat=%s" 103 | total_received_coin: "SELECT SUM(coin_val) AS total_coin FROM t_action WHERE type='givetip' AND state='completed' AND to_user=%s AND coin=%s" 104 | userhistory: 105 | sql: "SELECT type, state, from_user, to_user, created_utc, to_addr, coin_val, coin, fiat_val, fiat, subreddit FROM t_action WHERE type IN ('givetip', 'redeem', 'withdraw') AND (from_user=%s OR to_user=%s) ORDER BY created_utc DESC LIMIT %s" 106 | limit: 75 107 | tips: 108 | sql_set: "SET @rank=0" 109 | sql_list: "SELECT @rank :=@rank+1 AS num, created_utc, from_user, to_user, coin_val, coin, fiat_val, fiat, subreddit, msg_link FROM t_action WHERE type='givetip' AND state='completed' ORDER BY created_utc ASC LIMIT %s" 110 | limit: 10000 111 | -------------------------------------------------------------------------------- /src/conf-sample/keywords.yml: -------------------------------------------------------------------------------- 1 | all: 2 | value: "float( self.u_from.get_balance(coin=self.coin, kind=('withdraw' if bool(self.addr_to) else 'givetip')) - (self.ctb.conf.coins[self.coin].txfee if bool(self.addr_to) else 0.0) ) if self.u_from.is_registered() and bool(self.coin) else None" 3 | for_fiat: false 4 | for_coin: true 5 | message: '^**(*・_・)ノ⌒*** {USER_FROM} ^gives {USER_TO} ^_everything_!.. ^all {AMOUNT} ^now ^belong ^to {USER_TO}' 6 | 7 | flip: 8 | value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,2)) )" 9 | for_fiat: true 10 | for_coin: true 11 | message: '{USER_FROM} ^flips ^the ^table ^**(╯°□°)╯︵┻━┻** ^and {USER_TO} ^wins {AMOUNT}' 12 | 13 | megaflip: 14 | value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,20)) )" 15 | for_fiat: true 16 | for_coin: true 17 | message: '{USER_FROM} ^flips ^10 ^tables ^**(╯°□°)╯︵┻━┻** ^and {USER_TO} ^wins {AMOUNT}' 18 | 19 | gigaflip: 20 | value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,200)) )" 21 | for_fiat: true 22 | for_coin: true 23 | message: '{USER_FROM} ^flips ^100 ^tables ^**(╯°□°)╯︵┻━┻** ^and {USER_TO} ^wins {AMOUNT}' 24 | 25 | roll: 26 | value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,6)) )" 27 | for_fiat: true 28 | for_coin: true 29 | message: '{USER_FROM} ^rolls ^around ^and {USER_TO} ^gets {AMOUNT}' 30 | 31 | megaroll: 32 | value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,60)) )" 33 | for_fiat: true 34 | for_coin: true 35 | message: '{USER_FROM} ^rolls ^around ^and {USER_TO} ^gets {AMOUNT}' 36 | 37 | gigaroll: 38 | value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,600)) )" 39 | for_fiat: true 40 | for_coin: true 41 | message: '{USER_FROM} ^rolls ^aroun ^and {USER_TO} ^gets {AMOUNT}' 42 | 43 | random01: 44 | value: "float( randint(0,100) ) / 1000.0" 45 | for_fiat: true 46 | for_coin: true 47 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 48 | 49 | random05: 50 | value: "float( randint(0,500) ) / 1000.0" 51 | for_fiat: true 52 | for_coin: true 53 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 54 | 55 | random1: 56 | value: "float( randint(0,1000) ) / 1000.0" 57 | for_fiat: true 58 | for_coin: true 59 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 60 | 61 | random3: 62 | value: "float( randint(0,3000) ) / 1000.0" 63 | for_fiat: true 64 | for_coin: true 65 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 66 | 67 | random5: 68 | value: "float( randint(0,5000) ) / 1000.0" 69 | for_fiat: true 70 | for_coin: true 71 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 72 | 73 | random10: 74 | value: "float( randint(0,10000) ) / 1000.0" 75 | for_fiat: true 76 | for_coin: true 77 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 78 | 79 | random15: 80 | value: "float( randint(0,15000) ) / 1000.0" 81 | for_fiat: true 82 | for_coin: true 83 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 84 | 85 | random25: 86 | value: "float( randint(0,25000) ) / 1000.0" 87 | for_fiat: true 88 | for_coin: true 89 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 90 | 91 | random50: 92 | value: "float( randint(0,50000) ) / 1000.0" 93 | for_fiat: true 94 | for_coin: true 95 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 96 | 97 | random100: 98 | value: "float( randint(0,100000) ) / 1000.0" 99 | for_fiat: true 100 | for_coin: true 101 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 102 | 103 | random250: 104 | value: "float( randint(0,250000) ) / 1000.0" 105 | for_fiat: true 106 | for_coin: true 107 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 108 | 109 | random500: 110 | value: "float( randint(0,500000) ) / 1000.0" 111 | for_fiat: true 112 | for_coin: true 113 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 114 | 115 | random1000: 116 | value: "float( randint(0,1000000) ) / 1000.0" 117 | for_fiat: true 118 | for_coin: true 119 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 120 | 121 | upvote: 122 | value: 0.1 123 | for_fiat: true 124 | for_coin: true 125 | message: '{USER_FROM} ^gives {USER_TO} ^an ^upvote... _^with ^benefits_ ^**ヽ(*・ω・)ノ** ^- ^worth {AMOUNT}' 126 | 127 | highfive: 128 | value: 0.25 129 | for_fiat: true 130 | for_coin: true 131 | message: '{USER_FROM} ^gives {USER_TO} ^a ^high ^five ^**☆☆☆** ^- ^worth {AMOUNT}' 132 | 133 | fart: 134 | value: 0.05 135 | for_fiat: true 136 | for_coin: true 137 | message: '{USER_FROM} ^farts ^on {USER_TO} ^**(⊙﹏⊙)** ^- ^worth {AMOUNT}' 138 | 139 | coffee: 140 | value: 1.0 141 | for_fiat: true 142 | for_coin: false 143 | message: '{USER_TO} ^gets ^a ^delicious ^cup ^of ^coffee ^from {USER_FROM} ^**(´◠ω◠`)** ^- ^worth {AMOUNT}' 144 | 145 | cookie: 146 | value: 0.33 147 | for_fiat: true 148 | for_coin: false 149 | message: '{USER_FROM} ^gives {USER_TO} ^a ^yummy ^cookie ^**♪┏(・o・)┛♪** ^- ^worth {AMOUNT}' 150 | 151 | sushi: 152 | value: 3.0 153 | for_fiat: true 154 | for_coin: false 155 | message: '{USER_FROM} ^serves {USER_TO} ^a ^roll ^of ^yummy ^sushi ^**(っ˘ڡ˘ς)** ^- ^worth {AMOUNT}' 156 | 157 | nothing: 158 | value: "float (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01)" 159 | for_fiat: true 160 | for_coin: true 161 | message: '^**(″・ิ_・ิ)っ** {USER_FROM} ^gives ^almost ^nothing ^to {USER_TO} ^- {AMOUNT}' 162 | 163 | gum: 164 | value: 1.5 165 | for_fiat: true 166 | for_coin: false 167 | message: '{USER_FROM} ^gives ^a ^pack ^of ^gum ^to {USER_TO} ^- ^worth {AMOUNT}' 168 | 169 | beer: 170 | value: 3.5 171 | for_fiat: true 172 | for_coin: false 173 | message: '{USER_FROM} ^gives ^a ^beer ^to {USER_TO} ^- ^worth {AMOUNT}' 174 | 175 | nicebeer: 176 | value: 5.0 177 | for_fiat: true 178 | for_coin: false 179 | message: '{USER_FROM} ^gives ^a ^nice ^beer ^**(〃・ิ‿・ิ)ゞ** ^to {USER_TO} ^- ^worth {AMOUNT}) ^**ƪ(˘▽˘ƪ)**' 180 | 181 | hug: 182 | value: 0.5 183 | for_fiat: true 184 | for_coin: true 185 | message: '^**(っ⌒‿⌒)っ** {USER_FROM} ^gives ^a ^nice ^hug ^**ヽ(*⌒∇⌒*)ノ** ^to {USER_TO} ^along ^with {AMOUNT}' 186 | 187 | kiss: 188 | value: 1.0 189 | for_fiat: true 190 | for_coin: true 191 | message: '{USER_TO} ^gets ^a ^kiss ^**(˘⌣˘)ε˘`)** ^from {USER_FROM} ^along ^with {AMOUNT}' 192 | 193 | nyan: 194 | value: 1.111111 195 | for_fiat: true 196 | for_coin: true 197 | message: '^**~(=\^‥\^)ノ** {USER_FROM} ^nyan ^nyan ^にゃん {USER_TO} ^にゃん ^にゃん {AMOUNT}' 198 | 199 | pi: 200 | value: 3.14159265 201 | for_fiat: true 202 | for_coin: true 203 | 204 | e: 205 | value: 2.7182818 206 | for_fiat: true 207 | for_coin: true 208 | 209 | leet: 210 | value: 1.337 211 | for_fiat: true 212 | for_coin: true 213 | 214 | quarter: 215 | value: 0.25 216 | for_fiat: true 217 | for_coin: true 218 | 219 | half: 220 | value: 0.5 221 | for_fiat: true 222 | for_coin: true 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # It's dogetipbot! 2 | 3 | ## Introduction 4 | 5 | dogetipbot is a fork of vindimy's ALTcointip bot, found at __https://github.com/vindimy/altcointip__ 6 | 7 | This is the version that's currently running on reddit at __http://www.reddit.com/r/dogetipbot/wiki/index__ 8 | 9 | v2 is being developed, but this is stable (for now). only minor patches will be added on to this repo as bugs are squashed. 10 | 11 | note that this bot only accepts dogecoins. 12 | 13 | The instructions below are from ALTcointip, but the instructions are the same. Cheers! 14 | 15 | ## ALTCointip Getting Started Instructions 16 | 17 | ### Python Dependencies 18 | 19 | The following Python libraries are necessary to run ALTcointip bot: 20 | 21 | * __jinja2__ (http://jinja.pocoo.org/) 22 | * __pifkoin__ (https://github.com/dpifke/pifkoin) 23 | * __praw__ (https://github.com/praw-dev/praw) 24 | * __sqlalchemy__ (http://www.sqlalchemy.org/) 25 | * __yaml__ (http://pyyaml.org/wiki/PyYAML) 26 | 27 | You can install `jinja2`, `praw`, `sqlalchemy`, and `yaml` using `pip` (Python Package Index tool) or a package manager in your OS. For `pifkoin`, you'll need to copy or symlink its "python" subdirectory to `src/ctb/pifkoin`. 28 | 29 | ### Database 30 | 31 | Create a new MySQL database instance and run included SQL file [altcointip.sql](altcointip.sql) to create necessary tables. Create a MySQL user and grant it all privileges on the database. If you don't like to deal with command-line MySQL, use `phpMyAdmin`. 32 | 33 | ### Coin Daemons 34 | 35 | Download one or more coin daemon executable. Create a configuration file for it in appropriate directory (such as `~/.dogecoin/dogecoin.conf` for Dogecoin), specifying `rpcuser`, `rpcpassword`, `rpcport`, and `server=1`, then start the daemon. It will take some time for the daemon to download the blockchain, after which you should verify that it's accepting commands (such as `dogecoind getinfo` and `dogecoind listaccounts`). 36 | 37 | ### Reddit Account 38 | 39 | You should create a dedicated Reddit account for your bot. Initially, Reddit will ask for CAPTCHA input when bot posts a comment or message. To remove CAPTCHA requirement, the bot account needs to accumulate positive karma. 40 | 41 | ### Configuration 42 | 43 | Copy included set of configuration files [src/conf-sample/](src/conf-sample/) as `src/conf/` and edit `reddit.yml`, `db.yml`, `coins.yml`, and `regex.yml`, specifying necessary settings. 44 | 45 | Most configuration options are described inline in provided sample configuration files. 46 | 47 | ### Running the Bot 48 | 49 | 1. Ensure MySQL is running and accepting connections given configured username/password 50 | 1. Ensure each configured coin daemon is running and responding to commands 51 | 1. Ensure Reddit authenticates configured user. _Note that from new users Reddit will require CAPTCHA responses when posting and sending messages. You will be able to type in CAPTCHA responses when required._ 52 | 1. Execute `_start.sh` from [src](src/) directory. The command will not return for as long as the bot is running. 53 | 54 | Here's the first few lines of DEBUG-level console output during successful initialization. 55 | 56 | user@host:/opt/altcointip/altcointip/src$ ./_start.sh 57 | INFO:cointipbot:CointipBot::init_logging(): -------------------- logging initialized -------------------- 58 | DEBUG:cointipbot:CointipBot::connect_db(): connecting to database... 59 | INFO:cointipbot:CointipBot::connect_db(): connected to database altcointip as altcointip 60 | DEBUG:cointipbot:CtbCoin::__init__(): connecting to Peercoin... 61 | DEBUG:bitcoin:Read 5 parameters from /opt/altcointip/coins/ppcoin/ppcoin.conf 62 | DEBUG:bitcoin:Making HTTP connection to 127.0.0.1:19902 63 | INFO:cointipbot:CtbCoin::__init__():: connected to Peercoin 64 | INFO:cointipbot:Setting tx fee of 0.010000 65 | DEBUG:bitcoin:Starting "settxfee" JSON-RPC request 66 | DEBUG:bitcoin:Got 36 byte response from server in 4 ms 67 | DEBUG:cointipbot:CtbCoin::__init__(): connecting to Primecoin... 68 | DEBUG:bitcoin:Read 5 parameters from /opt/altcointip/coins/primecoin/primecoin.conf 69 | DEBUG:bitcoin:Making HTTP connection to 127.0.0.1:18772 70 | INFO:cointipbot:CtbCoin::__init__():: connected to Primecoin 71 | INFO:cointipbot:Setting tx fee of 0.010000 72 | DEBUG:bitcoin:Starting "settxfee" JSON-RPC request 73 | DEBUG:bitcoin:Got 36 byte response from server in 1 ms 74 | DEBUG:cointipbot:CtbCoin::__init__(): connecting to Megacoin... 75 | DEBUG:bitcoin:Read 5 parameters from /opt/altcointip/coins/megacoin/megacoin.conf 76 | DEBUG:bitcoin:Making HTTP connection to 127.0.0.1:17950 77 | INFO:cointipbot:CtbCoin::__init__():: connected to Megacoin 78 | INFO:cointipbot:Setting tx fee of 0.010000 79 | DEBUG:bitcoin:Starting "settxfee" JSON-RPC request 80 | DEBUG:bitcoin:Got 36 byte response from server in 1 ms 81 | DEBUG:cointipbot:CtbCoin::__init__(): connecting to Litecoin... 82 | DEBUG:bitcoin:Read 5 parameters from /opt/altcointip/coins/litecoin/litecoin.conf 83 | DEBUG:bitcoin:Making HTTP connection to 127.0.0.1:19332 84 | INFO:cointipbot:CtbCoin::__init__():: connected to Litecoin 85 | INFO:cointipbot:Setting tx fee of 0.020000 86 | DEBUG:bitcoin:Starting "settxfee" JSON-RPC request 87 | DEBUG:bitcoin:Got 36 byte response from server in 2 ms 88 | DEBUG:cointipbot:CtbCoin::__init__(): connecting to Namecoin... 89 | DEBUG:bitcoin:Read 5 parameters from /opt/altcointip/coins/namecoin/namecoin.conf 90 | DEBUG:bitcoin:Making HTTP connection to 127.0.0.1:18336 91 | INFO:cointipbot:CtbCoin::__init__():: connected to Namecoin 92 | INFO:cointipbot:Setting tx fee of 0.010000 93 | DEBUG:bitcoin:Starting "settxfee" JSON-RPC request 94 | DEBUG:bitcoin:Got 36 byte response from server in 1 ms 95 | DEBUG:cointipbot:CtbCoin::__init__(): connecting to Bitcoin... 96 | DEBUG:bitcoin:Read 5 parameters from /opt/altcointip/coins/bitcoin/bitcoin.conf 97 | DEBUG:bitcoin:Making HTTP connection to 127.0.0.1:18332 98 | INFO:cointipbot:CtbCoin::__init__():: connected to Bitcoin 99 | INFO:cointipbot:Setting tx fee of 0.000100 100 | DEBUG:bitcoin:Starting "settxfee" JSON-RPC request 101 | DEBUG:bitcoin:Got 36 byte response from server in 1 ms 102 | DEBUG:cointipbot:CtbExchange::__init__(): initialized exchange crypto-trade.com 103 | DEBUG:cointipbot:CtbExchange::__init__(): initialized exchange www.bitstamp.net 104 | DEBUG:cointipbot:CtbExchange::__init__(): initialized exchange bter.com 105 | DEBUG:cointipbot:CtbExchange::__init__(): initialized exchange blockchain.info 106 | DEBUG:cointipbot:CtbExchange::__init__(): initialized exchange campbx.com 107 | DEBUG:cointipbot:CtbExchange::__init__(): initialized exchange vircurex.com 108 | DEBUG:cointipbot:CtbExchange::__init__(): initialized exchange pubapi.cryptsy.com 109 | DEBUG:cointipbot:CtbExchange::__init__(): initialized exchange btc-e.com 110 | DEBUG:cointipbot:CointipBot::connect_reddit(): connecting to Reddit... 111 | INFO:cointipbot:CointipBot::connect_reddit(): logged in to Reddit as ALTcointip 112 | ... 113 | 114 | ALTcointip bot is configured by default to append INFO-level log messages to `logs/info.log`, and WARNING-level log messages to `logs/warning.log`, while DEBUG-level log messages are output to the console. 115 | 116 | ### Cron: Backups 117 | 118 | Backups are very important! The last thing you want is losing user wallets or records of transactions in the database. 119 | 120 | There are three simple backup scripts included that support backing up the database, wallets, and configuration files to local directory and (optionally) to a remote host with `rsync`. Make sure to schedule regular backups with cron and test whether they are actually performed. Example cron configuration: 121 | 122 | 0 8,20 * * * cd /opt/altcointip/altcointip/src && python _backup_db.py ~/backups 123 | 0 9,21 * * * cd /opt/altcointip/altcointip/src && python _backup_wallets.py ~/backups 124 | 0 10 * * * cd /opt/altcointip/altcointip/src && python _backup_config.py ~/backups 125 | 126 | ### Cron: Statistics 127 | 128 | ALTcointip bot can be configured to generate tipping statistics pages (overall and per-user) and publish them using subreddit's wiki. After you configure and enable statistics in configuration, add the following cron job to update the main statistics page periodically: 129 | 130 | 0 */3 * * * cd /opt/altcointip/altcointip/src && python _update_stats.py 131 | 132 | ### What If I Want To Enable More Cryptocoins Later? 133 | 134 | If you want to add a new cryptocoin after you already have a few registered users, you need to retroactively create the new cryptocoin address for users who have already registered. See [src/_add_coin.py](src/_add_coin.py) for details. 135 | -------------------------------------------------------------------------------- /src/ctb/ctb_coin.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of ALTcointip. 3 | 4 | ALTcointip is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | ALTcointip is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with ALTcointip. If not, see . 16 | """ 17 | 18 | import logging, re, time 19 | from pifkoin.bitcoind import Bitcoind, BitcoindException 20 | from httplib import CannotSendRequest 21 | 22 | lg = logging.getLogger('cointipbot') 23 | 24 | class CtbCoin(object): 25 | """ 26 | Coin class for cointip bot 27 | """ 28 | 29 | conn = None 30 | conf = None 31 | 32 | def __init__(self, _conf = None): 33 | """ 34 | Initialize CtbCoin with given parameters. _conf is a coin config dictionary defined in conf/coins.yml 35 | """ 36 | 37 | # verify _conf is a config dictionary 38 | if not _conf or not hasattr(_conf, 'name') or not hasattr(_conf, 'config_file') or not hasattr(_conf, 'txfee'): 39 | raise Exception("CtbCoin::__init__(): _conf is empty or invalid") 40 | 41 | self.conf = _conf 42 | 43 | # connect to coin daemon 44 | try: 45 | lg.debug("CtbCoin::__init__(): connecting to %s...", self.conf.name) 46 | self.conn = Bitcoind(self.conf.config_file, rpcserver=self.conf.config_rpcserver) 47 | except BitcoindException as e: 48 | lg.error("CtbCoin::__init__(): error connecting to %s using %s: %s", self.conf.name, self.conf.config_file, e) 49 | raise 50 | 51 | lg.info("CtbCoin::__init__():: connected to %s", self.conf.name) 52 | time.sleep(0.5) 53 | 54 | # set transaction fee 55 | lg.info("Setting tx fee of %f", self.conf.txfee) 56 | self.conn.settxfee(self.conf.txfee) 57 | 58 | def getbalance(self, _user = None, _minconf = None): 59 | """ 60 | Get user's tip or withdraw balance. _minconf is number of confirmations to use. 61 | Returns (float) balance 62 | """ 63 | lg.debug("CtbCoin::getbalance(%s, %s)", _user, _minconf) 64 | 65 | user = self.verify_user(_user=_user) 66 | minconf = self.verify_minconf(_minconf=_minconf) 67 | balance = float(0) 68 | 69 | try: 70 | balance = self.conn.getbalance(user, minconf) 71 | except BitcoindException as e: 72 | lg.error("CtbCoin.getbalance(): error getting %s (minconf=%s) balance for %s: %s", self.conf.name, minconf, user, e) 73 | raise 74 | 75 | time.sleep(0.5) 76 | return float(balance) 77 | 78 | def sendtouser(self, _userfrom = None, _userto = None, _amount = None, _minconf = 1): 79 | """ 80 | Transfer (move) coins to user 81 | Returns (bool) 82 | """ 83 | lg.debug("CtbCoin::sendtouser(%s, %s, %s)", _userfrom, _userto, _amount) 84 | 85 | userfrom = self.verify_user(_user=_userfrom) 86 | userto = self.verify_user(_user=_userto) 87 | amount = self.verify_amount(_amount=_amount) 88 | 89 | # send request to coin daemon 90 | try: 91 | lg.info("CtbCoin::sendtouser(): moving %s %s from %s to %s", amount, self.conf.name, userfrom, userto) 92 | result = self.conn.move(userfrom, userto, amount) 93 | time.sleep(0.5) 94 | except Exception as e: 95 | lg.error("CtbCoin::sendtouser(): error sending %s %s from %s to %s: %s", amount, self.conf.name, userfrom, userto, e) 96 | return False 97 | 98 | time.sleep(0.5) 99 | return True 100 | 101 | def sendtoaddr(self, _userfrom = None, _addrto = None, _amount = None): 102 | """ 103 | Send coins to address 104 | Returns (string) txid 105 | """ 106 | lg.debug("CtbCoin::sendtoaddr(%s, %s, %s)", _userfrom, _addrto, _amount) 107 | 108 | userfrom = self.verify_user(_user=_userfrom) 109 | addrto = self.verify_addr(_addr=_addrto) 110 | amount = self.verify_amount(_amount=_amount) 111 | minconf = self.verify_minconf(_minconf=self.conf.minconf.withdraw) 112 | txid = "" 113 | 114 | # send request to coin daemon 115 | try: 116 | lg.info("CtbCoin::sendtoaddr(): sending %s %s from %s to %s", amount, self.conf.name, userfrom, addrto) 117 | 118 | # Unlock wallet, if applicable 119 | if hasattr(self.conf, 'walletpassphrase'): 120 | lg.debug("CtbCoin::sendtoaddr(): unlocking wallet...") 121 | self.conn.walletpassphrase(self.conf.walletpassphrase, 1) 122 | 123 | # Perform transaction 124 | lg.debug("CtbCoin::sendtoaddr(): calling sendfrom()...") 125 | txid = self.conn.sendfrom(userfrom, addrto, amount, minconf) 126 | 127 | # Lock wallet, if applicable 128 | if hasattr(self.conf, 'walletpassphrase'): 129 | lg.debug("CtbCoin::sendtoaddr(): locking wallet...") 130 | self.conn.walletlock() 131 | 132 | except Exception as e: 133 | lg.error("CtbCoin::sendtoaddr(): error sending %s %s from %s to %s: %s", amount, self.conf.name, userfrom, addrto, e) 134 | raise 135 | 136 | time.sleep(0.5) 137 | return str(txid) 138 | 139 | def validateaddr(self, _addr = None): 140 | """ 141 | Verify that _addr is a valid coin address 142 | Returns (bool) 143 | """ 144 | lg.debug("CtbCoin::validateaddr(%s)", _addr) 145 | 146 | addr = self.verify_addr(_addr=_addr) 147 | addr_valid = self.conn.validateaddress(addr) 148 | time.sleep(0.5) 149 | 150 | if not addr_valid.has_key('isvalid') or not addr_valid['isvalid']: 151 | lg.debug("CtbCoin::validateaddr(%s): not valid", addr) 152 | return False 153 | else: 154 | lg.debug("CtbCoin::validateaddr(%s): valid", addr) 155 | return True 156 | 157 | def getnewaddr(self, _user = None): 158 | """ 159 | Generate a new address for _user 160 | Returns (string) address 161 | """ 162 | 163 | user = self.verify_user(_user=_user) 164 | addr = "" 165 | counter = 0 166 | 167 | while True: 168 | try: 169 | # Unlock wallet for keypoolrefill 170 | if hasattr(self.conf, 'walletpassphrase'): 171 | self.conn.walletpassphrase(self.conf.walletpassphrase, 1) 172 | 173 | # Generate new address 174 | addr = self.conn.getnewaddress(user) 175 | 176 | # Lock wallet 177 | if hasattr(self.conf, 'walletpassphrase'): 178 | self.conn.walletlock() 179 | 180 | if not addr: 181 | raise Exception("CtbCoin::getnewaddr(%s): empty addr", user) 182 | 183 | time.sleep(0.1) 184 | return str(addr) 185 | 186 | except BitcoindException as e: 187 | lg.error("CtbCoin::getnewaddr(%s): BitcoindException: %s", user, e) 188 | raise 189 | except CannotSendRequest as e: 190 | if counter < 3: 191 | lg.warning("CtbCoin::getnewaddr(%s): CannotSendRequest, retrying") 192 | counter += 1 193 | time.sleep(10) 194 | continue 195 | else: 196 | raise 197 | except Exception as e: 198 | if str(e) == "timed out" and counter < 3: 199 | lg.warning("CtbCoin::getnewaddr(%s): timed out, retrying") 200 | counter += 1 201 | time.sleep(10) 202 | continue 203 | else: 204 | lg.error("CtbCoin::getnewaddr(%s): Exception: %s", user, e) 205 | raise 206 | 207 | 208 | def verify_user(self, _user = None): 209 | """ 210 | Verify and return a username 211 | """ 212 | 213 | if not _user or not type(_user) in [str, unicode]: 214 | raise Exception("CtbCoin::verify_user(): _user wrong type (%s) or empty (%s)", type(_user), _user) 215 | 216 | return str(_user.lower()) 217 | 218 | def verify_addr(self, _addr = None): 219 | """ 220 | Verify and return coin address 221 | """ 222 | 223 | if not _addr or not type(_addr) in [str, unicode]: 224 | raise Exception("CtbCoin::verify_addr(): _addr wrong type (%s) or empty (%s)", type(_addr),_addr) 225 | 226 | return re.escape(str(_addr)) 227 | 228 | def verify_amount(self, _amount = None): 229 | """ 230 | Verify and return amount 231 | """ 232 | 233 | if not _amount or not type(_amount) in [int, float] or not _amount > 0: 234 | raise Exception("CtbCoin::verify_amount(): _amount wrong type (%s), empty, or negative (%s)", type(_amount), _amount) 235 | 236 | return _amount 237 | 238 | def verify_minconf(self, _minconf = None): 239 | """ 240 | Verify and return minimum number of confirmations 241 | """ 242 | 243 | if not _minconf or not type(_minconf) == int or not _minconf >= 0: 244 | raise Exception("CtbCoin::verify_minconf(): _minconf wrong type (%s), empty, or negative (%s)", type(_minconf), _minconf) 245 | 246 | return _minconf 247 | -------------------------------------------------------------------------------- /src/ctb/ctb_stats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | This file is part of ALTcointip. 6 | 7 | ALTcointip is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | ALTcointip is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with ALTcointip. If not, see . 19 | """ 20 | 21 | import logging, re, time 22 | import ctb_misc 23 | 24 | lg = logging.getLogger('cointipbot') 25 | 26 | def update_stats(ctb=None): 27 | """ 28 | Update stats wiki page 29 | """ 30 | 31 | stats = "" 32 | 33 | if not ctb.conf.reddit.stats.enabled: 34 | return None 35 | 36 | for s in sorted(vars(ctb.conf.db.sql.globalstats)): 37 | lg.debug("update_stats(): getting stats for '%s'" % s) 38 | sql = ctb.conf.db.sql.globalstats[s].query 39 | stats += "\n\n### %s\n\n" % ctb.conf.db.sql.globalstats[s].name 40 | stats += "%s\n\n" % ctb.conf.db.sql.globalstats[s].desc 41 | 42 | mysqlexec = ctb.db.execute(sql) 43 | if mysqlexec.rowcount <= 0: 44 | lg.warning("update_stats(): query <%s> returned nothing" % ctb.conf.db.sql.globalstats[s].query) 45 | continue 46 | 47 | if ctb.conf.db.sql.globalstats[s].type == "line": 48 | m = mysqlexec.fetchone() 49 | k = mysqlexec.keys()[0] 50 | value = format_value(m, k, '', ctb) 51 | stats += "%s = **%s**\n" % (k, value) 52 | 53 | elif ctb.conf.db.sql.globalstats[s].type == "table": 54 | stats += ("|".join(mysqlexec.keys())) + "\n" 55 | stats += ("|".join([":---"] * len(mysqlexec.keys()))) + "\n" 56 | for m in mysqlexec: 57 | values = [] 58 | for k in mysqlexec.keys(): 59 | values.append(format_value(m, k, '', ctb)) 60 | stats += ("|".join(values)) + "\n" 61 | 62 | else: 63 | lg.error("update_stats(): don't know what to do with type '%s'" % ctb.conf.db.sql.globalstats[s].type) 64 | return False 65 | 66 | stats += "\n" 67 | 68 | lg.debug("update_stats(): updating subreddit '%s', page '%s'" % (ctb.conf.reddit.stats.subreddit, ctb.conf.reddit.stats.page)) 69 | return ctb_misc.praw_call(ctb.reddit.edit_wiki_page, ctb.conf.reddit.stats.subreddit, ctb.conf.reddit.stats.page, stats, "Update by ALTcointip bot") 70 | 71 | def update_tips(ctb=None): 72 | """ 73 | Update page listing all tips 74 | """ 75 | 76 | if not ctb.conf.reddit.stats.enabled: 77 | return None 78 | 79 | # Start building stats page 80 | tip_list = "### All Completed Tips\n\n" 81 | 82 | q = ctb.db.execute(ctb.conf.db.sql.tips.sql_set) 83 | tips = ctb.db.execute(ctb.conf.db.sql.tips.sql_list, (ctb.conf.db.sql.tips.limit)) 84 | tip_list += ("|".join(tips.keys())) + "\n" 85 | tip_list += ("|".join([":---"] * len(tips.keys()))) + "\n" 86 | 87 | # Build tips table 88 | for t in tips: 89 | values = [] 90 | for k in tips.keys(): 91 | values.append(format_value(t, k, '', ctb)) 92 | tip_list += ("|".join(values)) + "\n" 93 | 94 | lg.debug("update_tips(): updating subreddit '%s', page '%s'" % (ctb.conf.reddit.stats.subreddit, ctb.conf.reddit.stats.page_tips)) 95 | ctb_misc.praw_call(ctb.reddit.edit_wiki_page, ctb.conf.reddit.stats.subreddit, ctb.conf.reddit.stats.page_tips, tip_list, "Update by ALTcointip bot") 96 | 97 | return True 98 | 99 | def update_all_user_stats(ctb=None): 100 | """ 101 | Update individual user stats for all uers 102 | """ 103 | 104 | if not ctb.conf.reddit.stats.enabled: 105 | lg.error('update_all_user_stats(): stats are not enabled in config.yml') 106 | return None 107 | 108 | users = ctb.db.execute(ctb.conf.db.sql.userstats.users) 109 | for u in users: 110 | update_user_stats(ctb=ctb, username=u['username']) 111 | 112 | def update_user_stats(ctb=None, username=None): 113 | """ 114 | Update individual user stats for given username 115 | """ 116 | 117 | if not ctb.conf.reddit.stats.enabled: 118 | return None 119 | 120 | # List of coins 121 | coins_q = ctb.db.execute(ctb.conf.db.sql.userstats.coins) 122 | coins = [] 123 | for c in coins_q: 124 | coins.append(c['coin']) 125 | 126 | # List of fiat 127 | fiat_q = ctb.db.execute(ctb.conf.db.sql.userstats.fiat) 128 | fiat = [] 129 | for f in fiat_q: 130 | fiat.append(f['fiat']) 131 | 132 | # Start building stats page 133 | user_stats = "### Tipping Summary for /u/%s\n\n" % username 134 | page = ctb.conf.reddit.stats.page + '_' + username 135 | 136 | # Total Tipped 137 | user_stats += "#### Total Tipped (Fiat)\n\n" 138 | user_stats += "fiat|total\n:---|---:\n" 139 | total_tipped = [] 140 | for f in fiat: 141 | mysqlexec = ctb.db.execute(ctb.conf.db.sql.userstats.total_tipped_fiat, (username, f)) 142 | total_tipped_fiat = mysqlexec.fetchone() 143 | if total_tipped_fiat['total_fiat'] != None: 144 | user_stats += "**%s**|%s %.2f\n" % (f, ctb.conf.fiat[f].symbol, total_tipped_fiat['total_fiat']) 145 | total_tipped.append("%s%.2f" % (ctb.conf.fiat[f].symbol, total_tipped_fiat['total_fiat'])) 146 | user_stats += "\n" 147 | 148 | user_stats += "#### Total Tipped (Coins)\n\n" 149 | user_stats += "coin|total\n:---|---:\n" 150 | for c in coins: 151 | mysqlexec = ctb.db.execute(ctb.conf.db.sql.userstats.total_tipped_coin, (username, c)) 152 | total_tipped_coin = mysqlexec.fetchone() 153 | if total_tipped_coin['total_coin'] != None: 154 | user_stats += "**%s**|%s %.6f\n" % (c, ctb.conf.coins[c].symbol, total_tipped_coin['total_coin']) 155 | user_stats += "\n" 156 | 157 | # Total received 158 | user_stats += "#### Total Received (Fiat)\n\n" 159 | user_stats += "fiat|total\n:---|---:\n" 160 | total_received = [] 161 | for f in fiat: 162 | mysqlexec = ctb.db.execute(ctb.conf.db.sql.userstats.total_received_fiat, (username, f)) 163 | total_received_fiat = mysqlexec.fetchone() 164 | if total_received_fiat['total_fiat'] != None: 165 | user_stats += "**%s**|%s %.2f\n" % (f, ctb.conf.fiat[f].symbol, total_received_fiat['total_fiat']) 166 | total_received.append("%s%.2f" % (ctb.conf.fiat[f].symbol, total_received_fiat['total_fiat'])) 167 | user_stats += "\n" 168 | 169 | user_stats += "#### Total Received (Coins)\n\n" 170 | user_stats += "coin|total\n:---|---:\n" 171 | for c in coins: 172 | mysqlexec = ctb.db.execute(ctb.conf.db.sql.userstats.total_received_coin, (username, c)) 173 | total_received_coin = mysqlexec.fetchone() 174 | if total_received_coin['total_coin'] != None: 175 | user_stats += "**%s**|%s %.6f\n" % (c, ctb.conf.coins[c].symbol, total_received_coin['total_coin']) 176 | user_stats += "\n" 177 | 178 | # History 179 | user_stats += "#### History\n\n" 180 | history = ctb.db.execute(ctb.conf.db.sql.userstats.history, (username, username)) 181 | user_stats += ("|".join(history.keys())) + "\n" 182 | user_stats += ("|".join([":---"] * len(history.keys()))) + "\n" 183 | 184 | # Build history table 185 | num_tipped = 0 186 | num_received = 0 187 | for m in history: 188 | if m['state'] == 'completed': 189 | if m['from_user'].lower() == username.lower(): 190 | num_tipped += 1 191 | elif m['to_user'].lower() == username.lower(): 192 | num_received += 1 193 | values = [] 194 | for k in history.keys(): 195 | values.append(format_value(m, k, username, ctb)) 196 | user_stats += ("|".join(values)) + "\n" 197 | 198 | # Submit changes 199 | lg.debug("update_user_stats(): updating subreddit '%s', page '%s'" % (ctb.conf.reddit.stats.subreddit, page)) 200 | ctb_misc.praw_call(ctb.reddit.edit_wiki_page, ctb.conf.reddit.stats.subreddit, page, user_stats, "Update by ALTcointip bot") 201 | 202 | # Update user flair on subreddit 203 | if ctb.conf.reddit.stats.userflair and ( len(total_tipped) > 0 or len(total_received) > 0 ): 204 | flair = "" 205 | if len(total_tipped) > 0: 206 | flair += "tipped[" + '|'.join(total_tipped) + "]" 207 | flair += " (%d)" % num_tipped 208 | if len(total_received) > 0: 209 | if len(total_tipped) > 0: 210 | flair += " / " 211 | flair += "received[" + '|'.join(total_received) + "]" 212 | flair += " (%d)" % num_received 213 | lg.debug("update_user_stats(): updating flair for %s (%s)", username, flair) 214 | r = ctb_misc.praw_call(ctb.reddit.get_subreddit, ctb.conf.reddit.stats.subreddit) 215 | res = ctb_misc.praw_call(r.set_flair, username, flair, '') 216 | lg.debug(res) 217 | 218 | return True 219 | 220 | def format_value(m, k, username, ctb, compact=False): 221 | """ 222 | Format value for display based on its type 223 | m[k] is the value, k is the database row name 224 | """ 225 | 226 | if not m[k]: 227 | return '-' 228 | 229 | # Format cryptocoin 230 | if type(m[k]) == float and k.find("coin") > -1: 231 | coin_symbol = ctb.conf.coins[m['coin']].symbol 232 | return "%s %.5g" % (coin_symbol, m[k]) 233 | 234 | # Format fiat 235 | elif type(m[k]) == float and ( k.find("fiat") > -1 or k.find("usd") > -1 ): 236 | fiat_symbol = ctb.conf.fiat[m['fiat']].symbol 237 | return "%s %.2f" % (fiat_symbol, m[k]) 238 | 239 | # Format username 240 | elif k.find("user") > -1 and type( m[k] ) in [str, unicode]: 241 | if compact: 242 | return ("**/u/%s**" % m[k]) if m[k].lower() == username.lower() else ("/u/%s" % m[k]) 243 | else: 244 | un = ("**%s**" % m[k]) if m[k].lower() == username.lower() else m[k] 245 | toreturn = "[%s](/u/%s)" % (un, re.escape(m[k])) 246 | if m[k].lower() != username.lower(): 247 | toreturn += "^[[stats]](/r/%s/wiki/%s_%s)" % (ctb.conf.reddit.stats.subreddit, ctb.conf.reddit.stats.page, m[k]) 248 | return toreturn 249 | 250 | # Format address 251 | elif k.find("addr") > -1: 252 | displayaddr = m[k][:6] + "..." + m[k][-5:] 253 | return "[%s](%s%s)" % (displayaddr, ctb.conf.coins[m['coin']].explorer.address, m[k]) 254 | 255 | # Format state 256 | elif k.find("state") > -1: 257 | if m[k] == 'completed': 258 | return unicode('✓', 'utf8') 259 | else: 260 | return m[k] 261 | 262 | # Format type 263 | elif k.find("type") > -1: 264 | if m[k] == 'givetip': 265 | return 'tip' 266 | if compact: 267 | if m[k] == 'withdraw': 268 | return 'w' 269 | if m[k] == 'redeem': 270 | return 'r' 271 | 272 | # Format subreddit 273 | elif k.find("subreddit") > -1: 274 | return "/r/%s" % m[k] 275 | 276 | # Format link 277 | elif k.find("link") > -1: 278 | return "[link](%s)" % m[k] 279 | 280 | # Format time 281 | elif k.find("utc") > -1: 282 | return "%s" % time.strftime('%Y-%m-%d', time.localtime(m[k])) 283 | 284 | # It's something else 285 | else: 286 | return str(m[k]) 287 | -------------------------------------------------------------------------------- /src/ctb/ctb_user.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of ALTcointip. 3 | 4 | ALTcointip is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | ALTcointip is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with ALTcointip. If not, see . 16 | """ 17 | 18 | import ctb_misc 19 | 20 | import logging, time, praw, re 21 | 22 | lg = logging.getLogger('cointipbot') 23 | 24 | class CtbUser(object): 25 | """ 26 | User class for cointip bot 27 | """ 28 | 29 | # Basic properties 30 | name=None 31 | giftamount=None 32 | joindate=None 33 | addr={} 34 | banned=False 35 | 36 | # Objects 37 | prawobj=None 38 | ctb=None 39 | 40 | def __init__(self, name=None, redditobj=None, ctb=None): 41 | """ 42 | Initialize CtbUser object with given parameters 43 | """ 44 | lg.debug("> CtbUser::__init__(%s)", name) 45 | 46 | if not bool(name): 47 | raise Exception("CtbUser::__init__(): name must be set") 48 | self.name = name 49 | 50 | if not bool(ctb): 51 | raise Exception("CtbUser::__init__(): ctb must be set") 52 | self.ctb = ctb 53 | 54 | if bool(redditobj): 55 | self.prawobj = redditobj 56 | 57 | # Determine if user is banned 58 | if ctb.conf.reddit.banned_users: 59 | if ctb.conf.reddit.banned_users.method == 'subreddit': 60 | for u in ctb.reddit.get_banned(ctb.conf.reddit.banned_users.subreddit): 61 | if self.name.lower() == u.name.lower(): 62 | self.banned = True 63 | elif ctb.conf.reddit.banned_users.method == 'list': 64 | for u in ctb.conf.reddit.banned_users.list: 65 | if self.name.lower() == u.lower(): 66 | self.banned = True 67 | else: 68 | lg.warning("CtbUser::__init__(): invalid method '%s' in banned_users config" % ctb.conf.reddit.banned_users.method) 69 | 70 | lg.debug("< CtbUser::__init__(%s) DONE", name) 71 | 72 | def __str__(self): 73 | """ 74 | Return string representation of self 75 | """ 76 | me = "" 77 | me = me % (self.name, self.giftamount, self.joindate, self.addr, self.prawobj, self.ctb, self.banned) 78 | return me 79 | 80 | def get_balance(self, coin=None, kind=None): 81 | """ 82 | If coin is specified, return float with coin balance for user. Else, return a dict with balance of each coin for user. 83 | """ 84 | lg.debug("> CtbUser::balance(%s)", self.name) 85 | 86 | if not bool(coin) or not bool(kind): 87 | raise Exception("CtbUser::balance(%s): coin or kind not set" % self.name) 88 | 89 | # Ask coin daemon for account balance 90 | lg.info("CtbUser::balance(%s): getting %s %s balance", self.name, coin, kind) 91 | balance = self.ctb.coins[coin].getbalance(_user=self.name, _minconf=self.ctb.conf.coins[coin].minconf[kind]) 92 | 93 | lg.debug("< CtbUser::balance(%s) DONE", self.name) 94 | return float(balance) 95 | 96 | def get_addr(self, coin=None): 97 | """ 98 | Return coin address of user 99 | """ 100 | lg.debug("> CtbUser::get_addr(%s, %s)", self.name, coin) 101 | 102 | if hasattr(self.addr, coin): 103 | return self.addr[coin] 104 | 105 | sql = "SELECT address from t_addrs WHERE username = %s AND coin = %s" 106 | mysqlrow = self.ctb.db.execute(sql, (self.name.lower(), coin.lower())).fetchone() 107 | if mysqlrow == None: 108 | lg.debug("< CtbUser::get_addr(%s, %s) DONE (no)", self.name, coin) 109 | return None 110 | else: 111 | self.addr[coin] = mysqlrow['address'] 112 | lg.debug("< CtbUser::get_addr(%s, %s) DONE (%s)", self.name, coin, self.addr[coin]) 113 | return self.addr[coin] 114 | 115 | lg.debug("< CtbUser::get_addr(%s, %s) DONE (should never happen)", self.name, coin) 116 | return None 117 | 118 | def is_on_reddit(self): 119 | """ 120 | Return true if username exists Reddit. Also set prawobj pointer while at it. 121 | """ 122 | lg.debug("> CtbUser::is_on_reddit(%s)", self.name) 123 | 124 | # Return true if prawobj is already set 125 | if bool(self.prawobj): 126 | lg.debug("< CtbUser::is_on_reddit(%s) DONE (yes)", self.name) 127 | return True 128 | 129 | try: 130 | self.prawobj = ctb_misc.praw_call(self.ctb.reddit.get_redditor, self.name) 131 | if self.prawobj: 132 | return True 133 | else: 134 | return False 135 | 136 | except Exception as e: 137 | lg.debug("< CtbUser::is_on_reddit(%s) DONE (no)", self.name) 138 | return False 139 | 140 | lg.warning("< CtbUser::is_on_reddit(%s): returning None (shouldn't happen)", self.name) 141 | return None 142 | 143 | def is_registered(self): 144 | """ 145 | Return true if user is registered with CointipBot 146 | """ 147 | lg.debug("> CtbUser::is_registered(%s)", self.name) 148 | 149 | try: 150 | # First, check t_users table 151 | sql = "SELECT * FROM t_users WHERE username = %s" 152 | mysqlrow = self.ctb.db.execute(sql, (self.name.lower())).fetchone() 153 | 154 | if mysqlrow == None: 155 | lg.debug("< CtbUser::is_registered(%s) DONE (no)", self.name) 156 | return False 157 | 158 | else: 159 | # Next, check t_addrs table for whether user has correct number of coin addresses 160 | sql_coins = "SELECT COUNT(*) AS count FROM t_addrs WHERE username = %s" 161 | mysqlrow_coins = self.ctb.db.execute(sql_coins, (self.name.lower())).fetchone() 162 | 163 | if int(mysqlrow_coins['count']) != len(self.ctb.coins): 164 | if int(mysqlrow_coins['count']) == 0: 165 | # Bot probably crashed during user registration process 166 | # Delete user 167 | lg.warning("CtbUser::is_registered(%s): deleting user, incomplete registration", self.name) 168 | sql_delete = "DELETE FROM t_users WHERE username = %s" 169 | mysql_res = self.ctb.db.execute(sql_delete, (self.name.lower())) 170 | # User is not registered 171 | return False 172 | else: 173 | raise Exception("CtbUser::is_registered(%s): user has %s coins but %s active" % (self.name, mysqlrow_coins['count'], len(self.ctb.coins))) 174 | 175 | # Set some properties 176 | self.giftamount = mysqlrow['giftamount'] 177 | 178 | # Done 179 | lg.debug("< CtbUser::is_registered(%s) DONE (yes)", self.name) 180 | return True 181 | 182 | except Exception, e: 183 | lg.error("CtbUser::is_registered(%s): error while executing <%s>: %s", self.name, sql % self.name.lower(), e) 184 | raise 185 | 186 | lg.warning("< CtbUser::is_registered(%s): returning None (shouldn't happen)", self.name) 187 | return None 188 | 189 | def tell(self, subj=None, msg=None, msgobj=None): 190 | """ 191 | Send a Reddit message to user 192 | """ 193 | lg.debug("> CtbUser::tell(%s)", self.name) 194 | 195 | if not bool(subj) or not bool(msg): 196 | raise Exception("CtbUser::tell(%s): subj or msg not set", self.name) 197 | 198 | if not self.is_on_reddit(): 199 | raise Exception("CtbUser::tell(%s): not a Reddit user", self.name) 200 | 201 | if bool(msgobj): 202 | lg.debug("CtbUser::tell(%s): replying to message", msgobj.id) 203 | ctb_misc.praw_call(msgobj.reply, msg) 204 | else: 205 | lg.debug("CtbUser::tell(%s): sending message", self.name) 206 | ctb_misc.praw_call(self.prawobj.send_message, subj, msg) 207 | 208 | lg.debug("< CtbUser::tell(%s) DONE", self.name) 209 | return True 210 | 211 | def register(self): 212 | """ 213 | Add user to database and generate coin addresses 214 | """ 215 | lg.debug("> CtbUser::register(%s)", self.name) 216 | 217 | # Add user to database 218 | try: 219 | sql_adduser = "INSERT INTO t_users (username) VALUES (%s)" 220 | mysqlexec = self.ctb.db.execute(sql_adduser, (self.name.lower())) 221 | if mysqlexec.rowcount <= 0: 222 | raise Exception("CtbUser::register(%s): rowcount <= 0 while executing <%s>" % ( self.name, sql_adduser % (self.name.lower()) )) 223 | except Exception, e: 224 | lg.error("CtbUser::register(%s): exception while executing <%s>: %s", self.name, sql_adduser % (self.name.lower()), e) 225 | raise 226 | 227 | # Get new coin addresses 228 | new_addrs = {} 229 | for c in self.ctb.coins: 230 | new_addrs[c] = self.ctb.coins[c].getnewaddr(_user=self.name.lower()) 231 | lg.info("CtbUser::register(%s): got %s address %s", self.name, c, new_addrs[c]) 232 | 233 | # Add coin addresses to database 234 | for c in new_addrs: 235 | try: 236 | sql_addr = "REPLACE INTO t_addrs (username, coin, address) VALUES (%s, %s, %s)" 237 | mysqlexec = self.ctb.db.execute(sql_addr, (self.name.lower(), c, new_addrs[c])) 238 | if mysqlexec.rowcount <= 0: 239 | # Undo change to database 240 | delete_user(_username=self.name.lower(), _db=self.ctb.db) 241 | raise Exception("CtbUser::register(%s): rowcount <= 0 while executing <%s>" % (self.name, sql_addr % (self.name.lower(), c, new_addrs[c]))) 242 | 243 | except Exception, e: 244 | # Undo change to database 245 | delete_user(_username=self.name.lower(), _db=self.ctb.db) 246 | raise 247 | 248 | lg.debug("< CtbUser::register(%s) DONE", self.name) 249 | return True 250 | 251 | def get_redeem_amount(self, coin=None, fiat=None): 252 | """ 253 | Return karma redeem amount for a given coin 254 | """ 255 | lg.debug("> CtbUser::get_redeem_amount(%s)", coin) 256 | 257 | if not coin or not self.ctb.coins.has_key(coin): 258 | raise Exception("CtbUser::get_redeem_amount(%s): invalid coin" % coin) 259 | if not fiat or not self.ctb.conf.fiat.has_key(fiat): 260 | raise Exception("CtbUser::get_redeem_amount(%s): invalid fiat" % fiat) 261 | 262 | # Check if we have coin's fiat value 263 | coin_value = self.ctb.coin_value(coin, fiat) 264 | if not coin_value or not coin_value > 0.0: 265 | lg.warning("CtbUser::get_redeem_amount(%s): coin_value not available", coin) 266 | return (None, None) 267 | 268 | # First, determine fiat value due to link karma 269 | link_mul = self.ctb.conf.reddit.redeem.multiplier.link 270 | if type(link_mul) in [str, unicode]: 271 | link_mul = eval(link_mul) 272 | if not type(link_mul) == float: 273 | raise Exception("CtbUser::get_redeem_amount(): type of link_mul is not float") 274 | link_val = float(self.prawobj.link_karma) * link_mul 275 | 276 | # Second, determine fiat value due to comment karma 277 | comm_mul = self.ctb.conf.reddit.redeem.multiplier.comment 278 | if type(comm_mul) in [str, unicode]: 279 | comm_mul = eval(comm_mul) 280 | if not type(comm_mul) == float: 281 | raise Exception("CtbUser::get_redeem_amount(): type of comm_mul is not float") 282 | comm_val = float(self.prawobj.comment_karma) * comm_mul 283 | 284 | # Third, determine base fiat value from config 285 | base_val = self.ctb.conf.reddit.redeem.base 286 | if type(base_val) in [str, unicode]: 287 | base_val = eval(base_val) 288 | if not type(base_val) == float: 289 | raise Exception("CtbUser::get_redeem_amount(): type of base_val is not float") 290 | 291 | # Sum link_val, comm_val, and base_val to get total fiat 292 | total_fiat = link_val + comm_val + base_val 293 | 294 | # Check if above maximum 295 | if total_fiat > self.ctb.conf.reddit.redeem.maximum: 296 | total_fiat = self.ctb.conf.reddit.redeem.maximum 297 | 298 | # Determine total coin value using exchange rate 299 | total_coin = total_fiat / coin_value 300 | 301 | lg.debug("< CtbUser::get_redeem_amount(%s) DONE", coin) 302 | return (total_coin, total_fiat) 303 | 304 | 305 | def delete_user(_username=None, _db=None): 306 | """ 307 | Delete _username from t_users and t_addrs tables 308 | """ 309 | lg.debug("> delete_user(%s)", _username) 310 | 311 | try: 312 | sql_arr = ["DELETE FROM t_users WHERE username = %s", 313 | "DELETE FROM t_addrs WHERE username = %s"] 314 | for sql in sql_arr: 315 | mysqlexec = _db.execute(sql, _username.lower()) 316 | if mysqlexec.rowcount <= 0: 317 | lg.warning("delete_user(%s): rowcount <= 0 while executing <%s>", _username, sql % _username.lower()) 318 | 319 | except Exception, e: 320 | lg.error("delete_user(%s): error while executing <%s>: %s", _username, sql % _username.lower(), e) 321 | return False 322 | 323 | lg.debug("< delete_user(%s) DONE", _username) 324 | return True 325 | -------------------------------------------------------------------------------- /src/conf-sample/regex.yml: -------------------------------------------------------------------------------- 1 | values: 2 | username: 3 | regex: '(@[\w-]{3,20})' 4 | amount: 5 | regex: '([0-9]{1,9}(?:\.[0-9]{0,16})?)' 6 | keywords: 7 | regex: '(all|flip|megaflip|gigaflip|roll|megaroll|gigaroll|sushi|random01|random05|random1|random3|random5|random10|random15|random25|random50|random100|random250|random500|random1000|upvote|pi|e|leet|quarter|half|cookie|nothing|gum|beer|nicebeer|hug|kiss|nyan|highfive|fart|coffee)' 8 | tip_init: 9 | regex: '(/u/mybotuser|mybotuser)' 10 | 11 | actions: 12 | givetip: 13 | enabled: true 14 | public: true 15 | simple: false 16 | regex: 17 | a: 18 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_AMOUNT}(\s+){REGEX_COIN}' 19 | rg_amount: 4 20 | rg_keyword: 0 21 | rg_address: 0 22 | rg_to_user: 0 23 | rg_coin: 6 24 | rg_fiat: 0 25 | aa: 26 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_KEYWORD}(\s+){REGEX_COIN}' 27 | rg_amount: 0 28 | rg_keyword: 4 29 | rg_address: 0 30 | rg_to_user: 0 31 | rg_coin: 6 32 | rg_fiat: 0 33 | b: 34 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_ADDRESS}(\s+){REGEX_AMOUNT}(\s+){REGEX_COIN}' 35 | rg_amount: 6 36 | rg_keyword: 0 37 | rg_address: 4 38 | rg_to_user: 0 39 | rg_coin: 8 40 | rg_fiat: 0 41 | bb: 42 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_ADDRESS}(\s+){REGEX_KEYWORD}(\s+){REGEX_COIN}' 43 | rg_amount: 0 44 | rg_keyword: 6 45 | rg_address: 4 46 | rg_to_user: 0 47 | rg_coin: 8 48 | rg_fiat: 0 49 | bbb: 50 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_ADDRESS}(\s+){REGEX_COIN}{REGEX_AMOUNT}' 51 | rg_amount: 7 52 | rg_keyword: 0 53 | rg_address: 4 54 | rg_to_user: 0 55 | rg_coin: 6 56 | rg_fiat: 0 57 | bbbb: 58 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_ADDRESS}(\s+){REGEX_COIN}{REGEX_KEYWORD}' 59 | rg_amount: 0 60 | rg_keyword: 7 61 | rg_address: 4 62 | rg_to_user: 0 63 | rg_coin: 6 64 | rg_fiat: 0 65 | c: 66 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_USER}(\s+){REGEX_AMOUNT}(\s+){REGEX_COIN}' 67 | rg_amount: 6 68 | rg_keyword: 0 69 | rg_address: 0 70 | rg_to_user: 4 71 | rg_coin: 8 72 | rg_fiat: 0 73 | cc: 74 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_USER}(\s+){REGEX_KEYWORD}(\s+){REGEX_COIN}' 75 | rg_amount: 0 76 | rg_keyword: 6 77 | rg_address: 0 78 | rg_to_user: 4 79 | rg_coin: 8 80 | rg_fiat: 0 81 | ccc: 82 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_USER}(\s+){REGEX_COIN}{REGEX_AMOUNT}' 83 | rg_amount: 7 84 | rg_keyword: 0 85 | rg_address: 0 86 | rg_to_user: 4 87 | rg_coin: 6 88 | rg_fiat: 0 89 | cccc: 90 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_USER}(\s+){REGEX_COIN}{REGEX_KEYWORD}' 91 | rg_amount: 0 92 | rg_keyword: 7 93 | rg_address: 0 94 | rg_to_user: 4 95 | rg_coin: 6 96 | rg_fiat: 0 97 | d: 98 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_FIAT}{REGEX_AMOUNT}(\s+){REGEX_COIN}' 99 | rg_amount: 5 100 | rg_keyword: 0 101 | rg_address: 0 102 | rg_to_user: 0 103 | rg_coin: 7 104 | rg_fiat: 4 105 | dd: 106 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_FIAT}{REGEX_KEYWORD}(\s+){REGEX_COIN}' 107 | rg_amount: 0 108 | rg_keyword: 5 109 | rg_address: 0 110 | rg_to_user: 0 111 | rg_coin: 7 112 | rg_fiat: 4 113 | e: 114 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_ADDRESS}(\s+){REGEX_FIAT}{REGEX_AMOUNT}(\s+){REGEX_COIN}' 115 | rg_amount: 7 116 | rg_keyword: 0 117 | rg_address: 4 118 | rg_to_user: 0 119 | rg_coin: 9 120 | rg_fiat: 6 121 | ee: 122 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_ADDRESS}(\s+){REGEX_FIAT}{REGEX_KEYWORD}(\s+){REGEX_COIN}' 123 | rg_amount: 0 124 | rg_keyword: 7 125 | rg_address: 4 126 | rg_to_user: 0 127 | rg_coin: 9 128 | rg_fiat: 6 129 | f: 130 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_USER}(\s+){REGEX_FIAT}{REGEX_AMOUNT}(\s+){REGEX_COIN}' 131 | rg_amount: 7 132 | rg_keyword: 0 133 | rg_address: 0 134 | rg_to_user: 4 135 | rg_coin: 9 136 | rg_fiat: 6 137 | ff: 138 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_USER}(\s+){REGEX_FIAT}{REGEX_KEYWORD}(\s+){REGEX_COIN}' 139 | rg_amount: 0 140 | rg_keyword: 7 141 | rg_address: 0 142 | rg_to_user: 4 143 | rg_coin: 9 144 | rg_fiat: 6 145 | g: 146 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_COIN}{REGEX_AMOUNT}' 147 | rg_amount: 5 148 | rg_keyword: 0 149 | rg_address: 0 150 | rg_to_user: 0 151 | rg_coin: 4 152 | rg_fiat: 0 153 | gg: 154 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_COIN}{REGEX_KEYWORD}' 155 | rg_amount: 0 156 | rg_keyword: 5 157 | rg_address: 0 158 | rg_to_user: 0 159 | rg_coin: 4 160 | rg_fiat: 0 161 | ggg: 162 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_FIAT}{REGEX_AMOUNT}' 163 | rg_amount: 5 164 | rg_keyword: 0 165 | rg_address: 0 166 | rg_to_user: 0 167 | rg_coin: 0 168 | rg_fiat: 4 169 | gggg: 170 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_FIAT}{REGEX_KEYWORD}' 171 | rg_amount: 0 172 | rg_keyword: 5 173 | rg_address: 0 174 | rg_to_user: 0 175 | rg_coin: 0 176 | rg_fiat: 4 177 | ggggg: 178 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_KEYWORD}' 179 | rg_amount: 0 180 | rg_keyword: 4 181 | rg_address: 0 182 | rg_to_user: 0 183 | rg_coin: 0 184 | rg_fiat: 0 185 | h: 186 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_USER}(\s+){REGEX_FIAT}{REGEX_AMOUNT}' 187 | rg_amount: 7 188 | rg_keyword: 0 189 | rg_address: 0 190 | rg_to_user: 4 191 | rg_coin: 0 192 | rg_fiat: 6 193 | hh: 194 | value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_USER}(\s+){REGEX_FIAT}{REGEX_KEYWORD}' 195 | rg_amount: 0 196 | rg_keyword: 7 197 | rg_address: 0 198 | rg_to_user: 4 199 | rg_coin: 0 200 | rg_fiat: 6 201 | info: 202 | enabled: true 203 | public: false 204 | simple: true 205 | regex: '(\+)(info|balance)' 206 | register: 207 | enabled: true 208 | public: false 209 | simple: true 210 | regex: '(\+)(register)' 211 | accept: 212 | enabled: true 213 | public: false 214 | simple: true 215 | regex: '(\+)(accept)' 216 | decline: 217 | enabled: true 218 | public: false 219 | simple: true 220 | regex: '(\+)(decline)' 221 | withdraw: 222 | enabled: true 223 | public: false 224 | simple: false 225 | regex: 226 | a: 227 | value: '(\+)(withdraw)(\s+){REGEX_ADDRESS}(\s+){REGEX_AMOUNT}(\s+){REGEX_COIN}' 228 | rg_amount: 6 229 | rg_keyword: 0 230 | rg_address: 4 231 | rg_to_user: 0 232 | rg_coin: 8 233 | rg_fiat: 0 234 | aa: 235 | value: '(\+)(withdraw)(\s+){REGEX_ADDRESS}(\s+){REGEX_KEYWORD}(\s+){REGEX_COIN}' 236 | rg_amount: 0 237 | rg_keyword: 6 238 | rg_address: 4 239 | rg_to_user: 0 240 | rg_coin: 8 241 | rg_fiat: 0 242 | aaa: 243 | value: '(\+)(withdraw)(\s+){REGEX_ADDRESS}(\s+){REGEX_COIN}{REGEX_AMOUNT}' 244 | rg_amount: 7 245 | rg_keyword: 0 246 | rg_address: 4 247 | rg_to_user: 0 248 | rg_coin: 6 249 | rg_fiat: 0 250 | aaaa: 251 | value: '(\+)(withdraw)(\s+){REGEX_ADDRESS}(\s+){REGEX_COIN}{REGEX_KEYWORD}' 252 | rg_amount: 0 253 | rg_keyword: 7 254 | rg_address: 4 255 | rg_to_user: 0 256 | rg_coin: 6 257 | rg_fiat: 0 258 | b: 259 | value: '(\+)(withdraw)(\s+){REGEX_ADDRESS}(\s+){REGEX_FIAT}{REGEX_AMOUNT}(\s+){REGEX_COIN}' 260 | rg_amount: 7 261 | rg_keyword: 0 262 | rg_address: 4 263 | rg_to_user: 0 264 | rg_coin: 9 265 | rg_fiat: 6 266 | bb: 267 | value: '(\+)(withdraw)(\s+){REGEX_ADDRESS}(\s+){REGEX_FIAT}{REGEX_KEYWORD}(\s+){REGEX_COIN}' 268 | rg_amount: 0 269 | rg_keyword: 7 270 | rg_address: 4 271 | rg_to_user: 0 272 | rg_coin: 9 273 | rg_fiat: 6 274 | history: 275 | enabled: true 276 | public: false 277 | simple: true 278 | regex: '(\+)(history)' 279 | redeem: 280 | enabled: true 281 | public: false 282 | simple: false 283 | regex: 284 | a: 285 | value: '(\+)(redeem)(\s+){REGEX_COIN}' 286 | rg_amount: 0 287 | rg_keyword: 0 288 | rg_address: 0 289 | rg_to_user: 0 290 | rg_coin: 4 291 | rg_fiat: 0 292 | rates: 293 | enabled: true 294 | public: false 295 | simple: true 296 | regex: '(\+)(rates)' 297 | 298 | keywords: 299 | all: 300 | value: "float( self.u_from.get_balance(coin=self.coin, kind=('withdraw' if bool(self.addr_to) else 'givetip')) - (self.ctb.conf.coins[self.coin].txfee if bool(self.addr_to) else 0.0) ) if self.u_from.is_registered() and bool(self.coin) else None" 301 | for_fiat: false 302 | for_coin: true 303 | message: '^**(*・_・)ノ⌒*** {USER_FROM} ^gives {USER_TO} ^_everything_!.. ^all {AMOUNT} ^now ^belong ^to {USER_TO}' 304 | flip: 305 | value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,2)) )" 306 | for_fiat: true 307 | for_coin: true 308 | message: '{USER_FROM} ^flips ^the ^table ^**(╯°□°)╯︵┻━┻** ^and {USER_TO} ^gets {AMOUNT}' 309 | megaflip: 310 | value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,20)) )" 311 | for_fiat: true 312 | for_coin: true 313 | message: '{USER_FROM} ^flips ^10 ^tables ^**(╯°□°)╯︵┻━┻** ^and {USER_TO} ^gets {AMOUNT}' 314 | gigaflip: 315 | value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,200)) )" 316 | for_fiat: true 317 | for_coin: true 318 | message: '{USER_FROM} ^flips ^100 ^tables ^**(╯°□°)╯︵┻━┻** ^and {USER_TO} ^gets {AMOUNT}' 319 | roll: 320 | value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,6)) )" 321 | for_fiat: true 322 | for_coin: true 323 | message: '{USER_FROM} ^rolls ^a ^die, ^and {USER_TO} ^gets {AMOUNT}' 324 | megaroll: 325 | value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,60)) )" 326 | for_fiat: true 327 | for_coin: true 328 | message: '{USER_FROM} ^rolls ^a ^mega-die, ^and {USER_TO} ^gets {AMOUNT}' 329 | gigaroll: 330 | value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,600)) )" 331 | for_fiat: true 332 | for_coin: true 333 | message: '{USER_FROM} ^rolls ^a ^giga-die, ^and {USER_TO} ^gets {AMOUNT}' 334 | sushiroll: 335 | value: 3.0 336 | for_fiat: true 337 | for_coin: false 338 | message: '{USER_FROM} ^serves {USER_TO} ^a ^roll ^of ^yummy ^sushi ^**(っ˘ڡ˘ς)** ^- ^worth {AMOUNT}' 339 | random1: 340 | value: "float( randint(0,1000) ) / 1000.0" 341 | for_fiat: true 342 | for_coin: true 343 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 344 | random10: 345 | value: "float( randint(0,10000) ) / 1000.0" 346 | for_fiat: true 347 | for_coin: true 348 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 349 | random25: 350 | value: "float( randint(0,25000) ) / 1000.0" 351 | for_fiat: true 352 | for_coin: true 353 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 354 | random50: 355 | value: "float( randint(0,50000) ) / 1000.0" 356 | for_fiat: true 357 | for_coin: true 358 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 359 | random100: 360 | value: "float( randint(0,100000) ) / 1000.0" 361 | for_fiat: true 362 | for_coin: true 363 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 364 | random250: 365 | value: "float( randint(0,250000) ) / 1000.0" 366 | for_fiat: true 367 | for_coin: true 368 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 369 | random500: 370 | value: "float( randint(0,500000) ) / 1000.0" 371 | for_fiat: true 372 | for_coin: true 373 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 374 | random1000: 375 | value: "float( randint(0,1000000) ) / 1000.0" 376 | for_fiat: true 377 | for_coin: true 378 | message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}" 379 | upvote: 380 | value: 0.1 381 | for_fiat: true 382 | for_coin: true 383 | message: '{USER_FROM} ^gives {USER_TO} ^an ^upvote... _^with ^benefits_ ^**ヽ(*・ω・)ノ** ^- ^worth {AMOUNT}' 384 | pi: 385 | value: 3.14159265 386 | for_fiat: true 387 | for_coin: true 388 | e: 389 | value: 2.7182818 390 | for_fiat: true 391 | for_coin: true 392 | leet: 393 | value: 1.337 394 | for_fiat: true 395 | for_coin: true 396 | quarter: 397 | value: 0.25 398 | for_fiat: true 399 | for_coin: true 400 | half: 401 | value: 0.5 402 | for_fiat: true 403 | for_coin: true 404 | cookie: 405 | value: 0.33 406 | for_fiat: true 407 | for_coin: false 408 | message: '{USER_FROM} ^gives {USER_TO} ^a ^yummy ^cookie ^**♪┏(・o・)┛♪** ^- ^worth {AMOUNT}' 409 | nothing: 410 | value: "float (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01)" 411 | for_fiat: true 412 | for_coin: true 413 | message: '^**(″・ิ_・ิ)っ** {USER_FROM} ^gives ^almost ^nothing ^to {USER_TO} ^- {AMOUNT}' 414 | gum: 415 | value: 1.5 416 | for_fiat: true 417 | for_coin: false 418 | message: '{USER_FROM} ^gives ^a ^pack ^of ^gum ^to {USER_TO} ^- ^worth {AMOUNT}' 419 | beer: 420 | value: 3.5 421 | for_fiat: true 422 | for_coin: false 423 | message: '{USER_FROM} ^gives ^a ^beer ^to {USER_TO} ^- ^worth {AMOUNT}' 424 | nicebeer: 425 | value: 5.0 426 | for_fiat: true 427 | for_coin: false 428 | message: '{USER_FROM} ^gives ^a ^nice ^beer ^**(〃・ิ‿・ิ)ゞ** ^to {USER_TO} ^- ^worth {AMOUNT}) ^**ƪ(˘▽˘ƪ)**' 429 | hug: 430 | value: 0.5 431 | for_fiat: true 432 | for_coin: true 433 | message: '^**(っ⌒‿⌒)っ** {USER_FROM} ^gives ^a ^nice ^hug ^**ヽ(*⌒∇⌒*)ノ** ^to {USER_TO} ^along ^with {AMOUNT}' 434 | kiss: 435 | value: 1.0 436 | for_fiat: true 437 | for_coin: true 438 | message: '{USER_TO} ^gets ^a ^kiss ^**(˘⌣˘)ε˘`)** ^from {USER_FROM} ^along ^with {AMOUNT}' 439 | nyan: 440 | value: 1.111111 441 | for_fiat: true 442 | for_coin: true 443 | message: '^**[^._.^]ノ彡** {USER_FROM} ^nyan ^nyan ^にゃん {USER_TO} ^にゃん ^にゃん {AMOUNT} ^**\(^O^)/**' 444 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., [http://fsf.org/] 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /src/cointipbot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | This file is part of ALTcointip. 4 | 5 | ALTcointip is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | ALTcointip is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with ALTcointip. If not, see . 17 | """ 18 | 19 | from ctb import ctb_action, ctb_coin, ctb_db, ctb_exchange, ctb_log, ctb_misc, ctb_user 20 | 21 | import gettext, locale, logging, praw, smtplib, sys, time, traceback, yaml 22 | from email.mime.text import MIMEText 23 | from jinja2 import Environment, PackageLoader 24 | 25 | from requests.exceptions import HTTPError, ConnectionError, Timeout 26 | from praw.errors import ExceptionList, APIException, InvalidCaptcha, InvalidUser, RateLimitExceeded 27 | from socket import timeout 28 | 29 | # Configure CointipBot logger 30 | logging.basicConfig() 31 | lg = logging.getLogger('cointipbot') 32 | 33 | 34 | class CointipBot(object): 35 | """ 36 | Main class for cointip bot 37 | """ 38 | 39 | conf = None 40 | db = None 41 | reddit = None 42 | coins = {} 43 | exchanges = {} 44 | jenv = None 45 | runtime = {'ev': {}, 'regex': []} 46 | 47 | def init_logging(self): 48 | """ 49 | Initialize logging handlers 50 | """ 51 | 52 | handlers = {} 53 | levels = ['warning', 'info', 'debug'] 54 | lg = logging.getLogger('cointipbot') 55 | bt = logging.getLogger('bitcoin') 56 | 57 | # Get handlers 58 | handlers = {} 59 | for l in levels: 60 | if self.conf.logs.levels[l].enabled: 61 | handlers[l] = logging.FileHandler(self.conf.logs.levels[l].filename, mode='a' if self.conf.logs.levels[l].append else 'w') 62 | handlers[l].setFormatter(logging.Formatter(self.conf.logs.levels[l].format)) 63 | 64 | # Set handlers 65 | for l in levels: 66 | if handlers.has_key(l): 67 | level = logging.WARNING if l == 'warning' else (logging.INFO if l == 'info' else logging.DEBUG) 68 | handlers[l].addFilter(ctb_log.LevelFilter(level)) 69 | lg.addHandler(handlers[l]) 70 | bt.addHandler(handlers[l]) 71 | 72 | # Set default levels 73 | lg.setLevel(logging.DEBUG) 74 | bt.setLevel(logging.DEBUG) 75 | 76 | lg.info('CointipBot::init_logging(): -------------------- logging initialized --------------------') 77 | return True 78 | 79 | def parse_config(self): 80 | """ 81 | Returns a Python object with CointipBot configuration 82 | """ 83 | lg.debug('CointipBot::parse_config(): parsing config files...') 84 | 85 | conf = {} 86 | try: 87 | prefix='./conf/' 88 | for i in ['coins', 'db', 'exchanges', 'fiat', 'keywords', 'logs', 'misc', 'reddit', 'regex']: 89 | lg.debug("CointipBot::parse_config(): reading %s%s.yml", prefix, i) 90 | conf[i] = yaml.load(open(prefix+i+'.yml')) 91 | except yaml.YAMLError as e: 92 | lg.error("CointipBot::parse_config(): error reading config file: %s", e) 93 | if hasattr(e, 'problem_mark'): 94 | lg.error("CointipBot::parse_config(): error position: (line %s, column %s)", e.problem_mark.line+1, e.problem_mark.column+1) 95 | sys.exit(1) 96 | 97 | lg.info('CointipBot::parse_config(): config files has been parsed') 98 | return ctb_misc.DotDict(conf) 99 | 100 | def connect_db(self): 101 | """ 102 | Returns a database connection object 103 | """ 104 | lg.debug('CointipBot::connect_db(): connecting to database...') 105 | 106 | dsn = "mysql+mysqldb://%s:%s@%s:%s/%s?charset=utf8" % (self.conf.db.auth.user, self.conf.db.auth.password, self.conf.db.auth.host, self.conf.db.auth.port, self.conf.db.auth.dbname) 107 | dbobj = ctb_db.CointipBotDatabase(dsn) 108 | 109 | try: 110 | conn = dbobj.connect() 111 | except Exception as e: 112 | lg.error("CointipBot::connect_db(): error connecting to database: %s", e) 113 | sys.exit(1) 114 | 115 | lg.info("CointipBot::connect_db(): connected to database %s as %s", self.conf.db.auth.dbname, self.conf.db.auth.user) 116 | return conn 117 | 118 | def connect_reddit(self): 119 | """ 120 | Returns a praw connection object 121 | """ 122 | lg.debug('CointipBot::connect_reddit(): connecting to Reddit...') 123 | 124 | conn = praw.Reddit(user_agent = self.conf.reddit.auth.user) 125 | conn.login(self.conf.reddit.auth.user, self.conf.reddit.auth.password) 126 | 127 | lg.info("CointipBot::connect_reddit(): logged in to Reddit as %s", self.conf.reddit.auth.user) 128 | return conn 129 | 130 | def self_checks(self): 131 | """ 132 | Run self-checks before starting the bot 133 | """ 134 | 135 | # Ensure bot is a registered user 136 | b = ctb_user.CtbUser(name=self.conf.reddit.auth.user.lower(), ctb=self) 137 | if not b.is_registered(): 138 | b.register() 139 | 140 | # Ensure (total pending tips) < (CointipBot's balance) 141 | for c in self.coins: 142 | ctb_balance = b.get_balance(coin=c, kind='givetip') 143 | pending_tips = float(0) 144 | actions = ctb_action.get_actions(atype='givetip', state='pending', coin=c, ctb=self) 145 | for a in actions: 146 | pending_tips += a.coinval 147 | if (ctb_balance - pending_tips) < -0.000001: 148 | raise Exception("CointipBot::self_checks(): CointipBot's %s balance (%s) < total pending tips (%s)" % (c.upper(), ctb_balance, pending_tips)) 149 | 150 | # Ensure coin balances are positive 151 | for c in self.coins: 152 | b = float(self.coins[c].conn.getbalance()) 153 | if b < 0: 154 | raise Exception("CointipBot::self_checks(): negative balance of %s: %s" % (c, b)) 155 | 156 | # Ensure user accounts are intact and balances are not negative 157 | sql = "SELECT username FROM t_users ORDER BY username" 158 | for mysqlrow in self.db.execute(sql): 159 | u = ctb_user.CtbUser(name=mysqlrow['username'], ctb=self) 160 | if not u.is_registered(): 161 | raise Exception("CointipBot::self_checks(): user %s is_registered() failed" % mysqlrow['username']) 162 | # for c in vars(self.coins): 163 | # if u.get_balance(coin=c, kind='givetip') < 0: 164 | # raise Exception("CointipBot::self_checks(): user %s %s balance is negative" % (mysqlrow['username'], c)) 165 | 166 | return True 167 | 168 | def expire_pending_tips(self): 169 | """ 170 | Decline any pending tips that have reached expiration time limit 171 | """ 172 | 173 | # Calculate timestamp 174 | seconds = int(self.conf.misc.times.expire_pending_hours * 3600) 175 | created_before = time.mktime(time.gmtime()) - seconds 176 | counter = 0 177 | 178 | # Get expired actions and decline them 179 | for a in ctb_action.get_actions(atype='givetip', state='pending', created_utc='< ' + str(created_before), ctb=self): 180 | a.expire() 181 | counter += 1 182 | 183 | # Done 184 | return (counter > 0) 185 | 186 | def check_inbox(self): 187 | """ 188 | Evaluate new messages in inbox 189 | """ 190 | lg.debug('> CointipBot::check_inbox()') 191 | 192 | try: 193 | 194 | # Try to fetch some messages 195 | messages = list(ctb_misc.praw_call(self.reddit.get_unread, limit=self.conf.reddit.scan.batch_limit)) 196 | messages.reverse() 197 | 198 | # Process messages 199 | for m in messages: 200 | # Sometimes messages don't have an author (such as 'you are banned from' message) 201 | if not m.author: 202 | lg.info("CointipBot::check_inbox(): ignoring msg with no author") 203 | ctb_misc.praw_call(m.mark_as_read) 204 | continue 205 | 206 | lg.info("CointipBot::check_inbox(): %s from %s", "comment" if m.was_comment else "message", m.author.name) 207 | 208 | # Ignore duplicate messages (sometimes Reddit fails to mark messages as read) 209 | if ctb_action.check_action(msg_id=m.id, ctb=self): 210 | lg.warning("CointipBot::check_inbox(): duplicate action detected (msg.id %s), ignoring", m.id) 211 | ctb_misc.praw_call(m.mark_as_read) 212 | continue 213 | 214 | # Ignore self messages 215 | if m.author and m.author.name.lower() == self.conf.reddit.auth.user.lower(): 216 | lg.debug("CointipBot::check_inbox(): ignoring message from self") 217 | ctb_misc.praw_call(m.mark_as_read) 218 | continue 219 | 220 | # Ignore messages from banned users 221 | if m.author and self.conf.reddit.banned_users: 222 | lg.debug("CointipBot::check_inbox(): checking whether user '%s' is banned..." % m.author) 223 | u = ctb_user.CtbUser(name = m.author.name, redditobj = m.author, ctb = self) 224 | if u.banned: 225 | lg.info("CointipBot::check_inbox(): ignoring banned user '%s'" % m.author) 226 | ctb_misc.praw_call(m.mark_as_read) 227 | continue 228 | 229 | action = None 230 | if m.was_comment: 231 | # Attempt to evaluate as comment / mention 232 | action = ctb_action.eval_comment(m, self) 233 | else: 234 | # Attempt to evaluate as inbox message 235 | action = ctb_action.eval_message(m, self) 236 | 237 | # Perform action, if found 238 | if action: 239 | lg.info("CointipBot::check_inbox(): %s from %s (m.id %s)", action.type, action.u_from.name, m.id) 240 | lg.debug("CointipBot::check_inbox(): message body: <%s>", m.body) 241 | action.do() 242 | else: 243 | lg.info("CointipBot::check_inbox(): no match") 244 | if self.conf.reddit.messages.sorry and not m.subject in ['post reply', 'comment reply']: 245 | user = ctb_user.CtbUser(name=m.author.name, redditobj=m.author, ctb=self) 246 | tpl = self.jenv.get_template('didnt-understand.tpl') 247 | msg = tpl.render(user_from=user.name, what='comment' if m.was_comment else 'message', source_link=m.permalink if hasattr(m, 'permalink') else None, ctb=self) 248 | lg.debug("CointipBot::check_inbox(): %s", msg) 249 | user.tell(subj='What?', msg=msg, msgobj=m if not m.was_comment else None) 250 | 251 | # Mark message as read 252 | ctb_misc.praw_call(m.mark_as_read) 253 | 254 | except (HTTPError, ConnectionError, Timeout, RateLimitExceeded, timeout) as e: 255 | lg.warning("CointipBot::check_inbox(): Reddit is down (%s), sleeping", e) 256 | time.sleep(self.conf.misc.times.sleep_seconds) 257 | pass 258 | except Exception as e: 259 | lg.error("CointipBot::check_inbox(): %s", e) 260 | raise 261 | 262 | lg.debug("< CointipBot::check_inbox() DONE") 263 | return True 264 | 265 | def init_subreddits(self): 266 | """ 267 | Determine a list of subreddits and create a PRAW object 268 | """ 269 | lg.debug("> CointipBot::init_subreddits()") 270 | 271 | try: 272 | 273 | if not hasattr(self.conf.reddit, 'subreddits'): 274 | my_reddits_list = None 275 | my_reddits_string = None 276 | 277 | if hasattr(self.conf.reddit.scan, 'these_subreddits'): 278 | # Subreddits are specified in conf.yml 279 | my_reddits_list = list(self.conf.reddit.scan.these_subreddits) 280 | 281 | elif self.conf.reddit.scan.my_subreddits: 282 | # Subreddits are subscribed to by bot user 283 | my_reddits = ctb_misc.praw_call(self.reddit.get_my_subreddits, limit=None) 284 | my_reddits_list = [] 285 | for my_reddit in my_reddits: 286 | my_reddits_list.append(my_reddit.display_name.lower()) 287 | my_reddits_list.sort() 288 | 289 | else: 290 | # No subreddits configured 291 | lg.debug("< CointipBot::check_subreddits() DONE (no subreddits configured to scan)") 292 | return False 293 | 294 | # Build subreddits string 295 | my_reddits_string = "+".join(my_reddits_list) 296 | 297 | # Get multi-reddit PRAW object 298 | lg.debug("CointipBot::check_subreddits(): multi-reddit string: %s", my_reddits_string) 299 | self.conf.reddit.subreddits = ctb_misc.praw_call(self.reddit.get_subreddit, my_reddits_string) 300 | 301 | except Exception as e: 302 | lg.error("CointipBot::check_subreddits(): coudln't get subreddits: %s", e) 303 | raise 304 | 305 | lg.debug("< CointipBot::init_subreddits() DONE") 306 | return True 307 | 308 | def check_subreddits(self): 309 | """ 310 | Evaluate new comments from configured subreddits 311 | """ 312 | lg.debug("> CointipBot::check_subreddits()") 313 | 314 | try: 315 | # Process comments until old comment reached 316 | 317 | # Get last_processed_comment_time if necessary 318 | if not hasattr(self.conf.reddit, 'last_processed_comment_time') or self.conf.reddit.last_processed_comment_time <= 0: 319 | self.conf.reddit.last_processed_comment_time = ctb_misc.get_value(conn=self.db, param0='last_processed_comment_time') 320 | updated_last_processed_time = 0 321 | 322 | # Fetch comments from subreddits 323 | my_comments = ctb_misc.praw_call(self.conf.reddit.subreddits.get_comments, limit=self.conf.reddit.scan.batch_limit) 324 | 325 | # Match each comment against regex 326 | counter = 0 327 | for c in my_comments: 328 | # Stop processing if old comment reached 329 | #lg.debug("check_subreddits(): c.id %s from %s, %s <= %s", c.id, c.subreddit.display_name, c.created_utc, self.conf.reddit.last_processed_comment_time) 330 | if c.created_utc <= self.conf.reddit.last_processed_comment_time: 331 | lg.debug("CointipBot::check_subreddits(): old comment reached") 332 | break 333 | counter += 1 334 | if c.created_utc > updated_last_processed_time: 335 | updated_last_processed_time = c.created_utc 336 | 337 | # Ignore duplicate comments (may happen when bot is restarted) 338 | if ctb_action.check_action(msg_id=c.id, ctb=self): 339 | lg.warning("CointipBot::check_inbox(): duplicate action detected (comment.id %s), ignoring", c.id) 340 | continue 341 | 342 | # Ignore comments from banned users 343 | if c.author and self.conf.reddit.banned_users: 344 | lg.debug("CointipBot::check_subreddits(): checking whether user '%s' is banned..." % c.author) 345 | u = ctb_user.CtbUser(name = c.author.name, redditobj = c.author, ctb = self) 346 | if u.banned: 347 | lg.info("CointipBot::check_subreddits(): ignoring banned user '%s'" % c.author) 348 | continue 349 | 350 | # Attempt to evaluate comment 351 | action = ctb_action.eval_comment(c, self) 352 | 353 | # Perform action, if found 354 | if action: 355 | lg.info("CointipBot::check_subreddits(): %s from %s (%s)", action.type, action.u_from.name, c.id) 356 | lg.debug("CointipBot::check_subreddits(): comment body: <%s>", c.body) 357 | action.do() 358 | else: 359 | lg.info("CointipBot::check_subreddits(): no match") 360 | 361 | lg.debug("CointipBot::check_subreddits(): %s comments processed", counter) 362 | if counter >= self.conf.reddit.scan.batch_limit - 1: 363 | lg.warning("CointipBot::check_subreddits(): conf.reddit.scan.batch_limit (%s) was not large enough to process all comments", self.conf.reddit.scan.batch_limit) 364 | 365 | except (HTTPError, RateLimitExceeded, timeout) as e: 366 | lg.warning("CointipBot::check_subreddits(): Reddit is down (%s), sleeping", e) 367 | time.sleep(self.conf.misc.times.sleep_seconds) 368 | pass 369 | except Exception as e: 370 | lg.error("CointipBot::check_subreddits(): coudln't fetch comments: %s", e) 371 | raise 372 | 373 | # Save updated last_processed_time value 374 | if updated_last_processed_time > 0: 375 | self.conf.reddit.last_processed_comment_time = updated_last_processed_time 376 | ctb_misc.set_value(conn=self.db, param0='last_processed_comment_time', value0=self.conf.reddit.last_processed_comment_time) 377 | 378 | lg.debug("< CointipBot::check_subreddits() DONE") 379 | return True 380 | 381 | def refresh_ev(self): 382 | """ 383 | Refresh coin/fiat exchange values using self.exchanges 384 | """ 385 | 386 | # Return if rate has been checked in the past hour 387 | seconds = int(1 * 3600) 388 | if hasattr(self.conf.exchanges, 'last_refresh') and self.conf.exchanges.last_refresh + seconds > int(time.mktime(time.gmtime())): 389 | lg.debug("< CointipBot::refresh_ev(): DONE (skipping)") 390 | return 391 | 392 | # For each enabled coin... 393 | for c in vars(self.conf.coins): 394 | if self.conf.coins[c].enabled: 395 | 396 | # Get BTC/coin exchange rate 397 | values = [] 398 | result = 0.0 399 | 400 | if not self.conf.coins[c].unit == 'btc': 401 | # For each exchange that supports this coin... 402 | for e in self.exchanges: 403 | if self.exchanges[e].supports_pair(_name1=self.conf.coins[c].unit, _name2='btc'): 404 | # Get ticker value from exchange 405 | value = self.exchanges[e].get_ticker_value(_name1=self.conf.coins[c].unit, _name2='btc') 406 | if value and float(value) > 0.0: 407 | values.append(float(value)) 408 | 409 | # Result is average of all responses 410 | if len(values) > 0: 411 | result = sum(values) / float(len(values)) 412 | 413 | else: 414 | # BTC/BTC rate is always 1 415 | result = 1.0 416 | 417 | # Assign result to self.runtime['ev'] 418 | if not self.runtime['ev'].has_key(c): 419 | self.runtime['ev'][c] = {} 420 | self.runtime['ev'][c]['btc'] = result 421 | 422 | # For each enabled fiat... 423 | for f in vars(self.conf.fiat): 424 | if self.conf.fiat[f].enabled: 425 | 426 | # Get fiat/BTC exchange rate 427 | values = [] 428 | result = 0.0 429 | 430 | # For each exchange that supports this fiat... 431 | for e in self.exchanges: 432 | if self.exchanges[e].supports_pair(_name1='btc', _name2=self.conf.fiat[f].unit): 433 | # Get ticker value from exchange 434 | value = self.exchanges[e].get_ticker_value(_name1='btc', _name2=self.conf.fiat[f].unit) 435 | if value and float(value) > 0.0: 436 | values.append(float(value)) 437 | 438 | # Result is average of all responses 439 | if len(values) > 0: 440 | result = sum(values) / float(len(values)) 441 | 442 | # Assign result to self.runtime['ev'] 443 | if not self.runtime['ev'].has_key('btc'): 444 | self.runtime['ev']['btc'] = {} 445 | self.runtime['ev']['btc'][f] = result 446 | 447 | lg.debug("CointipBot::refresh_ev(): %s", self.runtime['ev']) 448 | 449 | # Update last_refresh 450 | self.conf.exchanges.last_refresh = int(time.mktime(time.gmtime())) 451 | 452 | def coin_value(self, _coin, _fiat): 453 | """ 454 | Quick method to return _fiat value of _coin 455 | """ 456 | try: 457 | value = self.runtime['ev'][_coin]['btc'] * self.runtime['ev']['btc'][_fiat] 458 | except KeyError as e: 459 | lg.warning("CointipBot::coin_value(%s, %s): KeyError", _coin, _fiat) 460 | value = 0.0 461 | return value 462 | 463 | def notify(self, _msg=None): 464 | """ 465 | Send _msg to configured destination 466 | """ 467 | 468 | # Construct MIME message 469 | msg = MIMEText(_msg) 470 | msg['Subject'] = self.conf.misc.notify.subject 471 | msg['From'] = self.conf.misc.notify.addr_from 472 | msg['To'] = self.conf.misc.notify.addr_to 473 | 474 | # Send MIME message 475 | server = smtplib.SMTP(self.conf.misc.notify.smtp_host) 476 | if self.conf.misc.notify.smtp_tls: 477 | server.starttls() 478 | server.login(self.conf.misc.notify.smtp_username, self.conf.misc.notify.smtp_password) 479 | server.sendmail(self.conf.misc.notify.addr_from, self.conf.misc.notify.addr_to, msg.as_string()) 480 | server.quit() 481 | 482 | def __init__(self, self_checks=True, init_reddit=True, init_coins=True, init_exchanges=True, init_db=True, init_logging=True): 483 | """ 484 | Constructor. Parses configuration file and initializes bot. 485 | """ 486 | lg.info("CointipBot::__init__()...") 487 | 488 | # Configuration 489 | self.conf = self.parse_config() 490 | 491 | # Logging 492 | if init_logging: 493 | self.init_logging() 494 | 495 | # Templating with jinja2 496 | self.jenv = Environment(trim_blocks=True, loader=PackageLoader('cointipbot', 'tpl/jinja2')) 497 | 498 | # Database 499 | if init_db: 500 | self.db = self.connect_db() 501 | 502 | # Coins 503 | if init_coins: 504 | for c in vars(self.conf.coins): 505 | if self.conf.coins[c].enabled: 506 | self.coins[c] = ctb_coin.CtbCoin(_conf=self.conf.coins[c]) 507 | if not len(self.coins) > 0: 508 | lg.error("CointipBot::__init__(): Error: please enable at least one type of coin") 509 | sys.exit(1) 510 | 511 | # Exchanges 512 | if init_exchanges: 513 | for e in vars(self.conf.exchanges): 514 | if self.conf.exchanges[e].enabled: 515 | self.exchanges[e] = ctb_exchange.CtbExchange(_conf=self.conf.exchanges[e]) 516 | if not len(self.exchanges) > 0: 517 | lg.warning("Cointipbot::__init__(): Warning: no exchanges are enabled") 518 | 519 | # Reddit 520 | if init_reddit: 521 | self.reddit = self.connect_reddit() 522 | self.init_subreddits() 523 | # Regex for Reddit messages 524 | ctb_action.init_regex(self) 525 | 526 | # Self-checks 527 | if self_checks: 528 | self.self_checks() 529 | 530 | lg.info("< CointipBot::__init__(): DONE, batch-limit = %s, sleep-seconds = %s", self.conf.reddit.scan.batch_limit, self.conf.misc.times.sleep_seconds) 531 | 532 | def __str__(self): 533 | """ 534 | Return string representation of self 535 | """ 536 | me = "