changeset 1037:f7ef8003fc35

Merge 0.8.3 stable branch
author Mikael Berthe <mikael@lilotux.net>
date Sun, 19 Nov 2006 11:51:14 +0100
parents 94d9a3cbb211 (diff) 3e3137ebc57c (current diff)
children f9e8fd9cb58b
files mcabber/ChangeLog mcabber/configure.ac mcabber/doc/help/Makefile.am mcabber/doc/mcabber.1.html mcabber/doc/mcabber.1.txt mcabber/src/commands.c mcabber/src/jab_iq.c mcabber/src/screen.c
diffstat 58 files changed, 2263 insertions(+), 210 deletions(-) [+]
line wrap: on
line diff
--- a/mcabber/ChangeLog	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/ChangeLog	Sun Nov 19 11:51:14 2006 +0100
@@ -1,3 +1,9 @@
+mcabber (0.8.4-dev)
+
+ * 
+
+ -- Mikael, ?
+
 mcabber (0.8.3)
 
  * Fix truncation of UTF-8 buddy names in the roster (Myhailo Danylenko)
--- a/mcabber/TODO	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/TODO	Sun Nov 19 11:51:14 2006 +0100
@@ -7,11 +7,15 @@
 TODO:
 
 * Implement automatic reconnection after network failure
+* GPG support
+* Improve the completion system
+* Connect hook
+* Macros
+* Nick highlight
+* VCard support
 * Show number of online contacts in folded groups
 * Publish personal information
-* GPG support
 * Colors for presence
-* Improve the completion system
 * Persistent room in the roster (join every connect)
 * MUC: advanced settings for room creation
 * MUC: display roles of room members
@@ -21,6 +25,9 @@
 * Sort roster by status
 * 2-levels roster display (jids, resources)
 * "Ignore list" (privacy lists)
+  See JEP-0191: Simple Communications Blocking (very new)
+* Use DNS _xmpp-client._tcp service
+* Options scrollback_time/scrollback_lines (a la irssi)
 
 * File transfer? :)
 
--- a/mcabber/configure.ac	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/configure.ac	Sun Nov 19 11:51:14 2006 +0100
@@ -2,7 +2,7 @@
 # Process this file with autoconf to produce a configure script.
 
 AC_PREREQ(2.59)
-AC_INIT([mcabber],[0.8.3],[mcabber@lilotux.net])
+AC_INIT([mcabber],[0.8.4-dev],[mcabber@lilotux.net])
 AM_INIT_AUTOMAKE
 AC_CONFIG_SRCDIR([src])
 AM_CONFIG_HEADER(config.h)
--- a/mcabber/doc/help/Makefile.am	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/doc/help/Makefile.am	Sun Nov 19 11:51:14 2006 +0100
@@ -2,7 +2,7 @@
 
 install-data-local:
 	$(INSTALL) -d $(DESTDIR)$(datadir)/mcabber/help
