]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/translator.rb
plugin(translator): removed google translate
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / translator.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Translator plugin for rbot
5 #
6 # Author:: Yaohan Chen <yaohan.chen@gmail.com>
7 # Copyright:: (C) 2007 Yaohan Chen
8 # License:: GPLv2
9 #
10 # This plugin allows using rbot to translate text on a few translation services
11 #
12 # TODO
13 #
14 # * Configuration for whether to show translation engine
15 # * Optionally sync default translators with karma.rb ranking
16
17 require 'set'
18 require 'timeout'
19
20 # base class for implementing a translation service
21 # = Attributes
22 # direction:: supported translation directions, a hash where each key is a source
23 #             language name, and each value is Set of target language names. The
24 #             methods in the Direction module are convenient for initializing this
25 #             attribute
26 class Translator
27   INFO = 'Some translation service'
28
29   class UnsupportedDirectionError < ArgumentError
30   end
31
32   class NoTranslationError < RuntimeError
33   end
34
35   attr_reader :directions, :cache
36
37   def initialize(directions, cache={}, bot)
38     @directions = directions
39     @cache = cache
40     @bot = bot
41   end
42
43   # whether the translator supports this direction
44   def support?(from, to)
45     from != to && @directions[from].include?(to)
46   end
47
48   # this implements argument checking and caching. subclasses should define the
49   # do_translate method to implement actual translation
50   def translate(text, from, to)
51     raise UnsupportedDirectionError unless support?(from, to)
52     raise ArgumentError, _("Cannot translate empty string") if text.empty?
53     request = [text, from, to]
54     unless @cache.has_key? request
55       translation = do_translate(text, from, to)
56       raise NoTranslationError if translation.empty?
57       @cache[request] = translation
58     else
59       @cache[request]
60     end
61   end
62
63   module Direction
64     # given the set of supported languages, return a hash suitable for the directions
65     # attribute which includes any language to any other language
66     def self.all_to_all(languages)
67       directions = all_to_none(languages)
68       languages.each {|l| directions[l] = languages.to_set}
69       directions
70     end
71
72     # a hash suitable for the directions attribute which includes any language from/to
73     # the given set of languages (center_languages)
74     def self.all_from_to(languages, center_languages)
75       directions = all_to_none(languages)
76       center_languages.each {|l| directions[l] = languages - [l]}
77       (languages - center_languages).each {|l| directions[l] = center_languages.to_set}
78       directions
79     end
80
81     # get a hash from a list of pairs
82     def self.pairs(list_of_pairs)
83       languages = list_of_pairs.flatten.to_set
84       directions = all_to_none(languages)
85       list_of_pairs.each do |(from, to)|
86         directions[from] << to
87       end
88       directions
89     end
90
91     # an empty hash with empty sets as default values
92     def self.all_to_none(languages)
93       Hash.new do |h, k|
94         # always return empty set when the key is non-existent, but put empty set in the
95         # hash only if the key is one of the languages
96         if languages.include? k
97           h[k] = Set.new
98         else
99           Set.new
100         end
101       end
102     end
103   end
104 end
105
106 class YandexTranslator < Translator
107   INFO = 'Yandex Translator <http://translate.yandex.com/>'
108   LANGUAGES = %w{ar az be bg ca cs da de el en es et fi fr he hr hu hy it ka lt lv mk nl no pl pt ro ru sk sl sq sr sv tr uk}
109
110   URL = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=%s&lang=%s-%s&text=%s'
111   KEY = 'trnsl.1.1.20140326T031210Z.1e298c8adb4058ed.d93278fea8d79e0a0ba76b6ab4bfbf6ac43ada72'
112   def initialize(cache, bot)
113     require 'uri'
114     require 'json'
115     super(Translator::Direction.all_to_all(LANGUAGES), cache, bot)
116   end
117
118   def translate(text, from, to)
119     res = @bot.httputil.get_response(URL % [KEY, from, to, URI.escape(text)])
120     res = JSON.parse(res.body)
121
122     if res['code'] != 200
123       raise Translator::NoTranslationError
124     else
125       res['text'].join(' ')
126     end
127   end
128
129 end
130
131 class TranslatorPlugin < Plugin
132   Config.register Config::IntegerValue.new('translator.timeout',
133     :default => 30, :validate => Proc.new{|v| v > 0},
134     :desc => _("Number of seconds to wait for the translation service before timeout"))
135   Config.register Config::StringValue.new('translator.destination',
136     :default => "en",
137     :desc => _("Default destination language to be used with translate command"))
138
139   TRANSLATORS = {
140     'yandex' => YandexTranslator,
141   }
142
143   def initialize
144     super
145     @failed_translators = []
146     @translators = {}
147     TRANSLATORS.each_pair do |name, c|
148       watch_for_fail(name) do
149         @translators[name] = c.new(@registry.sub_registry(name), @bot)
150         map "#{name} :from :to *phrase",
151           :action => :cmd_translate, :thread => true
152       end
153     end
154
155     Config.register Config::ArrayValue.new('translator.default_list',
156       :default => TRANSLATORS.keys,
157       :validate => Proc.new {|l| l.all? {|t| TRANSLATORS.has_key?(t)}},
158       :desc => _("List of translators to try in order when translator name not specified"),
159       :on_change => Proc.new {|bot, v| update_default})
160     update_default
161   end
162
163   def watch_for_fail(name, &block)
164     begin
165       yield
166     rescue Exception
167       debug 'Translator error: '+$!.to_s
168       debug $@.join("\n")
169       @failed_translators << { :name => name, :reason => $!.to_s }
170
171       warning _("Translator %{name} cannot be used: %{reason}") %
172              {:name => name, :reason => $!}
173       map "#{name} [*args]", :action => :failed_translator,
174                              :defaults => {:name => name, :reason => $!}
175     end
176   end
177
178   def failed_translator(m, params)
179     m.reply _("Translator %{name} cannot be used: %{reason}") %
180             {:name => params[:name], :reason => params[:reason]}
181   end
182
183   def help(plugin, topic=nil)
184     case (topic.intern rescue nil)
185     when :failed
186       unless @failed_translators.empty?
187         failed_list = @failed_translators.map { |t| _("%{bold}%{translator}%{bold}: %{reason}") % {
188           :translator => t[:name],
189           :reason => t[:reason],
190           :bold => Bold
191         }}
192
193         _("Failed translators: %{list}") % { :list => failed_list.join(", ") }
194       else
195         _("None of the translators failed")
196       end
197     else
198       if @translators.has_key?(plugin)
199         translator = @translators[plugin]
200         _('%{translator} <from> <to> <phrase> => Look up phrase using %{info}, supported from -> to languages: %{directions}') % {
201           :translator => plugin,
202           :info => translator.class::INFO,
203           :directions => translator.directions.map do |source, targets|
204                            _('%{source} -> %{targets}') %
205                            {:source => source, :targets => targets.to_a.join(', ')}
206                          end.join(' | ')
207         }
208       else
209         help_str = _('Command: <translator> <from> <to> <phrase>, where <translator> is one of: %{translators}. If "translator" is used in place of the translator name, the first translator in translator.default_list which supports the specified direction will be picked automatically. Use "help <translator>" to look up supported from and to languages') %
210                      {:translators => @translators.keys.join(', ')}
211
212         help_str << "\n" + _("%{bold}Note%{bold}: %{failed_amt} translators failed, see %{reverse}%{prefix}help translate failed%{reverse} for details") % {
213           :failed_amt => @failed_translators.size,
214           :bold => Bold,
215           :reverse => Reverse,
216           :prefix => @bot.config['core.address_prefix'].first
217         }
218
219         help_str
220       end
221     end
222   end
223
224   def languages
225     @languages ||= @translators.map { |t| t.last.directions.keys }.flatten.uniq
226   end
227
228   def update_default
229     @default_translators = bot.config['translator.default_list'] & @translators.keys
230   end
231
232   def cmd_translator(m, params)
233     params[:to] = @bot.config['translator.destination'] if params[:to].nil?
234     params[:from] ||= 'auto'
235     translator = @default_translators.find {|t| @translators[t].support?(params[:from], params[:to])}
236
237     if translator
238       cmd_translate m, params.merge({:translator => translator, :show_provider => false})
239     else
240       m.reply _('None of the default translators (translator.default_list) supports translating from %{source} to %{target}') % {:source => params[:from], :target => params[:to]}
241     end
242   end
243
244   def cmd_translate(m, params)
245     # get the first word of the command
246     tname = params[:translator] || m.message[/\A(\w+)\s/, 1]
247     translator = @translators[tname]
248     from, to, phrase = params[:from], params[:to], params[:phrase].to_s
249     if translator
250       watch_for_fail(tname) do
251         begin
252           translation = Timeout.timeout(@bot.config['translator.timeout']) do
253             translator.translate(phrase, from, to)
254           end
255           m.reply(if params[:show_provider]
256                     _('%{translation} (provided by %{translator})') %
257                       {:translation => translation, :translator => tname.gsub("_", " ")}
258                   else
259                     translation
260                   end)
261
262         rescue Translator::UnsupportedDirectionError
263           m.reply _("%{translator} doesn't support translating from %{source} to %{target}") %
264                   {:translator => tname, :source => from, :target => to}
265         rescue Translator::NoTranslationError
266           m.reply _('%{translator} failed to provide a translation') %
267                   {:translator => tname}
268         rescue Timeout::Error
269           m.reply _('The translator timed out')
270         end
271       end
272     else
273       m.reply _('No translator called %{name}') % {:name => tname}
274     end
275   end
276
277   # URL translation has nothing to do with Translators so let's make it
278   # separate, and Google exclusive for now
279   def cmd_translate_url(m, params)
280     params[:to] = @bot.config['translator.destination'] if params[:to].nil?
281     params[:from] ||= 'auto'
282
283     translate_url = "http://translate.google.com/translate?sl=%{from}&tl=%{to}&u=%{url}" % {
284       :from => params[:from],
285       :to   => params[:to],
286       :url  => CGI.escape(params[:url].to_s)
287     }
288
289     m.reply(translate_url)
290   end
291 end
292
293 plugin = TranslatorPlugin.new
294 req = Hash[*%w(from to).map { |e| [e.to_sym, /#{plugin.languages.join("|")}/] }.flatten]
295
296 plugin.map 'translate [:from] [:to] :url',
297            :action => :cmd_translate_url, :requirements => req.merge(:url => %r{^https?://[^\s]*})
298 plugin.map 'translator [:from] [:to] :url',
299            :action => :cmd_translate_url, :requirements => req.merge(:url => %r{^https?://[^\s]*})
300 plugin.map 'translate [:from] [:to] *phrase',
301            :action => :cmd_translator, :thread => true, :requirements => req
302 plugin.map 'translator [:from] [:to] *phrase',
303            :action => :cmd_translator, :thread => true, :requirements => req