tipjar 0.1.16

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,5 @@
1
+ module Bitbot::Doc
2
+ def on_doc(m)
3
+ m.reply "Useful Information : TipJar Wiki - https://github.com/weirdthall/TipJar/wiki"
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ module Bitbot::Help
2
+ def on_help(m, args)
3
+ m.reply "Channel Commands: +tip, +tipstats, +txfee"
4
+ m.reply "Private Message Commands: balance, history, withdraw, deposit"
5
+ end
6
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ module Bitbot::Online
2
+ def on_doc(m)
3
+ m.reply "TipJar is online. Please type +help to interact with me."
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Bitbot::Ping
2
+ def on_ping
3
+ m.user.msg "PONG"
4
+ end
5
+ end