-	for lang in en fr pl; do \
+	for lang in en fr pl uk; do \
 		$(INSTALL) -d $(DESTDIR)$(datadir)/mcabber/help/$$lang; \
 		$(INSTALL) -m 0644 $$lang/* \
 		           $(DESTDIR)$(datadir)/mcabber/help/$$lang; \
--- a/mcabber/doc/help/en/hlp_move.txt	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/doc/help/en/hlp_move.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -1,5 +1,5 @@
 
  /MOVE [groupname]
 
-Move the current buddy to the requested group.  If no group is specified, then the buddy is moved to the default group.
+Move the current buddy to the requested group.  If no group is specified, then the buddy is moved to the default group.  If the group "groupname" doesn't exist, it is created.
 Tip: if the chatmode is enabled, you can use "/roster alternate" to jump to the moved buddy.
--- a/mcabber/doc/help/en/hlp_roster.txt	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/doc/help/en/hlp_roster.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -27,6 +27,10 @@
  Show roster
 /roster toggle
  Toggle roster visibility
+/roster item_lock [jid]
+ Lock the roster item so it remains visible regardless of its status
+/roster item_unlock [jid]
+ Undo the effects of item_lock
 /roster search bud
  Search for a buddy with a name or jid containing "bud" (only in the displayed buddylist)
 /roster alternate
--- a/mcabber/doc/help/fr/hlp_move.txt	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/doc/help/fr/hlp_move.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -1,5 +1,5 @@
 
  /MOVE [groupname]
 
-Déplace le contact sélectionné vers le groupe spécifié. Si aucun groupe n'est donné, le contact est déplacé vers le groupe par défaut.
+Déplace le contact sélectionné vers le groupe spécifié. Si aucun groupe n'est donné, le contact est déplacé vers le groupe par défaut. Si le groupe "groupname" n'existe pas, il est créé.
 Astuce : si le mode discussion (chatmode) est activé, vous pouvez utiliser "/roster alternate" pour vous positionner sur le contact que vous venez de déplacer.
--- a/mcabber/doc/help/fr/hlp_roster.txt	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/doc/help/fr/hlp_roster.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -26,6 +26,10 @@
  Affiche le roster
 /roster toggle
  Inverse l'affichage du roster (hide/show)
+/roster item_lock [jid]
+ Affiche le contact dans le roster, même s'il est déconnecté
+/roster item_unlock [jid]
+ Annule l'effet de /roster item_lock
 /roster search bud
  Cherche un contact dont le nom ou le jid contient "bud" (seulement dans les contacts affichés)
 /roster alternate
--- a/mcabber/doc/help/pl/hlp_move.txt	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/doc/help/pl/hlp_move.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -1,5 +1,5 @@
 
  /MOVE [nazwa grupy]
 
-Przenosi aktualną osobę do grupy "nazwa grupy".  Jeśli nie podano nazwy grupy, wtedy osoba jest przenoszona do grupy domyślnej.
+Przenosi aktualną osobę do grupy "nazwa grupy".  Jeśli nie podano nazwy grupy, wtedy osoba jest przenoszona do grupy domyślnej.  Jeśli grupa "nazwa grupy" nie istnieje, zostaje utworzona.
 Podpowiedź: jeśli jest włączony tryb czatu, możesz użyć "/roster alternate" aby skoczyć do przeniesionej osoby.
--- a/mcabber/doc/help/pl/hlp_roster.txt	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/doc/help/pl/hlp_roster.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -27,6 +27,10 @@
  Pokazuje listę kontaktów
 /roster toggle
  Włącza/wyłącza listę kontaktów
+/roster item_lock [jid]
+ Zablokuj wyświetlanie elementu rostera, tak aby był widoczny cały czas (bez względu na jego status)
+/roster item_unlock [jid]
+ Odblokuj wyświetlanie elementu rostera, element będzie widoczny ze względu na jego status
 /roster search [osoba]
  Szuka osoby "osoba", lub osoby której nazwa zawiera słowo "osoba"
 /roster alternate
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,6 @@
+
+ /HELP [команда|+вираз]
+
+Друкує допоміжну інформацію про команду або вираз.
+Без аргументу друкує оце повідомлення.
+Наявні команди: add, alias, authorization, bind, buffer, clear, connect, del, disconnect, event, group, help, info, move, msay, quit, rawxml, rename, request, room, roster, say_to, say, set, status_to, status, version.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_add.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,9 @@
+
+ /ADD [jid [прізвисько]]
+
+Додає користувача "jid" у загальну групу вашого спису контактів, а також надсилає запит на повідомлення про присутність до цього "jid". Якщо прізвисько не вказане, буде використаний "jid". Якщо "jid" не вказаний (чи "" або "."), береться поточний.
+
+/add [jid [прізвисько]]
+Додає до списку контактів "jid" як "nickname"
+
+Приклад: "/add somebody@jabber.server.com Дехто"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_alias.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,16 @@
+
+ /ALIAS [ім'я [= рядок команди]]
+
+Ця команда дозволяє спростити складні чи довгі команди, призначивши їм друге ім'я (аліас).
+Керування аліасами здійснюється командами:
+
+/alias
+ Друкує спис визначених на даний момент аліасів.
+/alias ім'я
+ Друкує поточне значення аліасу "ім'я"
+/alias ім'я =
+ Видаляє аліас "ім'я"
+/alias ім'я = рядок команди
+ Призначає для "рядок команди" інше ім'я (аліас) "ім'я"
+
+Приклад: "/alias кава = status away Пішов варити каву..."
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_authorization.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,14 @@
+
+ /AUTHORIZATION allow|cancel|request|request_unsubscribe [jid]
+
+Цією командою можна керувати підпискою на повідомлення про присутність - надсилати запити до інших, а також дозволяти чи забороняти їм отримувати повідомлення про ваш статус.
+Якщо не вказати "jid", команда працюватиме з поточним.
+
+/authorization allow
+ Дозволяє отримувати повідомлення про ваш статус.
+/authorization cancel
+ Відміняє підписку на ваші повідомлення про статус.
+/authorization request
+ Надсилає запит на повідомлення про присутність.
+/authorization request_unsubscribe
+ Надсилає запит на відписку від повідомлень про присутність.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_bind.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,17 @@
+
+ /BIND [код [= рядок команди]]
+
+Прив'язує до клавіші з кодом "код" команду "рядок команди". Щоб визначити код клавіші - натисніть її, якщо в віконці логу з'явиться щось на зразок "Unknown key=265", значить клавіша не призначена, і її код 265 (в цьому випадку).
+
+/bind
+ Друкує спис поточних призначень.
+/bind код
+ Друкує призначену цьому коду клавіші команду.
+/bind код =
+ Відміняє призначення до цього коду.
+/bind код = рядок команди
+ Прив'язує команду до клавіші із заданим кодом.
+
+Приклад: "/bind 265 = status away" (265 - це F1).
+Примітка: коди можуть відрізнятися в залежності від вашої libncureses.
+Примітка: аліаси теж можуть використовуватися в команді.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_buffer.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,32 @@
+
+ /BUFFER [clear|purge|top|bottom|date|%|search_backward|search_forward]
+ /BUFFER [scroll_lock|scroll_unlock|scroll_toggle]
+
+Різноманітні операції з буфером розмови. Наприклад, ви можете шукати деякий текст в буфері, очистити вікно розмови, ітп.
+
+/buffer clear
+ Очистити поточне вікно діалогу.
+/buffer purge
+ Очистити вікно діалогу а також потерти його вміст з пам'яті.
+/buffer bottom
+ Перейти до кінця буферу.
+/buffer top
+ Перейти до початку буферу.
+/buffer up [n]
+ Посунути буфер вверх на n рядків (якщо не вказано - пів екрану).
+/buffer down [n]
+ Посунути буфер вниз на n рядків (якщо не вказано - пів екрану).
+/buffer date [дата]
+ Перейти до першого повідомлення після [дати] (дата вигляду РРРР-ММ-ДД).
+/buffer % n
+ Перейти до позиції n% у буфері.
+/buffer search_backward текст
+ Шукати текст в буфері назад.
+/buffer search_forward text
+ Шукати текст в буфері вперед.
+/buffer scroll_lock
+ Заборонити переміщення буферу.
+/buffer scroll_unlock
+ Зняти заборону переміщення буферу.
+/buffer scroll_toggle
+ Переключити стан буферу (дозволене/не дозволене переміщення).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_clear.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,4 @@
+
+ /CLEAR
+
+Фактично - скорочення до "/buffer clear". Очищує поточне вікно діалогу.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_connect.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,4 @@
+
+ /CONNECT
+
+Встановити з'єднання з сервером.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_del.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,4 @@
+
+ /DEL
+
+Видалити поточний контакт з списку. На додачу відписатися від його повідомлень про статус, і відписати його від ваших.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_disconnect.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,5 @@
+
+ /DISCONNECT
+
+Розірвати з'єднання з сервером.
+Майте на увазі, що спис контактів зберігається на сервері, отже після від'єднання там запанує пустка.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_event.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,15 @@
+
+ /EVENT #N|* accept|ignore|reject
+ /EVENT list
+
+Розібратися з очікуючими подіями (запити на авторизацію ітп).
+Якщо перший параметр - зірочка (*), відповідь буде застосована до всіх подій.
+
+/event N|* accept
+ Подія номер #N отримає позитивну відповідь.
+/event N|* ignore
+ Не відповідати на подію номер N.
+/event N|* reject
+ Подія номер N отримає негативну відповідь.
+/event list
+ Друкує перелік очікуючих подій.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_group.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,11 @@
+
+ /GROUP fold|unfold|toggle
+
+Змінює відображення поточної групи.
+
+/group fold
+ Згорнути дерево поточної групи у списку.
+/group unfold
+ Розгорнути дерево поточної групи у списку.
+/group toggle
+ Переключити стан відображення поточної групи.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_help.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,5 @@
+
+ /HELP [команда|+вираз]
+
+Друкує допоміжну інформацію до команди або виразу.
+Приклад: "/help buffer"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_info.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,5 @@
+
+ /INFO
+
+Друкує інформацію про поточний об'єкт (контакт, групу, ітп...).
+Для контактів ресурси друкуються із статусом, пріоритетом, та повідомленням статусу (якщо є) для кожного ресурсу.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_move.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,5 @@
+
+ /MOVE [група]
+
+Переміщує поточний контакт до вказаної групи. Якщо групу не вказано, переміщує до головної. Якщо група не існує, створює її.
+Примітка: Якщо в режимі чату, можна використати "/roster alternate", щоб перейти до переміщеного контакту.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_msay.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,18 @@
+
+ /MSAY begin|verbatim|send|send_to|toggle|abort
+
+Надсилає багаторядкове повідомлення. Щоб написати одне повідомлення з декількох рядків потрібно використовувати багаторядковий режим. В цьому режимі кожний введений рядок (за виключенням рядків команд) додається до багаторядкового повідомлення. Коли повідомлення завершене, воно може бути надіслано за допомогою "/msay send".
+
+/msay begin [тема]
+ Починає багаторядкове повідомлення. Зауважте, можна вказати тему цього повідомлення.
+/msay verbatim
+ Входить в багаторядковий режим, в якому розпізнається лише команда "/msay send|abort".
+/msay send
+ Відсилає поточне повідомлення до поточного контакту.
+/msay send_to jid
+ Відсилає повідомлення до "jid".
+/msay toggle
+ Переключається в/із багаторядкового режиму (begin/send).
+ Примітка: може використовуватися для швидкого переключення режимів (наприклад, "bind M13 = msay toggle" дозволяє починати й закінчувати повідомлення Meta-Enter).
+/msay abort
+ Вийти з багаторядкового режиму, не надіславши повідомлення.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_quit.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,4 @@
+
+ /QUIT
+
+Закриває всі з'єднання й завершує mcabber.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_rawxml.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,9 @@
+
+ /RAWXML send рядок
+
+Надсилає рядок до серверу (ніяк його при цьому не змінюючи).
+
+Увага!
+Використовуйте цю команду лише якщо ви знаєте, що робите.
+
+Приклад: "/rawxml send <presence><show>away</show></presence>"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_rename.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,4 @@
+
+ /RENAME ім'я
+
+Змінює прізвисько поточного контакту або назву групи.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_request.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,9 @@
+
+ /REQUEST time|version [jid]
+
+Надсилає запит "IQ" до jid або до поточного контакту. Якщо ресурс не вказаний, то зпит буде послано всім відомим ресурсам.
+
+/request time
+ Запитати котра година в контакту.
+/request version
+ Запитати яку версію має контакт.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_room.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,36 @@
+
+ /ROOM join|leave|names|nick|remove|topic|unlock|destroy
+ /ROOM privmsg|invite|kick|ban|role|affil
+
+Команда, відповідальна за дії в багатокористувацькому чаті.
+
+/room join [кімната [прізвисько [пароль]]]
+ Ввійти в "кімнату", під ім'ям "прізвисько". Якщо не вказане (або ""), береться з параметру "nickname" (mcabberrc). Якщо поточний контакт є кімнатою, можна використати "." замість повного ім'я кімнати. Пароль може використовуватися для входу в захищені кімнати. Для імен з пробілами використовуйте лапки.
+/room leave [повідомлення]
+ Вийти з поточної кімнати.
+/room names
+ Друкує імена присутніх в кімнаті.
+/room nick прізвисько
+ Змінити ваше прізвисько в поточній кімнаті.
+/room privmsg прізвисько повідомлення
+ Надіслати приватне повідомлення до "прізвисько".
+/room remove
+ Потерти поточну кімнату з списку контактів (ви повинні спочатку вийти).
+/room topic тема
+ Встановити тему обговорення в поточній кімнаті.
+/room unlock
+ Відкрити поточну кімнату (якщо ви її власник).
+/room destroy [причина]
+ Розвалити поточну кімнату (обережно!)
+/room whois прізвисько
+ Друкує інформацію про "прізвисько".
+/room ban jid [причина]
+ Вибанити jid з кімнати.
+/room invite jid [причина]
+ Запросити jid до поточної кімнати.
+/room kick прізвисько [причина]
+ Кікнути "прізвисько" з кімнати.
+/room role jid роль [причина]
+ Змінити роль jid у комнаті ("none", "visitor", "participant", "moderator")
+/room affil jid ступінь [причина]
+ Змінити ступінь довіри для jid ("none", "member", "admin", "owner").
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_roster.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,40 @@
+
+ /ROSTER bottom|top|hide_offline|show_offline|toggle_offline
+ /ROSTER hide|show|toggle
+ /ROSTER alternate|unread_first|unread_next
+ /ROSTER search ім'я
+
+Керує списком контактів.
+
+/roster bottom
+ Перейти до кінця списку.
+/roster top
+ Перейти на початок списку.
+/roster up
+ Перейти вище по списку.
+/roster down
+ Перейти нижче по списку.
+/roster hide_offline
+ Сховати неприсутні контакти.
+/roster show_offline
+ Показувати неприсутні контакти також.
+/roster toggle_offline
+ Переключити відображення неприсутніх контактів.
+/roster hide
+ Сховати список.
+/roster show
+ Показати список.
+/roster toggle
+ Переключити відображення списку.
+/roster item_lock [jid]
+ Закріплює вказаний об'єкт списку так, що він завжди відображується, незважаючи на свій статус.
+/roster item_unlock [jid]
+ Відміняє дію item_lock.
+/roster search ім'я
+ Шукати контакт з прізвиськом або jid, що вміщують "ім'я" (тільки в контактах, що відображуються)
+/roster alternate
+ Прейти до "альтернативного" контакту. "Альтернативний" контакт - це останній контакт, який був в режимі чату. Ця команда може використовуватись після команд типу "/roster unread_next" (Ctrl-q).
+/roster unread_first
+ Перейти до першого непрочитанного повідомлення.
+/roster unread_next
+ Перейти до наступного непрочитанного повідомлення.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_say.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,4 @@
+
+ /SAY текст
+
+Надсилає текст до поточного контакту. Це може стати в нагоді, якщо ви хочете надіслати повідомлення з символом '/' на початку.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_say_to.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,5 @@
+
+ /SAY_TO jid текст
+
+Надсилає текст до вказаного jid.
+Майте на увазі, що ця команда не встановлює стандартного ресурсу для контакту, отже, якщо вам треба надіслати декілька повідомлень до визначених ресурсів, вам треба використовувати "/say_to" для кожного повідомлення.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_set.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,4 @@
+
+ /SET параметр [= значення]
+
+Встановлює або друкує значення вказаного параметру.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_status.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,8 @@
+
+ /STATUS [online|avail|invisible|free|dnd|notavail|away [-|повідомлення]]
+
+Друкує або встановлює ваш статус.
+Якщо статус не вказано, друкує поточний.
+Якщо не вказане повідомлення, використовується повідомлення вказане відповідним параметром message* (з mcabberrc).
+Якщо відповідний message* не визначений, залишається поточне повідомлення.
+"-" тре поточне повідомлення.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_status_to.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,6 @@
+
+ /STATUS_TO jid online|avail|invisible|free|dnd|notavail|away [повідомлення]
+
+Надсилає до jid вказаний статус. Поточний контакт задається "." .
+Примітка: Цей статус буде скинуто наступною командою "/status". Автоматична зміна статусу також це робить.
+Примітка: jid може вказувати й ресурс (користувач@сервер/ресурс).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/doc/help/uk/hlp_version.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -0,0 +1,4 @@
+
+ /VERSION
+
+Друкує версію mcabber'у.
--- a/mcabber/doc/mcabber.1	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/doc/mcabber.1	Sun Nov 19 11:51:14 2006 +0100
@@ -291,7 +291,7 @@
 
 .TP
 \fB/move\fR [groupname]
-Move the current buddy to the requested group\&. If no group is specified, then the buddy is moved to the default group\&. Tip: if the chatmode is enabled, you can use "/roster alternate" to jump to the moved buddy\&.
+Move the current buddy to the requested group\&. If no group is specified, then the buddy is moved to the default group\&.  If the group groupname doesn't exist, it is created\&. Tip: if the chatmode is enabled, you can use "/roster alternate" to jump to the moved buddy\&.
 
 .TP
 \fB/msay\fR begin|verbatim|send|send_to|toggle|toggle_verbatim|abort
@@ -350,12 +350,14 @@
  \fBtop\fR       	jump to the top of the roster
  \fBup\fR        	move up in the roster
  \fBdown\fR      	move down in the roster
- \fBhide_offline\fR	hide offline buddies
- \fBshow_offline\fR	show offline buddies
+ \fBhide_offline\fR  	hide offline buddies
+ \fBshow_offline\fR  	show offline buddies
  \fBtoggle_offline\fR	toggle display of offline buddies
  \fBhide\fR      	hide roster (full\-width chat window)
  \fBshow\fR      	show roster
  \fBtoggle\fR    	toggle roster visibility
+ \fBitem_lock\fR jid  	lock the roster item so it remains visible regardless of its status
+ \fBitem_unlock\fR jid	undo the effects of item_lock
  \fBsearch\fR bud	search for a buddy with a name or jid containing "bud" (only in the displayed buddylist)
  \fBalternate\fR 	jump to alternate buddy\&. The "alternate" buddy is the last buddy left while being in chat mode (this command is thus especially useful after commands like "/roster unread_first")
  \fBunread_first\fR	jump to the first unread message
--- a/mcabber/doc/mcabber.1.html	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/doc/mcabber.1.html	Sun Nov 19 11:51:14 2006 +0100
@@ -728,7 +728,8 @@
 </b></dt>
 <dd>
         Move the current buddy to the requested group.  If no group is
-        specified, then the buddy is moved to the default group.<br />
+        specified, then the buddy is moved to the default group.
+        If the group groupname doesn't exist, it is created.<br />
         Tip: if the chatmode is enabled, you can use "/roster alternate"
         to jump to the moved buddy.
 </dd>
@@ -1054,6 +1055,22 @@
 </tr>
 <tr valign="top">
 <td>
+<b>item_lock</b> jid
+</td>
+<td>
+lock the roster item so it remains visible regardless of its status
+</td>
+</tr>
+<tr valign="top">
+<td>
+<b>item_unlock</b> jid
+</td>
+<td>
+undo the effects of item_lock
+</td>
+</tr>
+<tr valign="top">
+<td>
 <b>search</b> bud
 </td>
 <td>
@@ -1148,8 +1165,8 @@
 License (GPL).</p>
 <div id="footer">
 <p>
-Version 0.8.3<br />
-Last updated 16-Nov-2006 18:48:00 CEST
+Version 0.8.3-dev<br />
+Last updated 01-Oct-2006 23:57:04 CEST
 </p>
 </div>
 </div>
--- a/mcabber/doc/mcabber.1.txt	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/doc/mcabber.1.txt	Sun Nov 19 11:51:14 2006 +0100
@@ -1,7 +1,7 @@
 MCABBER(1)
 ===========
 Mikael BERTHE <mcabber@lilotux.net>
-v0.8.3, November 2006
+v0.8.4-dev, November 2006
 
 NAME
 ----
@@ -263,7 +263,8 @@
 
 /move [groupname]::
         Move the current buddy to the requested group.  If no group is
-        specified, then the buddy is moved to the default group. +
+        specified, then the buddy is moved to the default group.
+        If the group groupname doesn't exist, it is created. +
         Tip: if the chatmode is enabled, you can use "/roster alternate"
         to jump to the moved buddy.
 
@@ -338,6 +339,8 @@
         'hide';;         hide roster (full-width chat window)
         'show';;         show roster
         'toggle';;       toggle roster visibility
+        'item_lock' jid;;   lock the roster item so it remains visible regardless of its status
+        'item_unlock' jid;; undo the effects of item_lock
         'search' bud;;   search for a buddy with a name or jid containing "bud" (only in the displayed buddylist)
         'alternate';;    jump to alternate buddy.  The "alternate" buddy is the last buddy left while being in chat mode (this command is thus especially useful after commands like "/roster unread_first")
         'unread_first';; jump to the first unread message
--- a/mcabber/libjabber/jabber.h	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/libjabber/jabber.h	Sun Nov 19 11:51:14 2006 +0100
@@ -281,6 +281,8 @@
 #define NS_DISCO_ITEMS "http://jabber.org/protocol/disco#items"
 #define NS_IQ_AUTH    "http://jabber.org/features/iq-auth"
 #define NS_REGISTER_FEATURE "http://jabber.org/features/iq-register"
+#define NS_MUC       "http://jabber.org/protocol/muc"
+#define NS_CHATSTATES "http://jabber.org/protocol/chatstates"
 
 #define NS_XDBGINSERT "jabber:xdb:ginsert"
 #define NS_XDBNSLIST  "jabber:xdb:nslist"
--- a/mcabber/mcabberrc.example	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/mcabberrc.example	Sun Nov 19 11:51:14 2006 +0100
@@ -60,13 +60,18 @@
 
 # Language
 # Help files have been translated into a few languages.
-# You can set lang to one of the following values: en (default), fr, pl.
+# You can set lang to one of the following values: en (default), fr, pl, uk.
 #set lang = en
 
 # Set hide_offline_buddies to 1 to display only connected buddies
 # in the roster.
 #set hide_offline_buddies = 0
 
+# Typing notifications, Chat States, Events (JEP-22/85)
+# Set disable_chatstates to 1 if you don't want to use typing notifications.
+# Note: changing this option once mcabber is running has no effect.
+#set disable_chatstates = 0
+
 # History logging
 # You can save the messages history: set logging = 1
 # You can load (read) the messages history: set load_logs = 1
@@ -219,6 +224,12 @@
 # Buddylist window width (minimum 2, default 24)
 #set roster_width=24
 #
+# The options "log_win_on_top" and "roster_win_on_right" can change the
+# position of the log window (top/bottom) and the position of the roster
+# (left/right).
+#set log_win_on_top = 0
+#set roster_win_on_right = 0
+#
 # Buddy name format (in status window):
 # - 0: (default) "<jid/resource>"
 # - 1: "name <jid/resource>" (name is omitted if same as the jid)
--- a/mcabber/src/commands.c	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/src/commands.c	Sun Nov 19 11:51:14 2006 +0100
@@ -33,6 +33,15 @@
 #include "settings.h"
 #include "events.h"
 
+#define IMSTATUS_AWAY           "away"
+#define IMSTATUS_ONLINE         "online"
+#define IMSTATUS_OFFLINE        "offline"
+#define IMSTATUS_FREE4CHAT      "free"
+#define IMSTATUS_INVISIBLE      "invisible"
+#define IMSTATUS_AVAILABLE      "avail"
+#define IMSTATUS_NOTAVAILABLE   "notavail"
+#define IMSTATUS_DONOTDISTURB   "dnd"
+
 // Commands callbacks
 static void do_roster(char *arg);
 static void do_status(char *arg);
@@ -142,10 +151,13 @@
   compl_add_category_word(COMPL_ROSTER, "hide_offline");
   compl_add_category_word(COMPL_ROSTER, "show_offline");
   compl_add_category_word(COMPL_ROSTER, "toggle_offline");
+  compl_add_category_word(COMPL_ROSTER, "item_lock");
+  compl_add_category_word(COMPL_ROSTER, "item_unlock");
   compl_add_category_word(COMPL_ROSTER, "alternate");
   compl_add_category_word(COMPL_ROSTER, "search");
   compl_add_category_word(COMPL_ROSTER, "unread_first");
   compl_add_category_word(COMPL_ROSTER, "unread_next");
+  compl_add_category_word(COMPL_ROSTER, "note");
 
   // Roster category
   compl_add_category_word(COMPL_BUFFER, "clear");
@@ -179,6 +191,7 @@
   // Room category
   compl_add_category_word(COMPL_ROOM, "affil");
   compl_add_category_word(COMPL_ROOM, "ban");
+  compl_add_category_word(COMPL_ROOM, "bookmark");
   compl_add_category_word(COMPL_ROOM, "destroy");
   compl_add_category_word(COMPL_ROOM, "invite");
   compl_add_category_word(COMPL_ROOM, "join");
@@ -200,7 +213,9 @@
   compl_add_category_word(COMPL_AUTH, "request_unsubscribe");
 
   // Request (query) category
+  compl_add_category_word(COMPL_REQUEST, "last");
   compl_add_category_word(COMPL_REQUEST, "time");
+  compl_add_category_word(COMPL_REQUEST, "vcard");
   compl_add_category_word(COMPL_REQUEST, "version");
 
   // Events category
@@ -221,8 +236,8 @@
   const gchar *value;
   char *newline = line;
 
-  // Ignore leading '/'
-  for (p1 = line ; *p1 == '/' ; p1++)
+  // Ignore leading COMMAND_CHAR
+  for (p1 = line ; *p1 == COMMAND_CHAR ; p1++)
     ;
   // Locate the end of the word
   for (p2 = p1 ; *p2 && (*p2 != ' ') ; p2++)
@@ -235,7 +250,7 @@
   if (value) {
     // There is an alias to expand
     newline = g_new(char, strlen(value)+strlen(p2)+2);
-    *newline = '/';
+    *newline = COMMAND_CHAR;
     strcpy(newline+1, value);
     strcat(newline, p2);
   }
@@ -253,8 +268,8 @@
   char *com;
   GSList *sl_com;
 
-  // Ignore leading '/'
-  for (p1 = command ; *p1 == '/' ; p1++)
+  // Ignore leading COMMAND_CHAR
+  for (p1 = command ; *p1 == COMMAND_CHAR ; p1++)
     ;
   // Locate the end of the command
   for (p2 = p1 ; *p2 && (*p2 != ' ') ; p2++)
@@ -309,7 +324,7 @@
   }
 
   // Network part
-  jb_send_msg(jid, msg, buddy_gettype(BUDDATA(current_buddy)), subj);
+  jb_send_msg(jid, msg, buddy_gettype(BUDDATA(current_buddy)), subj, NULL);
 }
 
 //  process_command(line)
@@ -338,12 +353,14 @@
     *p = 0;
 
   // Command "quit"?
-  if ((!strncasecmp(xpline, "/quit", 5)) && (scr_get_multimode() != 2) )
+  if ((scr_get_multimode() != 2)
+      && (!strncasecmp(xpline, mkcmdstr("quit"), strlen(mkcmdstr("quit")))))
     if (!xpline[5] || xpline[5] == ' ')
       return 255;
 
   // If verbatim multi-line mode, we check if another /msay command is typed
-  if ((scr_get_multimode() == 2) && (strncasecmp(xpline, "/msay ", 6))) {
+  if ((scr_get_multimode() == 2)
+      && (strncasecmp(xpline, mkcmdstr("msay "), strlen(mkcmdstr("msay "))))) {
     // It isn't an /msay command
     scr_append_multiline(xpline);
     g_free(xpline);
@@ -397,7 +414,7 @@
     return 0;
   }
 
-  if (*line != '/') {
+  if (*line != COMMAND_CHAR) {
     // This isn't a command
     if (scr_get_multimode())
       scr_append_multiline(line);
@@ -410,63 +427,224 @@
   return process_command(line);
 }
 
+// Helper routine for buffer item_{lock,unlock}
+static void roster_buddylock(char *jid, bool lock)
+{
+  gpointer bud = NULL;
+  bool may_need_refresh = FALSE;
+
+  // Allow special jid "" or "." (current buddy)
+  if (jid && (!*jid || !strcmp(jid, ".")))
+    jid = NULL;
+
+  if (jid) {
+    // The JID has been specified.  Quick check...
+    if (check_jid_syntax(jid)) {
+      scr_LogPrint(LPRINT_NORMAL, "<%s> is not a valid Jabber ID.", jid);
+    } else {
+      // Find the buddy
+      GSList *roster_elt;
+      roster_elt = roster_find(jid, jidsearch,
+                               ROSTER_TYPE_USER|ROSTER_TYPE_ROOM);
+      if (roster_elt)
+        bud = roster_elt->data;
+      else
+        scr_LogPrint(LPRINT_NORMAL, "This jid isn't in the roster.");
+      may_need_refresh = TRUE;
+    }
+  } else {
+    // Use the current buddy
+    if (current_buddy)
+      bud = BUDDATA(current_buddy);
+  }
+
+  // Update the ROSTER_FLAG_USRLOCK flag
+  if (bud) {
+    buddy_setflags(bud, ROSTER_FLAG_USRLOCK, lock);
+    if (may_need_refresh)
+      buddylist_build();
+      update_roster = TRUE;
+  }
+}
+
+//  display_and_free_note(note, winId)
+// Display the note information in the winId buffer, and free note
+// (winId is a bare jid or NULL for the status window, in which case we
+// display the note jid too)
+static void display_and_free_note(struct annotation *note, const char *winId)
+{
+  gchar tbuf[128];
+  GString *sbuf;
+  guint msg_flag = HBB_PREFIX_INFO;
+  /* We use the flag prefix_info for the first line, and prefix_none
+     for the other lines, for better readability */
+
+  if (!note)
+    return;
+
+  sbuf = g_string_new("");
+
+  if (!winId) {
+    // We're writing to the status window, so let's show the jid too.
+    g_string_printf(sbuf, "Annotation on <%s>", note->jid);
+    scr_WriteIncomingMessage(winId, sbuf->str, 0, msg_flag);
+    msg_flag = HBB_PREFIX_NONE;
+  }
+
+  // If we have the creation date, display it
+  if (note->cdate) {
+    strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S",
+             localtime(&note->cdate));
+    g_string_printf(sbuf, "Note created  %s", tbuf);
+    scr_WriteIncomingMessage(winId, sbuf->str, 0, msg_flag);
+    msg_flag = HBB_PREFIX_NONE;
+  }
+  // If we have the modification date, display it
+  // unless it's the same as the creation date
+  if (note->mdate && note->mdate != note->cdate) {
+    strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S",
+             localtime(&note->mdate));
+    g_string_printf(sbuf, "Note modified %s", tbuf);
+    scr_WriteIncomingMessage(winId, sbuf->str, 0, msg_flag);
+    msg_flag = HBB_PREFIX_NONE;
+  }
+  // Note text
+  g_string_printf(sbuf, "Note: %s", note->text);
+  scr_WriteIncomingMessage(winId, sbuf->str, 0, msg_flag);
+
+  g_string_free(sbuf, TRUE);
+  g_free(note->text);
+  g_free(note->jid);
+  g_free(note);
+}
+
+static void display_all_annotations()
+{
+  GSList *notes;
+  notes = jb_get_all_storage_rosternotes();
+  // Call display_and_free_note() for each note,
+  // with winId = NULL (special window)
+  g_slist_foreach(notes, (GFunc)&display_and_free_note, NULL);
+  g_slist_free(notes);
+}
+
+static void roster_note(char *arg)
+{
+  const char *jid;
+  guint type;
+
+  if (!current_buddy)
+    return;
+
+  jid = buddy_getjid(BUDDATA(current_buddy));
+  type = buddy_gettype(BUDDATA(current_buddy));
+
+  if (!jid && type == ROSTER_TYPE_SPECIAL && !arg) {
+    // We're in the status window (the only special buffer currently)
+    // Let's display all server notes
+    display_all_annotations();
+    return;
+  }
+
+  if (!jid || (type != ROSTER_TYPE_USER &&
+               type != ROSTER_TYPE_ROOM &&
+               type != ROSTER_TYPE_AGENT)) {
+    scr_LogPrint(LPRINT_NORMAL, "This item can't have a note.");
+    return;
+  }
+
+  if (arg && *arg) {  // Set a note
+    gchar *msg, *notetxt;
+    msg = to_utf8(arg);
+    if (!strcmp(msg, "-"))
+      notetxt = NULL; // delete note
+    else
+      notetxt = msg;
+    jb_set_storage_rosternotes(jid, notetxt);
+    g_free(msg);
+  } else {      // Display a note
+    struct annotation *note = jb_get_storage_rosternotes(jid, FALSE);
+    if (note) {
+      display_and_free_note(note, jid);
+    } else {
+      scr_WriteIncomingMessage(jid, "This item doesn't have a note.", 0,
+                               HBB_PREFIX_INFO);
+    }
+  }
+}
+
 /* Commands callback functions */
 /* All these do_*() functions will be called with a "arg" parameter */
 /* (with arg not null)                                              */
 
 static void do_roster(char *arg)
 {
-  if (!strcasecmp(arg, "top")) {
+  char **paramlst;
+  char *subcmd;
+
+  paramlst = split_arg(arg, 2, 1); // subcmd, arg
+  subcmd = *paramlst;
+  arg = *(paramlst+1);
+
+  if (!subcmd || !*subcmd) {
+    scr_LogPrint(LPRINT_NORMAL, "Missing parameter.");
+    free_arg_lst(paramlst);
+    return;
+  }
+
+  if (!strcasecmp(subcmd, "top")) {
     scr_RosterTop();
     update_roster = TRUE;
-  } else if (!strcasecmp(arg, "bottom")) {
+  } else if (!strcasecmp(subcmd, "bottom")) {
     scr_RosterBottom();
     update_roster = TRUE;
-  } else if (!strcasecmp(arg, "hide")) {
+  } else if (!strcasecmp(subcmd, "hide")) {
     scr_RosterVisibility(0);
-  } else if (!strcasecmp(arg, "show")) {
+  } else if (!strcasecmp(subcmd, "show")) {
     scr_RosterVisibility(1);
-  } else if (!strcasecmp(arg, "toggle")) {
+  } else if (!strcasecmp(subcmd, "toggle")) {
     scr_RosterVisibility(-1);
-  } else if (!strcasecmp(arg, "hide_offline")) {
+  } else if (!strcasecmp(subcmd, "hide_offline")) {
     buddylist_set_hide_offline_buddies(TRUE);
     if (current_buddy)
       buddylist_build();
     update_roster = TRUE;
-  } else if (!strcasecmp(arg, "show_offline")) {
+  } else if (!strcasecmp(subcmd, "show_offline")) {
     buddylist_set_hide_offline_buddies(FALSE);
     buddylist_build();
     update_roster = TRUE;
-  } else if (!strcasecmp(arg, "toggle_offline")) {
+  } else if (!strcasecmp(subcmd, "toggle_offline")) {
     buddylist_set_hide_offline_buddies(-1);
     buddylist_build();
     update_roster = TRUE;
-  } else if (!strcasecmp(arg, "unread_first")) {
+  } else if (!strcasecmp(subcmd, "item_lock")) {
+    roster_buddylock(arg, TRUE);
+  } else if (!strcasecmp(subcmd, "item_unlock")) {
+    roster_buddylock(arg, FALSE);
+  } else if (!strcasecmp(subcmd, "unread_first")) {
     scr_RosterUnreadMessage(0);
-  } else if (!strcasecmp(arg, "unread_next")) {
+  } else if (!strcasecmp(subcmd, "unread_next")) {
     scr_RosterUnreadMessage(1);
-  } else if (!strcasecmp(arg, "alternate")) {
+  } else if (!strcasecmp(subcmd, "alternate")) {
     scr_RosterJumpAlternate();
-  } else if (!strncasecmp(arg, "search", 6)) {
-    char *string = arg+6;
-    if (*string && (*string != ' ')) {
-      scr_LogPrint(LPRINT_NORMAL, "Unrecognized parameter!");
+  } else if (!strncasecmp(subcmd, "search", 6)) {
+    strip_arg_special_chars(arg);
+    if (!arg || !*arg) {
+      scr_LogPrint(LPRINT_NORMAL, "What name or JID are you looking for?");
+      free_arg_lst(paramlst);
       return;
     }
-    while (*string == ' ')
-      string++;
-    if (!*string) {
-      scr_LogPrint(LPRINT_NORMAL, "What name or JID are you looking for?");
-      return;
-    }
-    scr_RosterSearch(string);
+    scr_RosterSearch(arg);
     update_roster = TRUE;
-  } else if (!strcasecmp(arg, "up")) {
+  } else if (!strcasecmp(subcmd, "up")) {
     scr_RosterUp();
-  } else if (!strcasecmp(arg, "down")) {
+  } else if (!strcasecmp(subcmd, "down")) {
     scr_RosterDown();
+  } else if (!strcasecmp(subcmd, "note")) {
+    roster_note(arg);
   } else
     scr_LogPrint(LPRINT_NORMAL, "Unrecognized parameter!");
+  free_arg_lst(paramlst);
 }
 
 //  setstatus(recipient, arg)
@@ -494,14 +672,14 @@
     return;
   }
 
-  if      (!strcasecmp(status, "offline"))   st = offline;
-  else if (!strcasecmp(status, "online"))    st = available;
-  else if (!strcasecmp(status, "avail"))     st = available;
-  else if (!strcasecmp(status, "away"))      st = away;
-  else if (!strcasecmp(status, "invisible")) st = invisible;
-  else if (!strcasecmp(status, "dnd"))       st = dontdisturb;
-  else if (!strcasecmp(status, "notavail"))  st = notavail;
-  else if (!strcasecmp(status, "free"))      st = freeforchat;
+  if      (!strcasecmp(status, IMSTATUS_OFFLINE))       st = offline;
+  else if (!strcasecmp(status, IMSTATUS_ONLINE))        st = available;
+  else if (!strcasecmp(status, IMSTATUS_AVAILABLE))     st = available;
+  else if (!strcasecmp(status, IMSTATUS_AWAY))          st = away;
+  else if (!strcasecmp(status, IMSTATUS_INVISIBLE))     st = invisible;
+  else if (!strcasecmp(status, IMSTATUS_DONOTDISTURB))  st = dontdisturb;
+  else if (!strcasecmp(status, IMSTATUS_NOTAVAILABLE))  st = notavail;
+  else if (!strcasecmp(status, IMSTATUS_FREE4CHAT))     st = freeforchat;
   else {
     scr_LogPrint(LPRINT_NORMAL, "Unrecognized status!");
     free_arg_lst(paramlst);
@@ -566,7 +744,7 @@
     } else {
       // Convert jid to lowercase
       char *p = jid;
-      for ( ; *p && *p != '/'; p++)
+      for ( ; *p && *p != JID_RESOURCE_SEPARATOR; p++)
         *p = tolower(*p);
       jid = jid_utf8 = to_utf8(jid);
     }
@@ -735,7 +913,7 @@
   }
 
   // We must use the bare jid in hk_message_out()
