bitbot 0.0.1 → 0.0.2

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.
data/bin/bitbot CHANGED
@@ -3,8 +3,7 @@
3
3
  require 'daemons'
4
4
  require 'yaml'
5
5
 
6
- require 'bitbot/bot'
7
- require 'bitbot/database'
6
+ require 'bitbot/plugin'
8
7
 
9
8
  def usage
10
9
  puts "Usage: #{File.basename($0)} {start|stop|run} <path to config.yml>"
@@ -27,9 +26,6 @@ unless File.exist?(File.dirname(config['data']['path']))
27
26
  exit 1
28
27
  end
29
28
 
30
- # Update the database file
31
- Bitbot::Database.new(File.join(config['data']['path'], "bitbot.db")).upgrade_schema()
32
-
33
29
  Daemons.run_proc('bitbot',
34
30
  :hard_exit => true,
35
31
  :backtrace => true,
@@ -38,19 +34,19 @@ Daemons.run_proc('bitbot',
38
34
  :monitor => true,
39
35
  :log_output => true) do
40
36
 
41
- bot = Bitbot::Bot.new(config)
42
-
43
- Thread.new do
44
- loop do
45
- begin
46
- bot.update_exchange_rates()
47
- bot.update_addresses()
48
- rescue => e
49
- puts "exception in loop addresses: #{e}"
50
- puts e.backtrace
51
- ensure
52
- sleep 60
53
- end
37
+ bot = Cinch::Bot.new do
38
+ configure do |c|
39
+ c.server = config['irc']['server']
40
+ c.port = config['irc']['port']
41
+ c.ssl.use = config['irc']['ssl']
42
+ c.channels = config['irc']['channels']
43
+ c.nick = config['irc']['nick'] || 'bitbot'
44
+ c.user = config['irc']['username'] || 'bitbot'
45
+ c.password = config['irc']['password']
46
+ c.verbose = config['irc']['verbose']
47
+
48
+ c.plugins.plugins = [Bitbot::Plugin]
49
+ c.plugins.options[Bitbot::Plugin] = config
54
50
  end
55
51
  end
56
52
 
data/bitbot.gemspec CHANGED
@@ -2,7 +2,7 @@ $:.push File.expand_path("../lib", __FILE__)
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{bitbot}
5
- s.version = "0.0.1"
5
+ s.version = "0.0.2"
6
6
  s.platform = Gem::Platform::RUBY
7
7
  s.authors = ["Zach Wily"]
8
8
  s.email = ["zach@zwily.com"]
@@ -78,7 +78,7 @@ module Bitbot
78
78
  from = from.to_i
79
79
 
80
80
  stats = {}
81
-
81
+
82
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
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
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 ])
@@ -0,0 +1,63 @@
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
+
16
+ class Bitbot::Plugin
17
+ include Cinch::Plugin
18
+
19
+ include Bitbot::Common
20
+ include Bitbot::Help
21
+ include Bitbot::Balance
22
+ include Bitbot::Deposit
23
+ include Bitbot::History
24
+ include Bitbot::Tip
25
+ include Bitbot::TipStats
26
+ include Bitbot::Withdraw
27
+ include Bitbot::UpdateAddresses
28
+ include Bitbot::UpdateExchangeRates
29
+
30
+ def initialize(bot)
31
+ super
32
+ Bitbot::Database.new(File.join(config['data']['path'], "bitbot.db")).upgrade_schema()
33
+ end
34
+
35
+ set :prefix, ""
36
+
37
+ #
38
+ # Private messages
39
+ #
40
+ match /^help(.*)$/, :method => :on_help, :react_on => :private
41
+ match /^balance$/, :method => :on_balance, :react_on => :private
42
+ match /^history$/, :method => :on_history, :react_on => :private
43
+ match /^withdraw(.*)$/, :method => :on_withdraw, :react_on => :private
44
+ match /^deposit$/, :method => :on_deposit, :react_on => :private
45
+
46
+ #
47
+ # Channel messages
48
+ #
49
+ match /^\+tipstats$/, :method => :on_tipstats, :react_on => :channel
50
+ match /^\+tip\s+(\w+)\s+([\d.]+)\s+?(.*)/, :method => :on_tip, :react_on => :channel
51
+
52
+ #
53
+ # Timer jobs
54
+ #
55
+ timer 60, :method => :on_update_exchange_rates
56
+ timer 60, :method => :on_update_addresses
57
+
58
+ #
59
+ # Also run the timer jobs on connect
60
+ #
61
+ listen_to :connect, :method => :on_update_exchange_rates
62
+ listen_to :connect, :method => :on_update_addresses
63
+ end
@@ -0,0 +1,7 @@
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 "Balance is #{satoshi_with_usd(db.get_balance_for_user_id(user_id))}"
5
+ end
6
+ end
7
+
@@ -0,0 +1,88 @@
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
+ #
28
+ # Returns a database handle for use in the current thread.
29
+ #
30
+ def db
31
+ Thread.current[:bitbot_db] ||=
32
+ Bitbot::Database.new(File.join(config['data']['path'], "bitbot.db"))
33
+ end
34
+
35
+ #
36
+ # Returns a Blockchain API helper. Everyone uses the same one,
37
+ # but it doesn't keep any state so it's fine.
38
+ #
39
+ def blockchain
40
+ @@cached_blockchain ||=
41
+ Bitbot::Blockchain.new(config['blockchain']['wallet_id'],
42
+ config['blockchain']['password1'],
43
+ config['blockchain']['password2'])
44
+ end
45
+
46
+ #
47
+ # Returns the User for the given username. If you want to get
48
+ # a User based on nick, User(nick) is easier.
49
+ #
50
+ def user_with_username(username)
51
+ bot.user_list.each do |user|
52
+ return user if user.user == username
53
+ end
54
+ nil
55
+ end
56
+
57
+ #
58
+ # Some number formatting helpers
59
+ #
60
+ def satoshi_to_str(satoshi)
61
+ str = "฿%.8f" % (satoshi.to_f / 10**8)
62
+ # strip trailing 0s
63
+ str.gsub(/0*$/, '')
64
+ end
65
+
66
+ def satoshi_to_usd(satoshi)
67
+ rate = exchange_rate("USD")
68
+ if rate
69
+ "$%.2f" % (satoshi.to_f / 10**8 * rate)
70
+ else
71
+ "$?"
72
+ end
73
+ end
74
+
75
+ def satoshi_with_usd(satoshi)
76
+ btc_str = satoshi_to_str(satoshi)
77
+ if satoshi < 0
78
+ btc_str = btc_str.irc(:red)
79
+ else
80
+ btc_str = btc_str.irc(:green)
81
+ end
82
+
83
+ usd_str = "[".irc(:grey) + satoshi_to_usd(satoshi).irc(:blue) + "]".irc(:grey)
84
+
85
+ "#{btc_str} #{usd_str}"
86
+ end
87
+ end
88
+
@@ -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 "Bitbot 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 Bitbot 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::Help
2
+ def on_help(m, args)
3
+ m.reply "Commands: balance, history, withdraw, deposit, +tip, +tipstats"
4
+ end
5
+ 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,61 @@
1
+ # coding: utf-8
2
+
3
+ module Bitbot::Tip
4
+ def on_tip(m, recipient, amount, message)
5
+ # Look up sender
6
+ user_id = db.get_or_create_user_id_for_username(m.user.user)
7
+
8
+ # Look up recipient
9
+ recipient_ircuser = m.channel.users.keys.find {|u| u.name == recipient }
10
+ unless recipient_ircuser
11
+ m.user.msg("Could not find #{recipient} in the channel list.")
12
+ return
13
+ end
14
+ recipient_user_id = db.get_or_create_user_id_for_username(recipient_ircuser.user)
15
+
16
+ # Convert amount to satoshi
17
+ satoshi = (amount.to_f * 10**8).to_i
18
+ if satoshi <= 0
19
+ m.user.msg("Cannot send a negative amount.")
20
+ return
21
+ end
22
+
23
+ # Attempt the transaction (will raise on InsufficientFunds)
24
+ begin
25
+ db.create_transaction_from_tip(user_id, recipient_user_id, satoshi, message)
26
+ rescue Bitbot::InsufficientFundsError
27
+ m.reply "Insufficient funds! It's the thought that counts.", true
28
+ return
29
+ end
30
+
31
+ # Success! Let the room know...
32
+ m.reply "[✔] Verified: ".irc(:grey).irc(:bold) +
33
+ m.user.user.irc(:bold) +
34
+ " ➜ ".irc(:grey) +
35
+ satoshi_with_usd(satoshi) +
36
+ " ➜ ".irc(:grey) +
37
+ recipient_ircuser.user.irc(:bold)
38
+
39
+ # ... and let the sender know privately ...
40
+ m.user.msg "You just sent " +
41
+ recipient_ircuser.user.irc(:bold) + " " +
42
+ satoshi_with_usd(satoshi) +
43
+ " in " +
44
+ m.channel.name.irc(:bold) +
45
+ " bringing your balance to " +
46
+ satoshi_with_usd(db.get_balance_for_user_id(user_id)) +
47
+ "."
48
+
49
+ # ... and let the recipient know privately.
50
+ recipient_ircuser.msg m.user.user.irc(:bold) +
51
+ " just sent you " +
52
+ satoshi_with_usd(satoshi) +
53
+ " in " +
54
+ m.channel.name.irc(:bold) +
55
+ " bringing your balance to " +
56
+ satoshi_with_usd(db.get_balance_for_user_id(recipient_user_id)) +
57
+ ". Type 'help' to list bitbot commands."
58
+ end
59
+ end
60
+
61
+
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+
3
+ module Bitbot::TipStats
4
+ def on_tipstats(m)
5
+ stats = db.get_tipping_stats
6
+
7
+ str = "Stats: ".irc(:grey) +
8
+ "tips today: " +
9
+ satoshi_with_usd(0 - stats[:total_tipped]) + " " +
10
+ "(#{stats[:total_tips]} tips) "
11
+
12
+ if stats[:tippers].length > 0
13
+ str += "biggest tipper: ".irc(:black) +
14
+ stats[:tippers][0][0].irc(:bold) +
15
+ " (#{satoshi_with_usd(0 - stats[:tippers][0][1])}) "
16
+ end
17
+
18
+ if stats[:tippees].length > 0
19
+ str += "biggest recipient: ".irc(:black) +
20
+ stats[:tippees][0][0].irc(:bold) +
21
+ " (#{satoshi_with_usd(stats[:tippees][0][1])}) "
22
+ end
23
+
24
+ m.reply str
25
+ end
26
+ end
27
+
28
+
29
+
@@ -0,0 +1,112 @@
1
+ module Bitbot::UpdateAddresses
2
+ def cache_file_path
3
+ File.join(config['data']['path'], "cached_addresses.yml")
4
+ end
5
+
6
+ def on_update_addresses(event = nil)
7
+ if cached_addresses.nil?
8
+ # Load from the cache, if available, on first load
9
+ self.cached_addresses = YAML.load(File.read(cache_file_path)) rescue nil
10
+ end
11
+
12
+ # Updates the cached map of depositing addresses.
13
+ new_addresses = {}
14
+ all_addresses = []
15
+
16
+ addresses = blockchain.get_addresses_in_wallet()
17
+ addresses.each do |address|
18
+ all_addresses << address["address"]
19
+ next unless address["label"] =~ /^\d+$/
20
+
21
+ user_id = address["label"].to_i
22
+
23
+ new_addresses[user_id] = address
24
+
25
+ # We set a flag on the address saying we need to get the
26
+ # confirmed balance IF the previous entry has the flag, OR
27
+ # the address is new OR if the balance does not equal the
28
+ # previous balance. We only clear the field when the balance
29
+ # equals the confirmed balance.
30
+ address["need_confirmed_balance"] = @cached_addresses[user_id]["need_confirmed_balance"] rescue true
31
+ if address["balance"] != (@cached_addresses[user_id]["balance"] rescue nil)
32
+ address["need_confirmed_balance"] = true
33
+ end
34
+ end
35
+
36
+ # Now go through new addresses, performing any confirmation checks
37
+ # for flagged ones.
38
+ new_addresses.each do |user_id, address|
39
+ if address["need_confirmed_balance"]
40
+ balance = blockchain.get_balance_for_address(address["address"])
41
+ address["confirmed_balance"] = balance
42
+
43
+ if address["confirmed_balance"] == address["balance"]
44
+ address["need_confirmed_balance"] = false
45
+ end
46
+
47
+ # Process any transactions for this address
48
+ process_new_transactions_for_address(address, user_id, all_addresses)
49
+ end
50
+ end
51
+
52
+ # Thread-safe? Sure, why not.
53
+ self.cached_addresses = new_addresses
54
+
55
+ # Cache them on disk for faster startups
56
+ File.write(cache_file_path, YAML.dump(new_addresses))
57
+ end
58
+
59
+ def process_new_transactions_for_address(address, user_id, all_addresses)
60
+ existing_transactions = {}
61
+ db.get_incoming_transaction_ids.each do |txid|
62
+ existing_transactions[txid] = true
63
+ end
64
+
65
+ response = blockchain.get_details_for_address(address["address"])
66
+
67
+ username = db.get_username_for_user_id(user_id)
68
+
69
+ response["txs"].each do |tx|
70
+ # Skip ones we already have in the database
71
+ next if existing_transactions[tx["hash"]]
72
+
73
+ # Skip any transactions that have an existing bitbot address
74
+ # as an input
75
+ if tx["inputs"].any? {|input| all_addresses.include? input["prev_out"]["addr"] }
76
+ debug "Skipping tx with bitbot input address: #{tx["hash"]}"
77
+ next
78
+ end
79
+
80
+ # find the total amount for this address
81
+ amount = 0
82
+ tx["out"].each do |out|
83
+ if out["addr"] == address["address"]
84
+ amount += out["value"]
85
+ end
86
+ end
87
+
88
+ # Skip unless it's in a block (>=1 confirmation)
89
+ if !tx["block_height"] || tx["block_height"] == 0
90
+ # TODO: only tell them this one time.
91
+ if ircuser = user_with_username(username)
92
+ ircuser.msg "Waiting for confirmation of transaction of " +
93
+ satoshi_with_usd(amount) +
94
+ " in transaction #{tx["hash"].irc(:grey)}"
95
+ end
96
+ next
97
+ end
98
+
99
+ # There is a unique constraint on incoming_transaction, so this
100
+ # will fail if for some reason we try to add it again.
101
+ if db.create_transaction_from_deposit(user_id, amount, tx["hash"])
102
+ # Notify the depositor, if they're around
103
+ if ircuser = user_with_username(username)
104
+ ircuser.msg "Received deposit of " +
105
+ satoshi_with_usd(amount) + ". Current balance is " +
106
+ satoshi_with_usd(db.get_balance_for_user_id(user_id)) + "."
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+
@@ -0,0 +1,5 @@
1
+ module Bitbot::UpdateExchangeRates
2
+ def on_update_exchange_rates(event = nil)
3
+ self.exchange_rates = blockchain.get_exchange_rates()
4
+ end
5
+ end
@@ -0,0 +1,37 @@
1
+ module Bitbot::Withdraw
2
+ def on_withdraw(m, args)
3
+ if args =~ /\s+([\d.]+)\s+([13][0-9a-zA-Z]{26,35})/
4
+ amount = $1.to_f
5
+ address = $2
6
+
7
+ user_id = db.get_or_create_user_id_for_username(m.user.user)
8
+ satoshi = (amount * 10**8).to_i
9
+
10
+ # Perform the local transaction in the database. Note that we
11
+ # don't do the blockchain update in the transaction, because we
12
+ # don't want to roll back the transaction if the blockchain update
13
+ # *appears* to fail. It might look like it failed, but really
14
+ # succeed, letting someone withdraw money twice.
15
+ # TODO: don't hardcode fee
16
+ begin
17
+ db.create_transaction_from_withdrawal(user_id, satoshi, 50000, address)
18
+ rescue Bitbot::InsufficientFundsError
19
+ m.reply "You don't have enough to withdraw #{satoshi_to_str(satoshi)} + 0.0005 fee"
20
+ return
21
+ end
22
+
23
+ response = blockchain.create_payment(address, satoshi, 50000)
24
+ if response["tx_hash"]
25
+ m.reply "Sent #{satoshi_with_usd(satoshi)} to #{address.irc(:bold)} " +
26
+ "in transaction #{response["tx_hash"].irc(:grey)}."
27
+ else
28
+ m.reply "Something may have gone wrong with your withdrawal. Please contact " +
29
+ "your friendly Bitbot administrator to investigate where your money is."
30
+ end
31
+ else
32
+ m.reply "Usage: withdraw <amount in BTC> <address>"
33
+ m.reply "0.0005 BTC will also be withdrawn for the transaction fee."
34
+ end
35
+ end
36
+ end
37
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitbot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-25 00:00:00.000000000 Z
12
+ date: 2013-05-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: cinch
@@ -122,8 +122,18 @@ files:
122
122
  - bin/bitbot
