]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/dictclient.rb
weather: URI-encode station
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / dictclient.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: DICT (RFC 2229) Protocol Client Plugin for rbot
5 #
6 # Author:: Yaohan Chen <yaohan.chen@gmail.com>
7 # Copyright:: (C) 2007 Yaohan Chen
8 # License:: GPL v2
9 #
10 # Looks up words on a DICT server. DEFINE and MATCH commands, as well as listing of
11 # databases and strategies are supported.
12 #
13 # TODO
14 # Improve output format
15
16
17 # requires Ruby/DICT <http://www.caliban.org/ruby/ruby-dict.shtml>
18 begin
19   require 'dict'
20   class ::DICTError
21     def initialize(msg, code = 1)
22       super(msg)
23     end
24   end
25 rescue LoadError
26   raise LoadError, "Ruby/DICT not found, grab it from http://www.caliban.org/ruby/ruby-dict.shtml"
27 end
28
29 class ::String
30   # Returns a new string truncated to length 'to'
31   # If ellipsis is not given, that will just be the first n characters,
32   # Else it will return a string in the form <head><ellipsis><tail>
33   # The total length of that string will not exceed 'to'.
34   # If tail is an Integer, the tail will be exactly 'tail' characters,
35   # if it is a Float/Rational tails length will be (to*tail).ceil.
36   #
37   # Contributed by apeiros
38   def truncate(to=32, ellipsis='…', tail=0.3)
39     str  = split(//)
40     return str.first(to).join('') if !ellipsis or str.length <= to
41     to  -= ellipsis.split(//).length
42     tail = (tail*to).ceil unless Integer === tail
43     to  -= tail
44     "#{str.first(to)}#{ellipsis}#{str.last(tail)}"
45   end
46 end
47
48 class ::Definition
49   def headword
50     definition[0].strip
51   end
52
53   def body
54     # two or more consecutive newlines are replaced with double spaces, while single
55     # newlines are replaced with single spaces
56     lb = /\r?\n/
57     definition[1..-1].join.
58       gsub(/\s*(:#{lb}){2,}\s*/, '  ').
59       gsub(/\s*#{lb}\s*/, ' ').strip
60   end
61 end
62
63 class DictClientPlugin < Plugin
64   Config.register Config::StringValue.new('dictclient.server',
65     :default => 'dict.org',
66     :desc => _('Hostname or hostname:port of the DICT server used to lookup words'))
67   Config.register Config::IntegerValue.new('dictclient.max_defs_before_collapse',
68     :default => 4,
69     :desc => _('When multiple databases reply a number of definitions that above this limit, only the database names will be listed. Otherwise, the full definitions from each database are replied'))
70   Config.register Config::IntegerValue.new('dictclient.max_length_per_def',
71     :default => 200,
72     :desc => _('Each definition is truncated to this length'))
73   Config.register Config::StringValue.new('dictclient.headword_format',
74     :default => "#{Bold}<headword>#{Bold}",
75     :desc => _('Format of headwords; <word> will be replaced with the actual word'))
76   Config.register Config::StringValue.new('dictclient.database_format',
77     :default => "#{Underline}<database>#{Underline}",
78     :desc => _('Format of database names; <database> will be replaced with the database name'))
79   Config.register Config::StringValue.new('dictclient.definition_format',
80     :default => '<headword>: <definition> -<database>',
81     :desc => _('Format of definitions. <word> will be replaced with the formatted headword, <def> will be replaced with the truncated definition, and <database> with the formatted database name'))
82   Config.register Config::StringValue.new('dictclient.match_format',
83     :default => '<matches>––<database>',
84     :desc => _('Format of match results. <matches> will be replaced with the formatted headwords, <database> with the formatted database name'))
85
86   def initialize
87     super
88   end
89
90   # create a DICT object, which is passed to the block. after the block finishes,
91   # the DICT object is automatically disconnected. the return value of the block
92   # is returned from this method.
93   # if an IRC message argument is passed, the error message will be replied
94   def with_dict(m=nil, &block)
95     server, port = @bot.config['dictclient.server'].split ':' if @bot.config['dictclient.server']
96     server ||= 'dict.org'
97     port ||= DICT::DEFAULT_PORT
98     ret = nil
99     begin
100       dict = DICT.new(server, port)
101       ret = yield dict
102       dict.disconnect
103     rescue ConnectError
104       m.reply _('An error occured connecting to the DICT server. Check the dictclient.server configuration or retry later') if m
105     rescue ProtocolError
106       m.reply _('A protocol error occured') if m
107     rescue DICTError
108       m.reply _('An error occured') if m
109     end
110     ret
111   end
112
113   def format_headword(w)
114     @bot.config['dictclient.headword_format'].gsub '<headword>', w
115   end
116
117   def format_database(d)
118     @bot.config['dictclient.database_format'].gsub '<database>', d
119   end
120
121   def cmd_define(m, params)
122     phrase = params[:phrase].to_s
123     results = with_dict(m) {|d| d.define(params[:database], params[:phrase])}
124     m.reply(
125       if results
126         # only list database headers if definitions come from different databases and
127         # the number of definitions is above dictclient.max_defs_before_collapse
128         if results.any? {|r| r.database != results[0].database} &&
129            results.length > @bot.config['dictclient.max_defs_before_collapse']
130           _("Many definitions for %{phrase} were found in %{databases}. Use 'define <phrase> from <database> to view a definition.") %
131           { :phrase => format_headword(phrase),
132             :databases => results.collect {|r| r.database}.uniq.
133                                   collect {|d| format_database d}.join(', ') }
134         # otherwise display the definitions
135         else
136           results.collect {|r|
137             @bot.config['dictclient.definition_format'].gsub(
138               '<headword>', format_headword(r.headword)
139             ).gsub(
140               '<database>', format_database(r.database)
141             ).gsub(
142               '<definition>', r.body.truncate(@bot.config['dictclient.max_length_per_def'])
143             )
144           }.join ' | '
145         end
146       else
147         _("No definition for %{phrase} found from %{database}.") %
148           { :phrase => format_headword(phrase),
149             :database => format_database(params[:database]) }
150       end
151     )
152   end
153
154   def cmd_match(m, params)
155     phrase = params[:phrase].to_s
156     results = with_dict(m) {|d| d.match(params[:database],
157                                         params[:strategy], phrase)}
158     m.reply(
159       if results
160         results.collect {|database, matches|
161           @bot.config['dictclient.match_format'].gsub(
162             '<matches>', matches.collect {|hit| format_headword hit}.join(', ')
163           ).gsub(
164             '<database>', format_database(database)
165           )
166         }.join ' '
167       else
168         _("Nothing matched %{query} from %{database} using %{strategy}") %
169         { :query => format_headword(phrase),
170           :database => format_database(params[:database]),
171           :strategy => params[:strategy] }
172       end
173     )
174   end
175
176   def cmd_databases(m, params)
177     with_dict(m) do |d|
178       m.reply _("Databases: %{list}") % {
179         :list => d.show_db.collect {|db, des| "#{format_database db}: #{des}"}.join(' | ')
180       }
181     end
182   end
183
184   def cmd_strategies(m, params)
185     with_dict(m) do |d|
186       m.reply _("Strategies: %{list}") % {
187         :list => d.show_strat.collect {|s, des| "#{s}: #{des}"}.join(' | ')
188       }
189     end
190   end
191
192   def help(plugin, topic='')
193     case topic
194     when 'define'
195       _('define <phrase> [from <database>] => Show definition of a phrase')
196     when 'match'
197       _('match <phrase> [using <strategy>] [from <database>] => Show phrases matching the given pattern')
198     when 'server information'
199       _('dictclient databases => List databases; dictclient strategies => List strategies')
200     else
201       _('look up phrases on the configured DICT server. topics: define, match, server information')
202     end
203   end
204 end
205
206 plugin = DictClientPlugin.new
207
208 plugin.map 'define *phrase [from :database]',
209            :action => 'cmd_define',
210            :defaults => {:database => DICT::ALL_DATABASES},
211            :threaded => true
212
213 plugin.map 'match *phrase [using :strategy] [from :database]',
214            :action => 'cmd_match',
215            :defaults => {:database => DICT::ALL_DATABASES,
216                          :strategy => DICT::DEFAULT_MATCH_STRATEGY },
217            :threaded => true
218
219 plugin.map 'dictclient databases', :action => 'cmd_databases', :thread => true
220 plugin.map 'dictclient strategies', :action => 'cmd_strategies', :thread => true