UndernetPHPclub Make your website dynamic
> > > > > > >

Программирование - EggDrop боты, IRC и TCL.



Разное

Как отлаживать куски программы в отдельном месте, не мешая боту?

    Удобно отлаживать кусочки текста тем, кто плохо изучил TCL следующим образом. Поместите куда угодно функцию и к ней бинд:
    bind dcc n test testproc
    proc testproc {hand idx arg} {
    # (хотя объявлять заранее не нужно, lappend сама создаст)
       set a ""
    # записываем 3 элемента в список
       lappend a AAAAA
       lappend a BBBBB
       lappend a CCCCC
    # выводим разультат на экран
       putlog "**test**: $a"
    # удаляем первый элемент
       set a [lrange $a 1 end]
    # выводим результат на экран
       putlog "**test**: $a"
    }
    
    Далее зайдите в DCC-чат как владелец бота и выполните команду ".test". Вы сразу увидите ее результат.

    Если на вашем не запрещена команда .tcl (рекомендуется включить для отладок), то можно обойтись без bind .. команда: в чате бота пишите любую TCL команду .tcl testproc [аргументы] или целый скрипт .tcl команда1; команда2; ...
Иногда бывают ТАКИЕ глюки, что руки опускаются...
    Да, к сожалению, TCL иногда не пишет об ошибках и не выполняет программу. Пример:
    set asd 1
    after 1000 testtest
    proc testtest {} {
       putlog "test 1: ...."
       # с этого места нихера работать не будет и в лог (чат бота) ошибки не идут 
       putlog "test 2: $asd"
    }
    
    Глюк проявляется при after + необъявленная переменная. Если объявить переменную гобальной, либо вызавать testtest напрямую (без after), то TCL пишет ошибку.
Важный совет при установке/проверке флагов юзеру
    matchattr $hand X $chan -- не правильно
    matchattr $hand |X $chan -- правильно
    matchattr $hand -|X $chan -- правильно
    Если сделать неправильно, проверяться будут глоабальные флаги, а не флаги канала. Тоже самое с chattr - не забывайте писать вертикальную черту
RAW: получение ВСЕГО текста от сервера. В частности, как узнать о коннекте/дисконнекте к серверу, т.к. стандарные средсва бота этого не позволяют.
    Все сообщения, что шлет сервер, имеют свой номер. Например, в ответ на /whois сервер шлет 4 или 5 raw-сообщений с номерами от 311 до 318. В случее ошибочной команды - 421. Когда сервер шлет информацию, что кто-то сменил топик или дал кому-то опа, сервер тоже на самом деле шлет raw-сообщение, только такие сообщения не нужно отлавливать, они хорошо документированы и принимаются по команде типа "bind mode..". Чтобы самостоятельно узнать, какому номеру соответствует нужное вам действие, постройте bind для всевозможных номеров (на каждый номер сообщения нужен 1 bind). Функция выглядит так:


    bind raw - 1 a bind raw - 2 a bind raw - 3 a ......... ......... proc a { from key msg } { putlog "RAW: $from | $key | $msg" }

    Чтобы сгенерировать такой список (не руками, конечно), напишите пару строк на любом языке, хоть на TCL. Мне быстрее это было сделать на PHP:


    < ? for ($i=0; $i<500; $i++) { echo "bind raw - $i a\n"; } ? > proc a { from key msg } { putlog "RAW: $from | $key | $msg" }

    Готовый код тут (raw.tcl). К запущенному боту подключать как .tcl source путь/raw.tcl

    Вставьте это в бота, сделайте .rehash и все неописанные ботом сообщения будут попадать в лог, что можно наблюдать в чате. Пишем любую команду, наример: .tcl putserv "WHOIS dima" (у вас .tcl [любой-TCL-код] скорее всего не работает, т.к. закоментирована в eggdrop.conf по соображению безопасности - раскомментируйте ее на время экспериментов). В ответ вы увидите результат и все дальше сами поймете.

    Краткая справка: При коннекте к серверу: 251 | TAHK :There are 1 users and 9 invisible on 1 servers 254 | TAHK 2 :channels formed 255 | TAHK :I have 10 clients and 0 servers WHOIS TAHK 311 | TAHK TAHK ~TAHK 212.193.6.215 * : !>>> HELP HTTP://BOT.NET.RU <<<! 319 | TAHK TAHK :@#phpclub #russiancyrillic 312 | TAHK TAHK *.undernet.org :The Undernet Underworld 317 | TAHK TAHK 325 993241829 :seconds idle, signon time 318 | TAHK TAHK :End of /WHOIS list. При входе в канал (это ловить не требуется): 332 | TAHK #phpclub :p 333 | TAHK #phpclub TAHK 993240617 353 | TAHK = #phpclub :TAHK @TAHK8 TAHK7 TAHK2 TAHK4 TAHK6 MAPC TAHK3 TAHK5 @DlMA 366 | TAHK #phpclub :End of /NAMES list. 368 | TAHK #phpclub :End of Channel Ban List 324 | TAHK #phpclub +n 329 | TAHK #phpclub 993223742 Хотя кое-что тут интересно: 329 - это код "время создания канала" т.е. 993223742 - unix-время создания канала 354 | TAHK #phpclub ~TAHK 212.193.6.215 TAHK H 354 | TAHK #phpclub ~TAHK8 ns.ipo.spb.ru TAHK8 H@ .....(полный список людей).... 354 | TAHK #phpclub xxx ip183.ipo.spb.ru DlMA H@ 315 | TAHK #phpclub :End of /WHO list. 331 | TAHK #russiancyrillic :No topic is set. 332 | TAHK #phpclub :p 333 | TAHK #phpclub TAHK 993240617 Расшифровка кодов: H@ простой чел с опом на канале H+ простой чел с "голосом" на канала H простой чел без опа на канала H*@ ИРЦоп с опом на канале H* ИРЦоп без опа H*+ ИРЦоп без опа на канале, но с "голосом" =) ("голос" - это когда "+" перед ником) И еще: если после этих кодов стоит "d", то юзер в режиме "d", это игнор всего текста из каналов (подробности - http://bot.net.ru/irc.php) Nifiga (неправильная команда - чтобы вести лог своих ошибок): 421 | TAHK Nifiga :Unknown command


