nadoka 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.gitignore +5 -0
  2. data/ChangeLog.old +1553 -0
  3. data/Gemfile +4 -0
  4. data/README.org +31 -0
  5. data/Rakefile +1 -0
  6. data/bin/nadoka +13 -0
  7. data/lib/rss_check.rb +206 -0
  8. data/lib/tagparts.rb +206 -0
  9. data/nadoka.gemspec +29 -0
  10. data/nadoka.rb +123 -0
  11. data/nadokarc +267 -0
  12. data/ndk/bot.rb +241 -0
  13. data/ndk/client.rb +288 -0
  14. data/ndk/config.rb +571 -0
  15. data/ndk/error.rb +61 -0
  16. data/ndk/logger.rb +311 -0
  17. data/ndk/server.rb +784 -0
  18. data/ndk/server_state.rb +324 -0
  19. data/ndk/version.rb +44 -0
  20. data/plugins/autoawaybot.nb +66 -0
  21. data/plugins/autodumpbot.nb +227 -0
  22. data/plugins/autoop.nb +56 -0
  23. data/plugins/backlogbot.nb +88 -0
  24. data/plugins/checkbot.nb +64 -0
  25. data/plugins/cronbot.nb +20 -0
  26. data/plugins/dictbot.nb +53 -0
  27. data/plugins/drbcl.rb +39 -0
  28. data/plugins/drbot.nb +93 -0
  29. data/plugins/evalbot.nb +49 -0
  30. data/plugins/gonzuibot.nb +41 -0
  31. data/plugins/googlebot.nb +345 -0
  32. data/plugins/identifynickserv.nb +43 -0
  33. data/plugins/mailcheckbot.nb +0 -0
  34. data/plugins/marldiabot.nb +99 -0
  35. data/plugins/messagebot.nb +96 -0
  36. data/plugins/modemanager.nb +150 -0
  37. data/plugins/opensearchbot.nb +156 -0
  38. data/plugins/opshop.nb +23 -0
  39. data/plugins/pastebot.nb +46 -0
  40. data/plugins/roulettebot.nb +33 -0
  41. data/plugins/rss_checkbot.nb +121 -0
  42. data/plugins/samplebot.nb +24 -0
  43. data/plugins/sendpingbot.nb +17 -0
  44. data/plugins/shellbot.nb +59 -0
  45. data/plugins/sixamobot.nb +77 -0
  46. data/plugins/tenkibot.nb +111 -0
  47. data/plugins/timestampbot.nb +62 -0
  48. data/plugins/titlebot.nb +226 -0
  49. data/plugins/translatebot.nb +301 -0
  50. data/plugins/twitterbot.nb +138 -0
  51. data/plugins/weba.nb +209 -0
  52. data/plugins/xibot.nb +113 -0
  53. data/rice/irc.rb +780 -0
  54. metadata +102 -0
