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.
- data/History.txt +4 -0
- data/Manifest.txt +50 -0
- data/README.txt +65 -0
- data/Rakefile +40 -0
- data/lib/impostor.rb +6 -0
- data/lib/www/impostor/phpbb2.rb +260 -0
- data/lib/www/impostor/wwf79.rb +254 -0
- data/lib/www/impostor/wwf80.rb +264 -0
- data/lib/www/impostor.rb +269 -0
- data/test/fixtures/phpbb2-get-new_topic-form-good-response.html +725 -0
- data/test/fixtures/phpbb2-get-viewtopic-for-new-topic-good-response.html +364 -0
- data/test/fixtures/phpbb2-get-viewtopic-for-new-topic-malformed-response.html +364 -0
- data/test/fixtures/phpbb2-index.html +361 -0
- data/test/fixtures/phpbb2-logged-in.html +349 -0
- data/test/fixtures/phpbb2-login.html +306 -0
- data/test/fixtures/phpbb2-not-logged-in.html +349 -0
- data/test/fixtures/phpbb2-post-new_topic-good-response.html +290 -0
- data/test/fixtures/phpbb2-post-reply-good-response.html +290 -0
- data/test/fixtures/phpbb2-post-reply-throttled-response.html +290 -0
- data/test/fixtures/phpbb2-too-many-posts.html +290 -0
- data/test/fixtures/wwf79-forum_posts.html +422 -0
- data/test/fixtures/wwf79-general-new-topic-error.html +46 -0
- data/test/fixtures/wwf79-general-posting-error.html +46 -0
- data/test/fixtures/wwf79-good-post-forum_posts.html +462 -0
- data/test/fixtures/wwf79-index.html +137 -0
- data/test/fixtures/wwf79-logged-in.html +46 -0
- data/test/fixtures/wwf79-login.html +123 -0
- data/test/fixtures/wwf79-new-topic-forum_posts-response.html +305 -0
- data/test/fixtures/wwf79-new-topic-post_message_form.html +212 -0
- data/test/fixtures/wwf79-not-logged-in.html +128 -0
- data/test/fixtures/wwf79-too-many-posts.html +46 -0
- data/test/fixtures/wwf79-too-many-topics.html +46 -0
- data/test/fixtures/wwf80-general-posting-error.html +54 -0
- data/test/fixtures/wwf80-get-new_topic-form-good-response.html +217 -0
- data/test/fixtures/wwf80-get-viewtopic-for-new-topic-good-response.html +290 -0
- data/test/fixtures/wwf80-index.html +371 -0
- data/test/fixtures/wwf80-logged-in.html +52 -0
- data/test/fixtures/wwf80-login.html +125 -0
- data/test/fixtures/wwf80-new_reply_form.html +204 -0
- data/test/fixtures/wwf80-not-logged-in.html +136 -0
- data/test/fixtures/wwf80-post-new_topic-good-response.html +293 -0
- data/test/fixtures/wwf80-post-reply-good-response.html +331 -0
- data/test/fixtures/wwf80-too-many-posts.html +54 -0
- data/test/test_helper.rb +38 -0
- data/test/test_www_impostor.rb +165 -0
- data/test/test_www_impostor_phpbb2.rb +536 -0
- data/test/test_www_impostor_wwf79.rb +535 -0
- data/test/test_www_impostor_wwf80.rb +535 -0
- data/vendor/plugins/impostor/lib/autotest/discover.rb +3 -0
- data/vendor/plugins/impostor/lib/autotest/impostor.rb +49 -0
- data.tar.gz.sig +3 -0
- metadata +156 -0
- 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
|
data/lib/www/impostor.rb
ADDED
@@ -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
|