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