4 # :title: Geo IP Plugin
6 # Author:: Raine Virta <rane@kapsi.fi>
7 # Copyright:: (C) 2008 Raine Virta
10 # Resolves the geographic locations of users (network-wide) and IP addresses
13 class InvalidHostError < RuntimeError; end
14 class BadAPIError < RuntimeError; end
16 HOST_NAME_REGEX = /^[a-z0-9\-]+(?:\.[a-z0-9\-]+)*\.[a-z]{2,4}/i
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)
23 def self.geoiptool(ip)
24 url = "http://www.geoiptool.com/en/?IP="
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
33 raw = Irc::Utils.bot.httputil.get_response(url+ip)
34 raw = raw.decompress_body(raw.raw_body)
36 regexes.each { |key, regex| res[key] = Iconv.conv('utf-8', 'ISO-8859-1', raw.scan(regex).to_s) }
42 url = "http://lakka.kapsi.fi:40086/lookup.yaml?host="
43 yaml = Irc::Utils.bot.httputil.get(url+ip)
44 return YAML::load(yaml)
48 url = "http://ipinfodb.com/ip_query.php?ip="
49 debug "Requesting #{url+ip}"
51 xml = Irc::Utils.bot.httputil.get(url+ip)
54 obj = REXML::Document.new(xml)
57 :country => obj.elements["Response"].elements["CountryName"].text,
58 :city => obj.elements["Response"].elements["City"].text,
59 :region => obj.elements["Response"].elements["RegionName"].text,
61 debug "Returning #{newobj}"
64 raise InvalidHostError
68 def self.resolve(hostname, api)
69 raise InvalidHostError unless valid_host?(hostname)
72 ip = Resolv.getaddress(hostname)
73 rescue Resolv::ResolvError
74 raise InvalidHostError
78 "blogama" => Proc.new { |ip| blogama(ip) },
79 "kapsi" => Proc.new { |ip| kapsi(ip) },
80 "geoiptool" => Proc.new { |ip| geoiptool(ip) },
83 raise BadAPIError unless jump_table.key?(api)
85 return jump_table[api].call(ip)
95 @hash[nick] = [] unless @hash[nick]
108 class GeoIpPlugin < Plugin
109 Config.register Config::ArrayValue.new('geoip.sources',
110 :default => [ "blogama", "kapsi", "geoiptool" ],
111 :desc => "Which API to use for lookups. Supported values: blogama, kapsi, geoiptool")
113 def help(plugin, topic="")
114 "geoip [<user|hostname|ip>] => returns the geographic location of whichever has been given -- note: user can be anyone on the network"
124 nick = m.whois[:nick].downcase
126 # need to see if the whois reply was invoked by this plugin
127 return unless @stack.has_nick?(nick)
130 msg = host2output(m.target.host, m.target.nick)
132 msg = "no such user on "+@bot.server.hostname.split(".")[-2]
134 @stack[nick].each do |source|
143 m.reply host2output(m.source.host, m.source.nick)
145 if m.replyto.class == Channel
147 # check if there is an user on the channel with nick same as input given
148 user = m.replyto.users.find { |user| user.nick == params[:input] }
151 m.reply host2output(user.host, user.nick)
156 # input is a host name or an IP
157 if GeoIP::valid_host?(params[:input])
158 m.reply host2output(params[:input])
160 # assume input is a nick
161 elsif params[:input] !~ /\./
162 nick = params[:input].downcase
164 @stack[nick] << m.replyto
167 m.reply "invalid input"
172 def host2output(host, nick=nil)
173 return "127.0.0.1 could not be res.. wait, what?" if host == "127.0.0.1"
175 geo = {:country => ""}
177 apis = @bot.config['geoip.sources']
178 apis.compact.each { |api|
179 geo = GeoIP::resolve(host, api)
180 if geo[:country] != ""
184 rescue GeoIP::InvalidHostError, RuntimeError
186 return _("#{nick}'s location could not be resolved")
188 return _("#{host} could not be resolved")
190 rescue GeoIP::BadAPIError
191 return _("The owner configured me to use an API that doesn't exist, bug them!")
194 res = _("%{thing} is #{nick ? "from" : "located in"}") % {
195 :thing => (nick ? nick : Resolv::getaddress(host)),
196 :country => geo[:country]
199 res << " %{city}" % {
201 } unless geo[:city].to_s.empty?
203 res << " %{region}," % {
204 :region => geo[:region]
205 } unless geo[:region].to_s.empty? || geo[:region] == geo[:city]
207 res << " %{country}" % {
208 :country => geo[:country]
215 plugin = GeoIpPlugin.new
216 plugin.map "geoip [:input]", :action => 'geoip', :thread => true