123
123
  - bitbot.gemspec
124
124
  - lib/bitbot/blockchain.rb
125
- - lib/bitbot/bot.rb
126
125
  - lib/bitbot/database.rb
126
+ - lib/bitbot/plugin.rb
127
+ - lib/bitbot/plugin/balance.rb
128
+ - lib/bitbot/plugin/common.rb
129
+ - lib/bitbot/plugin/deposit.rb
130
+ - lib/bitbot/plugin/help.rb
131
+ - lib/bitbot/plugin/history.rb
132
+ - lib/bitbot/plugin/tip.rb
133
+ - lib/bitbot/plugin/tipstats.rb
134
+ - lib/bitbot/plugin/update_addresses.rb
135
+ - lib/bitbot/plugin/update_exchange_rates.rb
136
+ - lib/bitbot/plugin/withdraw.rb
127
137
  - lib/ircstring.rb
128
138
  homepage: http://github.com/zwily/bitbot
129
139
  licenses: []
data/lib/bitbot/bot.rb DELETED
@@ -1,404 +0,0 @@
1
- # coding: utf-8
2
-
3
- require 'cinch'
4
- require 'bitbot/database'
5
- require 'bitbot/blockchain'
6
- require 'ircstring'
7
-
8
- module Bitbot
9
- class Bot < Cinch::Bot
10
- def initialize(config = {})
11
- @bot_config = config
12
- @cached_addresses = nil
13
- @cached_exchange_rates = nil
14
-
15
- super() do
16
- configure do |c|
17
- c.server = config['irc']['server']
18
- c.port = config['irc']['port']
19
- c.ssl.use = config['irc']['ssl']
20
- c.channels = config['irc']['channels']
21
- c.nick = config['irc']['nick'] || 'bitbot'
22
- c.user = config['irc']['username'] || 'bitbot'
23
- c.password = config['irc']['password']
24
- c.verbose = config['irc']['verbose']
25
- end
26
-
27
- on :private, /^help$/ do |m|
28
- self.bot.command_help(m)
29
- end
30
-
31
- on :private, /^balance/ do |m|
32
- self.bot.command_balance(m)
33
- end
34
-
35
- on :private, /^history/ do |m|
36
- self.bot.command_history(m)
37
- end
38
-
39
- on :private, /^withdraw$/ do |m|
40
- m.reply "Usage: withdraw <amount in BTC> <address>"
41
- end
42
-
43
- on :private, /^withdraw\s+([\d.]+)\s+([13][0-9a-zA-Z]{26,35})/ do |m, amount, address|
44
- self.bot.command_withdraw(m, amount, address)
45
- end
46
-
47
- on :private, /^deposit/ do |m|
48
- self.bot.command_deposit(m)
49
- end
50
-
51
- on :channel, /^\+tipstats$/ do |m|
52
- self.bot.command_tipstats(m)
53
- end
54
-
55
- on :channel, /^\+tip\s+(\w+)\s+([\d.]+)\s+?(.*)/ do |m, recipient, amount, message|
56
- self.bot.command_tip(m, recipient, amount, message)
57
- end
58
- end
59
- end
60
-
61
- def get_db
62
- Bitbot::Database.new(File.join(@bot_config['data']['path'], "bitbot.db"))
63
- end
64
-
65
- def get_blockchain
66
- Bitbot::Blockchain.new(@bot_config['blockchain']['wallet_id'],
67
- @bot_config['blockchain']['password1'],
68
- @bot_config['blockchain']['password2'])
69
- end
70
-
71
- def satoshi_to_str(satoshi)
72
- str = "฿%.8f" % (satoshi.to_f / 10**8)
73
- # strip trailing 0s
74
- str.gsub(/0*$/, '')
75
- end
76
-
77
- def satoshi_to_usd(satoshi)
78
- if @cached_exchange_rates && @cached_exchange_rates["USD"]
79
- "$%.2f" % (satoshi.to_f / 10**8 * @cached_exchange_rates["USD"]["15m"])
80
- else
81
- "$?"
82
- end
83
- end
84
-
85
- def satoshi_with_usd(satoshi)
86
- btc_str = satoshi_to_str(satoshi)
87
- if satoshi < 0
88
- btc_str = btc_str.irc(:red)
89
- else
90
- btc_str = btc_str.irc(:green)
91
- end
92
-
93
- usd_str = "[".irc(:grey) + satoshi_to_usd(satoshi).irc(:blue) + "]".irc(:grey)
94
-
95
- "#{btc_str} #{usd_str}"
96
- end
97
-
98
- # This should be called periodically to keep exchange rates up to
99
- # date.
100
- def update_exchange_rates
101
- @cached_exchange_rates = get_blockchain().get_exchange_rates()
102
- end
103
-
104
- # This method needs to be called periodically, like every minute in
105
- # order to process new transactions.
106
- def update_addresses
107
- cache_file = File.join(@bot_config['data']['path'], "cached_addresses.yml")
108
- if @cached_addresses.nil?
109
- # Load from the cache, if available, on first load
110
- @cached_addresses = YAML.load(File.read(cache_file)) rescue nil
111
- end
112
-
113
- blockchain = get_blockchain()
114
-
115
- # Updates the cached map of depositing addresses.
116
- new_addresses = {}
117
- all_addresses = []
118
-
119
- addresses = blockchain.get_addresses_in_wallet()
120
- addresses.each do |address|
121
- all_addresses << address["address"]
122
- next unless address["label"] =~ /^\d+$/
123
-
124
- user_id = address["label"].to_i
125
-
126
- new_addresses[user_id] = address
127
-
128
- # We set a flag on the address saying we need to get the
129
- # confirmed balance IF the previous entry has the flag, OR
130
- # the address is new OR if the balance does not equal the
131
- # previous balance. We only clear the field when the balance
132
- # equals the confirmed balance.
133
- address["need_confirmed_balance"] = @cached_addresses[user_id]["need_confirmed_balance"] rescue true
134
- if address["balance"] != (@cached_addresses[user_id]["balance"] rescue nil)
135
- address["need_confirmed_balance"] = true
136
- end
137
- end
138
-
139
- # Now go through new addresses, performing any confirmation checks
140
- # for flagged ones.
141
- new_addresses.each do |user_id, address|
142
- if address["need_confirmed_balance"]
143
- balance = blockchain.get_balance_for_address(address["address"])
144
- address["confirmed_balance"] = balance
145
-
146
- if address["confirmed_balance"] == address["balance"]
147
- address["need_confirmed_balance"] = false
148
- end
149
-
150
- # Process any transactions for this address
151
- self.process_new_transactions_for_address(address, user_id, all_addresses)
152
- end
153
- end
154
-
155
- # Thread-safe? Sure, why not.
156
- @cached_addresses = new_addresses
157
-
158
- # Cache them on disk for faster startups
159
- File.write(cache_file, YAML.dump(@cached_addresses))
160
- end
161
-
162
- def process_new_transactions_for_address(address, user_id, all_addresses)
163
- db = get_db()
164
- blockchain = get_blockchain()
165
-
166
- existing_transactions = {}
167
- db.get_incoming_transaction_ids().each do |txid|
168
- existing_transactions[txid] = true
169
- end
170
-
171
- response = blockchain.get_details_for_address(address["address"])
172
-
173
- username = db.get_username_for_user_id(user_id)
174
- ircuser = self.user_with_username(username)
175
-
176
- response["txs"].each do |tx|
177
- # Skip ones we already have in the database
178
- next if existing_transactions[tx["hash"]]
179
-
180
- # Skip any transactions that have an existing bitbot address
181
- # as an input
182
- if tx["inputs"].any? {|input| all_addresses.include? input["prev_out"]["addr"] }
183
- debug "Skipping tx with bitbot input address: #{tx["hash"]}"
184
- next
185
- end
186
-
187
- # find the total amount for this address
188
- amount = 0
189
- tx["out"].each do |out|
190
- if out["addr"] == address["address"]
191
- amount += out["value"]
192
- end
193
- end
194
-
195
- # Skip unless it's in a block (>=1 confirmation)
196
- if !tx["block_height"] || tx["block_height"] == 0
197
- ircuser.msg "Waiting for confirmation of transaction of " +
198
- satoshi_with_usd(amount) +
199
- " in transaction #{tx["hash"].irc(:grey)}"
200
- next
201
- end
202
-
203
- # There is a unique constraint on incoming_transaction, so this
204
- # will fail if for some reason we try to add it again.
205
- if db.create_transaction_from_deposit(user_id, amount, tx["hash"])
206
- # Notify the depositor
207
- if ircuser
208
- ircuser.msg "Received deposit of " +
209
- satoshi_with_usd(amount) + ". Current balance is " +
210
- satoshi_with_usd(db.get_balance_for_user_id(user_id)) + "."
211
- end
212
- end
213
- end
214
- end
215
-
216
- def user_with_username(username)
217
- self.bot.user_list.each do |user|
218
- return user if user.user == username
219
- end
220
- end
221
-
222
- def command_help(msg)
223
- msg.reply "Commands: balance, history, withdraw, deposit, +tip, +tipstats"
224
- end
225
-
226
- def command_balance(msg)
227
- db = get_db()
228
- user_id = db.get_or_create_user_id_for_username(msg.user.user)
229
-
230
- msg.reply "Balance is #{satoshi_with_usd(db.get_balance_for_user_id(user_id))}"
231
- end
232
-
233
- def command_deposit(msg, create = true)
234
- db = get_db()
235
- user_id = db.get_or_create_user_id_for_username(msg.user.user)
236
-
237
- unless @cached_addresses
238
- msg.reply "Bitbot is not initialized yet. Please try again later."
239
- return
240
- end
241
-
242
- if address = @cached_addresses[user_id]
243
- msg.reply "Send deposits to #{address["address"].irc(:bold)}. " +
244
- "This address is specific to you, and any funds delivered " +
245
- "to it will be added to your account after confirmation."
246
- return
247
- end
248
-
249
- unless create
250
- msg.reply "There was a problem getting your deposit address. " +
251
- "Please contact your friends Bitbot admin."
252
- return
253
- end
254
-
255
- # Attempt to create an address.
256
- blockchain = get_blockchain()
257
- blockchain.create_deposit_address_for_user_id(user_id)
258
-
259
- # Force a refresh of the cached address list...
260
- self.update_addresses()
261
-
262
- self.command_deposit(msg, false)
263
- end
264
-
265
- def command_history(msg)
266
- db = get_db()
267
- user_id = db.get_or_create_user_id_for_username(msg.user.user)
268
-
269
- command_balance(msg)
270
-
271
- n = 0
272
- db.get_transactions_for_user_id(user_id).each do |tx|
273
- time = Time.at(tx[:created_at].to_i).strftime("%Y-%m-%d")
274
- amount = satoshi_with_usd(tx[:amount])
275
- action = if tx[:amount] < 0 && tx[:other_user_id]
276
- "to #{tx[:other_username]}"
277
- elsif tx[:amount] > 0 && tx[:other_user_id]
278
- "from #{tx[:other_username]}"
279
- elsif tx[:withdrawal_address]
280
- "withdrawal to #{tx[:withdrawal_address]}"
281
- elsif tx[:incoming_transaction]
282
- "deposit from tx #{tx[:incoming_transaction]}"
283
- end
284
-
285
- msg.reply "#{time.irc(:grey)}: #{amount} #{action} #{"(#{tx[:note]})".irc(:grey) if tx[:note]}"
286
-
287
- n += 1
288
- break if n >= 10
289
- end
290
- end
291
-
292
- def command_withdraw(msg, amount, address)
293
- db = get_db()
294
- user_id = db.get_or_create_user_id_for_username(msg.user.user)
295
-
296
- satoshi = (amount.to_f * 10**8).to_i
297
-
298
- # Perform the local transaction in the database. Note that we
299
- # don't do the blockchain update in the transaction, because we
300
- # don't want to roll back the transaction if the blockchain update
301
- # *appears* to fail. It might look like it failed, but really
302
- # succeed, letting someone withdraw money twice.
303
- # TODO: don't hardcode fee
304
- begin
305
- db.create_transaction_from_withdrawal(user_id, satoshi, 500000, address)
306
- rescue InsufficientFundsError
307
- msg.reply "You don't have enough to withdraw #{satoshi_to_str(satoshi)} + 0.0005 fee"
308
- return
309
- end
310
-
311
- blockchain = get_blockchain()
312
- response = blockchain.create_payment(address, satoshi, 500000)
313
- if response["tx_hash"]
314
- msg.reply "Sent #{satoshi_with_usd(satoshi)} to #{address.irc(:bold)} " +
315
- "in transaction #{response["tx_hash"].irc(:grey)}."
316
- else
317
- msg.reply "Something may have gone wrong with your withdrawal. Please contact " +
318
- "your friendly Bitbot administrator to investigate where your money is."
319
- end
320
- end
321
-
322
- def command_tipstats(msg)
323
- db = get_db()
324
- stats = db.get_tipping_stats
325
-
326
- str = "Stats: ".irc(:grey) +
327
- "tips today: " +
328
- satoshi_with_usd(0 - stats[:total_tipped]) + " " +
329
- "#{stats[:total_tips]} tips "
330
-
331
- if stats[:tippers].length > 0
332
- str += "biggest tipper: ".irc(:black) +
333
- stats[:tippers][0][0].irc(:bold) +
334
- " (#{satoshi_with_usd(0 - stats[:tippers][0][1])}) "
335
- end
336
-
337
- if stats[:tippees].length > 0
338
- str += "biggest recipient: ".irc(:black) +
339
- stats[:tippees][0][0].irc(:bold) +
340
- " (#{satoshi_with_usd(stats[:tippees][0][1])}) "
341
- end
342
-
343
- msg.reply str
344
- end
345
-
346
- def command_tip(msg, recipient, amount, message)
347
- db = get_db()
348
-
349
- # Look up sender
350
- user_id = db.get_or_create_user_id_for_username(msg.user.user)
351
-
352
- # Look up recipient
353
- recipient_ircuser = msg.channel.users.keys.find {|u| u.name == recipient }
354
- unless recipient_ircuser
355
- msg.user.msg("Could not find #{recipient} in the channel list.")
356
- return
357
- end
358
- recipient_user_id = db.get_or_create_user_id_for_username(recipient_ircuser.user)
359
-
360
- # Convert amount to satoshi
361
- satoshi = (amount.to_f * 10**8).to_i
362
- if satoshi <= 0
363
- msg.user.msg("Cannot send a negative amount.")
364
- return
365
- end
366
-
367
- # Attempt the transaction (will raise on InsufficientFunds)
368
- begin
369
- db.create_transaction_from_tip(user_id, recipient_user_id, satoshi, message)
370
- rescue InsufficientFundsError
371
- msg.reply "Insufficient funds! It's the thought that counts.", true
372
- return
373
- end
374
-
375
- # Success! Let the room know...
376
- msg.reply "[✔] Verified: ".irc(:grey).irc(:bold) +
377
- msg.user.user.irc(:bold) +
378
- " ➜ ".irc(:grey) +
379
- satoshi_with_usd(satoshi) +
380
- " ➜ ".irc(:grey) +
381
- recipient_ircuser.user.irc(:bold)
382
-
383
- # ... and let the sender know privately ...
384
- msg.user.msg "You just sent " +
385
- recipient_ircuser.user.irc(:bold) + " " +
386
- satoshi_with_usd(satoshi) +
387
- " in " +
388
- msg.channel.name.irc(:bold) +
389
- " bringing your balance to " +
390
- satoshi_with_usd(db.get_balance_for_user_id(user_id)) +
391
- "."
392
-
393
- # ... and let the recipient know privately.
394
- recipient_ircuser.msg msg.user.user.irc(:bold) +
395
- " just sent you " +
396
- satoshi_with_usd(satoshi) +
397
- " in " +
398
- msg.channel.name.irc(:bold) +
399
- " bringing your balance to " +
400
- satoshi_with_usd(db.get_balance_for_user_id(recipient_user_id)) +
401
- ". Type 'help' to list bitbot commands."
402
- end
403
- end
404
- end