nadoka 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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