First commit

This commit is contained in:
jwinterm
2017-11-10 08:00:49 -08:00
commit b83b1ed61c
59 changed files with 5542 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
*.py[cod]
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# cointipbot
*.log*
pifkoin
conf
+339
View File
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc., [http://fsf.org/]
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{description}
Copyright (C) {year} {fullname}
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
{signature of Ty Coon}, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
+128
View File
@@ -0,0 +1,128 @@
# Reddit ALTcointip bot
## Introduction
For introduction to and use of ALTcointip bot, see __http://www.reddit.com/r/ALTcointip/wiki/index__
## Getting Started
>**NEW: See [altcointip-chef](https://github.com/vindimy/altcointip-chef) for Chef-automated installation and configuration of ALTcointip bot.**
### Python Dependencies
The following Python libraries are necessary to run ALTcointip bot:
* __jinja2__ (http://jinja.pocoo.org/)
* __pifkoin__ (https://github.com/dpifke/pifkoin)
* __praw__ (https://github.com/praw-dev/praw)
* __sqlalchemy__ (http://www.sqlalchemy.org/)
* __yaml__ (http://pyyaml.org/wiki/PyYAML)
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`.
### Database
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`.
### Coin Daemons
Download one or more coin daemon executable. Create a configuration file for it in appropriate directory (such as `~/.mycoin/mycoin.conf` for Litecoin), 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 `mycoind getinfo` and `mycoind listaccounts`).
### Reddit Account
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.
### Configuration
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.
Most configuration options are described inline in provided sample configuration files.
### Running the Bot
1. Ensure MySQL is running and accepting connections given configured username/password
1. Ensure each configured coin daemon is running and responding to commands
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._
1. Execute `_start.sh` from [src](src/) directory. The command will not return for as long as the bot is running.
Here's the first few lines of DEBUG-level console output during successful initialization.
user@host:/opt/altcointip/altcointip/src$ ./_start.sh
INFO:cointipbot:CointipBot::init_logging(): -------------------- logging initialized --------------------
DEBUG:cointipbot:CointipBot::connect_db(): connecting to database...
INFO:cointipbot:CointipBot::connect_db(): connected to database altcointip as altcointip
DEBUG:cointipbot:CtbCoin::__init__(): connecting to Peercoin...
DEBUG:bitcoin:Read 5 parameters from /opt/altcointip/coins/ppcoin/ppcoin.conf
DEBUG:bitcoin:Making HTTP connection to 127.0.0.1:19902
INFO:cointipbot:CtbCoin::__init__():: connected to Peercoin
INFO:cointipbot:Setting tx fee of 0.010000
DEBUG:bitcoin:Starting "settxfee" JSON-RPC request
DEBUG:bitcoin:Got 36 byte response from server in 4 ms
DEBUG:cointipbot:CtbCoin::__init__(): connecting to Primecoin...
DEBUG:bitcoin:Read 5 parameters from /opt/altcointip/coins/primecoin/primecoin.conf
DEBUG:bitcoin:Making HTTP connection to 127.0.0.1:18772
INFO:cointipbot:CtbCoin::__init__():: connected to Primecoin
INFO:cointipbot:Setting tx fee of 0.010000
DEBUG:bitcoin:Starting "settxfee" JSON-RPC request
DEBUG:bitcoin:Got 36 byte response from server in 1 ms
DEBUG:cointipbot:CtbCoin::__init__(): connecting to Megacoin...
DEBUG:bitcoin:Read 5 parameters from /opt/altcointip/coins/megacoin/megacoin.conf
DEBUG:bitcoin:Making HTTP connection to 127.0.0.1:17950
INFO:cointipbot:CtbCoin::__init__():: connected to Megacoin
INFO:cointipbot:Setting tx fee of 0.010000
DEBUG:bitcoin:Starting "settxfee" JSON-RPC request
DEBUG:bitcoin:Got 36 byte response from server in 1 ms
DEBUG:cointipbot:CtbCoin::__init__(): connecting to Litecoin...
DEBUG:bitcoin:Read 5 parameters from /opt/altcointip/coins/litecoin/litecoin.conf
DEBUG:bitcoin:Making HTTP connection to 127.0.0.1:19332
INFO:cointipbot:CtbCoin::__init__():: connected to Litecoin
INFO:cointipbot:Setting tx fee of 0.020000
DEBUG:bitcoin:Starting "settxfee" JSON-RPC request
DEBUG:bitcoin:Got 36 byte response from server in 2 ms
DEBUG:cointipbot:CtbCoin::__init__(): connecting to Namecoin...
DEBUG:bitcoin:Read 5 parameters from /opt/altcointip/coins/namecoin/namecoin.conf
DEBUG:bitcoin:Making HTTP connection to 127.0.0.1:18336
INFO:cointipbot:CtbCoin::__init__():: connected to Namecoin
INFO:cointipbot:Setting tx fee of 0.010000
DEBUG:bitcoin:Starting "settxfee" JSON-RPC request
DEBUG:bitcoin:Got 36 byte response from server in 1 ms
DEBUG:cointipbot:CtbCoin::__init__(): connecting to Bitcoin...
DEBUG:bitcoin:Read 5 parameters from /opt/altcointip/coins/bitcoin/bitcoin.conf
DEBUG:bitcoin:Making HTTP connection to 127.0.0.1:18332
INFO:cointipbot:CtbCoin::__init__():: connected to Bitcoin
INFO:cointipbot:Setting tx fee of 0.000100
DEBUG:bitcoin:Starting "settxfee" JSON-RPC request
DEBUG:bitcoin:Got 36 byte response from server in 1 ms
DEBUG:cointipbot:CtbExchange::__init__(): initialized exchange crypto-trade.com
DEBUG:cointipbot:CtbExchange::__init__(): initialized exchange www.bitstamp.net
DEBUG:cointipbot:CtbExchange::__init__(): initialized exchange bter.com
DEBUG:cointipbot:CtbExchange::__init__(): initialized exchange blockchain.info
DEBUG:cointipbot:CtbExchange::__init__(): initialized exchange campbx.com
DEBUG:cointipbot:CtbExchange::__init__(): initialized exchange vircurex.com
DEBUG:cointipbot:CtbExchange::__init__(): initialized exchange pubapi.cryptsy.com
DEBUG:cointipbot:CtbExchange::__init__(): initialized exchange btc-e.com
DEBUG:cointipbot:CointipBot::connect_reddit(): connecting to Reddit...
INFO:cointipbot:CointipBot::connect_reddit(): logged in to Reddit as ALTcointip
...
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.
### Cron: Backups
Backups are very important! The last thing you want is losing user wallets or record of transactions in the databse.
There are three simple backup scripts included that support backing up 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:
0 8,20 * * * cd /opt/altcointip/altcointip/src && python _backup_db.py ~/backups
0 9,21 * * * cd /opt/altcointip/altcointip/src && python _backup_wallets.py ~/backups
0 10 * * * cd /opt/altcointip/altcointip/src && python _backup_config.py ~/backups
### Cron: Statistics
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:
0 */3 * * * cd /opt/altcointip/altcointip/src && python _update_stats.py
### What If I Want To Enable More Cryptocoins Later?
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.
+42
View File
@@ -0,0 +1,42 @@
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";
CREATE TABLE IF NOT EXISTS `t_action` (
`type` enum('givetip','withdraw','info','register','accept','decline','history','redeem','rates') NOT NULL,
`state` enum('completed','pending','failed','declined','expired') NOT NULL,
`created_utc` int(11) unsigned NOT NULL,
`from_user` varchar(30) NOT NULL,
`to_user` varchar(30) DEFAULT NULL,
`to_addr` varchar(34) DEFAULT NULL,
`coin_val` float unsigned DEFAULT NULL,
`fiat_val` float unsigned DEFAULT NULL,
`txid` varchar(64) DEFAULT NULL,
`coin` varchar(3) DEFAULT NULL,
`fiat` varchar(3) DEFAULT NULL,
`subreddit` varchar(30) DEFAULT NULL,
`msg_id` varchar(10) NOT NULL,
`msg_link` varchar(200) DEFAULT NULL,
PRIMARY KEY (`type`,`created_utc`,`msg_id`),
UNIQUE KEY `msg_id` (`msg_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `t_addrs` (
`username` varchar(30) NOT NULL,
`coin` varchar(3) NOT NULL,
`address` varchar(34) NOT NULL,
PRIMARY KEY (`username`,`coin`),
UNIQUE KEY `address` (`address`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `t_users` (
`username` varchar(30) NOT NULL,
`joindate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`giftamount` float DEFAULT '0',
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `t_values` (
`param0` varchar(64) NOT NULL,
`value0` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`param0`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
View File
+25
View File
@@ -0,0 +1,25 @@
# Here's how to add a new coin type to CointipBot
# * Make sure CointipBot instance is NOT running
# * Install and run coin daemon, make sure it's synced with network
# * Configure and nable new coin in config.yml
# * Then run this script, specifying coin (such as "python _add_coin.py btc")
# * After this script has finished, you can reusme the tip bot normally
import cointipbot, logging, sys
from ctb import ctb_coin, ctb_misc
if not len(sys.argv) == 2:
print "Usage: %s COIN" % sys.argv[0]
print "(COIN refers to ctb.conf[COIN], a hash location in coins.yml)"
sys.exit(1)
coin = sys.argv[1]
logging.basicConfig()
lg = logging.getLogger('cointipbot')
lg.setLevel(logging.DEBUG)
ctb = cointipbot.CointipBot(self_checks=False, init_reddit=False, init_coins=False, init_exchanges=False, init_db=True, init_logging=True)
ctb.coins[coin] = ctb_coin.CtbCoin(_conf=ctb.conf.coins[coin])
ctb_misc.add_coin(coin, ctb.db, ctb.coins)
+37
View File
@@ -0,0 +1,37 @@
# Simple script to back up ALTcointip conf/ dir
import sys, os, datetime
from distutils.spawn import find_executable
import cointipbot
if not len(sys.argv) in [2, 3] or not os.access(sys.argv[1], os.W_OK):
print "Usage: %s DIRECTORY [RSYNC-TO]" % sys.argv[0]
print "(DIRECTORY must be writeable, RSYNC-TO is optional location to RSYNC the file to)"
sys.exit(1)
ctb = cointipbot.CointipBot(self_checks=False, init_reddit=False, init_coins=False, init_db=False, init_logging=False)
if not find_executable('zip'):
print "zip executable not found, please install zip"
sys.exit(1)
if hasattr(ctb.conf.misc.backup, 'encryptionpassphrase') and not find_executable('gpg'):
print "encryptionpassphrase is specified but gpg executable not found, please install gpg"
sys.exit(1)
filename = "%s/conf_%s.zip" % (sys.argv[1], datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
print "Backing up to %s..." % filename
os.popen("zip -r %s conf/" % filename)
try:
print "Encrypting..."
os.popen("gpg --batch --passphrase '%s' -c %s" % (ctb.conf.misc.backup.encryptionpassphrase, filename))
os.popen("rm -f %s" % filename)
filename += '.gpg'
except AttributeError:
print "Not encrypting"
if len(sys.argv) == 3:
print "Calling rsync..."
os.popen("rsync -urltv %s %s" % (filename, sys.argv[2]))
+37
View File
@@ -0,0 +1,37 @@
# Simple script to back up ALTcointip database
import sys, os, datetime
from distutils.spawn import find_executable
import cointipbot
if not len(sys.argv) in [2, 3] or not os.access(sys.argv[1], os.W_OK):
print "Usage: %s DIRECTORY [RSYNC-TO]" % sys.argv[0]
print "(DIRECTORY must be writeable, RSYNC-TO is optional location to RSYNC the file to)"
sys.exit(1)
ctb = cointipbot.CointipBot(self_checks=False, init_reddit=False, init_coins=False, init_db=True, init_logging=False)
if not find_executable('gzip'):
print "gzip executable not found, please install gzip"
sys.exit(1)
if hasattr(ctb.conf.misc.backup, 'encryptionpassphrase') and not find_executable('gpg'):
print "encryptionpassphrase is specified but gpg executable not found, please install gpg"
sys.exit(1)
filename = "%s/%s_%s.sql.gz" % (sys.argv[1], ctb.conf.db.auth.dbname, datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
print "Backing up to %s..." % filename
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))
try:
print "Encrypting..."
os.popen("gpg --batch --passphrase '%s' -c %s" % (ctb.conf.misc.backup.encryptionpassphrase, filename))
os.popen("rm -f %s" % filename)
filename += '.gpg'
except AttributeError:
print "Not encrypting"
if len(sys.argv) == 3:
print "Calling rsync..."
os.popen("rsync -urltv %s %s" % (filename, sys.argv[2]))
+46
View File
@@ -0,0 +1,46 @@
# Simple script to back up active coin wallets
import sys, os, datetime, logging
from distutils.spawn import find_executable
import cointipbot
logging.basicConfig()
lg = logging.getLogger('cointipbot')
lg.setLevel(logging.DEBUG)
if not len(sys.argv) in [2, 3] or not os.access(sys.argv[1], os.W_OK):
print "Usage: %s DIRECTORY [RSYNC-TO]" % sys.argv[0]
print "(DIRECTORY must be writeable, RSYNC-TO is optional location to RSYNC the file to)"
sys.exit(1)
ctb = cointipbot.CointipBot(self_checks=False, init_reddit=False, init_coins=True, init_db=False, init_logging=False)
if not find_executable('gzip'):
print "gzip executable not found, please install gzip"
sys.exit(1)
if hasattr(ctb.conf.misc.backup, 'encryptionpassphrase') and not find_executable('gpg'):
print "encryptionpassphrase is specified but gpg executable not found, please install gpg"
sys.exit(1)
for c in ctb.coins:
filename = "%s/wallet_%s_%s.dat" % (sys.argv[1], ctb.conf.coins[c].unit, datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
print "Backing up %s wallet to %s..." % (ctb.conf.coins[c].name, filename)
ctb.coins[c].conn.backupwallet(filename)
print "Compressing..."
os.popen("gzip --best %s" % filename)
filename += '.gz'
try:
print "Encrypting..."
os.popen("gpg --batch --passphrase '%s' -c %s" % (ctb.conf.misc.backup.encryptionpassphrase, filename))
os.popen("rm -f %s" % filename)
filename += '.gpg'
except AttributeError:
print "Not encrypting"
if len(sys.argv) == 3:
print "Calling rsync..."
os.popen("rsync -urltv %s %s" % (filename, sys.argv[2]))
Executable
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
user=$1
if [ ! -f ./cointipbot.py ] ; then
# Output help message and exit
echo "Usage: $0 [username]"
echo "if [username] is specified, script will be started as user [username]"
echo "$0 must be run from 'src' directory of ALTcointip"
exit 1
fi
if [ -z "$user" ] ; then
# Run as current user
python -c 'import cointipbot; cb=cointipbot.CointipBot(); cb.main()'
else
# Run as $user
sudo su - $user -c "cd `pwd` && python -c 'import cointipbot; cb=cointipbot.CointipBot(); cb.main()'"
fi
+37
View File
@@ -0,0 +1,37 @@
"""
This file is part of ALTcointip.
ALTcointip is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ALTcointip is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ALTcointip. If not, see <http://www.gnu.org/licenses/>.
"""
import cointipbot, logging
from ctb import ctb_stats
logging.basicConfig()
lg = logging.getLogger('cointipbot')
lg.setLevel(logging.DEBUG)
ctb = cointipbot.CointipBot(self_checks=False, init_reddit=True, init_coins=False, init_exchanges=False, init_db=True, init_logging=False)
# Update stats page
result = ctb_stats.update_stats(ctb=ctb)
lg.debug(result)
# Update tips page
result = ctb_stats.update_tips(ctb=ctb)
lg.debug(result)
# This isn't needed because it happens during the tip processing
#result = ctb_stats.update_all_user_stats(ctb=ctb)
#lg.debug(result)
+578
View File
@@ -0,0 +1,578 @@
#!/usr/bin/env python
"""
This file is part of ALTcointip.
ALTcointip is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ALTcointip is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ALTcointip. If not, see <http://www.gnu.org/licenses/>.
"""
from ctb import ctb_action, ctb_coin, ctb_db, ctb_exchange, ctb_log, ctb_misc, ctb_user
import gettext, locale, logging, praw, smtplib, sys, time, traceback, yaml
from email.mime.text import MIMEText
from jinja2 import Environment, PackageLoader
from requests.exceptions import HTTPError, ConnectionError, Timeout
from praw.errors import ExceptionList, APIException, InvalidCaptcha, InvalidUser, RateLimitExceeded
from socket import timeout
# Configure CointipBot logger
logging.basicConfig()
lg = logging.getLogger('cointipbot')
class CointipBot(object):
"""
Main class for cointip bot
"""
conf = None
db = None
reddit = None
coins = {}
exchanges = {}
jenv = None
runtime = {'ev': {}, 'regex': []}
def init_logging(self):
"""
Initialize logging handlers
"""
handlers = {}
levels = ['warning', 'info', 'debug']
lg = logging.getLogger('cointipbot')
bt = logging.getLogger('bitcoin')
# Get handlers
handlers = {}
for l in levels:
if self.conf.logs.levels[l].enabled:
handlers[l] = logging.FileHandler(self.conf.logs.levels[l].filename, mode='a' if self.conf.logs.levels[l].append else 'w')
handlers[l].setFormatter(logging.Formatter(self.conf.logs.levels[l].format))
# Set handlers
for l in levels:
if handlers.has_key(l):
level = logging.WARNING if l == 'warning' else (logging.INFO if l == 'info' else logging.DEBUG)
handlers[l].addFilter(ctb_log.LevelFilter(level))
lg.addHandler(handlers[l])
bt.addHandler(handlers[l])
# Set default levels
lg.setLevel(logging.DEBUG)
bt.setLevel(logging.DEBUG)
lg.info('CointipBot::init_logging(): -------------------- logging initialized --------------------')
return True
def parse_config(self):
"""
Returns a Python object with CointipBot configuration
"""
lg.debug('CointipBot::parse_config(): parsing config files...')
conf = {}
try:
prefix='./conf/'
for i in ['coins', 'db', 'exchanges', 'fiat', 'keywords', 'logs', 'misc', 'reddit', 'regex']:
lg.debug("CointipBot::parse_config(): reading %s%s.yml", prefix, i)
conf[i] = yaml.load(open(prefix+i+'.yml'))
except yaml.YAMLError as e:
lg.error("CointipBot::parse_config(): error reading config file: %s", e)
if hasattr(e, 'problem_mark'):
lg.error("CointipBot::parse_config(): error position: (line %s, column %s)", e.problem_mark.line+1, e.problem_mark.column+1)
sys.exit(1)
lg.info('CointipBot::parse_config(): config files has been parsed')
return ctb_misc.DotDict(conf)
def connect_db(self):
"""
Returns a database connection object
"""
lg.debug('CointipBot::connect_db(): connecting to database...')
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)
dbobj = ctb_db.CointipBotDatabase(dsn)
try:
conn = dbobj.connect()
except Exception as e:
lg.error("CointipBot::connect_db(): error connecting to database: %s", e)
sys.exit(1)
lg.info("CointipBot::connect_db(): connected to database %s as %s", self.conf.db.auth.dbname, self.conf.db.auth.user)
return conn
def connect_reddit(self):
"""
Returns a praw connection object
"""
lg.debug('CointipBot::connect_reddit(): connecting to Reddit...')
conn = praw.Reddit(user_agent = self.conf.reddit.auth.user)
conn.login(self.conf.reddit.auth.user, self.conf.reddit.auth.password)
lg.info("CointipBot::connect_reddit(): logged in to Reddit as %s", self.conf.reddit.auth.user)
return conn
def self_checks(self):
"""
Run self-checks before starting the bot
"""
# Ensure bot is a registered user
b = ctb_user.CtbUser(name=self.conf.reddit.auth.user.lower(), ctb=self)
if not b.is_registered():
b.register()
# Ensure (total pending tips) < (CointipBot's balance)
for c in self.coins:
ctb_balance = b.get_balance(coin=c, kind='givetip')
pending_tips = float(0)
actions = ctb_action.get_actions(atype='givetip', state='pending', coin=c, ctb=self)
for a in actions:
pending_tips += a.coinval
if (ctb_balance - pending_tips) < -0.000001:
raise Exception("CointipBot::self_checks(): CointipBot's %s balance (%s) < total pending tips (%s)" % (c.upper(), ctb_balance, pending_tips))
# Ensure coin balances are positive
for c in self.coins:
b = float(self.coins[c].conn.getbalance())
if b < 0:
raise Exception("CointipBot::self_checks(): negative balance of %s: %s" % (c, b))
# Ensure user accounts are intact and balances are not negative
sql = "SELECT username FROM t_users ORDER BY username"
for mysqlrow in self.db.execute(sql):
u = ctb_user.CtbUser(name=mysqlrow['username'], ctb=self)
if not u.is_registered():
raise Exception("CointipBot::self_checks(): user %s is_registered() failed" % mysqlrow['username'])
# for c in vars(self.coins):
# if u.get_balance(coin=c, kind='givetip') < 0:
# raise Exception("CointipBot::self_checks(): user %s %s balance is negative" % (mysqlrow['username'], c))
return True
def expire_pending_tips(self):
"""
Decline any pending tips that have reached expiration time limit
"""
# Calculate timestamp
seconds = int(self.conf.misc.times.expire_pending_hours * 3600)
created_before = time.mktime(time.gmtime()) - seconds
counter = 0
# Get expired actions and decline them
for a in ctb_action.get_actions(atype='givetip', state='pending', created_utc='< ' + str(created_before), ctb=self):
a.expire()
counter += 1
# Done
return (counter > 0)
def check_inbox(self):
"""
Evaluate new messages in inbox
"""
lg.debug('> CointipBot::check_inbox()')
try:
# Try to fetch some messages
messages = list(ctb_misc.praw_call(self.reddit.get_unread, limit=self.conf.reddit.scan.batch_limit))
messages.reverse()
# Process messages
for m in messages:
# Sometimes messages don't have an author (such as 'you are banned from' message)
if not m.author:
lg.info("CointipBot::check_inbox(): ignoring msg with no author")
ctb_misc.praw_call(m.mark_as_read)
continue
lg.info("CointipBot::check_inbox(): %s from %s", "comment" if m.was_comment else "message", m.author.name)
# Ignore duplicate messages (sometimes Reddit fails to mark messages as read)
if ctb_action.check_action(msg_id=m.id, ctb=self):
lg.warning("CointipBot::check_inbox(): duplicate action detected (msg.id %s), ignoring", m.id)
ctb_misc.praw_call(m.mark_as_read)
continue
# Ignore self messages
if m.author and m.author.name.lower() == self.conf.reddit.auth.user.lower():
lg.debug("CointipBot::check_inbox(): ignoring message from self")
ctb_misc.praw_call(m.mark_as_read)
continue
# Ignore messages from banned users
if m.author and self.conf.reddit.banned_users:
lg.debug("CointipBot::check_inbox(): checking whether user '%s' is banned..." % m.author)
u = ctb_user.CtbUser(name = m.author.name, redditobj = m.author, ctb = self)
if u.banned:
lg.info("CointipBot::check_inbox(): ignoring banned user '%s'" % m.author)
ctb_misc.praw_call(m.mark_as_read)
continue
action = None
if m.was_comment:
# Attempt to evaluate as comment / mention
action = ctb_action.eval_comment(m, self)
else:
# Attempt to evaluate as inbox message
action = ctb_action.eval_message(m, self)
# Perform action, if found
if action:
lg.info("CointipBot::check_inbox(): %s from %s (m.id %s)", action.type, action.u_from.name, m.id)
lg.debug("CointipBot::check_inbox(): message body: <%s>", m.body)
action.do()
else:
lg.info("CointipBot::check_inbox(): no match")
if self.conf.reddit.messages.sorry and not m.subject in ['post reply', 'comment reply']:
user = ctb_user.CtbUser(name=m.author.name, redditobj=m.author, ctb=self)
tpl = self.jenv.get_template('didnt-understand.tpl')
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)
lg.debug("CointipBot::check_inbox(): %s", msg)
user.tell(subj='What?', msg=msg, msgobj=m if not m.was_comment else None)
# Mark message as read
ctb_misc.praw_call(m.mark_as_read)
except (HTTPError, ConnectionError, Timeout, timeout) as e:
lg.warning("CointipBot::check_inbox(): Reddit is down (%s), sleeping", e)
time.sleep(self.conf.misc.times.sleep_seconds)
pass
except RateLimitExceeded as e:
lg.warning("CointipBot::check_inbox(): rate limit exceeded, sleeping for %s seconds", e.sleep_time)
time.sleep(e.sleep_time)
time.sleep(1)
pass
except Exception as e:
lg.error("CointipBot::check_inbox(): %s", e)
raise
lg.debug("< CointipBot::check_inbox() DONE")
return True
def init_subreddits(self):
"""
Determine a list of subreddits and create a PRAW object
"""
lg.debug("> CointipBot::init_subreddits()")
try:
if not hasattr(self.conf.reddit, 'subreddits'):
my_reddits_list = None
my_reddits_string = None
if hasattr(self.conf.reddit.scan, 'these_subreddits'):
# Subreddits are specified in conf.yml
my_reddits_list = list(self.conf.reddit.scan.these_subreddits)
elif self.conf.reddit.scan.my_subreddits:
# Subreddits are subscribed to by bot user
my_reddits = ctb_misc.praw_call(self.reddit.get_my_subreddits, limit=None)
my_reddits_list = []
for my_reddit in my_reddits:
my_reddits_list.append(my_reddit.display_name.lower())
my_reddits_list.sort()
else:
# No subreddits configured
lg.debug("< CointipBot::check_subreddits() DONE (no subreddits configured to scan)")
return False
# Build subreddits string
my_reddits_string = "+".join(my_reddits_list)
# Get multi-reddit PRAW object
lg.debug("CointipBot::check_subreddits(): multi-reddit string: %s", my_reddits_string)
self.conf.reddit.subreddits = ctb_misc.praw_call(self.reddit.get_subreddit, my_reddits_string)
except Exception as e:
lg.error("CointipBot::check_subreddits(): coudln't get subreddits: %s", e)
raise
lg.debug("< CointipBot::init_subreddits() DONE")
return True
def check_subreddits(self):
"""
Evaluate new comments from configured subreddits
"""
lg.debug("> CointipBot::check_subreddits()")
try:
# Process comments until old comment reached
# Get last_processed_comment_time if necessary
if not hasattr(self.conf.reddit, 'last_processed_comment_time') or self.conf.reddit.last_processed_comment_time <= 0:
self.conf.reddit.last_processed_comment_time = ctb_misc.get_value(conn=self.db, param0='last_processed_comment_time')
updated_last_processed_time = 0
# Fetch comments from subreddits
my_comments = ctb_misc.praw_call(self.conf.reddit.subreddits.get_comments, limit=self.conf.reddit.scan.batch_limit)
# Match each comment against regex
counter = 0
for c in my_comments:
# Stop processing if old comment reached
#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)
if c.created_utc <= self.conf.reddit.last_processed_comment_time:
lg.debug("CointipBot::check_subreddits(): old comment reached")
break
counter += 1
if c.created_utc > updated_last_processed_time:
updated_last_processed_time = c.created_utc
# Ignore duplicate comments (may happen when bot is restarted)
if ctb_action.check_action(msg_id=c.id, ctb=self):
lg.warning("CointipBot::check_inbox(): duplicate action detected (comment.id %s), ignoring", c.id)
continue
# Ignore comments from banned users
if c.author and self.conf.reddit.banned_users:
lg.debug("CointipBot::check_subreddits(): checking whether user '%s' is banned..." % c.author)
u = ctb_user.CtbUser(name = c.author.name, redditobj = c.author, ctb = self)
if u.banned:
lg.info("CointipBot::check_subreddits(): ignoring banned user '%s'" % c.author)
continue
# Attempt to evaluate comment
action = ctb_action.eval_comment(c, self)
# Perform action, if found
if action:
lg.info("CointipBot::check_subreddits(): %s from %s (%s)", action.type, action.u_from.name, c.id)
lg.debug("CointipBot::check_subreddits(): comment body: <%s>", c.body)
action.do()
else:
lg.info("CointipBot::check_subreddits(): no match")
lg.debug("CointipBot::check_subreddits(): %s comments processed", counter)
if counter >= self.conf.reddit.scan.batch_limit - 1:
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)
except (HTTPError, RateLimitExceeded, timeout) as e:
lg.warning("CointipBot::check_subreddits(): Reddit is down (%s), sleeping", e)
time.sleep(self.conf.misc.times.sleep_seconds)
pass
except Exception as e:
lg.error("CointipBot::check_subreddits(): coudln't fetch comments: %s", e)
raise
# Save updated last_processed_time value
if updated_last_processed_time > 0:
self.conf.reddit.last_processed_comment_time = updated_last_processed_time
ctb_misc.set_value(conn=self.db, param0='last_processed_comment_time', value0=self.conf.reddit.last_processed_comment_time)
lg.debug("< CointipBot::check_subreddits() DONE")
return True
def refresh_ev(self):
"""
Refresh coin/fiat exchange values using self.exchanges
"""
# Return if rate has been checked in the past hour
seconds = int(1 * 3600)
if hasattr(self.conf.exchanges, 'last_refresh') and self.conf.exchanges.last_refresh + seconds > int(time.mktime(time.gmtime())):
lg.debug("< CointipBot::refresh_ev(): DONE (skipping)")
return
# For each enabled coin...
for c in vars(self.conf.coins):
if self.conf.coins[c].enabled:
# Get BTC/coin exchange rate
values = []
result = 0.0
if not self.conf.coins[c].unit == 'btc':
# For each exchange that supports this coin...
for e in self.exchanges:
if self.exchanges[e].supports_pair(_name1=self.conf.coins[c].unit, _name2='btc'):
# Get ticker value from exchange
value = self.exchanges[e].get_ticker_value(_name1=self.conf.coins[c].unit, _name2='btc')
if value and float(value) > 0.0:
values.append(float(value))
# Result is average of all responses
if len(values) > 0:
result = sum(values) / float(len(values))
else:
# BTC/BTC rate is always 1
result = 1.0
# Assign result to self.runtime['ev']
if not self.runtime['ev'].has_key(c):
self.runtime['ev'][c] = {}
self.runtime['ev'][c]['btc'] = result
# For each enabled fiat...
for f in vars(self.conf.fiat):
if self.conf.fiat[f].enabled:
# Get fiat/BTC exchange rate
values = []
result = 0.0
# For each exchange that supports this fiat...
for e in self.exchanges:
if self.exchanges[e].supports_pair(_name1='btc', _name2=self.conf.fiat[f].unit):
# Get ticker value from exchange
value = self.exchanges[e].get_ticker_value(_name1='btc', _name2=self.conf.fiat[f].unit)
if value and float(value) > 0.0:
values.append(float(value))
# Result is average of all responses
if len(values) > 0:
result = sum(values) / float(len(values))
# Assign result to self.runtime['ev']
if not self.runtime['ev'].has_key('btc'):
self.runtime['ev']['btc'] = {}
self.runtime['ev']['btc'][f] = result
lg.debug("CointipBot::refresh_ev(): %s", self.runtime['ev'])
# Update last_refresh
self.conf.exchanges.last_refresh = int(time.mktime(time.gmtime()))
def coin_value(self, _coin, _fiat):
"""
Quick method to return _fiat value of _coin
"""
try:
value = self.runtime['ev'][_coin]['btc'] * self.runtime['ev']['btc'][_fiat]
except KeyError as e:
lg.warning("CointipBot::coin_value(%s, %s): KeyError", _coin, _fiat)
value = 0.0
return value
def notify(self, _msg=None):
"""
Send _msg to configured destination
"""
# Construct MIME message
msg = MIMEText(_msg)
msg['Subject'] = self.conf.misc.notify.subject
msg['From'] = self.conf.misc.notify.addr_from
msg['To'] = self.conf.misc.notify.addr_to
# Send MIME message
server = smtplib.SMTP(self.conf.misc.notify.smtp_host)
if self.conf.misc.notify.smtp_tls:
server.starttls()
server.login(self.conf.misc.notify.smtp_username, self.conf.misc.notify.smtp_password)
server.sendmail(self.conf.misc.notify.addr_from, self.conf.misc.notify.addr_to, msg.as_string())
server.quit()
def __init__(self, self_checks=True, init_reddit=True, init_coins=True, init_exchanges=True, init_db=True, init_logging=True):
"""
Constructor. Parses configuration file and initializes bot.
"""
lg.info("CointipBot::__init__()...")
# Configuration
self.conf = self.parse_config()
# Logging
if init_logging:
self.init_logging()
# Templating with jinja2
self.jenv = Environment(trim_blocks=True, loader=PackageLoader('cointipbot', 'tpl/jinja2'))
# Database
if init_db:
self.db = self.connect_db()
# Coins
if init_coins:
for c in vars(self.conf.coins):
if self.conf.coins[c].enabled:
self.coins[c] = ctb_coin.CtbCoin(_conf=self.conf.coins[c])
if not len(self.coins) > 0:
lg.error("CointipBot::__init__(): Error: please enable at least one type of coin")
sys.exit(1)
# Exchanges
if init_exchanges:
for e in vars(self.conf.exchanges):
if self.conf.exchanges[e].enabled:
self.exchanges[e] = ctb_exchange.CtbExchange(_conf=self.conf.exchanges[e])
if not len(self.exchanges) > 0:
lg.warning("Cointipbot::__init__(): Warning: no exchanges are enabled")
# Reddit
if init_reddit:
self.reddit = self.connect_reddit()
self.init_subreddits()
# Regex for Reddit messages
ctb_action.init_regex(self)
# Self-checks
if self_checks:
self.self_checks()
lg.info("< CointipBot::__init__(): DONE, batch-limit = %s, sleep-seconds = %s", self.conf.reddit.scan.batch_limit, self.conf.misc.times.sleep_seconds)
def __str__(self):
"""
Return string representation of self
"""
me = "<CointipBot: sleepsec=%s, batchlim=%s, ev=%s"
me = me % (self.conf.misc.times.sleep_seconds, self.conf.reddit.scan.batch_limit, self.runtime['ev'])
return me
def main(self):
"""
Main loop
"""
while (True):
try:
lg.debug("CointipBot::main(): beginning main() iteration")
# Refresh exchange rate values
self.refresh_ev()
# Check personal messages
self.check_inbox()
# Expire pending tips
self.expire_pending_tips()
# Check subreddit comments for tips
if self.conf.reddit.scan.my_subreddits or hasattr(self.conf.reddit.scan, 'these_subreddits'):
self.check_subreddits()
# Sleep
lg.debug("CointipBot::main(): sleeping for %s seconds...", self.conf.misc.times.sleep_seconds)
time.sleep(self.conf.misc.times.sleep_seconds)
except Exception as e:
lg.error("CointipBot::main(): exception: %s", e)
tb = traceback.format_exc()
lg.error("CointipBot::main(): traceback: %s", tb)
# Send a notification, if enabled
if self.conf.misc.notify.enabled:
self.notify(_msg=tb)
sys.exit(1)
+457
View File
@@ -0,0 +1,457 @@
# Bitcoin
btc:
enabled: false
unit: btc
name: Bitcoin
symbol: 'B⃦'
config_file: '~/.bitcoin/bitcoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 1
withdraw: 144
txmin:
givetip: 0.00000001
withdraw: 0.0005
txfee: 0.0001
walletpassphrase: 'mypass1'
explorer:
address: 'http://blockchain.info/address/'
transaction: 'http://blockchain.info/tx/'
regex:
address: '([1|3][1-9a-z]{20,40})'
units: '(btc|bitcoin|bitcoins|xbt|Ƀ|฿|B⃦)'
# Litecoin
ltc:
enabled: false
unit: ltc
name: Litecoin
symbol: 'Ł'
config_file: '~/.litecoin/litecoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 4
withdraw: 576
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.001
walletpassphrase: 'mypass1'
explorer:
address: 'http://block-explorer.com/address/'
transaction: 'http://block-explorer.com/tx/'
regex:
address: '(L[1-9a-z]{20,40})'
units: '(ltc|litecoin|litecoins|Ł)'
# Peercoin
ppc:
enabled: false
unit: ppc
name: Peercoin
symbol: 'Ƥ'
config_file: '~/.ppcoin/ppcoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 1
withdraw: 144
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.01
walletpassphrase: 'mypass1'
explorer:
address: 'http://ppc.cryptocoinexplorer.com/address/'
transaction: 'http://ppc.cryptocoinexplorer.com/tx/'
regex:
address: '(P[1-9a-z]{20,40})'
units: '(ppc|ppcoin|ppcoins|peercoin|peercoins|Ƥ)'
# Namecoin
nmc:
enabled: false
unit: nmc
name: Namecoin
symbol: 'Ɲ'
config_file: '~/.namecoin/bitcoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 1
withdraw: 144
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.01
explorer:
address: 'http://explorer.dot-bit.org/a/'
transaction: 'http://explorer.dot-bit.org/tx/'
regex:
address: '([N|M][1-9a-z]{20,40})'
units: '(nmc|namecoin|namecoins|Ɲ)'
# Novacoin
nvc:
enabled: false
unit: nvc
name: Novacoin
symbol: 'Ɣ'
config_file: '~/.novacoin/novacoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 1
withdraw: 144
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.01
walletpassphrase: 'mypass1'
explorer:
address: 'http://nvc.cryptocoinexplorer.com/address/'
transaction: 'http://nvc.cryptocoinexplorer.com/tx/'
regex:
address: '(4[1-9a-z]{20,40})'
units: '(nvc|novacoin|novacoins|Ɣ)'
# Terracoin
trc:
enabled: false
unit: trc
name: Terracoin
symbol: 'Ʈ'
config_file: '~/.terracoin/terracoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 5
withdraw: 720
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.01
walletpassphrase: 'mypass1'
explorer:
address: 'http://trc.cryptocoinexplorer.com/address/'
transaction: 'http://trc.cryptocoinexplorer.com/tx/'
regex:
address: '(1[1-9a-z]{20,40})'
units: '(trc|terracoin|terracoins|Ʈ)'
# Devcoin
dvc:
enabled: false
unit: dvc
name: Devcoin
symbol: 'Ð'
config_file: '~/.devcoin/devcoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 1
withdraw: 144
txmin:
givetip: 0.1
withdraw: 0.5
txfee: 0.5
explorer:
address: 'http://darkgamex.ch:2751/address/'
transaction: 'http://darkgamex.ch:2751/tx/'
regex:
address: '(1[1-9a-z]{20,40})'
units: '(dvc|devcoin|devcoins|Ð)'
# Primecoin
xpm:
enabled: false
unit: xpm
name: Primecoin
symbol: 'Ψ'
config_file: '~/.primecoin/primecoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 10
withdraw: 1440
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.01
walletpassphrase: 'mypass1'
explorer:
address: 'http://xpm.cryptocoinexplorer.com/address/'
transaction: 'http://xpm.cryptocoinexplorer.com/transaction/'
regex:
address: '(A[1-9a-z]{20,40})'
units: '(xpm|primecoin|primecoins|Ψ)'
# Feathercoin
ftc:
enabled: false
unit: ftc
name: Feathercoin
symbol: 'Ƒ'
config_file: '~/.feathercoin/feathercoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 4
withdraw: 576
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.01
walletpassphrase: 'mypass1'
explorer:
address: 'http://explorer.feathercoin.com/address/'
transaction: 'http://explorer.feathercoin.com/tx/'
regex:
address: '([67][1-9a-z]{20,40})'
units: '(ftc|feathercoin|feathercoins|Ƒ)'
# Megacoin
mec:
enabled: false
unit: mec
name: Megacoin
symbol: 'M'
config_file: '~/.megacoin/megacoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 4
withdraw: 576
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.01
walletpassphrase: 'mypass1'
explorer:
address: 'http://mega.rapta.net:2750/address/'
transaction: 'http://mega.rapta.net:2750/tx/'
regex:
address: '(M[1-9a-z]{20,40})'
units: '(mec|megacoin|megacoins|MΣC)'
# Quarkcoin
qrk:
enabled: false
unit: qrk
name: Quarkcoin
symbol: 'Ǫ'
config_file: '~/.quarkcoin/quarkcoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 20
withdraw: 2880
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.0001
walletpassphrase: 'mypass1'
explorer:
address: 'http://bitinfocharts.com/quarkcoin/address/'
transaction: 'http://bitinfocharts.com/quarkcoin/tx/'
regex:
address: '(Q[1-9a-z]{20,40})'
units: '(qrk|quark|quarks|quarkcoin|quarkcoins|Ǫ)'
# Zetacoin
zet:
enabled: false
unit: zet
name: Zetacoin
symbol: 'Ȥ'
config_file: '~/.zetacoin/zetacoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 20
withdraw: 2880
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.0001
walletpassphrase: 'mypass1'
explorer:
address: 'http://bit.usr.sh:2750/chain/Zetacoin/address/'
transaction: 'http://bit.usr.sh:2750/chain/Zetacoin/tx/'
regex:
address: '(Z[1-9a-z]{20,40})'
units: '(zet|zetacoin|zetacoins|Ȥ)'
# Digitalcoin
dgc:
enabled: false
unit: dgc
name: Digitalcoin
symbol: 'Ɗ'
config_file: '~/.digitalcoin/digitalcoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 15
withdraw: 2160
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.0001
walletpassphrase: 'mypass1'
explorer:
address: 'http://dgc.cryptocoinexplorer.com/address/'
transaction: 'http://dgc.cryptocoinexplorer.com/tx/'
regex:
address: '(D[1-9a-z]{20,40})'
units: '(dgc|digitalcoin|digitalcoins|Ɗ)'
# Infinitecoin
ifc:
enabled: false
unit: ifc
name: Infinitecoin
symbol: 'Ĩ'
config_file: '~/.infinitecoin/infinitecoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 20
withdraw: 2880
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.0001
walletpassphrase: 'mypass1'
explorer:
address: 'http://exploretheblocks.com:2750/address/'
transaction: 'http://exploretheblocks.com:2750/tx/'
regex:
address: '(i[1-9a-z]{20,40})'
units: '(ifc|infinitecoin|infinitecoins|Ĩ)'
# Ixcoin
ixc:
enabled: false
unit: ixc
name: Ixcoin
symbol: 'メ'
config_file: '~/.ixcoin/ixcoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 1
withdraw: 144
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.01
walletpassphrase: 'mypass1'
explorer:
address: '#'
transaction: '#'
regex:
address: '(x[1-9a-z]{20,40})'
units: '(ixc|ixcoin|ixcoins|メ)'
# BBQCoin
bqc:
enabled: false
unit: bqc
name: BBQCoin
symbol: 'Ɓ'
config_file: '~/.bbqcoin/bbqcoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 10
withdraw: 1440
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.0001
walletpassphrase: 'mypass1'
explorer:
address: 'http://bbq.cryptocoinexplorer.com/address/'
transaction: 'http://bbq.cryptocoinexplorer.com/tx/'
regex:
address: '(b[1-9a-z]{20,40})'
units: '(bqc|bbq|bbqcoin|bbqcoins|Ɓ)'
# Stablecoin
sbc:
enabled: false
unit: sbc
name: Stablecoin
symbol: 'Ş'
config_file: '~/.stablecoin/stablecoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 15
withdraw: 2160
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.0001
walletpassphrase: 'mypass1'
explorer:
address: 'http://sbc.blockexplorer.io/address/'
transaction: 'http://sbc.blockexplorer.io/tx/'
regex:
address: '(s[1-9a-z]{20,40})'
units: '(sbc|stablecoin|stablecoins|Ş)'
# I0coin
i0c:
enabled: false
unit: i0c
name: i0coin
symbol: 'Ǐ'
config_file: '~/.i0coin/i0coin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 1
withdraw: 144
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.0001
walletpassphrase: 'mypass1'
explorer:
address: '#'
transaction: '#'
regex:
address: '(x[1-9a-z]{20,40})'
units: '(i0c|i0coin|i0coins|Ǐ)'
# Mincoin
mnc:
enabled: false
unit: mnc
name: Mincoin
symbol: 'Ӎ'
config_file: '~/.mincoin/mincoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 10
withdraw: 1440
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.0001
walletpassphrase: 'mypass1'
explorer:
address: 'http://blockexplorer.vircurpool.com/address/'
transaction: 'http://blockexplorer.vircurpool.com/tx/'
regex:
address: '(M[1-9a-z]{20,40})'
units: '(mnc|mincoin|mincoins|Ӎ)'
# Securecoin
src:
enabled: false
unit: src
name: Securecoin
symbol: 'Š'
config_file: '~/.securecoin/securecoin.conf'
config_rpcserver: '127.0.0.1'
minconf:
givetip: 10
withdraw: 1440
txmin:
givetip: 0.00000001
withdraw: 0.05
txfee: 0.0001
walletpassphrase: 'mypass1'
explorer:
address: '#'
transaction: '#'
regex:
address: '(s[1-9a-z]{20,40})'
units: '(src|securecoin|securecoins|Š)'
+110
View File
@@ -0,0 +1,110 @@
auth:
user: 'mysqluser'
password: 'mysqlpass'
host: localhost
port: 3306
dbname: mysqldb
sql:
globalstats:
01_total_tipped_usd:
name: "Total Accepted Tips (USD)"
desc: "Total value of all tips given and accepted in USD (default) fiat"
type: line
query: "SELECT SUM(fiat_val) AS total_usd, fiat FROM t_action WHERE type = 'givetip' AND state = 'completed' AND fiat = 'usd'"
01a_total_tipped_usd_by_coin:
name: "Total Accepted Tips (USD) By Coin"
desc: "Total value of all tips given and accepted in USD (default) fiat grouped by coin"
type: table
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"
02_total_tips_expired_and_declined:
name: "Total Expired and Declined Tips (USD)"
desc: "Total value of all tips given that weren't accepted (expired or declined) in USD (default) fiat"
type: line
query: "SELECT SUM(fiat_val) AS total_usd, fiat FROM t_action WHERE type = 'givetip' AND state = 'expired' OR state = 'declined' AND fiat = 'usd'"
03_total_users_registered:
name: "Total Users Registered"
desc: "Number of registered users"
type: line
query: "SELECT COUNT(username) AS total_users FROM t_users"
04_total_tippers:
name: "Total Tippers"
desc: "Number of users who tipped at least once"
type: line
query: "SELECT COUNT(from_user) AS total_tippers FROM (SELECT DISTINCT from_user FROM t_action WHERE type = 'givetip') AS t_distinct_action"
05_total_tips:
name: "Total Number of Tips"
desc: "Total number of tips given"
type: line
query: "SELECT COUNT(msg_id) AS total_tips FROM t_action WHERE type = 'givetip' AND state = 'completed'"
05a_total_tips_by_coin:
name: "Total Number of Tips (by coin)"
desc: "Total number of tips given grouped by coin"
type: table
query: "SELECT coin, count(*) AS total_tips FROM t_action WHERE type = 'givetip' AND state = 'completed' GROUP BY coin ORDER BY coin"
05b_total_karma_redeemed:
name: "Total Karma Redeemed (USD)"
desc: "Total value of redeemed karma"
type: line
query: "SELECT SUM(fiat_val) AS total_usd, fiat FROM t_action WHERE type = 'redeem' AND state = 'completed' AND fiat = 'usd'"
06_top_10_tippers:
name: "Top 10 Tippers"
desc: "Top 10 all-time tippers as determined by total USD/EUR (fiat) value of their tips."
type: table
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"
07_top_10_tips:
name: "Top 10 Tips"
desc: "Top 10 all-time tips as determined by their USD/EUR (fiat) value."
type: table
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"
07a_top_5_tips_btc:
name: "Top 5 Tips (BTC)"
desc: "Top 5 all-time BTC tips as determined by coin amount"
type: table
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"
07b_top_5_tips_ltc:
name: "Top 5 Tips (LTC)"
desc: "Top 5 all-time LTC tips as determined by coin amount"
type: table
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"
07c_top_5_tips_ppc:
name: "Top 5 Tips (PPC)"
desc: "Top 5 all-time PPC tips as determined by coin amount"
type: table
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"
07d_top_5_tips_nmc:
name: "Top 5 Tips (NMC)"
desc: "Top 5 all-time NMC tips as determined by coin amount"
type: table
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"
07e_top_5_tips_xpm:
name: "Top 5 Tips (XPM)"
desc: "Top 5 all-time XPM tips as determined by coin amount"
type: table
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"
07f_top_5_tips_mec:
name: "Top 5 Tips (MEC)"
desc: "Top 5 all-time MEC tips as determined by coin amount"
type: table
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"
08_top_10_receivers:
name: "Top 10 Receivers"
desc: "Top 10 all-time tip receivers as determined by total USD/EUR (fiat) value of their received tips."
type: table
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"
userstats:
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"
coins: 'SELECT DISTINCT coin FROM t_action WHERE coin IS NOT NULL ORDER BY coin'
fiat: 'SELECT DISTINCT fiat FROM t_action WHERE fiat IS NOT NULL ORDER BY fiat'
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"
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"
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"
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"
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"
userhistory:
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"
limit: 75
tips:
sql_set: "SET @rank=0"
sql_list: "SELECT @rank :=@rank+1 AS num, created_utc, from_user, to_user, coin_val, coin, fiat_val, fiat, subreddit FROM t_action WHERE type='givetip' AND state='completed' ORDER BY created_utc ASC LIMIT %s"
limit: 10000
+109
View File
@@ -0,0 +1,109 @@
blockchaininfo:
enabled: true
domain: 'blockchain.info'
https: true
urlpaths: ['/ticker']
jsonpaths: ['{THING_TO}.15m']
coinlist: ['btc']
fiatlist: ['usd','cny','jpy','sgd','hkd','cad','aud','nzd','gbp','dkk','sek','brl','chf','eur','rub','sll','pln','thb']
uppercase: true
vircurex:
enabled: true
domain: 'api.vircurex.com'
https: true
urlpaths: ['/api/get_highest_bid.json?base={THING_FROM}&alt={THING_TO}', '/api/get_lowest_ask.json?base={THING_FROM}&alt={THING_TO}']
jsonpaths: ['value']
coinlist: ['anc','btc','dgc','dvc','frc','ftc','i0c','ixc','ltc','nmc','nvc','ppc','trc','xpm']
fiatlist: ['usd','eur']
uppercase: true
btce:
enabled: true
domain: 'btc-e.com'
https: true
urlpaths: ['/api/2/{THING_FROM}_{THING_TO}/ticker']
jsonpaths: ['ticker.avg']
coinlist: ['btc','ltc','nmc','nvc','trc','ppc','ftc','xpm']
fiatlist: ['usd','eur','rur']
uppercase: false
bitstamp:
enabled: true
domain: 'www.bitstamp.net'
https: true
urlpaths: ['/api/ticker/']
jsonpaths: ['ask']
coinlist: ['btc']
fiatlist: ['usd']
uppercase: false
mtgox:
enabled: false
domain: 'data.mtgox.com'
https: true
urlpaths: ['/api/2/{THING_FROM}{THING_TO}/money/ticker_fast']
jsonpaths: ['data.buy.value']
coinlist: ['btc']
fiatlist: ['usd','eur','jpy','gbp','rub']
uppercase: true
campbx:
enabled: true
domain: 'campbx.com'
https: true
urlpaths: ['/api/xticker.php']
jsonpaths: ['Last Trade']
coinlist: ['btc']
fiatlist: ['usd']
uppercase: false
cryptsy:
enabled: true
domain: 'pubapi.cryptsy.com'
https: false
urlpaths: ['/api.php?method=marketdatav2']
jsonpaths: ['return.markets.{THING_FROM}/{THING_TO}.lasttradeprice']
coinlist: ['btc','alf','amc','anc','arg','bqc','bte','btg','buk','cap','cgb','clr','cmc','crc','csc','dgc','dmd','elc','emd','frc','frk','fst','ftc','gdc','glc','glx','hbn','ixc','kgc','lk7','lky','ltc','mec','mnc','nbl','nec','nmc','nrb','nvc','phs','ppc','pts','pxc','pyc','qrk','sbc','spt','src','tag','tek','trc','wdc','xjo','xpm','yac','zet']
fiatlist: []
uppercase: true
bter:
enabled: true
domain: 'bter.com'
https: true
urlpaths: ['/api/1/ticker/{THING_FROM}_{THING_TO}']
jsonpaths: ['avg']
coinlist: ['btc','ltc','ppc','frc','ftc','cnc','bqc','btb','wdc','yac','xpm','pts','zcc','dtc','red','cent','zet','src','mec','qrk']
fiatlist: ['cny']
uppercase: false
cryptotrade:
enabled: true
domain: 'crypto-trade.com'
https: true
urlpaths: ['/api/1/ticker/{THING_FROM}_{THING_TO}']
jsonpaths: ['data.min_ask', 'data.max_bid']
coinlist: ['btc','ltc','nmc','xpm','ppc','trc','ftc','dvc','wdc','dgc']
fiatlist: ['usd','eur']
uppercase: false
coinse:
enabled: true
domain: 'www.coins-e.com'
https: true
urlpaths: ['/api/v2/markets/data/']
jsonpaths: ['markets.{THING_FROM}_{THING_TO}.marketstat.24h.avg_rate']
coinlist: ['btc','alp','amc','anc','bet','bqc','cgb','cin','crc','dem','dtc','elc','elp','emd','frk','ftc','gdc','glc','kgc','ltc','mec','nan','net','nrb','nvc','orb','ppc','pts','qrk','rec','red','spt','tag','trc','uno','wdc','xnc','xpm','zet']
fiatlist: []
uppercase: true
cryptocoincharts:
enabled: true
domain: 'cryptocoincharts.info'
https: false
urlpaths: ['/v2/api/tradingPair/{THING_FROM}_{THING_TO}']
jsonpaths: ['price']
coinlist: ['btc','ltc','ppc','nmc','xmp','mec','bqc','dgc','ifc','ixc','mnc','qrk','sbc','zet','doge','nxt','msc','ftc','nvc','dvc','frc','trc']
fiatlist: ['usd','eur']
uppercase: false
+53
View File
@@ -0,0 +1,53 @@
# USD
usd:
enabled: true
unit: usd
symbol: '$'
name: US Dollar
regex:
units: '(\$|usd)'
# CNY
cny:
enabled: true
unit: cny
symbol: '¥'
name: Chinese Yuan
regex:
units: '(¥|cny)'
# EUR
eur:
enabled: true
unit: eur
symbol: '€'
name: Euro
regex:
units: '(€|eur)'
# GBP
gbp:
enabled: true
unit: gbp
symbol: '£'
name: British Pound
regex:
units: '(£|gbp)'
# JPY
jpy:
enabled: true
unit: jpy
symbol: '¥'
name: Japanese Yen
regex:
units: '(¥|jpy)'
# RUB
rub:
enabled: true
unit: rub
symbol: 'R'
name: Russian Ruble
regex:
units: '(r|rur)'
+222
View File
@@ -0,0 +1,222 @@
all:
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"
for_fiat: false
for_coin: true
message: '^**(*・_・)ノ⌒*** {USER_FROM} ^gives {USER_TO} ^_everything_!.. ^all {AMOUNT} ^now ^belong ^to {USER_TO}'
flip:
value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,2)) )"
for_fiat: true
for_coin: true
message: '{USER_FROM} ^flips ^the ^table ^**(╯°□°)╯︵┻━┻** ^and {USER_TO} ^wins {AMOUNT}'
megaflip:
value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,20)) )"
for_fiat: true
for_coin: true
message: '{USER_FROM} ^flips ^10 ^tables ^**(╯°□°)╯︵┻━┻** ^and {USER_TO} ^wins {AMOUNT}'
gigaflip:
value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,200)) )"
for_fiat: true
for_coin: true
message: '{USER_FROM} ^flips ^100 ^tables ^**(╯°□°)╯︵┻━┻** ^and {USER_TO} ^wins {AMOUNT}'
roll:
value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,6)) )"
for_fiat: true
for_coin: true
message: '{USER_FROM} ^rolls ^around ^and {USER_TO} ^gets {AMOUNT}'
megaroll:
value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,60)) )"
for_fiat: true
for_coin: true
message: '{USER_FROM} ^rolls ^around ^and {USER_TO} ^gets {AMOUNT}'
gigaroll:
value: "float( (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01) * float(randint(1,600)) )"
for_fiat: true
for_coin: true
message: '{USER_FROM} ^rolls ^aroun ^and {USER_TO} ^gets {AMOUNT}'
random01:
value: "float( randint(0,100) ) / 1000.0"
for_fiat: true
for_coin: true
message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}"
random05:
value: "float( randint(0,500) ) / 1000.0"
for_fiat: true
for_coin: true
message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}"
random1:
value: "float( randint(0,1000) ) / 1000.0"
for_fiat: true
for_coin: true
message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}"
random3:
value: "float( randint(0,3000) ) / 1000.0"
for_fiat: true
for_coin: true
message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}"
random5:
value: "float( randint(0,5000) ) / 1000.0"
for_fiat: true
for_coin: true
message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}"
random10:
value: "float( randint(0,10000) ) / 1000.0"
for_fiat: true
for_coin: true
message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}"
random15:
value: "float( randint(0,15000) ) / 1000.0"
for_fiat: true
for_coin: true
message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}"
random25:
value: "float( randint(0,25000) ) / 1000.0"
for_fiat: true
for_coin: true
message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}"
random50:
value: "float( randint(0,50000) ) / 1000.0"
for_fiat: true
for_coin: true
message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}"
random100:
value: "float( randint(0,100000) ) / 1000.0"
for_fiat: true
for_coin: true
message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}"
random250:
value: "float( randint(0,250000) ) / 1000.0"
for_fiat: true
for_coin: true
message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}"
random500:
value: "float( randint(0,500000) ) / 1000.0"
for_fiat: true
for_coin: true
message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}"
random1000:
value: "float( randint(0,1000000) ) / 1000.0"
for_fiat: true
for_coin: true
message: "{USER_FROM} ^asks ^randomness ^to ^decide ^luck ^of {USER_TO} ^and ^result ^is {AMOUNT}"
upvote:
value: 0.1
for_fiat: true
for_coin: true
message: '{USER_FROM} ^gives {USER_TO} ^an ^upvote... _^with ^benefits_ ^**ヽ(*・ω・)ノ** ^- ^worth {AMOUNT}'
highfive:
value: 0.25
for_fiat: true
for_coin: true
message: '{USER_FROM} ^gives {USER_TO} ^a ^high ^five ^**☆☆☆** ^- ^worth {AMOUNT}'
fart:
value: 0.05
for_fiat: true
for_coin: true
message: '{USER_FROM} ^farts ^on {USER_TO} ^**(⊙﹏⊙)** ^- ^worth {AMOUNT}'
coffee:
value: 1.0
for_fiat: true
for_coin: false
message: '{USER_TO} ^gets ^a ^delicious ^cup ^of ^coffee ^from {USER_FROM} ^**(´◠ω◠`)** ^- ^worth {AMOUNT}'
cookie:
value: 0.33
for_fiat: true
for_coin: false
message: '{USER_FROM} ^gives {USER_TO} ^a ^yummy ^cookie ^**♪┏(・o・)┛♪** ^- ^worth {AMOUNT}'
sushi:
value: 3.0
for_fiat: true
for_coin: false
message: '{USER_FROM} ^serves {USER_TO} ^a ^roll ^of ^yummy ^sushi ^**(っ˘ڡ˘ς)** ^- ^worth {AMOUNT}'
nothing:
value: "float (self.ctb.conf.coins[self.coin].txmin.givetip if bool(self.coin) else 0.01)"
for_fiat: true
for_coin: true
message: '^**(″・ิ_・ิ)っ** {USER_FROM} ^gives ^almost ^nothing ^to {USER_TO} ^- {AMOUNT}'
gum:
value: 1.5
for_fiat: true
for_coin: false
message: '{USER_FROM} ^gives ^a ^pack ^of ^gum ^to {USER_TO} ^- ^worth {AMOUNT}'
beer:
value: 3.5
for_fiat: true
for_coin: false
message: '{USER_FROM} ^gives ^a ^beer ^to {USER_TO} ^- ^worth {AMOUNT}'
nicebeer:
value: 5.0
for_fiat: true
for_coin: false
message: '{USER_FROM} ^gives ^a ^nice ^beer ^**(〃・ิ‿・ิ)ゞ** ^to {USER_TO} ^- ^worth {AMOUNT}) ^**ƪ(˘▽˘ƪ)**'
hug:
value: 0.5
for_fiat: true
for_coin: true
message: '^**(っ⌒‿⌒)っ** {USER_FROM} ^gives ^a ^nice ^hug ^**ヽ(*⌒∇⌒*)ノ** ^to {USER_TO} ^along ^with {AMOUNT}'
kiss:
value: 1.0
for_fiat: true
for_coin: true
message: '{USER_TO} ^gets ^a ^kiss ^**(˘⌣˘)ε˘`)** ^from {USER_FROM} ^along ^with {AMOUNT}'
nyan:
value: 1.111111
for_fiat: true
for_coin: true
message: '^**~(=\^‥\^)ノ** {USER_FROM} ^nyan ^nyan ^にゃん {USER_TO} ^にゃん ^にゃん {AMOUNT}'
pi:
value: 3.14159265
for_fiat: true
for_coin: true
e:
value: 2.7182818
for_fiat: true
for_coin: true
leet:
value: 1.337
for_fiat: true
for_coin: true
quarter:
value: 0.25
for_fiat: true
for_coin: true
half:
value: 0.5
for_fiat: true
for_coin: true
+16
View File
@@ -0,0 +1,16 @@
levels:
warning:
enabled: true
append: true
format: '%(levelname)s %(asctime)s %(message)s'
filename: logs/warning.log
info:
enabled: true
append: true
format: '%(levelname)s %(asctime)s %(message)s'
filename: logs/info.log
debug:
enabled: false
append: false
format: '%(levelname)s %(asctime)s %(message)s'
filename: logs/debug.log
+18
View File
@@ -0,0 +1,18 @@
times:
sleep_seconds: 60
expire_pending_hours: 48
backup:
encryptionpassphrase: 'ChangeAndRememberMe11'
qr_service_url: 'https://chart.googleapis.com/chart?cht=qr&choe=UTF-8&chs=300x300&chl='
notify:
enabled: false
addr_from: 'user@example.com'
addr_to: 'you@example.com'
subject: 'ALTcointip error'
smtp_host: 'smtp.gmail.com:587'
smtp_tls: true
smtp_username: 'user@example.com'
smtp_password: 'examplePass'
+48
View File
@@ -0,0 +1,48 @@
auth:
user: 'mybotuser'
password: 'mybotpass'
scan:
batch_limit: 1000
my_subreddits: false
# these_subreddits: ["all"]
help:
enabled: true
url: 'http://www.reddit.com/r/mysubreddit/wiki/index'
contact:
enabled: true
url: 'http://www.reddit.com/message/compose/?to=YOUR_NAME'
messages:
verified: true
pending: false
expired: false
declined: false
sorry: true
redeem:
unit: 'usd'
multicoin: true
min_karma: 150
base: 0.1
multiplier:
link: 0.00005
comment: 0.000005
maximum: 0.5
account: 'YOUR_ACCOUNT_NAME'
banned_users:
method: list
list: ['mybotuser', 'bitcointip', 'altcointip']
subreddit: 'mysubreddit'
stats:
enabled: false
userflair: false
subreddit: 'mysubreddit'
page: 'stats'
url: 'http://www.reddit.com/r/mysubreddit/wiki/stats'
page_tips: 'tips'
url_tips: 'http://www.reddit.com/r/mysubreddit/wiki/tips'
+296
View File
@@ -0,0 +1,296 @@
values:
username:
regex: '(@[\w-]{3,20})'
amount:
regex: '([0-9]{1,9}(?:\.[0-9]{0,16})?)'
keywords:
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)'
tip_init:
regex: '(/u/mybotuser|mybotuser)'
actions:
givetip:
enabled: true
public: true
simple: false
regex:
a:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_AMOUNT}(\s+){REGEX_COIN}'
rg_amount: 4
rg_keyword: 0
rg_address: 0
rg_to_user: 0
rg_coin: 6
rg_fiat: 0
aa:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_KEYWORD}(\s+){REGEX_COIN}'
rg_amount: 0
rg_keyword: 4
rg_address: 0
rg_to_user: 0
rg_coin: 6
rg_fiat: 0
b:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_ADDRESS}(\s+){REGEX_AMOUNT}(\s+){REGEX_COIN}'
rg_amount: 6
rg_keyword: 0
rg_address: 4
rg_to_user: 0
rg_coin: 8
rg_fiat: 0
bb:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_ADDRESS}(\s+){REGEX_KEYWORD}(\s+){REGEX_COIN}'
rg_amount: 0
rg_keyword: 6
rg_address: 4
rg_to_user: 0
rg_coin: 8
rg_fiat: 0
bbb:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_ADDRESS}(\s+){REGEX_COIN}{REGEX_AMOUNT}'
rg_amount: 7
rg_keyword: 0
rg_address: 4
rg_to_user: 0
rg_coin: 6
rg_fiat: 0
bbbb:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_ADDRESS}(\s+){REGEX_COIN}{REGEX_KEYWORD}'
rg_amount: 0
rg_keyword: 7
rg_address: 4
rg_to_user: 0
rg_coin: 6
rg_fiat: 0
c:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_USER}(\s+){REGEX_AMOUNT}(\s+){REGEX_COIN}'
rg_amount: 6
rg_keyword: 0
rg_address: 0
rg_to_user: 4
rg_coin: 8
rg_fiat: 0
cc:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_USER}(\s+){REGEX_KEYWORD}(\s+){REGEX_COIN}'
rg_amount: 0
rg_keyword: 6
rg_address: 0
rg_to_user: 4
rg_coin: 8
rg_fiat: 0
ccc:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_USER}(\s+){REGEX_COIN}{REGEX_AMOUNT}'
rg_amount: 7
rg_keyword: 0
rg_address: 0
rg_to_user: 4
rg_coin: 6
rg_fiat: 0
cccc:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_USER}(\s+){REGEX_COIN}{REGEX_KEYWORD}'
rg_amount: 0
rg_keyword: 7
rg_address: 0
rg_to_user: 4
rg_coin: 6
rg_fiat: 0
d:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_FIAT}{REGEX_AMOUNT}(\s+){REGEX_COIN}'
rg_amount: 5
rg_keyword: 0
rg_address: 0
rg_to_user: 0
rg_coin: 7
rg_fiat: 4
dd:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_FIAT}{REGEX_KEYWORD}(\s+){REGEX_COIN}'
rg_amount: 0
rg_keyword: 5
rg_address: 0
rg_to_user: 0
rg_coin: 7
rg_fiat: 4
e:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_ADDRESS}(\s+){REGEX_FIAT}{REGEX_AMOUNT}(\s+){REGEX_COIN}'
rg_amount: 7
rg_keyword: 0
rg_address: 4
rg_to_user: 0
rg_coin: 9
rg_fiat: 6
ee:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_ADDRESS}(\s+){REGEX_FIAT}{REGEX_KEYWORD}(\s+){REGEX_COIN}'
rg_amount: 0
rg_keyword: 7
rg_address: 4
rg_to_user: 0
rg_coin: 9
rg_fiat: 6
f:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_USER}(\s+){REGEX_FIAT}{REGEX_AMOUNT}(\s+){REGEX_COIN}'
rg_amount: 7
rg_keyword: 0
rg_address: 0
rg_to_user: 4
rg_coin: 9
rg_fiat: 6
ff:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_USER}(\s+){REGEX_FIAT}{REGEX_KEYWORD}(\s+){REGEX_COIN}'
rg_amount: 0
rg_keyword: 7
rg_address: 0
rg_to_user: 4
rg_coin: 9
rg_fiat: 6
g:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_COIN}{REGEX_AMOUNT}'
rg_amount: 5
rg_keyword: 0
rg_address: 0
rg_to_user: 0
rg_coin: 4
rg_fiat: 0
gg:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_COIN}{REGEX_KEYWORD}'
rg_amount: 0
rg_keyword: 5
rg_address: 0
rg_to_user: 0
rg_coin: 4
rg_fiat: 0
ggg:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_FIAT}{REGEX_AMOUNT}'
rg_amount: 5
rg_keyword: 0
rg_address: 0
rg_to_user: 0
rg_coin: 0
rg_fiat: 4
gggg:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_FIAT}{REGEX_KEYWORD}'
rg_amount: 0
rg_keyword: 5
rg_address: 0
rg_to_user: 0
rg_coin: 0
rg_fiat: 4
ggggg:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_KEYWORD}'
rg_amount: 0
rg_keyword: 4
rg_address: 0
rg_to_user: 0
rg_coin: 0
rg_fiat: 0
h:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_USER}(\s+){REGEX_FIAT}{REGEX_AMOUNT}'
rg_amount: 7
rg_keyword: 0
rg_address: 0
rg_to_user: 4
rg_coin: 0
rg_fiat: 6
hh:
value: '(\+){REGEX_TIP_INIT}(\s+){REGEX_USER}(\s+){REGEX_FIAT}{REGEX_KEYWORD}'
rg_amount: 0
rg_keyword: 7
rg_address: 0
rg_to_user: 4
rg_coin: 0
rg_fiat: 6
info:
enabled: true
public: false
simple: true
regex: '(\+)(info|balance)'
register:
enabled: true
public: false
simple: true
regex: '(\+)(register)'
accept:
enabled: true
public: false
simple: true
regex: '(\+)(accept)'
decline:
enabled: true
public: false
simple: true
regex: '(\+)(decline)'
withdraw:
enabled: true
public: false
simple: false
regex:
a:
value: '(\+)(withdraw)(\s+){REGEX_ADDRESS}(\s+){REGEX_AMOUNT}(\s+){REGEX_COIN}'
rg_amount: 6
rg_keyword: 0
rg_address: 4
rg_to_user: 0
rg_coin: 8
rg_fiat: 0
aa:
value: '(\+)(withdraw)(\s+){REGEX_ADDRESS}(\s+){REGEX_KEYWORD}(\s+){REGEX_COIN}'
rg_amount: 0
rg_keyword: 6
rg_address: 4
rg_to_user: 0
rg_coin: 8
rg_fiat: 0
aaa:
value: '(\+)(withdraw)(\s+){REGEX_ADDRESS}(\s+){REGEX_COIN}{REGEX_AMOUNT}'
rg_amount: 7
rg_keyword: 0
rg_address: 4
rg_to_user: 0
rg_coin: 6
rg_fiat: 0
aaaa:
value: '(\+)(withdraw)(\s+){REGEX_ADDRESS}(\s+){REGEX_COIN}{REGEX_KEYWORD}'
rg_amount: 0
rg_keyword: 7
rg_address: 4
rg_to_user: 0
rg_coin: 6
rg_fiat: 0
b:
value: '(\+)(withdraw)(\s+){REGEX_ADDRESS}(\s+){REGEX_FIAT}{REGEX_AMOUNT}(\s+){REGEX_COIN}'
rg_amount: 7
rg_keyword: 0
rg_address: 4
rg_to_user: 0
rg_coin: 9
rg_fiat: 6
bb:
value: '(\+)(withdraw)(\s+){REGEX_ADDRESS}(\s+){REGEX_FIAT}{REGEX_KEYWORD}(\s+){REGEX_COIN}'
rg_amount: 0
rg_keyword: 7
rg_address: 4
rg_to_user: 0
rg_coin: 9
rg_fiat: 6
history:
enabled: true
public: false
simple: true
regex: '(\+)(history)'
redeem:
enabled: true
public: false
simple: false
regex:
a:
value: '(\+)(redeem)(\s+){REGEX_COIN}'
rg_amount: 0
rg_keyword: 0
rg_address: 0
rg_to_user: 0
rg_coin: 4
rg_fiat: 0
rates:
enabled: true
public: false
simple: true
regex: '(\+)(rates)'
View File
File diff suppressed because it is too large Load Diff
+246
View File
@@ -0,0 +1,246 @@
"""
This file is part of ALTcointip.
ALTcointip is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ALTcointip is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ALTcointip. If not, see <http://www.gnu.org/licenses/>.
"""
import logging, re, time
from pifkoin.bitcoind import Bitcoind, BitcoindException
from httplib import CannotSendRequest
lg = logging.getLogger('cointipbot')
class CtbCoin(object):
"""
Coin class for cointip bot
"""
conn = None
conf = None
def __init__(self, _conf = None):
"""
Initialize CtbCoin with given parameters. _conf is a coin config dictionary defined in conf/coins.yml
"""
# verify _conf is a config dictionary
if not _conf or not hasattr(_conf, 'name') or not hasattr(_conf, 'config_file') or not hasattr(_conf, 'txfee'):
raise Exception("CtbCoin::__init__(): _conf is empty or invalid")
self.conf = _conf
# connect to coin daemon
try:
lg.debug("CtbCoin::__init__(): connecting to %s...", self.conf.name)
self.conn = Bitcoind(self.conf.config_file, rpcserver=self.conf.config_rpcserver)
except BitcoindException as e:
lg.error("CtbCoin::__init__(): error connecting to %s using %s: %s", self.conf.name, self.conf.config_file, e)
raise
lg.info("CtbCoin::__init__():: connected to %s", self.conf.name)
time.sleep(0.5)
# set transaction fee
lg.info("Setting tx fee of %f", self.conf.txfee)
self.conn.settxfee(self.conf.txfee)
def getbalance(self, _user = None, _minconf = None):
"""
Get user's tip or withdraw balance. _minconf is number of confirmations to use.
Returns (float) balance
"""
lg.debug("CtbCoin::getbalance(%s, %s)", _user, _minconf)
user = self.verify_user(_user=_user)
minconf = self.verify_minconf(_minconf=_minconf)
balance = float(0)
try:
balance = self.conn.getbalance(user, minconf)
except BitcoindException as e:
lg.error("CtbCoin.getbalance(): error getting %s (minconf=%s) balance for %s: %s", self.conf.name, minconf, user, e)
raise
time.sleep(0.5)
return float(balance)
def sendtouser(self, _userfrom = None, _userto = None, _amount = None, _minconf = 1):
"""
Transfer (move) coins to user
Returns (bool)
"""
lg.debug("CtbCoin::sendtouser(%s, %s, %.9f)", _userfrom, _userto, _amount)
userfrom = self.verify_user(_user=_userfrom)
userto = self.verify_user(_user=_userto)
amount = self.verify_amount(_amount=_amount)
# send request to coin daemon
try:
lg.info("CtbCoin::sendtouser(): moving %.9f %s from %s to %s", amount, self.conf.name, userfrom, userto)
result = self.conn.move(userfrom, userto, amount)
time.sleep(0.5)
except Exception as e:
lg.error("CtbCoin::sendtouser(): error moving %.9f %s from %s to %s: %s", amount, self.conf.name, userfrom, userto, e)
return False
time.sleep(0.5)
return True
def sendtoaddr(self, _userfrom = None, _addrto = None, _amount = None):
"""
Send coins to address
Returns (string) txid
"""
lg.debug("CtbCoin::sendtoaddr(%s, %s, %.9f)", _userfrom, _addrto, _amount)
userfrom = self.verify_user(_user=_userfrom)
addrto = self.verify_addr(_addr=_addrto)
amount = self.verify_amount(_amount=_amount)
minconf = self.verify_minconf(_minconf=self.conf.minconf.withdraw)
txid = ""
# send request to coin daemon
try:
lg.info("CtbCoin::sendtoaddr(): sending %.9f %s from %s to %s", amount, self.conf.name, userfrom, addrto)
# Unlock wallet, if applicable
if hasattr(self.conf, 'walletpassphrase'):
lg.debug("CtbCoin::sendtoaddr(): unlocking wallet...")
self.conn.walletpassphrase(self.conf.walletpassphrase, 1)
# Perform transaction
lg.debug("CtbCoin::sendtoaddr(): calling sendfrom()...")
txid = self.conn.sendfrom(userfrom, addrto, amount, minconf)
# Lock wallet, if applicable
if hasattr(self.conf, 'walletpassphrase'):
lg.debug("CtbCoin::sendtoaddr(): locking wallet...")
self.conn.walletlock()
except Exception as e:
lg.error("CtbCoin::sendtoaddr(): error sending %.9f %s from %s to %s: %s", amount, self.conf.name, userfrom, addrto, e)
raise
time.sleep(0.5)
return str(txid)
def validateaddr(self, _addr = None):
"""
Verify that _addr is a valid coin address
Returns (bool)
"""
lg.debug("CtbCoin::validateaddr(%s)", _addr)
addr = self.verify_addr(_addr=_addr)
addr_valid = self.conn.validateaddress(addr)
time.sleep(0.5)
if not addr_valid.has_key('isvalid') or not addr_valid['isvalid']:
lg.debug("CtbCoin::validateaddr(%s): not valid", addr)
return False
else:
lg.debug("CtbCoin::validateaddr(%s): valid", addr)
return True
def getnewaddr(self, _user = None):
"""
Generate a new address for _user
Returns (string) address
"""
user = self.verify_user(_user=_user)
addr = ""
counter = 0
while True:
try:
# Unlock wallet for keypoolrefill
if hasattr(self.conf, 'walletpassphrase'):
self.conn.walletpassphrase(self.conf.walletpassphrase, 1)
# Generate new address
addr = self.conn.getnewaddress(user)
# Lock wallet
if hasattr(self.conf, 'walletpassphrase'):
self.conn.walletlock()
if not addr:
raise Exception("CtbCoin::getnewaddr(%s): empty addr", user)
time.sleep(0.1)
return str(addr)
except BitcoindException as e:
lg.error("CtbCoin::getnewaddr(%s): BitcoindException: %s", user, e)
raise
except CannotSendRequest as e:
if counter < 3:
lg.warning("CtbCoin::getnewaddr(%s): CannotSendRequest, retrying")
counter += 1
time.sleep(10)
continue
else:
raise
except Exception as e:
if str(e) == "timed out" and counter < 3:
lg.warning("CtbCoin::getnewaddr(%s): timed out, retrying")
counter += 1
time.sleep(10)
continue
else:
lg.error("CtbCoin::getnewaddr(%s): Exception: %s", user, e)
raise
def verify_user(self, _user = None):
"""
Verify and return a username
"""
if not _user or not type(_user) in [str, unicode]:
raise Exception("CtbCoin::verify_user(): _user wrong type (%s) or empty (%s)", type(_user), _user)
return str(_user.lower())
def verify_addr(self, _addr = None):
"""
Verify and return coin address
"""
if not _addr or not type(_addr) in [str, unicode]:
raise Exception("CtbCoin::verify_addr(): _addr wrong type (%s) or empty (%s)", type(_addr),_addr)
return re.escape(str(_addr))
def verify_amount(self, _amount = None):
"""
Verify and return amount
"""
if not _amount or not type(_amount) in [int, float] or not _amount > 0:
raise Exception("CtbCoin::verify_amount(): _amount wrong type (%s), empty, or negative (%s)", type(_amount), _amount)
return _amount
def verify_minconf(self, _minconf = None):
"""
Verify and return minimum number of confirmations
"""
if not _minconf or not type(_minconf) == int or not _minconf >= 0:
raise Exception("CtbCoin::verify_minconf(): _minconf wrong type (%s), empty, or negative (%s)", type(_minconf), _minconf)
return _minconf
+33
View File
@@ -0,0 +1,33 @@
"""
This file is part of ALTcointip.
ALTcointip is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ALTcointip is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ALTcointip. If not, see <http://www.gnu.org/licenses/>.
"""
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, ForeignKey, Numeric, UnicodeText
from sqlalchemy.pool import SingletonThreadPool
class CointipBotDatabase:
metadata = MetaData()
def __init__(self, dsn_url):
'''Pass a DSN URL conforming to the SQLAlchemy API'''
self.dsn_url = dsn_url
def connect(self):
'''Return a connection object'''
engine = create_engine(self.dsn_url, echo_pool=True, poolclass=SingletonThreadPool)
self.metadata.create_all(engine)
return engine
+128
View File
@@ -0,0 +1,128 @@
"""
This file is part of ALTcointip.
ALTcointip is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ALTcointip is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ALTcointip. If not, see <http://www.gnu.org/licenses/>.
"""
import json, logging, urllib2, httplib
lg = logging.getLogger('cointipbot')
class CtbExchange(object):
"""
Exchange class for cointip bot
"""
conf = None
def __init__(self, _conf = None):
"""
Initialize CtbExchange with given parameters.
_conf is an exchange config dictionary defind in conf/exchanges.yml
"""
if not _conf or not hasattr(_conf, 'urlpaths') or not hasattr(_conf, 'jsonpaths') or not hasattr(_conf, 'coinlist') or not hasattr(_conf, 'fiatlist'):
raise Exception("CtbExchange::__init__(): _conf is empty or invalid")
self.conf = _conf
# Convert coinlist and fiatlist values to lowercase
self.conf.coinlist = map(lambda x:x.lower(), self.conf.coinlist)
self.conf.fiatlist = map(lambda x:x.lower(), self.conf.fiatlist)
lg.debug("CtbExchange::__init__(): initialized exchange %s" % self.conf.domain)
def supports(self, _name = None):
"""
Return True if exchange supports given coin/fiat _name
"""
if not _name or not type(_name) in [str, unicode]:
raise Exception("CtbExchange::supports(): _name is empty or wrong type")
name = str(_name).lower()
if name in self.conf.coinlist or name in self.conf.fiatlist:
#lg.debug("CtbExchange::supports(%s): YES" % name)
return True
else:
#lg.debug("CtbExchange::supports(%s): NO" % name)
return False
def supports_pair(self, _name1 = None, _name2 = None):
"""
Return true of exchange supports given coin/fiat pair
"""
return self.supports(_name=_name1) and self.supports(_name=_name2)
def get_ticker_value(self, _name1 = None, _name2 = None):
"""
Return (float) ticker value for given pair
"""
if _name1 == _name2:
return float(1)
if not self.supports_pair(_name1=_name1, _name2=_name2):
raise Exception("CtbExchange::get_ticker_value(%s, %s, %s): pair not supported" % (self.conf.domain, _name1, _name2))
results = []
for myurlpath in self.conf.urlpaths:
for myjsonpath in self.conf.jsonpaths:
toreplace = {'{THING_FROM}': _name1.upper() if self.conf.uppercase else _name1.lower(), '{THING_TO}': _name2.upper() if self.conf.uppercase else _name2.lower()}
for t in toreplace:
myurlpath = myurlpath.replace(t, toreplace[t])
myjsonpath = myjsonpath.replace(t, toreplace[t])
try:
lg.debug("CtbExchange::get_ticker_value(%s, %s, %s): calling %s to get %s...", self.conf.domain, _name1, _name2, myurlpath, myjsonpath)
if self.conf.https:
connection = httplib.HTTPSConnection(self.conf.domain, timeout=5)
connection.request("GET", myurlpath, {}, {})
else:
connection = httplib.HTTPConnection(self.conf.domain, timeout=5)
connection.request("GET", myurlpath)
response = json.loads(connection.getresponse().read())
result = xpath_get(response, myjsonpath)
lg.debug("CtbExchange::get_ticker_value(%s, %s, %s): result: %.6f", self.conf.domain, _name1, _name2, float(result))
results.append( float(result) )
except urllib2.URLError as e:
lg.error("CtbExchange::get_ticker_value(%s, %s, %s): %s", self.conf.domain, _name1, _name2, e)
return 0.0
except urllib2.HTTPError as e:
lg.error("CtbExchange::get_ticker_value(%s, %s, %s): %s", self.conf.domain, _name1, _name2, e)
return 0.0
except Exception as e:
lg.error("CtbExchange::get_ticker_value(%s, %s, %s): %s", self.conf.domain, _name1, _name2, e)
return 0.0
# Return average of all responses
return ( sum(results) / float(len(results)) )
def xpath_get(mydict, path):
elem = mydict
try:
for x in path.strip('.').split('.'):
try:
x = int(x)
elem = elem[x]
except ValueError:
elem = elem.get(x)
except:
pass
return elem
+25
View File
@@ -0,0 +1,25 @@
"""
This file is part of ALTcointip.
ALTcointip is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ALTcointip is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ALTcointip. If not, see <http://www.gnu.org/licenses/>.
"""
import logging
class LevelFilter(logging.Filter):
def __init__(self, level):
self.level = level
def filter(self, record):
return record.levelno >= self.level
+203
View File
@@ -0,0 +1,203 @@
"""
This file is part of ALTcointip.
ALTcointip is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ALTcointip is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ALTcointip. If not, see <http://www.gnu.org/licenses/>.
"""
import ctb_user
import logging, time
from requests.exceptions import HTTPError, ConnectionError, Timeout
from praw.errors import ExceptionList, APIException, InvalidCaptcha, InvalidUser, RateLimitExceeded
from socket import timeout
lg = logging.getLogger('cointipbot')
def praw_call(prawFunc, *extraArgs, **extraKwArgs):
"""
Call prawFunc() with extraArgs and extraKwArgs
Retry if Reddit is down
"""
while True:
try:
res = prawFunc(*extraArgs, **extraKwArgs)
return res
except (HTTPError, ConnectionError, Timeout, timeout) as e:
if str(e) in [ "400 Client Error: Bad Request", "403 Client Error: Forbidden", "404 Client Error: Not Found" ]:
lg.warning("praw_call(): Reddit returned error (%s)", e)
return False
else:
lg.warning("praw_call(): Reddit returned error (%s), sleeping...", e)
time.sleep(30)
pass
except APIException as e:
if str(e) == "(DELETED_COMMENT) `that comment has been deleted` on field `parent`":
lg.warning("praw_call(): deleted comment: %s", e)
return False
else:
raise
except RateLimitExceeded as e:
lg.warning("praw_call(): rate limit exceeded, sleeping for %s seconds", e.sleep_time)
time.sleep(e.sleep_time)
time.sleep(1)
pass
except Exception as e:
raise
return True
def reddit_get_parent_author(comment, reddit, ctb):
"""
Return author of comment's parent comment
"""
lg.debug("> reddit_get_parent_author()")
while True:
try:
parentpermalink = comment.permalink.replace(comment.id, comment.parent_id[3:])
commentlinkid = None
if hasattr(comment, 'link_id'):
commentlinkid = comment.link_id[3:]
else:
comment2 = reddit.get_submission(comment.permalink).comments[0]
commentlinkid = comment2.link_id[3:]
parentid = comment.parent_id[3:]
if commentlinkid == parentid:
parentcomment = reddit.get_submission(parentpermalink)
else:
parentcomment = reddit.get_submission(parentpermalink).comments[0]
if parentcomment and hasattr(parentcomment, 'author') and parentcomment.author:
lg.debug("< reddit_get_parent_author(%s) -> %s", comment.id, parentcomment.author.name)
return parentcomment.author.name
else:
lg.warning("< reddit_get_parent_author(%s) -> NONE", comment.id)
return None
except (IndexError, APIException) as e:
lg.warning("reddit_get_parent_author(): couldn't get author: %s", e)
return None
except (HTTPError, RateLimitExceeded, timeout) as e:
if str(e) in [ "400 Client Error: Bad Request", "403 Client Error: Forbidden", "404 Client Error: Not Found" ]:
lg.warning("reddit_get_parent_author(): Reddit returned error (%s)", e)
return None
else:
lg.warning("reddit_get_parent_author(): Reddit returned error (%s), sleeping...", e)
time.sleep(ctb.conf.misc.times.sleep_seconds)
pass
except Exception as e:
raise
lg.error("reddit_get_parent_author(): returning None (should not get here)")
return None
def get_value(conn, param0=None):
"""
Fetch a value from t_values table
"""
lg.debug("> get_value()")
if param0 == None:
raise Exception("get_value(): param0 == None")
value = None
sql = "SELECT value0 FROM t_values WHERE param0 = %s"
try:
mysqlrow = conn.execute(sql, (param0)).fetchone()
if mysqlrow == None:
lg.error("get_value(): query <%s> didn't return any rows", sql % (param0))
return None
value = mysqlrow['value0']
except Exception, e:
lg.error("get_value(): error executing query <%s>: %s", sql % (param0), e)
raise
lg.debug("< get_value() DONE (%s)", value)
return value
def set_value(conn, param0=None, value0=None):
"""
Set a value in t_values table
"""
lg.debug("> set_value(%s, %s)", param0, value0)
if param0 == None or value0 == None:
raise Exception("set_value(): param0 == None or value0 == None")
sql = "REPLACE INTO t_values (param0, value0) VALUES (%s, %s)"
try:
mysqlexec = conn.execute(sql, (param0, value0))
if mysqlexec.rowcount <= 0:
lg.error("set_value(): query <%s> didn't affect any rows", sql % (param0, value0))
return False
except Exception, e:
lg.error("set_value: error executing query <%s>: %s", sql % (param0, value0), e)
raise
lg.debug("< set_value() DONE")
return True
def add_coin(coin, db, coins):
"""
Add new coin address to each user
"""
lg.debug("> add_coin(%s)", coin)
sql_select = "SELECT username FROM t_users WHERE username NOT IN (SELECT username FROM t_addrs WHERE coin = %s) ORDER BY username"
sql_insert = "REPLACE INTO t_addrs (username, coin, address) VALUES (%s, %s, %s)"
try:
mysqlsel = db.execute(sql_select, (coin))
for m in mysqlsel:
# Generate new coin address for user
new_addr = coins[coin].getnewaddr(_user=m['username'])
lg.info("add_coin(): got new address %s for %s", new_addr, m['username'])
# Add new coin address to MySQL
mysqlins = db.execute(sql_insert, (m['username'].lower(), coin, new_addr))
if mysqlins.rowcount <= 0:
raise Exception("add_coin(%s): rowcount <= 0 when executing <%s>", coin, sql_insert % (m['username'].lower(), coin, new_addr))
time.sleep(1)
except Exception, e:
lg.error("add_coin(%s): error: %s", coin, e)
raise
lg.debug("< add_coin(%s) DONE", coin)
return True
class DotDict(object):
def __init__(self, d):
for a, b in d.items():
if isinstance(b, (list, tuple)):
setattr(self, a, [DotDict(x) if isinstance(x, dict) else x for x in b])
else:
setattr(self, a, DotDict(b) if isinstance(b, dict) else b)
def __getitem__(self, val):
return getattr(self, val)
def has_key(self, key):
return hasattr(self, key)
+286
View File
@@ -0,0 +1,286 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
This file is part of ALTcointip.
ALTcointip is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ALTcointip is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ALTcointip. If not, see <http://www.gnu.org/licenses/>.
"""
import logging, re, time
import ctb_misc
lg = logging.getLogger('cointipbot')
def update_stats(ctb=None):
"""
Update stats wiki page
"""
stats = ""
if not ctb.conf.reddit.stats.enabled:
return None
for s in sorted(vars(ctb.conf.db.sql.globalstats)):
lg.debug("update_stats(): getting stats for '%s'" % s)
sql = ctb.conf.db.sql.globalstats[s].query
stats += "\n\n### %s\n\n" % ctb.conf.db.sql.globalstats[s].name
stats += "%s\n\n" % ctb.conf.db.sql.globalstats[s].desc
mysqlexec = ctb.db.execute(sql)
if mysqlexec.rowcount <= 0:
lg.warning("update_stats(): query <%s> returned nothing" % ctb.conf.db.sql.globalstats[s].query)
continue
if ctb.conf.db.sql.globalstats[s].type == "line":
m = mysqlexec.fetchone()
k = mysqlexec.keys()[0]
value = format_value(m, k, '', ctb)
stats += "%s = **%s**\n" % (k, value)
elif ctb.conf.db.sql.globalstats[s].type == "table":
stats += ("|".join(mysqlexec.keys())) + "\n"
stats += ("|".join([":---"] * len(mysqlexec.keys()))) + "\n"
for m in mysqlexec:
values = []
for k in mysqlexec.keys():
values.append(format_value(m, k, '', ctb))
stats += ("|".join(values)) + "\n"
else:
lg.error("update_stats(): don't know what to do with type '%s'" % ctb.conf.db.sql.globalstats[s].type)
return False
stats += "\n"
lg.debug("update_stats(): updating subreddit '%s', page '%s'" % (ctb.conf.reddit.stats.subreddit, ctb.conf.reddit.stats.page))
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")
def update_tips(ctb=None):
"""
Update page listing all tips
"""
if not ctb.conf.reddit.stats.enabled:
return None
# Start building stats page
tip_list = "### All Completed Tips\n\n"
q = ctb.db.execute(ctb.conf.db.sql.tips.sql_set)
tips = ctb.db.execute(ctb.conf.db.sql.tips.sql_list, (ctb.conf.db.sql.tips.limit))
tip_list += ("|".join(tips.keys())) + "\n"
tip_list += ("|".join([":---"] * len(tips.keys()))) + "\n"
# Build tips table
for t in tips:
values = []
for k in tips.keys():
values.append(format_value(t, k, '', ctb, compact=True))
tip_list += ("|".join(values)) + "\n"
lg.debug("update_tips(): updating subreddit '%s', page '%s'" % (ctb.conf.reddit.stats.subreddit, ctb.conf.reddit.stats.page_tips))
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")
return True
def update_all_user_stats(ctb=None):
"""
Update individual user stats for all uers
"""
if not ctb.conf.reddit.stats.enabled:
lg.error('update_all_user_stats(): stats are not enabled in config.yml')
return None
users = ctb.db.execute(ctb.conf.db.sql.userstats.users)
for u in users:
update_user_stats(ctb=ctb, username=u['username'])
def update_user_stats(ctb=None, username=None):
"""
Update individual user stats for given username
"""
if not ctb.conf.reddit.stats.enabled:
return None
# List of coins
coins_q = ctb.db.execute(ctb.conf.db.sql.userstats.coins)
coins = []
for c in coins_q:
coins.append(c['coin'])
# List of fiat
fiat_q = ctb.db.execute(ctb.conf.db.sql.userstats.fiat)
fiat = []
for f in fiat_q:
fiat.append(f['fiat'])
# Start building stats page
user_stats = "### Tipping Summary for /u/%s\n\n" % username
page = ctb.conf.reddit.stats.page + '_' + username
# Total Tipped
user_stats += "#### Total Tipped (Fiat)\n\n"
user_stats += "fiat|total\n:---|---:\n"
total_tipped = []
for f in fiat:
mysqlexec = ctb.db.execute(ctb.conf.db.sql.userstats.total_tipped_fiat, (username, f))
total_tipped_fiat = mysqlexec.fetchone()
if total_tipped_fiat['total_fiat'] != None:
user_stats += "**%s**|%s %.2f\n" % (f, ctb.conf.fiat[f].symbol, total_tipped_fiat['total_fiat'])
total_tipped.append("%s%.2f" % (ctb.conf.fiat[f].symbol, total_tipped_fiat['total_fiat']))
user_stats += "\n"
user_stats += "#### Total Tipped (Coins)\n\n"
user_stats += "coin|total\n:---|---:\n"
for c in coins:
mysqlexec = ctb.db.execute(ctb.conf.db.sql.userstats.total_tipped_coin, (username, c))
total_tipped_coin = mysqlexec.fetchone()
if total_tipped_coin['total_coin'] != None:
user_stats += "**%s**|%s %.6f\n" % (c, ctb.conf.coins[c].symbol, total_tipped_coin['total_coin'])
user_stats += "\n"
# Total received
user_stats += "#### Total Received (Fiat)\n\n"
user_stats += "fiat|total\n:---|---:\n"
total_received = []
for f in fiat:
mysqlexec = ctb.db.execute(ctb.conf.db.sql.userstats.total_received_fiat, (username, f))
total_received_fiat = mysqlexec.fetchone()
if total_received_fiat['total_fiat'] != None:
user_stats += "**%s**|%s %.2f\n" % (f, ctb.conf.fiat[f].symbol, total_received_fiat['total_fiat'])
total_received.append("%s%.2f" % (ctb.conf.fiat[f].symbol, total_received_fiat['total_fiat']))
user_stats += "\n"
user_stats += "#### Total Received (Coins)\n\n"
user_stats += "coin|total\n:---|---:\n"
for c in coins:
mysqlexec = ctb.db.execute(ctb.conf.db.sql.userstats.total_received_coin, (username, c))
total_received_coin = mysqlexec.fetchone()
if total_received_coin['total_coin'] != None:
user_stats += "**%s**|%s %.6f\n" % (c, ctb.conf.coins[c].symbol, total_received_coin['total_coin'])
user_stats += "\n"
# History
user_stats += "#### History\n\n"
history = ctb.db.execute(ctb.conf.db.sql.userstats.history, (username, username))
user_stats += ("|".join(history.keys())) + "\n"
user_stats += ("|".join([":---"] * len(history.keys()))) + "\n"
# Build history table
num_tipped = 0
num_received = 0
for m in history:
if m['state'] == 'completed':
if m['from_user'].lower() == username.lower():
num_tipped += 1
elif m['to_user'].lower() == username.lower():
num_received += 1
values = []
for k in history.keys():
values.append(format_value(m, k, username, ctb))
user_stats += ("|".join(values)) + "\n"
# Submit changes
lg.debug("update_user_stats(): updating subreddit '%s', page '%s'" % (ctb.conf.reddit.stats.subreddit, page))
ctb_misc.praw_call(ctb.reddit.edit_wiki_page, ctb.conf.reddit.stats.subreddit, page, user_stats, "Update by ALTcointip bot")
# Update user flair on subreddit
if ctb.conf.reddit.stats.userflair and ( len(total_tipped) > 0 or len(total_received) > 0 ):
flair = ""
if len(total_tipped) > 0:
flair += "tipped[" + '|'.join(total_tipped) + "]"
flair += " (%d)" % num_tipped
if len(total_received) > 0:
if len(total_tipped) > 0:
flair += " / "
flair += "received[" + '|'.join(total_received) + "]"
flair += " (%d)" % num_received
lg.debug("update_user_stats(): updating flair for %s (%s)", username, flair)
r = ctb_misc.praw_call(ctb.reddit.get_subreddit, ctb.conf.reddit.stats.subreddit)
res = ctb_misc.praw_call(r.set_flair, username, flair, '')
lg.debug(res)
return True
def format_value(m, k, username, ctb, compact=False):
"""
Format value for display based on its type
m[k] is the value, k is the database row name
"""
if not m[k]:
return '-'
# Format cryptocoin
if type(m[k]) == float and k.find("coin") > -1:
coin_symbol = ctb.conf.coins[m['coin']].symbol
return "%s&nbsp;%.5g" % (coin_symbol, m[k])
# Format fiat
elif type(m[k]) == float and ( k.find("fiat") > -1 or k.find("usd") > -1 ):
fiat_symbol = ctb.conf.fiat[m['fiat']].symbol
return "%s&nbsp;%.2f" % (fiat_symbol, m[k])
# Format username
elif k.find("user") > -1 and type( m[k] ) in [str, unicode]:
if compact:
return ("**/u/%s**" % m[k]) if m[k].lower() == username.lower() else ("/u/%s" % m[k])
else:
un = ("**%s**" % m[k]) if m[k].lower() == username.lower() else m[k]
toreturn = "[%s](/u/%s)" % (un, re.escape(m[k]))
if m[k].lower() != username.lower():
toreturn += "^[[stats]](/r/%s/wiki/%s_%s)" % (ctb.conf.reddit.stats.subreddit, ctb.conf.reddit.stats.page, m[k])
return toreturn
# Format address
elif k.find("addr") > -1:
displayaddr = m[k][:6] + "..." + m[k][-5:]
return "[%s](%s%s)" % (displayaddr, ctb.conf.coins[m['coin']].explorer.address, m[k])
# Format state
elif k.find("state") > -1:
if m[k] == 'completed':
return unicode('✓', 'utf8')
else:
return m[k]
# Format type
elif k.find("type") > -1:
if m[k] == 'givetip':
return 'tip'
if compact:
if m[k] == 'withdraw':
return 'w'
if m[k] == 'redeem':
return 'r'
# Format subreddit
elif k.find("subreddit") > -1:
return "/r/%s" % m[k]
# Format link
elif k.find("link") > -1:
return "[link](%s)" % m[k]
# Format time
elif k.find("utc") > -1:
return "%s" % time.strftime('%Y-%m-%d', time.localtime(m[k]))
# It's something else
else:
return str(m[k])
+324
View File
@@ -0,0 +1,324 @@
"""
This file is part of ALTcointip.
ALTcointip is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ALTcointip is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ALTcointip. If not, see <http://www.gnu.org/licenses/>.
"""
import ctb_misc
import logging, time, praw, re
lg = logging.getLogger('cointipbot')
class CtbUser(object):
"""
User class for cointip bot
"""
# Basic properties
name=None
giftamount=None
joindate=None
addr={}
banned=False
# Objects
prawobj=None
ctb=None
def __init__(self, name=None, redditobj=None, ctb=None):
"""
Initialize CtbUser object with given parameters
"""
lg.debug("> CtbUser::__init__(%s)", name)
if not bool(name):
raise Exception("CtbUser::__init__(): name must be set")
self.name = name
if not bool(ctb):
raise Exception("CtbUser::__init__(): ctb must be set")
self.ctb = ctb
if bool(redditobj):
self.prawobj = redditobj
# Determine if user is banned
if ctb.conf.reddit.banned_users:
if ctb.conf.reddit.banned_users.method == 'subreddit':
for u in ctb.reddit.get_banned(ctb.conf.reddit.banned_users.subreddit):
if self.name.lower() == u.name.lower():
self.banned = True
elif ctb.conf.reddit.banned_users.method == 'list':
for u in ctb.conf.reddit.banned_users.list:
if self.name.lower() == u.lower():
self.banned = True
else:
lg.warning("CtbUser::__init__(): invalid method '%s' in banned_users config" % ctb.conf.reddit.banned_users.method)
lg.debug("< CtbUser::__init__(%s) DONE", name)
def __str__(self):
"""
Return string representation of self
"""
me = "<CtbUser: name=%s, giftamnt=%s, joindate=%s, addr=%s, redditobj=%s, banned=%s>"
me = me % (self.name, self.giftamount, self.joindate, self.addr, self.prawobj, self.banned)
return me
def get_balance(self, coin=None, kind=None):
"""
If coin is specified, return float with coin balance for user. Else, return a dict with balance of each coin for user.
"""
lg.debug("> CtbUser::balance(%s)", self.name)
if not bool(coin) or not bool(kind):
raise Exception("CtbUser::balance(%s): coin or kind not set" % self.name)
# Ask coin daemon for account balance
lg.info("CtbUser::balance(%s): getting %s %s balance", self.name, coin, kind)
balance = self.ctb.coins[coin].getbalance(_user=self.name, _minconf=self.ctb.conf.coins[coin].minconf[kind])
lg.debug("< CtbUser::balance(%s) DONE", self.name)
return float(balance)
def get_addr(self, coin=None):
"""
Return coin address of user
"""
lg.debug("> CtbUser::get_addr(%s, %s)", self.name, coin)
if hasattr(self.addr, coin):
return self.addr[coin]
sql = "SELECT address from t_addrs WHERE username = %s AND coin = %s"
mysqlrow = self.ctb.db.execute(sql, (self.name.lower(), coin.lower())).fetchone()
if not mysqlrow:
lg.debug("< CtbUser::get_addr(%s, %s) DONE (no)", self.name, coin)
return None
else:
self.addr[coin] = mysqlrow['address']
lg.debug("< CtbUser::get_addr(%s, %s) DONE (%s)", self.name, coin, self.addr[coin])
return self.addr[coin]
lg.debug("< CtbUser::get_addr(%s, %s) DONE (should never happen)", self.name, coin)
return None
def is_on_reddit(self):
"""
Return true if username exists Reddit. Also set prawobj pointer while at it.
"""
lg.debug("> CtbUser::is_on_reddit(%s)", self.name)
# Return true if prawobj is already set
if bool(self.prawobj):
lg.debug("< CtbUser::is_on_reddit(%s) DONE (yes)", self.name)
return True
try:
self.prawobj = ctb_misc.praw_call(self.ctb.reddit.get_redditor, self.name)
if self.prawobj:
return True
else:
return False
except Exception as e:
lg.debug("< CtbUser::is_on_reddit(%s) DONE (no)", self.name)
return False
lg.warning("< CtbUser::is_on_reddit(%s): returning None (shouldn't happen)", self.name)
return None
def is_registered(self):
"""
Return true if user is registered with CointipBot
"""
lg.debug("> CtbUser::is_registered(%s)", self.name)
try:
# First, check t_users table
sql = "SELECT * FROM t_users WHERE username = %s"
mysqlrow = self.ctb.db.execute(sql, (self.name.lower())).fetchone()
if not mysqlrow:
lg.debug("< CtbUser::is_registered(%s) DONE (no)", self.name)
return False
else:
# Next, check t_addrs table for whether user has correct number of coin addresses
sql_coins = "SELECT COUNT(*) AS count FROM t_addrs WHERE username = %s"
mysqlrow_coins = self.ctb.db.execute(sql_coins, (self.name.lower())).fetchone()
if int(mysqlrow_coins['count']) != len(self.ctb.coins):
if int(mysqlrow_coins['count']) == 0:
# Bot probably crashed during user registration process
# Delete user
lg.warning("CtbUser::is_registered(%s): deleting user, incomplete registration", self.name)
sql_delete = "DELETE FROM t_users WHERE username = %s"
mysql_res = self.ctb.db.execute(sql_delete, (self.name.lower()))
# User is not registered
return False
else:
raise Exception("CtbUser::is_registered(%s): user has %s coins but %s active" % (self.name, mysqlrow_coins['count'], len(self.ctb.coins)))
# Set some properties
self.giftamount = mysqlrow['giftamount']
# Done
lg.debug("< CtbUser::is_registered(%s) DONE (yes)", self.name)
return True
except Exception, e:
lg.error("CtbUser::is_registered(%s): error while executing <%s>: %s", self.name, sql % self.name.lower(), e)
raise
lg.warning("< CtbUser::is_registered(%s): returning None (shouldn't happen)", self.name)
return None
def tell(self, subj=None, msg=None, msgobj=None):
"""
Send a Reddit message to user
"""
lg.debug("> CtbUser::tell(%s)", self.name)
if not bool(subj) or not bool(msg):
raise Exception("CtbUser::tell(%s): subj or msg not set", self.name)
if not self.is_on_reddit():
raise Exception("CtbUser::tell(%s): not a Reddit user", self.name)
if bool(msgobj):
lg.debug("CtbUser::tell(%s): replying to message", msgobj.id)
ctb_misc.praw_call(msgobj.reply, msg)
else:
lg.debug("CtbUser::tell(%s): sending message", self.name)
ctb_misc.praw_call(self.prawobj.send_message, subj, msg)
lg.debug("< CtbUser::tell(%s) DONE", self.name)
return True
def register(self):
"""
Add user to database and generate coin addresses
"""
lg.debug("> CtbUser::register(%s)", self.name)
# Add user to database
try:
sql_adduser = "INSERT INTO t_users (username) VALUES (%s)"
mysqlexec = self.ctb.db.execute(sql_adduser, (self.name.lower()))
if mysqlexec.rowcount <= 0:
raise Exception("CtbUser::register(%s): rowcount <= 0 while executing <%s>" % ( self.name, sql_adduser % (self.name.lower()) ))
except Exception, e:
lg.error("CtbUser::register(%s): exception while executing <%s>: %s", self.name, sql_adduser % (self.name.lower()), e)
raise
# Get new coin addresses
new_addrs = {}
for c in self.ctb.coins:
new_addrs[c] = self.ctb.coins[c].getnewaddr(_user=self.name.lower())
lg.info("CtbUser::register(%s): got %s address %s", self.name, c, new_addrs[c])
# Add coin addresses to database
for c in new_addrs:
try:
sql_addr = "REPLACE INTO t_addrs (username, coin, address) VALUES (%s, %s, %s)"
mysqlexec = self.ctb.db.execute(sql_addr, (self.name.lower(), c, new_addrs[c]))
if mysqlexec.rowcount <= 0:
# Undo change to database
delete_user(_username=self.name.lower(), _db=self.ctb.db)
raise Exception("CtbUser::register(%s): rowcount <= 0 while executing <%s>" % (self.name, sql_addr % (self.name.lower(), c, new_addrs[c])))
except Exception, e:
# Undo change to database
delete_user(_username=self.name.lower(), _db=self.ctb.db)
raise
lg.debug("< CtbUser::register(%s) DONE", self.name)
return True
def get_redeem_amount(self, coin=None, fiat=None):
"""
Return karma redeem amount for a given coin
"""
lg.debug("> CtbUser::get_redeem_amount(%s)", coin)
if not coin or not self.ctb.coins.has_key(coin):
raise Exception("CtbUser::get_redeem_amount(%s): invalid coin" % coin)
if not fiat or not self.ctb.conf.fiat.has_key(fiat):
raise Exception("CtbUser::get_redeem_amount(%s): invalid fiat" % fiat)
# Check if we have coin's fiat value
coin_value = self.ctb.coin_value(coin, fiat)
if not coin_value or not coin_value > 0.0:
lg.warning("CtbUser::get_redeem_amount(%s): coin_value not available", coin)
return (None, None)
# First, determine fiat value due to link karma
link_mul = self.ctb.conf.reddit.redeem.multiplier.link
if type(link_mul) in [str, unicode]:
link_mul = eval(link_mul)
if not type(link_mul) == float:
raise Exception("CtbUser::get_redeem_amount(): type of link_mul is not float")
link_val = float(self.prawobj.link_karma) * link_mul
# Second, determine fiat value due to comment karma
comm_mul = self.ctb.conf.reddit.redeem.multiplier.comment
if type(comm_mul) in [str, unicode]:
comm_mul = eval(comm_mul)
if not type(comm_mul) == float:
raise Exception("CtbUser::get_redeem_amount(): type of comm_mul is not float")
comm_val = float(self.prawobj.comment_karma) * comm_mul
# Third, determine base fiat value from config
base_val = self.ctb.conf.reddit.redeem.base
if type(base_val) in [str, unicode]:
base_val = eval(base_val)
if not type(base_val) == float:
raise Exception("CtbUser::get_redeem_amount(): type of base_val is not float")
# Sum link_val, comm_val, and base_val to get total fiat
total_fiat = link_val + comm_val + base_val
# Check if above maximum
if total_fiat > self.ctb.conf.reddit.redeem.maximum:
total_fiat = self.ctb.conf.reddit.redeem.maximum
# Determine total coin value using exchange rate
total_coin = total_fiat / coin_value
lg.debug("< CtbUser::get_redeem_amount(%s) DONE", coin)
return (total_coin, total_fiat)
def delete_user(_username=None, _db=None):
"""
Delete _username from t_users and t_addrs tables
"""
lg.debug("> delete_user(%s)", _username)
try:
sql_arr = ["DELETE FROM t_users WHERE username = %s",
"DELETE FROM t_addrs WHERE username = %s"]
for sql in sql_arr:
mysqlexec = _db.execute(sql, _username.lower())
if mysqlexec.rowcount <= 0:
lg.warning("delete_user(%s): rowcount <= 0 while executing <%s>", _username, sql % _username.lower())
except Exception, e:
lg.error("delete_user(%s): error while executing <%s>: %s", _username, sql % _username.lower(), e)
return False
lg.debug("< delete_user(%s) DONE", _username)
return True
View File
+6
View File
@@ -0,0 +1,6 @@
{% set user_from = a.u_from.name %}
{% set coin_name = a.coin.upper() %}
{% set address = a.addr_to %}
I'm sorry {{ user_from | replace('_', '\_') }}, __{{ coin_name }}__ address __{{ address | escape }}__ appears to be invalid (is there a typo?).
{% include 'footer.tpl' %}
+6
View File
@@ -0,0 +1,6 @@
{% set user_from = a.u_from.name %}
{% set user_bot = ctb.conf.reddit.auth.user %}
Sorry {{ user_from | replace('_', '\_') }}, you're already registered! You can view your balances and deposit addresses with __[+info](http://www.reddit.com/message/compose?to={{ user_bot }}&subject=info&message=%2Binfo)__ command.
For more commands and helpful information on getting started, see the footer of this message.
{% include 'footer.tpl' %}
View File
+3
View File
@@ -0,0 +1,3 @@
Sorry {{ a.u_from.name | replace('_', '\_') }}, action type '{{ a.type }}' is currently disabled. Please try again later.
{% include 'footer.tpl' %}
+67
View File
@@ -0,0 +1,67 @@
{% set title_fmt = "^__[%s]__:" % title %}
{% set user_from_fmt = " ^/u/%s" % a.u_from.name %}
{% set arrow_fmt = " ^->" %}
{% if a.u_to: %}
{% set user_to_fmt = " ^/u/%s" % a.u_to.name %}
{% if ctb.conf.reddit.stats.enabled: %}
{% set stats_user_to_fmt = " ^^[[stats]](%s_%s)" % (ctb.conf.reddit.stats.url, a.u_to.name) %}
{% endif %}
{% endif %}
{% if a.addr_to: %}
{% set ex = ctb.conf.coins[a.coin].explorer %}
{% set user_to_fmt = " ^[%s](%s%s)" % (a.addr_to, ex.address, a.addr_to) %}
{% set arrow_fmt = " ^[->](%s%s)" % (ex.transaction, a.txid) %}
{% endif %}
{% if a.coinval: %}
{% if a.coinval < 0.0001 %}
{% set coin_amount = ( a.coinval * 100000000.0 ) %}
{% set amount_prefix_short = "s" %}
{% set amount_prefix_long = "satoshi" %}
{% elif a.coinval < 1.0 %}
{% set coin_amount = ( a.coinval * 1000.0 ) %}
{% set amount_prefix_short = "m" %}
{% set amount_prefix_long = "milli" %}
{% elif a.coinval >= 1000.0 %}
{% set coin_amount = ( a.coinval / 1000.0 ) %}
{% set amount_prefix_short = "K" %}
{% set amount_prefix_long = "kilo" %}
{% else %}
{% set coin_amount = a.coinval %}
{% endif %}
{% set coin_name = ctb.conf.coins[a.coin].name %}
{% set coin_symbol = ctb.conf.coins[a.coin].symbol %}
{% set coin_amount_fmt = " __^%s%s%.6g ^%s%ss__" % (amount_prefix_short, coin_symbol, coin_amount, amount_prefix_long, coin_name) %}
{% endif %}
{% if a.fiatval: %}
{% set fiat_amount = a.fiatval %}
{% set fiat_symbol = ctb.conf.fiat[a.fiat].symbol %}
{% set fiat_amount_fmt = "&nbsp;^__(%s%.4f)__" % (fiat_symbol, fiat_amount) %}
{% endif %}
{% if ctb.conf.reddit.stats.enabled: %}
{% set stats_user_from_fmt = " ^^[[stats]](%s_%s)" % (ctb.conf.reddit.stats.url, a.u_from.name) %}
{% set stats_link_fmt = " ^[[global_stats]](%s)" % ctb.conf.reddit.stats.url %}
{% endif %}
{% if ctb.conf.reddit.help.enabled: %}
{% set help_link_fmt = " ^[[help]](%s)" % ctb.conf.reddit.help.url %}
{% endif %}
{% if a.type == 'givetip' and a.keyword and ctb.conf.keywords[a.keyword].message %}
{% set txt = ctb.conf.keywords[a.keyword].message %}
{% if stats_user_from_fmt %}
{% set txt = txt | replace("{USER_FROM}", user_from_fmt + stats_user_from_fmt) %}
{% else %}
{% set txt = txt | replace("{USER_FROM}", user_from_fmt) %}
{% endif %}
{% if stats_user_to_fmt %}
{% set txt = txt | replace("{USER_TO}", user_to_fmt + stats_user_to_fmt) %}
{% else %}
{% set txt = txt | replace("{USER_TO}", user_to_fmt) %}
{% endif %}
{% if fiat_amount_fmt %}
{% set txt = txt | replace("{AMOUNT}", coin_amount_fmt + fiat_amount_fmt) %}
{% else %}
{% set txt = txt | replace("{AMOUNT}", coin_amount_fmt) %}
{% endif %}
{{ txt }}
{% else %}
{{ 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 }}
{% endif %}
+9
View File
@@ -0,0 +1,9 @@
{% if ctb and ctb.conf.reddit.help.enabled %}
{% set help_link = "[verify syntax](%s)" % ctb.conf.reddit.help.url %}
{% else %}
{% set help_link = "verify syntax" %}
{% endif %}
Sorry {{ user_from | replace('_', '\_') }}, I didn't understand your {{ what }}. Please {{ help_link }} and try again.
{% set user = user_from %}
{% include 'footer.tpl' %}
+35
View File
@@ -0,0 +1,35 @@
{% if not user and a %}
{% set user = a.u_from.name %}
{% endif %}
{% if not user_bot and ctb %}
{% set user_bot = ctb.conf.reddit.auth.user %}
{% endif %}
{% set compose_url = "http://www.reddit.com/message/compose?to=%s&subject=%s&message=%%2B%s" %}
{% set i_url = compose_url % (user_bot, "info", "info") %}
{% set h_url = compose_url % (user_bot, "history", "history") %}
{% set w_url = compose_url % (user_bot, "withdraw", "withdraw%20ADDRESS%20AMOUNT%20COIN_NAME") %}
{% set r_url = compose_url % (user_bot, "rates", "rates") %}
{% set k_url = compose_url % (user_bot, "redeem", "redeem%20COIN_NAME") %}
{% if ctb and ctb.conf.reddit.help.enabled %}
{% set help_link = " ^[[help]](%s)" % ctb.conf.reddit.help.url %}
{% endif %}
{% if ctb and ctb.conf.reddit.contact.enabled %}
{% set contact_link = " ^[[contact]](%s)" % ctb.conf.reddit.contact.url %}
{% endif %}
{% if ctb and ctb.conf.reddit.stats.enabled %}
{% set stats_user_link = " **^[[your_stats]](%s_%s)**" % (ctb.conf.reddit.stats.url, user) %}
{% set stats_global_link = " ^[[global_stats]](%s)" % ctb.conf.reddit.stats.url %}
{% endif %}
*****
^Helpful ^Links|&nbsp;
:---|:---
{% if a and a.msg and a.msg.permalink %}
^Source ^comment|^[[link]]({{ a.msg.permalink }})
{% elif source_link %}
^Source ^comment|^[[link]]({{ source_link }})
{% endif %}
^Quick ^commands|**^[+info]({{ i_url }})** ^[+history]({{ h_url }}) ^[+redeem]({{ k_url }}) ^[+rates]({{ r_url }}) ^[+withdraw]({{ w_url }})
^Resources|{{ help_link }}{{ contact_link }}{{ stats_user_link }}{{ stats_global_link }}
{% include 'announcement.tpl' %}
+11
View File
@@ -0,0 +1,11 @@
{% set user = a.u_from.name %}
Hello {{ user | replace('_', '\_') }}, here are your last {{ limit }} transactions.
{{ "|".join(keys) }}
{{ "|".join([":---"] * (keys|length)) }}
{% for h in history %}
{{ "|".join(h) }}
{% endfor %}
{% include 'footer.tpl' %}
+24
View File
@@ -0,0 +1,24 @@
{% set user_from = a.u_from.name %}
{% set fiat_total_fmt = "%s%.2f" % (fiat_symbol, fiat_total) %}
Hello {{ user_from | replace('_', '\_') }}! Here's your account info.
coin|deposit address|balance
:---|:---|---:
{% for i in info %}
{% set name_fmt = "%s (%s)" % (ctb.conf.coins[i.coin].name, i.coin.upper()) %}
{% 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) %}
{% set coin_bal_fmt = "%.9g" % i.balance %}
{% if i.fiat_balance %}
{% set fiat_bal_fmt = " ^%s%.2f" % ( i.fiat_symbol, i.fiat_balance ) %}
{% else %}
{% set fiat_bal_fmt = "" %}
{% endif %}
__{{ name_fmt }}__|{{ address_fmt }}|__{{ coin_bal_fmt }}{{ fiat_bal_fmt }}__
{% endfor %}
&nbsp;|&nbsp;|&nbsp;
__TOTAL__| |__{{ fiat_total_fmt }}__
Use addresses above to deposit coins into your account.
{% include 'footer.tpl' %}
+5
View File
@@ -0,0 +1,5 @@
{% set user_from = a.u_from.name %}
{% set amount_fmt = "%s%.6g" % (ctb.conf.fiat[a.fiat].symbol, a.fiatval) %}
Sorry {{ user_from | replace('_', '\_') }}, you don't have any coin balances enough for a {{ amount_fmt }} tip.
{% include 'footer.tpl' %}
+3
View File
@@ -0,0 +1,3 @@
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.
{% include 'footer.tpl' %}
+5
View File
@@ -0,0 +1,5 @@
{% set user_from = a.u_from.name %}
{% set user_to = a.u_to.name %}
I'm sorry {{ user_from | replace('_', '\_') }}, your tip has failed because user **{{ user_to | replace('_', '\_') }}** is not on Reddit.
{% include 'footer.tpl' %}
+5
View File
@@ -0,0 +1,5 @@
{% set user_from = a.u_from.name %}
{% set user_bot = ctb.conf.reddit.auth.user %}
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!
{% include 'footer.tpl' %}
+3
View File
@@ -0,0 +1,3 @@
Hello {{ user_from | replace('_', '\_') }}, your pending tips have been declined.
{% include 'footer.tpl' %}
+4
View File
@@ -0,0 +1,4 @@
{% set user = a.u_from.name %}
Hey {{ user | replace('_', '\_') }}, I couldn't get the rates for you because exchange **{{ exchange }}** didn't respond. Please try again later.
{% include 'footer.tpl' %}
+15
View File
@@ -0,0 +1,15 @@
{% set user = a.u_from.name %}
Hello {{ user | replace('_', '\_') }}, here are the latest exchange rates.
coin|average{% for e in exchanges %}{{ "|" + e }}{% endfor %}
:---|---:{% for e in exchanges %}{{ "|---:" }}{% endfor %}
{% for c in coins %}
{{ "\n**%s&nbsp;(%s)**|%s%.6f^%s%.4f" % (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 %}
{% if rates[c][e].btc and rates[c][e][fiat] %}{{ "|%s%.6f^%s%.4f" % (ctb.conf.coins.btc.symbol, rates[c][e].btc, ctb.conf.fiat[fiat].symbol, rates[c][e][fiat]) }}{% else %}{{ "|-" }}{% endif %}
{% endfor %}
{% endfor %}
{% include 'footer.tpl' %}
+7
View File
@@ -0,0 +1,7 @@
I'm sorry {{ a.u_from.name | replace('_', '\_') }}, you've already redeemed your {{ coin }} karma.
{% if coin %}
Try redeeming a different coin.
{% endif %}
{% include 'footer.tpl' %}
+3
View File
@@ -0,0 +1,3 @@
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.
{% include 'footer.tpl' %}
+25
View File
@@ -0,0 +1,25 @@
{% if a.coinval: %}
{% if a.coinval < 0.0001 %}
{% set coin_amount = ( a.coinval * 100000000.0 ) %}
{% set amount_prefix_short = "s" %}
{% set amount_prefix_long = "satoshi" %}
{% elif a.coinval < 1.0 %}
{% set coin_amount = ( a.coinval * 1000.0 ) %}
{% set amount_prefix_short = "m" %}
{% set amount_prefix_long = "milli" %}
{% elif a.coinval >= 1000.0 %}
{% set coin_amount = ( a.coinval / 1000.0 ) %}
{% set amount_prefix_short = "K" %}
{% set amount_prefix_long = "kilo" %}
{% else %}
{% set coin_amount = a.coinval %}
{% endif %}
{% endif %}
{% set coinval_fmt = "%s%s%.6g %s%ss" % (amount_prefix_short, ctb.conf.coins[a.coin].symbol, coin_amount, amount_prefix_long, ctb.conf.coins[a.coin].name) %}
{% if amount_prefix_short %}
{% set coinval_fmt = coinval_fmt + " (%s%.9f %ss)" % (ctb.conf.coins[a.coin].symbol, a.coinval, ctb.conf.coins[a.coin].name) %}
{% endif %}
{% set fiatval_fmt = "%s%.4f" % (ctb.conf.fiat[a.fiat].symbol, a.fiatval) %}
Hey {{ a.u_from.name | replace('_', '\_') }}, you have received __{{ coinval_fmt }} ({{ fiatval_fmt }})__ for your karma.
{% include 'footer.tpl' %}
+5
View File
@@ -0,0 +1,5 @@
{% set coinval_fmt = "%s%.9f" % (ctb.conf.coins[a.coin].symbol, a.coinval) %}
{% set fiatval_fmt = "%s%.4f" % (ctb.conf.fiat[a.fiat].symbol, a.fiatval) %}
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.
{% include 'footer.tpl' %}
+3
View File
@@ -0,0 +1,3 @@
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 }}).
{% include 'footer.tpl' %}
+6
View File
@@ -0,0 +1,6 @@
{% set user_from = a.u_from.name %}
{% set user_to = a.u_to.name %}
{% set coin_name = ctb.conf.coins[a.coin].name %}
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.
{% include 'footer.tpl' %}
+9
View File
@@ -0,0 +1,9 @@
{% set user_from = a.u_from.name %}
{% set amount_fmt = "%.9f %s" % (a.coinval, a.coin.upper()) %}
{% set min_fmt = "%.9g" % min_value %}
{% set coin_name = ctb.conf.coins[a.coin].name %}
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.
If you really need to withdraw this amount, try depositing some {{ coin_name }}s to meet the minimum limit, then withdrawing everything.
{% include 'footer.tpl' %}
+30
View File
@@ -0,0 +1,30 @@
{% set user_from = a.u_from.name %}
{% set user_to = a.u_to.name %}
{% set user_bot = ctb.conf.reddit.auth.user %}
{% if a.coinval: %}
{% if a.coinval < 0.0001 %}
{% set coin_amount = ( a.coinval * 100000000.0 ) %}
{% set amount_prefix_short = "s" %}
{% set amount_prefix_long = "satoshi" %}
{% elif a.coinval < 1.0 %}
{% set coin_amount = ( a.coinval * 1000.0 ) %}
{% set amount_prefix_short = "m" %}
{% set amount_prefix_long = "milli" %}
{% elif a.coinval >= 1000.0 %}
{% set coin_amount = ( a.coinval / 1000.0 ) %}
{% set amount_prefix_short = "K" %}
{% set amount_prefix_long = "kilo" %}
{% else %}
{% set coin_amount = a.coinval %}
{% endif %}
{% endif %}
{% set coinval_fmt = "%s%s%.6g %s%ss" % (amount_prefix_short, ctb.conf.coins[a.coin].symbol, coin_amount, amount_prefix_long, ctb.conf.coins[a.coin].name) %}
{% if amount_prefix_short %}
{% set coinval_fmt = coinval_fmt + " (%s%.9f %ss)" % (ctb.conf.coins[a.coin].symbol, a.coinval, ctb.conf.coins[a.coin].name) %}
{% endif %}
{% set fiatval_fmt = "%s%.4f" % (ctb.conf.fiat[a.fiat].symbol, a.fiatval) %}
{% set expire_days_fmt = "%.2g" % ( ctb.conf.misc.times.expire_pending_hours / 24.0 ) %}
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.__
{% set user = user_to %}
{% include 'footer.tpl' %}
+16
View File
@@ -0,0 +1,16 @@
{% set user_from = a.u_from.name %}
{% set balance_fmt = "%.9f %s" % (balance, a.coin.upper()) %}
I'm sorry {{ user_from | replace('_', '\_') }}, your {{ action_name }} balance of **{{ balance_fmt }}** is insufficient for this {{ action_name }}.
{% if action_name == 'withdraw' %}
{% set coin_name = ctb.conf.coins[a.coin].name %}
{% set coin_confs = ctb.conf.coins[a.coin].minconf.withdraw %}
{% set coin_fee_fmt = "%.6g" % ctb.conf.coins[a.coin].txfee %}
Withdrawals are subject to network confirmations and network fees. {{ coin_name }} requires {{ coin_confs }} confirmations and a {{ coin_fee_fmt }} fee.
If withdrawal balance above doesn't match your reported tip balance, try waiting for more network confirmations.
>**Tip:** To withdraw everything, use the `ALL` keyword - `+withdraw ADDRESS ALL {{ coin_name }}` - and I'll automatically deduct the required network fee.
{% endif %}
{% include 'footer.tpl' %}
+28
View File
@@ -0,0 +1,28 @@
{% set user_from = a.u_from.name %}
{% set user_to = a.u_to.name %}
{% if a.coinval: %}
{% if a.coinval < 0.0001 %}
{% set coin_amount = ( a.coinval * 100000000.0 ) %}
{% set amount_prefix_short = "s" %}
{% set amount_prefix_long = "satoshi" %}
{% elif a.coinval < 1.0 %}
{% set coin_amount = ( a.coinval * 1000.0 ) %}
{% set amount_prefix_short = "m" %}
{% set amount_prefix_long = "milli" %}
{% elif a.coinval >= 1000.0 %}
{% set coin_amount = ( a.coinval / 1000.0 ) %}
{% set amount_prefix_short = "K" %}
{% set amount_prefix_long = "kilo" %}
{% else %}
{% set coin_amount = a.coinval %}
{% endif %}
{% endif %}
{% set coinval_fmt = "%s%s%.6g %s%ss" % (amount_prefix_short, ctb.conf.coins[a.coin].symbol, coin_amount, amount_prefix_long, ctb.conf.coins[a.coin].name) %}
{% if amount_prefix_short %}
{% set coinval_fmt = coinval_fmt + " (%s%.9f %ss)" % (ctb.conf.coins[a.coin].symbol, a.coinval, ctb.conf.coins[a.coin].name) %}
{% endif %}
{% set fiatval_fmt = "%s%.4f" % (ctb.conf.fiat[a.fiat].symbol, a.fiatval) %}
Hey {{ user_to | replace('_', '\_') }}, you have received a __{{ coinval_fmt }} ({{ fiatval_fmt }})__ tip from /u/{{ user_from }}.
{% set user = user_to %}
{% include 'footer.tpl' %}
+12
View File
@@ -0,0 +1,12 @@
{% set user_from = a.u_from.name %}
{% if a.u_to %}
{% set user_to_fmt = "/u/%s" % a.u_to.name %}
{% else %}
{% set user_to_fmt = a.addr_to %}
{% endif %}
{% set coin_amount = a.coinval %}
{% set coin_name = ctb.conf.coins[a.coin].name %}
{% set coin_amount_fmt = "%.9f %s(s)" % (coin_amount, coin_name) %}
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.
{% include 'footer.tpl' %}
+8
View File
@@ -0,0 +1,8 @@
{% set user_from = a.u_from.name %}
{% set user_bot = ctb.conf.reddit.auth.user %}
Welcome {{ user_from | replace('_', '\_') }}!
Thank you for registering. You can now view your balances and deposit addresses with __[+info](http://www.reddit.com/message/compose?to={{ user_bot }}&subject=info&message=%2Binfo)__ command.
For more commands and helpful information on getting started, see the footer of this message. Happy tipping!
{% include 'footer.tpl' %}