mc 0.0.4

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,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