Eggdrop и национальные символы.

Введение.

Далеко не новость то, что популярный бот eggdrop (http://eggheads.org) имеет проблемы при работе с нелатинскими символами. Достаточно посмотреть множество сообщений форума (http://forum.egghelp.org) по поводу проблем с символами национальных алфавитов. Также были опрошены пользователи eggdrop, чей алфавит содержит символы не входящие в латинцу, имеют ли они какие либо проблемы с ботом при использовании национальных символов. Большая часть опрошенных, которые в основном используют латинский алфавит и дополнительно буквы латинского алфавита с диакритическими знаками (например алфавит чешского языка содержит помимо латиницы ещё буквы 'ACDEEINORSTUUYZacdeeinorstuuyz'), ответили мне что особенных проблем они не испытывают, просто потому что легко могут заменить буквы с диакритическими знаками на такие же без них. Так, один французско-говорящий собеседник объяснил что 'garcon' (мальчик, фр.) может быть легко записано как 'garcon' и такая замена вполне допустима. С другой стороны, большинство тех, чей алфавит значительно отличается от латинского (например кириллический или греческий) ответили утвердительно на этот вопрос. Те или иные проблемы при использовании национальных символов у них возникают, например проблема с регистрозависимостью 'bind' или вывод 'крякозябликов' вместо ожидаемых символов.

Что же, оставим тех кто могут легко заменять символы национальных алфавитов на чистую латиницу и перейдём к нам, простым смертным, которые любят и уважают свой родной язык и хотят научить бота любить родину.

В скобочках, с указанием версии, будет помечаться то, что уже перенесено или исправлено в официальных исходниках бота.

1. Патч Suzi project

Данный патч предназначен для устранения ВСЕХ нижеописанных проблем с национальными символами (это те, которые не входят в ANSI и имеют коды больше 127). Например это актуально для кириллического алфавита, греческого и других. Сразу хотелось бы отметить, что патч не является чем то революционным и тем более мы не претендуем на оригинальность подхода для коррекции проблемы (хотя именно патчей, готовых, не встречалось). Одна из проблем, которую исправляет патч -- "съедание" символов с кодом 255 в сеансе dcc или telnet (для cp1251 это 'я', для koi8-r это 'Ъ', для iso8859-1 это буква 'y'). Этот патч, господина с ником vd включен в патч без изменений. (1.6.20+)

Идея конвертирования всех входящих/исходящих строк в/из utf-8, как это требует API TCL озвучивалась на форумах посвящённых eggdrop неоднократно, но конкретной реализации кажется не было. Что, впрочем, вполне обосновано -- это повлекло бы за собой множество несовместимостей и возможных ошибок и не годилось для быстрого решения проблемы. Этот патч представляет собой конкретную реализацию коррекции вызовов TCL API внутри eggdrop, где в качестве параметра(ов) или возвращаемого значения используется строка, выполняя преобразования в формат требуемый TCL API. То есть является кардинальным изменением структуры вызовов (хотя реально, в патче на данный момент используются подстановки).

2. Проблемы присущие eggdrop при использовании национальных символов.

Все описанные ниже проблемы так или иначе проявляются когда используются национальные символы:

  • проблема с вводом/выводом/обработкой строк при использовании encoding system отличной от iso8859-1 (да и с ней тоже проблем много, одним словом проблемы есть)
  • проблема с регистрозависимостью и несрабатыванием bind'ов содержащих нелатинские символы.
  • проблема с установкой ника бота с нелатинскими символмами.
  • проблема со "съеданием" символа с кодом 255 в party-line или сеансе DCC chat. (1.6.20+)
  • проблема с DCC командами '.chhandle' и '.handle' и их TCL аналогами. (1.6.20+)
  • проблема с определением кодировки локали. (1.6.20+)

Некоторые из этих проблем связаны между собой и имеют одни корни, так устраняя одну из них устраняется и другая. Но они намеренно разделены и упорядочены по важности.

3. Причины проблем и их проявления

Что же, я думаю Вы не верите мне на слово, давайте проверять. Итак, у нас есть скомпилированный и инсталированный оригинальный бот (без каких бы то ни было патчей) в директории '~/eggdrop'. Я использую eggdrop1.6.18 и TCL8.4.13. Бот минимально сконфигурирован и подключен к irc серверу.

3.1. Проблема с вводом/выводом/обработкой строк при использовании encoding system отличной от iso8859-1.

Эта проблема имеет давнюю историю, ей посвящено множество тем форума, отдельных статей и вопросов. Но универсального решения до сих пор, кажется, не было. Причина этой проблемы проста и банальна. В своё время разработчики TCL перевели внутренности интерпретатора на unicode, а функции API принимающие/возвращающие в качестве параметров строки требуют чтобы эти строки были utf-8. Но увы и ах, eggdrop передаёт/получает строки TCL интерпретатора как последовательность байт (byte array в терминах TCL), а не как строку.

Напишем небольшой тестовый скрипт 'scripts/test.cp1251.tcl' (кодировка файла однобайтная cp1251):

 bind pub -|- {!test} test_handler
bind pub -|- {!тест} test_handler
proc test_handler { unick uhost uhandle uchannel utext } {
set text [string toupper "$unick on $uchannel, you said (ты сказал): $utext ([encoding system])"]
putserv "PRIVMSG $channel :$text"
}

Незатейливо проверяем в локали с кодировкой iso8859-1:

 < lynxy> !test SoMe TeXt нЕкИй ТеКсТ
 <eggbot> LYNXY ON #EGGTEST, YOU SAID (ТЫ СКАЗАЛ): SOME TEXT нЕкИй ТеКсТ (ISO8859-1)
 < lynxy> !tEsT SoMe TeXt нЕкИй ТеКсТ
 <eggbot> LYNXY ON #EGGTEST, YOU SAID (ТЫ СКАЗАЛ): SOME TEXT нЕкИй ТеКсТ (ISO8859-1)
 < lynxy> !тест SoMe TeXt нЕкИй ТеКсТ
 < lynxy> !тЕсТ SoMe TeXt нЕкИй ТеКсТ

Видим сразу две проблемы: во-первых не сработал 'bind' содержащий национальные символы, во-вторых 'string toupper' отработал только на английских символах и на национальных, которые были в исходном тексте, но те котрые были введены в качестве параметра обработчику не были конвертированы в верхний регистр.

Почему не сработал 'string toupper'? Причина этому проста, для того чтобы работала правильно эта функция, строка должна быть верно сконвертированна из однобайтной кодировки, в которой она приходит с сервера во внутреннее unicode представление TCL. Пользователи моего irc сервера используют кодировку cp1251. Что же, установим encoding system cp1251 (например командой '.tcl encoding system cp1251' или принудительно в конфигурационном файле устанавливаем 'encoding system cp1251' и делаем '.rehash' или, что правильней всего, запускаем бота в нужной локали LANG=ru_RU.cp1251 ./eggdrop).

Проверяем, что у нас получилось:

 < lynxy> !test SoMe TeXt нЕкИй ТеКсТ
 <eggbot> LYNXY ON #EGGTEST, YOU SAID ("+ !ZPWP): SOME TEXT нЕкИй ТеКсТ (CP1251)
 < lynxy> !tEsT SoMe TeXt нЕкИй ТеКсТ
 <eggbot> LYNXY ON #EGGTEST, YOU SAID ("+ !ZPWP): SOME TEXT нЕкИй ТеКсТ (CP1251)
 < lynxy> !тест SoMe TeXt нЕкИй ТеКсТ
 < lynxy> !тЕсТ SoMe TeXt нЕкИй ТеКсТ

Стало немного хуже, по-прежнему не работает 'bind' и 'string toupper', а с национальными символами содержащимися в скрипте произошло что то непонятное. Попробуем разобраться что же произошло. Возьмём две строки 'ТЫ СКАЗАЛ' и '"+ !ZPWP' и напишем коды символов в unicode:

 +---+------+---+------+
| 1 | uni | 2 | uni |
+---+------+---+------+
| Т | 0422 | | 0022 |
| Ы | 042B | + | 002B |
| | 0415 | | 0015 |
| С | 0421 | ! | 0021 |
| . | .... | . | .... |
+---+------+---+------+

Смотрим в party-line '.binds'

 .binds
 [13:26] tcl: builtin dcc call: *dcc:binds lynxy 7
 [13:26] #lynxy# binds
 Command bindings:
 TYPE FLGS COMMAND HITS BINDING (TCL)
 evnt -|- init-server 1 evnt:init_server
 pub -|- !B5AB 0 test_handler
 pub -|- !test 2 test_handler

Стало немного понятней, он просто отрезает верхний байт. И понятно почему не работает 'bind', в функцию попадают символы в "кастрированном" unicode. Более того, чтобы правильно работал регистронезависимый 'bind' необходимо запускать бота в соответствующей локали, потому что для сравнения используются функции библиотеки C 'strcasecmp' и подобные, чьё поведение зависит от локали.

Ну раз он такой плохой попробуем избежать этого. Модифицируем скрипт и будем передавать функциям написанным на C строки предварительно сконвертированные в однобайтную кодировку ('encoding converto'), а при получении строк будем приводить их к TCL unicode ('encoding convertfrom'):

 bind pub -|- {!test} test_handler
 bind pub -|- [encoding convertto {!тест}] test_handler
 proc test_handler { unick uhost uhandle uchannel utext } {
  set nick [encoding convertfrom $unick]
  set channel [encoding convertfrom $uchannel]
  set text [encoding convertfrom $utext]
  set str [string toupper "$nick on $channel, you said (ты сказал): $text ([encoding system])"]
  putserv [encoding convertto "PRIVMSG $channel :$text"]
 }

И запускаем бота в правильной локали:

 user@sys:~/eggdrop> LANG=ru_RU.cp1251 ./eggdrop

Проверяем:

 < lynxy> !test SoMe TeXt нЕкИй ТеКсТ
 <eggbot> LYNXY ON #EGGTEST, YOU SAID (ТЫ СКАЗАЛ): SOME TEXT НЕКИЙ ТЕКСТ (CP1251)
 < lynxy> !tEsT SoMe TeXt нЕкИй ТеКсТ
 <eggbot> LYNXY ON #EGGTEST, YOU SAID (ТЫ СКАЗАЛ): SOME TEXT НЕКИЙ ТЕКСТ (CP1251)
 < lynxy> !тест SoMe TeXt нЕкИй ТеКсТ
 <eggbot> LYNXY ON #EGGTEST, YOU SAID (ТЫ СКАЗАЛ): SOME TEXT НЕКИЙ ТЕКСТ (CP1251)
 <lynxy> !тЕсТ SoMe TeXt нЕкИй ТеКсТ
 <eggbot> LYNXY ON #EGGTEST, YOU SAID (ТЫ СКАЗАЛ): SOME TEXT НЕКИЙ ТЕКСТ (CP1251)

Ухты! Кажется что работает! Сработал и 'string toupper' и 'bind', в том числе независимо от регистра. Но поводов для веселья в данном случае не так и много. Во-первых это просто утомительно, каждый раз писать переконвертацию для каждой строки которая возможно содержит национальные символы. Конечно, можно написать некие обёртки для часто используемых функций, например:

 proc i18nbind {type flags name handler} {
  bind $type $flag [encoding convertto $name] $handler
 }

Однако это не решит ВСЕХ проблем (например party-line так не вылечить). Я предпочитаю изменить исходный текст eggdrop для достижения требуемого эффекта, чтобы можно было безболезненно использовать национальные символы избегая постоянной конверсии. Проблема установлена и её можно достаточно просто устранить если сделать эти преобразования незаметными и реализовать их на уровне языка C.

Я думаю что разработчики eggdrop намеренно не сделали автоматическую конвертацию С-строк в кодировке локали в/из UTF-8, поскольку это можно сделать внутри TCL интерпретатора. Но, на мой взгляд, такой способ передачи строк в интерпретатор почти бесполезен, потому что в 99% случаев требуется именно строка, а не последовательность байт и приходится её конвертировать, загромождая код лишними преобразованиями. Патч изменяет это поведение и в интерпретатор строки передаются уже правильно сформированными в utf-8 (и вы всегда можете конвертировать их обратно в последовательность байт посредством encoding convertto, но мне ещё ни разу не встречалось случая когда это было необходимо). Проблема с установкой боту ника содержащего национальные символы имеет тот же источник и решается подобным образом.

3.2. Проблема со "съеданием" символа с кодом 255 в party-line или сеансе DCC chat. (1.6.20+)

!!! ВНИМАНИЕ! Здесь использованы материалы vd и я никоим образом не претендую на авторство !!!

К боту могут подключаться два вида клиентов:

  1. IRC клиенты по протоколу DCC CHAT
  2. Telnet клиенты по протоколу telnet

И хотя эти протоколы похожи между собой, но telnet протокол поддерживает так же управляющие последовательности, которые впрочем обрабатываются ботом в режиме мягкой задумчивости (а попросту говоря он их вырезает). Но IRC клиенты в сеансе DCC CHAT не используют управляющие последовательности и ничего про них не знают, а бот, поскольку не умеет отличать одних клиентов от других со спокойной совестью вырезает национальные символы с кодом 255. Например для кодировки cp1251 это буква 'я':

 .say #eggtest меня зовут Настя
 [14:20] tcl: builtin dcc call: *dcc:say lynxy 7 #eggtest мен зовут Наст
 [14:20] #lynxy# (#eggtest) say мен зовут Наст
 Said to #eggtest: мен зовут Наст

Даже для разлюбезной кодировки iso8859-1 которую к делу и не к делу рекомендуют устанавливать всеразличные "умельцы" это буква 'y' (впрочем я не знаю насколько часто она используется в других языках, но факт налицо):

 .say #eggtest say yo-yo
 [14:24] tcl: builtin dcc call: *dcc:say lynxy 7 #eggtest sa o-o
 [14:24] #lynxy# (#eggtest) say sa o-o
 Said to #eggtest: sa o-o

Эта проблема исправляется патчем vd, который учит eggdrop отличать клиентов. Отличаются они при авторизации, при подключении к боту Telnet клиентом, в частности на этапе ввод логина и пароля, telnet клиент выдает себя тем что он посылает управляющие коды вначале текста что используется для его идентификации и, следовательно, управляющие последовательности вырезаются. IRC клиенты управляющих последовательностей при соединении не посылают и в дальнейшем работа с таким клиентом ведётся без вырезания управляющих последовательностей.

3.3. Проблема с DCC командами '.chhandle' и '.handle' и их TCL аналогами. (1.6.20+)

Ошибки в DCC командах chhandle, handle (и их аналогах для TCL скриптов) приводят к замене национальных символов на '?' (знак вопроса), хотя при этом добавить хендл с национальными символами можно. Конечно, это не самое лучшее решение использовать хендлы содержащие национальные символы, но ведь если можно добавить то почему нельзя переименовать?!

 .+user LamoЮзер
 [14:37] tcl: builtin dcc call: *dcc:+user lynxy 7 LamoЮзер
 [14:37] #lynxy# +user LamoЮзер
 Added LamoЮзер (no host) with no password and no flags.
 .chhandle LamoЮзер ЛамоЮзер
 [14:37] tcl: builtin dcc call: *dcc:chhandle lynxy 7 LamoЮзер ЛамоЮзер
 [14:37] Switched 0 notes from LamoЮзер to ????????.
 [14:37] #lynxy# chhandle LamoЮзер ????????
 Changed.

Проблема заключена в процедурах 'cmd_handle', 'cmd_chhandle' и 'tcl_chhandle' которые считают все символы с кодом больше 127 плохими и заменяет их на '?' (знак вопроса), при этом другие процедуры так не считают.

3.4. Проблема с определением кодировки локали. (1.6.20+)

Хотя эта проблема незначительная, я всё таки опишу её.

Создадим небольшой тестовый файл 'eggenc.conf':

 user@sys:~/eggdrop> echo -e "putlog \"eggdrop encoding: [encoding system]\"; die;" > eggenc.conf

Проверяем правильность определения кодировки локали:

 user@sys:~/eggdrop> echo $LANG
 ru_RU.cp1251
 user@sys:~/eggdrop> ./eggdrop eggenc.conf
 Eggdrop v1.6.18 (C) 1997 Robey Pointer (C) 2006 Eggheads
 [10:48] --- Loading eggdrop v1.6.18 (Sat Jul 22 2006)
 [10:48] eggdrop encoding: cp1251
 [10:48] * EXIT

Выглядит хорошо, а теперь другой тест запускаем бота в локали ru_RU.koi8-r:

 user@sys:~/eggdrop> LANG=ru_RU.koi8-r ./eggdrop eggenc.conf
 Eggdrop v1.6.18 (C) 1997 Robey Pointer (C) 2006 Eggheads
 [10:57] --- Loading eggdrop v1.6.18 (Sat Jul 22 2006)
 [10:57] eggdrop encoding: iso8859-1
 [10:57] * EXIT

А вот и не угадал!

Ещё пример:

 user@sys:~/eggdrop> LANG=ko_KR.euckr ./eggdrop eggenc.conf
 Eggdrop v1.6.18 (C) 1997 Robey Pointer (C) 2006 Eggheads
 [10:58] --- Loading eggdrop v1.6.18 (Sat Jul 22 2006)
 [10:58] eggdrop encoding: iso8859-1
 [10:58] * EXIT

Опять мимо.

Возможное решение проблемы состоит в принудительном указании кодировки в начале конфигурационного файла, например так:

 encoding system koi8-r

или вот так:

encoding system euc-kr

Одако, если запустить интерпретатор 'tclsh' в тех же самых локалях он верно определяет кодировки:

 user@sys:~/eggdrop> LANG=ru_RU.koi8r tclsh
 % encoding system; exit
 koi8-r
 user@sys:~/eggdrop> LANG=ko_KR.euckr tclsh
 % encoding system; exit
 euc-kr

Спрашивать где проблема -- бессмысленно, она в коде eggdrop. А точнее в файле 'src/tcl.c' строки 597-682, с незапамятных времён содержится код, который пытается определить кодировку и установить её для интерпретатора. Если верить коментариям к этому участку кода он взят из исходников TCL. Но видимо это было так давно, что сейчас он просто работает неверно. Более того, в этом коде нет необходимости, потому что библиотека TCL во время инициализации сама делает определение кодировки локали. После удаления этого странного кода бот стал уверенно определять ВСЕ кодировки которые есть на моей системе.

4. Windows и Eggdrop

Eggdrop собранный в среде Cygwin (более известный как Windrop) имеет все вышеописанные проблемы (которые так же лечатся патчем) плюс ещё одну.

Насколько мне известно, Cygwin на данный момент не поддерживает никаких кодировок локалей кроме ASCII и UTF-8, но в нашем случае они почти бесполезны. И регистронезависимые бинды содержащие национальные символы не будут работать регистронезависимо. Поэтому мною был сделан отдельный патч, который заставляет windrop использовать функции сравнения строк Windows API вместо аналогичных Cygwin'a. Использовать или нет этот дополнительный патч -- решать Вам.

---

Дата: 28-Июль-2006

Автор: Анастасия Зёмина a.k.a. lynxy (WeNet irc.wenet.ru:6667, #eggtest), Suzi project team

Благодарности: ZemIn, Buster, Yxaaaaaaa.

Рейтинг:   / 4
ПлохоОтлично