]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/geoip.rb
geoip: Add blogama and allow for fallback options
[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   def self.blogama(ip)
48     url = "http://ipinfodb.com/ip_query.php?ip="
49     debug "Requesting #{url+ip}"
50
51     xml = Irc::Utils.bot.httputil.get(url+ip)
52
53     if xml
54       obj = REXML::Document.new(xml)
55       debug "Found #{obj}"
56       newobj = {
57         :country => obj.elements["Response"].elements["CountryName"].text,
58         :city => obj.elements["Response"].elements["City"].text,
59         :region => obj.elements["Response"].elements["RegionName"].text,
60       }
61       debug "Returning #{newobj}"
62       return newobj
63     else
64       raise InvalidHostError
65     end
66   end
67
68   def self.resolve(hostname, api)
69     raise InvalidHostError unless valid_host?(hostname)
70
71     begin
72       ip = Resolv.getaddress(hostname)
73     rescue Resolv::ResolvError
74       raise InvalidHostError
75     end
76
77     jump_table = {
78         "blogama" => Proc.new { |ip| blogama(ip) },
79         "kapsi" => Proc.new { |ip| kapsi(ip) },
80         "geoiptool" => Proc.new { |ip| geoiptool(ip) },
81     }
82
83     raise BadAPIError unless jump_table.key?(api)
84
85     return jump_table[api].call(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 => [ "blogama", "kapsi", "geoiptool" ],
111       :desc => "Which API to use for lookups. Supported values: blogama, kapsi, geoiptool")
112
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"
115   end
116
117   def initialize
118     super
119
120     @stack = Stack.new
121   end
122
123   def whois(m)
124     nick = m.whois[:nick].downcase
125
126     # need to see if the whois reply was invoked by this plugin
127     return unless @stack.has_nick?(nick)
128
129     if m.target
130       msg = host2output(m.target.host, m.target.nick)
131     else
132       msg = "no such user on "+@bot.server.hostname.split(".")[-2]
133     end
134     @stack[nick].each do |source|
135       @bot.say source, msg
136     end
137
138     @stack.clear(nick)
139   end
140
141   def geoip(m, params)
142     if params.empty?
143       m.reply host2output(m.source.host, m.source.nick)
144     else
145       if m.replyto.class == Channel
146
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] }
149
150         if user
151           m.reply host2output(user.host, user.nick)
152           return
153         end
154       end
155
156       # input is a host name or an IP
157       if GeoIP::valid_host?(params[:input])
158          m.reply host2output(params[:input])
159
160       # assume input is a nick
161       elsif params[:input] !~ /\./
162         nick = params[:input].downcase
163
164         @stack[nick] << m.replyto
165         @bot.whois(nick)
166       else
167         m.reply "invalid input"
168       end
169     end
170   end
171
172   def host2output(host, nick=nil)
173     return "127.0.0.1 could not be res.. wait, what?" if host == "127.0.0.1"
174
175     geo = {:country => ""}
176     begin
177       apis = @bot.config['geoip.sources']
178       apis.compact.each { |api|
179         geo = GeoIP::resolve(host, api)
180         if geo[:country] != ""
181           break
182         end
183       }
184     rescue GeoIP::InvalidHostError, RuntimeError
185       if nick
186         return _("#{nick}'s location could not be resolved")
187       else
188         return _("#{host} could not be resolved")
189       end
190     rescue GeoIP::BadAPIError
191       return _("The owner configured me to use an API that doesn't exist, bug them!")
192     end
193
194     res = _("%{thing} is #{nick ? "from" : "located in"}") % {
195       :thing   => (nick ? nick : Resolv::getaddress(host)),
196       :country => geo[:country]
197     }
198
199     res << " %{city}" % {
200       :city => geo[:city]
201     } unless geo[:city].to_s.empty?
202
203     res << " %{region}," % {
204       :region  => geo[:region]
205     } unless geo[:region].to_s.empty? || geo[:region] == geo[:city]
206
207     res << " %{country}" % {
208       :country => geo[:country]
209     }
210
211     return res
212   end
213 end
214
215 plugin = GeoIpPlugin.new
216 plugin.map "geoip [:input]", :action => 'geoip', :thread => true