alexmchale-commerce-bank-client 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +0 -0
- data/VERSION.yml +4 -0
- data/lib/commercebank.rb +332 -0
- data/lib/commercebank/appconfig.rb +28 -0
- data/lib/commercebank/gmail.rb +156 -0
- data/lib/commercebank/monkey.rb +98 -0
- data/test/commerce_bank_client_test.rb +6 -0
- data/test/monkeypatch_test.rb +11 -0
- data/test/test_helper.rb +10 -0
- metadata +82 -0
data/README.markdown
ADDED
File without changes
|
data/VERSION.yml
ADDED
data/lib/commercebank.rb
ADDED
@@ -0,0 +1,332 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'net/http'
|
3
|
+
require 'net/https'
|
4
|
+
require 'hpricot'
|
5
|
+
require 'andand'
|
6
|
+
require 'cgi'
|
7
|
+
require 'yaml'
|
8
|
+
require 'ftools'
|
9
|
+
require 'time'
|
10
|
+
require 'date'
|
11
|
+
require 'json'
|
12
|
+
require 'htmlentities'
|
13
|
+
require 'commercebank/monkey.rb'
|
14
|
+
require 'commercebank/appconfig.rb'
|
15
|
+
require 'commercebank/gmail.rb'
|
16
|
+
|
17
|
+
class Array
|
18
|
+
def binary
|
19
|
+
map {|e| yield(e) ? [e, nil] : [nil, e]}.transpose.map {|a| a.compact}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Object
|
24
|
+
def to_cents
|
25
|
+
(to_s.gsub(/[^-.0-9]/, '').to_f * 100).to_i
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Date
|
30
|
+
def days_in_month
|
31
|
+
(Date.parse("12/31/#{strftime("%Y")}") << (12 - month)).day
|
32
|
+
end
|
33
|
+
|
34
|
+
def last_sunday
|
35
|
+
d = self
|
36
|
+
d -= 1 until d.wday == 0
|
37
|
+
d
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Hash
|
42
|
+
def to_url
|
43
|
+
map {|key, value| "#{CGI.escape key.to_s}=#{CGI.escape value.to_s}"}.join "&"
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_cookie
|
47
|
+
map {|key, value| "#{key}=#{value}"}.join('; ')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class WebClient
|
52
|
+
attr_reader :fields, :cookies
|
53
|
+
|
54
|
+
def initialize
|
55
|
+
@cookies = Hash.new
|
56
|
+
@http = Net::HTTP.new('banking.commercebank.com', 443)
|
57
|
+
@http.use_ssl = true
|
58
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
59
|
+
end
|
60
|
+
|
61
|
+
def get(path, form = nil)
|
62
|
+
response = @http.get(path, header)
|
63
|
+
add_cookies(response)
|
64
|
+
@fields = (form && get_form(response.body, form)) || Hash.new
|
65
|
+
response
|
66
|
+
end
|
67
|
+
|
68
|
+
def post(path, form = nil)
|
69
|
+
response = @http.post(path, @fields.to_url, header)
|
70
|
+
add_cookies(response)
|
71
|
+
@fields = (form && get_form(response.body, form)) || Hash.new
|
72
|
+
response
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def header
|
78
|
+
{ 'Cookie' => @cookies.to_cookie }
|
79
|
+
end
|
80
|
+
|
81
|
+
def get_form(body, name)
|
82
|
+
Hpricot.buffer_size = 262144
|
83
|
+
doc = Hpricot.parse(body)
|
84
|
+
form = (doc/"##{name}").first
|
85
|
+
fields = Hash[*((form/"input").map {|e| [ e.attributes['name'], e.attributes['value'] ]}.flatten)]
|
86
|
+
fields['TestJavaScript'] = 'OK'
|
87
|
+
fields
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_cookies(response)
|
91
|
+
CGI::Cookie.parse(response.header['set-cookie']).each do |key, value|
|
92
|
+
@cookies[key] = value.first
|
93
|
+
end
|
94
|
+
|
95
|
+
@cookies.delete 'path'
|
96
|
+
@cookies.delete 'expires'
|
97
|
+
|
98
|
+
self
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class CommerceBank
|
103
|
+
attr_reader :register, :current, :available
|
104
|
+
|
105
|
+
def initialize
|
106
|
+
@config = AppConfig.new('~/.commerce.yaml')
|
107
|
+
|
108
|
+
client = WebClient.new
|
109
|
+
|
110
|
+
client.get('/')
|
111
|
+
|
112
|
+
client.get('/CBI/login.aspx', 'MAINFORM')
|
113
|
+
|
114
|
+
client.fields['txtUserID'] = @config[:username]
|
115
|
+
response = client.post('/CBI/login.aspx', 'MAINFORM')
|
116
|
+
|
117
|
+
# If a question was asked, answer it then get the password page.
|
118
|
+
question = response.body.scan(/Your security question: (.*?)<\/td>/i).first.andand.first
|
119
|
+
if question
|
120
|
+
client.fields['txtChallengeAnswer'] = @config[question]
|
121
|
+
client.fields['saveComputer'] = 'rdoBindDeviceNo'
|
122
|
+
response = client.post('/CBI/login.aspx', 'MAINFORM')
|
123
|
+
end
|
124
|
+
|
125
|
+
raise "could not reach the password page" unless client.fields['__EVENTTARGET'] == 'btnLogin'
|
126
|
+
|
127
|
+
client.fields['txtPassword'] = @config[:password]
|
128
|
+
response = client.post('/CBI/login.aspx')
|
129
|
+
|
130
|
+
response = client.get('/CBI/Accounts/CBI/Activity.aspx', 'MAINFORM')
|
131
|
+
(@current, @available) = parse_balance(response.body)
|
132
|
+
@pending = parse_pending(response.body)
|
133
|
+
|
134
|
+
client.fields['Anthem_UpdatePage'] = 'true'
|
135
|
+
client.fields['txtFilterFromDate:textBox'] = Time.parse('1/1/2000').strftime('%m/%d/%Y')
|
136
|
+
client.fields['txtFilterToDate:textBox'] = Time.now.strftime('%m/%d/%Y')
|
137
|
+
response = client.post('/CBI/Accounts/CBI/Activity.aspx?Anthem_CallBack=true')
|
138
|
+
|
139
|
+
raw_data = JSON.parse(response.body)
|
140
|
+
@register = parse_register(raw_data['controls']['pnlPosted'])
|
141
|
+
end
|
142
|
+
|
143
|
+
def daily_summary
|
144
|
+
today, yesterday, this_week, last_week = [], [], [], []
|
145
|
+
|
146
|
+
register.each do |entry|
|
147
|
+
if entry[:date] == Date.today then today << entry
|
148
|
+
elsif entry[:date] == (Date.today - 1) then yesterday << entry
|
149
|
+
elsif entry[:date] >= Date.today.last_sunday then this_week << entry
|
150
|
+
elsif entry[:date] >= (Date.today.last_sunday - 1).last_sunday then last_week << entry
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
{ 'Pending' => @pending,
|
155
|
+
'Today' => today,
|
156
|
+
'Yesterday' => yesterday,
|
157
|
+
'This Week' => this_week,
|
158
|
+
'Last Week' => last_week,
|
159
|
+
:order => [ 'Pending', 'Today', 'Yesterday', 'This Week', 'Last Week' ] }
|
160
|
+
end
|
161
|
+
|
162
|
+
def monthly_summary(day_in_month = (Date.today - Date.today.day))
|
163
|
+
first_of_month = day_in_month - day_in_month.day + 1
|
164
|
+
last_of_month = first_of_month + day_in_month.days_in_month - 1
|
165
|
+
entries = register.find_all {|entry| entry[:date] >= first_of_month && entry[:date] <= last_of_month}
|
166
|
+
{ day_in_month.strftime('%B') => entries }
|
167
|
+
end
|
168
|
+
|
169
|
+
def print_all
|
170
|
+
summarize 'All' => register
|
171
|
+
end
|
172
|
+
|
173
|
+
def print_daily_summary
|
174
|
+
print(summarize(daily_summary))
|
175
|
+
end
|
176
|
+
|
177
|
+
def gmail_daily_summary
|
178
|
+
subject = "Daily Summary"
|
179
|
+
summary = summarize_html(daily_summary)
|
180
|
+
|
181
|
+
username = @config['GMail Username']
|
182
|
+
password = @config['GMail Password']
|
183
|
+
|
184
|
+
GMail.new(username, password).send(username, subject, summary, 'text/html')
|
185
|
+
end
|
186
|
+
|
187
|
+
def gmail_monthly_summary
|
188
|
+
last_month = Date.today - Date.today.day
|
189
|
+
subject = "#{last_month.strftime('%B')} Summary"
|
190
|
+
summary = summarize_html(monthly_summary(last_month))
|
191
|
+
|
192
|
+
username = @config['GMail Username']
|
193
|
+
password = @config['GMail Password']
|
194
|
+
|
195
|
+
GMail.new(username, password).send(username, subject, summary, 'text/html')
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def summarize(entries)
|
201
|
+
(entries[:order] || entries.keys).map do |label|
|
202
|
+
next if entries[label].length == 0
|
203
|
+
|
204
|
+
label.to_s + ":\n" + entries[label].map do |e|
|
205
|
+
[
|
206
|
+
e[:date].strftime('%02m/%02d/%04Y '),
|
207
|
+
"%-100s " % e[:destination],
|
208
|
+
"%10s " % e[:delta].to_dollars(:show_plus),
|
209
|
+
e[:total] && ("%10s " % e[:total].to_dollars),
|
210
|
+
"\n"
|
211
|
+
].compact.join
|
212
|
+
end.join
|
213
|
+
end.compact.join("\n")
|
214
|
+
end
|
215
|
+
|
216
|
+
def summarize_html(entries)
|
217
|
+
html = ''
|
218
|
+
|
219
|
+
(entries[:order] || entries.keys).each do |label|
|
220
|
+
next if entries[label].length == 0
|
221
|
+
|
222
|
+
use_total = entries[label].find {|e| e[:total]}
|
223
|
+
|
224
|
+
html += '<h2 style="font-family: garamond, georgia, serif">' + label + '</h2>'
|
225
|
+
|
226
|
+
html += '<table cellspacing="0" cellpadding="5" style="font-size: 12px; border-style: solid; border-width: 2px; border-color: #DDDDDD" width="100%">'
|
227
|
+
|
228
|
+
html += '<tr style="font-weight: bold; background-color: #DDDDDD">'
|
229
|
+
html += '<th style="text-align: left" width="75">Date</th>'
|
230
|
+
html += '<th style="text-align: left">Destination</th>'
|
231
|
+
html += '<th style="text-align: right" width="75">Amount</th>'
|
232
|
+
html += '<th style="text-align: right" width="75">Total</th>' if use_total
|
233
|
+
html += '</tr>'
|
234
|
+
|
235
|
+
even = true
|
236
|
+
entries[label].each do |e|
|
237
|
+
even = !even
|
238
|
+
|
239
|
+
delta = "%s%0.2f" % [ (e[:delta] >= 0 ? '+' : '-'), e[:delta].abs/100.0 ]
|
240
|
+
total = "%0.2f" % (e[:total].to_i/100.0)
|
241
|
+
|
242
|
+
row_style = {
|
243
|
+
'font-weight' => 'normal',
|
244
|
+
'background-color' => even ? '#DDDDDD' : '#FFFFFF'
|
245
|
+
}.map {|k, v| "#{k}: #{v}"}.join('; ')
|
246
|
+
|
247
|
+
html += sprintf '<tr style="%s">', row_style
|
248
|
+
html += '<td style="text-align: left">' + e[:date].strftime('%m/%d/%Y') + '</td>'
|
249
|
+
html += '<td style="text-align: left">' + e[:destination] + '</td>'
|
250
|
+
html += '<td style="text-align: right">' + delta + '</td>'
|
251
|
+
html += '<td style="text-align: right">' + total + '</td>' if use_total
|
252
|
+
html += '</tr>'
|
253
|
+
end
|
254
|
+
|
255
|
+
html += '</table>'
|
256
|
+
end
|
257
|
+
|
258
|
+
html
|
259
|
+
end
|
260
|
+
|
261
|
+
def parse_balance(body)
|
262
|
+
Hpricot.buffer_size = 262144
|
263
|
+
doc = Hpricot.parse(body)
|
264
|
+
summaryRows = doc/"table.summaryTable"/"tr"
|
265
|
+
current = (summaryRows[3]/"td")[1].inner_html.to_cents
|
266
|
+
available = (summaryRows[4]/"td")[1].inner_html.to_cents
|
267
|
+
[current, available]
|
268
|
+
end
|
269
|
+
|
270
|
+
def parse_pending(body)
|
271
|
+
Hpricot.buffer_size = 262144
|
272
|
+
doc = Hpricot.parse(body)
|
273
|
+
coder = HTMLEntities.new
|
274
|
+
|
275
|
+
(doc/"#grdMemoPosted"/"tr").map do |e|
|
276
|
+
next nil unless (e['class'] == 'item' || e['class'] == 'alternatingItem')
|
277
|
+
|
278
|
+
values = (e/"td").map {|e1| coder.decode(e1.inner_html.strip)}
|
279
|
+
|
280
|
+
debit = values[2].to_cents
|
281
|
+
credit = values[3].to_cents
|
282
|
+
delta = credit - debit
|
283
|
+
|
284
|
+
{ :date => Date.parse(values[0]),
|
285
|
+
:destination => values[1],
|
286
|
+
:delta => delta,
|
287
|
+
:debit => debit,
|
288
|
+
:credit => credit }
|
289
|
+
end.compact
|
290
|
+
end
|
291
|
+
|
292
|
+
def parse_register(body)
|
293
|
+
Hpricot.buffer_size = 262144
|
294
|
+
doc = Hpricot.parse(body)
|
295
|
+
coder = HTMLEntities.new
|
296
|
+
(doc/"#grdHistory"/"tr").map do |e|
|
297
|
+
next nil unless [ 'item', 'alternatingitem' ].include? e['class'].to_s.downcase
|
298
|
+
|
299
|
+
anchor = e.at("a")
|
300
|
+
values = (e/"td").map {|e1| e1.inner_html}
|
301
|
+
date = Date.parse(values[0])
|
302
|
+
check = values[1].strip
|
303
|
+
debit = values[3].to_cents
|
304
|
+
credit = values[4].to_cents
|
305
|
+
delta = credit - debit
|
306
|
+
total = values[5].to_cents
|
307
|
+
|
308
|
+
images = (e/"a").find_all do |e1|
|
309
|
+
e1['target'].to_s.downcase == 'checkimage'
|
310
|
+
end.map do |e1|
|
311
|
+
{ :url => e1['href'], :title => e1.inner_html.strip }
|
312
|
+
end
|
313
|
+
|
314
|
+
{
|
315
|
+
:destination => coder.decode(anchor.inner_html.strip),
|
316
|
+
:url => anchor['href'],
|
317
|
+
:date => date,
|
318
|
+
:check => check,
|
319
|
+
:images => images,
|
320
|
+
:delta => delta,
|
321
|
+
:debit => debit,
|
322
|
+
:credit => credit,
|
323
|
+
:total => total
|
324
|
+
}
|
325
|
+
end.compact
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
if $0 == __FILE__
|
330
|
+
cb = CommerceBank.new
|
331
|
+
cb.gmail_daily_summary
|
332
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'pp'
|
3
|
+
require 'andand'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
class AppConfig
|
7
|
+
def initialize(path)
|
8
|
+
@path = path
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](field)
|
12
|
+
field = field.to_s
|
13
|
+
path = File.expand_path(@path)
|
14
|
+
config = File.exists?(path) ? YAML.load(File.read path) : Hash.new
|
15
|
+
|
16
|
+
unless config[field]
|
17
|
+
print "Please enter the following:\n"
|
18
|
+
print field, ": "
|
19
|
+
|
20
|
+
config[field] = gets.to_s.chomp
|
21
|
+
|
22
|
+
File.open(path, 'w') {|file| file.write(config.to_yaml)}
|
23
|
+
File.chmod(0600, path)
|
24
|
+
end
|
25
|
+
|
26
|
+
config[field]
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'net/smtp'
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
class GMail
|
6
|
+
def initialize(username, password)
|
7
|
+
@username = username
|
8
|
+
@password = password
|
9
|
+
end
|
10
|
+
|
11
|
+
def start(to, subject, body, body_type = 'text/plain')
|
12
|
+
@to = to.to_s
|
13
|
+
@subject = subject.to_s
|
14
|
+
@body = body.to_s
|
15
|
+
@body_type = body_type
|
16
|
+
@attachments = []
|
17
|
+
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_data(name, data, type)
|
22
|
+
@attachments << { :name => name, :data => data, :type => type }
|
23
|
+
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_file(filename, content_type)
|
28
|
+
add_data File.basename(File.expand_path(filename)), File.read(filename), content_type
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_jpeg(filename, data = nil)
|
32
|
+
name = File.basename filename
|
33
|
+
data ||= File.read(filename)
|
34
|
+
type = 'image/jpeg'
|
35
|
+
|
36
|
+
add_data name, data, type
|
37
|
+
end
|
38
|
+
|
39
|
+
def compose
|
40
|
+
boundary = rand(2**128).to_s(16)
|
41
|
+
|
42
|
+
if @attachments.length > 0
|
43
|
+
attachments = @attachments.map do |attachment|
|
44
|
+
"--#{boundary}\r\n" +
|
45
|
+
"Content-Type: #{attachment[:type]}; name=\"#{attachment[:name]}\"\r\n" +
|
46
|
+
"Content-Disposition: attachment; filename=\"#{attachment[:name]}\"\r\n" +
|
47
|
+
"Content-Transfer-Encoding: base64\r\n" +
|
48
|
+
"\r\n" +
|
49
|
+
Base64.encode64(attachment[:data])
|
50
|
+
end.compact.join
|
51
|
+
|
52
|
+
"Date: #{Time.now.to_s}\r\n" +
|
53
|
+
"From: #{@username}\r\n" +
|
54
|
+
"To: #{@to}\r\n" +
|
55
|
+
"Subject: #{@subject}\r\n" +
|
56
|
+
"MIME-Version: 1.0\r\n" +
|
57
|
+
"Content-Type: multipart/mixed; boundary=\"#{boundary}\"\r\n" +
|
58
|
+
"\r\n" +
|
59
|
+
"--#{boundary}\r\n" +
|
60
|
+
"Content-Type: text/plain\r\n" +
|
61
|
+
"\r\n" +
|
62
|
+
"#{@body}\r\n" +
|
63
|
+
"\r\n" +
|
64
|
+
attachments +
|
65
|
+
"--#{boundary}--\r\n" +
|
66
|
+
"\r\n.\r\n"
|
67
|
+
else
|
68
|
+
"Date: #{Time.now.to_s}\r\n" +
|
69
|
+
"From: #{@username}\r\n" +
|
70
|
+
"To: #{@to}\r\n" +
|
71
|
+
"Subject: #{@subject}\r\n" +
|
72
|
+
"Content-Type: text/plain\r\n" +
|
73
|
+
"\r\n" +
|
74
|
+
"#{@body}\r\n" +
|
75
|
+
"\r\n.\r\n"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def dispatch
|
80
|
+
Net::SMTP.start('smtp.gmail.com', 587, 'gmail.com', @username, @password, :plain) do |smtp|
|
81
|
+
smtp.send_message compose, @username, @to
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def send(to, subject, body, content_type = 'text/plan')
|
86
|
+
Net::SMTP.start('smtp.gmail.com', 587, 'gmail.com', @username, @password, :plain) do |smtp|
|
87
|
+
msg = "From: #{@username}\r\nTo: #{to}\r\nSubject: #{subject}\r\nContent-Type: #{content_type}\r\n\r\n#{body}"
|
88
|
+
smtp.send_message msg, @username, to
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Net::SMTP monkeypatching was taken from:
|
94
|
+
# http://www.stephenchu.com/2006/06/how-to-use-gmail-smtp-server-to-send.html
|
95
|
+
Net::SMTP.class_eval do
|
96
|
+
private
|
97
|
+
def do_start(helodomain, user, secret, authtype)
|
98
|
+
raise IOError, 'SMTP session already started' if @started
|
99
|
+
check_auth_args(user, secret) if (user or secret)
|
100
|
+
|
101
|
+
sock = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
|
102
|
+
@socket = Net::InternetMessageIO.new(sock)
|
103
|
+
@socket.read_timeout = 60 #@read_timeout
|
104
|
+
@socket.debug_output = STDERR #@debug_output
|
105
|
+
|
106
|
+
check_response(critical { recv_response() })
|
107
|
+
do_helo(helodomain)
|
108
|
+
|
109
|
+
raise 'openssl library not installed' unless defined?(OpenSSL)
|
110
|
+
starttls
|
111
|
+
ssl = OpenSSL::SSL::SSLSocket.new(sock)
|
112
|
+
ssl.sync_close = true
|
113
|
+
ssl.connect
|
114
|
+
@socket = Net::InternetMessageIO.new(ssl)
|
115
|
+
@socket.read_timeout = 60 #@read_timeout
|
116
|
+
@socket.debug_output = STDERR #@debug_output
|
117
|
+
do_helo(helodomain)
|
118
|
+
|
119
|
+
authenticate user, secret, authtype if user
|
120
|
+
@started = true
|
121
|
+
ensure
|
122
|
+
unless @started
|
123
|
+
# authentication failed, cancel connection.
|
124
|
+
@socket.close if not @started and @socket and not @socket.closed?
|
125
|
+
@socket = nil
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def do_helo(helodomain)
|
130
|
+
begin
|
131
|
+
if @esmtp
|
132
|
+
ehlo helodomain
|
133
|
+
else
|
134
|
+
helo helodomain
|
135
|
+
end
|
136
|
+
rescue Net::ProtocolError
|
137
|
+
if @esmtp
|
138
|
+
@esmtp = false
|
139
|
+
@error_occured = false
|
140
|
+
retry
|
141
|
+
end
|
142
|
+
raise
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def starttls
|
147
|
+
getok('STARTTLS')
|
148
|
+
end
|
149
|
+
|
150
|
+
def quit
|
151
|
+
begin
|
152
|
+
getok('QUIT')
|
153
|
+
rescue EOFError
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'pp'
|
3
|
+
require 'andand'
|
4
|
+
require 'cgi'
|
5
|
+
require 'yaml'
|
6
|
+
require 'RMagick'
|
7
|
+
|
8
|
+
class Array
|
9
|
+
def binary
|
10
|
+
map {|e| yield(e) ? [e, nil] : [nil, e]}.transpose.map {|a| a.compact}
|
11
|
+
end
|
12
|
+
|
13
|
+
def paramify
|
14
|
+
hash = Hash.new
|
15
|
+
|
16
|
+
hash.merge! pop if last.kind_of? Hash
|
17
|
+
each {|e| hash[e] = true}
|
18
|
+
|
19
|
+
hash
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Object
|
24
|
+
def to_cents
|
25
|
+
(to_s.gsub(/[^-.0-9]/, '').to_f * 100).to_i
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_dollars(*options)
|
29
|
+
options = options.paramify
|
30
|
+
|
31
|
+
plus = options[:show_plus] ? '+' : ''
|
32
|
+
minus = options[:hide_minus] ? '' : '-'
|
33
|
+
sign = to_i >= 0 ? plus : minus
|
34
|
+
|
35
|
+
("%s%0.2f" % [ sign, to_i.abs / 100.0 ]).commify
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Date
|
40
|
+
def days_in_month
|
41
|
+
(Date.parse("12/31/#{strftime("%Y")}") << (12 - month)).day
|
42
|
+
end
|
43
|
+
|
44
|
+
def last_sunday
|
45
|
+
d = self
|
46
|
+
d -= 1 until d.wday == 0
|
47
|
+
d
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Hash
|
52
|
+
def to_url
|
53
|
+
map {|key, value| "#{CGI.escape key.to_s}=#{CGI.escape value.to_s}"}.join "&"
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_cookie
|
57
|
+
map {|key, value| "#{key}=#{value}"}.join('; ')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class String
|
62
|
+
def commify
|
63
|
+
reverse.gsub(/(\d\d\d)(?=\d)/, '\1,').reverse
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module Magick
|
68
|
+
class Image
|
69
|
+
def autocrop(red = 65535, green = 65535, blue = 65535)
|
70
|
+
low_x = 0
|
71
|
+
low_y = 0
|
72
|
+
high_x = columns
|
73
|
+
high_y = rows
|
74
|
+
|
75
|
+
croppable = Proc.new do |x, y|
|
76
|
+
pixel = pixel_color(x, y)
|
77
|
+
(pixel.red == red) && (pixel.green == green) && (pixel.blue == blue)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Scan the top horizontal.
|
81
|
+
low_y += 1 until (low_y == rows) || (low_x..high_x).find {|x| !croppable.call(x, low_y)}
|
82
|
+
|
83
|
+
# Scan the bottom horizontal.
|
84
|
+
high_y -= 1 until (low_y == high_y) || (low_x..high_x).find {|x| !croppable.call(x, high_y)}
|
85
|
+
|
86
|
+
# Scan the left vertical.
|
87
|
+
low_x += 1 until (low_x == columns) || (low_y..high_y).find {|y| !croppable.call(low_x, y)}
|
88
|
+
|
89
|
+
# Scan the right vertical.
|
90
|
+
high_x -= 1 until (low_x == high_x) || (low_y..high_y).find {|y| !croppable.call(high_x, y)}
|
91
|
+
|
92
|
+
width = high_x - low_x
|
93
|
+
height = high_y - low_y
|
94
|
+
|
95
|
+
crop low_x, low_y, width, height
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class MonkeyPatchTest < Test::Unit::TestCase
|
4
|
+
should "convert a hash to a valid url parameter string" do
|
5
|
+
h1 = { :foo => 1, :bar => 2, :baz => 3 }
|
6
|
+
assert_equal h1.to_url, 'foo=1&bar=2&baz=3'
|
7
|
+
|
8
|
+
h2 = { :foo => "What's up Doc", :bar => "1" }
|
9
|
+
assert_equal h2.to_url, "foo=What%27s+up+Doc&bar=1"
|
10
|
+
end
|
11
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: alexmchale-commerce-bank-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alex McHale
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-03-10 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: json
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.1.3
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: andand
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.3.1
|
34
|
+
version:
|
35
|
+
description:
|
36
|
+
email: alexmchale@gmail.com
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- README.markdown
|
43
|
+
files:
|
44
|
+
- VERSION.yml
|
45
|
+
- README.markdown
|
46
|
+
- lib/commercebank.rb
|
47
|
+
- lib/commercebank
|
48
|
+
- lib/commercebank/monkey.rb
|
49
|
+
- lib/commercebank/gmail.rb
|
50
|
+
- lib/commercebank/appconfig.rb
|
51
|
+
- test/monkeypatch_test.rb
|
52
|
+
- test/test_helper.rb
|
53
|
+
- test/commerce_bank_client_test.rb
|
54
|
+
has_rdoc: true
|
55
|
+
homepage: http://github.com/alexmchale/commerce-bank-client
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options:
|
58
|
+
- --inline-source
|
59
|
+
- --charset=UTF-8
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
version:
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: "0"
|
73
|
+
version:
|
74
|
+
requirements: []
|
75
|
+
|
76
|
+
rubyforge_project:
|
77
|
+
rubygems_version: 1.2.0
|
78
|
+
signing_key:
|
79
|
+
specification_version: 2
|
80
|
+
summary: CBC is a client for Commerce Bank's website.
|
81
|
+
test_files: []
|
82
|
+
|