├── 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 = "