Будьте осторожны со временем [unixtime]

    В ботах, во премя выполненения какого-то события время неподвижно, т.е. функция [unixtime] все время возвращает одно и тоже число. Проверить это просто:


    .tcl putlog "1: [unixtime]"; benchmark 100000 { append s "s" } ; putlog "2: [unixtime]";

    В примере функция benchmark 100'000 раз выполняет тестовый скрипт. Длиться это около 10 секунд, а [unixtime] не меняется. Выход простой - используйте в таких особых случаях [clock second] Она вернет тоже, что и [unixtime], т.е. компьютерное время, но зато она не блокируется на время обработки событий в боте.

    Кстати, если вам не лень писать 2 слова, вместо одного, программа будет работать быстрее, вот тесты:

     .tcl benchmark 10000 { set a [unixtime] }
     All time: 0.839 sec. One cycle time: 0.000083 sec (Check time ~ 1 sec)
     .tcl benchmark 10000 { set a [clock seconds] }
     All time: 0.614 sec. One cycle time: 0.000061 sec (Check time ~ 0 sec)
    
    Как видно, [clock seconds] на треть быстрее.


Где *вечно* хранить кучу разных переменных + как зафиксировать RESTART/REHASH?

    С начала, о сути проблемы. Часто возникает необходимость хранить свои собственные переменные/массивы/списки так, чтобы они не стирались при перезапуске бота, rehash и restart.

    Решение проблемы rehash. Допусим, нам надо иметь глобальную переменную $x, которая не должна пропадать при .rehash и в начале (при старте бота) там долно быть наше нужное значение, к примеру строка "привет". Для того, чтобы переменная не потерялась при .rehash, используйте такой код:


    # в глобальной области программы (не внутри какой-либо функции): if {![info exists x]} {set x "привет"}

    При .rehash бот перегружает все скрипты (уничтожив старые функции и добавив выполнив код повтороно, т.е. добавив новые), но не уничтожает объявленные глобальные переменные, активные таймеры (after ms script) и т.п. результат деятельности программы. А при .restart, как и при запуске бота, и память, и скрипты, и все остальное уничтожается. Т.е. если вы добавили новые скомпилированные модули, то они перезагрузятся. (Чтобы зафиксировать коннект к серверу - смотрите главу о RAW)

    Если подумать над сказанным выше (и куском кода), то можно сделать 2 простых вывода:

    • чтобы зафиксировать событие запуска бота или .restart, нужно использовать приведенный выше пример, а вместо "set x ..." вызывать процедуру bot_start, в которой поместить все то, что должен делать бот при старте. Например, туда можно поместить set x "привет" (не забыв написать global x в начале функции) и кучу других необходимых действий.
    • чтобы зафиксировать .rehash нужно сделать аналогично, только не проверять на наличие какой-либо переменной, а прямо в глобальной области любого .tcl скрипта вызвать функцию bot_rehash и в ней разместить все необходимые действия

    Итак, после этих знаний вернемся к вопросу, где длительно хранить свои переменные. Самое простое решение - это хранение в XTRA полях бота. Пример:


    setuser <hand> XTRA <название переменной> <значение переменной>

    Хранить это можно как у всех пользователей, так и у какого-то одного (либо персональюную информацию, либо общую). Т.е. для общей инфомарции можно завести мнимого юзера -my, и уже в его XTRA поля писать разные данные. Назвать данного пользователя надо с "-", чтобы сложнее было его испортить. Например, нужно поставить заглушку на вход пользователей с именем, начинающимся на "-", и не давать удалять их командой .-user.

    Все было бы хорошо, но такой способ имеет существенные ограничения - размер переменных не бесконечен, а ограничен примерно в 1000 байт. Поэтому вариант хранения в XTRA полях не подходит для больших данных, которых, увы, много.

    Решение ниже.


    # # инициализация массива MyVar для постоянного хранения данных # proc init_myvar {} { global myvar set name "myvar.txt" if {![file exists $name]} { catch { set g [open $name w+] puts $g "test test" close $g } } catch { set g [open $name r] while {![eof $g]} { set s [gets $g] set myvar([lindex $s 0]) [sminus $s 1] } close $g } if {![info exists myvar(test)]} { putlog "File $name (any variables) not read!" die "Can't read myvar file" } } if {![info exists init_myvar]} { set init_myvar 1 init_myvar } # # чтение из массива MyVar # proc gv {name} { global myvar if {[info exists myvar($name)]} {return $myvar($name)} return "" } # # запись в массив MyVar + сохранение на диск # proc sv {name value} { global myvar set myvar($name) $value if {$myvar($name)==""} {unset myvar($name)} set ok 0 catch { set g [open "myvar.txt" w+] foreach a [array names myvar] { if {[strlen $a]>0} { puts $g "$a $myvar($a)" } } close $g set ok 1 } if {$ok!=1} {die "Can't save myvar"} }

    Описание.

    Как видите, кусок текста

    if {![info exists init_myvar]} {
       set init_myvar 1
       init_myvar
    }
    
    проверяет, не произошел ли старт бота или .restat по команде, и если да - вызывает функцию загрузки сохраненный данных.

    Работает пример просто. Для записи/чтения чего-то в область постоянного хранения нужно испрользовать sv и gv (сокращение 'set variable' и 'get variable'):


    # сохранить данные sv <название переменной> <значение переменной> # пример записи переменной $mypass под именем 'pass' в память: sv pass $mypass # загрузить данные set <куда загрузить> [gv <название переменной>] # пример чтения из памяти переменной под названием 'pass' и запись # ее в переменную $mypass set mypass [gv pass]

    Программа хранит данные в памяти и записывает при изменении их на диск. Если ей неудается создать/прочитать файл для храниения myvar.txt, то бот сразу завершает работу, выводя сообщение на экран (если ошибка проиходит при старте) и в логи.

    Если записать в переменную пустую строку (""), то переменная из области памяти удалется. Если попробовать прочитать несуществующую переменную, вам вернут пустую строку.

    Вы можете хранить любые данные и под любыми именами, кроме:

    • не используйте пробел в названиях переменных
    • не используйте символ перехода на новую строку в самих данных

    Иначе вам придется переделать процедуру чтения/сохранения из/в файл.

