impostor 0.0.1

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.
Files changed (53) hide show
  1. data/History.txt +4 -0
  2. data/Manifest.txt +50 -0
  3. data/README.txt +65 -0
  4. data/Rakefile +40 -0
  5. data/lib/impostor.rb +6 -0
  6. data/lib/www/impostor/phpbb2.rb +260 -0
  7. data/lib/www/impostor/wwf79.rb +254 -0
  8. data/lib/www/impostor/wwf80.rb +264 -0
  9. data/lib/www/impostor.rb +269 -0
  10. data/test/fixtures/phpbb2-get-new_topic-form-good-response.html +725 -0
  11. data/test/fixtures/phpbb2-get-viewtopic-for-new-topic-good-response.html +364 -0
  12. data/test/fixtures/phpbb2-get-viewtopic-for-new-topic-malformed-response.html +364 -0
  13. data/test/fixtures/phpbb2-index.html +361 -0
  14. data/test/fixtures/phpbb2-logged-in.html +349 -0
  15. data/test/fixtures/phpbb2-login.html +306 -0
  16. data/test/fixtures/phpbb2-not-logged-in.html +349 -0
  17. data/test/fixtures/phpbb2-post-new_topic-good-response.html +290 -0
  18. data/test/fixtures/phpbb2-post-reply-good-response.html +290 -0
  19. data/test/fixtures/phpbb2-post-reply-throttled-response.html +290 -0
  20. data/test/fixtures/phpbb2-too-many-posts.html +290 -0
  21. data/test/fixtures/wwf79-forum_posts.html +422 -0
  22. data/test/fixtures/wwf79-general-new-topic-error.html +46 -0
  23. data/test/fixtures/wwf79-general-posting-error.html +46 -0
  24. data/test/fixtures/wwf79-good-post-forum_posts.html +462 -0
  25. data/test/fixtures/wwf79-index.html +137 -0
  26. data/test/fixtures/wwf79-logged-in.html +46 -0
  27. data/test/fixtures/wwf79-login.html +123 -0
  28. data/test/fixtures/wwf79-new-topic-forum_posts-response.html +305 -0
  29. data/test/fixtures/wwf79-new-topic-post_message_form.html +212 -0
  30. data/test/fixtures/wwf79-not-logged-in.html +128 -0
  31. data/test/fixtures/wwf79-too-many-posts.html +46 -0
  32. data/test/fixtures/wwf79-too-many-topics.html +46 -0
  33. data/test/fixtures/wwf80-general-posting-error.html +54 -0
  34. data/test/fixtures/wwf80-get-new_topic-form-good-response.html +217 -0
  35. data/test/fixtures/wwf80-get-viewtopic-for-new-topic-good-response.html +290 -0
  36. data/test/fixtures/wwf80-index.html +371 -0
  37. data/test/fixtures/wwf80-logged-in.html +52 -0
  38. data/test/fixtures/wwf80-login.html +125 -0
  39. data/test/fixtures/wwf80-new_reply_form.html +204 -0
  40. data/test/fixtures/wwf80-not-logged-in.html +136 -0
  41. data/test/fixtures/wwf80-post-new_topic-good-response.html +293 -0
  42. data/test/fixtures/wwf80-post-reply-good-response.html +331 -0
  43. data/test/fixtures/wwf80-too-many-posts.html +54 -0
  44. data/test/test_helper.rb +38 -0
  45. data/test/test_www_impostor.rb +165 -0
  46. data/test/test_www_impostor_phpbb2.rb +536 -0
  47. data/test/test_www_impostor_wwf79.rb +535 -0
  48. data/test/test_www_impostor_wwf80.rb +535 -0
  49. data/vendor/plugins/impostor/lib/autotest/discover.rb +3 -0
  50. data/vendor/plugins/impostor/lib/autotest/impostor.rb +49 -0
  51. data.tar.gz.sig +3 -0
  52. metadata +156 -0
  53. metadata.gz.sig +3 -0