@@ -0,0 +1,62 @@
1
+ # -*-ruby-*-
2
+ #
3
+ # Copyright (c) 2004-2005 SASADA Koichi <ko1 at atdot.net>
4
+ #
5
+ # This program is free software with ABSOLUTELY NO WARRANTY.
6
+ # You can re-distribute and/or modify this program under
7
+ # the same terms of the Ruby's license.
8
+ #
9
+ #
10
+ # $Id$
11
+ #
12
+
13
+ =begin
14
+
15
+ == Abstract
16
+
17
+ Add time stamp to each log.
18
+
19
+
20
+ == Configuration
21
+ BotConfig = [
22
+ {
23
+ :name => :TimeStampBot,
24
+ :interval => 60 * 60, # sec
25
+ :stampformat => '== %y/%m/%d-%H:%M:%S ==========================================',
26
+ :client => false,
27
+ }
28
+ ]
29
+
30
+ =end
31
+
32
+ class TimeStampBot < Nadoka::NDK_Bot
33
+ def bot_initialize
34
+ @interval = @bot_config.fetch(:interval, 60 * 60) # default: 1 hour
35
+ @stampformat = @bot_config.fetch(:stampformat,
36
+ '== %y/%m/%d-%H:%M:%S ==========================================')
37
+ @client = @bot_config.fetch(:client, false)
38
+ @nexttime = nexttime
39
+ end
40
+
41
+ def nexttime
42
+ t = (Time.now.to_i + @interval)
43
+ Time.at(t - (t % @interval))
44
+ end
45
+
46
+ def on_timer tm
47
+ if tm >= @nexttime
48
+ stamp_log
49
+ @nexttime = nexttime
50
+ end
51
+ end
52
+
53
+ def stamp_log
54
+ msg = @nexttime.strftime(@stampformat)
55
+ @state.channels.each{|ch|
56
+ @logger.clog(ch, msg, true)
57
+ }
58
+ @logger.slog(msg, true) if @client
59
+ end
60
+ end
61
+
62
+
@@ -0,0 +1,226 @@
1
+ # -*-ruby-*-
2
+ #
3
+ # Copyright (c) 2009, 2011 Kazuhiro NISHIYAMA
4
+ #
5
+ # This program is free software with ABSOLUTELY NO WARRANTY.
6
+ # You can re-distribute and/or modify this program under
7
+ # the same terms of the Ruby's license.
8
+ #
9
+
10
+ =begin
11
+
12
+ == Abstract
13
+
14
+ Reply title of URL.
15
+
16
+ == Configuration
17
+
18
+ BotConfig << {
19
+ :name => :TitleBot,
20
+ :ch => //,
21
+ :timeout => 10,
22
+ }
23
+
24
+ =end
25
+
26
+ require 'nkf'
27
+ require 'open-uri'
28
+ require 'timeout'
29
+ require 'tmpdir'
30
+
31
+ begin
32
+ require 'rubygems'
33
+ require 'nokogiri'
34
+ rescue LoadError
35
+ end
36
+
37
+ module URL2Title
38
+ module_function
39
+
40
+ def get_title(url)
41
+ uri = URI(url)
42
+ info = { :uri => uri }
43
+ info[:errors] = []
44
+ case uri.host
45
+ when /localhost/, /\A127\./, /\A192\.168\./, /\A10\./
46
+ info[:title] = "(ignored)"
47
+ return info
48
+ end
49
+ uri.open(:content_length_proc => proc{|x| raise Errno::EFBIG if x && x > 1048576}) do |f|
50
+ info[:uri] = f.base_uri
51
+ body = f.read
52
+
53
+ if /\.blog\d+\.fc2\.com\z/ =~ uri.host
54
+ # set last content-type only
55
+ f.meta_add_field("content-type", f.meta["content-type"].split(/, /)[-1])
56
+ end
57
+
58
+ case f.content_type
59
+ when /\Atext\//
60
+ charset = f.charset{} # without block, returns "iso-8859-1"
61
+
62
+ # Content-Encoding
63
+ case
64
+ when f.content_encoding.empty?
65
+ # ignore
66
+ when f.content_encoding.any?{|c_e| /deflate/ =~ c_e }
67
+ require "zlib"
68
+ body = Zlib::Inflate.inflate(body)
69
+ when f.content_encoding.any?{|c_e| /gzip/ =~ c_e }
70
+ require "zlib"
71
+ body = Zlib::GzipReader.new(StringIO.new(body)).read || ''
72
+ end
73
+
74
+ # encoding
75
+ if NKF.guess(body) == NKF::BINARY
76
+ info[:title] = "(binary)"
77
+ return info
78
+ end
79
+ body = NKF.nkf("-wm0x --numchar-input", body)
80
+
81
+ title = nil
82
+ case uri.host
83
+ when /\A(?:www\.so-net\.ne\.jp)\z/
84
+ if %r"<title\b(?>[^<>]*)>(.*?)</title(?>[^<>]*)>"miu =~ body
85
+ title = $1
86
+ end
87
+ if %r!<dt id="ttl">(.*?)</dt>!miu =~ body
88
+ title = $1
89
+ end
90
+ else
91
+ if %r"<title\b(?>[^<>]*)>(.*?)</title(?>[^<>]*)>"miu =~ body
92
+ title = $1
93
+ end
94
+ if uri.fragment && defined?(::Nokogiri)
95
+ begin
96
+ doc = Nokogiri::HTML(body, uri.to_s, 'utf-8')
97
+ xpath = "//*[@id='#{uri.fragment}' or @name='#{uri.fragment}']"
98
+ fragment_element = doc.xpath(xpath)
99
+ # tDiary style
100
+ unless fragment_element.xpath("span[@class='sanchor']").empty?
101
+ fragment_element = fragment_element.xpath("..")
102
+ end
103
+ info[:fragment_text] = truncate(fragment_element.text)
104
+ rescue Exception => e
105
+ info[:errors] << e
106
+ end
107
+ end
108
+ end
109
+ info[:title] = title || body
110
+ return info
111
+ when /\Aimage\//
112
+ if f.respond_to?(:path) && f.path
113
+ info[:title] = `identify '#{f.path}'`.sub(/\A#{Regexp.quote(f.path)}/, '').strip
114
+ return info
115
+ else
116
+ info[:title] = "(unknown image format)"
117
+ return info
118
+ end
119
+ else
120
+ info[:title] = "#{f.content_type} #{f.size} bytes"
121
+ return info
122
+ end
123
+ end
124
+ rescue Errno::EFBIG
125
+ info[:title] = "(too big)"
126
+ return info
127
+ end
128
+
129
+ def truncate s
130
+ if /\A(?>(.{197})....)/mu =~ s
131
+ return $1+'...'
132
+ else
133
+ return s
134
+ end
135
+ end
136
+
137
+ def prepare_url(url)
138
+ url.sub(/\/\#!\//, '/')
139
+ end
140
+
141
+ def url2title(url)
142
+ url = prepare_url(url)
143
+ info = get_title(url)
144
+ info[:title] = truncate(info[:title])
145
+ info
146
+ end
147
+ end
148
+
149
+ if __FILE__ == $0
150
+ def u2t(url)
151
+ URL2Title.url2title(url)
152
+ rescue
153
+ $!.inspect
154
+ end
155
+ if ARGV.empty?
156
+ # TODO: test
157
+ else
158
+ ARGV.each do |url|
159
+ info = u2t(url)
160
+ p info
161
+ puts url
162
+ puts info[:title]
163
+ end
164
+ end
165
+ exit
166
+ end
167
+
168
+ class TitleBot < Nadoka::NDK_Bot
169
+ include URL2Title
170
+
171
+ def bot_initialize
172
+ if @bot_config.key?(:channels)
173
+ channels = '\A(?:' + @bot_config[:channels].collect{|ch|
174
+ Regexp.quote(ch)
175
+ }.join('|') + ')\z'
176
+ @available_channel = Regexp.compile(channels)
177
+ else
178
+ @available_channel = @bot_config.fetch(:ch, //)
179
+ end
180
+
181
+ @same_bot = @bot_config.fetch(:same_bot, /(?!)/)
182
+ @nkf_options = @bot_config.fetch(:nkf, "--oc=CP50221 --numchar-input --fb-xml")
183
+ @timeout = @bot_config.fetch(:timeout, 10)
184
+ @too_long_threshold = @bot_config.fetch(:too_long_threshold, 250)
185
+ end
186
+
187
+ def send_notice(ch, msg)
188
+ msg = msg.tr("\r\n", " ")
189
+ if @nkf_options
190
+ msg = NKF.nkf(@nkf_options, msg)
191
+ end
192
+ super(ch, msg)
193
+ end
194
+
195
+ def on_privmsg prefix, ch, msg
196
+ return unless @available_channel === ch
197
+
198
+ if /https?:/ === msg
199
+ return if @state.channel_users(ccn(ch)).find{|x| @same_bot =~ x }
200
+
201
+ url, = URI.extract(msg, ["http", "https"])
202
+ info = Timeout.timeout(@timeout) do
203
+ url2title(url)
204
+ end
205
+ return unless info[:title]
206
+ if url != info[:uri].to_s
207
+ send_notice(ch, "title bot: #{info[:title]} - #{info[:uri]}")
208
+ else
209
+ send_notice(ch, "title bot: #{info[:title]}")
210
+ end
211
+ if info[:fragment_text]
212
+ # ignore when fragment_text is too long
213
+ if info[:fragment_text].size < @too_long_threshold
214
+ send_notice(ch, "title bot:: #{info[:fragment_text]}")
215
+ end
216
+ end
217
+ info[:errors].each do |e|
218
+ @manager.ndk_error e
219
+ send_notice(ch, "title bot error: #{e}")
220
+ end
221
+ end
222
+ rescue Exception => e
223
+ send_notice(ch, "title bot error! #{e}")
224
+ @manager.ndk_error e
225
+ end
226
+ end
@@ -0,0 +1,301 @@
1
+ # -*-ruby-*-
2
+ #
3
+ # Copyright (c) 2010 SASADA Koichi <ko1 at atdot.net>
4
+ #
5
+ # This program is free software with ABSOLUTELY NO WARRANTY.
6
+ # You can re-distribute and/or modify this program under
7
+ # the same terms of the Ruby's license.
8
+ #
9
+
10
+ =begin
11
+
12
+ == Usage with irc client
13
+
14
+ trans> hello
15
+ -> translate hello as a English to Japanese.
16
+
17
+ trans:ja> hello
18
+ -> ditto.
19
+
20
+ trans:en,ja> hello
21
+ -> ditto.
22
+
23
+ trans:(([lang_from],)[lang_to])> [phrase]
24
+ -> translate [phrase] as lang_from to lang_to.
25
+
26
+ transj> [phrase]
27
+ -> translate to Japanese
28
+
29
+ transe> [phrase]
30
+ -> translate to English
31
+
32
+ == Configuration:
33
+
34
+ BotConfig = [
35
+ {
36
+ :name => :TranslateBot,
37
+ :ch => /.*/,
38
+ :referer => 'http://rubyforge.org/projects/nadoka/',
39
+ # Register URL at http://code.google.com/intl/ja/apis/ajaxsearch/signup.html
40
+ # and set your URL to :referer and your API key to :api_key if you want.
41
+ :to_lang => 'ja',
42
+ :to_lang2 => 'en',
43
+ :ch_kcode => :tojis,
44
+ },
45
+ ]
46
+
47
+ =end
48
+
49
+ require 'iconv'
50
+ require 'kconv'
51
+ require 'shellwords'
52
+ require 'cgi'
53
+ require 'open-uri'
54
+ begin
55
+ require 'json'
56
+ rescue LoadError
57
+ require 'rubygems'
58
+ require 'json'
59
+ end
60
+
61
+ class TranslateBot < Nadoka::NDK_Bot
62
+ def bot_initialize
63
+ @available_channel = @bot_config[:ch] || /.*/
64
+ @search_default_lang = (@bot_config[:search_default_lang] || 'ja').sub(/^lang_/, '')
65
+ @referer = @bot_config[:referer] || 'http://rubyforge.org/projects/nadoka/'
66
+ @ch_kcode = @bot_config.fetch(:ch_kcode, :tojis)
67
+ @to_lang = @bot_config.fetch(:to_lang, 'ja')
68
+ @to_lang2 = @bot_config.fetch(:to_lang2, 'en')
69
+ @bing_appid = @bot_config.fetch(:bing_appid, nil)
70
+ end
71
+
72
+ def on_privmsg prefix, ch, msg
73
+ if @available_channel === ch and /^tr/ =~ msg
74
+ if response = dispatch_command(msg)
75
+ response.each{|r|
76
+ send_notice(ch, r) if r
77
+ }
78
+ end
79
+ end
80
+ end
81
+
82
+ SHORT_LANG = {'e' => 'en', 'j' => 'ja'}
83
+
84
+ def dispatch_command msg
85
+ begin
86
+ case msg
87
+ when /^trans(?:late)?(:(.*?))?>\s*(.+)/o
88
+ translate($3, *parse_trans_option($2, $3))
89
+ when /^trans([ej])>\s*(.+)/o
90
+ translate($2, nil, SHORT_LANG[$1])
91
+ when /^trans(?:late)?r(:(.*?))?>\s*(.+)/o
92
+ translate_r($3, *parse_trans_option($2, $3))
93
+ when /^translang>(.+)/
94
+ lang = $1.strip
95
+ desc = 'Unknown. See http://code.google.com/intl/ja/apis/ajaxlanguage/documentation/reference.html#LangNameArray'
96
+ r = LANGUAGE_MAP_S2L.fetch(lang.downcase,
97
+ LANGUAGE_MAP.fetch(lang.upcase, desc))
98
+ "translang> #{lang} = #{r}"
99
+ end
100
+ rescue Exception => e
101
+ "translate bot: #{e.message}"
102
+ end
103
+ end
104
+
105
+ def detect_lang str
106
+ uri = "http://ajax.googleapis.com/ajax/services/language/detect?v=1.0&q="
107
+ uri << CGI.escape(str.toutf8)
108
+
109
+ result = open(uri, "Referer" => @referer) do |f|
110
+ JSON.parse(f.read)
111
+ end
112
+
113
+ if result["responseData"]
114
+ result["responseData"]["language"]
115
+ end
116
+ end
117
+
118
+ def parse_trans_option opt, str
119
+ case opt
120
+ when nil
121
+ from_lang = detect_lang(str)
122
+ to_lang = @to_lang
123
+ if to_lang == from_lang
124
+ to_lang = @to_lang2
125
+ end
126
+ [from_lang, to_lang]
127
+ when /\A([\w\-]+)[, \>]([\w\-]+)\z/
128
+ [$1, $2]
129
+ when /\A([\w\-]+)\z/
130
+ [nil, $1]
131
+ else
132
+ raise "can't parse translation option: #{opt}"
133
+ end
134
+ end
135
+
136
+ def translate phrase, from_lang, to_lang
137
+ r = []
138
+
139
+ gr = translate_ggl(phrase, from_lang, to_lang)
140
+ r << "g:translate bot (#{gr[1]}): #{gr[0]}"
141
+
142
+ if @bing_appid
143
+ mr = translate_ms(phrase, from_lang, to_lang) if @bing_appid
144
+ r << "m:translate bot (#{mr[1]}): #{mr[0]}"
145
+ end
146
+ r
147
+ end
148
+
149
+ def translate_r phrase, from_lang, to_lang
150
+ rs = []
151
+ %w(ggl ms).each{|system|
152
+ r = send("translate_#{system}", phrase, from_lang, to_lang)
153
+ from_lang = r[2]
154
+ first = r[0]
155
+ if from_lang
156
+ r = send("translate_#{system}", r[0], to_lang, from_lang)
157
+ rs << "#{system.split(//)[0]}:trans_r (#{from_lang}>#{to_lang}>#{from_lang}): #{r[0]} (#{first})"
158
+ end
159
+ }
160
+ rs
161
+ end
162
+
163
+
164
+ def translate_ggl(phrase, from_lang, to_lang)
165
+ uri = 'http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q='
166
+ uri << CGI.escape(phrase.toutf8)
167
+ uri << "&langpair=#{from_lang}%7C#{to_lang}"
168
+
169
+ result = open(uri, "Referer" => @referer) do |f|
170
+ JSON.parse(f.read)
171
+ end
172
+
173
+ if result["responseData"]
174
+ text = CGI.unescapeHTML(result["responseData"]["translatedText"])
175
+ text = text.send(@ch_kcode) if @ch_kcode
176
+ from_lang ||= result["responseData"]["detectedSourceLanguage"]
177
+ opts = "#{from_lang}>#{to_lang}"
178
+ [text, opts, from_lang]
179
+ else
180
+ opts = "#{from_lang ? "from #{from_lang} to " : ''}#{to_lang}"
181
+ ["#{result["responseDetails"]} (#{uri})", opts]
182
+ end
183
+ end
184
+
185
+ ## ms translate
186
+
187
+ def get_result_ms result
188
+ # puts result
189
+ doc = REXML::Document.new(result)
190
+ doc.elements.map{|e| e.text}[0]
191
+ end
192
+
193
+ def translate_ms phrase, from_lang, to_lang
194
+ api_url = 'http://api.microsofttranslator.com/V2/Http.svc/Translate'
195
+ uri = "#{api_url}?appId=#{@bing_appid}&text=#{CGI.escape(phrase.toutf8)}&to=#{CGI.escape(to_lang)}"
196
+ begin
197
+ text = get_result_ms open(uri, "Referer" => @referer).read
198
+ text = text.send(@ch_kcode) if @ch_kcode
199
+ opts = "#{from_lang}>#{to_lang}"
200
+ [text, opts, from_lang]
201
+ rescue OpenURI::HTTPError => e
202
+ opts = "#{from_lang ? "from #{from_lang} to " : ''}#{to_lang}"
203
+ ["#{e.message} (uri)", opts]
204
+ end
205
+ end
206
+
207
+ # copy from http://code.google.com/intl/ja/apis/ajaxlanguage/documentation/reference.html
208
+ LANGUAGE_MAP = {
209
+ 'AFRIKAANS' => 'af',
210
+ 'ALBANIAN' => 'sq',
211
+ 'AMHARIC' => 'am',
212
+ 'ARABIC' => 'ar',
213
+ 'ARMENIAN' => 'hy',
214
+ 'AZERBAIJANI' => 'az',
215
+ 'BASQUE' => 'eu',
216
+ 'BELARUSIAN' => 'be',
217
+ 'BENGALI' => 'bn',
218
+ 'BIHARI' => 'bh',
219
+ 'BULGARIAN' => 'bg',
220
+ 'BURMESE' => 'my',
221
+ 'CATALAN' => 'ca',
222
+ 'CHEROKEE' => 'chr',
223
+ 'CHINESE' => 'zh',
224
+ 'CHINESE_SIMPLIFIED' => 'zh-CN',
225
+ 'CHINESE_TRADITIONAL' => 'zh-TW',
226
+ 'CROATIAN' => 'hr',
227
+ 'CZECH' => 'cs',
228
+ 'DANISH' => 'da',
229
+ 'DHIVEHI' => 'dv',
230
+ 'DUTCH'=> 'nl',
231
+ 'ENGLISH' => 'en',
232
+ 'ESPERANTO' => 'eo',
233
+ 'ESTONIAN' => 'et',
234
+ 'FILIPINO' => 'tl',
235
+ 'FINNISH' => 'fi',
236
+ 'FRENCH' => 'fr',
237
+ 'GALICIAN' => 'gl',
238
+ 'GEORGIAN' => 'ka',
239
+ 'GERMAN' => 'de',
240
+ 'GREEK' => 'el',
241
+ 'GUARANI' => 'gn',
242
+ 'GUJARATI' => 'gu',
243
+ 'HEBREW' => 'iw',
244
+ 'HINDI' => 'hi',
245
+ 'HUNGARIAN' => 'hu',
246
+ 'ICELANDIC' => 'is',
247
+ 'INDONESIAN' => 'id',
248
+ 'INUKTITUT' => 'iu',
249
+ 'ITALIAN' => 'it',
250
+ 'JAPANESE' => 'ja',
251
+ 'KANNADA' => 'kn',
252
+ 'KAZAKH' => 'kk',
253
+ 'KHMER' => 'km',
254
+ 'KOREAN' => 'ko',
255
+ 'KURDISH'=> 'ku',
256
+ 'KYRGYZ'=> 'ky',
257
+ 'LAOTHIAN'=> 'lo',
258
+ 'LATVIAN' => 'lv',
259
+ 'LITHUANIAN' => 'lt',
260
+ 'MACEDONIAN' => 'mk',
261
+ 'MALAY' => 'ms',
262
+ 'MALAYALAM' => 'ml',
263
+ 'MALTESE' => 'mt',
264
+ 'MARATHI' => 'mr',
265
+ 'MONGOLIAN' => 'mn',
266
+ 'NEPALI' => 'ne',
267
+ 'NORWEGIAN' => 'no',
268
+ 'ORIYA' => 'or',
269
+ 'PASHTO' => 'ps',
270
+ 'PERSIAN' => 'fa',
271
+ 'POLISH' => 'pl',
272
+ 'PORTUGUESE' => 'pt-PT',
273
+ 'PUNJABI' => 'pa',
274
+ 'ROMANIAN' => 'ro',
275
+ 'RUSSIAN' => 'ru',
276
+ 'SANSKRIT' => 'sa',
277
+ 'SERBIAN' => 'sr',
278
+ 'SINDHI' => 'sd',
279
+ 'SINHALESE' => 'si',
280
+ 'SLOVAK' => 'sk',
281
+ 'SLOVENIAN' => 'sl',
282
+ 'SPANISH' => 'es',
283
+ 'SWAHILI' => 'sw',
284
+ 'SWEDISH' => 'sv',
285
+ 'TAJIK' => 'tg',
286
+ 'TAMIL' => 'ta',
287
+ 'TAGALOG' => 'tl',
288
+ 'TELUGU' => 'te',
289
+ 'THAI' => 'th',
290
+ 'TIBETAN' => 'bo',
291
+ 'TURKISH' => 'tr',
292
+ 'UKRAINIAN' => 'uk',
293
+ 'URDU' => 'ur',
294
+ 'UZBEK' => 'uz',
295
+ 'UIGHUR' => 'ug',
296
+ 'VIETNAMESE' => 'vi',
297
+ 'UNKNOWN' => ''
298
+ }
299
+
300
+ LANGUAGE_MAP_S2L = LANGUAGE_MAP.invert
301
+ end