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.
@@ -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