sunflower 0.4.5 → 0.5
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/README +1 -1
- data/example-bot.rb +7 -8
- data/lib/sunflower/commontasks.rb +3 -77
- data/lib/sunflower/core.rb +257 -78
- data/lib/sunflower/list.rb +255 -0
- data/lib/sunflower.rb +1 -1
- metadata +3 -12
- data/lib/sunflower/listmaker.rb +0 -160
- data/scripts/ZDBOT.rb +0 -62
- data/scripts/aktualizacjapilkarzy.rb +0 -339
- data/scripts/author-list.rb +0 -36
- data/scripts/changeimage.rb +0 -42
- data/scripts/fix-multiple-same-refs.rb +0 -102
- data/scripts/insight.rb +0 -133
- data/scripts/make-id2team-list.rb +0 -32
- data/scripts/wanted.rb +0 -72
- data/use-easy-bot.rb +0 -54
    
        data/README
    CHANGED
    
    
    
        data/example-bot.rb
    CHANGED
    
    | @@ -1,12 +1,11 @@ | |
| 1 | 
            +
            # coding: utf-8
         | 
| 1 2 | 
             
            # This is the most basic bot possible.
         | 
| 2 3 |  | 
| 3 | 
            -
            require 'sunflower | 
| 4 | 
            +
            require 'sunflower'
         | 
| 4 5 |  | 
| 5 | 
            -
            s=Sunflower.new
         | 
| 6 | 
            -
            s. | 
| 6 | 
            +
            s = Sunflower.new.login
         | 
| 7 | 
            +
            s.summary = 'test summary'
         | 
| 7 8 |  | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
            p | 
| 11 | 
            -
            p.write p.text+"\n\ntest"
         | 
| 12 | 
            -
            p.save
         | 
| 9 | 
            +
            p = Sunflower::Page.new 'Test'
         | 
| 10 | 
            +
            p.text += "\n\ntest"
         | 
| 11 | 
            +
            p.save
         | 
| @@ -1,80 +1,6 @@ | |
| 1 1 | 
             
            # coding: utf-8
         | 
| 2 | 
            -
            # extends Page with some methods letting easily perform common tasks
         | 
| 3 2 |  | 
| 4 | 
            -
            class Page
         | 
| 5 | 
            -
            	# executes methods on self
         | 
| 6 | 
            -
            	# "commands" is array of arrays
         | 
| 7 | 
            -
            	# page.execute([
         | 
| 8 | 
            -
            		# [:replace, 'something', 'whatever'],
         | 
| 9 | 
            -
            		# [:append, 'some things']
         | 
| 10 | 
            -
            	# ])
         | 
| 11 | 
            -
            	#        equals to
         | 
| 12 | 
            -
            	# page.replace('something', 'whatever')
         | 
| 13 | 
            -
            	# page.append('some things')
         | 
| 14 | 
            -
            	
         | 
| 15 | 
            -
            	# allowed modifiers:
         | 
| 16 | 
            -
            	# r, required
         | 
| 17 | 
            -
            	# oi:module, only-if:module
         | 
| 18 | 
            -
            	# !oi:module, only-if-not:module
         | 
| 19 | 
            -
            	# s:append to summary, summary:append to summary
         | 
| 20 | 
            -
            	def execute commands
         | 
| 21 | 
            -
            		originalText = self.text.dup
         | 
| 22 | 
            -
            	
         | 
| 23 | 
            -
            		commands.each do |cmd|
         | 
| 24 | 
            -
            			f=cmd.shift
         | 
| 25 | 
            -
            			if f.class==Array
         | 
| 26 | 
            -
            				methodName=f.shift
         | 
| 27 | 
            -
            				modifiers=f.map{|i|
         | 
| 28 | 
            -
            					i+=':' if !i.include? ':'
         | 
| 29 | 
            -
            					i=i.split(':',-1)
         | 
| 30 | 
            -
            					i[0]=i[0].downcase.gsub(/[^a-z!]/,'')
         | 
| 31 | 
            -
            					
         | 
| 32 | 
            -
            					i[0]='r' if i[0]=='required'
         | 
| 33 | 
            -
            					i[0]='oi' if i[0]=='onlyif'
         | 
| 34 | 
            -
            					i[0]='!oi' if i[0]=='onlyifnot'
         | 
| 35 | 
            -
            					i[0]='s' if i[0]=='summary'
         | 
| 36 | 
            -
            					
         | 
| 37 | 
            -
            					type=i.shift
         | 
| 38 | 
            -
            					i=i.join(':')
         | 
| 39 | 
            -
            					
         | 
| 40 | 
            -
            					[type,i]
         | 
| 41 | 
            -
            				}
         | 
| 42 | 
            -
            				modifiers=Hash[*(modifiers.flatten)]
         | 
| 43 | 
            -
            			else
         | 
| 44 | 
            -
            				methodName=f
         | 
| 45 | 
            -
            				modifiers={}
         | 
| 46 | 
            -
            			end
         | 
| 47 | 
            -
            			
         | 
| 48 | 
            -
            			if modifiers['oi']
         | 
| 49 | 
            -
            				if !@modulesExecd.index(modifiers['oi'].strip)
         | 
| 50 | 
            -
            					next #skip this command
         | 
| 51 | 
            -
            				end
         | 
| 52 | 
            -
            			end
         | 
| 53 | 
            -
            			if modifiers['!oi']
         | 
| 54 | 
            -
            				if @modulesExecd.index(modifiers['oi'].strip)
         | 
| 55 | 
            -
            					next #skip this command
         | 
