riki 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in riki.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem "rake"
8
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Nicholas E. Rabenau
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # Riki
2
+
3
+ `riki` is a MediaWiki client written in Ruby.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'riki'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install riki
18
+
19
+ ## Usage
20
+ ### Working with Wikipedia
21
+
22
+ # Optional - this step is only required if you are working with a non-english Wikipedia
23
+ Riki.url = 'http://de.wikipedia.org/w/api.php'
24
+
25
+ # find a page
26
+ ruby = Riki::Page.find('Ruby')
27
+
28
+ # query its properties
29
+ ruby.last_modified # some date
30
+
31
+ ### Working with a custom MediaWiki installation that requires authentication
32
+
33
+ # Tell riki where to to use
34
+ Riki.url = 'http://example.com/wiki/api.php'
35
+ Riki.username = 'jon_doe'
36
+ Riki.password = 's3cret'
37
+
38
+ # everything else is the same as above
39
+
40
+ ## Commandline Client
41
+
42
+ `riki` comes with a simple command-line app that takes one or more titles of wikipedia pages and prints the Wikipedia page as plain text to STDOUT. Additional tools can be chained, e.g. `fmt` can be used to achieve word wrapping:
43
+
44
+ riki "Sinatra_(software)" | fmt -w $COLUMNS
45
+
46
+ ## Troubleshooting
47
+
48
+ Riki uses [RestClient](http://github.com/archiloque/rest-client) under the hood, to the simplest way to understand what goes wrong is to turn on logging for RestClient. For the bin script, the simplest way to achieve that is to set the appropriate environment variable:
49
+
50
+ RESTCLIENT_LOG=stdout riki Ruby
51
+
52
+ This command will invove the `riki` script with RestClient's logging set to STDOUT.
53
+
54
+ ## Contributing
55
+
56
+ 1. Fork it
57
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
58
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
59
+ 4. Push to the branch (`git push origin my-new-feature`)
60
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ task :default => :test
5
+
6
+ require 'rake/testtask'
7
+ Rake::TestTask.new(:test) do |test|
8
+ test.test_files = FileList["test/**/*.rb"].exclude("test/test_helper.rb")
9
+ test.verbose = false
10
+ test.warning = false
11
+ end
data/TODO.md ADDED
@@ -0,0 +1,3 @@
1
+ * Revisions class. Multiple revisions are part of a page; page content reader takes an optional revision parameter (defaults to latest)
2
+ * Find a page by id
3
+ * Handle MediaWiki redirects
data/bin/riki ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler'
4
+ Bundler.require
5
+ require 'active_support'
6
+
7
+ include Riki
8
+
9
+ prefs = Preferences::User.load(File.basename($0))
10
+ Riki::Base.url = prefs[:url] if prefs[:url] # prevent overriding with nil
11
+
12
+ # These may be nil. If present, all operations will occur under this account
13
+ Riki::Base.username = prefs[:username]
14
+ Riki::Base.password = prefs[:password]
15
+ Riki::Base.domain = prefs[:domain]
16
+ Riki::Base.cache = ActiveSupport::Cache::FileStore.new(File.expand_path(File.join('~', '.cache', File.basename($0))))
17
+
18
+ if ARGV.empty?
19
+ STDERR.puts "Error: Missing page title. Usage: #{$0} TITLE [TITLE]*"
20
+ exit 1
21
+ end
22
+
23
+ begin
24
+ results = Page.find_by_title(ARGV)
25
+ rescue PageNotFound
26
+ STDERR.puts $!
27
+ exit 2
28
+ rescue PageInvalid
29
+ STDERR.puts $!
30
+ exit 3
31
+ end
32
+
33
+ if results.empty?
34
+ STDERR.puts "No pages found for '#{ARGV.join(' ')}'"
35
+ exit 4
36
+ else
37
+ results.each{|page|
38
+ puts page.to_s
39
+ STDERR.puts "Page #{page.title} last modified #{page.last_modified}"
40
+ }
41
+ end
data/bin/riki-prefs ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'bundler'
5
+ Bundler.require
6
+
7
+ NAMESPACE = 'riki'
8
+
9
+ options = Preferences::User.load!(NAMESPACE)
10
+
11
+ verbose = false
12
+ dirty = false
13
+
14
+ def set_or_delete(options, key, value)
15
+ if value
16
+ options[key] = value
17
+ else
18
+ options.delete(key)
19
+ end
20
+ end
21
+
22
+ option_parser = OptionParser.new do |opts|
23
+ opts.on('-v', '--verbose') do
24
+ verbose = true
25
+ end
26
+
27
+ opts.on('-h', '--help') do
28
+ STDERR.puts "#{File.basename($0)} writes preferences for #{NAMESPACE}, the Ruby library for MediaWiki"
29
+ exit(1)
30
+ end
31
+
32
+ opts.on('-m [URL]', '--url [URL]') do |url|
33
+ set_or_delete(options, :url, url)
34
+ dirty = true
35
+ end
36
+
37
+ opts.on('-u [USER]', '--user [USER]') do |user|
38
+ set_or_delete(options, :username, user)
39
+ dirty = true
40
+ end
41
+
42
+ opts.on('-p [PASSWORD]', '--password [PASSWORD]') do |password|
43
+ set_or_delete(options, :password, password)
44
+ dirty = true
45
+ end
46
+
47
+ opts.on('-d [DOMAIN]', '--domain [DOMAIN]') do |domain|
48
+ set_or_delete(options, :domain, domain)
49
+ dirty = true
50
+ end
51
+ end
52
+
53
+ option_parser.parse!
54
+
55
+ STDERR.puts "Loaded user preferences from #{Preferences::User.user_preferences_file(NAMESPACE)}" if verbose
56
+
57
+ if !dirty
58
+ puts options.inspect # dump if called without args
59
+ else
60
+ STDERR.puts "Saving modified user preferences to #{Preferences::User.user_preferences_file(NAMESPACE)}" if verbose
61
+ Preferences::User.save!(options, NAMESPACE)
62
+ puts Preferences::User.load!(NAMESPACE).inspect if verbose
63
+ end
@@ -0,0 +1,42 @@
1
+ require 'yaml'
2
+
3
+ module Preferences
4
+ def self.load(file_name)
5
+ YAML::load_file(file_name)
6
+ end
7
+
8
+ def self.save(attribs, file_name)
9
+ File.open(file_name, 'w') do |out|
10
+ YAML::dump(attribs, out)
11
+ end
12
+ end
13
+
14
+ module User
15
+ FILE_NAME = 'preferences.yaml'
16
+
17
+ def self.load!(namespace)
18
+ Preferences.load(user_preferences_file(namespace))
19
+ end
20
+
21
+ def self.load(namespace)
22
+ begin
23
+ self.load!(namespace)
24
+ rescue
25
+ {}
26
+ end
27
+ end
28
+
29
+ def self.save!(attribs, namespace)
30
+ FileUtils.makedirs(user_preferences_path(namespace))
31
+ Preferences.save(attribs, user_preferences_file(namespace))
32
+ end
33
+
34
+ def self.user_preferences_path(namespace)
35
+ File.expand_path(File.join('~', '.config', namespace))
36
+ end
37
+
38
+ def self.user_preferences_file(namespace)
39
+ File.join(user_preferences_path(namespace), FILE_NAME)
40
+ end
41
+ end
42
+ end
data/lib/riki.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'riki/version'
2
+ require 'riki/errors'
3
+ require 'riki/base'
4
+ require 'riki/page'
5
+
6
+ require 'preferences/base'
7
+
8
+ module Riki
9
+ end
data/lib/riki/base.rb ADDED
@@ -0,0 +1,145 @@
1
+ require 'rest_client'
2
+ require 'xml'
3
+ require 'active_support'
4
+
5
+ module Riki
6
+ class Base
7
+ class << self
8
+ attr_accessor :username, :password, :domain, :url
9
+ attr_writer :cache
10
+
11
+ def url
12
+ @url ||= 'http://en.wikipedia.org/w/api.php'
13
+ end
14
+
15
+ HEADERS = {'User-Agent' => "Riki/v#{Riki::VERSION}"}
16
+
17
+ DEFAULT_OPTIONS = {:limit => 500,
18
+ :maxlag => 5,
19
+ :retry_count => 3,
20
+ :retry_delay => 10}
21
+
22
+ def cache
23
+ @cache ||= ActiveSupport::Cache::NullStore.new
24
+ end
25
+
26
+ def find_by_id(ids)
27
+ expects_array = ids.first.kind_of?(Array)
28
+ return ids.first if expects_array && ids.first.empty?
29
+
30
+ ids = ids.flatten.compact.uniq
31
+
32
+ case ids.size
33
+ when 0
34
+ raise NotFoundError, "Couldn't find #{name} without an ID"
35
+ when 1
36
+ result = find_one(ids.first)
37
+ expects_array ? [ result ] : result
38
+ else
39
+ find_some(ids)
40
+ end
41
+ end
42
+
43
+ #
44
+ # Login to MediaWiki
45
+ #
46
+ def login
47
+ raise "No authentication information provided" if Riki::Base.username.nil? || Riki::Base.password.nil?
48
+ api_request({'action' => 'login', 'lgname' => Riki::Base.username, 'lgpassword' => Riki::Base.password, 'lgdomain' => Riki::Base.domain})
49
+ end
50
+
51
+ # Generic API request to API
52
+ #
53
+ # [form_data] hash or string of attributes to post
54
+ # [continue_xpath] XPath selector for query continue parameter
55
+ # [retry_count] Counter for retries
56
+ #
57
+ # Returns XML document
58
+ def api_request(form_data, continue_xpath = nil, retry_count = 1, p_options = {})
59
+ if (!cookies.blank? && cookies["#{cookieprefix}_session"])
60
+ # Attempt to re-use cookies
61
+ else
62
+ # there is no session, we are not currently trying to login, and we gave sufficient auth information
63
+ login if form_data['action'] != 'login' && !Riki::Base.username.blank? && !Riki::Base.password.blank?
64
+ end
65
+
66
+ options = DEFAULT_OPTIONS.merge(p_options)
67
+
68
+ if form_data.kind_of? Hash
69
+ form_data['format'] = 'xml'
70
+ form_data['maxlag'] = options[:maxlag]
71
+ form_data['includexmlnamespace'] = 'true'
72
+ end
73
+
74
+ RestClient.post(Riki::Base.url, form_data, HEADERS.merge({:cookies => cookies})) do |response, &block|
75
+ if response.code == 503 and retry_count < options[:retry_count]
76
+ log.warn("503 Service Unavailable: #{response.body}. Retry in #{options[:retry_delay]} seconds.")
77
+ sleep(options[:retry_delay])
78
+ api_request(form_data, continue_xpath, retry_count + 1)
79
+ end
80
+
81
+ # Check response for errors and return XML
82
+ raise "Bad response: #{response}" unless response.code >= 200 and response.code < 300
83
+
84
+ doc = parse_response(response.dup)
85
+
86
+ # TODO Handle MediaWiki redirects
87
+
88
+ if(form_data['action'] == 'login')
89
+ login_result = doc.find_first('m:login')['result']
90
+ Riki::Base.cookieprefix = doc.find_first('m:login')['cookieprefix']
91
+
92
+ @cookies.merge!(response.cookies)
93
+ case login_result
94
+ when "Success" then Riki::Base.cache.write(cache_key(:cookies), @cookies)
95
+ when "NeedToken" then api_request(form_data.merge('lgtoken' => doc.find('/m:api/m:login').first['token']))
96
+ else raise Unauthorized.new("Login failed: " + login_result)
97
+ end
98
+ end
99
+ continue = (continue_xpath and doc.find('m:query-continue').empty?) ? doc.find_first(continue_xpath).value : nil
100
+
101
+ return [doc, continue]
102
+ end
103
+ end
104
+
105
+ def parse_response(res)
106
+ res = res.force_encoding("UTF-8") if res.respond_to?(:force_encoding)
107
+ doc = XML::Parser.string(res).parse.root
108
+ doc.namespaces.default_prefix = 'm'
109
+
110
+ raise "Response does not contain Mediawiki API XML: #{res}" unless ["api", "mediawiki"].include? doc.name
111
+
112
+ errors = doc.find('/api/error')
113
+ if errors.any?
114
+ code = errors.first["code"]
115
+ info = errors.first["info"]
116
+ raise RikiError.lookup(code).new(info)
117
+ end
118
+
119
+ if warnings = doc.find('warnings') && warnings
120
+ warning("API warning: #{warnings.map{|e| e.text}.join(", ")}")
121
+ end
122
+
123
+ doc
124
+ end
125
+
126
+ def cache_key(key)
127
+ "#{Riki::Base.url}##{key}"
128
+ end
129
+
130
+ def cookies
131
+ @cookies ||= Riki::Base.cache.fetch(cache_key(:cookies)) do
132
+ {}
133
+ end
134
+ end
135
+
136
+ def cookieprefix
137
+ @cookieprefix ||= Riki::Base.cache.read(cache_key(:cookieprefix))
138
+ end
139
+
140
+ def cookieprefix=(cp)
141
+ @cookieprefix ||= Riki::Base.cache.write(cache_key(:cookieprefix), cp)
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,46 @@
1
+ module Riki
2
+ # = Riki Errors
3
+ #
4
+ # Generic Riki exception class.
5
+ class RikiError < StandardError
6
+ class << self
7
+ def lookup(code)
8
+ ERRORS[code.to_sym] || RikiError
9
+ end
10
+ end
11
+ end
12
+
13
+ # Raised when the authorization information was present, but was not sufficient to complete the request
14
+ class Unauthorized < RikiError #:nodoc:
15
+ end
16
+
17
+ # Raised when an object cannot be found with the query parameters supplied
18
+ class NotFound < RikiError #:nodoc:
19
+ attr_reader :title
20
+
21
+ def initialize(message, title)
22
+ super(message)
23
+ @title = title
24
+ end
25
+ end
26
+
27
+ # Raised when a page that was searched for could not be found
28
+ class PageNotFound < NotFound #:nodoc:
29
+ def initialize(title)
30
+ super("Could not find a page named '#{title}'", title)
31
+ end
32
+ end
33
+
34
+ # Raised when a page that was searched for is not a valid
35
+ class PageInvalid < NotFound #:nodoc:
36
+ def initialize(title)
37
+ super("The page title '#{title}' is not valid", title)
38
+ end
39
+ end
40
+
41
+ class RikiError < StandardError
42
+ ERRORS = {
43
+ :readapidenied => Unauthorized
44
+ }
45
+ end
46
+ end