@@ -0,0 +1,264 @@
1
+ require 'rubygems'
2
+ require 'hpricot'
3
+ gem 'mechanize', '>= 0.7.0'
4
+ require 'mechanize'
5
+ require 'cgi'
6
+
7
+ ##
8
+ # Web Wiz Forums version 8.0 of the Impostor
9
+ #
10
+
11
+ class WWW::Impostor
12
+
13
+ class Wwf80 < WWW::Impostor
14
+
15
+ ##
16
+ # After initializing the parent a mechanize agent is created
17
+ #
18
+ # Additional configuration parameters:
19
+ #
20
+ # :new_reply_page
21
+ # :new_topic_page
22
+ #
23
+ # Typical configuration parameters
24
+ # { :type => :wwf80,
25
+ # :app_root => 'http://example.com/forum/',
26
+ # :login_page => 'login_user.asp',
27
+ # :new_reply_page => 'new_reply_form.asp',
28
+ # :new_topic_page => 'new_topic_form.asp',
29
+ # :user_agent => 'Windows IE 7',
30
+ # :username => 'myuser',
31
+ # :password => 'mypasswd' }
32
+
33
+ def initialize(config={})
34
+ super(config)
35
+ @agent = WWW::Mechanize.new
36
+ @agent.user_agent_alias = user_agent
37
+ # jar is a yaml file
38
+ @agent.cookie_jar.load(cookie_jar) if cookie_jar && File.exist?(cookie_jar)
39
+ @message = nil
40
+ @loggedin = false
41
+ end
42
+
43
+ ##
44
+ # clean up the state of the library and log out
45
+
46
+ def logout
47
+ return false unless @loggedin
48
+
49
+ @agent.cookie_jar.save_as(cookie_jar) if cookie_jar
50
+ save_topics
51
+
52
+ @forum = nil
53
+ @topic = nil
54
+ @message = nil
55
+
56
+ @loggedin = false
57
+ true
58
+ end
59
+
60
+ def new_topic(forum=@forum, subject=@subject, message=@message)
61
+ raise PostError.new("forum not set") unless forum
62
+ raise PostError.new("topic name not given") unless subject
63
+ raise PostError.new("message not set") unless message
64
+
65
+ login
66
+ raise PostError.new("not logged in") unless @loggedin
67
+
68
+ uri = new_topic_page
69
+ uri.query = "FID=#{forum}"
70
+
71
+ # get the submit form
72
+ begin
73
+ page = @agent.get(uri)
74
+ rescue StandardError => err
75
+ raise PostError.new(err)
76
+ end
77
+ form = page.form('frmMessageForm') rescue nil
78
+ button = form.buttons.with.name('Submit').first rescue nil
79
+ raise PostError.new("post form not found") unless button && form
80
+
81
+ # set up the form and submit it
82
+ form.subject = subject
83
+ form.message = message
84
+ begin
85
+ page = @agent.submit(form, button)
86
+ rescue StandardError => err
87
+ raise PostError.new(err)
88
+ end
89
+
90
+ error = page.search("//table[@class='errorTable']")
91
+ if error
92
+ msgs = error.search("//td")
93
+
94
+ # throttled
95
+ too_many = (msgs.last.innerText =~
96
+ /You have exceeded the number of posts permitted in the time span/ rescue
97
+ false)
98
+ raise ThrottledError.new(msgs.last.innerText.gsub(/\s+/m,' ').strip) if too_many
99
+
100
+ # general error
101
+ had_error = (error.last.innerText =~
102
+ /Error: Message Not Posted/ rescue
103
+ false)
104
+ raise PostError.new(error.last.innerText.gsub(/\s+/m,' ').strip) if had_error
105
+ end
106
+
107
+ # look up the new topic id
108
+ form = page.form('frmMessageForm') rescue nil
109
+ topic = form['TID'].to_i rescue 0
110
+ raise PostError.new('unexpected new topic ID') if topic < 1
111
+
112
+ # save new topic id and topic name
113
+ add_subject(forum, topic, subject)
114
+
115
+ @forum=forum; @topic=topic; @subject=subject; @message=message
116
+ return true
117
+ end
118
+
119
+ ##
120
+ # Attempt to post to the forum
121
+
122
+ def post(forum = @forum, topic = @topic, message = @message)
123
+ raise PostError.new("forum not set") unless forum
124
+ raise PostError.new("topic not set") unless topic
125
+ raise PostError.new("message not set") unless message
126
+
127
+ login
128
+ raise PostError.new("not logged in") unless @loggedin
129
+
130
+ uri = new_reply_page
131
+ uri.query = "TID=#{topic}"
132
+
133
+ # get the submit form
134
+ begin
135
+ page = @agent.get(uri)
136
+ rescue StandardError => err
137
+ raise PostError.new(err)
138
+ end
139
+
140
+ form = page.form('frmMessageForm') rescue nil
141
+ button = form.buttons.with.name('Submit').first rescue nil
142
+ raise PostError.new("post form not found") unless button && form
143
+
144
+ # set up the form and submit it
145
+ form.message = message
146
+ begin
147
+ page = @agent.submit(form, button)
148
+ rescue StandardError => err
149
+ raise PostError.new(err)
150
+ end
151
+
152
+ error = page.search("//table[@class='errorTable']")
153
+ if error
154
+ msgs = error.search("//td")
155
+
156
+ # throttled
157
+ too_many = (msgs.last.innerText =~
158
+ /You have exceeded the number of posts permitted in the time span/ rescue
159
+ false)
160
+ raise ThrottledError.new(msgs.last.innerText.gsub(/\s+/m,' ').strip) if too_many
161
+
162
+ # general error
163
+ had_error = (error.last.innerText =~
164
+ /Error: Message Not Posted/ rescue
165
+ false)
166
+ raise PostError.new(error.last.innerText.gsub(/\s+/m,' ').strip) if had_error
167
+ end
168
+
169
+ @forum=forum; @topic=topic; @subject=get_subject(forum,topic); @message=message
170
+ return true
171
+ end
172
+
173
+ ##
174
+ # Get the new reply page for the application (specific to WWF8.0)
175
+
176
+ def new_reply_page
177
+ URI.join(app_root, config[:new_reply_page])
178
+ end
179
+
180
+ ##
181
+ # Get the new topic page for the application (specific to WWF8.0)
182
+
183
+ def new_topic_page
184
+ URI.join(app_root, config[:new_topic_page])
185
+ end
186
+
187
+ ##
188
+ # does the work of logging into WWF 8.0
189
+
190
+ def login
191
+ return true if @loggedin
192
+
193
+ # get the login page
194
+ page = fetch_login_page
195
+
196
+ # return if we are already logged in from a cookie state
197
+ return true if logged_in?(page)
198
+
199
+ # setup the form and submit
200
+ form, button = login_form_and_button(page)
201
+ page = post_login(form, button)
202
+
203
+ # set up the rest of the state if we are logged in
204
+ @loggedin = logged_in?(page)
205
+ load_topics if @loggedin
206
+
207
+ @loggedin
208
+ end
209
+
210
+ def version
211
+ @version ||= self.class.to_s
212
+ end
213
+
214
+ protected
215
+
216
+ ##
217
+ # does the work of posting the login form
218
+
219
+ def post_login(form, button)
220
+ begin
221
+ page = @agent.submit(form, button)
222
+ rescue StandardError => err
223
+ raise LoginError.new(err)
224
+ end
225
+ end
226
+
227
+ ##
228
+ # returns the login form and its button from the login page
229
+
230
+ def login_form_and_button(page)
231
+ form = page.forms.with.name('frmLogin').first rescue nil
232
+ raise LoginError.new("unknown login page format") unless form
233
+
234
+ button = form.buttons.with.name('Submit').first
235
+ form['name'] = username
236
+ form['password'] = password
237
+
238
+ return form, button
239
+ end
240
+
241
+ ##
242
+ # fetches the login page
243
+
244
+ def fetch_login_page
245
+ begin
246
+ page = @agent.get(login_page)
247
+ rescue StandardError => err
248
+ raise LoginError.new(err)
249
+ end
250
+ end
251
+
252
+ ##
253
+ # Checks if the agent is already logged by stored cookie
254
+
255
+ def logged_in?(page)
256
+ mm = page.search("//a[@class='nav']")
257
+ return false unless mm
258
+ mm.each do |m|
259
+ return true if (m.innerText =~ /Logout \[#{username}\]/ rescue false)
260
+ end
261
+ false
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,269 @@
1
+ require 'rubygems'
2
+ Dir.glob(File.join(File.dirname(__FILE__), 'impostor/*.rb')).each {|f| require f }
3
+
4
+ module WWW
5
+
6
+ ##
7
+ # imPOSTor posts messages to non-RESTful forums and blogs
8
+ #
9
+ # == Example
10
+ # require 'rubygems'
11
+ # require 'impostor'
12
+ #
13
+ # # config yaml has options specefic to wwf79, wwf80, phpbb2, etc.
14
+ # # read the impostor docs for options to the kind of forum in use
15
+ # # config can be keyed by symbols or strings
16
+ # post = WWW::Impostor.new(YAML.load_file('config.yml'))
17
+ # message = %q!hello world is to application
18
+ # programmers as tea pots are to graphics programmers!
19
+ # # your application store forum and topic ids
20
+ # post.post(forum=5,topic=10,message)
21
+ # # make a new topic
22
+ # subject = "about programmers..."
23
+ # post.new_topic(forum=7,subject,message)
24
+ # post.logout
25
+ #
26
+ # keys and values that can be set in the impostor configuration
27
+ #
28
+ # :type - kind of imPOSTor, :phpbb2, :wwf79, :wwf80, etc.
29
+ # :username - forum username
30
+ # :password - forum password
31
+ # :topics_cache - cache of forum topics
32
+ # :user_agent - Mechanize browser user-agent
33
+ # :cookie_jar - saved cookies from Mechanize browser
34
+ # :app_root - url to forum
35
+ # :login_page - forum login page
36
+ #
37
+ # See documentation for each type of imPOSTor for additional configuration
38
+ # parameters that are needed for the specific kind of imPOSTor. A sample
39
+ # configuration is provided in the documentation for each.
40
+
41
+ class Impostor
42
+
43
+ class << self #:nodoc:
44
+ alias orig_new new
45
+ def new(conf)
46
+ klass = WWW::Impostor.create(conf)
47
+ klass.orig_new(conf)
48
+ end
49
+ end
50
+
51
+ ##
52
+ # Gem version of Impostor
53
+
54
+ VERSION = '0.0.1'
55
+
56
+ ##
57
+ # An application error
58
+
59
+ class ImpostorError < RuntimeError
60
+
61
+ ##
62
+ # The original exception
63
+
64
+ attr_accessor :original_exception
65
+
66
+ ##
67
+ # Creates a new ImpostorError with +message+ and +original_exception+
68
+
69
+ def initialize(e)
70
+ exception = e.class == String ? StandardError.new(e) : e
71
+ @original_exception = exception
72
+ message = "Impostor error: #{exception.message} (#{exception.class})"
73
+ super message
74
+ end
75
+
76
+ end
77
+
78
+ ##
79
+ # An error for impostor login failure
80
+
81
+ class LoginError < ImpostorError; end
82
+
83
+ ##
84
+ # An error for impostor post failure
85
+
86
+ class PostError < ImpostorError; end
87
+
88
+ ##
89
+ # An error for impostor when a topic id can't be found based on a
90
+ # name/title.
91
+
92
+ class TopicError < ImpostorError; end
93
+
94
+ ##
95
+ # An error for impostor when the receiving forum rejects the post due to
96
+ # a throttling or spam error but which the user can re-attempt at a later
97
+ # time.
98
+
99
+ class ThrottledError < ImpostorError; end
100
+
101
+ ##
102
+ # Pass in a config hash to initialize
103
+
104
+ def initialize(conf={})
105
+ @config = conf
106
+ load_topics
107
+ end
108
+
109
+ ##
110
+ # Instantiate a specific impostor based on its symbol name
111
+
112
+ def self.create(conf={})
113
+ type = conf[:type] || conf[:type.to_s]
114
+ clz = type.is_a?(Class) ? type : eval("WWW::Impostor::#{type.to_s.capitalize}")
115
+ clz
116
+ end
117
+
118
+ ##
119
+ # Access the current config and key it without regard for symbols or strings
120
+
121
+ def config(*key)
122
+ return @config if key.empty?
123
+ @config[key.first.to_sym] || @config[key.first.to_s]
124
+ end
125
+
126
+ ##
127
+ # Get/set the application version that impostor is interfacing with
128
+
129
+ attr_accessor :version
130
+
131
+ ##
132
+ # Login to the forum, returns true if logged in, false otherwise
133
+
134
+ def login; end
135
+
136
+ ##
137
+ # Log out of the forum, true if logged in, false otherwise
138
+
139
+ def logout; end
140
+
141
+ ##
142
+ # Load the topics that the impostor already knows about
143
+
144
+ def load_topics
145
+ cache = config[:topics_cache] ||= ""
146
+ if File::exist?(cache)
147
+ @topics = YAML::load_file(cache)
148
+ else
149
+ @topics = Hash.new
150
+ end
151
+ end
152
+
153
+ ##
154
+ # Add subject to topics hash
155
+
156
+ def add_subject(forum,topic,name)
157
+ if @topics[forum].nil?
158
+ @topics[forum] = {topic, name}
159
+ else
160
+ @topics[forum][topic] = name
161
+ end
162
+ end
163
+
164
+ ##
165
+ # Save the topics
166
+
167
+ def save_topics
168
+ cache = config[:topics_cache] ||= ""
169
+ if File::exist?(cache)
170
+ File.open(cache, 'w') do |out|
171
+ YAML.dump(@topics, out)
172
+ end
173
+ end
174
+ end
175
+
176
+ ##
177
+ # Post the message
178
+
179
+ def post(forum = @forum, topic = @topic, message = @message); end
180
+
181
+ ##
182
+ # get/set the current message
183
+
184
+ attr_accessor :message
185
+
186
+ ##
187
+ # get/set the current subject
188
+
189
+ attr_accessor :subject
190
+
191
+ ##
192
+ # Get/set the form id
193
+
194
+ attr_accessor :forum
195
+
196
+ ##
197
+ # Get/set the topic id
198
+
199
+ attr_accessor :topic
200
+
201
+ ##
202
+ # Get the topic name (subject) based on forum and topic ids
203
+
204
+ def get_subject(forum = @forum, topic = @topic)
205
+ if @topics && @topics[forum]
206
+ return @topics[forum][topic]
207
+ end
208
+ nil
209
+ end
210
+
211
+ ##
212
+ # Make a new topic
213
+
214
+ def new_topic(forum=@forum, subject=@subject, message=@message); end
215
+
216
+ ##
217
+ # Gets the application root of the application such as
218
+ # http://example.com/phpbb or http://example.com/forums
219
+
220
+ def app_root
221
+ config[:app_root]
222
+ end
223
+
224
+ protected
225
+
226
+ ##
227
+ # Get the topics cache
228
+
229
+ def topics_cache
230
+ config[:topics_cache]
231
+ end
232
+
233
+ ##
234
+ # Get the login page for the application
235
+
236
+ def login_page
237
+ URI.join(app_root, config[:login_page])
238
+ end
239
+
240
+ ##
241
+ # Get the username for the application
242
+
243
+ def username
244
+ config[:username]
245
+ end
246
+
247
+ ##
248
+ # Get the password for the application
249
+
250
+ def password
251
+ config[:password]
252
+ end
253
+
254
+ ##
255
+ # A Mechanize user agent name, see the mechanize documentation
256
+ # 'Linux Mozilla', 'Mac Safari', 'Windows IE 7', etc.
257
+
258
+ def user_agent
259
+ config[:user_agent]
260
+ end
261
+
262
+ ##
263
+ # is a yaml file for WWW::Mechanize::CookieJar
264
+
265
+ def cookie_jar
266
+ config[:cookie_jar]
267
+ end
268
+ end
269
+ end