| 56 | 
            -
            				end
         | 
| 57 | 
            -
            			end
         | 
| 58 | 
            -
            			
         | 
| 59 | 
            -
            			oldText=self.text
         | 
| 60 | 
            -
            			self.method(methodName).call(*cmd)
         | 
| 61 | 
            -
            			newText=self.text
         | 
| 62 | 
            -
            			
         | 
| 63 | 
            -
            			@modulesExecd<<methodName if oldText!=newText
         | 
| 64 | 
            -
            			
         | 
| 65 | 
            -
            			if modifiers['s'] && oldText!=newText
         | 
| 66 | 
            -
            				@summaryAppend<<modifiers['s'].strip
         | 
| 67 | 
            -
            			end
         | 
| 68 | 
            -
            			
         | 
| 69 | 
            -
            			if modifiers['r'] && oldText==newText
         | 
| 70 | 
            -
            				self.text = originalText
         | 
| 71 | 
            -
            				break #reset text and stop executing commands
         | 
| 72 | 
            -
            			end
         | 
| 73 | 
            -
            		end
         | 
| 74 | 
            -
            	end
         | 
| 75 | 
            -
            	
         | 
| 76 | 
            -
            	
         | 
| 77 | 
            -
            	
         | 
| 3 | 
            +
            class Sunflower::Page
         | 
| 78 4 | 
             
            	# replaces "from" with "to" in page text
         | 
| 79 5 | 
             
            	# "from" may be regex
         | 
| 80 6 | 
             
            	def replace from, to, once=false
         | 
| @@ -152,7 +78,7 @@ class Page | |
| 152 78 |  | 
| 153 79 | 
             
            		str.gsub!(/\[\[([^\|\]]+)(\||\]\])/){
         | 
| 154 80 | 
             
            			name, rest = $1, $2
         | 
| 155 | 
            -
            			"[[#{self.sunflower.cleanup_title name}#{rest}"
         | 
| 81 | 
            +
            			"[[#{self.sunflower.cleanup_title name, true, true}#{rest}"
         | 
| 156 82 | 
             
            		}
         | 
| 157 83 |  | 
| 158 84 | 
             
            		# headings
         | 
| @@ -164,7 +90,7 @@ class Page | |
| 164 90 |  | 
| 165 91 | 
             
            		if wikiid = self.sunflower.siteinfo['general']['wikiid']
         | 
| 166 92 | 
             
            			if self.respond_to? :"code_cleanup_#{wikiid}"
         | 
| 167 | 
            -
            				str = self. | 
| 93 | 
            +
            				str = self.send :"code_cleanup_#{wikiid}", str
         | 
| 168 94 | 
             
            			end
         | 
| 169 95 | 
             
            		end
         | 
| 170 96 |  | 
    
        data/lib/sunflower/core.rb
    CHANGED
    
    | @@ -3,8 +3,6 @@ require 'rest-client' | |
| 3 3 | 
             
            require 'json'
         | 
| 4 4 | 
             
            require 'cgi'
         | 
| 5 5 |  | 
| 6 | 
            -
            class SunflowerError < StandardError; end
         | 
| 7 | 
            -
             | 
| 8 6 | 
             
            # Main class. To start working, you have to create new Sunflower:
         | 
| 9 7 | 
             
            #   s = Sunflower.new('en.wikipedia.org')
         | 
| 10 8 | 
             
            # And then log in:
         | 
| @@ -19,7 +17,7 @@ class SunflowerError < StandardError; end | |
| 19 17 | 
             
            #
         | 
| 20 18 | 
             
            # You can use multiple Sunflowers at once, to work on multiple wikis.
         | 
| 21 19 | 
             
            class Sunflower
         | 
| 22 | 
            -
            	VERSION = '0. | 
| 20 | 
            +
            	VERSION = '0.5'
         | 
