mc 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ desc 'Search campaigns and members'
2
+ command :search do |c|
3
+ # helper/search-campaigns (string apikey, string query, int offset, string snip_start, string snip_end)
4
+ c.desc 'Search all campaigns for the specified query terms'
5
+ c.command :campaigns do |s|
6
+ s.action do |global,options,args|
7
+ campaigns = required_argument("You need to include a search argument: mc search campaigns <query terms>", args.join(' '))
8
+ @output.search @mailchimp_cached.helper_search_members(:query => query)
9
+ end
10
+ end
11
+
12
+ # helper/search-members (string apikey, string query, string id, int offset)
13
+ c.desc 'Search account wide or on a specific list using the specified query terms'
14
+ c.command :members do |s|
15
+ s.action do |global,options,args|
16
+ query = required_argument("You need to include a search argument: mc search members <query terms>", args.join(' '))
17
+ @output.search @mailchimp_cached.helper_search_members(:query => query)
18
+ end
19
+ end
20
+ end
21
+
@@ -0,0 +1,11 @@
1
+ desc 'Manage templates within your account'
2
+
3
+ command :templates do |c|
4
+ # templates/list (string apikey, struct types, struct filters)
5
+ c.desc 'Retrieve various templates available in the system'
6
+ c.command :list do |s|
7
+ s.action do |global,options,args|
8
+ @output.standard @mailchimp_cached.templates_list
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ desc 'Users'
2
+ command :users do |c|
3
+ # users/invite (string apikey, string email, string role, string msg)
4
+ c.desc 'Invite a user to your account'
5
+ c.command :invite do |s|
6
+ s.action do |global,options,args|
7
+ emails = create_email_struct(options[:email])
8
+
9
+ status = @mailchimp.vip_add(:id => id, :emails => emails)
10
+ puts status
11
+ end
12
+ end
13
+
14
+ # useres/logins (string apikey)
15
+ c.desc 'Retrieve the list of active logins.'
16
+ c.command :logins do |s|
17
+ s.action do
18
+ puts @mailchimp_cached.users_logins
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,51 @@
1
+ desc 'VIPs'
2
+ command :vip do |c|
3
+ c.desc 'List ID'
4
+ c.flag :id
5
+
6
+ # vip/activity (string apikey)
7
+ c.desc 'Show all Activity (opens/clicks) for VIPs over the past 10 days'
8
+ c.command :activity do |s|
9
+ s.action do
10
+ @output.standard @mailchimp_cached.vip_activity, :fields => [:email, :action, :title, :timestamp, :url], :reverse => true
11
+ end
12
+ end
13
+
14
+ # vip/add (string apikey, string id, array emails)
15
+ c.desc 'Add VIP(s)'
16
+ c.command :add do |s|
17
+ s.action do |global,options,args|
18
+ id = get_required_argument(:id, options[:id], global[:default_list])
19
+ emails = create_email_struct(required_argument("Need to provide an email address.", args))
20
+
21
+ status = @mailchimp.vip_add(:id => id, :emails => emails)
22
+ if emails.count == status['success_count']
23
+ puts "Successfully added email(s)!"
24
+ else
25
+ @output.errors status
26
+ end
27
+ end
28
+ end
29
+
30
+ # vip/del (string apikey, string id, array emails)
31
+ c.desc 'Remove VIP(s)'
32
+ c.command :remove do |s|
33
+ s.action do |global,options,args|
34
+ id = get_required_argument(:id, options[:id], global[:default_list])
35
+ emails = create_email_struct(required_argument("Need to provide an email address.", args))
36
+
37
+ status = @mailchimp.vip_del(:id => id, :emails => emails)
38
+ #TODO: create helper method to display success
39
+ puts status
40
+ #status["success"] > 0 ? "#{options[:email]} removed!" : "Oops!"
41
+ end
42
+ end
43
+
44
+ # vip/members (string apikey)
45
+ c.desc 'List all VIPs'
46
+ c.command :members do |s|
47
+ s.action do
48
+ @output.standard @mailchimp_cached.vip_members, :fields => [:email, :list_name, :member_rating]
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,62 @@
1
+ module Helper
2
+ def na(item)
3
+ if item.nil?
4
+ return "NA"
5
+ elsif item.is_a? Float
6
+ '%.2f' % item
7
+ else
8
+ item
9
+ end
10
+ end
11
+
12
+ def get_required_argument(name, option, global)
13
+ return option unless option.nil?
14
+ return global unless global.nil?
15
+ help_now!("--#{name.to_s} is required")
16
+ end
17
+
18
+ def required_option(name, *options)
19
+ options.each {|o| return o unless o.nil? or o.empty?}
20
+ exit_now!("Error: --#{name.to_s} is required")
21
+ end
22
+
23
+ def required_argument(msg, *arguments)
24
+ arguments.each {|a| return a unless a.nil? or a.empty?}
25
+ exit_now!(msg)
26
+ end
27
+
28
+ def get_last_campaign_id
29
+ @mailchimp_cached.campaigns_list(:limit => 1, :sort_field => "create_time")["data"].first["id"]
30
+ end
31
+
32
+ def create_email_struct(emails)
33
+ struct = []
34
+ emails.each do |email|
35
+ struct << {:email => email}
36
+ end
37
+
38
+ return struct
39
+ end
40
+
41
+ def not_implemented
42
+ raise "This command is not implemented yet."
43
+ end
44
+
45
+ def view_to_print(global, fields, print_options=nil)
46
+ if global[:raw]
47
+ # only show the first field and nothing else
48
+ return fields.first, {:show_index => false}
49
+ else
50
+ return fields, print_options
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def pad(num, total)
57
+ padding = ""
58
+ amount_to_pad = total.to_s.size - num.to_s.size
59
+ amount_to_pad.times {padding << " "}
60
+ padding + num.to_s
61
+ end
62
+ end
@@ -0,0 +1,29 @@
1
+ require 'gibbon'
2
+ require 'filecache'
3
+
4
+ class MailChimp
5
+ def initialize(apikey, options={})
6
+ @api = Gibbon::API.new(apikey)
7
+ @api.throws_exceptions = true
8
+ @options = options
9
+
10
+ @exporter = @api.get_exporter
11
+ end
12
+
13
+ def method_missing(method_name, *args)
14
+ puts "DEBUG: Calling '#{method_name}(#{args})'..." if @options[:debug]
15
+ category = method_name.to_s.split('_').first
16
+ method = method_name.to_s.split('_')[1..-1].join('_')
17
+ if category == "export"
18
+ @exporter.send(method, *args)
19
+ else
20
+ if method == 'send'
21
+ # handle wonk case of 'send' method
22
+ @api.send(category).send(*args)
23
+ else
24
+ # had to change this from .send(method, *args) to .method_missing b/c of gibbon 1.0.3
25
+ @api.send(category).method_missing(method, *args)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,44 @@
1
+ require 'gibbon'
2
+ require 'filecache'
3
+ require 'digest/sha1'
4
+
5
+ class MailChimpCached < MailChimp
6
+ def initialize(apikey, options={})
7
+ super(apikey, options)
8
+
9
+ # configure filecache
10
+ cache_dir = File.join(File.expand_path(ENV['HOME']), ".mailchimp-cache")
11
+
12
+ # expire in one day
13
+ expiry = 60 * 60 * 24
14
+
15
+ @cache = FileCache.new(apikey, cache_dir, expiry)
16
+ @skip_cache = options[:skip_cache]
17
+ end
18
+
19
+ def cache_value(key, value)
20
+ puts "cache returns: #{@cache.set(key, value)}"
21
+ end
22
+
23
+ private
24
+
25
+ def method_missing(method_name, *args)
26
+ puts "DEBUG: Calling '#{method_name}(#{args})'..." if @options[:debug]
27
+ cache_key = Digest::SHA1.hexdigest(method_name.to_s + args.to_s)
28
+
29
+ if result = @cache.get(cache_key) and not @skip_cache
30
+ puts "DEBUG: USING CACHED RESULT" if @options[:debug]
31
+ return result
32
+ else
33
+ category = method_name.to_s.split('_').first
34
+ method = method_name.to_s.split('_')[1..-1].join('_')
35
+
36
+ throw "error: don't support caching send" if method == 'send'
37
+
38
+ result = @api.send(category).method_missing(method, *args)
39
+ @cache.set(cache_key, result)
40
+
41
+ return result
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,140 @@
1
+ module Report
2
+ class BaseReport
3
+ def initialize(api)
4
+ @api = api
5
+ end
6
+
7
+ def run
8
+ raise NotImplementedError.new("You must implement the run method.")
9
+ end
10
+ end
11
+
12
+ class CampaignStats < BaseReport
13
+ def initialize(api, start=0)
14
+ super(api)
15
+ @start = start
16
+ end
17
+
18
+ def run
19
+ last_campaign_sent = @api.campaigns(:filters => {:status => "sent"}, :start => @start, :limit => 1)
20
+ campaign_data = last_campaign_sent['data'].first
21
+ campaign_id = campaign_data['id']
22
+
23
+ campaign_stats = @api.campaign_stats({:cid => campaign_id})
24
+ emails_sent = campaign_stats['emails_sent']
25
+
26
+ email_domains = @api.campaign_email_domain_performance({:cid => campaign_id})
27
+
28
+ puts "Details for #{campaign_data['title']} - #{campaign_data['archive_url']}"
29
+ puts "==="
30
+ puts "Sent at #{campaign_data['send_time']}"
31
+ puts "Last open at #{campaign_stats['last_open']}"
32
+
33
+ puts "--"
34
+ puts "Emails sent: #{emails_sent}"
35
+ puts "Opens: #{campaign_stats['unique_opens']} (%#{(campaign_stats['unique_opens'].to_f / emails_sent).round(4) * 100})"
36
+ puts "Users who clicked: #{campaign_stats['users_who_clicked']} (%#{(campaign_stats['users_who_clicked'].to_f / emails_sent).round(4) * 100})"
37
+ puts "Clicks: #{campaign_stats['unique_clicks']}"
38
+
39
+ puts "--"
40
+ puts "Unsubscribers: #{campaign_stats['unsubscribes']}"
41
+ puts "Hard/Soft Bounces: #{campaign_stats['hard_bounces']}/#{campaign_stats['soft_bounces']}"
42
+ puts "Abuse reports: #{campaign_stats['abuse_reports']}"
43
+
44
+ puts "--"
45
+ email_domains.each do |domain|
46
+ puts "#{domain['emails']} sent to #{domain['domain']} - open/click count: #{domain['opens']}/#{domain['clicks']} - open/click %: #{domain['opens_pct']}/#{domain['clicks_pct']}"
47
+ end
48
+
49
+ puts "--"
50
+ most_clicked_links(campaign_id).first(20).each do |url,clicks|
51
+ puts "#{clicks} - #{url[0..70]}"
52
+ end
53
+ end
54
+
55
+ def most_clicked_links(campaign_id)
56
+ most_clicked = {}
57
+
58
+ url_click_stats = @api.campaign_click_stats({:cid => campaign_id})
59
+ url_click_stats.each do |stat|
60
+ most_clicked[stat.first] = stat.last['unique']
61
+ end
62
+
63
+ most_clicked.sort_by {|url,clicks| -clicks}
64
+ end
65
+ end
66
+
67
+ class TotalClicks < BaseReport
68
+ def initialize(api, list_id)
69
+ super(api)
70
+ @list_id = list_id
71
+ end
72
+
73
+ def run
74
+ @api.campaigns(:filters => {:list_id => @list_id}, :limit => 25)['data'].each do |campaign|
75
+ puts "#{campaign['subject']} - #{@api.campaignStats({:cid => campaign['id']})['clicks'].to_i}"
76
+ end
77
+ end
78
+ end
79
+
80
+ class ArchiveUrls < BaseReport
81
+ def initialize(api, list_id)
82
+ super(api)
83
+ @list_id = list_id
84
+ end
85
+
86
+ def run
87
+ @api.campaigns(:filters => {:list_id => @list_id}, :limit => 20)['data'].each do |campaign|
88
+ if campaign['subject'] != nil
89
+ puts "<a href=\"#{campaign['archive_url']}\" class=\"link\">#{campaign['subject'].match('\d+')}</a>,"
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ class Zeitgest < BaseReport
96
+ def initialize(api, list_id, year)
97
+ super(api)
98
+ @list_id = list_id
99
+ @year = year
100
+ end
101
+
102
+ def run
103
+ @campaigns = load_campaigns
104
+ yearly_most_popular
105
+ end
106
+
107
+ def load_campaigns
108
+ campaigns = []
109
+
110
+ @api.campaigns_list(:filters => {:list_id => @list_id}, :limit => 100)['data'].each do |campaign|
111
+ if campaign['send_time'] != nil && campaign['send_time'].match(/#{@year.to_s}/)
112
+ campaigns << campaign
113
+ end
114
+ end
115
+
116
+ return campaigns
117
+ end
118
+
119
+ def yearly_most_popular
120
+ stats = {}
121
+ max_rate = 0
122
+
123
+ @campaigns.each do |campaign|
124
+ @api.campaignClickStats(:cid => campaign['id']).each do |item|
125
+ url, stat = item
126
+ click_rate = (stat["clicks"] / campaign['emails_sent'].to_f) * 100
127
+ max_rate = click_rate if click_rate > max_rate
128
+ stats[url] = [click_rate, campaign['send_time'][5..6]]
129
+ end
130
+ end
131
+
132
+ normalize = 100 / max_rate
133
+
134
+ stats.sort_by { |url, clicks| clicks[0] }.reverse[0..100].each_with_index do |(url, data), index|
135
+ click_rate, month = data
136
+ puts "#{index}. #{url} - #{click_rate * normalize} in #{month}"
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,4 @@
1
+ module MC
2
+ # have to check const defined for 1.8.7 to avoid warnings
3
+ VERSION = '0.0.4' unless const_defined?(:VERSION)
4
+ end
@@ -0,0 +1,174 @@
1
+ require 'awesome_print'
2
+ require 'table_print'
3
+
4
+ class ConsoleWriter
5
+ def initialize(options)
6
+ @options = options
7
+ puts "*** DEBUG ON ***" if @options[:debug]
8
+ end
9
+
10
+ def standard(results, options={})
11
+ redirect_output? results, options
12
+ as_table results, options
13
+ end
14
+
15
+ def as_hash(results, options={})
16
+ redirect_output? results, options
17
+
18
+ if results.is_a? Hash and results.has_key? 'data'
19
+ ap results['data'].first, options
20
+ else
21
+ ap results, options
22
+ end
23
+ end
24
+
25
+ def as_table(results, options={})
26
+ redirect_output? results, options
27
+
28
+ results.reverse! if options[:reverse]
29
+ tp results, options[:fields]
30
+ end
31
+
32
+ def errors(results)
33
+ puts "Error count: #{results['error_count']}"
34
+ results['errors'].each do |error|
35
+ puts error['error']
36
+ end
37
+ end
38
+
39
+ def flat_hash(hash, k = [])
40
+ return {k => hash} unless hash.is_a?(Hash)
41
+ hash.inject({}){ |h, v| h.merge! flat_hash(v[-1], k + [v[0]]) }
42
+ end
43
+
44
+ def as_formatted(results, options={})
45
+ redirect_output? results, options
46
+
47
+ # set default options
48
+ options[:show_header] ||= true
49
+ options[:show_index] ||= false
50
+ options[:debug] ||= false
51
+
52
+ fields = options[:fields]
53
+
54
+ # find the correct staring level in the returned json
55
+ if results.kind_of? Hash
56
+ # array of hashes or just a single hash?
57
+ if results["data"].nil?
58
+ results = [results]
59
+ else
60
+ results = results["data"]
61
+ end
62
+ end
63
+
64
+ if fields == :all
65
+ fields = []
66
+ results.first.each_key {|key| fields << key}
67
+ end
68
+
69
+ results.reverse! if options[:reverse]
70
+ results = results[0..options[:limit]] if options[:limit]
71
+
72
+ puts "Fields: #{[*fields].join(', ')}" unless fields.nil? || !options[:show_header]
73
+
74
+ results.to_enum.with_index(1) do |item, index|
75
+ if options[:debug] || fields.nil?
76
+ puts "Number of items: #{results.count}"
77
+ puts "Fields:"
78
+ item.each do |k,v|
79
+ if v.kind_of? Hash
80
+ puts "#{k} ="
81
+ v.each do |k,v|
82
+ puts " #{k} = #{v}"
83
+ end
84
+ elsif v.kind_of? Array
85
+ puts "#{k} ="
86
+ v.each do |i|
87
+ if i.kind_of? Hash
88
+ i.each do |k,v|
89
+ puts " #{k} = #{v}"
90
+ end
91
+ else
92
+ puts " #{i}"
93
+ end
94
+ end
95
+ else
96
+ puts "#{k} = #{v}"
97
+ end
98
+ end
99
+ return
100
+ else
101
+ values_to_print = []
102
+ [*fields].each do |field|
103
+ if field.class == Hash
104
+ values_to_print << item[field.keys.first.to_s][field.values.first.to_s]
105
+ else
106
+ values_to_print << item[field.to_s]
107
+ end
108
+ end
109
+
110
+ print "#{pad(index, results.count)} - " if options[:show_index]
111
+ puts "#{values_to_print.join(' ')}"
112
+ end
113
+ end
114
+ end
115
+
116
+ def search(results)
117
+ redirect_output?(results)
118
+
119
+ i = results
120
+ if i['exact_matches']['total'].to_i == 0 and i['full_search']['total'].to_i == 0
121
+ exit_now!("No matches found.")
122
+ end
123
+
124
+ members = []
125
+
126
+ if i['exact_matches']['total'].to_i > 0
127
+ puts "#{'='*20} Exact Matches (#{i['exact_matches']['total']}) #{'='*20}"
128
+
129
+ i['exact_matches']['members'].each do |member|
130
+ members << member
131
+ end
132
+ elsif i['full_search']['total'].to_i > 0
133
+ puts "#{'='*26} Matches (#{i['full_search']['total']}) #{'='*26}"
134
+ i['full_search']['members'].each do |member|
135
+ members << member
136
+ end
137
+ end
138
+
139
+ tp members, :email, :id, :member_rating, :status, "VIP?" => {:display_method => :is_gmonkey}
140
+ end
141
+
142
+ private
143
+
144
+ def debug(results)
145
+ puts "#{'*'*50}"
146
+ puts results
147
+ puts results.class
148
+ puts "#{'*'*50}"
149
+ end
150
+
151
+
152
+ def show_member(m)
153
+ puts "#{m['id']} - #{m['email']}: Rating=#{m['member_rating']}, List=#{m['list_name']}, VIP?=#{m['is_gmonkey']}"
154
+ end
155
+
156
+ def redirect_output?(results, options={})
157
+ if @options[:output]
158
+ case @options[:output].to_sym
159
+ when :table
160
+ as_table results, options
161
+ exit_now!('')
162
+ when :formatted
163
+ as_formatted results, options
164
+ exit_now!('')
165
+ when :raw
166
+ puts results
167
+ exit_now!('')
168
+ when :awesome, :hash
169
+ ap results
170
+ exit_now!('')
171
+ end
172
+ end
173
+ end
174
+ end