-  rp = strchr(jid, '/');
+  rp = strchr(jid, JID_RESOURCE_SEPARATOR);
   if (rp) bare_jid = g_strndup(jid, rp - jid);
   else   bare_jid = (char*)jid;
 
@@ -759,7 +937,7 @@
   if (hmsg != msg) g_free(hmsg);
 
   // Network part
-  jb_send_msg(jid, msg, ROSTER_TYPE_USER, subj);
+  jb_send_msg(jid, msg, ROSTER_TYPE_USER, subj, NULL);
 
   if (rp) g_free(bare_jid);
   return 0;
@@ -804,8 +982,8 @@
     scr_LogPrint(LPRINT_NORMAL, "Missing parameter.");
     scr_LogPrint(LPRINT_NORMAL, "Please read the manual before using "
                  "the /msay command.");
-    scr_LogPrint(LPRINT_NORMAL, "(Use \"/msay begin\" to enter "
-                 "multi-line mode...)");
+    scr_LogPrint(LPRINT_NORMAL, "(Use \"%s begin\" to enter "
+                 "multi-line mode...)", mkcmdstr("msay"));
     free_arg_lst(paramlst);
     return;
   }
@@ -841,10 +1019,11 @@
 
     scr_LogPrint(LPRINT_NORMAL, "Entered %smulti-line message mode.",
                  verbat ? "VERBATIM " : "");
-    scr_LogPrint(LPRINT_NORMAL, "Select a buddy and use \"/msay send\" "
-                 "when your message is ready.");
+    scr_LogPrint(LPRINT_NORMAL, "Select a buddy and use \"%s send\" "
+                 "when your message is ready.", mkcmdstr("msay"));
     if (verbat)
-      scr_LogPrint(LPRINT_NORMAL, "Use \"/msay abort\" to abort this mode.");
+      scr_LogPrint(LPRINT_NORMAL, "Use \"%s abort\" to abort this mode.",
+                   mkcmdstr("msay"));
     g_free(subj_utf8);
     return;
   } else if (strcasecmp(subcmd, "send") && strcasecmp(subcmd, "send_to")) {
@@ -856,7 +1035,7 @@
 
   if (!scr_get_multimode()) {
     scr_LogPrint(LPRINT_NORMAL, "No message to send.  "
-                 "Use \"/msay begin\" first.");
+                 "Use \"%s begin\" first.", mkcmdstr("msay"));
     return;
   }
 
@@ -1036,8 +1215,10 @@
   } else if (!strcasecmp(subcmd, "down")) {
     buffer_updown(1, arg);
   } else if (!strcasecmp(subcmd, "search_backward")) {
+    strip_arg_special_chars(arg);
     buffer_search(-1, arg);
   } else if (!strcasecmp(subcmd, "search_forward")) {
+    strip_arg_special_chars(arg);
     buffer_search(1, arg);
   } else if (!strcasecmp(subcmd, "date")) {
     buffer_date(arg);
@@ -1050,7 +1231,7 @@
   free_arg_lst(paramlst);
 }
 
-static void do_clear(char *arg)    // Alias for "/buffer clear"
+static void do_clear(char *arg)    // Alias for "buffer clear"
 {
   do_buffer("clear");
 }
@@ -1144,8 +1325,22 @@
                  type == ROSTER_TYPE_GROUP ? "group" :
                  (type == ROSTER_TYPE_SPECIAL ? "special" : "unknown"));
   }
+  g_free(buffer);
 
-  g_free(buffer);
+  // Tell the user if this item has an annotation.
+  if (type == ROSTER_TYPE_USER ||
+      type == ROSTER_TYPE_ROOM ||
+      type == ROSTER_TYPE_AGENT) {
+    struct annotation *note = jb_get_storage_rosternotes(jid, TRUE);
+    if (note) {
+      // We do not display the note, we just tell the user.
+      g_free(note->text);
+      g_free(note->jid);
+      g_free(note);
+      scr_WriteIncomingMessage(jid, "(This item has an annotation)", 0,
+                               HBB_PREFIX_INFO);
+    }
+  }
 }
 
 // room_names() is a variation of do_info(), for chatrooms only
@@ -1499,7 +1694,6 @@
   char **paramlst;
   char *roomname, *nick, *pass;
   char *roomname_tmp = NULL;
-  char *tmpnick = NULL;
   char *pass_utf8;
 
   paramlst = split_arg(arg, 3, 0); // roomid, nickname, password
@@ -1533,25 +1727,15 @@
   }
 
   // If no nickname is provided with the /join command,
-  // we try the "nickname" option, then the username part of the jid.
-  if (!nick || !*nick) {
-    nick = (char*)settings_opt_get("nickname");
-    if (!nick) {
-      nick = (char*)settings_opt_get("username");
-      if (nick && (strchr(nick, '@') > nick)) {
-        char *p;
-        nick = tmpnick = g_strdup(nick);
-        p = strchr(nick, '@');
-        *p = 0;
-      }
-    }
-  } else {
-    nick = tmpnick = to_utf8(nick);
-  }
+  // we try to get a default nickname.
+  if (!nick || !*nick)
+    nick = default_muc_nickname();
+  else
+    nick = to_utf8(nick);
   // If we still have no nickname, give up
   if (!nick || !*nick) {
     scr_LogPrint(LPRINT_NORMAL, "Please specify a nickname.");
-    g_free(tmpnick);
+    g_free(nick);
     free_arg_lst(paramlst);
     return;
   }
@@ -1563,7 +1747,7 @@
   scr_LogPrint(LPRINT_LOGNORM, "Sent a join request to <%s>...", roomname);
 
   g_free(roomname_tmp);
-  g_free(tmpnick);
+  g_free(nick);
   g_free(pass_utf8);
   buddylist_build();
   update_roster = TRUE;
@@ -1843,8 +2027,8 @@
 
   arg = to_utf8(arg);
   // Set the topic
-  msg = g_strdup_printf("/me has set the topic to: %s", arg);
-  jb_send_msg(buddy_getjid(bud), msg, ROSTER_TYPE_ROOM, arg);
+  msg = g_strdup_printf("%s has set the topic to: %s", mkcmdstr("me"), arg);
+  jb_send_msg(buddy_getjid(bud), msg, ROSTER_TYPE_ROOM, arg, NULL);
   g_free(arg);
   g_free(msg);
 }
@@ -1960,6 +2144,42 @@
   free_arg_lst(paramlst);
 }
 
+static void room_bookmark(gpointer bud, char *arg)
+{
+  const char *roomid;
+  const char *name = NULL, *nick = NULL;
+  enum { bm_add = 0, bm_del = 1 } action = 0;
+  int autojoin = 0;
+
+  if (arg && *arg) {
+    // /room bookmark [add|del] [[+|-]autojoin]
+    char **paramlst;
+    char **pp;
+
+    paramlst = split_arg(arg, 2, 0); // At most 2 parameters
+    for (pp = paramlst; *pp; pp++) {
+      if (!strcasecmp(*pp, "add"))
+        action = bm_add;
+      else if (!strcasecmp(*pp, "del"))
+        action = bm_del;
+      else if (!strcasecmp(*pp, "-autojoin"))
+        autojoin = 0;
+      else if (!strcasecmp(*pp, "+autojoin") || !strcasecmp(*pp, "autojoin"))
+        autojoin = 1;
+    }
+    free_arg_lst(paramlst);
+  }
+
+  roomid = buddy_getjid(bud);
+
+  if (action == bm_add) {
+    name = buddy_getname(bud);
+    nick = buddy_getnickname(bud);
+  }
+
+  jb_set_storage_bookmark(roomid, name, nick, NULL, autojoin);
+}
+
 static void do_room(char *arg)
 {
   char **paramlst;
@@ -2036,6 +2256,9 @@
   } else if (!strcasecmp(subcmd, "whois"))  {
     if ((arg = check_room_subcommand(arg, TRUE, bud)) != NULL)
       room_whois(bud, arg, TRUE);
+  } else if (!strcasecmp(subcmd, "bookmark"))  {
+    if ((arg = check_room_subcommand(arg, FALSE, bud)) != NULL)
+      room_bookmark(bud, arg);
   } else {
     scr_LogPrint(LPRINT_NORMAL, "Unrecognized parameter!");
   }
@@ -2149,6 +2372,10 @@
       numtype = iqreq_version;
     else if (!strcasecmp(type, "time"))
       numtype = iqreq_time;
+    else if (!strcasecmp(type, "last"))
+      numtype = iqreq_last;
+    else if (!strcasecmp(type, "vcard"))
+      numtype = iqreq_vcard;
     else if (!strcasecmp(type, "show_list")) {
       // Undocumented command, for debugging purposes only
       jb_iqs_display_list();
@@ -2159,7 +2386,7 @@
 
   if (!type || !numtype) {
     scr_LogPrint(LPRINT_NORMAL,
-                 "Please specify a query type (version, time).");
+                 "Please specify a query type (version, time...).");
     free_arg_lst(paramlst);
     return;
   }
@@ -2176,7 +2403,7 @@
     } else {
       // Convert jid to lowercase
       char *p;
-      for (p = jid; *p && *p != '/'; p++)
+      for (p = jid; *p && *p != JID_RESOURCE_SEPARATOR; p++)
         *p = tolower(*p);
       jid = jid_utf8 = to_utf8(jid);
     }
@@ -2192,6 +2419,8 @@
     switch (numtype) {
       case iqreq_version:
       case iqreq_time:
+      case iqreq_last:
+      case iqreq_vcard:
           jb_request(jid, numtype);
           break;
       default:
@@ -2269,6 +2498,7 @@
 static void do_disconnect(char *arg)
 {
   jb_disconnect();
+  AutoConnection = false;
 }
 
 static void do_help(char *arg)
--- a/mcabber/src/hooks.c	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/src/hooks.c	Sun Nov 19 11:51:14 2006 +0100
@@ -57,10 +57,10 @@
       bmsg = g_strdup_printf("<%s> %s", resname, msg);
     }
     wmsg = bmsg;
-    if (!strncmp(msg, "/me ", 4))
+    if (!strncmp(msg, mkcmdstr("me "), strlen(mkcmdstr("me "))))
       wmsg = mmsg = g_strdup_printf("*%s %s", resname, msg+4);
   } else {
-    if (!strncmp(msg, "/me ", 4))
+    if (!strncmp(msg, mkcmdstr("me "), strlen(mkcmdstr("me "))))
       wmsg = mmsg = g_strdup_printf("*%s %s", jid, msg+4);
     else
       wmsg = (char*) msg;
@@ -92,7 +92,7 @@
         wmsg = bmsg = g_strdup(msg);
       } else {
         wmsg = bmsg = g_strdup_printf("PRIV#<%s> %s", resname, msg);
-        if (!strncmp(msg, "/me ", 4))
+        if (!strncmp(msg, mkcmdstr("me "), strlen(mkcmdstr("me "))))
           wmsg = mmsg = g_strdup_printf("PRIV#*%s %s", resname, msg+4);
       }
     } else {
@@ -148,13 +148,12 @@
     scr_Beep();
   }
 
-  // We need to rebuild the list if the sender is unknown or
+  // We need to update the roster if the sender is unknown or
   // if the sender is offline/invisible and hide_offline_buddies is set
   if (new_guy ||
       (buddy_getstatus(roster_usr->data, NULL) == offline &&
        buddylist_get_hide_offline_buddies()))
   {
-    buddylist_build();
     update_roster = TRUE;
   }
 