| 23 21 |  | 
| 24 22 | 
             
            	INVALID_CHARS = %w(# < > [ ] | { })
         | 
| 25 23 | 
             
            	INVALID_CHARS_REGEX = Regexp.union *INVALID_CHARS
         | 
| @@ -28,7 +26,19 @@ class Sunflower | |
| 28 26 | 
             
            	def self.path
         | 
| 29 27 | 
             
            		File.join(ENV['HOME'], 'sunflower-userdata')
         | 
| 30 28 | 
             
            	end
         | 
| 31 | 
            -
             | 
| 29 | 
            +
            	
         | 
| 30 | 
            +
            	# Returns array of [url, username, password], or nil if userdata is unavailable or invalid.
         | 
| 31 | 
            +
            	def self.read_userdata
         | 
| 32 | 
            +
            		data = nil
         | 
| 33 | 
            +
            		data = File.read(Sunflower.path).split(/\r?\n/).map{|i| i.strip} rescue nil
         | 
| 34 | 
            +
            		
         | 
| 35 | 
            +
            		if data && data.length==3 && data.all?{|a| a and a != ''}
         | 
| 36 | 
            +
            			return data
         | 
| 37 | 
            +
            		else
         | 
| 38 | 
            +
            			return nil
         | 
| 39 | 
            +
            		end
         | 
| 40 | 
            +
            	end
         | 
| 41 | 
            +
            	
         | 
| 32 42 | 
             
            	# Summary used when saving edits with this Sunflower.
         | 
| 33 43 | 
             
            	attr_accessor :summary
         | 
| 34 44 | 
             
            	# Whether to run #code_cleanup when calling #save.
         | 
| @@ -38,6 +48,11 @@ class Sunflower | |
| 38 48 | 
             
            	# Siteinfo, as returned by API call.
         | 
| 39 49 | 
             
            	attr_accessor :siteinfo
         | 
| 40 50 |  | 
| 51 | 
            +
            	# Whether we are logged in.
         | 
| 52 | 
            +
            	def logged_in?; @loggedin; end
         | 
| 53 | 
            +
            	# Username if logged in; nil otherwise.
         | 
| 54 | 
            +
            	attr_reader :username
         | 
| 55 | 
            +
            	
         | 
| 41 56 | 
             
            	# Whether this user (if logged in) has bot rights.
         | 
| 42 57 | 
             
            	def is_bot?; @is_bot; end
         | 
| 43 58 |  | 
| @@ -49,29 +64,72 @@ class Sunflower | |
| 49 64 | 
             
            	attr_writer :log
         | 
| 50 65 | 
             
            	def log?; @log; end
         | 
| 51 66 |  | 
| 52 | 
            -
            	#  | 
| 53 | 
            -
            	 | 
| 54 | 
            -
             | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 67 | 
            +
            	# Used by #initialize to convert short identifiers such as "b:pl" to domains such as "pl.wikibooks.org".
         | 
| 68 | 
            +
            	# Identifier is of the format "type:lang" or "lang:type" (see below for valid values).
         | 
| 69 | 
            +
            	# 
         | 
| 70 | 
            +
            	# Either or both parts can be ommitted; default type is "w", default lang is "en". 
         | 
| 71 | 
            +
            	# (Since clashes are impossible, the colon can be ommitted in such cases as well.)
         | 
| 72 | 
            +
            	# 
         | 
| 73 | 
            +
            	# lang can be any valid language code. It is ignored for type == "meta" or "commons".
         | 
| 74 | 
            +
            	# 
         | 
| 75 | 
            +
            	# Valid values for type are the same as used for inter-wiki links, that is:
         | 
| 76 | 
            +
            	# [w] Wikipedia
         | 
| 77 | 
            +
            	# [b] Wikibooks
         | 
| 78 | 
            +
            	# [n] Wikinews
         | 
| 79 | 
            +
            	# [q] Wikiquote
         | 
| 80 | 
            +
            	# [s] Wikisource
         | 
| 81 | 
            +
            	# [v] Wikiversity
         | 
| 82 | 
            +
            	# [wikt] Wiktionary
         | 
| 83 | 
            +
            	# [species] Wikispecies
         | 
| 84 | 
            +
            	# [commons] Wikimedia Commons
         | 
| 85 | 
            +
            	# [meta] Wikimedia Meta-Wiki 
         | 
| 86 | 
            +
            	def self.resolve_wikimedia_id id
         | 
| 87 | 
            +
            		keys = id.split(':').select{|a| a and !a.empty? }
         | 
| 88 | 
            +
            		
         | 
| 89 | 
            +
            		raise ArgumentError, 'invalid format' if keys.length > 2
         | 
| 90 | 
            +
            		
         | 
| 91 | 
            +
            		type_map = {
         | 
| 92 | 
            +
            			'b' => 'XX.wikibooks.org',
         | 
| 93 | 
            +
            			'q' => 'XX.wikiquote.org',
         | 
| 94 | 
            +
            			'n' => 'XX.wikinews.org',
         | 
| 95 | 
            +
            			'w' => 'XX.wikipedia.org',
         | 
| 96 | 
            +
            			'wikt' => 'XX.wiktionary.org',
         | 
| 97 | 
            +
            			'species' => 'XX.wikispecies.org',
         | 
| 98 | 
            +
            			'v' => 'XX.wikiversity.org',
         | 
| 99 | 
            +
            			's' => 'XX.wikisource.org',
         | 
| 100 | 
            +
            			'commons' => 'commons.wikimedia.org',
         | 
| 101 | 
            +
            			'meta' => 'meta.wikimedia.org',
         | 
| 102 | 
            +
            		}
         | 
| 103 | 
            +
            		
         | 
| 104 | 
            +
            		types, langs = keys.partition{|a| type_map.keys.include? a }
         | 
| 105 | 
            +
            		type = types.first || 'w'
         | 
| 106 | 
            +
            		lang = langs.first || 'en'
         | 
| 60 107 |  | 
| 108 | 
            +
            		return type_map[type].sub 'XX', lang
         | 
| 109 | 
            +
            	end
         | 
| 110 | 
            +
            	
         | 
| 111 | 
            +
            	# Initialize a new Sunflower working on a wiki with given URL, for ex. "pl.wikipedia.org". url can also be a shorthand identifier such as "b:pl" - see Sunflower.resolve_wikimedia_id for details.
         | 
| 112 | 
            +
            	def initialize url=nil
         | 
| 61 113 | 
             
            		if !url
         | 
| 62 | 
            -
            			 | 
| 63 | 
            -
             | 
| 114 | 
            +
            			userdata = Sunflower.read_userdata()
         | 
| 115 | 
            +
            			
         | 
| 116 | 
            +
            			if userdata
         | 
| 117 | 
            +
            				url = userdata[0]
         | 
| 64 118 | 
             
            			else
         | 
| 65 | 
            -
            				raise  | 
| 119 | 
            +
            				raise Sunflower::Error, 'initialize: no URL supplied and no userdata found!'
         | 
| 66 120 | 
             
            			end
         | 
| 67 121 | 
             
            		end
         | 
| 68 122 |  | 
| 69 | 
            -
            		@ | 
| 70 | 
            -
            		 | 
| 123 | 
            +
            		@wikiURL = (url.include?('.') ? url : Sunflower.resolve_wikimedia_id(url))
         | 
| 124 | 
            +
            		
         | 
| 125 | 
            +
            		@warnings = true
         | 
| 126 | 
            +
            		@log = false
         | 
| 71 127 |  | 
| 72 | 
            -
            		@ | 
| 128 | 
            +
            		@loggedin = false
         | 
| 129 | 
            +
            		@username = nil
         | 
| 130 | 
            +
            		@is_bot = false
         | 
| 73 131 |  | 
| 74 | 
            -
            		@ | 
| 132 | 
            +
            		@cookies = {}
         | 
| 75 133 |  | 
| 76 134 | 
             
            		siprop = 'general|namespaces|namespacealiases|specialpagealiases|magicwords|interwikimap|dbrepllag|statistics|usergroups|extensions|fileextensions|rightsinfo|languages|skins|extensiontags|functionhooks|showhooks|variables'
         | 
| 77 135 | 
             
            		@siteinfo = self.API(action: 'query', meta: 'siteinfo', siprop: siprop)['query']
         | 
| @@ -79,6 +137,10 @@ class Sunflower | |
| 79 137 | 
             
            		_build_ns_map
         | 
| 80 138 | 
             
            	end
         | 
| 81 139 |  | 
| 140 | 
            +
            	def inspect
         | 
| 141 | 
            +
            		"#<Sunflower #{@loggedin ? @username : "[anon]"}@#{@wikiURL}#{@is_bot ? ' [bot]' : ''}>"
         | 
| 142 | 
            +
            	end
         | 
| 143 | 
            +
            	
         | 
| 82 144 | 
             
            	# Private. Massages data from siteinfo to be used for recognizing namespaces.
         | 
| 83 145 | 
             
            	def _build_ns_map
         | 
| 84 146 | 
             
            		@namespace_to_id = {} # all keys lowercase
         | 
| @@ -99,6 +161,7 @@ class Sunflower | |
| 99 161 | 
             
            			@namespace_to_id[ h['*'].downcase ] = h['id'].to_i
         | 
| 100 162 | 
             
            		end
         | 
| 101 163 | 
             
            	end
         | 
| 164 | 
            +
            	private :_build_ns_map
         | 
| 102 165 |  | 
| 103 166 | 
             
            	# Call the API. Returns a hash of JSON response. Request can be a HTTP request string or a hash.
         | 
| 104 167 | 
             
            	def API request
         | 
| @@ -116,25 +179,69 @@ class Sunflower | |
| 116 179 | 
             
            		JSON.parse resp.to_str
         | 
| 117 180 | 
             
            	end
         | 
| 118 181 |  | 
| 182 | 
            +
            	# Call the API. While more results are available via the xxcontinue parameter, call it again. 
         | 
| 183 | 
            +
            	# 
         | 
| 184 | 
            +
            	# Assumes action=query. 
         | 
| 185 | 
            +
            	# 
         | 
| 186 | 
            +
            	# By default returns an array of all API responses. Attempts to merge the responses
         | 
| 187 | 
            +
            	# into a response that would have been returned if the limit was infinite. merge_on is
         | 
| 188 | 
            +
            	# the key of response['query'] to merge consecutive responses on.
         | 
| 189 | 
            +
            	# 
         | 
| 190 | 
            +
            	# If limit given, will perform no more than this many API calls before returning.
         | 
| 191 | 
            +
            	# If limit is 1, behaves exactly like #API.
         | 
| 192 | 
            +
            	# 
         | 
| 193 | 
            +
            	# Example: get list of all pages linking to Main Page:
         | 
| 194 | 
            +
            	#   
         | 
| 195 | 
            +
            	#   sunflower.API_continued "action=query&list=backlinks&bllimit=max&bltitle=Main_Page", 'backlinks', 'blcontinue'
         | 
| 196 | 
            +
            	def API_continued request, merge_on, xxcontinue, limit=nil
         | 
| 197 | 
            +
            		out = []
         | 
| 198 | 
            +
            		
         | 
| 199 | 
            +
            		# gather
         | 
| 200 | 
            +
            		res = self.API(request)
         | 
| 201 | 
            +
            		out << res
         | 
| 202 | 
            +
            		while res['query-continue'] and (!limit || out.length < limit)
         | 
| 203 | 
            +
            			api_endpoint = if request.is_a? String
         | 
| 204 | 
            +
            				request + "&#{xxcontinue}=#{res["query-continue"][merge_on][xxcontinue]}"
         | 
| 205 | 
            +
            			elsif request.is_a? Hash
         | 
| 206 | 
            +
            				request.merge({xxcontinue => res["query-continue"][merge_on][xxcontinue]})
         | 
| 207 | 
            +
            			end
         | 
| 208 | 
            +
            			
         | 
| 209 | 
            +
            			res = self.API(api_endpoint)
         | 
| 210 | 
            +
            			out << res
         | 
| 211 | 
            +
            		end
         | 
| 212 | 
            +
            		
         | 
| 213 | 
            +
            		# merge
         | 
| 214 | 
            +
            		merged = out[0]
         | 
| 215 | 
            +
            		meth = (merged['query'][merge_on].is_a?(Hash) ? :merge! : :concat)
         | 
| 216 | 
            +
            		
         | 
| 217 | 
            +
            		out.drop(1).each do |cur|
         | 
| 218 | 
            +
            			merged['query'][merge_on].send meth, cur['query'][merge_on]
         | 
| 219 | 
            +
            		end
         | 
| 220 | 
            +
            		
         | 
| 221 | 
            +
            		return merged
         | 
| 222 | 
            +
            	end
         | 
| 223 | 
            +
            	
         | 
| 119 224 | 
             
            	# Log in using given info.
         | 
| 120 225 | 
             
            	def login user='', password=''
         | 
| 121 226 | 
             
            		if user=='' || password==''
         | 
| 122 | 
            -
            			 | 
| 123 | 
            -
             | 
| 124 | 
            -
             | 
| 227 | 
            +
            			userdata = Sunflower.read_userdata()
         | 
| 228 | 
            +
            			
         | 
| 229 | 
            +
            			if userdata
         | 
| 230 | 
            +
            				user = userdata[1] if user==''
         | 
| 231 | 
            +
            				password = userdata[2] if password==''
         | 
| 125 232 | 
             
            			else
         | 
| 126 | 
            -
            				raise  | 
| 233 | 
            +
            				raise Sunflower::Error, 'login: no user/pass supplied and no userdata found!'
         | 
| 127 234 | 
             
            			end
         | 
| 128 235 | 
             
            		end
         | 
| 129 236 |  | 
| 130 | 
            -
            		raise  | 
| 237 | 
            +
            		raise Sunflower::Error, 'bad username!' if user =~ INVALID_CHARS_REGEX
         | 
| 131 238 |  | 
| 132 239 |  | 
| 133 240 | 
             
            		# 1. get the login token
         | 
| 134 241 | 
             
            		response = RestClient.post(
         | 
| 135 242 | 
             
            			'http://'+@wikiURL+'/w/api.php?'+"action=login&lgname=#{CGI.escape user}&lgpassword=#{CGI.escape password}"+'&format=json', 
         | 
| 136 243 | 
             
            			nil,
         | 
| 137 | 
            -
            			{:user_agent =>  | 
| 244 | 
            +
            			{:user_agent => "Sunflower #{VERSION} alpha"}
         | 
| 138 245 | 
             
            		)
         | 
| 139 246 |  | 
| 140 247 | 
             
            		@cookies = response.cookies
         | 
| @@ -146,7 +253,7 @@ class Sunflower | |
| 146 253 | 
             
            		response = RestClient.post(
         | 
| 147 254 | 
             
            			'http://'+@wikiURL+'/w/api.php?'+"action=login&lgname=#{CGI.escape user}&lgpassword=#{CGI.escape password}&lgtoken=#{token}"+'&format=json',
         | 
| 148 255 | 
             
            			nil,
         | 
| 149 | 
            -
            			{:user_agent =>  | 
| 256 | 
            +
            			{:user_agent => "Sunflower #{VERSION} alpha", :cookies => @cookies}
         | 
| 150 257 | 
             
            		)
         | 
| 151 258 |  | 
| 152 259 | 
             
            		json = JSON.parse response.to_str
         | 
| @@ -158,7 +265,7 @@ class Sunflower | |
| 158 265 | 
             
            		})
         | 
| 159 266 |  | 
| 160 267 |  | 
| 161 | 
            -
            		raise  | 
| 268 | 
            +
            		raise Sunflower::Error, 'unable to log in (no cookies received)!' if !@cookies
         | 
| 162 269 |  | 
| 163 270 |  | 
| 164 271 | 
             
            		# 3. confirm you did log in by checking the watchlist.
         | 
| @@ -166,9 +273,12 @@ class Sunflower | |
| 166 273 | 
             
            		r=self.API('action=query&list=watchlistraw')
         | 
| 167 274 | 
             
            		if r['error'] && r['error']['code']=='wrnotloggedin'
         | 
| 168 275 | 
             
            			@loggedin=false
         | 
| 169 | 
            -
            			raise  | 
| 276 | 
            +
            			raise Sunflower::Error, 'unable to log in!'
         | 
| 170 277 | 
             
            		end
         | 
| 171 278 |  | 
| 279 | 
            +
            		# set the username
         | 
| 280 | 
            +
            		@username = user
         | 
| 281 | 
            +
            		
         | 
| 172 282 | 
             
            		# 4. check bot rights
         | 
| 173 283 | 
             
            		r=self.API('action=query&list=allusers&aulimit=1&augroup=bot&aufrom='+(CGI.escape user))
         | 
| 174 284 | 
             
            		unless r['query']['allusers'][0]['name']==user
         | 
| @@ -187,7 +297,9 @@ class Sunflower | |
| 187 297 | 
             
            	end
         | 
| 188 298 |  | 
| 189 299 | 
             
            	# Cleans up underscores, percent-encoding and title-casing in title (with optional anchor).
         | 
| 190 | 
            -
            	def cleanup_title title
         | 
| 300 | 
            +
            	def cleanup_title title, preserve_case=false, preserve_colon=false
         | 
| 301 | 
            +
            		return '' if title.strip == ''
         | 
| 302 | 
            +
            		
         | 
| 191 303 | 
             
            		name, anchor = title.split '#', 2
         | 
| 192 304 |  | 
| 193 305 | 
             
            		# CGI.unescape also changes pluses to spaces; code borrowed from there
         | 
| @@ -197,6 +309,10 @@ class Sunflower | |
| 197 309 | 
             
            		name = unescape.call(name).gsub(/[ _]+/, ' ').strip
         | 
| 198 310 | 
             
            		anchor = unescape.call(anchor.gsub(/\.([0-9a-fA-F]{2})/, '%\1')).gsub(/[ _]+/, ' ').strip if anchor
         | 
| 199 311 |  | 
| 312 | 
            +
            		leading_colon = name[0]==':'
         | 
| 313 | 
            +
            		name = name.sub(/^:\s*/, '') if leading_colon
         | 
| 314 | 
            +
            		leading_colon = false if !preserve_colon
         | 
| 315 | 
            +
            		
         | 
| 200 316 | 
             
            		# FIXME unicode? downcase, upcase
         | 
| 201 317 |  | 
| 202 318 | 
             
            		if name.include? ':'
         | 
| @@ -206,9 +322,9 @@ class Sunflower | |
| 206 322 | 
             
            			end
         | 
| 207 323 | 
             
            		end
         | 
| 208 324 |  | 
| 209 | 
            -
            		name[0] = name[0].upcase if @siteinfo["general"]["case"] == "first-letter"
         | 
| 325 | 
            +
            		name[0] = name[0].upcase if !preserve_case and @siteinfo["general"]["case"] == "first-letter"
         | 
| 210 326 |  | 
| 211 | 
            -
            		return [ns ? "#{ns}:" : nil,  name,  anchor ? "##{anchor}" : nil].join ''
         | 
| 327 | 
            +
            		return [leading_colon ? ':' : nil,  ns ? "#{ns}:" : nil,  name,  anchor ? "##{anchor}" : nil].join ''
         | 
| 212 328 | 
             
            	end
         | 
| 213 329 |  | 
| 214 330 | 
             
            	# Returns the localized namespace name for ns, which may be namespace number, canonical name, or any namespace alias.
         | 
| @@ -245,76 +361,103 @@ class Sunflower | |
| 245 361 | 
             
            end
         | 
| 246 362 |  | 
| 247 363 | 
             
            # Class representing a single Wiki page. To load specified page, use #new. To save it back, use #save.
         | 
| 248 | 
            -
            class Page
         | 
| 364 | 
            +
            class Sunflower::Page
         | 
| 249 365 | 
             
            	# Characters which MediaWiki does not permit in page title.
         | 
| 250 366 | 
             
            	INVALID_CHARS = %w(# < > [ ] | { })
         | 
| 251 367 | 
             
            	# Regex matching characters which MediaWiki does not permit in page title.
         | 
| 252 368 | 
             
            	INVALID_CHARS_REGEX = Regexp.union *INVALID_CHARS
         | 
| 253 369 |  | 
| 254 | 
            -
            	# The  | 
| 370 | 
            +
            	# The Sunflower instance this page belongs to.
         | 
| 371 | 
            +
            	attr_reader :sunflower
         | 
| 372 | 
            +
            	
         | 
| 373 | 
            +
            	# The current text of the page. Lazy-loaded.
         | 
| 255 374 | 
             
            	attr_accessor :text
         | 
| 256 | 
            -
            	# The text of the page, as of when it was loaded.
         | 
| 375 | 
            +
            	# The text of the page, as of when it was loaded. Lazy-loaded.
         | 
| 257 376 | 
             
            	attr_reader :orig_text
         | 
| 258 377 |  | 
| 259 | 
            -
            	#  | 
| 260 | 
            -
            	 | 
| 378 | 
            +
            	# Page title, as passed to #initialize and cleaned by Sunflower#cleanup_title.
         | 
| 379 | 
            +
            	# Real page title as canonicalized by MediaWiki software can be accessed via #real_title
         | 
| 380 | 
            +
            	# (but it should always be the same).
         | 
| 381 | 
            +
            	attr_reader :title
         | 
| 261 382 |  | 
| 262 | 
            -
            	#  | 
| 263 | 
            -
            	 | 
| 264 | 
            -
            	#  | 
| 265 | 
            -
            	attr_reader : | 
| 266 | 
            -
            	 | 
| 383 | 
            +
            	# Value of given attribute, as returned by API call prop=info for this page. Lazy-loaded.
         | 
| 384 | 
            +
            	attr_reader :pageid, :ns, :touched, :lastrevid, :counter, :length, :starttimestamp, :edittoken, :protection
         | 
| 385 | 
            +
            	# Value of `title` attribute, as returned by API call prop=info for this page. Lazy-loaded. See #title.
         | 
| 386 | 
            +
            	attr_reader :real_title
         | 
| 387 | 
            +
            	
         | 
| 388 | 
            +
            	# Whether this datum is already loaded. Can be set to true to suppress loading
         | 
| 389 | 
            +
            	# (used e.g. by Sunflower::List#pages_preloaded)
         | 
| 390 | 
            +
            	attr_accessor :preloaded_text, :preloaded_attrs
         | 
| 267 391 |  | 
| 268 392 | 
             
            	# calling any of these accessors will fetch the data.
         | 
| 269 | 
            -
            	 | 
| 393 | 
            +
            	# getters...
         | 
| 394 | 
            +
            	[:pageid, :ns, :real_title, :touched, :lastrevid, :counter, :length, :starttimestamp, :edittoken, :protection].each do |meth|
         | 
| 395 | 
            +
            		remove_method meth # to avoid warnings when running with ruby -w
         | 
| 270 396 | 
             
            		define_method meth do
         | 
| 271 397 | 
             
            			preload_attrs unless @preloaded_attrs
         | 
| 272 398 | 
             
            			instance_variable_get "@#{meth}"
         | 
| 273 399 | 
             
            		end
         | 
| 274 400 | 
             
            	end
         | 
| 401 | 
            +
            	[:text, :orig_text].each do |meth|
         | 
| 402 | 
            +
            		remove_method meth # to avoid warnings when running with ruby -w
         | 
| 403 | 
            +
            		define_method meth do
         | 
| 404 | 
            +
            			preload_text unless @preloaded_text
         | 
| 405 | 
            +
            			instance_variable_get "@#{meth}"
         | 
| 406 | 
            +
            		end
         | 
| 407 | 
            +
            	end
         | 
| 408 | 
            +
            	# setters...
         | 
| 409 | 
            +
            	[:text=].each do |meth|
         | 
| 410 | 
            +
            		remove_method meth # to avoid warnings when running with ruby -w
         | 
| 411 | 
            +
            		define_method meth do |a|
         | 
| 412 | 
            +
            			preload_text unless @preloaded_text
         | 
| 413 | 
            +
            			instance_variable_set "@#{meth.to_s.chop}", a
         | 
| 414 | 
            +
            		end
         | 
| 415 | 
            +
            	end
         | 
| 275 416 |  | 
| 276 | 
            -
            	# Load the specified page.  | 
| 277 | 
            -
            	#
         | 
| 278 | 
            -
            	#  | 
| 279 | 
            -
            	 | 
| 280 | 
            -
             | 
| 417 | 
            +
            	# Load the specified page. 
         | 
| 418 | 
            +
            	# Only the text will be immediately loaded - attributes and edit token will be loaded when needed, or when you call #preload_attrs.
         | 
| 419 | 
            +
            	# 
         | 
| 420 | 
            +
            	# If you are using multiple Sunflowers, you have to specify which one this page belongs to using the second argument of function.
         | 
| 421 | 
            +
            	# You can pass either a Sunflower object, wiki URL, or a shorthand id as specified in Sunflower.resolve_wikimedia_id.
         | 
| 422 | 
            +
            	def initialize title='', url=''
         | 
| 423 | 
            +
            		raise Sunflower::Error, 'title invalid: '+title if title =~ INVALID_CHARS_REGEX
         | 
| 281 424 |  | 
| 282 425 | 
             
            		@modulesExecd=[] #used by sunflower-commontasks.rb
         | 
| 283 426 | 
             
            		@summaryAppend=[] #used by sunflower-commontasks.rb
         | 
| 284 427 |  | 
| 285 | 
            -
            		 | 
| 286 | 
            -
            		
         | 
| 287 | 
            -
             | 
| 288 | 
            -
             | 
| 289 | 
            -
            			 | 
| 290 | 
            -
            			raise  | 
| 428 | 
            +
            		case url
         | 
| 429 | 
            +
            		when Sunflower
         | 
| 430 | 
            +
            			@sunflower = url
         | 
| 431 | 
            +
            		when '', nil
         | 
| 432 | 
            +
            			count = ObjectSpace.each_object(Sunflower){|o| @sunflower=o}
         | 
| 433 | 
            +
            			raise Sunflower::Error, 'no Sunflowers present' if count==0
         | 
| 434 | 
            +
            			raise Sunflower::Error, 'you must pass wiki name if using multiple Sunflowers at once' if count>1
         | 
| 291 435 | 
             
            		else
         | 
| 292 | 
            -
            			 | 
| 436 | 
            +
            			url = (url.include?('.') ? url : Sunflower.resolve_wikimedia_id(url))
         | 
| 437 | 
            +
            			ObjectSpace.each_object(Sunflower){|o| @sunflower=o if o.wikiURL==url}
         | 
| 438 | 
            +
            			raise Sunflower::Error, "no Sunflower for #{url}" if !@sunflower
         | 
| 293 439 | 
             
            		end
         | 
| 294 440 |  | 
| 295 | 
            -
            		raise SunflowerError, "no Sunflower for #{wiki}" if !@sunflower
         | 
| 296 | 
            -
            		
         | 
| 297 441 | 
             
            		@title = @sunflower.cleanup_title title
         | 
| 298 442 |  | 
| 299 | 
            -
            		 | 
| 300 | 
            -
             | 
| 301 | 
            -
            			@orig_text=''
         | 
| 302 | 
            -
            			return
         | 
| 303 | 
            -
            		end
         | 
| 304 | 
            -
            		
         | 
| 305 | 
            -
            		preload_text
         | 
| 443 | 
            +
            		@preloaded_text = false
         | 
| 444 | 
            +
            		@preloaded_attrs = false
         | 
| 306 445 | 
             
            	end
         | 
| 307 446 |  | 
| 308 447 | 
             
            	# Load the text of this page. Semi-private.
         | 
| 309 448 | 
             
            	def preload_text
         | 
| 310 | 
            -
            		 | 
| 311 | 
            -
            		r = r['query']['pages'].first
         | 
| 312 | 
            -
            		if r['missing']
         | 
| 449 | 
            +
            		if title == ''
         | 
| 313 450 | 
             
            			@text = ''
         | 
| 314 | 
            -
            		elsif r['invalid']
         | 
| 315 | 
            -
            			raise SunflowerError, 'title invalid: '+@title
         | 
| 316 451 | 
             
            		else
         | 
| 317 | 
            -
            			 | 
| 452 | 
            +
            			r = @sunflower.API('action=query&prop=revisions&rvprop=content&titles='+CGI.escape(@title))
         | 
| 453 | 
            +
            			r = r['query']['pages'].values.first
         | 
| 454 | 
            +
            			if r['missing']
         | 
| 455 | 
            +
            				@text = ''
         | 
| 456 | 
            +
            			elsif r['invalid']
         | 
| 457 | 
            +
            				raise Sunflower::Error, 'title invalid: '+@title
         | 
| 458 | 
            +
            			else
         | 
| 459 | 
            +
            				@text = r['revisions'][0]['*']
         | 
| 460 | 
            +
            			end
         | 
| 318 461 | 
             
            		end
         | 
| 319 462 |  | 
| 320 463 | 
             
            		@orig_text = @text.dup
         | 
| @@ -325,8 +468,9 @@ class Page | |
| 325 468 | 
             
            	# Load the metadata associated with this page. Semi-private.
         | 
| 326 469 | 
             
            	def preload_attrs
         | 
| 327 470 | 
             
            		r = @sunflower.API('action=query&prop=info&inprop=protection&intoken=edit&titles='+CGI.escape(@title))
         | 
| 328 | 
            -
            		r = r['query']['pages'].first
         | 
| 471 | 
            +
            		r = r['query']['pages'].values.first
         | 
| 329 472 | 
             
            		r.each{|key, value|
         | 
| 473 | 
            +
            			key = 'real_title' if key == 'title'
         | 
| 330 474 | 
             
            			self.instance_variable_set('@'+key, value)
         | 
| 331 475 | 
             
            		}
         | 
| 332 476 |  | 
| @@ -359,8 +503,8 @@ class Page | |
| 359 503 |  | 
| 360 504 | 
             
            		summary = @sunflower.summary if !summary
         | 
| 361 505 |  | 
| 362 | 
            -
            		raise  | 
| 363 | 
            -
            		raise  | 
| 506 | 
            +
            		raise Sunflower::Error, 'title invalid: '+title if title =~ INVALID_CHARS_REGEX
         | 
| 507 | 
            +
            		raise Sunflower::Error, 'no summary!' if (!summary or summary=='') && @summaryAppend.empty?
         | 
| 364 508 |  | 
| 365 509 | 
             
            		unless @summaryAppend.empty?
         | 
| 366 510 | 
             
            			if !summary or summary==''
         | 
| @@ -383,18 +527,53 @@ class Page | |
| 383 527 | 
             
            	alias :put :save
         | 
| 384 528 |  | 
| 385 529 | 
             
            	def self.get title, wiki=''
         | 
| 386 | 
            -
            		 | 
| 530 | 
            +
            		self.new(title, wiki)
         | 
| 387 531 | 
             
            	end
         | 
| 388 532 |  | 
| 389 533 | 
             
            	def self.load title, wiki=''
         | 
| 390 | 
            -
            		 | 
| 534 | 
            +
            		self.new(title, wiki)
         | 
| 535 | 
            +
            	end
         | 
| 536 | 
            +
            end
         | 
| 537 | 
            +
             | 
| 538 | 
            +
            # For backwards compatibility. Deprecated.
         | 
| 539 | 
            +
            class Page # :nodoc:
         | 
| 540 | 
            +
            	class << self
         | 
| 541 | 
            +
            		def new *a
         | 
| 542 | 
            +
            			warn "warning: toplevel Page class has been renamed to Sunflower::Page, this alias will be removed in v0.6"
         | 
| 543 | 
            +
            			Sunflower::Page.new *a
         | 
| 544 | 
            +
            		end
         | 
| 545 | 
            +
            		alias get new
         | 
| 546 | 
            +
            		alias load new
         | 
| 391 547 | 
             
            	end
         | 
| 392 548 | 
             
            end
         | 
| 393 549 |  | 
| 394 | 
            -
             | 
| 395 | 
            -
             | 
| 396 | 
            -
             | 
| 397 | 
            -
             | 
| 550 | 
            +
            # For backwards compatibility. Deprecated.
         | 
| 551 | 
            +
            # 
         | 
| 552 | 
            +
            # We use inheritance shenanigans to keep the usage in "begin ... rescue ... end" working.
         | 
| 553 | 
            +
            class SunflowerError < StandardError # :nodoc:
         | 
| 554 | 
            +
            	%w[== backtrace exception inspect message set_backtrace to_s].each do |meth|
         | 
| 555 | 
            +
            		define_method meth.to_sym do |*a, &b|
         | 
| 556 | 
            +
            			if self.class == Sunflower::Error and !@warned
         | 
| 557 | 
            +
            				warn "warning: toplevel SunflowerError class has been renamed to Sunflower::Error, this alias will be removed in v0.6"
         | 
| 558 | 
            +
            				@warned = true
         | 
| 559 | 
            +
            			end
         | 
| 560 | 
            +
            			
         | 
| 561 | 
            +
            			super *a, &b
         | 
| 562 | 
            +
            		end
         | 
| 563 | 
            +
            	end
         | 
| 564 | 
            +
            	
         | 
| 565 | 
            +
            	class << self
         | 
| 566 | 
            +
            		def new *a
         | 
| 567 | 
            +
            			warn "warning: toplevel SunflowerError class has been renamed to Sunflower::Error, this alias will be removed in v0.6" unless self == Sunflower::Error
         | 
| 568 | 
            +
            			super
         | 
| 569 | 
            +
            		end
         | 
| 570 | 
            +
            		def exception *a
         | 
| 571 | 
            +
            			warn "warning: toplevel SunflowerError class has been renamed to Sunflower::Error, this alias will be removed in v0.6" unless self == Sunflower::Error
         | 
| 572 | 
            +
            			super
         | 
| 573 | 
            +
            		end
         | 
| 398 574 | 
             
            	end
         | 
| 399 575 | 
             
            end
         | 
| 400 576 |  | 
| 577 | 
            +
             | 
| 578 | 
            +
            # Represents an error raised by Sunflower.
         | 
| 579 | 
            +
            class Sunflower::Error < SunflowerError; end
         |