Другие события
    Для процедуры хранения данных нам было необходимо ловить момент запуска бота, restart или rehash. Чтобы установить обработчик на само событие - момент, когда команда поступила, но бот еще не выполнил, читайте документацию на событие EVNT (чтобы повесить свою функцию на это событие, используют обычный bind): см. tcl-commands.rus.txt, раздел "Прочие функции".

    Там написано, как принимать события от команды kill -[тип] [pid], rehesh - до и после выполнения, переключение логов, коннект к сервеку и т.п.

Замена и регулярные выражения

    Иногда я забываю порядок перечисления аргументов в функциях регулярных выражений, думаю кто-то тоже, поэтому привожу несколько примеров. Для тех, кто писал на PHP/Perl и знаком с регулярными выражения и их различий в PHP & Perl, то в TCL нет диапазона повтора атомов. Т.е. можно использовать все традиционные функции PHP-ориентированных регулярных выражений, если верить документации. Хотя не проверял, сумеет ли TCL понять некоторые хитрые диапазоны символов, как это может PHP.

    По случаю сообщаю об ошибке в русскоязычной документации TCL. Это не описано в документации, но TCL понимает фигурные скобки, как наиболее гибкий множитель:

    { число1 [ , [ число2 ] ] }

    Примеры (выражения - выделены):
    a{4}   ==> равносильно aaaa    (повторить а ровно 4 раза)
    a{2,4} ==> равносильно aaa?a?  (повторить a от 2х до 4х включительно раз)
    a{0,1} ==> равносильно a?
    a{1,}  ==> равносильно a+      (повторить a от 1го до +бесконечности раз)
    a{0,}  ==> равносильно a*
    
    В примерах, а - это любой атом, множество, подстрока или что-то еще

    regsub -all -nocase {[^a-z0-9_()-]} $filename "_" filename
    Заменить в функции $filename все символы, кроме букв и цифр на символ "_". Ответ записать в переменную $filename (писать без $). При замене не учитывать регистр букв (-nocase) и заменить не только первый найденный кусок, попадающий под шаблон, но все остальные (-all).

    if {[regexp -nocase {([0-9][0-9]?)} $string - int]} {соответствует} {нет}
    Проверить на соответствие шаблону и вырезать что-нибудь в добавочные перенные. $string - входящая строка. В переменную "-" (минус после $string) поместят всю найденную строку, но это не нужные данные. После этого минуса не указывая $ перечислить переменные, соответствующие маркерам (круглым скобкам в шаблоне).

    if {[string match шаблон проверяемая_строка]} {соответствует} {нет}
    Еще в TCL есть простейший вид regexp - это проверка соответствия строки некому шиблону. В шаблоне можно использовать только *, ? и указывать диапазон букв с помощью [..]. Затем слеш позволяет превратить особые символы в обычные (и самого себя, если указать двойной слеш). Никаких более функций не поддерживается. Будьте осторожны, когда будете проверять соответствие какого-то хоста под шаблон бана, т.к. символы [ и \ в IRC понимаются буквально, а в string match - как начало диапазона и экранирование особого символа.




Переменные/функции

В TCL бывают обычные переменные, списки и массивы. Работа со списками вынесена в отдельную главу, а тут описаны действия над переменными и массивами. Информация данной главы следует из мануала TCL, но найти ее крайне трудно при переходе на TCL с какого-либо традиционного веб-языка, типа PHP =)

