]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/geoip.rb
refactor: httputil no longer core module see #38
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / geoip.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Geo IP Plugin
5 #
6 # Author:: Raine Virta <rane@kapsi.fi>
7 # Copyright:: (C) 2008 Raine Virta
8 # License:: GPL v2
9 #
10 # Resolves the geographic locations of users (network-wide) and IP addresses
11
12 module ::GeoIP
13   class InvalidHostError < RuntimeError; end
14   class BadAPIError < RuntimeError; end
15
16   HOST_NAME_REGEX  = /^[a-z0-9\-]+(?:\.[a-z0-9\-]+)*\.[a-z]{2,4}/i
17
18   def self.valid_host?(hostname)
19     hostname =~ HOST_NAME_REGEX ||
20     hostname =~ Resolv::IPv4::Regex && (hostname.split(".").map { |e| e.to_i }.max <= 255)
21   end
22
23   def self.geoiptool(bot, ip)
24     url = "http://www.geoiptool.com/en/?IP="
25     regexes  = {
26       :country => %r{Country:.*?<a href=".*?" target="_blank"> (.*?)</a>}m,
27       :region  => %r{Region:.*?<a href=".*?" target="_blank">(.*?)</a>}m,
28       :city    => %r{City:.*?<td align="left" class="arial_bold">(.*?)</td>}m,
29       :lat     => %r{Latitude:.*?<td align="left" class="arial_bold">(.*?)</td>}m,
30       :lon     => %r{Longitude:.*?<td align="left" class="arial_bold">(.*?)</td>}m
31     }
32     res = {}
33     raw = bot.httputil.get_response(url+ip)
34     raw = raw.decompress_body(raw.raw_body)
35
36     regexes.each { |key, regex| res[key] = raw.scan(regex).join('') }
37
38     return res
39   end
40
41   IPINFODB_URL = "http://api.ipinfodb.com/v2/ip_query.php?key=%{key}&ip=%{ip}"
42
43   def self.ipinfodb(bot, ip)
44     key = bot.config['geoip.ipinfodb_key']
45     return if not key or key.empty?
46     url = IPINFODB_URL % {
47       :ip => ip,
48       :key => key
49     }
50     debug "Requesting #{url}"
51
52     xml = bot.httputil.get(url)
53
54     if xml
55       obj = REXML::Document.new(xml)
56       debug "Found #{obj}"
57       newobj = {
58         :country => obj.elements["Response"].elements["CountryName"].text,
59         :city => obj.elements["Response"].elements["City"].text,
60         :region => obj.elements["Response"].elements["RegionName"].text,
61       }
62       debug "Returning #{newobj}"
63       return newobj
64     else
65       raise InvalidHostError
66     end
67   end
68
69   JUMP_TABLE = {
70     "ipinfodb" => Proc.new { |bot, ip| ipinfodb(bot, ip) },
71     "geoiptool" => Proc.new { |bot, ip| geoiptool(bot, ip) },
72   }
73
74   def self.resolve(bot, hostname, api)
75     raise InvalidHostError unless valid_host?(hostname)
76
77     begin
78       ip = Resolv.getaddress(hostname)
79     rescue Resolv::ResolvError
80       raise InvalidHostError
81     end
82
83     raise BadAPIError unless JUMP_TABLE.key?(api)
84
85     return JUMP_TABLE[api].call(bot, ip)
86   end
87 end
88
89 class Stack
90   def initialize
91     @hash = {}
92   end
93
94   def [](nick)
95     @hash[nick] = [] unless @hash[nick]
96     @hash[nick]
97   end
98
99   def has_nick?(nick)
100     @hash.has_key?(nick)
101   end
102
103   def clear(nick)
104     @hash.delete(nick)
105   end
106 end
107
108 class GeoIpPlugin < Plugin
109   Config.register Config::ArrayValue.new('geoip.sources',
110       :default => [ "ipinfodb", "geoiptool" ],
111       :desc => "Which API to use for lookups. Supported values: ipinfodb, geoiptool")
112   Config.register Config::StringValue.new('geoip.ipinfodb_key',
113       :default => "",
114       :desc => "API key for the IPinfoDB geolocation service")
115
116   def help(plugin, topic="")
117     "geoip [<user|hostname|ip>] => returns the geographic location of whichever has been given -- note: user can be anyone on the network"
118   end
119
120   def initialize
121     super
122
123     @stack = Stack.new
124   end
125
126   def whois(m)
127     nick = m.whois[:nick].downcase
128
129     # need to see if the whois reply was invoked by this plugin
130     return unless @stack.has_nick?(nick)
131
132     if m.target
133       msg = host2output(m.target.host, m.target.nick)
134     else
135       msg = "no such user on "+@bot.server.hostname.split(".")[-2]
136     end
137     @stack[nick].each do |source|
138       @bot.say source, msg
139     end
140
141     @stack.clear(nick)
142   end
143
144   def geoip(m, params)
145     if params.empty?
146       m.reply host2output(m.source.host, m.source.nick)
147     else
148       if m.replyto.class == Channel
149
150         # check if there is an user on the channel with nick same as input given
151         user = m.replyto.users.find { |usr| usr.nick == params[:input] }
152
153         if user
154           m.reply host2output(user.host, user.nick)
155           return
156         end
157       end
158
159       # input is a host name or an IP
160       if GeoIP::valid_host?(params[:input])
161          m.reply host2output(params[:input])
162
163       # assume input is a nick
164       elsif params[:input] !~ /\./
165         nick = params[:input].downcase
166
167         @stack[nick] << m.replyto
168         @bot.whois(nick)
169       else
170         m.reply "invalid input"
171       end
172     end
173   end
174
175   def host2output(host, nick=nil)
176     return "127.0.0.1 could not be res.. wait, what?" if host == "127.0.0.1"
177
178     geo = {:country => ""}
179     begin
180       apis = @bot.config['geoip.sources']
181       apis.compact.each { |api|
182         geo = GeoIP::resolve(@bot, host, api)
183         if geo and geo[:country] != ""
184           break
185         end
186       }
187     rescue GeoIP::InvalidHostError, RuntimeError
188       if nick
189         return _("%{nick}'s location could not be resolved") % { :nick => nick }
190       else
191         return _("%{host} could not be resolved") % { :host => host }
192       end
193     rescue GeoIP::BadAPIError
194       return _("The owner configured me to use an API that doesn't exist, bug them!")
195     end
196
197     location = []
198     location << geo[:city] unless geo[:city].nil_or_empty?
199     location << geo[:region] unless geo[:region].nil_or_empty? or geo[:region] == geo[:city]
200     location << geo[:country] unless geo[:country].nil_or_empty?
201
202     if nick
203       res = _("%{nick} is from %{location}")
204     else
205       res = _("%{host} is located in %{location}")
206     end
207
208     return res % {
209       :nick => nick,
210       :host => host,
211       :location => location.join(', ')
212     }
213   end
214 end
215
216 plugin = GeoIpPlugin.new
217 plugin.map "geoip [:input]", :action => 'geoip', :thread => true