mirror of
https://github.com/jwinterm/groestltip.git
synced 2026-10-17 05:51:25 +02:00
First commit
This commit is contained in:
+40
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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;
|
||||||
@@ -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)
|
||||||
@@ -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]))
|
||||||
@@ -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]))
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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|Š)'
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)'
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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'
|
||||||
@@ -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'
|
||||||
@@ -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)'
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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 %.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 %.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])
|
||||||
@@ -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
|
||||||
@@ -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' %}
|
||||||
@@ -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' %}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Sorry {{ a.u_from.name | replace('_', '\_') }}, action type '{{ a.type }}' is currently disabled. Please try again later.
|
||||||
|
|
||||||
|
{% include 'footer.tpl' %}
|
||||||
@@ -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 = " ^__(%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 %}
|
||||||
@@ -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' %}
|
||||||
@@ -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|
|
||||||
|
:---|:---
|
||||||
|
{% 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' %}
|
||||||
@@ -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' %}
|
||||||
@@ -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 %}
|
||||||
|
| |
|
||||||
|
__TOTAL__| |__{{ fiat_total_fmt }}__
|
||||||
|
|
||||||
|
Use addresses above to deposit coins into your account.
|
||||||
|
|
||||||
|
{% include 'footer.tpl' %}
|
||||||
@@ -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' %}
|
||||||
@@ -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' %}
|
||||||
@@ -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' %}
|
||||||
@@ -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' %}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Hello {{ user_from | replace('_', '\_') }}, your pending tips have been declined.
|
||||||
|
|
||||||
|
{% include 'footer.tpl' %}
|
||||||
@@ -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' %}
|
||||||
@@ -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 (%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' %}
|
||||||
@@ -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' %}
|
||||||
@@ -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' %}
|
||||||
@@ -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' %}
|
||||||
@@ -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' %}
|
||||||
@@ -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' %}
|
||||||
@@ -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' %}
|
||||||
@@ -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' %}
|
||||||
@@ -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' %}
|
||||||
@@ -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' %}
|
||||||
@@ -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' %}
|
||||||
@@ -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' %}
|
||||||
@@ -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' %}
|
||||||
Reference in New Issue
Block a user