]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/dictclient.rb
53c1f7c0f154268e053b8f1005a521c3e3b2afa3
[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 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     definition[1..-1].join.gsub(/\s+/, ' ').strip
46   end
47 end
48
49 class DictClientPlugin < Plugin
50   BotConfig.register BotConfigStringValue.new('dictclient.server',
51     :default => 'dict.org',
52     :desc => 'Hostname or hostname:port of the DICT server used to lookup words')
53   BotConfig.register BotConfigIntegerValue.new('dictclient.max_defs_before_collapse',
54     :default => 4,
55     :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')
56   BotConfig.register BotConfigIntegerValue.new('dictclient.max_length_per_def',
57     :default => 200,
58     :desc => 'Each definition is truncated to this length') 
59   BotConfig.register BotConfigStringValue.new('dictclient.headword_format',
60     :default => "#{Bold}<headword>#{Bold}",
61     :desc => 'Format of headwords; <word> will be replaced with the actual word')
62   BotConfig.register BotConfigStringValue.new('dictclient.database_format',
63     :default => "#{Underline}<database>#{Underline}",
64     :desc => 'Format of database names; <database> will be replaced with the database name')
65   BotConfig.register BotConfigStringValue.new('dictclient.definition_format',
66     :default => '<headword>: <definition> -<database>',
67     :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')
68   BotConfig.register BotConfigStringValue.new('dictclient.match_format',
69     :default => '<matches>––<database>',
70     :desc => 'Format of match results. <matches> will be replaced with the formatted headwords, <database> with the formatted database name')
71   
72   def initialize
73     super
74   end
75   
76   # create a DICT object, which is passed to the block. after the block finishes,
77   # the DICT object is automatically disconnected. the return value of the block
78   # is returned from this method.
79   # if an IRC message argument is passed, the error message will be replied
80   def with_dict(m=nil &block)
81     server, port = @bot.config['dictclient.server'].split ':' if @bot.config['dictclient.server']
82     server ||= 'dict.org'
83     port ||= DICT::DEFAULT_PORT
84     ret = nil
85     begin
86       dict = DICT.new(server, port)
87       ret = yield dict
88       dict.disconnect
89     rescue ConnectError
90       m.reply 'An error occured connecting to the DICT server. Check the dictclient.server configuration or retry later' if m
91     rescue ProtocolError
92       m.reply 'A protocol error occured' if m
93     rescue DICTError
94       m.reply 'An error occured' if m
95     end
96     ret
97   end
98   
99   def format_headword(w)
100     @bot.config['dictclient.headword_format'].gsub '<headword>', w
101   end
102     
103   def format_database(d)
104     @bot.config['dictclient.database_format'].gsub '<database>', d
105   end
106   
107   def cmd_define(m, params)
108     phrase = params[:phrase].to_s
109     results = with_dict(m) {|d| d.define(params[:database], params[:phrase])}
110     m.reply(
111       if results
112         # only list database headers if definitions come from different databases and
113         # the number of definitions is above dictclient.max_defs_before_collapse
114         if results.any? {|r| r.database != results[0].database} &&
115            results.length > @bot.config['dictclient.max_defs_before_collapse']
116           "Definitions for #{format_headword phrase} were found in #{
117             results.collect {|r| r.database}.uniq.collect {|d|
118               format_database d}.join ', '
119           }. Specify database to view full result."
120         # otherwise display the definitions
121         else
122           results.collect {|r|
123             @bot.config['dictclient.definition_format'].gsub(
124               '<headword>', format_headword(r.headword)
125             ).gsub(
126               '<database>', format_database(r.database)
127             ).gsub(
128               '<definition>', r.body.truncate(@bot.config['dictclient.max_length_per_def'])
129             )
130           }.join ' '
131         end
132       else
133         "No definition for #{format_headword phrase} found from #{
134           format_database params[:database]}."
135       end
136     )
137   end
138   
139   def cmd_match(m, params)
140     phrase = params[:phrase].to_s
141     results = with_dict(m) {|d| d.match(params[:database],
142                                         params[:strategy], phrase)}
143     m.reply(
144       if results
145         results.collect {|database, matches|
146           @bot.config['dictclient.match_format'].gsub(
147             '<matches>', matches.collect {|m| format_headword m}.join(', ')
148           ).gsub(
149             '<database>', format_database(database)
150           )
151         }.join ' '
152       else
153         "Nothing matched for #{format_headword phrase
154         } from #{format_database params[:database]} using #{params[:strategy]}"
155       end
156     )
157   end
158     
159   def cmd_databases(m, params)
160     with_dict(m) do |d|
161       m.reply "Databases: #{
162         d.show_db.collect {|db, d|"#{format_database db}: #{d}"}.join '; '
163       }"
164     end
165   end
166   
167   def cmd_strategies(m, params)
168     with_dict(m) do |d|
169       m.reply "Strategies: #{
170         d.show_strat.collect {|s, d| "#{s}: #{d}"}.join '; '
171       }"
172     end
173   end
174     
175   def help(plugin, topic='')
176     "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"
177   end
178 end
179
180 plugin = DictClientPlugin.new
181
182 plugin.map 'define *phrase [from :database]',
183            :action => 'cmd_define',
184            :defaults => {:database => DICT::ALL_DATABASES}
185
186 plugin.map 'match *phrase [using :strategy] [from :database]',
187            :action => 'cmd_match',
188            :defaults => {:database => DICT::ALL_DATABASES,
189                          :strategy => DICT::DEFAULT_MATCH_STRATEGY }
190
191 plugin.map 'dictclient databases', :action => 'cmd_databases'
192 plugin.map 'dictclient strategies', :action => 'cmd_strategies'