@@ -174,7 +173,7 @@
     wmsg = bmsg = g_strdup_printf("PRIV#<%s> %s", nick, msg);
   } else {
     wmsg = (char*)msg;
-    if (!strncmp(msg, "/me ", 4)) {
+    if (!strncmp(msg, mkcmdstr("me "), strlen(mkcmdstr("me ")))) {
       const char *myid = settings_opt_get("username");
       if (myid)
         wmsg = mmsg = g_strdup_printf("*%s %s", settings_opt_get("username"),
--- a/mcabber/src/jab_iq.c	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/src/jab_iq.c	Sun Nov 19 11:51:14 2006 +0100
@@ -34,8 +34,24 @@
 #include "hbuf.h"
 
 
+// Bookmarks for IQ:private storage
+xmlnode bookmarks;
+// Roster notes for IQ:private storage
+xmlnode rosternotes;
+
 static GSList *iqs_list;
 
+// Enum for vCard attributes
+enum vcard_attr {
+  vcard_home    = 1<<0,
+  vcard_work    = 1<<1,
+  vcard_postal  = 1<<2,
+  vcard_voice   = 1<<3,
+  vcard_fax     = 1<<4,
+  vcard_cell    = 1<<5,
+  vcard_inet    = 1<<6,
+  vcard_pref    = 1<<7,
+};
 
 //  iqs_new(type, namespace, prefix, timeout)
 // Create a query (GET, SET) IQ structure.  This function should not be used
@@ -209,8 +225,9 @@
     if (!name)
       name = cleanalias;
 
-    // Tricky... :-\  My guess is that if there is no '@', this is an agent
-    if (strchr(cleanalias, '@'))
+    // Tricky... :-\  My guess is that if there is no JID_DOMAIN_SEPARATOR,
+    // this is an agent.
+    if (strchr(cleanalias, JID_DOMAIN_SEPARATOR))
       roster_type = ROSTER_TYPE_USER;
     else
       roster_type = ROSTER_TYPE_AGENT;
@@ -253,7 +270,7 @@
   scr_LogPrint(LPRINT_LOGNORM, "%s", buf);
 
   // bjid should now really be the "bare JID", let's strip the resource
-  p = strchr(bjid, '/');
+  p = strchr(bjid, JID_RESOURCE_SEPARATOR);
   if (p) *p = '\0';
 
   scr_WriteIncomingMessage(bjid, buf, 0, HBB_PREFIX_INFO);
@@ -317,7 +334,7 @@
   scr_LogPrint(LPRINT_LOGNORM, "%s", buf);
 
   // bjid should now really be the "bare JID", let's strip the resource
-  p = strchr(bjid, '/');
+  p = strchr(bjid, JID_RESOURCE_SEPARATOR);
   if (p) *p = '\0';
 
   scr_WriteIncomingMessage(bjid, buf, 0, HBB_PREFIX_INFO);
@@ -354,6 +371,374 @@
   jab_send(jc, iqn->xmldata);
 }
 
+static void iqscallback_last(eviqs *iqp, xmlnode xml_result, guint iqcontext)
+{
+  xmlnode ansqry;
+  char *p;
+  char *bjid;
+  char *buf;
+
+  // Leave now if we cannot process xml_result
+  if (!xml_result || iqcontext) return;
+
+  ansqry = xmlnode_get_tag(xml_result, "query");
+  if (!ansqry) {
+    scr_LogPrint(LPRINT_LOGNORM, "Invalid IQ:last result!");
+    return;
+  }
+  // Display IQ result sender...
+  p = xmlnode_get_attrib(xml_result, "from");
+  if (!p) {
+    scr_LogPrint(LPRINT_LOGNORM, "Invalid IQ:last result (no sender name).");
+    return;
+  }
+  bjid = p;
+
+  buf = g_strdup_printf("Received IQ:last result from <%s>", bjid);
+  scr_LogPrint(LPRINT_LOGNORM, "%s", buf);
+
+  // bjid should now really be the "bare JID", let's strip the resource
+  p = strchr(bjid, JID_RESOURCE_SEPARATOR);
+  if (p) *p = '\0';
+
+  scr_WriteIncomingMessage(bjid, buf, 0, HBB_PREFIX_INFO);
+  g_free(buf);
+
+  // Get result data...
+  p = xmlnode_get_attrib(ansqry, "seconds");
+  if (p) {
+    long int s;
+    GString *sbuf;
+    sbuf = g_string_new("Idle time: ");
+    s = atol(p);
+    // Days
+    if (s > 86400L) {
+      g_string_append_printf(sbuf, "%ldd ", s/86400L);
+      s %= 86400L;
+    }
+    // hh:mm:ss
+    g_string_append_printf(sbuf, "%02ld:", s/3600L);
+    s %= 3600L;
+    g_string_append_printf(sbuf, "%02ld:%02ld", s/60L, s%60L);
+    scr_WriteIncomingMessage(bjid, sbuf->str, 0, HBB_PREFIX_NONE);
+    g_string_free(sbuf, TRUE);
+  } else {
+    scr_WriteIncomingMessage(bjid, "No idle time reported.",
+                             0, HBB_PREFIX_NONE);
+  }
+  p = xmlnode_get_data(ansqry);
+  if (p) {
+    buf = g_strdup_printf("Status message: %s", p);
+    scr_WriteIncomingMessage(bjid, buf, 0, HBB_PREFIX_INFO);
+    g_free(buf);
+  }
+}
+
+void request_last(const char *fulljid)
+{
+  eviqs *iqn;
+
+  iqn = iqs_new(JPACKET__GET, NS_LAST, "last", IQS_DEFAULT_TIMEOUT);
+  xmlnode_put_attrib(iqn->xmldata, "to", fulljid);
+  iqn->callback = &iqscallback_last;
+  jab_send(jc, iqn->xmldata);
+}
+
+static void display_vcard_item(const char *bjid, const char *label,
+                               enum vcard_attr vcard_attrib, const char *text)
+{
+  char *buf;
+
+  if (!text || !bjid || !label)
+    return;
+
+  buf = g_strdup_printf("%s: %s%s%s%s%s%s%s%s%s%s", label,
+                        (vcard_attrib & vcard_home ? "[home]" : ""),
+                        (vcard_attrib & vcard_work ? "[work]" : ""),
+                        (vcard_attrib & vcard_postal ? "[postal]" : ""),
+                        (vcard_attrib & vcard_voice ? "[voice]" : ""),
+                        (vcard_attrib & vcard_fax  ? "[fax]"  : ""),
+                        (vcard_attrib & vcard_cell ? "[cell]" : ""),
+                        (vcard_attrib & vcard_inet ? "[inet]" : ""),
+                        (vcard_attrib & vcard_pref ? "[pref]" : ""),
+                        (vcard_attrib ? " " : ""),
+                        text);
+  scr_WriteIncomingMessage(bjid, buf, 0, HBB_PREFIX_NONE);
+  g_free(buf);
+}
+
+static void handle_vcard_node(const char *barejid, xmlnode vcardnode)
+{
+  xmlnode x;
+  const char *p;
+
+  x = xmlnode_get_firstchild(vcardnode);
+  for ( ; x; x = xmlnode_get_nextsibling(x)) {
+    const char *data;
+    enum vcard_attr vcard_attrib = 0;
+
+    p = xmlnode_get_name(x);
+    data = xmlnode_get_data(x);
+    if (!p || !data)
+      continue;
+
+    if (!strcmp(p, "FN"))
+      display_vcard_item(barejid, "Name", vcard_attrib, data);
+    else if (!strcmp(p, "NICKNAME"))
+      display_vcard_item(barejid, "Nickname", vcard_attrib, data);
+    else if (!strcmp(p, "URL"))
+      display_vcard_item(barejid, "URL", vcard_attrib, data);
+    else if (!strcmp(p, "BDAY"))
+      display_vcard_item(barejid, "Birthday", vcard_attrib, data);
+    else if (!strcmp(p, "TZ"))
+      display_vcard_item(barejid, "Timezone", vcard_attrib, data);
+    else if (!strcmp(p, "TITLE"))
+      display_vcard_item(barejid, "Title", vcard_attrib, data);
+    else if (!strcmp(p, "ROLE"))
+      display_vcard_item(barejid, "Role", vcard_attrib, data);
+    else if (!strcmp(p, "DESC"))
+      display_vcard_item(barejid, "Comment", vcard_attrib, data);
+    else if (!strcmp(p, "N")) {
+      data = xmlnode_get_tag_data(x, "FAMILY");
+      display_vcard_item(barejid, "Family Name", vcard_attrib, data);
+      data = xmlnode_get_tag_data(x, "GIVEN");
+      display_vcard_item(barejid, "Given Name", vcard_attrib, data);
+      data = xmlnode_get_tag_data(x, "MIDDLE");
+      display_vcard_item(barejid, "Middle Name", vcard_attrib, data);
+    } else if (!strcmp(p, "ORG")) {
+      data = xmlnode_get_tag_data(x, "ORGNAME");
+      display_vcard_item(barejid, "Organisation name", vcard_attrib, data);
+      data = xmlnode_get_tag_data(x, "ORGUNIT");
+      display_vcard_item(barejid, "Organisation unit", vcard_attrib, data);
+    } else {
+      // The HOME, WORK and PREF attributes are common to the remaining fields
+      // (ADR, TEL & EMAIL)
+      if (xmlnode_get_tag(x, "HOME"))
+        vcard_attrib |= vcard_home;
+      if (xmlnode_get_tag(x, "WORK"))
+        vcard_attrib |= vcard_work;
+      if (xmlnode_get_tag(x, "PREF"))
+        vcard_attrib |= vcard_pref;
+      if (!strcmp(p, "ADR")) {          // Address
+        if (xmlnode_get_tag(x, "POSTAL"))
+          vcard_attrib |= vcard_postal;
+        data = xmlnode_get_tag_data(x, "EXTADD");
+        display_vcard_item(barejid, "Addr (ext)", vcard_attrib, data);
+        data = xmlnode_get_tag_data(x, "STREET");
+        display_vcard_item(barejid, "Street", vcard_attrib, data);
+        data = xmlnode_get_tag_data(x, "LOCALITY");
+        display_vcard_item(barejid, "Locality", vcard_attrib, data);
+        data = xmlnode_get_tag_data(x, "REGION");
+        display_vcard_item(barejid, "Region", vcard_attrib, data);
+        data = xmlnode_get_tag_data(x, "PCODE");
+        display_vcard_item(barejid, "Postal code", vcard_attrib, data);
+        data = xmlnode_get_tag_data(x, "CTRY");
+        display_vcard_item(barejid, "Country", vcard_attrib, data);
+      } else if (!strcmp(p, "TEL")) {   // Telephone
+        data = xmlnode_get_tag_data(x, "NUMBER");
+        if (data) {
+          if (xmlnode_get_tag(x, "VOICE"))
+            vcard_attrib |= vcard_voice;
+          if (xmlnode_get_tag(x, "FAX"))
+            vcard_attrib |= vcard_fax;
+          if (xmlnode_get_tag(x, "CELL"))
+            vcard_attrib |= vcard_cell;
+          display_vcard_item(barejid, "Phone", vcard_attrib, data);
+        }
+      } else if (!strcmp(p, "EMAIL")) { // Email
+        if (xmlnode_get_tag(x, "INTERNET"))
+          vcard_attrib |= vcard_inet;
+        data = xmlnode_get_tag_data(x, "USERID");
+        display_vcard_item(barejid, "Email", vcard_attrib, data);
+      }
+    }
+  }
+}
+
+static void iqscallback_vcard(eviqs *iqp, xmlnode xml_result, guint iqcontext)
+{
+  xmlnode ansqry;
+  char *p;
+  char *bjid;
+  char *buf;
+
+  // Leave now if we cannot process xml_result
+  if (!xml_result || iqcontext) return;
+
+  // Display IQ result sender...
+  p = xmlnode_get_attrib(xml_result, "from");
+  if (!p) {
+    scr_LogPrint(LPRINT_LOGNORM, "Invalid IQ:vCard result (no sender name).");
+    return;
+  }
+  bjid = p;
+
+  buf = g_strdup_printf("Received IQ:vCard result from <%s>", bjid);
+  scr_LogPrint(LPRINT_LOGNORM, "%s", buf);
+
+  // Get the vCard node
+  ansqry = xmlnode_get_tag(xml_result, "vCard");
+  if (!ansqry) {
+    scr_LogPrint(LPRINT_LOGNORM, "Empty IQ:vCard result!");
+    return;
+  }
+
+  // bjid should really be the "bare JID", let's strip the resource
+  p = strchr(bjid, JID_RESOURCE_SEPARATOR);
+  if (p) *p = '\0';
+
+  scr_WriteIncomingMessage(bjid, buf, 0, HBB_PREFIX_INFO);
+  g_free(buf);
+
+  // Get result data...
+  handle_vcard_node(bjid, ansqry);
+}
+
+void request_vcard(const char *jid)
+{
+  eviqs *iqn;
+  char *barejid;
+
+  barejid = jidtodisp(jid);
+
+  // Create a new IQ structure.  We use NULL for the namespace because
+  // we'll have to use a special tag, not the usual "query" one.
+  iqn = iqs_new(JPACKET__GET, NULL, "vcard", IQS_DEFAULT_TIMEOUT);
+  xmlnode_put_attrib(iqn->xmldata, "to", barejid);
+  // Remove the useless <query/> tag, and insert a vCard one.
+  xmlnode_hide(xmlnode_get_tag(iqn->xmldata, "query"));
+  xmlnode_put_attrib(xmlnode_insert_tag(iqn->xmldata, "vCard"),
+                     "xmlns", NS_VCARD);
+  iqn->callback = &iqscallback_vcard;
+  jab_send(jc, iqn->xmldata);
+
+  g_free(barejid);
+}
+
+static void storage_bookmarks_parse_conference(xmlnode xmldata)
+{
+  const char *jid, *name, *autojoin;
+  char *bjid;
+  GSList *room_elt;
+
+  jid = xmlnode_get_attrib(xmldata, "jid");
+  if (!jid)
+    return;
+  name = xmlnode_get_attrib(xmldata, "name");
+  autojoin = xmlnode_get_attrib(xmldata, "autojoin");
+
+  bjid = jidtodisp(jid); // Bare jid
+
+  // Make sure this is a room (it can be a conversion user->room)
+  room_elt = roster_find(bjid, jidsearch, 0);
+  if (!room_elt) {
+    room_elt = roster_add_user(bjid, name, NULL, ROSTER_TYPE_ROOM, sub_none);
+  } else {
+    buddy_settype(room_elt->data, ROSTER_TYPE_ROOM);
+    /*
+    // If the name is available, should we use it?
+    // I don't think so, it would be confusing because this item is already
+    // in the roster.
+    if (name)
+      buddy_setname(room_elt->data, name);
+    */
+  }
+
+  // Is autojoin set?
+  // If it is, we'll look up for more information (nick? password?) and
+  // try to join the room.
+  if (autojoin && !strcmp(autojoin, "1")) {
+    char *nick, *passwd;
+    char *tmpnick = NULL;
+    nick = xmlnode_get_tag_data(xmldata, "nick");
+    passwd = xmlnode_get_tag_data(xmldata, "password");
+    if (!nick || !*nick)
+      nick = tmpnick = default_muc_nickname();
+    // Let's join now
+    scr_LogPrint(LPRINT_LOGNORM, "Auto-join bookmark <%s>", bjid);
+    jb_room_join(bjid, nick, passwd);
+    g_free(tmpnick);
+  }
+  g_free(bjid);
+}
+
+static void iqscallback_storage_bookmarks(eviqs *iqp, xmlnode xml_result,
+                                          guint iqcontext)
+{
+  xmlnode x, ansqry;
+  char *p;
+
+  // Leave now if we cannot process xml_result
+  if (!xml_result || iqcontext) return;
+
+  ansqry = xmlnode_get_tag(xml_result, "query");
+  ansqry = xmlnode_get_tag(ansqry, "storage");
+  if (!ansqry) {
+    scr_LogPrint(LPRINT_LOG, "Invalid IQ:private result! (storage:bookmarks)");
+    return;
+  }
+
+  // Walk through the storage tags
+  x = xmlnode_get_firstchild(ansqry);
+  for ( ; x; x = xmlnode_get_nextsibling(x)) {
+    p = xmlnode_get_name(x);
+    // If the current node is a conference item, parse it and update the roster
+    if (p && !strcmp(p, "conference"))
+      storage_bookmarks_parse_conference(x);
+  }
+  // Copy the bookmarks node
+  xmlnode_free(bookmarks);
+  bookmarks = xmlnode_dup(ansqry);
+}
+
+static void request_storage_bookmarks(void)
+{
+  eviqs *iqn;
+  xmlnode x;
+
+  iqn = iqs_new(JPACKET__GET, NS_PRIVATE, "storage", IQS_DEFAULT_TIMEOUT);
+
+  x = xmlnode_insert_tag(xmlnode_get_tag(iqn->xmldata, "query"), "storage");
+  xmlnode_put_attrib(x, "xmlns", "storage:bookmarks");
+
+  iqn->callback = &iqscallback_storage_bookmarks;
+  jab_send(jc, iqn->xmldata);
+}
+
+static void iqscallback_storage_rosternotes(eviqs *iqp, xmlnode xml_result,
+                                            guint iqcontext)
+{
+  xmlnode ansqry;
+
+  // Leave now if we cannot process xml_result
+  if (!xml_result || iqcontext) return;
+
+  ansqry = xmlnode_get_tag(xml_result, "query");
+  ansqry = xmlnode_get_tag(ansqry, "storage");
+  if (!ansqry) {
+    scr_LogPrint(LPRINT_LOG, "Invalid IQ:private result! "
+                 "(storage:rosternotes)");
+    return;
+  }
+  // Copy the rosternotes node
+  xmlnode_free(rosternotes);
+  rosternotes = xmlnode_dup(ansqry);
+}
+
+static void request_storage_rosternotes(void)
+{
+  eviqs *iqn;
+  xmlnode x;
+
+  iqn = iqs_new(JPACKET__GET, NS_PRIVATE, "storage", IQS_DEFAULT_TIMEOUT);
+
+  x = xmlnode_insert_tag(xmlnode_get_tag(iqn->xmldata, "query"), "storage");
+  xmlnode_put_attrib(x, "xmlns", "storage:rosternotes");
+
+  iqn->callback = &iqscallback_storage_rosternotes;
+  jab_send(jc, iqn->xmldata);
+}
+
 void iqscallback_auth(eviqs *iqp, xmlnode xml_result)
 {
   if (jstate == STATE_GETAUTH) {
@@ -372,6 +757,8 @@
     jstate = STATE_SENDAUTH;
   } else if (jstate == STATE_SENDAUTH) {
     request_roster();
+    request_storage_bookmarks();
+    request_storage_rosternotes();
     jstate = STATE_LOGGED;
   }
 }
@@ -391,19 +778,6 @@
   if (!iqs_callback(id, xmldata, IQS_CONTEXT_RESULT))
     return;
 
-  /*
-  if (!strcmp(id, "VCARDreq")) {
-    x = xmlnode_get_firstchild(xmldata);
-    if (!x) x = xmldata;
-
-    scr_LogPrint(LPRINT_LOGNORM, "Got VCARD");    // TODO
-    return;
-  } else if (!strcmp(id, "versionreq")) {
-    scr_LogPrint(LPRINT_LOGNORM, "Got version");  // TODO
-    return;
-  }
-  */
-
   x = xmlnode_get_tag(xmldata, "query");
   if (!x) return;
 
@@ -416,23 +790,52 @@
     // Post-login stuff
     // Usually we request the roster only at connection time
     // so we should be there only once.  (That's ugly, however)
-    jb_setstatus(available, NULL, NULL);
+    jb_setprevstatus();
   }
 }
 
+static void handle_iq_disco_info(jconn conn, char *from, const char *id,
+                                 xmlnode xmldata)
+{
+  xmlnode x, y;
+  xmlnode myquery;
+
+  x = jutil_iqnew(JPACKET__RESULT, NS_DISCO_INFO);
+  xmlnode_put_attrib(x, "id", id);
+  xmlnode_put_attrib(x, "to", xmlnode_get_attrib(xmldata, "from"));
+  myquery = xmlnode_get_tag(x, "query");
+
+  y = xmlnode_insert_tag(myquery, "identity");
+  xmlnode_put_attrib(y, "category", "client");
+  xmlnode_put_attrib(y, "type", "pc");
+  xmlnode_put_attrib(y, "name", PACKAGE_NAME);
+
+  xmlnode_put_attrib(xmlnode_insert_tag(myquery, "feature"),
+                     "var", NS_DISCO_INFO);
+  xmlnode_put_attrib(xmlnode_insert_tag(myquery, "feature"),
+                     "var", NS_MUC);
+  xmlnode_put_attrib(xmlnode_insert_tag(myquery, "feature"),
+                     "var", NS_CHATSTATES);
+  xmlnode_put_attrib(xmlnode_insert_tag(myquery, "feature"),
+                     "var", NS_TIME);
+  xmlnode_put_attrib(xmlnode_insert_tag(myquery, "feature"),
+                     "var", NS_VERSION);
+
+  jab_send(jc, x);
+  xmlnode_free(x);
+}
+
 static void handle_iq_version(jconn conn, char *from, const char *id,
                               xmlnode xmldata)
 {
-  xmlnode senderquery, x;
+  xmlnode x;
   xmlnode myquery;
   char *os = NULL;
   char *ver = mcabber_version();
 
-  // "from" has already been converted to user locale
   scr_LogPrint(LPRINT_LOGNORM, "Received an IQ version request from <%s>",
                from);
 
-  senderquery = xmlnode_get_tag(xmldata, "query");
   if (!settings_opt_get_int("iq_version_hide_os")) {
     struct utsname osinfo;
     uname(&osinfo);
@@ -461,7 +864,7 @@
 static void handle_iq_time(jconn conn, char *from, const char *id,
                               xmlnode xmldata)
 {
-  xmlnode senderquery, x;
+  xmlnode x;
   xmlnode myquery;
   char *buf, *utf8_buf;
   time_t now_t;
@@ -469,11 +872,9 @@
 
   time(&now_t);
 
-  // "from" has already been converted to user locale
   scr_LogPrint(LPRINT_LOGNORM, "Received an IQ time request from <%s>", from);
 
   buf = g_new0(char, 512);
-  senderquery = xmlnode_get_tag(xmldata, "query");
 
   x = jutil_iqnew(JPACKET__RESULT, NS_TIME);
   xmlnode_put_attrib(x, "id", id);
@@ -519,7 +920,9 @@
 
   x = xmlnode_get_tag(xmldata, "query");
   ns = xmlnode_get_attrib(x, "xmlns");
-  if (ns && !strcmp(ns, NS_VERSION)) {
+  if (ns && !strcmp(ns, NS_DISCO_INFO)) {
+    handle_iq_disco_info(conn, from, id, xmldata);
+  } else if (ns && !strcmp(ns, NS_VERSION)) {
     handle_iq_version(conn, from, id, xmldata);
   } else if (ns && !strcmp(ns, NS_TIME)) {
     handle_iq_time(conn, from, id, xmldata);
@@ -608,4 +1011,36 @@
   }
 }
 
+//  send_storage_bookmarks()
+// Send the current bookmarks node to update the server.
+// Note: the sender should check we're online.
+void send_storage_bookmarks(void)
+{
+  eviqs *iqn;
+
+  if (!bookmarks) return;
+
+  iqn = iqs_new(JPACKET__SET, NS_PRIVATE, "storage", IQS_DEFAULT_TIMEOUT);
+  xmlnode_insert_node(xmlnode_get_tag(iqn->xmldata, "query"), bookmarks);
+
+  jab_send(jc, iqn->xmldata);
+  iqs_del(iqn->id); // XXX
+}
+
+//  send_storage_rosternotes()
+// Send the current rosternotes node to update the server.
+// Note: the sender should check we're online.
+void send_storage_rosternotes(void)
+{
+  eviqs *iqn;
+
+  if (!rosternotes) return;
+
+  iqn = iqs_new(JPACKET__SET, NS_PRIVATE, "storage", IQS_DEFAULT_TIMEOUT);
+  xmlnode_insert_node(xmlnode_get_tag(iqn->xmldata, "query"), rosternotes);
+
+  jab_send(jc, iqn->xmldata);
+  iqs_del(iqn->id); // XXX
+}
+
 /* vim: set expandtab cindent cinoptions=>2\:2(0:  For Vim users... */
--- a/mcabber/src/jab_priv.h	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/src/jab_priv.h	Sun Nov 19 11:51:14 2006 +0100
@@ -29,6 +29,7 @@
 #define IQS_CONTEXT_ERROR   2U
 
 extern enum enum_jstate jstate;
+extern xmlnode bookmarks, rosternotes;
 
 extern char *mcabber_version(void);
 
@@ -43,6 +44,10 @@
 void iqscallback_auth(eviqs *iqp, xmlnode xml_result);
 void request_version(const char *fulljid);
 void request_time(const char *fulljid);
+void request_last(const char *fulljid);
+void request_vcard(const char *barejid);
+void send_storage_bookmarks(void);
+void send_storage_rosternotes(void);
 
 #endif /* __JAB_PRIV_H__ */
 
--- a/mcabber/src/jabglue.c	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/src/jabglue.c	Sun Nov 19 11:51:14 2006 +0100
@@ -36,7 +36,10 @@
 #define JABBERPORT      5222
 #define JABBERSSLPORT   5223
 
+#define RECONNECTION_TIMEOUT    60L
+
 jconn jc;
+guint AutoConnection;
 enum enum_jstate jstate;
 
 char imstatus2char[imstatus_size+1] = {
@@ -46,11 +49,13 @@
 static time_t LastPingTime;
 static unsigned int KeepaliveDelay;
 static enum imstatus mystatus = offline;
+static enum imstatus mywantedstatus = available;
 static gchar *mystatusmsg;
 static unsigned char online;
 
 static void statehandler(jconn, int);
 static void packethandler(jconn, jpacket);
+void handle_state_events(char* from, xmlnode xmldata);
 
 static void logger(jconn j, int io, const char *buf)
 {
@@ -67,7 +72,7 @@
 
   alias = g_strdup(jid);
 
-  if ((ptr = strchr(alias, '/')) != NULL) {
+  if ((ptr = strchr(alias, JID_RESOURCE_SEPARATOR)) != NULL) {
     *ptr = 0;
   }
   return alias;
@@ -79,11 +84,11 @@
   char *jid = g_new(char, 3 +
                     strlen(username) + strlen(servername) + strlen(resource));
   strcpy(jid, username);
-  if (!strchr(jid, '@')) {
-    strcat(jid, "@");
+  if (!strchr(jid, JID_DOMAIN_SEPARATOR)) {
+    strcat(jid, JID_DOMAIN_SEPARATORSTR);
     strcat(jid, servername);
   }
-  strcat(jid, "/");
+  strcat(jid, JID_RESOURCE_SEPARATORSTR);
   strcat(jid, resource);
   return jid;
 }
@@ -133,6 +138,11 @@
     jb_setstatus(offline, NULL, "");
     // End the XML flow
     jb_send_raw("</stream:stream>");
+    /*
+    // Free status message
+    g_free(mystatusmsg);
+    mystatusmsg = NULL;
+    */
   }
 
   // Announce it to the user
@@ -165,16 +175,48 @@
   KeepaliveDelay = delay;
 }
 
+//  check_connection()
+// Check if we've been disconnected for a while (predefined timeout),
+// and if so try to reconnect.
+static void check_connection(void)
+{
+  static time_t disconnection_timestamp = 0L;
+  time_t now;
+
+  // Maybe we're voluntarily offline...
+  if (!AutoConnection)
+    return;
+
+  // Are we totally disconnected?
+  if (jc && jc->state != JCONN_STATE_OFF) {
+    disconnection_timestamp = 0L;
+    return;
+  }
+
+  time(&now);
+  if (!disconnection_timestamp) {
+    disconnection_timestamp = now;
+    return;
+  }
+
+  // If the reconnection_timeout is reached, try to reconnect.
+  if (now > disconnection_timestamp + RECONNECTION_TIMEOUT) {
+    mcabber_connect();
+    disconnection_timestamp = 0L;
+  }
+}
+
 void jb_main()
 {
   time_t now;
   fd_set fds;
-  long autoaway_timeout;
+  long timeout;
   struct timeval tv;
-  static time_t last_eviqs_check = 0;
+  static time_t last_eviqs_check = 0L;
 
   if (!online) {
     safe_usleep(10000);
+    check_connection();
     return;
   }
 
@@ -201,11 +243,20 @@
     }
   }
 
-  autoaway_timeout = scr_GetAutoAwayTimeout(now);
-  if (tv.tv_sec > autoaway_timeout) {
-    tv.tv_sec = autoaway_timeout;
+  // Check auto-away timeout
+  timeout = scr_GetAutoAwayTimeout(now);
+  if (tv.tv_sec > timeout) {
+    tv.tv_sec = timeout;
   }
 
+#if defined JEP0022 || defined JEP0085
+  // Check composing timeout
+  timeout = scr_GetChatStatesTimeout(now);
+  if (tv.tv_sec > timeout) {
+    tv.tv_sec = timeout;
+  }
+#endif
+
   if (!tv.tv_sec)
     tv.tv_usec = 350000;
 
@@ -355,8 +406,6 @@
 {
   xmlnode x;
 
-  if (!online) return;
-
   if (msg) {
     // The status message has been specified.  We'll use it, unless it is
     // "-" which is a special case (option meaning "no status message").
@@ -376,28 +425,39 @@
     }
   }
 
-  x = presnew(st, recipient, (st != invisible ? msg : NULL));
-  jab_send(jc, x);
-  xmlnode_free(x);
+  // Only send the packet if we're online.
+  // (But we want to update internal status even when disconnected,
+  // in order to avoid some problems during network failures)
+  if (online) {
+    x = presnew(st, recipient, (st != invisible ? msg : NULL));
+    jab_send(jc, x);
+    xmlnode_free(x);
+  }
 
   // If we didn't change our _global_ status, we are done
   if (recipient) return;
 
-  // Send presence to chatrooms
-  if (st != invisible) {
-    struct T_presence room_presence;
-    room_presence.st = st;
-    room_presence.msg = msg;
-    foreach_buddy(ROSTER_TYPE_ROOM, &roompresence, &room_presence);
+  if (online) {
+    // Send presence to chatrooms
+    if (st != invisible) {
+      struct T_presence room_presence;
+      room_presence.st = st;
+      room_presence.msg = msg;
+      foreach_buddy(ROSTER_TYPE_ROOM, &roompresence, &room_presence);
+    }
   }
 
-  // We'll need to update the roster if we switch to/from offline because
-  // we don't know the presences of buddies when offline...
-  if (mystatus == offline || st == offline)
-    update_roster = TRUE;
+  if (online) {
+    // We'll need to update the roster if we switch to/from offline because
+    // we don't know the presences of buddies when offline...
+    if (mystatus == offline || st == offline)
+      update_roster = TRUE;
 
-  hk_mystatuschange(0, mystatus, st, (st != invisible ? msg : ""));
-  mystatus = st;
+    hk_mystatuschange(0, mystatus, st, (st != invisible ? msg : ""));
+    mystatus = st;
+  }
+  if (st)
+    mywantedstatus = st;
   if (msg != mystatusmsg) {
     g_free(mystatusmsg);
     if (*msg)
@@ -410,11 +470,38 @@
   scr_UpdateMainStatus(TRUE);
 }
 
+//  jb_setprevstatus()
+// Set previous status.  This wrapper function is used after a disconnection.
+inline void jb_setprevstatus(void)
+{
+  jb_setstatus(mywantedstatus, NULL, mystatusmsg);
+}
+
+//  new_msgid()
+// Generate a new id string.  The caller should free it.
+static char *new_msgid(void)
+{
+  static guint msg_idn;
+  time_t now;
+  time(&now);
+  if (!msg_idn)
+    srand(now);
+  msg_idn += 1U + (unsigned int) (9.0 * (rand() / (RAND_MAX + 1.0)));
+  return g_strdup_printf("%u%d", msg_idn, (int)(now%10L));
+}
+
 void jb_send_msg(const char *jid, const char *text, int type,
-                 const char *subject)
+                 const char *subject, const char *msgid)
 {
   xmlnode x;
   gchar *strtype;
+#if defined JEP0022 || defined JEP0085
+  xmlnode event;
+  char *rname, *barejid;
+  GSList *sl_buddy;
+  guint use_jep85 = 0;
+  struct jep0085 *jep85 = NULL;
+#endif
 
   if (!online) return;
 
@@ -429,12 +516,268 @@
     y = xmlnode_insert_tag(x, "subject");
     xmlnode_insert_cdata(y, subject, (unsigned) -1);
   }
+
+#if defined JEP0022 || defined JEP0085
+  // If typing notifications are disabled, we can skip all this stuff...
+  if (chatstates_disabled || type == ROSTER_TYPE_ROOM)
+    goto jb_send_msg_no_chatstates;
+
+  rname = strchr(jid, JID_RESOURCE_SEPARATOR);
+  barejid = jidtodisp(jid);
+  sl_buddy = roster_find(barejid, jidsearch, ROSTER_TYPE_USER);
+  g_free(barejid);
+
+  // If we can get a resource name, we use it.  Else we use NULL,
+  // which hopefully will give us the most likely resource.
+  if (rname)
+    rname++;
+  if (sl_buddy)
+    jep85 = buddy_resource_jep85(sl_buddy->data, rname);
+#endif
+
+#ifdef JEP0085
+  /* JEP-0085 5.1
+   * "Until receiving a reply to the initial content message (or a standalone
+   * notification) from the Contact, the User MUST NOT send subsequent chat
+   * state notifications to the Contact."
+   * In our implementation support is initially "unknown", they it's "probed"
+   * and can become "ok".
+   */
+  if (jep85 && (jep85->support == CHATSTATES_SUPPORT_OK ||
+                jep85->support == CHATSTATES_SUPPORT_UNKNOWN)) {
+    event = xmlnode_insert_tag(x, "active");
+    xmlnode_put_attrib(event, "xmlns", NS_CHATSTATES);
+    if (jep85->support == CHATSTATES_SUPPORT_UNKNOWN)
+      jep85->support = CHATSTATES_SUPPORT_PROBED;
+    else
+      use_jep85 = 1;
+    jep85->last_state_sent = ROSTER_EVENT_ACTIVE;
+  }
+#endif
+#ifdef JEP0022
+  /* JEP-22
+   * If the Contact supports JEP-0085, we do not use JEP-0022.
+   * If not, we try to fall back to JEP-0022.
+   */
+  if (!use_jep85) {
+    struct jep0022 *jep22 = NULL;
+    event = xmlnode_insert_tag(x, "x");
+    xmlnode_put_attrib(event, "xmlns", NS_EVENT);
+    xmlnode_insert_tag(event, "composing");
+
+    if (sl_buddy)
+      jep22 = buddy_resource_jep22(sl_buddy->data, rname);
+    if (jep22)
+      jep22->last_state_sent = ROSTER_EVENT_ACTIVE;
+
+    // An id is mandatory when using JEP-0022.
+    if (!msgid && (text || subject)) {
+      msgid = new_msgid();
+      // Let's update last_msgid_sent
+      // (We do not update it when the msgid is provided by the caller,
+      // because this is probably a special message...)
+      if (jep22) {
+        g_free(jep22->last_msgid_sent);
+        jep22->last_msgid_sent = g_strdup(msgid);
+      }
+    }
+  }
+#endif
+
+jb_send_msg_no_chatstates:
+  xmlnode_put_attrib(x, "id", msgid);
+
   jab_send(jc, x);
   xmlnode_free(x);
 
   jb_reset_keepalive();
 }
 
+
+#ifdef JEP0085
+//  jb_send_jep85_chatstate()
+// Send a JEP-85 chatstate.
+static void jb_send_jep85_chatstate(const char *jid, guint state)
+{
+  xmlnode x;
+  xmlnode event;
+  char *rname, *barejid;
+  GSList *sl_buddy;
+  const char *chattag;
+  struct jep0085 *jep85 = NULL;
+
+  if (!online) return;
+
+  rname = strchr(jid, JID_RESOURCE_SEPARATOR);
+  barejid = jidtodisp(jid);
+  sl_buddy = roster_find(barejid, jidsearch, ROSTER_TYPE_USER);
+  g_free(barejid);
+
+  // If we can get a resource name, we use it.  Else we use NULL,
+  // which hopefully will give us the most likely resource.
+  if (rname)
+    rname++;
+  if (sl_buddy)
+    jep85 = buddy_resource_jep85(sl_buddy->data, rname);
+
+  if (!jep85 || (jep85->support != CHATSTATES_SUPPORT_OK))
+    return;
+
+  if (state == jep85->last_state_sent)
+    return;
+
+  if (state == ROSTER_EVENT_ACTIVE)
+    chattag = "active";
+  else if (state == ROSTER_EVENT_COMPOSING)
+    chattag = "composing";
+  else if (state == ROSTER_EVENT_PAUSED)
+    chattag = "paused";
+  else {
+    scr_LogPrint(LPRINT_LOGNORM, "Error: unsupported JEP-85 state (%d)", state);
+    return;
+  }
+
+  jep85->last_state_sent = state;
+
+  x = jutil_msgnew(TMSG_CHAT, (char*)jid, NULL, NULL);
+
+  event = xmlnode_insert_tag(x, chattag);
+  xmlnode_put_attrib(event, "xmlns", NS_CHATSTATES);
+
+  jab_send(jc, x);
+  xmlnode_free(x);
+
+  jb_reset_keepalive();
+}
+#endif
+
+#ifdef JEP0022
+//  jb_send_jep22_event()
+// Send a JEP-22 message event (delivered, composing...).
+static void jb_send_jep22_event(const char *jid, guint type)
+{
+  xmlnode x;
+  xmlnode event;
+  const char *msgid;
+  char *rname, *barejid;
+  GSList *sl_buddy;
+  struct jep0022 *jep22 = NULL;
+  guint jep22_state;
+
+  if (!online) return;
+
+  rname = strchr(jid, JID_RESOURCE_SEPARATOR);
+  barejid = jidtodisp(jid);
+  sl_buddy = roster_find(barejid, jidsearch, ROSTER_TYPE_USER);
+  g_free(barejid);
+
+  // If we can get a resource name, we use it.  Else we use NULL,
+  // which hopefully will give us the most likely resource.
+  if (rname)
+    rname++;
+  if (sl_buddy)
+    jep22 = buddy_resource_jep22(sl_buddy->data, rname);
+
+  if (!jep22)
+    return; // XXX Maybe we could try harder (other resources?)
+
+  msgid = jep22->last_msgid_rcvd;
+
+  // For composing events (composing, active, inactive, paused...),
+  // JEP22 only has 2 states; we'll use composing and active.
+  if (type == ROSTER_EVENT_COMPOSING)
+    jep22_state = ROSTER_EVENT_COMPOSING;
+  else if (type == ROSTER_EVENT_ACTIVE ||
+           type == ROSTER_EVENT_PAUSED)
+    jep22_state = ROSTER_EVENT_ACTIVE;
+  else
+    jep22_state = 0; // ROSTER_EVENT_NONE
+
+  if (jep22_state) {
+    // Do not re-send a same event
+    if (jep22_state == jep22->last_state_sent)
+      return;
+    jep22->last_state_sent = jep22_state;
+  }
+
+  x = jutil_msgnew(TMSG_CHAT, (char*)jid, NULL, NULL);
+
+  event = xmlnode_insert_tag(x, "x");
+  xmlnode_put_attrib(event, "xmlns", NS_EVENT);
+  if (type == ROSTER_EVENT_DELIVERED)
+    xmlnode_insert_tag(event, "delivered");
+  else if (type == ROSTER_EVENT_COMPOSING)
+    xmlnode_insert_tag(event, "composing");
+  xmlnode_put_attrib(event, "id", msgid);
+
+  jab_send(jc, x);
+  xmlnode_free(x);
+
+  jb_reset_keepalive();
+}
+#endif
+
+//  jb_send_chatstate(buddy, state)
+// Send a chatstate or event (JEP-22/85) according to the buddy's capabilities.
+// The message is sent to one of the resources with the highest priority.
+#if defined JEP0022 || defined JEP0085
+void jb_send_chatstate(gpointer buddy, guint chatstate)
+{
+  const char *jid;
+  struct jep0085 *jep85 = NULL;
+  struct jep0022 *jep22 = NULL;
+
+  jid = buddy_getjid(buddy);
+  if (!jid) return;
+
+#ifdef JEP0085
+  jep85 = buddy_resource_jep85(buddy, NULL);
+  if (jep85 && jep85->support == CHATSTATES_SUPPORT_OK) {
+    jb_send_jep85_chatstate(jid, chatstate);
+    return;
+  }
+#endif
+#ifdef JEP0022
+  jep22 = buddy_resource_jep22(buddy, NULL);
+  if (jep22 && jep22->support == CHATSTATES_SUPPORT_OK) {
+    jb_send_jep22_event(jid, chatstate);
+  }
+#endif
+}
+#endif
+
+//  chatstates_reset_probed(fulljid)
+// If the JEP has been probed for this contact, set it back to unknown so
+// that we probe it again.  The parameter must be a full jid (w/ resource).
+#if defined JEP0022 || defined JEP0085
+static void chatstates_reset_probed(const char *fulljid)
+{
+  char *rname, *barejid;
+  GSList *sl_buddy;
+  struct jep0085 *jep85;
+  struct jep0022 *jep22;
+
+  rname = strchr(fulljid, JID_RESOURCE_SEPARATOR);
+  if (!rname++)
+    return;
+
+  barejid = jidtodisp(fulljid);
+  sl_buddy = roster_find(barejid, jidsearch, ROSTER_TYPE_USER);
+  g_free(barejid);
+
+  if (!sl_buddy)
+    return;
+
+  jep85 = buddy_resource_jep85(sl_buddy->data, rname);
+  jep22 = buddy_resource_jep22(sl_buddy->data, rname);
+
+  if (jep85 && jep85->support == CHATSTATES_SUPPORT_PROBED)
+    jep85->support = CHATSTATES_SUPPORT_UNKNOWN;
+  if (jep22 && jep22->support == CHATSTATES_SUPPORT_PROBED)
+    jep22->support = CHATSTATES_SUPPORT_UNKNOWN;
+}
+#endif
+
 //  jb_subscr_send_auth(jid)
 // Allow jid to receive our presence updates
 void jb_subscr_send_auth(const char *jid)
@@ -600,10 +943,24 @@
   } else if (reqtype == iqreq_time) {
     request_fn = &request_time;
     strreqtype = "time";
+  } else if (reqtype == iqreq_last) {
+    request_fn = &request_last;
+    strreqtype = "last";
+  } else if (reqtype == iqreq_vcard) {
+    // Special case
   } else
     return;
 
-  if (strchr(jid, '/')) {
+  // vCard request
+  if (reqtype == iqreq_vcard) {
+    char *bjid = jidtodisp(jid);
+    request_vcard(bjid);
+    scr_LogPrint(LPRINT_NORMAL, "Sent vCard request to <%s>", bjid);
+    g_free(bjid);
+    return;
+  }
+
+  if (strchr(jid, JID_RESOURCE_SEPARATOR)) {
     // This is a full JID
     (*request_fn)(jid);
     scr_LogPrint(LPRINT_NORMAL, "Sent %s request to <%s>", strreqtype, jid);
@@ -818,6 +1175,219 @@
   jb_reset_keepalive();
 }
 
+//  jb_set_storage_bookmark(roomid, name, nick, passwd, autojoin)
+// Update the private storage bookmarks: add a conference room.
+// If name is nil, we remove the bookmark.
+void jb_set_storage_bookmark(const char *roomid, const char *name,
+                             const char *nick, const char *passwd, int autojoin)
+{
+  xmlnode x;
+  bool changed = FALSE;
+
+  if (!roomid)
+    return;
+
+  // If we have no bookmarks, probably the server doesn't support them.
+  if (!bookmarks) {
+    scr_LogPrint(LPRINT_LOGNORM,
+                 "Sorry, your server doesn't seem to support private storage.");
+    return;
+  }
+
+  // Walk through the storage tags
+  x = xmlnode_get_firstchild(bookmarks);
+  for ( ; x; x = xmlnode_get_nextsibling(x)) {
+    const char *jid;
+    const char *p;
+    p = xmlnode_get_name(x);
+    // If the current node is a conference item, see if we have to replace it.
+    if (p && !strcmp(p, "conference")) {
+      jid = xmlnode_get_attrib(x, "jid");
+      if (!jid)
+        continue;
+      if (!strcmp(jid, roomid)) {
+        // We've found a bookmark for this room.  Let's hide it and we'll
+        // create a new one.
+        xmlnode_hide(x);
+        changed = TRUE;
+        break;
+      }
+    }
+  }
+
+  // Let's create a node/bookmark for this roomid, if the name is not NULL.
+  if (name) {
+    x = xmlnode_insert_tag(bookmarks, "conference");
+    xmlnode_put_attrib(x, "jid", roomid);
+    xmlnode_put_attrib(x, "name", name);
+    xmlnode_put_attrib(x, "autojoin", autojoin ? "1" : "0");
+    if (nick)
+      xmlnode_insert_cdata(xmlnode_insert_tag(x, "nick"), nick, -1);
+    if (passwd)
+      xmlnode_insert_cdata(xmlnode_insert_tag(x, "password"), passwd, -1);
+    changed = TRUE;
+  }
+
+  if (!changed)
+    return;
+
+  if (online)
+    send_storage_bookmarks();
+  else
+    scr_LogPrint(LPRINT_LOGNORM,
+                 "Warning: you're not connected to the server.");
+}
+
+static struct annotation *parse_storage_rosternote(xmlnode notenode)
+{
+  const char *p;
+  struct annotation *note = g_new0(struct annotation, 1);
+  p = xmlnode_get_attrib(notenode, "cdate");
+  if (p)
+    note->cdate = from_iso8601(p, 1);
+  p = xmlnode_get_attrib(notenode, "mdate");
+  if (p)
+    note->mdate = from_iso8601(p, 1);
+  note->text = g_strdup(xmlnode_get_data(notenode));
+  note->jid = g_strdup(xmlnode_get_attrib(notenode, "jid"));
+  return note;
+}
+
+//  jb_get_all_storage_rosternotes()
+// Return a GSList with all storage annotations.
+// The caller should g_free the list and its contents.
+GSList *jb_get_all_storage_rosternotes(void)
+{
+  xmlnode x;
+  GSList *sl_notes = NULL;
+
+  // If we have no rosternotes, probably the server doesn't support them.
+  if (!rosternotes)
+    return NULL;
+
+  // Walk through the storage rosternotes tags
+  x = xmlnode_get_firstchild(rosternotes);
+  for ( ; x; x = xmlnode_get_nextsibling(x)) {
+    const char *p;
+    struct annotation *note;
+    p = xmlnode_get_name(x);
+
+    // We want a note item
+    if (!p || strcmp(p, "note"))
+      continue;
+    // Just in case, check the jid...
+    if (!xmlnode_get_attrib(x, "jid"))
+      continue;
+    // Ok, let's add the note to our list
+    note = parse_storage_rosternote(x);
+    sl_notes = g_slist_append(sl_notes, note);
+  }
+  return sl_notes;
+}
+
+//  jb_get_storage_rosternotes(barejid, silent)
+// Return the annotation associated with this jid.
+// If silent is TRUE, no warning is displayed when rosternotes is disabled
+// The caller should g_free the string and structure after use.
+struct annotation *jb_get_storage_rosternotes(const char *barejid, int silent)
+{
+  xmlnode x;
+
+  if (!barejid)
+    return NULL;
+
+  // If we have no rosternotes, probably the server doesn't support them.
+  if (!rosternotes) {
+    if (!silent)
+      scr_LogPrint(LPRINT_LOGNORM, "Sorry, "
+                   "your server doesn't seem to support private storage.");
+    return NULL;
+  }
+
+  // Walk through the storage rosternotes tags
+  x = xmlnode_get_firstchild(rosternotes);
+  for ( ; x; x = xmlnode_get_nextsibling(x)) {
+    const char *jid;
+    const char *p;
+    p = xmlnode_get_name(x);
+    // We want a note item
+    if (!p || strcmp(p, "note"))
+      continue;
+    // Just in case, check the jid...
+    jid = xmlnode_get_attrib(x, "jid");
+    if (jid && !strcmp(jid, barejid))  // We've found a note for this contact.
+      return parse_storage_rosternote(x);
+  }
+  return NULL;  // No note found
+}
+
+//  jb_set_storage_rosternotes(barejid, note)
+// Update the private storage rosternotes: add/delete a note.
+// If note is nil, we remove the existing note.
+void jb_set_storage_rosternotes(const char *barejid, const char *note)
+{
+  xmlnode x;
+  bool changed = FALSE;
+  const char *cdate = NULL;
+
+  if (!barejid)
+    return;
+
+  // If we have no rosternotes, probably the server doesn't support them.
+  if (!rosternotes) {
+    scr_LogPrint(LPRINT_LOGNORM,
+                 "Sorry, your server doesn't seem to support private storage.");
+    return;
+  }
+
+  // Walk through the storage tags
+  x = xmlnode_get_firstchild(rosternotes);
+  for ( ; x; x = xmlnode_get_nextsibling(x)) {
+    const char *jid;
+    const char *p;
+    p = xmlnode_get_name(x);
+    // If the current node is a conference item, see if we have to replace it.
+    if (p && !strcmp(p, "note")) {
+      jid = xmlnode_get_attrib(x, "jid");
+      if (!jid)
+        continue;
+      if (!strcmp(jid, barejid)) {
+        // We've found a note for this jid.  Let's hide it and we'll
+        // create a new one.
+        cdate = xmlnode_get_attrib(x, "cdate");
+        xmlnode_hide(x);
+        changed = TRUE;
+        break;
+      }
+    }
+  }
+
+  // Let's create a node for this jid, if the note is not NULL.
+  if (note) {
+    char mdate[20];
+    time_t now;
+    time(&now);
+    to_iso8601(mdate, now);
+    if (!cdate)
+      cdate = mdate;
+    x = xmlnode_insert_tag(rosternotes, "note");
+    xmlnode_put_attrib(x, "jid", barejid);
+    xmlnode_put_attrib(x, "cdate", cdate);
+    xmlnode_put_attrib(x, "mdate", mdate);
+    xmlnode_insert_cdata(x, note, -1);
+    changed = TRUE;
+  }
+
+  if (!changed)
+    return;
+
+  if (online)
+    send_storage_rosternotes();
+  else
+    scr_LogPrint(LPRINT_LOGNORM,
+                 "Warning: you're not connected to the server.");
+}
+
 static void gotmessage(char *type, const char *from, const char *body,
                        const char *enc, time_t timestamp)
 {
@@ -826,7 +1396,7 @@
 
   jid = jidtodisp(from);
 
-  rname = strchr(from, '/');
+  rname = strchr(from, JID_RESOURCE_SEPARATOR);
   if (rname) rname++;
 
   // Check for unexpected groupchat messages
@@ -979,13 +1549,17 @@
         if (previous_state != JCONN_STATE_OFF)
           scr_LogPrint(LPRINT_LOGNORM, "[Jabber] Not connected to the server");
 
+        // Sometimes the state isn't correctly updated
+        if (jc)
+          jc->state = JCONN_STATE_OFF;
         online = FALSE;
         mystatus = offline;
-        if (mystatusmsg) {
-          g_free(mystatusmsg);
-          mystatusmsg = NULL;
-        }
+        // Free bookmarks
+        xmlnode_free(bookmarks);
+        bookmarks = NULL;
+        // Free roster
         roster_free();
+        // Update display
         update_roster = TRUE;
         scr_UpdateBuddyWindow();
         break;
@@ -1002,6 +1576,8 @@
         scr_LogPrint(LPRINT_LOGNORM, "[Jabber] Communication with the server "
                      "established");
         online = TRUE;
+        // We set AutoConnection to true after the 1st successful connection
+        AutoConnection = true;
         break;
 
     case JCONN_STATE_CONNECTING:
@@ -1022,10 +1598,8 @@
 
   x = xmlnode_get_firstchild(xmldata);
   for ( ; x; x = xmlnode_get_nextsibling(x)) {
-    if ((p = xmlnode_get_name(x)) && !strcmp(p, "x"))
-      if ((p = xmlnode_get_attrib(x, "xmlns")) && !strcmp(p, xmlns)) {
-        break;
-    }
+    if ((p = xmlnode_get_attrib(x, "xmlns")) && !strcmp(p, xmlns))
+      break;
   }
   return x;
 }
@@ -1058,6 +1632,7 @@
   unsigned int statuscode = 0;
   GSList *room_elt;
   int log_muc_conf;
+  guint msgflags;
 
   log_muc_conf = settings_opt_get_int("log_muc_conf");
 
@@ -1117,7 +1692,6 @@
     g_free(mbuf);
     // Send back an unavailable packet
     jb_setstatus(offline, roomjid, "");
-    buddylist_build();
     scr_DrawRoster();
     return;
   }
@@ -1219,8 +1793,11 @@
       }
     }
 
-    scr_WriteIncomingMessage(roomjid, mbuf, usttime,
-                             HBB_PREFIX_INFO|HBB_PREFIX_NOFLAG);
+    msgflags = HBB_PREFIX_INFO;
+    if (!we_left)
+      msgflags |= HBB_PREFIX_NOFLAG;
+
+    scr_WriteIncomingMessage(roomjid, mbuf, usttime, msgflags);
 
     if (log_muc_conf) hlog_write_message(roomjid, 0, FALSE, mbuf);
 
@@ -1287,7 +1864,6 @@
   } else
     scr_LogPrint(LPRINT_LOGNORM, "MUC DBG: no rname!"); /* DBG */
 
