-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathvenmo_history.rb
210 lines (173 loc) · 6.98 KB
/
venmo_history.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#!/usr/bin/env ruby
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'http'
gem 'nap', require: 'rest'
gem 'cocoapods', '~> 0.34.1'
gem 'csv'
gem 'addressable'
gem 'optparse'
end
EVENTS_ENDPOINT_RESULT_LIMIT = 50 # Could be changed by Venmo without notice
MAX_EVENT_RECORDS_TO_RETRIEVE = 2000 # Script will stop once 2,000 records are fetched. Setting as a precaution.
def DISPLAY_USAGE_AND_EXIT
puts "\nUsage: ruby venmo_history.rb [OPTIONS]\n\n"
puts " 🗃 Export all of your venmo history to CSV."
puts " 💻 Developed by Avery Ryder -> (Github: ryderpro)\n\n"
puts "Required Options:\n\n"
puts " -d, -date [2018-01-01] fetch transactions from now to past date"
puts " -t, -token [TOKEN] access token to use Venmo API\n\n"
puts "More Options:\n\n"
puts " -b, -before_id [ID] start after specific transaction_id"
puts " -h, -help show this message and exit.\n\n"
exit
end
##### PARSE ARGUMENTS #####
@params = {}
@username = nil
@user_id = nil
@last_date_to_include = nil
OptionParser.new do |parser|
parser.on("-d STR", "--date", String)
parser.on("-t STR", "--token", String)
parser.on("-b STR", "--before_id", String)
parser.on("-h STR", "---help") do
DISPLAY_USAGE_AND_EXIT()
end
end.parse!(into: @params)
unless @params[:date] || @params[:token]
puts "\n🚷 Missing required arguments. Run $ ruby venmo_history.rb -help\n\n"
exit
end
@last_date_to_include = Date.parse(@params[:date])
@before_id_argument = @params[:before_id] if @params[:before_id]
puts "\n💎 Getting ready to fetch Venmos from now back to #{@params[:date]}...\n\n"
##### HELPERS #####
def get(path, params = {})
headers = {:accept => "application/json", :cookie => "api_access_token=#{@params[:token]}"}
url = venmo_url(path)
response = HTTP.headers(headers).get(venmo_url(path), :params => params)
puts "🐕 Fetching #{path} with #{params}... #{response.status}"
unless response.status.success?
responseBody = response.parse
puts "🚷 Error trying requesting #{path}...\n\n"
puts "Error Message: #{responseBody["error"]["message"]}\n\n"
exit
end
parsed_response = response.parse
puts "🐕🐕🐕 Retrieved #{parsed_response["data"].count} records.\n\n" if parsed_response["data"]
parsed_response
end
def venmo_url(path)
Addressable::URI.new({scheme: "https",host: "api.venmo.com",path: File.join("v1", path)})
end
def get_username_and_id
self_response = get("me")
@username = self_response["data"]["user"]["username"]
@user_id = self_response["data"]["user"]["id"]
if !@username || @username.to_s == '' || !@user_id || @user_id.to_s == ''
puts "🚷 Exiting, test fetch for /me returned no username and/or id."
end
puts "💎 Found username - #{@username} \n\n"
end
# Results are sorted in descending order by "date_updated". Most recent events first.
def fetch_paginated_events(options = {})
paginated_get("stories/target-or-actor/#{@user_id}", options)
end
def paginated_get(path, options = {})
Enumerator.new do |y|
query_params = {limit: EVENTS_ENDPOINT_RESULT_LIMIT}
before_id = @params[:before_id] || nil
total = 0
max_results = options[:limit]
date_lower_limit = options[:to_date]
loop do
query_params[:before_id] = before_id if before_id
query_params[:limit] = max_results - total if (max_results - total) < EVENTS_ENDPOINT_RESULT_LIMIT
response = get(path, query_params)
data = response["data"]
total += data.length
data.each do |element|
y.yield element
end
break if !response["pagination"]["next"]
last_event_date = Date.parse(data.last["date_updated"])
break if (data.empty? || total >= max_results || last_event_date <= date_lower_limit)
before_id = data.last["id"]
end
end
end
##### MAIN #####
# Fetch username and userId
get_username_and_id
# Fetch events, will make potentially several calls to `/events` endpoint
events = fetch_paginated_events({limit: MAX_EVENT_RECORDS_TO_RETRIEVE, to_date: @last_date_to_include})
transactions = []
events.map { |event|
hash = {:id => event["id"]}
case event["type"]
when "payment"
user_paid_you = event["payment"]["action"] == "pay" && event["payment"]["actor"]["username"] != @username
you_paid_user = event["payment"]["action"] == "pay" && event["payment"]["actor"]["username"] == @username
you_charged_user = event["payment"]["action"] == "charge" && event["payment"]["actor"]["username"] == @username
user_charged_you = event["payment"]["action"] == "charge" && event["payment"]["actor"]["username"] != @username
display_name = user_paid_you || user_charged_you ? event["payment"]["actor"]["display_name"] : event["payment"]["target"]["user"]["display_name"] || "unknown"
description = "#{event["payment"]["action"].upcase} #{display_name} for #{event["payment"]["note"]}".gsub("\n",' ').gsub(",",' ').gsub("-",' ')
# date_completed is date_created if you paid user.
# I found a few Venmo records where date_completed was several years after the payment happened.
date_completed = you_paid_user ? event["date_created"] : event["payment"]["date_completed"]
hash.merge!(
{
:date_completed => Date.parse(date_completed || event["date_updated"]),
:description => description,
:amount => event["payment"]["amount"],
:name => display_name
}
)
if you_paid_user || user_charged_you
hash[:amount] *= -1
end
when "transfer"
hash.merge!(
{
:date_completed => Date.parse(event["transfer"]["destination"]["transfer_to_estimate"]),
:description => "#{event["type"].upcase}",
:amount => -event["transfer"]["amount"],
:name => "#{event["transfer"]["destination"]["name"]} #{event["transfer"]["destination"]["last_four"]}",
}
)
when "refund"
hash.merge!(
{
:date_completed => Date.parse(event["refund"]["estimated_arrival"]),
:description => "#{event["type"].upcase}, venmo might not have been received.",
:amount => event["refund"]["amount"],
:name => "#{event["refund"]["destination"]["name"]}",
}
)
else
puts "⚠️ Found unknown event found. Fix directly in your CSV file.\n\n #{event} \n\n"
hash.merge!(
{
:date_completed => event["date_updated"],
:description => "unknown",
:amount => 0,
:name => "unknown",
}
)
end
transactions << hash if hash[:date_completed] >= @last_date_to_include
}
transactions.map { |record|
record[:date_completed] = record[:date_completed].strftime("%D")
}
file_name = "venmos_#{transactions.first[:date_completed].gsub("/","")}_to_#{transactions.last[:date_completed].gsub("/","")}.csv"
puts "💎 Exporting #{transactions.length} transactions. Look for #{file_name}."
CSV.open(file_name, "wb") do |csv|
csv << transactions.first.keys
transactions.each do |hash|
csv << hash.values
end
end
puts "✅ Finished.\n\n#{"🔨"*25}\n\n"