tipjar 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|