-  buddylist_build();
   scr_DrawRoster();
 }
 
@@ -1299,10 +1875,10 @@
   const char *rname;
   enum imstatus ust;
   char bpprio;
-  time_t timestamp = 0;
+  time_t timestamp = 0L;
   xmlnode muc_packet;
 
-  rname = strchr(from, '/');
+  rname = strchr(from, JID_RESOURCE_SEPARATOR);
   if (rname) rname++;
 
   r = jidtodisp(from);
@@ -1375,10 +1951,10 @@
 {
   char *p, *r, *s;
   xmlnode x;
-  char *body=NULL;
+  char *body = NULL;
   char *enc = NULL;
   char *tmp = NULL;
-  time_t timestamp = 0;
+  time_t timestamp = 0L;
 
   body = xmlnode_get_tag_data(xmldata, "body");
 
@@ -1390,7 +1966,7 @@
       gchar *subj = p;
       // Get the room (s) and the nickname (r)
       s = g_strdup(from);
-      r = strchr(s, '/');
+      r = strchr(s, JID_RESOURCE_SEPARATOR);
       if (r) *r++ = 0;
       else   r = s;
       // Set the new topic
@@ -1422,6 +1998,8 @@
     }
   }
 
+  handle_state_events(from, xmldata);
+
   // Not used yet...
   x = xml_get_xmlns(xmldata, NS_ENCRYPTED);
   if (x && (p = xmlnode_get_data(x)) != NULL) {
@@ -1434,12 +2012,134 @@
   if (type && !strcmp(type, TMSG_ERROR)) {
     if ((x = xmlnode_get_tag(xmldata, TMSG_ERROR)) != NULL)
       display_server_error(x);
+#if defined JEP0022 || defined JEP0085
+    // If the JEP85/22 support is probed, set it back to unknown so that
+    // we probe it again.
+    chatstates_reset_probed(from);
+#endif
   }
   if (from && body)
     gotmessage(type, from, body, enc, timestamp);
   g_free(tmp);
 }
 
