tipjar 0.1.16
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/Gemfile +3 -0
- data/README.md +140 -0
- data/README_old.md +140 -0
- data/Rakefile +3 -0
- data/bin/bitbot +55 -0
- data/bitbot.gemspec +28 -0
- data/lib/bitbot/blockchain.rb +70 -0
- data/lib/bitbot/database.rb +142 -0
- data/lib/bitbot/plugin.rb +83 -0
- data/lib/bitbot/plugin/balance.rb +8 -0
- data/lib/bitbot/plugin/common.rb +119 -0
- data/lib/bitbot/plugin/deposit.rb +33 -0
- data/lib/bitbot/plugin/doc.rb +5 -0
- data/lib/bitbot/plugin/help.rb +6 -0
- data/lib/bitbot/plugin/history.rb +27 -0
- data/lib/bitbot/plugin/online.rb +5 -0
- data/lib/bitbot/plugin/ping.rb +5 -0
- data/lib/bitbot/plugin/prize.rb +29 -0
- data/lib/bitbot/plugin/ticker.rb +15 -0
- data/lib/bitbot/plugin/tip.rb +61 -0
- data/lib/bitbot/plugin/tipstats.rb +29 -0
- data/lib/bitbot/plugin/txfee.rb +6 -0
- data/lib/bitbot/plugin/update_addresses.rb +112 -0
- data/lib/bitbot/plugin/update_exchange_rates.rb +5 -0
- data/lib/bitbot/plugin/version.rb +5 -0
- data/lib/bitbot/plugin/withdraw.rb +35 -0
- data/lib/ircstring.rb +55 -0
- data/tipjar.gemspec +28 -0
- metadata +159 -0
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'sqlite3'
|
2
|
+
|
3
|
+
module Bitbot
|
4
|
+
class InsufficientFundsError < StandardError; end
|
5
|
+
|
6
|
+
class Database
|
7
|
+
def initialize(path)
|
8
|
+
@db = SQLite3::Database.new path
|
9
|
+
@db.execute('PRAGMA foreign_keys = ON')
|
10
|
+
end
|
11
|
+
|
12
|
+
def upgrade_schema
|
13
|
+
@db.execute(<<-ENDSQL)
|
14
|
+
create table if not exists users (
|
15
|
+
id integer primary key autoincrement,
|
16
|
+
created_at integer not null,
|
17
|
+
username text not null unique
|
18
|
+
)
|
19
|
+
ENDSQL
|
20
|
+
|
21
|
+
@db.execute(<<-ENDSQL)
|
22
|
+
create table if not exists transactions (
|
23
|
+
id integer primary key autoincrement,
|
24
|
+
created_at integer not null,
|
25
|
+
amount numeric not null,
|
26
|
+
note text,
|
27
|
+
withdrawal_address text,
|
28
|
+
incoming_transaction text unique,
|
29
|
+
user_id int references users(id) not null,
|
30
|
+
other_user_id int references users(id)
|
31
|
+
)
|
32
|
+
ENDSQL
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_or_create_user_id_for_username(username)
|
36
|
+
user_id = @db.get_first_value("select id from users where username = ?", [ username ])
|
37
|
+
unless user_id
|
38
|
+
@db.execute("insert into users(created_at, username) values (?, ?)", [ Time.now.to_i, username ])
|
39
|
+
user_id = @db.get_first_value("select last_insert_rowid()")
|
40
|
+
end
|
41
|
+
|
42
|
+
user_id
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_username_for_user_id(user_id)
|
46
|
+
@db.get_first_value("select username from users where id = ?", [ user_id ])
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_balance_for_user_id(user_id)
|
50
|
+
@db.get_first_value("select coalesce(sum(amount), 0) from transactions where user_id = ?", [ user_id ])
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_transactions_for_user_id(user_id)
|
54
|
+
result = []
|
55
|
+
@db.execute("select t.id, t.created_at, t.amount, t.note, t.withdrawal_address, t.incoming_transaction, t.other_user_id, u.username from transactions t left outer join users u on t.other_user_id = u.id where user_id = ? order by t.created_at desc", [ user_id ]) do |row|
|
56
|
+
result << {
|
57
|
+
:id => row[0],
|
58
|
+
:created_at => row[1],
|
59
|
+
:amount => row[2],
|
60
|
+
:note => row[3],
|
61
|
+
:withdrawal_address => row[4],
|
62
|
+
:incoming_transaction => row[5],
|
63
|
+
:other_user_id => row[6],
|
64
|
+
:other_username => row[7]
|
65
|
+
}
|
66
|
+
end
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_tipping_stats(from = nil)
|
71
|
+
# If no from is specified, use midnight of today.
|
72
|
+
if from.nil?
|
73
|
+
# TODO: this is hardcoding an offset of -0700 - fix that to make
|
74
|
+
# the timezone configurable
|
75
|
+
now = Time.now - 60 * 60 * 7
|
76
|
+
from = Time.local(now.year, now.month, now.day)
|
77
|
+
end
|
78
|
+
from = from.to_i
|
79
|
+
|
80
|
+
stats = {}
|
81
|
+
|
82
|
+
stats[:total_tipped] = @db.get_first_value("select coalesce(sum(amount), 0) from transactions t where t.other_user_id is not null and t.user_id <> t.other_user_id and t.amount < 0 and t.created_at > ?", [ from ])
|
83
|
+
stats[:total_tips] = @db.get_first_value("select count(*) from transactions t where t.other_user_id is not null and t.user_id <> t.other_user_id and t.amount < 0 and t.created_at > ?", [ from ])
|
84
|
+
stats[:tippers] = @db.execute("select * from (select username, sum(amount) total from transactions t, users u where t.user_id = u.id and other_user_id is not null and amount < 0 and user_id <> other_user_id and t.created_at >= ? group by username) foo order by total asc", [ from ])
|
85
|
+
stats[:tippees] = @db.execute("select * from (select username, sum(amount) total from transactions t, users u where t.user_id = u.id and amount > 0 and user_id <> other_user_id and t.created_at >= ? group by username) foo order by total desc", [ from ])
|
86
|
+
stats
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns an array of all the Bitcoin transaction ids for deposits
|
90
|
+
def get_incoming_transaction_ids
|
91
|
+
transaction_ids = []
|
92
|
+
@db.execute("select incoming_transaction from transactions where incoming_transaction is not null") do |row|
|
93
|
+
transaction_ids << row[0]
|
94
|
+
end
|
95
|
+
|
96
|
+
transaction_ids
|
97
|
+
end
|
98
|
+
|
99
|
+
# Adds a transaction with a deposit. Returns true if the row was
|
100
|
+
# added, and false if the insert failed for some reason (like if the
|
101
|
+
# transaction_id already exists).
|
102
|
+
def create_transaction_from_deposit(user_id, amount, transaction_id)
|
103
|
+
@db.execute("insert into transactions (created_at, amount, incoming_transaction, user_id) values (?, ?, ?, ?)",
|
104
|
+
[ Time.now.to_i, amount, transaction_id, user_id ])
|
105
|
+
return @db.changes == 1
|
106
|
+
end
|
107
|
+
|
108
|
+
# Adds a transaction for a withdrawal.
|
109
|
+
def create_transaction_from_withdrawal(user_id, amount, fee, address)
|
110
|
+
@db.transaction(:exclusive) do
|
111
|
+
# verify current balance
|
112
|
+
current_balance = self.get_balance_for_user_id(user_id)
|
113
|
+
if current_balance < (amount + fee)
|
114
|
+
raise InsufficientFundsError, "Insufficient funds; Current balance: #{current_balance} for amount #{amount} + fee #{fee}"
|
115
|
+
end
|
116
|
+
|
117
|
+
@db.execute("insert into transactions (created_at, user_id, amount, withdrawal_address)
|
118
|
+
values (?, ?, ?, ?)", [ Time.now.to_i, user_id, (0 - (amount + fee)), address ])
|
119
|
+
end
|
120
|
+
|
121
|
+
true
|
122
|
+
end
|
123
|
+
|
124
|
+
def create_transaction_from_tip(from_user_id, to_user_id, amount, message)
|
125
|
+
@db.transaction(:exclusive) do
|
126
|
+
# verify current balance
|
127
|
+
current_balance = self.get_balance_for_user_id(from_user_id)
|
128
|
+
if current_balance < amount
|
129
|
+
raise InsufficientFundsError, "Insufficient funds; Current balance: #{current_balance} for amount #{amount}"
|
130
|
+
end
|
131
|
+
|
132
|
+
now = Time.now.to_i
|
133
|
+
@db.execute("insert into transactions (created_at, user_id, amount, note, other_user_id)
|
134
|
+
values (?, ?, ?, ?, ?)", [ now, from_user_id, (0 - amount), message, to_user_id ])
|
135
|
+
@db.execute("insert into transactions (created_at, user_id, amount, note, other_user_id)
|
136
|
+
values (?, ?, ?, ?, ?)", [ now, to_user_id, amount, message, from_user_id ])
|
137
|
+
end
|
138
|
+
|
139
|
+
true
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'cinch'
|
2
|
+
require 'ircstring'
|
3
|
+
|
4
|
+
require 'bitbot/database'
|
5
|
+
require 'bitbot/plugin/common'
|
6
|
+
require 'bitbot/plugin/help'
|
7
|
+
require 'bitbot/plugin/balance'
|
8
|
+
require 'bitbot/plugin/deposit'
|
9
|
+
require 'bitbot/plugin/history'
|
10
|
+
require 'bitbot/plugin/tip'
|
11
|
+
require 'bitbot/plugin/tipstats'
|
12
|
+
require 'bitbot/plugin/withdraw'
|
13
|
+
require 'bitbot/plugin/update_addresses'
|
14
|
+
require 'bitbot/plugin/update_exchange_rates'
|
15
|
+
require 'bitbot/plugin/txfee'
|
16
|
+
require 'bitbot/plugin/doc'
|
17
|
+
require 'bitbot/plugin/online'
|
18
|
+
require 'bitbot/plugin/prize'
|
19
|
+
|
20
|
+
class Bitbot::Plugin
|
21
|
+
include Cinch::Plugin
|
22
|
+
|
23
|
+
include Bitbot::Common
|
24
|
+
include Bitbot::Help
|
25
|
+
include Bitbot::Balance
|
26
|
+
include Bitbot::Deposit
|
27
|
+
include Bitbot::History
|
28
|
+
include Bitbot::Tip
|
29
|
+
include Bitbot::TipStats
|
30
|
+
include Bitbot::Withdraw
|
31
|
+
include Bitbot::UpdateAddresses
|
32
|
+
include Bitbot::UpdateExchangeRates
|
33
|
+
include Bitbot::Txfee
|
34
|
+
include Bitbot::Doc
|
35
|
+
include Bitbot::Online
|
36
|
+
include Bitbot::Prize
|
37
|
+
|
38
|
+
def initialize(bot)
|
39
|
+
super
|
40
|
+
Bitbot::Database.new(File.join(config['data']['path'], "bitbot.db")).upgrade_schema()
|
41
|
+
end
|
42
|
+
|
43
|
+
set :prefix, ""
|
44
|
+
|
45
|
+
#
|
46
|
+
# Private messages
|
47
|
+
#
|
48
|
+
match /^help(.*)$/, :method => :on_help, :react_on => :private
|
49
|
+
match /^balance$/, :method => :on_balance, :react_on => :private
|
50
|
+
match /^history$/, :method => :on_history, :react_on => :private
|
51
|
+
match /^withdraw(.*)$/, :method => :on_withdraw, :react_on => :private
|
52
|
+
match /^deposit$/, :method => :on_deposit, :react_on => :private
|
53
|
+
match /^\+tip\s+(\w+)\s+([\d.]+)\s+?(.*)/, :method => :on_tip, :react_on => :private
|
54
|
+
|
55
|
+
#
|
56
|
+
# Channel messages
|
57
|
+
#
|
58
|
+
match /^\+help(.*)$/, :method => :on_help, :react_on => :channel
|
59
|
+
match /^\+tipstats$/, :method => :on_tipstats, :react_on => :channel
|
60
|
+
match /^\+tip\s+(\w+)\s+([\d.]+)\s+?(.*)/, :method => :on_tip, :react_on => :channel
|
61
|
+
match /^\+doc$/, :method => :on_tipstats, :react_on => :channel
|
62
|
+
match /^\+ticker$/, :method => :on_tipstats, :react_on => :channel
|
63
|
+
|
64
|
+
#
|
65
|
+
# Timer jobs
|
66
|
+
#
|
67
|
+
timer 60, :method => :on_update_exchange_rates
|
68
|
+
timer 60, :method => :on_update_addresses
|
69
|
+
timer 10, :method => :on_prize_win_check_balance
|
70
|
+
|
71
|
+
#
|
72
|
+
# Also run the timer jobs on connect
|
73
|
+
#
|
74
|
+
listen_to :connect, :method => :on_update_exchange_rates
|
75
|
+
listen_to :connect, :method => :on_update_addresses
|
76
|
+
listen_to :connect, :method => :on_online
|
77
|
+
listen_to :connect, :method => :on_doc
|
78
|
+
|
79
|
+
#
|
80
|
+
# Run these jobs on every channel message
|
81
|
+
#
|
82
|
+
listen_to :channel, :method => :on_prize
|
83
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module Bitbot::Balance
|
2
|
+
def on_balance(m)
|
3
|
+
user_id = db.get_or_create_user_id_for_username(m.user.user)
|
4
|
+
m.reply "Your current balance is #{satoshi_with_usd(db.get_balance_for_user_id(user_id))}"
|
5
|
+
m.reply "Please note that a transaction fee of #{satoshi_to_str(withdrawal_fee)} will be added to your withdrawal"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'ircstring'
|
4
|
+
require 'bitbot/database'
|
5
|
+
require 'bitbot/blockchain'
|
6
|
+
|
7
|
+
module Bitbot::Common
|
8
|
+
def cached_addresses
|
9
|
+
@cached_addresses
|
10
|
+
end
|
11
|
+
|
12
|
+
def cached_addresses=(new)
|
13
|
+
@cached_addresses = new
|
14
|
+
end
|
15
|
+
|
16
|
+
def exchange_rate(currency)
|
17
|
+
if @cached_exchange_rates
|
18
|
+
return @cached_exchange_rates[currency]["15m"]
|
19
|
+
end
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def exchange_rates=(new)
|
24
|
+
@cached_exchange_rates = new
|
25
|
+
end
|
26
|
+
|
27
|
+
def withdrawal_fee
|
28
|
+
config['blockchain']['withdrawal_fee'] || 50000
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Returns a database handle for use in the current thread.
|
33
|
+
#
|
34
|
+
def db
|
35
|
+
Thread.current[:bitbot_db] ||=
|
36
|
+
Bitbot::Database.new(File.join(config['data']['path'], "bitbot.db"))
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Returns a Blockchain API helper. Everyone uses the same one,
|
41
|
+
# but it doesn't keep any state so it's fine.
|
42
|
+
#
|
43
|
+
def blockchain
|
44
|
+
@@cached_blockchain ||=
|
45
|
+
Bitbot::Blockchain.new(config['blockchain']['wallet_id'],
|
46
|
+
config['blockchain']['password1'],
|
47
|
+
config['blockchain']['password2'])
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Returns the User for the given username. If you want to get
|
52
|
+
# a User based on nick, User(nick) is easier.
|
53
|
+
#
|
54
|
+
def user_with_username(username)
|
55
|
+
bot.user_list.each do |user|
|
56
|
+
return user if user.user == username
|
57
|
+
end
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Takes a string, and returns an int with number of satoshi.
|
63
|
+
# Eventually this could be smart enough to handle specified units too,
|
64
|
+
# rather than just assuming BTC every time.
|
65
|
+
#
|
66
|
+
def str_to_satoshi(str)
|
67
|
+
(str.to_f * 10**8).to_i
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Some number formatting helpers
|
72
|
+
#
|
73
|
+
def satoshi_to_str(satoshi)
|
74
|
+
str = "฿%.8f" % (satoshi.to_f / 10**8)
|
75
|
+
# strip trailing 0s
|
76
|
+
str.gsub(/0*$/, '')
|
77
|
+
end
|
78
|
+
|
79
|
+
def satoshi_to_usd(satoshi)
|
80
|
+
rate = exchange_rate("USD")
|
81
|
+
if rate
|
82
|
+
"$%.2f" % (satoshi.to_f / 10**8 * rate)
|
83
|
+
else
|
84
|
+
"$?"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def satoshi_to_gbp(satoshi)
|
89
|
+
rate = exchange_rate("GBP")
|
90
|
+
if rate
|
91
|
+
"$%.2f" % (satoshi.to_f / 10**8 * rate)
|
92
|
+
else
|
93
|
+
"$?"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def satoshi_to_eur(satoshi)
|
98
|
+
rate = exchange_rate("EUR")
|
99
|
+
if rate
|
100
|
+
"$%.2f" % (satoshi.to_f / 10**8 * rate)
|
101
|
+
else
|
102
|
+
"$?"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def satoshi_with_usd(satoshi)
|
107
|
+
btc_str = satoshi_to_str(satoshi)
|
108
|
+
if satoshi < 0
|
109
|
+
btc_str = btc_str.irc(:red)
|
110
|
+
else
|
111
|
+
btc_str = btc_str.irc(:green)
|
112
|
+
end
|
113
|
+
|
114
|
+
usd_str = "[".irc(:grey) + satoshi_to_usd(satoshi).irc(:blue) + "]".irc(:grey)
|
115
|
+
|
116
|
+
"#{btc_str} #{usd_str}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Bitbot::Deposit
|
2
|
+
def on_deposit(m, create = true)
|
3
|
+
user_id = db.get_or_create_user_id_for_username(m.user.user)
|
4
|
+
|
5
|
+
unless cached_addresses
|
6
|
+
m.reply "TipJar is not initialized yet. Please try again later."
|
7
|
+
return
|
8
|
+
end
|
9
|
+
|
10
|
+
if address = cached_addresses[user_id]
|
11
|
+
m.reply "Send deposits to #{address["address"].irc(:bold)}. " +
|
12
|
+
"This address is specific to you, and any funds delivered " +
|
13
|
+
"to it will be added to your account after confirmation."
|
14
|
+
return
|
15
|
+
end
|
16
|
+
|
17
|
+
unless create
|
18
|
+
m.reply "There was a problem getting your deposit address. " +
|
19
|
+
"Please contact your friendly TipJar admin."
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
# Attempt to create an address.
|
24
|
+
blockchain.create_deposit_address_for_user_id(user_id)
|
25
|
+
|
26
|
+
# Force a refresh of the cached address list...
|
27
|
+
on_update_addresses
|
28
|
+
|
29
|
+
# Now run again, to show them the address we just looked up.
|
30
|
+
on_deposit(m, false)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Bitbot::History
|
2
|
+
def on_history(m)
|
3
|
+
user_id = db.get_or_create_user_id_for_username(m.user.user)
|
4
|
+
|
5
|
+
on_balance(m)
|
6
|
+
|
7
|
+
n = 0
|
8
|
+
db.get_transactions_for_user_id(user_id).each do |tx|
|
9
|
+
time = Time.at(tx[:created_at].to_i).strftime("%Y-%m-%d")
|
10
|
+
amount = satoshi_with_usd(tx[:amount])
|
11
|
+
action = if tx[:amount] < 0 && tx[:other_user_id]
|
12
|
+
"to #{tx[:other_username]}"
|
13
|
+
elsif tx[:amount] > 0 && tx[:other_user_id]
|
14
|
+
"from #{tx[:other_username]}"
|
15
|
+
elsif tx[:withdrawal_address]
|
16
|
+
"withdrawal to #{tx[:withdrawal_address]}"
|
17
|
+
elsif tx[:incoming_transaction]
|
18
|
+
"deposit from tx #{tx[:incoming_transaction]}"
|
19
|
+
end
|
20
|
+
|
21
|
+
m.reply "#{time.irc(:grey)}: #{amount} #{action} #{"(#{tx[:note]})".irc(:grey) if tx[:note]}"
|
22
|
+
|
23
|
+
n += 1
|
24
|
+
break if n >= 10
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|