]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/geoip.rb
geoip: blogama is dead, long live ipinfodb
[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(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 = Irc::Utils.bot.httputil.get_response(url+ip)
34     raw = raw.decompress_body(raw.raw_body)
35
36     regexes.each { |key, regex| res[key] = Iconv.conv('utf-8', 'ISO-8859-1', raw.scan(regex).to_s) }
37
38     return res
39   end
40
41   def self.kapsi(ip)
42     url = "http://lakka.kapsi.fi:40086/lookup.yaml?host="
43     yaml = Irc::Utils.bot.httputil.get(url+ip)
44     return YAML::load(yaml)
45   end
46
47   IPINFODB_URL = "http://api.ipinfodb.com/v2/ip_query.php?key=%{key}&ip=%{ip}"
48
49   def self.ipinfodb(ip)
50     url = IPINFODB_URL % {
51       :ip => ip,
52       :key => Irc::Utils.bot.config['geoip.ipinfodb_key']
53     }
54     debug "Requesting #{url}"
55
56     xml = Irc::Utils.bot.httputil.get(url)
57
58     if xml
59       obj = REXML::Document.new(xml)
60       debug "Found #{obj}"
61       newobj = {
62         :country => obj.elements["Response"].elements["CountryName"].text,
63         :city => obj.elements["Response"].elements["City"].text,
64         :region => obj.elements["Response"].elements["RegionName"].text,
65       }
66       debug "Returning #{newobj}"
67       return newobj
68     else
69       raise InvalidHostError
70     end
71   end
72
73   def self.resolve(hostname, api)
74     raise InvalidHostError unless valid_host?(hostname)
75
76     begin
77       ip = Resolv.getaddress(hostname)
78     rescue Resolv::ResolvError
79       raise InvalidHostError
80     end
81
82     jump_table = {
83         "ipinfodb" => Proc.new { |ip| ipinfodb(ip) },
84         "kapsi" => Proc.new { |ip| kapsi(ip) },
85         "geoiptool" => Proc.new { |ip| geoiptool(ip) },
86     }
87
88     raise BadAPIError unless jump_table.key?(api)
89
90     return jump_table[api].call(ip)
91   end
92 end
93
94 class Stack
95   def initialize
96     @hash = {}
97   end
98
99   def [](nick)
100     @hash[nick] = [] unless @hash[nick]
101     @hash[nick]
102   end
103
104   def has_nick?(nick)
105     @hash.has_key?(nick)
106   end
107
108   def clear(nick)
109     @hash.delete(nick)
110   end
111 end
112
113 class GeoIpPlugin < Plugin
114   Config.register Config::ArrayValue.new('geoip.sources',
115       :default => [ "ipinfodb", "kapsi", "geoiptool" ],
116       :desc => "Which API to use for lookups. Supported values: ipinfodb, kapsi, geoiptool")
117   Config.register Config::StringValue.new('geoip.ipinfodb_key',
118       :default => "",
119       :desc => "API key for the IPinfoDB geolocation service")
120
121   def help(plugin, topic="")
122     "geoip [<user|hostname|ip>] => returns the geographic location of whichever has been given -- note: user can be anyone on the network"
123   end
124
125   def initialize
126     super
127
128     @stack = Stack.new
129   end
130
131   def whois(m)
132     nick = m.whois[:nick].downcase
133
134     # need to see if the whois reply was invoked by this plugin
135     return unless @stack.has_nick?(nick)
136
137     if m.target
138       msg = host2output(m.target.host, m.target.nick)
139     else
140       msg = "no such user on "+@bot.server.hostname.split(".")[-2]
141     end
142     @stack[nick].each do |source|
143       @bot.say source, msg
144     end
145
146     @stack.clear(nick)
147   end
148
149   def geoip(m, params)
150     if params.empty?
151       m.reply host2output(m.source.host, m.source.nick)
152     else
153       if m.replyto.class == Channel
154
155         # check if there is an user on the channel with nick same as input given
156         user = m.replyto.users.find { |user| user.nick == params[:input] }
157
158         if user
159           m.reply host2output(user.host, user.nick)
160           return
161         end
162       end
163
164       # input is a host name or an IP
165       if GeoIP::valid_host?(params[:input])
166          m.reply host2output(params[:input])
167
168       # assume input is a nick
169       elsif params[:input] !~ /\./
170         nick = params[:input].downcase
171
172         @stack[nick] << m.replyto
173         @bot.whois(nick)
174       else
175         m.reply "invalid input"
176       end
177     end
178   end
179
180   def host2output(host, nick=nil)
181     return "127.0.0.1 could not be res.. wait, what?" if host == "127.0.0.1"
182
183     geo = {:country => ""}
184     begin
185       apis = @bot.config['geoip.sources']
186       apis.compact.each { |api|
187         geo = GeoIP::resolve(host, api)
188         if geo[:country] != ""
189           break
190         end
191       }
192     rescue GeoIP::InvalidHostError, RuntimeError
193       if nick
194         return _("%{nick}'s location could not be resolved") % { :nick => nick }
195       else
196         return _("%{host} could not be resolved") % { :host => host }
197       end
198     rescue GeoIP::BadAPIError
199       return _("The owner configured me to use an API that doesn't exist, bug them!")
200     end
201
202     location = []
203     location << geo[:city] unless geo[:city].nil_or_empty?
204     location << geo[:region] unless geo[:region].nil_or_empty? or geo[:region] == geo[:city]
205     location << geo[:country] unless geo[:country].nil_or_empty?
206
207     if nick
208       res = _("%{nick} is from %{location}")
209     else
210       res = _("%{host} is located in %{location}")
211     end
212
213     return res % {
214       :nick => nick,
215       :host => host,
216       :location => location.join(', ')
217     }
218   end
219 end
220
221 plugin = GeoIpPlugin.new
222 plugin.map "geoip [:input]", :action => 'geoip', :thread => true