+void handle_state_events(char *from, xmlnode xmldata)
+{
+#if defined JEP0022 || defined JEP0085
+  xmlnode state_ns = NULL;
+  const char *body;
+  char *rname, *jid;
+  GSList *sl_buddy;
+  guint events;
+  struct jep0022 *jep22 = NULL;
+  struct jep0085 *jep85 = NULL;
+  enum {
+    JEP_none,
+    JEP_85,
+    JEP_22
+  } which_jep = JEP_none;
+
+  rname = strchr(from, JID_RESOURCE_SEPARATOR);
+  jid   = jidtodisp(from);
+  sl_buddy = roster_find(jid, jidsearch, ROSTER_TYPE_USER);
+
+  /* XXX Actually that's wrong, since it filters out server "offline"
+     messages (for JEP-0022).  This JEP is (almost) deprecated so
+     we don't really care. */
+  if (!sl_buddy || !rname++) {
+    g_free(jid);
+    return;
+  }
+
+  /* Let's see chich JEP the contact uses.  If possible, we'll use
+     JEP-85, if not we'll look for JEP-22 support. */
+  events = buddy_resource_getevents(sl_buddy->data, rname);
+
+  jep85 = buddy_resource_jep85(sl_buddy->data, rname);
+  if (jep85) {
+    state_ns = xml_get_xmlns(xmldata, NS_CHATSTATES);
+    if (state_ns)
+      which_jep = JEP_85;
+  }
+
+  if (which_jep != JEP_85) { /* Fall back to JEP-0022 */
+    jep22 = buddy_resource_jep22(sl_buddy->data, rname);
+    if (jep22) {
+      state_ns = xml_get_xmlns(xmldata, NS_EVENT);
+      if (state_ns)
+        which_jep = JEP_22;
+    }
+  }
+
+  if (!which_jep) { /* Sender does not use chat states */
+    g_free(jid);
+    return;
+  }
+
+  body = xmlnode_get_tag_data(xmldata, "body");
+
+  if (which_jep == JEP_85) { /* JEP-0085 */
+    const char *p;
+    jep85->support = CHATSTATES_SUPPORT_OK;
+
+    p = xmlnode_get_name(state_ns);
+    if (!strcmp(p, "composing")) {
+      jep85->last_state_rcvd = ROSTER_EVENT_COMPOSING;
+    } else if (!strcmp(p, "active")) {
+      jep85->last_state_rcvd = ROSTER_EVENT_ACTIVE;
+    } else if (!strcmp(p, "paused")) {
+      jep85->last_state_rcvd = ROSTER_EVENT_PAUSED;
+    } else if (!strcmp(p, "inactive")) {
+      jep85->last_state_rcvd = ROSTER_EVENT_INACTIVE;
+    } else if (!strcmp(p, "gone")) {
+      jep85->last_state_rcvd = ROSTER_EVENT_GONE;
+    }
+    events = jep85->last_state_rcvd;
+  } else {              /* JEP-0022 */
+#ifdef JEP0022
+    const char *msgid;
+    jep22->support = CHATSTATES_SUPPORT_OK;
+    jep22->last_state_rcvd = ROSTER_EVENT_NONE;
+
+    msgid = xmlnode_get_attrib(xmldata, "id");
+
+    if (xmlnode_get_tag(state_ns, "composing")) {
+      // Clear composing if the message contains a body
+      if (body)
+        events &= ~ROSTER_EVENT_COMPOSING;
+      else
+        events |= ROSTER_EVENT_COMPOSING;
+      jep22->last_state_rcvd |= ROSTER_EVENT_COMPOSING;
+
+    } else {
+      events &= ~ROSTER_EVENT_COMPOSING;
+    }
+
+    // Cache the message id
+    g_free(jep22->last_msgid_rcvd);
+    if (msgid)
+      jep22->last_msgid_rcvd = g_strdup(msgid);
+    else
+      jep22->last_msgid_rcvd = NULL;
+
+    if (xmlnode_get_tag(state_ns, "delivered")) {
+      jep22->last_state_rcvd |= ROSTER_EVENT_DELIVERED;
+
+      // Do we have to send back an ACK?
+      if (body)
+        jb_send_jep22_event(from, ROSTER_EVENT_DELIVERED);
+    }
+#endif
+  }
+
+  buddy_resource_setevents(sl_buddy->data, rname, events);
+
+  update_roster = TRUE;
+
+  g_free(jid);
+#endif
+}
+
 static void evscallback_subscription(eviqs *evp, guint evcontext)
 {
   char *barejid;
@@ -1568,20 +2268,14 @@
     newbuddy = FALSE;
   }
 