Объявление переменных перед первым использованием:
    Установить переменную
    set a hello
    # тоже самое:
    set a "hello"
    
    Использование особых символов $, [, ], (, ), {, }, ", \
    set a "hello \[ скобка \" кавычка \{ еще скобка \$ доллар \\ слеш"
    # тоже самое:
    set a  {hello [ скобка " кавычка \{ еще скобка $ доллар \ слеш}
    
    Установить список с перечнем элементов:
    set mylist {
       "hello"
       "world"
    }
    
    Установить элемент массива (это приводит к авто-созданию самого массива)
    set myarray(a) "hello"
    set myarray(b) "world"
    
    Сослаться на переменную, имя которой в другой переменной:
    set $name hello
    
    Если до выполнения этой команды в $name поместить "qqq", по после выполнения появится переменная $qqq, содержащая "hello". Чтобы проще понять, в боте выполните пример:
    .tcl set a b; set $a 1; putlog "\$a=$a, \$b=$b"
    вернет:
    $a=b, $b=1
    

Проверить существование переменой
    С обычными переменными и списками:
    if {[info exists название_переменной]} {существует} {не существует} (НЕЛЬЗЯ использовать $ в названии переменной )

    С массивами: if {[array exists название_массива]} {существует} {не существует} (НЕЛЬЗЯ использовать $ в названии переменной )

    C элементами массива (проверьте сперва, что массив есть, иначе будет ошибка): if {[info exists название_массива(элемент)]} {существует} {не существует} Ипользовать $ тут по прежнему нельзя, но если элемент у вас храниться в переменно, то и в проверки его надо писать с $. Пример (есть ли элемент $myvar(test)):
    info exists myvar(test)
    Пример (есть ли элемент $myvar($i), $i - переменная): info exists myvar($i)

Удалить переменную

    unset назвение_переменной или unset название_массива или unset название_массива(элемент) (использовать $ перед названием перенной нельзя). Предварительно проверяйте, существует ли переменная/массив/элемент массива, иначе будет ошибка.

Существование фунции

    info commands маска вернет список определенных в самом TCL, боте или скриптах функций. Если использовать маску *, то вам вернут все функции. Удобно проверять, когда вы используете специфические функции, а суещствуют ли они вообще. Пример:


    # check function md5 if {[info commands md5] != "md5" } { die "Function md5() not found. Can't load <***.tcl>" }


Список переменных

    Кроме проверки существования конкретных переменных можно с помощью info vars маска получить список всех локальных переменных + объявленных глоабальных. "Локальность" заключается в том, что команда info скорее всего будет выполнена не в глобальной области, а внутри какой-то функции. В качестве маски можно использовать "*", чтобы получить все переменные.

    Чтобы получить глобальные переменные - info globals маска




Работа со списками.

Здесь краткая справка по работе с массивами.

Что такое список?

    Это массив. Только в TCL их называют списками. Все функции для списков начинаются на букву 'L'. Кроме списков в TCL существуют и настоящие массивы, но для использования массивов начинающим программистам нужно начать работу со списков.

Как задать пустой список?

    set mylist ""
    Хоть и задали пустую строку, однако это правильно.

Как задать список из 2-х слов?

    set mylist "слово1 слово2"
    См. другие примеры в мане, функция list.

Как использовать разделитель, отличный от пробела?

    См. в мане split (команда разделяет строку на части и создает из них правильный Tcl-список): split mylist разделитель. Пример: split "bot.net.ru" .
    вернет список из 3х слов (обратите внимание на крайнюю точку справа в строке с примером).

    Еще есть join - склеить список в строку, разделяя элементы нужным разделитем (по умолчанию - проблел): join mylist разделитель

Как добавить элемент к списку?

    См. в мане lappend - добавить в конец. Пример: lappend mylist новый_элемент (функция действует над $mylist, а не возвращает результат). См. linsert - для вставки в любое место списка любого числа новых элементов: linsert список позиция элемент1 элемент2 ...

Как узнать кол-во элементов в списке?

    llength mylist

Как удалить все эленты с первого по N?

    Такая операция часто нужна, например в команде !topic #канал фраза нужно вырезать навание канала ([lindex $arg 0]) и затем сделать переменную topic без первого слова. Для этого не удалим, а возьмем все с N-ного элемента до конца. Для этого есть lrange mylist начало конец (конец - не включительно). Таким образом: set mylist [lrange $mylist N end] где end - спец. слово, указывает на последний элемент. Чтобы удалить все, кроме первого элемента, в качестве N подставьте 1. Еще для удаления или замены можо использовать lreplace, см. ман.

    Но это еще не все. Попробуйте в качестве $mylist поставить строку, содержащую символы [ или \ и вы увидите глюки. Чтобы правильно воспользоваться командой, выполните ее так: set mylist [join [lrange [split $mylist] N end]]
Как обратится к какому либо элементу списка?

    lindex mylist позиция (с нуля) - вернет нужный элемент
Как вывести на экран (в чат бота) список?

    putlog "$mylist"
c Undernet is a registred trademark of Undernet IRC Network
c 2001 UndernetPHPClub. All rights reserved.
  UndernetPHPclub