Missed some warn -> warning conversion
[rbot] / 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 require 'dict'
19
20 class ::String
21   # Returns a new string truncated to length 'to'
22   # If ellipsis is not given, that will just be the first n characters,
23   # Else it will return a string in the form <head><ellipsis><tail>
24   # The total length of that string will not exceed 'to'.
25   # If tail is an Integer, the tail will be exactly 'tail' characters,
26   # if it is a Float/Rational tails length will be (to*tail).ceil.
27   #
28   # Contributed by apeiros
29   def truncate(to=32, ellipsis='…', tail=0.3)
30     str  = split(//)
31     return str.first(to).join('') if !ellipsis or str.length <= to
32     to  -= ellipsis.split(//).length
33     tail = (tail*to).ceil unless Integer === tail
34     to  -= tail
35     "#{str.first(to)}#{ellipsis}#{str.last(tail)}"
36   end
37 end
38
39 class ::Definition
40   def headword
41     definition[0].strip
42   end
43
44   def body
45     # two or more consecutive newlines are replaced with double spaces, while single
46     # newlines are replaced with single spaces
47     lb = /\r?\n/
48     definition[1..-1].join.
49       gsub(/\s*(:#{lb}){2,}\s*/, '  ').
50       gsub(/\s*#{lb}\s*/, ' ').strip
51   end
52 end
53
54 class DictClientPlugin < Plugin
55   BotConfig.register BotConfigStringValue.new('dictclient.server',
56     :default => 'dict.org',
57     :desc => _('Hostname or hostname:port of the DICT server used to lookup words'))
58   BotConfig.register BotConfigIntegerValue.new('dictclient.max_defs_before_collapse',
59     :default => 4,
60     :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'))
61   BotConfig.register BotConfigIntegerValue.new('dictclient.max_length_per_def',
62     :default => 200,
63     :desc => _('Each definition is truncated to this length'))
64   BotConfig.register BotConfigStringValue.new('dictclient.headword_format',
65     :default => "#{Bold}<headword>#{Bold}",
66     :desc => _('Format of headwords; <word> will be replaced with the actual word'))
67   BotConfig.register BotConfigStringValue.new('dictclient.database_format',
68     :default => "#{Underline}<database>#{Underline}",
69     :desc => _('Format of database names; <database> will be replaced with the database name'))
70   BotConfig.register BotConfigStringValue.new('dictclient.definition_format',
71     :default => '<headword>: <definition> -<database>',
72     :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'))
73   BotConfig.register BotConfigStringValue.new('dictclient.match_format',
74     :default => '<matches>––<database>',
75     :desc => _('Format of match results. <matches> will be replaced with the formatted headwords, <database> with the formatted database name'))
76   
77   def initialize
78     super
79   end
80   
81   # create a DICT object, which is passed to the block. after the block finishes,
82   # the DICT object is automatically disconnected. the return value of the block
83   # is returned from this method.
84   # if an IRC message argument is passed, the error message will be replied
85   def with_dict(m=nil &block)
86     server, port = @bot.config['dictclient.server'].split ':' if @bot.config['dictclient.server']
87     server ||= 'dict.org'
88     port ||= DICT::DEFAULT_PORT
89     ret = nil
90     begin
91       dict = DICT.new(server, port)
92       ret = yield dict
93       dict.disconnect
94     rescue ConnectError
95       m.reply _('An error occured connecting to the DICT server. Check the dictclient.server configuration or retry later') if m
96     rescue ProtocolError
97       m.reply _('A protocol error occured') if m
98     rescue DICTError
99       m.reply _('An error occured') if m
100     end
101     ret
102   end
103   
104   def format_headword(w)
105     @bot.config['dictclient.headword_format'].gsub '<headword>', w
106   end
107     
108   def format_database(d)
109     @bot.config['dictclient.database_format'].gsub '<database>', d
110   end
111   
112   def cmd_define(m, params)
113     phrase = params[:phrase].to_s
114     results = with_dict(m) {|d| d.define(params[:database], params[:phrase])}
115     m.reply(
116       if results
117         # only list database headers if definitions come from different databases and
118         # the number of definitions is above dictclient.max_defs_before_collapse
119         if results.any? {|r| r.database != results[0].database} &&
120            results.length > @bot.config['dictclient.max_defs_before_collapse']
121           _("Many definitions for %{phrase} were found in %{databases}. Use 'define <phrase> from <database> to view a definition.") % 
122           { :phrase => format_headword(phrase),
123             :databases => results.collect {|r| r.database}.uniq.
124                                   collect {|d| format_database d}.join(', ') }
125         # otherwise display the definitions
126         else
127           results.collect {|r|
128             @bot.config['dictclient.definition_format'].gsub(
129               '<headword>', format_headword(r.headword)
130             ).gsub(
131               '<database>', format_database(r.database)
132             ).gsub(
133               '<definition>', r.body.truncate(@bot.config['dictclient.max_length_per_def'])
134             )
135           }.join ' | '
136         end
137       else
138         _("No definition for %{phrase} found from %{database}.") % 
139           { :phrase => format_headword(phrase),
140             :database => format_database(params[:database]) }
141       end
142     )
143   end
144   
145   def cmd_match(m, params)
146     phrase = params[:phrase].to_s
147     results = with_dict(m) {|d| d.match(params[:database],
148                                         params[:strategy], phrase)}
149     m.reply(
150       if results
151         results.collect {|database, matches|
152           @bot.config['dictclient.match_format'].gsub(
153             '<matches>', matches.collect {|m| format_headword m}.join(', ')
154           ).gsub(
155             '<database>', format_database(database)
156           )
157         }.join ' '
158       else
159         _("Nothing matched %{query} from %{database} using %{strategy}") % 
160         { :query => format_headword(phrase),
161           :database => format_database(params[:database]),
162           :strategy => params[:strategy] }
163       end
164     )
165   end
166     
167   def cmd_databases(m, params)
168     with_dict(m) do |d|
169       m.reply _("Databases: %{list}") % {
170         :list => d.show_db.collect {|db, des| "#{format_database db}: #{des}"}.join(' | ')
171       }
172     end
173   end
174   
175   def cmd_strategies(m, params)
176     with_dict(m) do |d|
177       m.reply _("Strategies: %{list}") % {
178         :list => d.show_strat.collect {|s, des| "#{s}: #{des}"}.join(' | ')
179       }
180     end
181   end
182     
183   def help(plugin, topic='')
184     _("define <phrase> [from <database>] => Show definition of a phrase; match <phrase> [using <strategy>] [from <database>] => Show matching phrases; dictclient databases => List databases; dictclient strategies => List strategies")
185   end
186 end
187
188 plugin = DictClientPlugin.new
189
190 plugin.map 'define *phrase [from :database]',
191            :action => 'cmd_define',
192            :defaults => {:database => DICT::ALL_DATABASES}
193
194 plugin.map 'match *phrase [using :strategy] [from :database]',
195            :action => 'cmd_match',
196            :defaults => {:database => DICT::ALL_DATABASES,
197                          :strategy => DICT::DEFAULT_MATCH_STRATEGY }
198
199 plugin.map 'dictclient databases', :action => 'cmd_databases'
200 plugin.map 'dictclient strategies', :action => 'cmd_strategies'