-  if (newbuddy) {
-    buddylist_build();
+  if (newbuddy)
     update_roster = TRUE;
-  }
   g_free(r);
 }
 
 static void packethandler(jconn conn, jpacket packet)
 {
   char *p;
-  /*
-  char *r, *s;
-  const char *m;
-  */
   char *from=NULL, *type=NULL;
 
   jb_reset_keepalive(); // reset keepalive timeout
@@ -1599,7 +2293,7 @@
   if (p) from = p;
 
   if (!from && packet->type != JPACKET_IQ) {
-    scr_LogPrint(LPRINT_LOGNORM, "Error in packet (could be UTF8-related)");
+    scr_LogPrint(LPRINT_LOGNORM, "Error in stream packet");
     return;
   }
 
--- a/mcabber/src/jabglue.h	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/src/jabglue.h	Sun Nov 19 11:51:14 2006 +0100
@@ -15,6 +15,7 @@
 #endif
 
 extern jconn jc;
+extern guint AutoConnection;
 
 extern char imstatus2char[];
 // Status chars: '_', 'o', 'i', 'f', 'd', 'n', 'a'
@@ -29,7 +30,16 @@
 enum iqreq_type {
   iqreq_none,
   iqreq_version,
-  iqreq_time
+  iqreq_time,
+  iqreq_last,
+  iqreq_vcard
+};
+
+struct annotation {
+  time_t cdate;
+  time_t mdate;
+  gchar *jid;
+  gchar *text;
 };
 
 char *compose_jid(const char *username, const char *servername,
@@ -49,9 +59,11 @@
 inline enum imstatus jb_getstatus(void);
 inline const char *jb_getstatusmsg(void);
 void jb_setstatus(enum imstatus st, const char *recipient, const char *msg);
+inline void jb_setprevstatus(void);
 void jb_send_msg(const char *jid, const char *text, int type,
-                 const char *subject);
+                 const char *subject, const char *id);
 void jb_send_raw(const char *str);
+void jb_send_chatstate(gpointer buddy, guint chatstate);
 void jb_keepalive(void);
 inline void jb_reset_keepalive(void);
 void jb_set_keepalive_delay(unsigned int delay);
@@ -63,6 +75,12 @@
                        struct role_affil ra, const char *reason);
 void jb_iqs_display_list(void);
 void jb_request(const char *jid, enum iqreq_type reqtype);
+void jb_set_storage_bookmark(const char *roomid, const char *name,
+                             const char *nick, const char *passwd,
+                             int autojoin);
+struct annotation *jb_get_storage_rosternotes(const char *barejid, int silent);
+GSList *jb_get_all_storage_rosternotes(void);
+void jb_set_storage_rosternotes(const char *barejid, const char *note);
 
 #endif /* __JABGLUE_H__ */
 
--- a/mcabber/src/main.c	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/src/main.c	Sun Nov 19 11:51:14 2006 +0100
@@ -317,6 +317,8 @@
   if (settings_opt_get_int("hide_offline_buddies") > 0)
     buddylist_set_hide_offline_buddies(TRUE);
 
+  chatstates_disabled = settings_opt_get_int("disable_chatstates");
+
   if (ret < 0) {
     scr_LogPrint(LPRINT_NORMAL, "No configuration file has been found.");
     scr_ShowBuddyWindow();
--- a/mcabber/src/roster.c	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/src/roster.c	Sun Nov 19 11:51:14 2006 +0100
@@ -51,6 +51,13 @@
   enum imrole role;
   enum imaffiliation affil;
   gchar *realjid;       /* for chatrooms, if buddy's real jid is known */
+  guint events;
+#ifdef JEP0022
+  struct jep0022 jep22;
+#endif
+#ifdef JEP0085
+  struct jep0085 jep85;
+#endif
 } res;
 
 /* This is a private structure type for the roster */
@@ -114,6 +121,10 @@
     g_free((gchar*)p_res->status_msg);
     g_free((gchar*)p_res->name);
     g_free((gchar*)p_res->realjid);
+#ifdef JEP0022
+    g_free(p_res->jep22.last_msgid_sent);
+    g_free(p_res->jep22.last_msgid_rcvd);
+#endif
   }
   // Free all nodes but the first (which is static)
   g_slist_free(*reslist);
@@ -208,6 +219,10 @@
   g_free(p_res->name);
   g_free(p_res->status_msg);
   g_free(p_res->realjid);
+#ifdef JEP0022
+  g_free(p_res->jep22.last_msgid_sent);
+  g_free(p_res->jep22.last_msgid_rcvd);
+#endif
   rost->resource = g_slist_delete_link(rost->resource, p_res_elt);
   return;
 }
@@ -338,7 +353,7 @@
     roster_usr->name  = g_strdup(name);
   } else {
     gchar *p, *str = g_strdup(jid);
-    p = strstr(str, "/");
+    p = strchr(str, JID_RESOURCE_SEPARATOR);
     if (p)  *p = '\0';
     roster_usr->name = g_strdup(str);
     g_free(str);
@@ -600,7 +615,7 @@
       // ROSTER_FLAG_MSG should already be set...
   }
 
-  if (buddylist && new_roster_item)
+  if (buddylist && (new_roster_item || !g_list_find(buddylist, roster_usr)))
     buddylist_build();
 }
 
@@ -795,7 +810,7 @@
       if (!hide_offline_buddies || roster_usrelt == roster_current_buddy ||
           (buddy_getstatus((gpointer)roster_usrelt, NULL) != offline) ||
           (buddy_getflags((gpointer)roster_usrelt) &
-               (ROSTER_FLAG_LOCK | ROSTER_FLAG_MSG))) {
+               (ROSTER_FLAG_LOCK | ROSTER_FLAG_USRLOCK | ROSTER_FLAG_MSG))) {
         // This user should be added.  Maybe the group hasn't been added yet?
         if (pending_group &&
             (hide_offline_buddies || roster_usrelt == roster_current_buddy)) {
@@ -1062,6 +1077,46 @@
   return 0;
 }
 
+guint buddy_resource_getevents(gpointer rosterdata, const char *resname)
+{
+  roster *roster_usr = rosterdata;
+  res *p_res = get_resource(roster_usr, resname);
+  if (p_res)
+    return p_res->events;
+  return ROSTER_EVENT_NONE;
+}
+
+void buddy_resource_setevents(gpointer rosterdata, const char *resname,
+                              guint events)
+{
+  roster *roster_usr = rosterdata;
+  res *p_res = get_resource(roster_usr, resname);
+  if (p_res)
+    p_res->events = events;
+}
+
+struct jep0022 *buddy_resource_jep22(gpointer rosterdata, const char *resname)
+{
+#ifdef JEP0022
+  roster *roster_usr = rosterdata;
+  res *p_res = get_resource(roster_usr, resname);
+  if (p_res)
+    return &p_res->jep22;
+#endif
+  return NULL;
+}
+
+struct jep0085 *buddy_resource_jep85(gpointer rosterdata, const char *resname)
+{
+#ifdef JEP0085
+  roster *roster_usr = rosterdata;
+  res *p_res = get_resource(roster_usr, resname);
+  if (p_res)
+    return &p_res->jep85;
+#endif
+  return NULL;
+}
+
 enum imrole buddy_getrole(gpointer rosterdata, const char *resname)
 {
   roster *roster_usr = rosterdata;
--- a/mcabber/src/roster.h	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/src/roster.h	Sun Nov 19 11:51:14 2006 +0100
@@ -71,7 +71,45 @@
 #define ROSTER_FLAG_MSG     1U      // Message not read
 #define ROSTER_FLAG_HIDE    (1U<<1) // Group hidden (or buddy window closed)
 #define ROSTER_FLAG_LOCK    (1U<<2) // Node should not be removed from buddylist
-// ROSTER_FLAG_LOCAL   (1U<<3) // Buddy not on server's roster  (??)
+#define ROSTER_FLAG_USRLOCK (1U<<3) // Node should not be removed from buddylist
+// ROSTER_FLAG_LOCAL   (1U<<4) // Buddy not on server's roster  (??)
+
+#define JEP0022
+#define JEP0085
+
+struct jep0022 {
+  guint support;
+  guint last_state_sent;
+  gchar *last_msgid_sent;
+  guint last_state_rcvd;
+  gchar *last_msgid_rcvd;
+};
+struct jep0085 {
+  guint support;
+  guint last_state_sent;
+  guint last_state_rcvd;
+};
+
+enum chatstate_support {
+  CHATSTATES_SUPPORT_UNKNOWN = 0,
+  CHATSTATES_SUPPORT_PROBED,
+  CHATSTATES_SUPPORT_NONE,
+  CHATSTATES_SUPPORT_OK
+};
+
+/* Message event and chat state flags */
+#define ROSTER_EVENT_NONE      0U
+/* JEP-22 Message Events */
+#define ROSTER_EVENT_OFFLINE   (1U<<0)
+#define ROSTER_EVENT_DELIVERED (1U<<1)
+#define ROSTER_EVENT_DISPLAYED (1U<<2)
+/* JEP-22 & JEP-85 */
+#define ROSTER_EVENT_COMPOSING (1U<<3)
+/* JEP-85 Chat State Notifications */
+#define ROSTER_EVENT_ACTIVE    (1U<<4)
+#define ROSTER_EVENT_PAUSED    (1U<<5)
+#define ROSTER_EVENT_INACTIVE  (1U<<6)
+#define ROSTER_EVENT_GONE      (1U<<7)
 
 extern GList *buddylist;
 extern GList *current_buddy;
@@ -134,6 +172,11 @@
 GSList *buddy_getresources_locale(gpointer rosterdata);
 void    buddy_resource_setname(gpointer rosterdata, const char *resname,
                                const char *newname);
+void    buddy_resource_setevents(gpointer rosterdata, const char *resname,
+                                 guint event);
+guint   buddy_resource_getevents(gpointer rosterdata, const char *resname);
+struct jep0022 *buddy_resource_jep22(gpointer rosterdata, const char *resname);
+struct jep0085 *buddy_resource_jep85(gpointer rosterdata, const char *resname);
 enum imrole buddy_getrole(gpointer rosterdata, const char *resname);
 enum imaffiliation buddy_getaffil(gpointer rosterdata, const char *resname);
 const char *buddy_getrjid(gpointer rosterdata, const char *resname);
--- a/mcabber/src/screen.c	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/src/screen.c	Sun Nov 19 11:51:14 2006 +0100
@@ -83,6 +83,8 @@
 int utf8_mode = 0;
 static bool Autoaway;
 static bool Curses;
+static bool log_win_on_top;
+static bool roster_win_on_right;
 static time_t LastActivity;
 
 static char       inputLine[INPUTLINE_LENGTH+1];
@@ -93,6 +95,11 @@
 static GList *cmdhisto_cur;
 static char   cmdhisto_backup[INPUTLINE_LENGTH+1];
 
+static int    chatstate; /* (0=active, 1=composing, 2=paused) */
+static bool   lock_chatstate;
+static time_t chatstate_timestamp;
+int chatstates_disabled;
+
 #define MAX_KEYSEQ_LENGTH 8
 
 typedef struct {
@@ -422,8 +429,14 @@
   tmp = g_new0(winbuf, 1);
 
   // Dimensions
-  x = Roster_Width;
-  y = 0;
+  if (roster_win_on_right)
+    x = 0;
+  else
+    x = Roster_Width;
+  if (log_win_on_top)
+    y = Log_Win_Height-1;
+  else
+    y = 0;
   lines = CHAT_WIN_HEIGHT;
   cols = maxX - Roster_Width;
   if (cols < 1) cols = 1;
@@ -770,6 +783,8 @@
 {
   int requested_size;
   gchar *ver, *message;
+  int chat_y_pos, chatstatus_y_pos, log_y_pos;
+  int roster_x_pos, chat_x_pos;
 
   Log_Win_Height = DEFAULT_LOG_WIN_HEIGHT;
   requested_size = settings_opt_get_int("log_win_height");
@@ -803,12 +818,34 @@
       Roster_Width = DEFAULT_ROSTER_WIDTH;
   }
 
+  log_win_on_top = (settings_opt_get_int("log_win_on_top") == 1);
+  roster_win_on_right = (settings_opt_get_int("roster_win_on_right") == 1);
+
+  if (log_win_on_top) {
+    chat_y_pos = Log_Win_Height-1;
+    log_y_pos = 0;
+    chatstatus_y_pos = Log_Win_Height-2;
+  } else {
+    chat_y_pos = 0;
+    log_y_pos = CHAT_WIN_HEIGHT+1;
+    chatstatus_y_pos = CHAT_WIN_HEIGHT;
+  }
+
+  if (roster_win_on_right) {
+    roster_x_pos = maxX - Roster_Width;
+    chat_x_pos = 0;
+  } else {
+    roster_x_pos = 0;
+    chat_x_pos = Roster_Width;
+  }
+
   if (fullinit) {
     /* Create windows */
-    rosterWnd = newwin(CHAT_WIN_HEIGHT, Roster_Width, 0, 0);
-    chatWnd   = newwin(CHAT_WIN_HEIGHT, maxX - Roster_Width, 0, Roster_Width);
-    logWnd    = newwin(Log_Win_Height-2, maxX, CHAT_WIN_HEIGHT+1, 0);
-    chatstatusWnd = newwin(1, maxX, CHAT_WIN_HEIGHT, 0);
+    rosterWnd = newwin(CHAT_WIN_HEIGHT, Roster_Width, chat_y_pos, roster_x_pos);
+    chatWnd   = newwin(CHAT_WIN_HEIGHT, maxX - Roster_Width, chat_y_pos,
+                       chat_x_pos);
+    logWnd    = newwin(Log_Win_Height-2, maxX, log_y_pos, 0);
+    chatstatusWnd = newwin(1, maxX, chatstatus_y_pos, 0);
     mainstatusWnd = newwin(1, maxX, maxY-2, 0);
     inputWnd  = newwin(1, maxX, maxY-1, 0);
     if (!rosterWnd || !chatWnd || !logWnd || !inputWnd) {
@@ -825,14 +862,15 @@
     /* Resize/move windows */
     wresize(rosterWnd, CHAT_WIN_HEIGHT, Roster_Width);
     wresize(chatWnd, CHAT_WIN_HEIGHT, maxX - Roster_Width);
-    mvwin(chatWnd, 0, Roster_Width);
-
     wresize(logWnd, Log_Win_Height-2, maxX);
-    mvwin(logWnd, CHAT_WIN_HEIGHT+1, 0);
+
+    mvwin(chatWnd, chat_y_pos, chat_x_pos);
+    mvwin(rosterWnd, chat_y_pos, roster_x_pos);
+    mvwin(logWnd, log_y_pos, 0);
 
     // Resize & move chat status window
     wresize(chatstatusWnd, 1, maxX);
-    mvwin(chatstatusWnd, CHAT_WIN_HEIGHT, 0);
+    mvwin(chatstatusWnd, chatstatus_y_pos, 0);
     // Resize & move main status window
     wresize(mainstatusWnd, 1, maxX);
     mvwin(mainstatusWnd, maxY-2, 0);
@@ -898,9 +936,21 @@
 static inline void resize_win_buffer(winbuf *wbp, int x, int y,
                                      int lines, int cols)
 {
+  int chat_x_pos, chat_y_pos;
+
+  if (log_win_on_top)
+    chat_y_pos = Log_Win_Height-1;
+  else
+    chat_y_pos = 0;
+
+  if (roster_win_on_right)
+    chat_x_pos = 0;
+  else
+    chat_x_pos = Roster_Width;
+
   // Resize/move buddy window
   wresize(wbp->win, lines, cols);
-  mvwin(wbp->win, 0, Roster_Width);
+  mvwin(wbp->win, chat_y_pos, chat_x_pos);
   werase(wbp->win);
   // If a panel exists, replace the old window with the new
   if (wbp->panel)
@@ -1039,6 +1089,31 @@
   g_free(buf_locale);
   g_free(buf);
 
+  // Display chatstates of the contact, if available.
+  if (btype & ROSTER_TYPE_USER) {
+    char eventchar = 0;
+    guint event;
+
+    // We do not specify the resource here, so one of the resources with the
+    // highest priority will be used.
+    event = buddy_resource_getevents(BUDDATA(current_buddy), NULL);
+
+    if (event == ROSTER_EVENT_ACTIVE)
+      eventchar = 'A';
+    else if (event == ROSTER_EVENT_COMPOSING)
+      eventchar = 'C';
+    else if (event == ROSTER_EVENT_PAUSED)
+      eventchar = 'P';
+    else if (event == ROSTER_EVENT_INACTIVE)
+      eventchar = 'I';
+    else if (event == ROSTER_EVENT_GONE)
+      eventchar = 'G';
+
+    if (eventchar)
+      mvwprintw(chatstatusWnd, 0, maxX-3, "[%c]", eventchar);
+  }
+
+
   if (forceupdate) {
     update_panels();
   }
@@ -1057,6 +1132,7 @@
   int cursor_backup;
   char status, pending;
   enum imstatus currentstatus = jb_getstatus();
+  int x_pos;
 
   // We can reset update_roster
   update_roster = FALSE;
@@ -1075,10 +1151,11 @@
   werase(rosterWnd);
 
   if (Roster_Width) {
+    int line_x_pos = roster_win_on_right ? 0 : Roster_Width-1;
     // Redraw the vertical line (not very good...)
     wattrset(rosterWnd, get_color(COLOR_GENERAL));
     for (i=0 ; i < CHAT_WIN_HEIGHT ; i++)
-      mvwaddch(rosterWnd, i, Roster_Width-1, ACS_VLINE);
+      mvwaddch(rosterWnd, i, line_x_pos, ACS_VLINE);
   }
 
   // Leave now if buddylist is empty or the roster is hidden
@@ -1107,6 +1184,11 @@
     offset = i + 1 - maxy;
   }
 
+  if (roster_win_on_right)
+    x_pos = 1; // 1 char offset (vertical line)
+  else
+    x_pos = 0;
+
   name = g_new0(char, 4*Roster_Width);
   rline = g_new0(char, 4*Roster_Width+1);
 
@@ -1116,6 +1198,7 @@
   for (i=0; i<maxy && buddy; buddy = g_list_next(buddy)) {
     unsigned short bflags, btype, ismsg, isgrp, ismuc, ishid, isspe;
     gchar *rline_locale;
+    GSList *resources;
 
     bflags = buddy_getflags(BUDDATA(buddy));
     btype = buddy_gettype(BUDDATA(buddy));
@@ -1134,6 +1217,18 @@
     status = '?';
     pending = ' ';
 
+    resources = buddy_getresources(BUDDATA(buddy));
+    for ( ; resources ; resources = g_slist_next(resources) ) {
+      guint events = buddy_resource_getevents(BUDDATA(buddy),
+                                              resources ? resources->data : "");
+      if (events & ROSTER_EVENT_PAUSED)
+        pending = '.';
+      if (events & ROSTER_EVENT_COMPOSING) {
+        pending = '+';
+        break;
+      }
+    }
+
     // Display message notice if there is a message flag, but not
     // for unfolded groups.
     if (ismsg && (!isgrp || ishid)) {
@@ -1157,7 +1252,7 @@
       else
         wattrset(rosterWnd, get_color(COLOR_ROSTERSEL));
       // The 3 following lines aim at coloring the whole line
-      wmove(rosterWnd, i, 0);
+      wmove(rosterWnd, i, x_pos);
       for (n = 0; n < maxx; n++)
         waddch(rosterWnd, ' ');
     } else {
@@ -1199,7 +1294,7 @@
     }
 
     rline_locale = from_utf8(rline);
-    mvwprintw(rosterWnd, i, 0, "%s", rline_locale);
+    mvwprintw(rosterWnd, i, x_pos, "%s", rline_locale);
     g_free(rline_locale);
     i++;
   }
@@ -1303,7 +1398,7 @@
   }
 }
 
-unsigned int scr_GetAutoAwayTimeout(time_t now)
+long int scr_GetAutoAwayTimeout(time_t now)
 {
   enum imstatus cur_st;
   unsigned int autoaway_timeout = settings_opt_get_int("autoaway");
@@ -1322,6 +1417,53 @@
     return LastActivity + (time_t)autoaway_timeout - now;
 }
 
+//  set_chatstate(state)
+// Set the current chat state (0=active, 1=composing, 2=paused)
+// If the chat state has changed, call jb_send_chatstate()
+static inline void set_chatstate(int state)
+{
+#if defined JEP0022 || defined JEP0085
+  if (chatstates_disabled)
+    return;
+  if (!chatmode)
+    state = 0;
+  if (state != chatstate) {
+    chatstate = state;
+    if (current_buddy &&
+        buddy_gettype(BUDDATA(current_buddy)) == ROSTER_TYPE_USER) {
+      guint jep_state;
+      if (chatstate == 1)
+        jep_state = ROSTER_EVENT_COMPOSING;
+      else if (chatstate == 2)
+        jep_state = ROSTER_EVENT_PAUSED;
+      else
+        jep_state = ROSTER_EVENT_ACTIVE;
+      jb_send_chatstate(BUDDATA(current_buddy), jep_state);
+    }
+    if (!chatstate)
+      chatstate_timestamp = 0;
+  }
+#endif
+}
+
+#if defined JEP0022 || defined JEP0085
+inline long int scr_GetChatStatesTimeout(time_t now)
+{
+  // Check if we're currently composing...
+  if (chatstate != 1 || !chatstate_timestamp)
+    return 86400;
+
+  // If the timeout is reached, let's change the state right now.
+  if (now >= chatstate_timestamp + COMPOSING_TIMEOUT) {
+    chatstate_timestamp = now;
+    set_chatstate(2);
+    return 86400;
+  }
+
+ return chatstate_timestamp + COMPOSING_TIMEOUT - now;
+}
+#endif
+
 // Check if we should enter/leave automatic away status
 void scr_CheckAutoAway(int activity)
 {
@@ -1359,6 +1501,11 @@
   if (!current_buddy || !newbuddy)  return;
   if (newbuddy == current_buddy)    return;
 
+  // We're moving to another buddy.  We're thus inactive wrt current_buddy.
+  set_chatstate(0);
+  // We don't want the chatstate to be changed again right now.
+  lock_chatstate = true;
+
   prev_st = buddy_getstatus(BUDDATA(current_buddy), NULL);
   buddy_setflags(BUDDATA(current_buddy), ROSTER_FLAG_LOCK, FALSE);
   if (chatmode)
@@ -2062,7 +2209,7 @@
   int quote = FALSE;
 
   // Not a command?
-  if ((ptr_inputline == inputLine) || (inputLine[0] != '/')) {
+  if ((ptr_inputline == inputLine) || (inputLine[0] != COMMAND_CHAR)) {
     if (!current_buddy) return -2;
     if (buddy_gettype(BUDDATA(current_buddy)) == ROSTER_TYPE_ROOM) {
       *p_row = inputLine;
@@ -2251,7 +2398,7 @@
 {
   if (!Curses) return;
   // Leave multi-line mode
-  process_command("/msay abort");
+  process_command(mkcmdstr("msay abort"));
   // Same as Ctrl-g, now
   scr_cancel_current_completion();
   scr_end_current_completion();
@@ -2442,7 +2589,7 @@
   if (boundcmd) {
     gchar *cmd, *boundcmd_locale;
     boundcmd_locale = from_utf8(boundcmd);
-    cmd = g_strdup_printf("/%s", boundcmd_locale);
+    cmd = g_strdup_printf(mkcmdstr("%s"), boundcmd_locale);
     scr_CheckAutoAway(TRUE);
     if (process_command(cmd))
       return 255; // Quit
@@ -2467,6 +2614,8 @@
   int key = kcode.value;
   int display_char = FALSE;
 
+  lock_chatstate = false;
+
   switch (kcode.mcode) {
     case 0:
         break;
@@ -2690,6 +2839,17 @@
   if (completion_started && key != 9 && key != KEY_RESIZE)
     scr_end_current_completion();
   refresh_inputline();
+
+  if (!lock_chatstate) {
+    // Set chat state to composing (1) if the user is currently composing,
+    // i.e. not an empty line and not a command line.
+    if (inputLine[0] == 0 || inputLine[0] == COMMAND_CHAR)
+      set_chatstate(0);
+    else
+      set_chatstate(1);
+    if (chatstate)
+      time(&chatstate_timestamp);
+  }
   return 0;
 }
 
--- a/mcabber/src/screen.h	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/src/screen.h	Sun Nov 19 11:51:14 2006 +0100
@@ -26,6 +26,11 @@
 // Note: message length is limited by the HBB_BLOCKSIZE size too
 #define MULTILINE_MAX_LINE_NUMBER 299
 
+// When chatstates are enabled, timeout (in seconds) before "composing"
+// becomes "paused" because of user inactivity.
+// Warning: setting this very low will cause more network traffic.
+#define COMPOSING_TIMEOUT 6L
+
 enum colors {
   COLOR_GENERAL = 3,
   COLOR_MSGOUT,
@@ -91,9 +96,14 @@
 
 inline void scr_Beep(void);
 
-unsigned int scr_GetAutoAwayTimeout(time_t now);
+long int scr_GetAutoAwayTimeout(time_t now);
 void scr_CheckAutoAway(int activity);
 
+#if defined JEP0022 || defined JEP0085
+long int scr_GetChatStatesTimeout(time_t now);
+#endif
+int chatstates_disabled;
+
 // For commands...
 void scr_RosterTop(void);
 void scr_RosterBottom(void);
--- a/mcabber/src/settings.c	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/src/settings.c	Sun Nov 19 11:51:14 2006 +0100
@@ -136,16 +136,18 @@
 
     if ((strchr(line, '=') != NULL)) {
       // Only accept the set, alias and bind commands
-      if (strncmp(line, "set ", 4) &&
-          strncmp(line, "bind ", 5) &&
-          strncmp(line, "alias ", 6)) {
+      if (strncmp(line, "set ", strlen("set ")) &&
+          strncmp(line, "bind ", strlen("bind ")) &&
+          strncmp(line, "alias ", strlen("alias "))) {
         scr_LogPrint(LPRINT_LOGNORM,
                      "Error in configuration file (l. %d): bad command", ln);
         err++;
         continue;
       }
-      *(--line) = '/';        // Set the leading '/' to build a command line
-      process_command(line);  // Process the command
+      // Set the leading COMMAND_CHAR to build a command line
+      // and process the command
+      *(--line) = COMMAND_CHAR;
+      process_command(line);
     } else {
       scr_LogPrint(LPRINT_LOGNORM,
                    "Error in configuration file (l. %d): no assignment", ln);
@@ -343,4 +345,26 @@
   }
 }
 
+
+//  default_muc_nickname()
+// Return the user's default nickname
+// The caller should free the string after use
+char *default_muc_nickname(void)
+{
+  char *nick;
+
+  // We try the "nickname" option, then the username part of the jid.
+  nick = (char*)settings_opt_get("nickname");
+  if (nick)
+    return g_strdup(nick);
+
+  nick = g_strdup(settings_opt_get("username"));
+  if (nick) {
+    char *p = strchr(nick, JID_DOMAIN_SEPARATOR);
+    if (p > nick)
+      *p = 0;
+  }
+  return nick;
+}
+
 /* vim: set expandtab cindent cinoptions=>2\:2(0:  For Vim users... */
--- a/mcabber/src/settings.h	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/src/settings.h	Sun Nov 19 11:51:14 2006 +0100
@@ -15,9 +15,14 @@
 #define SETTINGS_TYPE_ALIAS     2
 #define SETTINGS_TYPE_BINDING   3
 
+#define COMMAND_CHAR    '/'
+#define COMMAND_CHARSTR "/"
+
 #define settings_opt_get(k)     settings_get(SETTINGS_TYPE_OPTION, k)
 #define settings_opt_get_int(k) settings_get_int(SETTINGS_TYPE_OPTION, k)
 
+#define mkcmdstr(cmd) COMMAND_CHARSTR cmd
+
 int     cfg_read_file(char *filename);
 guint   parse_assigment(gchar *assignment,
                         const gchar **pkey, const gchar **pval);
@@ -30,6 +35,8 @@
                          void (*pfunc)(void *param, char *k, char *v),
                          void *param);
 
+char *default_muc_nickname(void);
+
 const gchar *isbound(int key);
 
 #endif /* __SETTINGS_H__ */
--- a/mcabber/src/utils.c	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/src/utils.c	Sun Nov 19 11:51:14 2006 +0100
@@ -39,6 +39,7 @@
 #include <glib.h>
 
 #include <config.h>
+#include "utils.h"
 #include "logprint.h"
 
 static int DebugEnabled;
@@ -313,7 +314,7 @@
 
   if (!jid) return 1;
 
-  domain = strchr(jid, '@');
+  domain = strchr(jid, JID_DOMAIN_SEPARATOR);
 
   /* the username is optional */
   if (!domain) {
@@ -325,8 +326,8 @@
     domain++;
 
     /* check for low and invalid ascii characters in the username */
-    for (str = jid; *str != '@'; str++) {
-      if (*str <= 32 || *str == ':' || *str == '@' ||
+    for (str = jid; *str != JID_DOMAIN_SEPARATOR; str++) {
+      if (*str <= ' ' || *str == ':' || *str == JID_DOMAIN_SEPARATOR ||
               *str == '<' || *str == '>' || *str == '\'' ||
               *str == '"' || *str == '&') {
         return 1;
@@ -335,7 +336,7 @@
     /* the username is okay as far as we can tell without LIBIDN */
   }
 
-  resource = strchr(domain, '/');
+  resource = strchr(domain, JID_RESOURCE_SEPARATOR);
 
   /* the resource is optional */
   if (resource) {
@@ -355,7 +356,7 @@
   if (domlen > 1023) return 1;
 
   /* make sure the hostname is valid characters */
-  for (str = domain; *str != '\0' && *str != '/'; str++) {
+  for (str = domain; *str != '\0' && *str != JID_RESOURCE_SEPARATOR; str++) {
     if (!(isalnum(*str) || *str == '.' || *str == '-' || *str == '_'))
       return 1;
   }
@@ -382,6 +383,8 @@
   int escape = FALSE;
   char *p;
 
+  if (!s) return;
+
   for (p = s; *p; p++) {
     if (*p == '"') {
       if (!escape) {
--- a/mcabber/src/utils.h	Sun Nov 19 10:53:51 2006 +0100
+++ b/mcabber/src/utils.h	Sun Nov 19 11:51:14 2006 +0100
@@ -9,6 +9,11 @@
 #define from_utf8(s) ((s) ? g_convert_with_fallback((s), -1, LocaleCharSet, \
                                         "UTF-8", NULL,NULL,NULL,NULL) : NULL)
 
+#define JID_RESOURCE_SEPARATOR      '/'
+#define JID_RESOURCE_SEPARATORSTR   "/"
+#define JID_DOMAIN_SEPARATOR        '@'
+#define JID_DOMAIN_SEPARATORSTR     "@"
+
 void ut_InitDebug(int level, const char *file);
 void ut_WriteLog(unsigned int flag, const char *data);