aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile23
-rwxr-xr-xconfigure12
-rw-r--r--lib/misc.c8
-rw-r--r--lib/proxy.c1
-rw-r--r--protocols/skype/.bzrignore19
-rw-r--r--protocols/skype/.mailmap1
-rw-r--r--protocols/skype/HACKING26
-rw-r--r--protocols/skype/Makefile107
-rw-r--r--protocols/skype/NEWS131
-rw-r--r--protocols/skype/README488
-rw-r--r--protocols/skype/asciidoc.conf21
-rw-r--r--protocols/skype/client.sh1
-rw-r--r--protocols/skype/skype.c1565
-rw-r--r--protocols/skype/skyped.cnf40
-rw-r--r--protocols/skype/skyped.py491
-rw-r--r--protocols/skype/skyped.txt52
-rw-r--r--protocols/skype/t/Makefile33
-rw-r--r--protocols/skype/t/bitlbee.conf0
-rwxr-xr-xprotocols/skype/t/irssi/livetest-irssi.sh109
-rw-r--r--protocols/skype/t/irssi/skype-call.test13
-rw-r--r--protocols/skype/t/irssi/skype-info.test12
-rw-r--r--protocols/skype/t/irssi/skype-login.test10
-rw-r--r--protocols/skype/t/irssi/skype-msg.test17
-rw-r--r--protocols/skype/t/irssi/trigger.pl1225
-rwxr-xr-xprotocols/skype/t/livetest-bitlbee.sh116
25 files changed, 4515 insertions, 6 deletions
diff --git a/Makefile b/Makefile
index 052a4ad9..e8049fa7 100644
--- a/Makefile
+++ b/Makefile
@@ -26,8 +26,11 @@ endif
# Expansion of variables
subdirobjs = $(foreach dir,$(subdirs),$(dir)/$(dir).o)
-all: $(OUTFILE) $(OTR_PI) systemd
+all: $(OUTFILE) $(OTR_PI) $(SKYPE_PI) systemd
$(MAKE) -C doc
+ifdef SKYPE_PI
+ $(MAKE) -C protocols/skype doc
+endif
uninstall: uninstall-bin uninstall-doc
@echo -e '\nmake uninstall does not remove files in '$(DESTDIR)$(ETCDIR)', you can use make uninstall-etc to do that.\n'
@@ -70,9 +73,15 @@ lcov: check
install-doc:
$(MAKE) -C doc install
+ifdef SKYPE_PI
+ $(MAKE) -C protocols/skype install-doc
+endif
uninstall-doc:
$(MAKE) -C doc uninstall
+ifdef SKYPE_PI
+ $(MAKE) -C protocols/skype uninstall-doc
+endif
install-bin:
mkdir -p $(DESTDIR)$(BINDIR)
@@ -108,6 +117,14 @@ ifdef OTR_PI
mkdir -p $(DESTDIR)$(PLUGINDIR)
install -m 0755 otr.so $(DESTDIR)$(PLUGINDIR)
endif
+ifdef SKYPE_PI
+ mkdir -p $(DESTDIR)$(PLUGINDIR)
+ install -m 0755 skype.so $(DESTDIR)$(PLUGINDIR)
+ mkdir -p $(DESTDIR)$(ETCDIR)/../skyped
+ install -m 0644 $(SRCDIR)protocols/skype/skyped.cnf $(DESTDIR)$(ETCDIR)/../skyped/skyped.cnf
+ install -m 0644 $(SRCDIR)protocols/skype/skyped.conf $(DESTDIR)$(ETCDIR)/../skyped/skyped.conf
+ install -m 0755 $(SRCDIR)protocols/skype/skyped.py $(DESTDIR)$(BINDIR)/skyped
+endif
systemd:
ifdef SYSTEMDSYSTEMUNITDIR
@@ -140,6 +157,10 @@ $(OTR_PI): %.so: $(SRCDIR)%.c
@echo '*' Building plugin $@
@$(CC) $(CFLAGS) $(OTRFLAGS) -fPIC -shared $(LDFLAGS) $< -o $@
+$(SKYPE_PI): $(SRCDIR)protocols/skype/skype.c
+ @echo '*' Building plugin skype
+ @$(CC) $(CFLAGS) -fPIC -shared $< -o $@
+
$(objects): %.o: $(SRCDIR)%.c
@echo '*' Compiling $<
@$(CC) -c $(CFLAGS) $< -o $@
diff --git a/configure b/configure
index 77dc560a..c10b1f6b 100755
--- a/configure
+++ b/configure
@@ -35,6 +35,7 @@ strip=1
gcov=0
plugins=1
otr=0
+skype=0
events=glib
ldap=0
@@ -82,6 +83,8 @@ Option Description Default
--plugins=0/1 Disable/enable plugins support $plugins
--otr=0/1/auto/plugin
Disable/enable OTR encryption support $otr
+--skype=0/1/plugin
+ Disable/enable Skype support $skype
--events=... Event handler (glib, libevent) $events
--ssl=... SSL library to use (gnutls, nss, openssl, bogus, auto)
@@ -109,6 +112,8 @@ pidfile=`eval echo "$pidfile" | sed 's/\/\{1,\}/\//g'`
ipcsocket=`eval echo "$ipcsocket" | sed 's/\/\{1,\}/\//g'`
pcdir=`eval echo "$pcdir" | sed 's/\/\{1,\}/\//g'`
+protocols_mods=""
+
cat<<EOF>Makefile.settings
## BitlBee settings, generated by configure
PREFIX=$prefix
@@ -537,6 +542,11 @@ elif [ "$otr" = "plugin" ]; then
echo 'OTR_PI=otr.so' >> Makefile.settings
fi
+if [ "$skype" = "1" -o "$skype" = "plugin" ]; then
+ echo 'SKYPE_PI=skype.so' >> Makefile.settings
+ protocols_mods="$protocol_mods skype(plugin)"
+fi
+
if [ ! -e doc/user-guide/help.txt ] && ! type xmlto > /dev/null 2> /dev/null; then
echo
echo 'WARNING: Building from an unreleased source tree without prebuilt helpfile.'
@@ -750,7 +760,7 @@ echo ' Using SSL library: '$ssl
#echo ' Building with these storage backends: '$STORAGES
if [ -n "$protocols" ]; then
- echo ' Building with these protocols:' $protocols
+ echo ' Building with these protocols:' $protocols$protocols_mods
case "$protocols" in
*purple*)
echo " Note that BitlBee-libpurple is supported on a best-effort basis. It's"
diff --git a/lib/misc.c b/lib/misc.c
index 05192d9c..711b927c 100644
--- a/lib/misc.c
+++ b/lib/misc.c
@@ -399,7 +399,7 @@ signed int do_iconv( char *from_cs, char *to_cs, char *src, char *dst, size_t si
cd = g_iconv_open( to_cs, from_cs );
if( cd == (GIConv) -1 )
- return( -1 );
+ return -1;
inbytesleft = size ? size : strlen( src );
outbytesleft = maxbuf - 1;
@@ -407,10 +407,10 @@ signed int do_iconv( char *from_cs, char *to_cs, char *src, char *dst, size_t si
*outbuf = '\0';
g_iconv_close( cd );
- if( res == (size_t) -1 )
- return( -1 );
+ if( res != 0 )
+ return -1;
else
- return( outbuf - dst );
+ return outbuf - dst;
}
/* A pretty reliable random number generator. Tries to use the /dev/random
diff --git a/lib/proxy.c b/lib/proxy.c
index b79afea4..3e5c9d49 100644
--- a/lib/proxy.c
+++ b/lib/proxy.c
@@ -84,6 +84,7 @@ static gboolean gaim_io_connected(gpointer data, gint source, b_input_condition
b_event_remove(phb->inpa);
closesocket(source);
dup2(new_fd, source);
+ closesocket(new_fd);
phb->inpa = b_input_add(source, B_EV_IO_WRITE, gaim_io_connected, phb);
return FALSE;
}
diff --git a/protocols/skype/.bzrignore b/protocols/skype/.bzrignore
new file mode 100644
index 00000000..e90a033b
--- /dev/null
+++ b/protocols/skype/.bzrignore
@@ -0,0 +1,19 @@
+Changelog
+HEADER.html
+*.gz
+*.asc
+.htaccess
+shot
+*.swp
+aclocal.m4
+autom4te.cache
+config.log
+config.mak
+config.status
+configure
+etc
+install-sh
+skype.so
+skyped.conf
+skyped.conf.dist
+skype.dylib*
diff --git a/protocols/skype/.mailmap b/protocols/skype/.mailmap
new file mode 100644
index 00000000..cc8d43f9
--- /dev/null
+++ b/protocols/skype/.mailmap
@@ -0,0 +1 @@
+Miklos Vajna <vmiklos@frugalware.org>
diff --git a/protocols/skype/HACKING b/protocols/skype/HACKING
new file mode 100644
index 00000000..f5516832
--- /dev/null
+++ b/protocols/skype/HACKING
@@ -0,0 +1,26 @@
+== Tabs
+
+I use the following tabs during the development:
+
+1) bitlbee-skype:
+
+vim, make, etc.
+
+2) bitlbee:
+
+gdb --args ./bitlbee -v -n -D
+run
+
+3) skyped:
+
+python skyped.py -n -d
+
+4) irssi
+
+== Get the code from git
+
+To get the code directly from git, you need:
+
+git clone git://vmiklos.hu/bitlbee-skype
+cd bitlbee-skype
+make autogen
diff --git a/protocols/skype/Makefile b/protocols/skype/Makefile
new file mode 100644
index 00000000..bebfe86c
--- /dev/null
+++ b/protocols/skype/Makefile
@@ -0,0 +1,107 @@
+-include ../../Makefile.settings
+ifdef SRCDIR
+SRCDIR := $(SRCDIR)protocls/skype/
+endif
+
+VERSION = 0.9.0
+DATE := $(shell date +%Y-%m-%d)
+# latest stable
+BITLBEE_VERSION = 3.0.1
+
+ifeq ($(ASCIIDOC),yes)
+MANPAGES = skyped.1
+else
+MANPAGES =
+endif
+
+ifeq ($(BITLBEE),yes)
+LIBS = skype.$(SHARED_EXT)
+else
+LIBS =
+endif
+
+all: $(LIBS) $(MANPAGES)
+
+skype.$(SHARED_EXT): skype.c config.mak
+ifeq ($(BITLBEE),yes)
+ $(CC) $(CFLAGS) $(SHARED_FLAGS) -o skype.$(SHARED_EXT) skype.c $(LDFLAGS)
+endif
+
+install: all
+ifeq ($(ASCIIDOC),yes)
+ $(INSTALL) -d $(DESTDIR)$(mandir)/man1
+ $(INSTALL) -m644 $(MANPAGES) $(DESTDIR)$(mandir)/man1
+endif
+ifeq ($(BITLBEE),yes)
+ $(INSTALL) -d $(DESTDIR)$(plugindir)
+ $(INSTALL) skype.$(SHARED_EXT) $(DESTDIR)$(plugindir)
+endif
+ifeq ($(SKYPE4PY),yes)
+ $(INSTALL) -d $(DESTDIR)$(bindir)
+ $(INSTALL) -d $(DESTDIR)$(sysconfdir)
+ $(INSTALL) skyped.py $(DESTDIR)$(bindir)/skyped
+ perl -p -i -e 's|/usr/local/etc/skyped|$(sysconfdir)|' $(DESTDIR)$(bindir)/skyped
+ $(INSTALL) -m644 skyped.conf.dist $(DESTDIR)$(sysconfdir)/skyped.conf
+ perl -p -i -e 's|\$${prefix}|$(prefix)|' $(DESTDIR)$(sysconfdir)/skyped.conf
+ $(INSTALL) -m644 skyped.cnf $(DESTDIR)$(sysconfdir)
+endif
+
+client: client.c
+
+autogen: configure.ac
+ cp $(shell ls /usr/share/automake-*/install-sh | tail -n1) ./
+ autoconf
+
+clean:
+ rm -f $(LIBS) $(MANPAGES)
+
+distclean: clean
+ rm -f config.log config.mak config.status
+
+autoclean: distclean
+ rm -rf aclocal.m4 autom4te.cache configure install-sh
+
+# take this from the kernel
+check:
+ perl checkpatch.pl --no-tree --file skype.c
+
+test: all
+ $(MAKE) -C t/ all
+
+dist:
+ git archive --format=tar --prefix=bitlbee-skype-$(VERSION)/ HEAD | tar xf -
+ mkdir -p bitlbee-skype-$(VERSION)
+ git log --no-merges |git name-rev --tags --stdin > bitlbee-skype-$(VERSION)/Changelog
+ make -C bitlbee-skype-$(VERSION) autogen
+ tar czf bitlbee-skype-$(VERSION).tar.gz bitlbee-skype-$(VERSION)
+ rm -rf bitlbee-skype-$(VERSION)
+
+release:
+ git tag $(VERSION)
+ $(MAKE) dist
+ gpg --comment "See http://vmiklos.hu/gpg/ for info" \
+ -ba bitlbee-skype-$(VERSION).tar.gz
+
+doc: skyped.1
+
+install-doc:
+ mkdir -p $(DESTDIR)$(MANDIR)/man1/
+ install -m 0644 $(SRCDIR)skyped.1 $(DESTDIR)$(MANDIR)/man1/
+
+uninstall-doc:
+ rm -f $(DESTDIR)$(MANDIR)/man1/skyped.1*
+
+HEADER.html: README Makefile
+ asciidoc -a toc -a numbered -a sectids -o HEADER.html -a icons -a data-uri --attribute iconsdir=./images/icons README
+ sed -i 's|@VERSION@|$(VERSION)|g' HEADER.html
+ sed -i 's|@BITLBEE_VERSION@|$(BITLBEE_VERSION)|g' HEADER.html
+
+Changelog: .git/refs/heads/master
+ git log --no-merges |git name-rev --tags --stdin >Changelog
+
+AUTHORS: .git/refs/heads/master
+ git shortlog -s -n |sed 's/.*\t//'> AUTHORS
+
+%.1: %.txt asciidoc.conf
+ a2x --asciidoc-opts="-f asciidoc.conf" \
+ -a bs_version=$(VERSION) -a bs_date=$(DATE) -f manpage $<
diff --git a/protocols/skype/NEWS b/protocols/skype/NEWS
new file mode 100644
index 00000000..b55b34c6
--- /dev/null
+++ b/protocols/skype/NEWS
@@ -0,0 +1,131 @@
+VERSION DESCRIPTION
+-----------------------------------------------------------------------------
+0.9.0 - merge support for building the plugin on OpenBSD
+ - merge support for running skyped without gobject and
+ pygnutls/pyopenssl - as a side effect this adds Windows support
+ - add /ctcp call|hangup support (you need BitlBee from bzr to use
+ this)
+ - add group support (see http://wiki.bitlbee.org/UiFix)
+0.8.4 - now using python2.7 directly in case python would point to python3k
+ - merge patch to avoid a crash when failing to connect to skyped
+ - merge support for building the plugin on NetBSD
+ - merge Debian patches
+0.8.3 - support for BitlBee 1.3dev
+ - fixed --debug switch (-d was fine)
+ - documentation fixes
+0.8.2 - building documentation is now optional
+ - new settings: test_join and show_moods
+ - '~' in skyped.conf is now expanded to the user's home directory
+ - groupchat channel names are now persistent (requires
+ BitlBee-1.2.6)
+0.8.1 - support for BitlBee 1.2.5
+ - support for Skype 2.1.0.81 and Skype4Py 1.0.32.0
+ - the plugin part now supports FreeBSD
+ - fix for edited messages, the prefix can now be configured
+0.8.0 - fix build on x86_64 (-fPIC usage)
+ - debug messages now have a timestamp
+ - more work on having the default config under ~/.skyped
+ - added a manual page for skyped
+0.7.2 - add --log option to skyped to allow logging while it the
+ daemon is in the background.
+ - prefer config files from ~/.skyped over /etc/skyped
+ - handle the case when LANG and LC_ALL env vars are empty
+0.7.1 - mostly internal changes, the monster read callback is
+ now replaced by tiny parser functions
+0.7.0 - made 'make config' more portable
+ - add 'skypeconsole' buddy for debugging purposes
+ - support autojoin for bookmarked groupchats
+ - skyped: make hardwired '/dev/null' portable and fix
+ Python-2.6 warnings
+0.6.3 - various osx-specific improvements (see the new screenshot!)
+ - added python-gnutls install instructions
+ - bitlbee.pc is now searched under
+ /usr/local/lib/pkgconfig by default to help LFS monkeys ;)
+0.6.2 - bugfix: make install required the plugin even in case
+ its build was disabled
+0.6.1 - added keepalive traffic to avoid disconnects in bitlbee
+ when there is no traffic for a long time
+ - now the plugin or skyped is automatically disabled if
+ the dependencies are not available; useful in case the
+ plugin is to be installed on a public server, or the
+ skyped is to be used with a public server only
+0.6.0 - works with BitlBee 1.2.1
+0.5.1 - configure now automatically detects the right prefix to
+ match witl BitlBee's one
+ - minor documentation improvements (public chats, bug
+ reporting address)
+0.5.0 - skyped now uses gnutls if possible, which seem to be
+ more stable, compared to openssl.
+ - skyped now tries to handle all read/write errors from/to
+ clients, and always just warn about failures, never exit.
+ - installation for Debian users should be more simple
+ - improved documentation
+ - this .0 release should be quite stable, only about 100
+ lines of new code
+0.4.2 - skyped should be now more responsive
+ - new skypeout_offline setting for hiding/showing SkypeOut
+ contacts
+ - support for SkypeOut calls
+ - support for querying the balance from Skype
+ - all setting should be documented now
+0.4.1 - support for building the plugin on Mac OSX
+ - tested with BitlBee 1.2 and Skype 2.0.0.63
+ - avoid ${prefix} (by autoconf) in the config file as we
+ don't handle such a variable
+ - now you can call echo123 (patch by Riskó Gergely)
+0.4.0 - support for starting, accepting and rejecting calls
+ - also updated documentation (the key is the account set
+ skype/call command)
+ - as usual with the .0 releases, be careful, ~200 lines of
+ new code
+0.3.2 - support for Skype 2.0.0.43
+ - skyped now automatically starts/shuts down skype
+ - improved 'make prepare' to handle more automake versions
+ - documentation improvements
+0.3.1 - beautify output when skyped is interrupted using ^C
+ - 'nick skype foo' now really sets display name, not the mood
+ text
+ - documentation fixups
+ - this version should be again as stable as 0.2.6 was
+0.3.0 - authentication support in skyped via ssl
+ - ~200 lines of new code, so be careful :)
+ - upgraders: please read the documentation about how to set up
+ your config the ssl certificate, this was no necessary till now
+0.2.6 - the server setting has a default value, 'localhost' so in most
+ cases you no longer have to set it explicitly
+ - support for receiving emoted messages, ie. when the user types
+ '/me foo'
+ - support for setting the display name (nick 0 "foo bar") - it
+ sets the mood text
+0.2.5 - now bitlbee's info command is supported (it displays full name,
+ birthday, homepage, age, etc.)
+0.2.4 - improve documentation based on feedback from people on #bitlbee
+ - fixed for Skype4Py >= 0.9.28.4
+ - tested with latest Skype beta, too (the one which supports
+ video)
+0.2.3 - fixed that annoying "creating groupchat failed" warning
+0.2.2 - don't change the topic if skype does not report a successful
+ topic change
+ - fixed for the recent bitlbee API changes
+0.2.1 - topic support in group chats
+ - bugfixes for multiline messages
+ - this version should be again as stable as 0.1.4 was
+0.2.0 - group chat support
+ - ~300 lines of new code, so be careful :)
+ - the version number mentions that this is not a minor change
+0.1.4 - documentation: mention the version of all deps (requirements
+ section)
+ - fix sending / sending accents
+ - don't use internal functions of skype4py
+ - skyped no longer dies when skype is killed
+0.1.3 - support for edited messages
+ - ignore empty messages (skype does the same)
+ - support for multiline messages
+ - switch to the x11 api instead of dbus (it's much more stable)
+0.1.2 - notification when a new call arrives in
+ - more documentation (vnc)
+ - first release which works with unpatched bitlbee
+0.1.1 - skyped now runs as daemon in the background by default
+ - skyped now automatically reconnects on Skype restarts
+0.1.0 - initial release
+ - see README for major features
diff --git a/protocols/skype/README b/protocols/skype/README
new file mode 100644
index 00000000..2c962d54
--- /dev/null
+++ b/protocols/skype/README
@@ -0,0 +1,488 @@
+= Skype plugin for BitlBee
+Miklos Vajna <vmiklos-at-vmiklos-dot-hu>
+
+== Status
+
+[quote, Wilmer van der Gaast (author of BitlBee)]
+____
+Okay, this exists now, with lots of thanks to vmiklos for his *excellent*
+work!!
+
+It's not in the main BitlBee and it'll never be for various reasons, but
+because it's a plugin that shouldn't be a problem.
+____
+
+One day I browsed the BitlBee bugtracker and found
+http://bugs.bitlbee.org/bitlbee/ticket/82[this] ticket. Then after a while I
+returned and saw that it was still open. So I wrote it.
+
+It's pretty stable (one day I wanted to restart it because of an upgrade
+and just noticed it was running for 2+ months without crashing), I use
+it for my daily work. Being a plug-in, no patching is required, you can
+just install it after installing BitlBee itself.
+
+NOTE: You will see that this implementation of the Skype plug-in still requires
+a Skype instance to be running. This is because I'm not motivated to reverse
+engineer Skype's
+http://en.wikipedia.org/wiki/Skype_Protocol#Obfuscation_Layer[obfuscation
+layer]. (Not mentioning that you should ask your lawyer about if it is legal or
+not..)
+
+== Requirements
+
+* Skype >= 1.4.0.99. The latest version I've tested is 2.1.0.81.
+* BitlBee >= 3.0. The latest version I've tested is @BITLBEE_VERSION@. Use
+ old versions (see the NEWS file about which one) if you have older BitlBee
+ installed.
+* Skype4Py >= 0.9.28.7. Previous versions won't work due to API changes.
+ The latest version I've tested is 1.0.32.0.
+
+* Python >= 2.5. Skype4Py does not work with 2.4.
+
+* OS: `bitlbee-skype` has been tested under Linux and Mac OS X. The plugin part
+ has been tested under Free/Open/NetBSD as well. The daemon part has been
+ tested on Windows, too.
+
+== How to set it up
+
+Before you start. The setup is the following: BitlBee can't connect directly to
+Skype servers (the company's ones). It needs a running Skype client to do so.
+In fact BitlBee will connect to `skyped` (a tcp server, provided in this
+package) and `skyped` will connect to to your Skype client.
+
+The benefit of this architecture is that you can run Skype and `skyped`
+on a machine different to the one where you run BitlBee (it can be even
+a public server) and/or your IRC client.
+
+NOTE: The order is important. First `skyped` starts Skype. Then `skyped`
+connects to Skype, finally BitlBee can connect to `skyped`.
+
+=== Installing under Frugalware or Debian
+
+- Install the necessary packages:
+
+----
+# pacman-g2 -S bitlbee-skype
+----
+
+or
+
+----
+# apt-get install skyped bitlbee-plugin-skype
+----
+
+(the later from the unstable repo)
+
+and you don't have to compile anything manually.
+
+=== Installing under OS X
+
+- Install the necessary packages from ports:
+
+NOTE: You have to edit the Portfile manually to include the install-dev target,
+just append install-dev after install-etc.
+
+----
+# port -v install bitlbee
+----
+
+and you have to install `bitlbee-skype` and `skype4py` from
+source.
+
+=== Installing from source
+
+NOTE: bitlbee-skype by default builds and installs skyped and the
+plugin. In case you just want to install the plugin for a public server
+or you want to use skyped with a public server (like
+`bitlbee1.asnetinc.net`), you don't need both.
+
+- You need the latest stable BitlBee release (unless you want to use a
+ public server):
+
+----
+$ wget http://get.bitlbee.org/src/bitlbee-@BITLBEE_VERSION@.tar.gz
+$ tar xf bitlbee-@BITLBEE_VERSION@.tar.gz
+$ cd bitlbee-@BITLBEE_VERSION@
+----
+
+- Now compile and install it:
+
+----
+$ ./configure
+$ make
+# make install install-dev
+----
+
+- To install http://skype4py.sourceforge.net/[Skype4Py] from source
+ (unless you want to install the plugin for a public server):
+
+----
+$ tar -zxvf Skype4Py-x.x.x.x.tar.gz
+$ cd Skype4Py-x.x.x.x
+# python setup.py install
+----
+
+- Get the plugin code (in an empty dir, or whereever you want, it does
+ not matter):
+
+----
+$ wget http://vmiklos.hu/project/bitlbee-skype/bitlbee-skype-@VERSION@.tar.gz
+$ tar xf bitlbee-skype-@VERSION@.tar.gz
+$ cd bitlbee-skype-@VERSION@
+----
+
+- Compile and install it:
+
+----
+$ ./configure
+$ make
+# make install
+----
+
+This will install the plugin to where BitlBee expects them, which is
+`/usr/local/lib/bitlbee` if you installed BitlBee from source.
+
+=== Configuring
+
+- Set up `~/.skyped/skyped.conf`: Create the `~/.skyped` directory, copy
+ `skyped.conf` and `skyped.cnf` from
+ `/usr/local/etc/skyped/skyped.conf` to `~/.skyped`, adjust `username`
+ and `password`. The `username` should be your Skype login and the
+ `password` can be whatever you want, but you will have to specify that
+ one when adding the Skype account to BitlBee (see later).
+
+NOTE: Here, and later - `/usr/local/etc` can be different on your installation
+if you used the `--sysconfdir` switch when running bitlbee-skype's `configure`.
+
+- Generate the SSL pem files:
+
+----
+# cd ~/.skyped
+# openssl req -new -x509 -days 365 -nodes -config skyped.cnf -out skyped.cert.pem \
+ -keyout skyped.key.pem
+----
+
+NOTE: Maybe you want to adjust the permissions in the `~/.skyped`
+dir. For example make it readable by just your user.
+
+- Start `skyped` (the tcp server):
+
+----
+$ skyped
+----
+
+- Start your `IRC` client, connect to BitlBee and add your account:
+
+----
+account add skype <user> <pass>
+account skype set server localhost
+----
+
+<user> should be your Skype account name, <pass> should be the one you declared
+in `skyped.conf`. If you want to run skyped on a remote machine, replace
+`localhost` with the name of the machine.
+
+If you are running skyped on a custom port:
+
+----
+account skype set port <port>
+----
+
+If you want to set your full name (optional):
+
+----
+account skype set display_name "John Smith"
+----
+
+If you want to see your skypeout contacts online as well (they are
+offline by default):
+
+----
+account skype set skypeout_offline false
+----
+
+== Setting up Skype in a VNC server (optional)
+
+Optionally, if you want to run Skype on a server, you might want to setup up
+a `VNC` server as well. I used `tightvnc` but probably other `VNC` servers will
+work, too.
+
+First run
+
+----
+$ vncpasswd ~/.vnc/passwd
+----
+
+and create a password. You will need it at least once.
+
+Now create `~/.vnc/xstartup` with the following contents:
+
+----
+#!/bin/sh
+
+blackbox
+----
+
+Adjust the permissions:
+
+----
+$ chmod +x ~/.vnc/xstartup
+----
+
+Then start the server:
+
+----
+$ vncserver
+----
+
+Then connect to it, start an `xterm`, set up Skype (username, password,
+enable X11 API and allow the `Skype4Py` client), quit from Skype, and
+start `skyped`. If you want to watch its traffic, enable debug messages
+and foreground mode:
+
+----
+$ skyped -n -d
+----
+
+== Features
+
+- Download nicks and away statuses from Skype
+
+- Noticing joins / parts while we're connected
+
+- Sending messages
+
+- Receiving messages
+
+- Receiving away status changes
+
+- `skyped` (the tcp daemon that is a gateway between Skype and tcp)
+
+- Error handling when `skyped` is not running and when it exits
+
+- Marking received messages as seen so that Skype won't say there are unread messages
+
+- Adding / removing contacts
+
+- Set away state when you do a `/away`.
+
+- When you `account off`, Skype will set status to `Offline`
+
+- When you `account on`, Skype will set status to `Online`
+
+- Detect when somebody wants to add you and ask for confirmation
+
+- Detect when somebody wants to transfer a file
+
+- Group chat support:
+
+ * Detect if we're invited
+
+ * Send / receive group chat messages
+
+ * Invite others (using `/invite <nick>`)
+
+ * Part from group chats
+
+ * Starting a group chat (using `/j #nick`)
+
+- Topic changes in group chats:
+
+ * Show the current topic (if any) on join
+
+ * Notice when someone changes the topic
+
+ * Support changing the topic using `/topic`
+
+- Viewing the profile using the `info` command.
+
+- Handling skype actions (when the `CHATMESSAGE` has `EMOTED` type)
+
+- Setting your display name using the `nick` command.
+
+- Running Skype on a machine different to BitlBee is possible, the
+ communication is encrypted.
+
+- Managing outgoing calls (with call duration at the end):
+
+ * `/ctcp nick call`
+ * `/ctcp nick hangup`
+
+- Managing outgoing SkypeOut or conference calls:
+
+ * `account skype set call +18005551234`
+ * `account skype set call nick1 nick2`
+ * `account skype set -del call`
+
+- Managing incoming calls via questions, just like when you add / remove
+ contacts.
+
+- Querying the current SkypeOut balance:
+
+ * `account skype set balance query`
+
+- For debug purposes, it's possible to send any command to `skyped`. To
+ achieve this, you need to:
+
+ * `account skype set skypeconsole true`
+
+ * then writing `skypeconsole: <command>` will work in the control
+ channel.
+
+ * `account skype set skypeconsole_receive true` will make the
+ `skypeconsole` account dump all the recieved raw traffic for you
+
+- If you want to automatically join bookmarked groupchats right after
+ you logged in, do:
+
+ * `account skype set auto_join true`
+
+- Edited messages are shown with the `EDIT:` prefix. If you don't like
+ this, you can set your own prefix using:
+
+ * `account skype set edit_prefix "updated message:"`
+
+- The `echo123` test account is hidden by default. If you want to see it:
+
+ * `account skype set test_join true`
+
+- Mood texts are not shown by default. If you want to see it:
+
+ * `account skype set show_moods true`
+
+- Group support:
+
+ * Skype groups are told to BitlBee
+ * The usual `/invite` in a group channel adds the buddy to the group in skype
+ as well (and if necessary, it creates a new group in Skype)
+
+== What needs to be done (aka. TODO)
+
+- Notice if foo invites bar. Currently you can see only that bar joined.
+
+- Public chats. See
+ link:https://developer.skype.com/jira/browse/SCL-381[this feature
+ request], this is because it is still not possible (under Linux) to
+ `join_chat` to a public chat..
+
+- Add yasrd (Yet Another Skype-Related Daemon) to allow using a public
+ server for users who are behind NAT.
+
+== I would like to have support for ...
+
+If something does not work and it's not in the TODO section, then please
+contact me! Please also try the link:HACKING[git version] before reporting a bug, your
+problem may be already fixed there.
+
+In fact, of course, I wrote this documentation after figured out how to do this
+setup, so maybe I left out some steps. If you needed 'any' additional tricks,
+then it would be nice to include them here.
+
+== Known bugs
+
+- File transfers are view-only from BitlBee. Quoting the
+ https://developer.skype.com/Docs/ApiDoc/FILETRANSFER_object[relevant
+ documentation]: 'File transfers cannot be initiated nor accepted via
+ API commands.' So it's not something I can add support for, sadly.
+
+== Screenshots
+
+You can reach some screenshots link:shot[here].
+
+== Additional resources
+
+You can reach the Changelog link:Changelog[here], and a gitweb interface
+http://vmiklos.hu/gitweb/?p=bitlbee-skype.git[here].
+
+The Skype API documentation is
+http://developer.skype.com/resources/public_api_ref.zip[here] if you're
+interested.
+
+== Testimonials
+
+----
+00:56 < scathe> I like your skype plugin :)
+----
+
+----
+It's really working great so far.
+
+Good Job and thank you!
+Sebastian
+----
+
+----
+Big respect for your work, i really appreciate it.
+
+Martin
+----
+
+----
+Thanks for bitlbee-skype. As a blind Linux user, I cannot use the
+skype GUI client because qt apps ar not accessible yet with the
+available screen readers. bitlbee-skype allows me to make use of skype
+without having to interact much with the GUI client, which helps me a
+lot.
+
+Lukas
+----
+
+----
+02:12 < newton> i must say, i love this little bee ;)
+02:15 < newton> tried it out today with the skype plugin, good work!
+----
+
+----
+18:10 < miCSu> it works fine
+----
+
+----
+13:56 < seo> i just want to thank you :)
+13:56 < seo> for bitlbee-skype
+13:57 < seo> it's working very well, so, again, thank you for your work, and for sharing it
+----
+
+----
+22:16 < ecraven> vmiklos: thanks a lot for the skype plugin for bitlbee!
+----
+
+----
+I'm blind and so I have to use a screen reader, in my case Gnome-Orca.
+But since Skype is written in QT, while Orca uses gtk+, I have no direct
+access to the Skype interface. That's why I desided to use Skyped and
+Erc.
+The text console is fully accessible.
+Thank you very much.
+
+Hermann
+----
+
+----
+i love that bitlbeeplugin. big thx for that.
+
+michael
+----
+
+----
+23:47 < krisfremen> thanks for creating this fabulous piece of software vmiklos :)
+----
+
+== Thanks
+
+to the following people:
+
+* people in link:AUTHORS[AUTHORS] for their contributions
+
+* Arkadiusz Wahlig, author of skype4py, for making suggestions to skyped
+
+* Gabor Adam Toth (tg), for noticing extra code is needed to handle multiline
+ messages
+
+* Cristobal Palmer (tarheelcoxn), for helping to testing the plugin in a
+ timezone different to mine
+
+* people on `#bitlbee` for feedback
+
+Back to my link:/projects[projects page].
+
+// vim: ft=asciidoc
diff --git a/protocols/skype/asciidoc.conf b/protocols/skype/asciidoc.conf
new file mode 100644
index 00000000..24a649c1
--- /dev/null
+++ b/protocols/skype/asciidoc.conf
@@ -0,0 +1,21 @@
+ifdef::doctype-manpage[]
+ifdef::backend-docbook[]
+[header]
+template::[header-declarations]
+<refentry>
+ <refentryinfo>
+ <date>{bs_date}</date>
+ </refentryinfo>
+ <refmeta>
+ <refentrytitle>{mantitle}</refentrytitle>
+ <manvolnum>{manvolnum}</manvolnum>
+ <refmiscinfo class="source">bitlbee-skype</refmiscinfo>
+ <refmiscinfo class="version">{bs_version}</refmiscinfo>
+ <refmiscinfo class="manual">bitlbee-skype manual</refmiscinfo>
+ </refmeta>
+ <refnamediv>
+ <refname>{manname}</refname>
+ <refpurpose>{manpurpose}</refpurpose>
+ </refnamediv>
+endif::backend-docbook[]
+endif::doctype-manpage[]
diff --git a/protocols/skype/client.sh b/protocols/skype/client.sh
new file mode 100644
index 00000000..7d7689a8
--- /dev/null
+++ b/protocols/skype/client.sh
@@ -0,0 +1 @@
+openssl s_client -host localhost -port 2727 -verify 0
diff --git a/protocols/skype/skype.c b/protocols/skype/skype.c
new file mode 100644
index 00000000..6a3e6393
--- /dev/null
+++ b/protocols/skype/skype.c
@@ -0,0 +1,1565 @@
+/*
+ * skype.c - Skype plugin for BitlBee
+ *
+ * Copyright (c) 2007, 2008, 2009, 2010, 2011 by Miklos Vajna <vmiklos@frugalware.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+ * USA.
+ */
+
+#define _XOPEN_SOURCE
+#define _BSD_SOURCE
+#include <poll.h>
+#include <bitlbee.h>
+#include <ssl_client.h>
+
+#define SKYPE_DEFAULT_SERVER "localhost"
+#define SKYPE_DEFAULT_PORT "2727"
+#define IRC_LINE_SIZE 1024
+#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
+
+/*
+ * Enumerations
+ */
+
+enum {
+ SKYPE_CALL_RINGING = 1,
+ SKYPE_CALL_MISSED,
+ SKYPE_CALL_CANCELLED,
+ SKYPE_CALL_FINISHED,
+ SKYPE_CALL_REFUSED
+};
+
+enum {
+ SKYPE_FILETRANSFER_NEW = 1,
+ SKYPE_FILETRANSFER_FAILED
+};
+
+/*
+ * Structures
+ */
+
+struct skype_data {
+ struct im_connection *ic;
+ char *username;
+ /* The effective file descriptor. We store it here so any function can
+ * write() to it. */
+ int fd;
+ /* File descriptor returned by bitlbee. we store it so we know when
+ * we're connected and when we aren't. */
+ int bfd;
+ /* ssl_getfd() uses this to get the file desciptor. */
+ void *ssl;
+ /* When we receive a new message id, we query the properties, finally
+ * the chatname. Store the properties here so that we can use
+ * imcb_buddy_msg() when we got the chatname. */
+ char *handle;
+ /* List, because of multiline messages. */
+ GList *body;
+ char *type;
+ /* This is necessary because we send a notification when we get the
+ * handle. So we store the state here and then we can send a
+ * notification about the handle is in a given status. */
+ int call_status;
+ char *call_id;
+ char *call_duration;
+ /* If the call is outgoing or not */
+ int call_out;
+ /* Same for file transfers. */
+ int filetransfer_status;
+ /* Using /j #nick we want to have a groupchat with two people. Usually
+ * not (default). */
+ char *groupchat_with;
+ /* The user who invited us to the chat. */
+ char *adder;
+ /* If we are waiting for a confirmation about we changed the topic. */
+ int topic_wait;
+ /* These are used by the info command. */
+ char *info_fullname;
+ char *info_phonehome;
+ char *info_phoneoffice;
+ char *info_phonemobile;
+ char *info_nrbuddies;
+ char *info_tz;
+ char *info_seen;
+ char *info_birthday;
+ char *info_sex;
+ char *info_language;
+ char *info_country;
+ char *info_province;
+ char *info_city;
+ char *info_homepage;
+ char *info_about;
+ /* When a call fails, we get the reason and later we get the failure
+ * event, so store the failure code here till then */
+ int failurereason;
+ /* If this is just an update of an already received message. */
+ int is_edit;
+ /* List of struct skype_group* */
+ GList *groups;
+ /* Pending user which has to be added to the next group which is
+ * created. */
+ char *pending_user;
+};
+
+struct skype_away_state {
+ char *code;
+ char *full_name;
+};
+
+struct skype_buddy_ask_data {
+ struct im_connection *ic;
+ /* This is also used for call IDs for simplicity */
+ char *handle;
+};
+
+struct skype_group {
+ int id;
+ char *name;
+ GList *users;
+};
+
+/*
+ * Tables
+ */
+
+const struct skype_away_state skype_away_state_list[] = {
+ { "AWAY", "Away" },
+ { "NA", "Not available" },
+ { "DND", "Do Not Disturb" },
+ { "INVISIBLE", "Invisible" },
+ { "OFFLINE", "Offline" },
+ { "SKYPEME", "Skype Me" },
+ { "ONLINE", "Online" },
+ { NULL, NULL}
+};
+
+/*
+ * Functions
+ */
+
+int skype_write(struct im_connection *ic, char *buf, int len)
+{
+ struct skype_data *sd = ic->proto_data;
+ struct pollfd pfd[1];
+
+ if (!sd->ssl)
+ return FALSE;
+
+ pfd[0].fd = sd->fd;
+ pfd[0].events = POLLOUT;
+
+ /* This poll is necessary or we'll get a SIGPIPE when we write() to
+ * sd->fd. */
+ poll(pfd, 1, 1000);
+ if (pfd[0].revents & POLLHUP) {
+ imc_logout(ic, TRUE);
+ return FALSE;
+ }
+ ssl_write(sd->ssl, buf, len);
+
+ return TRUE;
+}
+
+int skype_printf(struct im_connection *ic, char *fmt, ...)
+{
+ va_list args;
+ char str[IRC_LINE_SIZE];
+
+ va_start(args, fmt);
+ vsnprintf(str, IRC_LINE_SIZE, fmt, args);
+ va_end(args);
+
+ return skype_write(ic, str, strlen(str));
+}
+
+static void skype_buddy_ask_yes(void *data)
+{
+ struct skype_buddy_ask_data *bla = data;
+ skype_printf(bla->ic, "SET USER %s ISAUTHORIZED TRUE",
+ bla->handle);
+ g_free(bla->handle);
+ g_free(bla);
+}
+
+static void skype_buddy_ask_no(void *data)
+{
+ struct skype_buddy_ask_data *bla = data;
+ skype_printf(bla->ic, "SET USER %s ISAUTHORIZED FALSE",
+ bla->handle);
+ g_free(bla->handle);
+ g_free(bla);
+}
+
+void skype_buddy_ask(struct im_connection *ic, char *handle, char *message)
+{
+ struct skype_buddy_ask_data *bla = g_new0(struct skype_buddy_ask_data,
+ 1);
+ char *buf;
+
+ bla->ic = ic;
+ bla->handle = g_strdup(handle);
+
+ buf = g_strdup_printf("The user %s wants to add you to "
+ "his/her buddy list, saying: '%s'.", handle, message);
+ imcb_ask(ic, buf, bla, skype_buddy_ask_yes, skype_buddy_ask_no);
+ g_free(buf);
+}
+
+static void skype_call_ask_yes(void *data)
+{
+ struct skype_buddy_ask_data *bla = data;
+ skype_printf(bla->ic, "SET CALL %s STATUS INPROGRESS",
+ bla->handle);
+ g_free(bla->handle);
+ g_free(bla);
+}
+
+static void skype_call_ask_no(void *data)
+{
+ struct skype_buddy_ask_data *bla = data;
+ skype_printf(bla->ic, "SET CALL %s STATUS FINISHED",
+ bla->handle);
+ g_free(bla->handle);
+ g_free(bla);
+}
+
+void skype_call_ask(struct im_connection *ic, char *call_id, char *message)
+{
+ struct skype_buddy_ask_data *bla = g_new0(struct skype_buddy_ask_data,
+ 1);
+
+ bla->ic = ic;
+ bla->handle = g_strdup(call_id);
+
+ imcb_ask(ic, message, bla, skype_call_ask_yes, skype_call_ask_no);
+}
+
+static char *skype_call_strerror(int err)
+{
+ switch (err) {
+ case 1:
+ return "Miscellaneous error";
+ case 2:
+ return "User or phone number does not exist.";
+ case 3:
+ return "User is offline";
+ case 4:
+ return "No proxy found";
+ case 5:
+ return "Session terminated.";
+ case 6:
+ return "No common codec found.";
+ case 7:
+ return "Sound I/O error.";
+ case 8:
+ return "Problem with remote sound device.";
+ case 9:
+ return "Call blocked by recipient.";
+ case 10:
+ return "Recipient not a friend.";
+ case 11:
+ return "Current user not authorized by recipient.";
+ case 12:
+ return "Sound recording error.";
+ default:
+ return "Unknown error";
+ }
+}
+
+static char *skype_group_by_username(struct im_connection *ic, char *username)
+{
+ struct skype_data *sd = ic->proto_data;
+ int i, j;
+
+ /* NEEDSWORK: we just search for the first group of the user, multiple
+ * groups / user is not yet supported by BitlBee. */
+
+ for (i = 0; i < g_list_length(sd->groups); i++) {
+ struct skype_group *sg = g_list_nth_data(sd->groups, i);
+ for (j = 0; j < g_list_length(sg->users); j++) {
+ if (!strcmp(g_list_nth_data(sg->users, j), username))
+ return sg->name;
+ }
+ }
+ return NULL;
+}
+
+static struct skype_group *skype_group_by_name(struct im_connection *ic, char *name)
+{
+ struct skype_data *sd = ic->proto_data;
+ int i;
+
+ for (i = 0; i < g_list_length(sd->groups); i++) {
+ struct skype_group *sg = g_list_nth_data(sd->groups, i);
+ if (!strcmp(sg->name, name))
+ return sg;
+ }
+ return NULL;
+}
+
+static void skype_parse_users(struct im_connection *ic, char *line)
+{
+ char **i, **nicks;
+
+ nicks = g_strsplit(line + 6, ", ", 0);
+ for (i = nicks; *i; i++)
+ skype_printf(ic, "GET USER %s ONLINESTATUS\n", *i);
+ g_strfreev(nicks);
+}
+
+static void skype_parse_user(struct im_connection *ic, char *line)
+{
+ int flags = 0;
+ char *ptr;
+ struct skype_data *sd = ic->proto_data;
+ char *user = strchr(line, ' ');
+ char *status = strrchr(line, ' ');
+
+ status++;
+ ptr = strchr(++user, ' ');
+ if (!ptr)
+ return;
+ *ptr = '\0';
+ ptr++;
+ if (!strncmp(ptr, "ONLINESTATUS ", 13)) {
+ if (!strcmp(user, sd->username))
+ return;
+ if (!set_getbool(&ic->acc->set, "test_join")
+ && !strcmp(user, "echo123"))
+ return;
+ ptr = g_strdup_printf("%s@skype.com", user);
+ imcb_add_buddy(ic, ptr, skype_group_by_username(ic, user));
+ if (strcmp(status, "OFFLINE") && (strcmp(status, "SKYPEOUT") ||
+ !set_getbool(&ic->acc->set, "skypeout_offline")))
+ flags |= OPT_LOGGED_IN;
+ if (strcmp(status, "ONLINE") && strcmp(status, "SKYPEME"))
+ flags |= OPT_AWAY;
+ imcb_buddy_status(ic, ptr, flags, NULL, NULL);
+ g_free(ptr);
+ } else if (!strncmp(ptr, "RECEIVEDAUTHREQUEST ", 20)) {
+ char *message = ptr + 20;
+ if (strlen(message))
+ skype_buddy_ask(ic, user, message);
+ } else if (!strncmp(ptr, "BUDDYSTATUS ", 12)) {
+ char *st = ptr + 12;
+ if (!strcmp(st, "3")) {
+ char *buf = g_strdup_printf("%s@skype.com", user);
+ imcb_add_buddy(ic, buf, skype_group_by_username(ic, user));
+ g_free(buf);
+ }
+ } else if (!strncmp(ptr, "MOOD_TEXT ", 10)) {
+ char *buf = g_strdup_printf("%s@skype.com", user);
+ bee_user_t *bu = bee_user_by_handle(ic->bee, ic, buf);
+ g_free(buf);
+ buf = ptr + 10;
+ if (bu)
+ imcb_buddy_status(ic, bu->handle, bu->flags, NULL,
+ *buf ? buf : NULL);
+ if (set_getbool(&ic->acc->set, "show_moods"))
+ imcb_log(ic, "User `%s' changed mood text to `%s'", user, buf);
+ } else if (!strncmp(ptr, "FULLNAME ", 9))
+ sd->info_fullname = g_strdup(ptr + 9);
+ else if (!strncmp(ptr, "PHONE_HOME ", 11))
+ sd->info_phonehome = g_strdup(ptr + 11);
+ else if (!strncmp(ptr, "PHONE_OFFICE ", 13))
+ sd->info_phoneoffice = g_strdup(ptr + 13);
+ else if (!strncmp(ptr, "PHONE_MOBILE ", 13))
+ sd->info_phonemobile = g_strdup(ptr + 13);
+ else if (!strncmp(ptr, "NROF_AUTHED_BUDDIES ", 20))
+ sd->info_nrbuddies = g_strdup(ptr + 20);
+ else if (!strncmp(ptr, "TIMEZONE ", 9))
+ sd->info_tz = g_strdup(ptr + 9);
+ else if (!strncmp(ptr, "LASTONLINETIMESTAMP ", 20))
+ sd->info_seen = g_strdup(ptr + 20);
+ else if (!strncmp(ptr, "BIRTHDAY ", 9))
+ sd->info_birthday = g_strdup(ptr + 9);
+ else if (!strncmp(ptr, "SEX ", 4))
+ sd->info_sex = g_strdup(ptr + 4);
+ else if (!strncmp(ptr, "LANGUAGE ", 9))
+ sd->info_language = g_strdup(ptr + 9);
+ else if (!strncmp(ptr, "COUNTRY ", 8))
+ sd->info_country = g_strdup(ptr + 8);
+ else if (!strncmp(ptr, "PROVINCE ", 9))
+ sd->info_province = g_strdup(ptr + 9);
+ else if (!strncmp(ptr, "CITY ", 5))
+ sd->info_city = g_strdup(ptr + 5);
+ else if (!strncmp(ptr, "HOMEPAGE ", 9))
+ sd->info_homepage = g_strdup(ptr + 9);
+ else if (!strncmp(ptr, "ABOUT ", 6)) {
+ sd->info_about = g_strdup(ptr + 6);
+
+ GString *st = g_string_new("Contact Information\n");
+ g_string_append_printf(st, "Skype Name: %s\n", user);
+ if (sd->info_fullname) {
+ if (strlen(sd->info_fullname))
+ g_string_append_printf(st, "Full Name: %s\n",
+ sd->info_fullname);
+ g_free(sd->info_fullname);
+ }
+ if (sd->info_phonehome) {
+ if (strlen(sd->info_phonehome))
+ g_string_append_printf(st, "Home Phone: %s\n",
+ sd->info_phonehome);
+ g_free(sd->info_phonehome);
+ }
+ if (sd->info_phoneoffice) {
+ if (strlen(sd->info_phoneoffice))
+ g_string_append_printf(st, "Office Phone: %s\n",
+ sd->info_phoneoffice);
+ g_free(sd->info_phoneoffice);
+ }
+ if (sd->info_phonemobile) {
+ if (strlen(sd->info_phonemobile))
+ g_string_append_printf(st, "Mobile Phone: %s\n",
+ sd->info_phonemobile);
+ g_free(sd->info_phonemobile);
+ }
+ g_string_append_printf(st, "Personal Information\n");
+ if (sd->info_nrbuddies) {
+ if (strlen(sd->info_nrbuddies))
+ g_string_append_printf(st,
+ "Contacts: %s\n", sd->info_nrbuddies);
+ g_free(sd->info_nrbuddies);
+ }
+ if (sd->info_tz) {
+ if (strlen(sd->info_tz)) {
+ char ib[256];
+ time_t t = time(NULL);
+ t += atoi(sd->info_tz)-(60*60*24);
+ struct tm *gt = gmtime(&t);
+ strftime(ib, 256, "%H:%M:%S", gt);
+ g_string_append_printf(st,
+ "Local Time: %s\n", ib);
+ }
+ g_free(sd->info_tz);
+ }
+ if (sd->info_seen) {
+ if (strlen(sd->info_seen)) {
+ char ib[256];
+ time_t it = atoi(sd->info_seen);
+ struct tm *tm = localtime(&it);
+ strftime(ib, 256, ("%Y. %m. %d. %H:%M"), tm);
+ g_string_append_printf(st,
+ "Last Seen: %s\n", ib);
+ }
+ g_free(sd->info_seen);
+ }
+ if (sd->info_birthday) {
+ if (strlen(sd->info_birthday) &&
+ strcmp(sd->info_birthday, "0")) {
+ char ib[256];
+ struct tm tm;
+ strptime(sd->info_birthday, "%Y%m%d", &tm);
+ strftime(ib, 256, "%B %d, %Y", &tm);
+ g_string_append_printf(st,
+ "Birthday: %s\n", ib);
+
+ strftime(ib, 256, "%Y", &tm);
+ int year = atoi(ib);
+ time_t t = time(NULL);
+ struct tm *lt = localtime(&t);
+ g_string_append_printf(st,
+ "Age: %d\n", lt->tm_year+1900-year);
+ }
+ g_free(sd->info_birthday);
+ }
+ if (sd->info_sex) {
+ if (strlen(sd->info_sex)) {
+ char *iptr = sd->info_sex;
+ while (*iptr++)
+ *iptr = tolower(*iptr);
+ g_string_append_printf(st,
+ "Gender: %s\n", sd->info_sex);
+ }
+ g_free(sd->info_sex);
+ }
+ if (sd->info_language) {
+ if (strlen(sd->info_language)) {
+ char *iptr = strchr(sd->info_language, ' ');
+ if (iptr)
+ iptr++;
+ else
+ iptr = sd->info_language;
+ g_string_append_printf(st,
+ "Language: %s\n", iptr);
+ }
+ g_free(sd->info_language);
+ }
+ if (sd->info_country) {
+ if (strlen(sd->info_country)) {
+ char *iptr = strchr(sd->info_country, ' ');
+ if (iptr)
+ iptr++;
+ else
+ iptr = sd->info_country;
+ g_string_append_printf(st,
+ "Country: %s\n", iptr);
+ }
+ g_free(sd->info_country);
+ }
+ if (sd->info_province) {
+ if (strlen(sd->info_province))
+ g_string_append_printf(st,
+ "Region: %s\n", sd->info_province);
+ g_free(sd->info_province);
+ }
+ if (sd->info_city) {
+ if (strlen(sd->info_city))
+ g_string_append_printf(st,
+ "City: %s\n", sd->info_city);
+ g_free(sd->info_city);
+ }
+ if (sd->info_homepage) {
+ if (strlen(sd->info_homepage))
+ g_string_append_printf(st,
+ "Homepage: %s\n", sd->info_homepage);
+ g_free(sd->info_homepage);
+ }
+ if (sd->info_about) {
+ if (strlen(sd->info_about))
+ g_string_append_printf(st, "%s\n",
+ sd->info_about);
+ g_free(sd->info_about);
+ }
+ imcb_log(ic, "%s", st->str);
+ g_string_free(st, TRUE);
+ }
+}
+
+static void skype_parse_chatmessage(struct im_connection *ic, char *line)
+{
+ struct skype_data *sd = ic->proto_data;
+ char buf[IRC_LINE_SIZE];
+ char *id = strchr(line, ' ');
+
+ if (!++id)
+ return;
+ char *info = strchr(id, ' ');
+
+ if (!info)
+ return;
+ *info = '\0';
+ info++;
+ if (!strcmp(info, "STATUS RECEIVED") || !strncmp(info, "EDITED_TIMESTAMP", 16)) {
+ /* New message ID:
+ * (1) Request its from field
+ * (2) Request its body
+ * (3) Request its type
+ * (4) Query chatname
+ */
+ skype_printf(ic, "GET CHATMESSAGE %s FROM_HANDLE\n", id);
+ if (!strcmp(info, "STATUS RECEIVED"))
+ skype_printf(ic, "GET CHATMESSAGE %s BODY\n", id);
+ else
+ sd->is_edit = 1;
+ skype_printf(ic, "GET CHATMESSAGE %s TYPE\n", id);
+ skype_printf(ic, "GET CHATMESSAGE %s CHATNAME\n", id);
+ } else if (!strncmp(info, "FROM_HANDLE ", 12)) {
+ info += 12;
+ /* New from field value. Store
+ * it, then we can later use it
+ * when we got the message's
+ * body. */
+ g_free(sd->handle);
+ sd->handle = g_strdup_printf("%s@skype.com", info);
+ } else if (!strncmp(info, "EDITED_BY ", 10)) {
+ info += 10;
+ /* This is the same as
+ * FROM_HANDLE, except that we
+ * never request these lines
+ * from Skype, we just get
+ * them. */
+ g_free(sd->handle);
+ sd->handle = g_strdup_printf("%s@skype.com", info);
+ } else if (!strncmp(info, "BODY ", 5)) {
+ info += 5;
+ sd->body = g_list_append(sd->body, g_strdup(info));
+ } else if (!strncmp(info, "TYPE ", 5)) {
+ info += 5;
+ g_free(sd->type);
+ sd->type = g_strdup(info);
+ } else if (!strncmp(info, "CHATNAME ", 9)) {
+ info += 9;
+ if (sd->handle && sd->body && sd->type) {
+ struct groupchat *gc = bee_chat_by_title(ic->bee, ic, info);
+ int i;
+ for (i = 0; i < g_list_length(sd->body); i++) {
+ char *body = g_list_nth_data(sd->body, i);
+ if (!strcmp(sd->type, "SAID") ||
+ !strcmp(sd->type, "EMOTED")) {
+ if (!strcmp(sd->type, "SAID")) {
+ if (!sd->is_edit)
+ g_snprintf(buf, IRC_LINE_SIZE, "%s",
+ body);
+ else {
+ g_snprintf(buf, IRC_LINE_SIZE, "%s %s",
+ set_getstr(&ic->acc->set, "edit_prefix"),
+ body);
+ sd->is_edit = 0;
+ }
+ } else
+ g_snprintf(buf, IRC_LINE_SIZE, "/me %s",
+ body);
+ if (!gc)
+ /* Private message */
+ imcb_buddy_msg(ic,
+ sd->handle, buf, 0, 0);
+ else
+ /* Groupchat message */
+ imcb_chat_msg(gc,
+ sd->handle, buf, 0, 0);
+ } else if (!strcmp(sd->type, "SETTOPIC") && gc)
+ imcb_chat_topic(gc,
+ sd->handle, body, 0);
+ else if (!strcmp(sd->type, "LEFT") && gc)
+ imcb_chat_remove_buddy(gc,
+ sd->handle, NULL);
+ }
+ g_list_free(sd->body);
+ sd->body = NULL;
+ }
+ }
+}
+
+static void skype_parse_call(struct im_connection *ic, char *line)
+{
+ struct skype_data *sd = ic->proto_data;
+ char *id = strchr(line, ' ');
+ char buf[IRC_LINE_SIZE];
+
+ if (!++id)
+ return;
+ char *info = strchr(id, ' ');
+
+ if (!info)
+ return;
+ *info = '\0';
+ info++;
+ if (!strncmp(info, "FAILUREREASON ", 14))
+ sd->failurereason = atoi(strchr(info, ' '));
+ else if (!strcmp(info, "STATUS RINGING")) {
+ if (sd->call_id)
+ g_free(sd->call_id);
+ sd->call_id = g_strdup(id);
+ skype_printf(ic, "GET CALL %s PARTNER_HANDLE\n", id);
+ sd->call_status = SKYPE_CALL_RINGING;
+ } else if (!strcmp(info, "STATUS MISSED")) {
+ skype_printf(ic, "GET CALL %s PARTNER_HANDLE\n", id);
+ sd->call_status = SKYPE_CALL_MISSED;
+ } else if (!strcmp(info, "STATUS CANCELLED")) {
+ skype_printf(ic, "GET CALL %s PARTNER_HANDLE\n", id);
+ sd->call_status = SKYPE_CALL_CANCELLED;
+ } else if (!strcmp(info, "STATUS FINISHED")) {
+ skype_printf(ic, "GET CALL %s PARTNER_HANDLE\n", id);
+ sd->call_status = SKYPE_CALL_FINISHED;
+ } else if (!strcmp(info, "STATUS REFUSED")) {
+ skype_printf(ic, "GET CALL %s PARTNER_HANDLE\n", id);
+ sd->call_status = SKYPE_CALL_REFUSED;
+ } else if (!strcmp(info, "STATUS UNPLACED")) {
+ if (sd->call_id)
+ g_free(sd->call_id);
+ /* Save the ID for later usage (Cancel/Finish). */
+ sd->call_id = g_strdup(id);
+ sd->call_out = TRUE;
+ } else if (!strcmp(info, "STATUS FAILED")) {
+ imcb_error(ic, "Call failed: %s",
+ skype_call_strerror(sd->failurereason));
+ sd->call_id = NULL;
+ } else if (!strncmp(info, "DURATION ", 9)) {
+ if (sd->call_duration)
+ g_free(sd->call_duration);
+ sd->call_duration = g_strdup(info+9);
+ } else if (!strncmp(info, "PARTNER_HANDLE ", 15)) {
+ info += 15;
+ if (!sd->call_status)
+ return;
+ switch (sd->call_status) {
+ case SKYPE_CALL_RINGING:
+ if (sd->call_out)
+ imcb_log(ic, "You are currently ringing "
+ "the user %s.", info);
+ else {
+ g_snprintf(buf, IRC_LINE_SIZE,
+ "The user %s is currently ringing you.",
+ info);
+ skype_call_ask(ic, sd->call_id, buf);
+ }
+ break;
+ case SKYPE_CALL_MISSED:
+ imcb_log(ic, "You have missed a call from user %s.",
+ info);
+ break;
+ case SKYPE_CALL_CANCELLED:
+ imcb_log(ic, "You cancelled the call to the user %s.",
+ info);
+ sd->call_status = 0;
+ sd->call_out = FALSE;
+ break;
+ case SKYPE_CALL_REFUSED:
+ if (sd->call_out)
+ imcb_log(ic, "The user %s refused the call.",
+ info);
+ else
+ imcb_log(ic,
+ "You refused the call from user %s.",
+ info);
+ sd->call_out = FALSE;
+ break;
+ case SKYPE_CALL_FINISHED:
+ if (sd->call_duration)
+ imcb_log(ic,
+ "You finished the call to the user %s "
+ "(duration: %s seconds).",
+ info, sd->call_duration);
+ else
+ imcb_log(ic,
+ "You finished the call to the user %s.",
+ info);
+ sd->call_out = FALSE;
+ break;
+ default:
+ /* Don't be noisy, ignore other statuses for now. */
+ break;
+ }
+ sd->call_status = 0;
+ }
+}
+
+static void skype_parse_filetransfer(struct im_connection *ic, char *line)
+{
+ struct skype_data *sd = ic->proto_data;
+ char *id = strchr(line, ' ');
+
+ if (!++id)
+ return;
+ char *info = strchr(id, ' ');
+
+ if (!info)
+ return;
+ *info = '\0';
+ info++;
+ if (!strcmp(info, "STATUS NEW")) {
+ skype_printf(ic, "GET FILETRANSFER %s PARTNER_HANDLE\n",
+ id);
+ sd->filetransfer_status = SKYPE_FILETRANSFER_NEW;
+ } else if (!strcmp(info, "STATUS FAILED")) {
+ skype_printf(ic, "GET FILETRANSFER %s PARTNER_HANDLE\n",
+ id);
+ sd->filetransfer_status = SKYPE_FILETRANSFER_FAILED;
+ } else if (!strncmp(info, "PARTNER_HANDLE ", 15)) {
+ info += 15;
+ if (!sd->filetransfer_status)
+ return;
+ switch (sd->filetransfer_status) {
+ case SKYPE_FILETRANSFER_NEW:
+ imcb_log(ic, "The user %s offered a new file for you.",
+ info);
+ break;
+ case SKYPE_FILETRANSFER_FAILED:
+ imcb_log(ic, "Failed to transfer file from user %s.",
+ info);
+ break;
+ }
+ sd->filetransfer_status = 0;
+ }
+}
+
+static struct skype_group *skype_group_by_id(struct im_connection *ic, int id)
+{
+ struct skype_data *sd = ic->proto_data;
+ int i;
+
+ for (i = 0; i < g_list_length(sd->groups); i++) {
+ struct skype_group *sg = (struct skype_group *)g_list_nth_data(sd->groups, i);
+
+ if (sg->id == id)
+ return sg;
+ }
+ return NULL;
+}
+
+static void skype_group_free(struct skype_group *sg, gboolean usersonly)
+{
+ int i;
+
+ for (i = 0; i < g_list_length(sg->users); i++) {
+ char *user = g_list_nth_data(sg->users, i);
+ g_free(user);
+ }
+ sg->users = NULL;
+ if (usersonly)
+ return;
+ g_free(sg->name);
+ g_free(sg);
+}
+
+/* Update the group of each user in this group */
+static void skype_group_users(struct im_connection *ic, struct skype_group *sg)
+{
+ int i;
+
+ for (i = 0; i < g_list_length(sg->users); i++) {
+ char *user = g_list_nth_data(sg->users, i);
+ char *buf = g_strdup_printf("%s@skype.com", user);
+ imcb_add_buddy(ic, buf, sg->name);
+ g_free(buf);
+ }
+}
+
+static void skype_parse_group(struct im_connection *ic, char *line)
+{
+ struct skype_data *sd = ic->proto_data;
+ char *id = strchr(line, ' ');
+
+ if (!++id)
+ return;
+
+ char *info = strchr(id, ' ');
+
+ if (!info)
+ return;
+ *info = '\0';
+ info++;
+
+ if (!strncmp(info, "DISPLAYNAME ", 12)) {
+ info += 12;
+
+ /* Name given for a group ID: try to update it or insert a new
+ * one if not found */
+ struct skype_group *sg = skype_group_by_id(ic, atoi(id));
+ if (sg) {
+ g_free(sg->name);
+ sg->name = g_strdup(info);
+ } else {
+ sg = g_new0(struct skype_group, 1);
+ sg->id = atoi(id);
+ sg->name = g_strdup(info);
+ sd->groups = g_list_append(sd->groups, sg);
+ }
+ } else if (!strncmp(info, "USERS ", 6)) {
+ struct skype_group *sg = skype_group_by_id(ic, atoi(id));
+
+ if (sg) {
+ char **i;
+ char **users = g_strsplit(info + 6, ", ", 0);
+
+ skype_group_free(sg, TRUE);
+ i = users;
+ while (*i) {
+ sg->users = g_list_append(sg->users, g_strdup(*i));
+ i++;
+ }
+ g_strfreev(users);
+ skype_group_users(ic, sg);
+ } else
+ log_message(LOGLVL_ERROR,
+ "No skype group with id %s. That's probably a bug.", id);
+ } else if (!strncmp(info, "NROFUSERS ", 10)) {
+ if (!sd->pending_user) {
+ /* Number of users changed in this group, query its type to see
+ * if it's a custom one we should care about. */
+ skype_printf(ic, "GET GROUP %s TYPE", id);
+ return;
+ }
+
+ /* This is a newly created group, we have a single user
+ * to add. */
+ struct skype_group *sg = skype_group_by_id(ic, atoi(id));
+
+ if (sg) {
+ skype_printf(ic, "ALTER GROUP %d ADDUSER %s", sg->id, sd->pending_user);
+ g_free(sd->pending_user);
+ sd->pending_user = NULL;
+ } else
+ log_message(LOGLVL_ERROR,
+ "No skype group with id %s. That's probably a bug.", id);
+ } else if (!strcmp(info, "TYPE CUSTOM_GROUP"))
+ /* This one is interesting, query its users. */
+ skype_printf(ic, "GET GROUP %s USERS", id);
+}
+
+static void skype_parse_chat(struct im_connection *ic, char *line)
+{
+ struct skype_data *sd = ic->proto_data;
+ char buf[IRC_LINE_SIZE];
+ char *id = strchr(line, ' ');
+
+ if (!++id)
+ return;
+ struct groupchat *gc;
+ char *info = strchr(id, ' ');
+
+ if (!info)
+ return;
+ *info = '\0';
+ info++;
+ /* Remove fake chat if we created one in skype_chat_with() */
+ gc = bee_chat_by_title(ic->bee, ic, "");
+ if (gc)
+ imcb_chat_free(gc);
+ if (!strcmp(info, "STATUS MULTI_SUBSCRIBED")) {
+ gc = bee_chat_by_title(ic->bee, ic, id);
+ if (!gc) {
+ gc = imcb_chat_new(ic, id);
+ imcb_chat_name_hint(gc, id);
+ }
+ skype_printf(ic, "GET CHAT %s ADDER\n", id);
+ skype_printf(ic, "GET CHAT %s TOPIC\n", id);
+ } else if (!strcmp(info, "STATUS DIALOG") && sd->groupchat_with) {
+ gc = imcb_chat_new(ic, id);
+ imcb_chat_name_hint(gc, id);
+ /* According to the docs this
+ * is necessary. However it
+ * does not seem the situation
+ * and it would open an extra
+ * window on our client, so
+ * just leave it out. */
+ /*skype_printf(ic, "OPEN CHAT %s\n", id);*/
+ g_snprintf(buf, IRC_LINE_SIZE, "%s@skype.com",
+ sd->groupchat_with);
+ imcb_chat_add_buddy(gc, buf);
+ imcb_chat_add_buddy(gc, sd->username);
+ g_free(sd->groupchat_with);
+ sd->groupchat_with = NULL;
+ skype_printf(ic, "GET CHAT %s ADDER\n", id);
+ skype_printf(ic, "GET CHAT %s TOPIC\n", id);
+ } else if (!strcmp(info, "STATUS UNSUBSCRIBED")) {
+ gc = bee_chat_by_title(ic->bee, ic, id);
+ if (gc)
+ gc->data = (void *)FALSE;
+ } else if (!strncmp(info, "ADDER ", 6)) {
+ info += 6;
+ g_free(sd->adder);
+ sd->adder = g_strdup_printf("%s@skype.com", info);
+ } else if (!strncmp(info, "TOPIC ", 6)) {
+ info += 6;
+ gc = bee_chat_by_title(ic->bee, ic, id);
+ if (gc && (sd->adder || sd->topic_wait)) {
+ if (sd->topic_wait) {
+ sd->adder = g_strdup(sd->username);
+ sd->topic_wait = 0;
+ }
+ imcb_chat_topic(gc, sd->adder, info, 0);
+ g_free(sd->adder);
+ sd->adder = NULL;
+ }
+ } else if (!strncmp(info, "ACTIVEMEMBERS ", 14)) {
+ info += 14;
+ gc = bee_chat_by_title(ic->bee, ic, id);
+ /* Hack! We set ->data to TRUE
+ * while we're on the channel
+ * so that we won't rejoin
+ * after a /part. */
+ if (!gc || gc->data)
+ return;
+ char **members = g_strsplit(info, " ", 0);
+ int i;
+ for (i = 0; members[i]; i++) {
+ if (!strcmp(members[i], sd->username))
+ continue;
+ g_snprintf(buf, IRC_LINE_SIZE, "%s@skype.com",
+ members[i]);
+ if (!g_list_find_custom(gc->in_room, buf,
+ (GCompareFunc)strcmp))
+ imcb_chat_add_buddy(gc, buf);
+ }
+ imcb_chat_add_buddy(gc, sd->username);
+ g_strfreev(members);
+ }
+}
+
+static void skype_parse_password(struct im_connection *ic, char *line)
+{
+ if (!strncmp(line+9, "OK", 2))
+ imcb_connected(ic);
+ else {
+ imcb_error(ic, "Authentication Failed");
+ imc_logout(ic, TRUE);
+ }
+}
+
+static void skype_parse_profile(struct im_connection *ic, char *line)
+{
+ imcb_log(ic, "SkypeOut balance value is '%s'.", line+21);
+}
+
+static void skype_parse_ping(struct im_connection *ic, char *line)
+{
+ /* Unused parameter */
+ line = line;
+ skype_printf(ic, "PONG\n");
+}
+
+static void skype_parse_chats(struct im_connection *ic, char *line)
+{
+ char **i;
+ char **chats = g_strsplit(line + 6, ", ", 0);
+
+ i = chats;
+ while (*i) {
+ skype_printf(ic, "GET CHAT %s STATUS\n", *i);
+ skype_printf(ic, "GET CHAT %s ACTIVEMEMBERS\n", *i);
+ i++;
+ }
+ g_strfreev(chats);
+}
+
+static void skype_parse_groups(struct im_connection *ic, char *line)
+{
+ char **i;
+ char **groups = g_strsplit(line + 7, ", ", 0);
+
+ i = groups;
+ while (*i) {
+ skype_printf(ic, "GET GROUP %s DISPLAYNAME\n", *i);
+ skype_printf(ic, "GET GROUP %s USERS\n", *i);
+ i++;
+ }
+ g_strfreev(groups);
+}
+
+static void skype_parse_alter_group(struct im_connection *ic, char *line)
+{
+ char *id = line + strlen("ALTER GROUP");
+
+ if (!++id)
+ return;
+
+ char *info = strchr(id, ' ');
+
+ if (!info)
+ return;
+ *info = '\0';
+ info++;
+
+ if (!strncmp(info, "ADDUSER ", 8)) {
+ struct skype_group *sg = skype_group_by_id(ic, atoi(id));
+
+ info += 8;
+ if (sg) {
+ char *buf = g_strdup_printf("%s@skype.com", info);
+ sg->users = g_list_append(sg->users, g_strdup(info));
+ imcb_add_buddy(ic, buf, sg->name);
+ g_free(buf);
+ } else
+ log_message(LOGLVL_ERROR,
+ "No skype group with id %s. That's probably a bug.", id);
+ }
+}
+
+typedef void (*skype_parser)(struct im_connection *ic, char *line);
+
+static gboolean skype_read_callback(gpointer data, gint fd,
+ b_input_condition cond)
+{
+ struct im_connection *ic = data;
+ struct skype_data *sd = ic->proto_data;
+ char buf[IRC_LINE_SIZE];
+ int st, i;
+ char **lines, **lineptr, *line;
+ static struct parse_map {
+ char *k;
+ skype_parser v;
+ } parsers[] = {
+ { "USERS ", skype_parse_users },
+ { "USER ", skype_parse_user },
+ { "CHATMESSAGE ", skype_parse_chatmessage },
+ { "CALL ", skype_parse_call },
+ { "FILETRANSFER ", skype_parse_filetransfer },
+ { "CHAT ", skype_parse_chat },
+ { "GROUP ", skype_parse_group },
+ { "PASSWORD ", skype_parse_password },
+ { "PROFILE PSTN_BALANCE ", skype_parse_profile },
+ { "PING", skype_parse_ping },
+ { "CHATS ", skype_parse_chats },
+ { "GROUPS ", skype_parse_groups },
+ { "ALTER GROUP ", skype_parse_alter_group },
+ };
+
+ /* Unused parameters */
+ fd = fd;
+ cond = cond;
+
+ if (!sd || sd->fd == -1)
+ return FALSE;
+ /* Read the whole data. */
+ st = ssl_read(sd->ssl, buf, sizeof(buf));
+ if (st > 0) {
+ buf[st] = '\0';
+ /* Then split it up to lines. */
+ lines = g_strsplit(buf, "\n", 0);
+ lineptr = lines;
+ while ((line = *lineptr)) {
+ if (!strlen(line))
+ break;
+ if (set_getbool(&ic->acc->set, "skypeconsole_receive"))
+ imcb_buddy_msg(ic, "skypeconsole", line, 0, 0);
+ for (i = 0; i < ARRAY_SIZE(parsers); i++)
+ if (!strncmp(line, parsers[i].k,
+ strlen(parsers[i].k))) {
+ parsers[i].v(ic, line);
+ break;
+ }
+ lineptr++;
+ }
+ g_strfreev(lines);
+ } else if (st == 0 || (st < 0 && !sockerr_again())) {
+ closesocket(sd->fd);
+ sd->fd = -1;
+
+ imcb_error(ic, "Error while reading from server");
+ imc_logout(ic, TRUE);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+gboolean skype_start_stream(struct im_connection *ic)
+{
+ struct skype_data *sd = ic->proto_data;
+ int st;
+
+ if (!sd)
+ return FALSE;
+
+ if (sd->bfd <= 0)
+ sd->bfd = b_input_add(sd->fd, B_EV_IO_READ,
+ skype_read_callback, ic);
+
+ /* Log in */
+ skype_printf(ic, "USERNAME %s\n", ic->acc->user);
+ skype_printf(ic, "PASSWORD %s\n", ic->acc->pass);
+
+ /* This will download all buddies and groups. */
+ st = skype_printf(ic, "SEARCH GROUPS CUSTOM\n");
+ skype_printf(ic, "SEARCH FRIENDS\n");
+
+ skype_printf(ic, "SET USERSTATUS ONLINE\n");
+
+ /* Auto join to bookmarked chats if requested.*/
+ if (set_getbool(&ic->acc->set, "auto_join"))
+ skype_printf(ic, "SEARCH BOOKMARKEDCHATS\n");
+ return st;
+}
+
+gboolean skype_connected(gpointer data, void *source, b_input_condition cond)
+{
+ struct im_connection *ic = data;
+ struct skype_data *sd = ic->proto_data;
+
+ /* Unused parameter */
+ cond = cond;
+
+ if (!source) {
+ sd->ssl = NULL;
+ imcb_error(ic, "Could not connect to server");
+ imc_logout(ic, TRUE);
+ return FALSE;
+ }
+ imcb_log(ic, "Connected to server, logging in");
+
+ return skype_start_stream(ic);
+}
+
+static void skype_login(account_t *acc)
+{
+ struct im_connection *ic = imcb_new(acc);
+ struct skype_data *sd = g_new0(struct skype_data, 1);
+
+ ic->proto_data = sd;
+
+ imcb_log(ic, "Connecting");
+ sd->ssl = ssl_connect(set_getstr(&acc->set, "server"),
+ set_getint(&acc->set, "port"), skype_connected, ic);
+ sd->fd = sd->ssl ? ssl_getfd(sd->ssl) : -1;
+ sd->username = g_strdup(acc->user);
+
+ sd->ic = ic;
+
+ if (set_getbool(&acc->set, "skypeconsole"))
+ imcb_add_buddy(ic, "skypeconsole", NULL);
+}
+
+static void skype_logout(struct im_connection *ic)
+{
+ struct skype_data *sd = ic->proto_data;
+ int i;
+
+ skype_printf(ic, "SET USERSTATUS OFFLINE\n");
+
+ while( ic->groupchats )
+ imcb_chat_free(ic->groupchats->data);
+
+ for (i = 0; i < g_list_length(sd->groups); i++) {
+ struct skype_group *sg = (struct skype_group *)g_list_nth_data(sd->groups, i);
+ skype_group_free(sg, FALSE);
+ }
+ g_free(sd->username);
+ g_free(sd->handle);
+ g_free(sd);
+ ic->proto_data = NULL;
+}
+
+static int skype_buddy_msg(struct im_connection *ic, char *who, char *message,
+ int flags)
+{
+ char *ptr, *nick;
+ int st;
+
+ /* Unused parameter */
+ flags = flags;
+
+ nick = g_strdup(who);
+ ptr = strchr(nick, '@');
+ if (ptr)
+ *ptr = '\0';
+
+ if (!strncmp(who, "skypeconsole", 12))
+ st = skype_printf(ic, "%s\n", message);
+ else
+ st = skype_printf(ic, "MESSAGE %s %s\n", nick, message);
+ g_free(nick);
+
+ return st;
+}
+
+const struct skype_away_state *skype_away_state_by_name(char *name)
+{
+ int i;
+
+ for (i = 0; skype_away_state_list[i].full_name; i++)
+ if (g_strcasecmp(skype_away_state_list[i].full_name, name) == 0)
+ return skype_away_state_list + i;
+
+ return NULL;
+}
+
+static void skype_set_away(struct im_connection *ic, char *state_txt,
+ char *message)
+{
+ const struct skype_away_state *state;
+
+ /* Unused parameter */
+ message = message;
+
+ if (state_txt == NULL)
+ state = skype_away_state_by_name("Online");
+ else
+ state = skype_away_state_by_name(state_txt);
+ skype_printf(ic, "SET USERSTATUS %s\n", state->code);
+}
+
+static GList *skype_away_states(struct im_connection *ic)
+{
+ static GList *l;
+ int i;
+
+ /* Unused parameter */
+ ic = ic;
+
+ if (l == NULL)
+ for (i = 0; skype_away_state_list[i].full_name; i++)
+ l = g_list_append(l,
+ (void *)skype_away_state_list[i].full_name);
+
+ return l;
+}
+
+static char *skype_set_display_name(set_t *set, char *value)
+{
+ account_t *acc = set->data;
+ struct im_connection *ic = acc->ic;
+
+ skype_printf(ic, "SET PROFILE FULLNAME %s", value);
+ return value;
+}
+
+static char *skype_set_balance(set_t *set, char *value)
+{
+ account_t *acc = set->data;
+ struct im_connection *ic = acc->ic;
+
+ skype_printf(ic, "GET PROFILE PSTN_BALANCE");
+ return value;
+}
+
+static void skype_call(struct im_connection *ic, char *value)
+{
+ char *nick = g_strdup(value);
+ char *ptr = strchr(nick, '@');
+
+ if (ptr)
+ *ptr = '\0';
+ skype_printf(ic, "CALL %s", nick);
+ g_free(nick);
+}
+
+static void skype_hangup(struct im_connection *ic)
+{
+ struct skype_data *sd = ic->proto_data;
+
+ if (sd->call_id) {
+ skype_printf(ic, "SET CALL %s STATUS FINISHED",
+ sd->call_id);
+ g_free(sd->call_id);
+ sd->call_id = 0;
+ } else
+ imcb_error(ic, "There are no active calls currently.");
+}
+
+static char *skype_set_call(set_t *set, char *value)
+{
+ account_t *acc = set->data;
+ struct im_connection *ic = acc->ic;
+
+ if (value)
+ skype_call(ic, value);
+ else
+ skype_hangup(ic);
+ return value;
+}
+
+static void skype_add_buddy(struct im_connection *ic, char *who, char *group)
+{
+ struct skype_data *sd = ic->proto_data;
+ char *nick, *ptr;
+
+ nick = g_strdup(who);
+ ptr = strchr(nick, '@');
+ if (ptr)
+ *ptr = '\0';
+
+ if (!group) {
+ skype_printf(ic, "SET USER %s BUDDYSTATUS 2 Please authorize me\n",
+ nick);
+ g_free(nick);
+ } else {
+ struct skype_group *sg = skype_group_by_name(ic, group);
+
+ if (!sg) {
+ /* No such group, we need to create it, then have to
+ * add the user once it's created. */
+ skype_printf(ic, "CREATE GROUP %s", group);
+ sd->pending_user = g_strdup(nick);
+ } else {
+ skype_printf(ic, "ALTER GROUP %d ADDUSER %s", sg->id, nick);
+ }
+ }
+}
+
+static void skype_remove_buddy(struct im_connection *ic, char *who, char *group)
+{
+ char *nick, *ptr;
+
+ /* Unused parameter */
+ group = group;
+
+ nick = g_strdup(who);
+ ptr = strchr(nick, '@');
+ if (ptr)
+ *ptr = '\0';
+ skype_printf(ic, "SET USER %s BUDDYSTATUS 1\n", nick);
+ g_free(nick);
+}
+
+void skype_chat_msg(struct groupchat *gc, char *message, int flags)
+{
+ struct im_connection *ic = gc->ic;
+
+ /* Unused parameter */
+ flags = flags;
+
+ skype_printf(ic, "CHATMESSAGE %s %s\n", gc->title, message);
+}
+
+void skype_chat_leave(struct groupchat *gc)
+{
+ struct im_connection *ic = gc->ic;
+ skype_printf(ic, "ALTER CHAT %s LEAVE\n", gc->title);
+ gc->data = (void *)TRUE;
+}
+
+void skype_chat_invite(struct groupchat *gc, char *who, char *message)
+{
+ struct im_connection *ic = gc->ic;
+ char *ptr, *nick;
+
+ nick = g_strdup(who);
+ ptr = strchr(nick, '@');
+ if (ptr)
+ *ptr = '\0';
+ skype_printf(ic, "ALTER CHAT %s ADDMEMBERS %s\n", gc->title, nick);
+ g_free(nick);
+}
+
+void skype_chat_topic(struct groupchat *gc, char *message)
+{
+ struct im_connection *ic = gc->ic;
+ struct skype_data *sd = ic->proto_data;
+ skype_printf(ic, "ALTER CHAT %s SETTOPIC %s\n",
+ gc->title, message);
+ sd->topic_wait = 1;
+}
+
+struct groupchat *skype_chat_with(struct im_connection *ic, char *who)
+{
+ struct skype_data *sd = ic->proto_data;
+ char *ptr, *nick;
+ nick = g_strdup(who);
+ ptr = strchr(nick, '@');
+ if (ptr)
+ *ptr = '\0';
+ skype_printf(ic, "CHAT CREATE %s\n", nick);
+ sd->groupchat_with = g_strdup(nick);
+ g_free(nick);
+ /* We create a fake chat for now. We will replace it with a real one in
+ * the real callback. */
+ return imcb_chat_new(ic, "");
+}
+
+static void skype_get_info(struct im_connection *ic, char *who)
+{
+ char *ptr, *nick;
+ nick = g_strdup(who);
+ ptr = strchr(nick, '@');
+ if (ptr)
+ *ptr = '\0';
+ skype_printf(ic, "GET USER %s FULLNAME\n", nick);
+ skype_printf(ic, "GET USER %s PHONE_HOME\n", nick);
+ skype_printf(ic, "GET USER %s PHONE_OFFICE\n", nick);
+ skype_printf(ic, "GET USER %s PHONE_MOBILE\n", nick);
+ skype_printf(ic, "GET USER %s NROF_AUTHED_BUDDIES\n", nick);
+ skype_printf(ic, "GET USER %s TIMEZONE\n", nick);
+ skype_printf(ic, "GET USER %s LASTONLINETIMESTAMP\n", nick);
+ skype_printf(ic, "GET USER %s BIRTHDAY\n", nick);
+ skype_printf(ic, "GET USER %s SEX\n", nick);
+ skype_printf(ic, "GET USER %s LANGUAGE\n", nick);
+ skype_printf(ic, "GET USER %s COUNTRY\n", nick);
+ skype_printf(ic, "GET USER %s PROVINCE\n", nick);
+ skype_printf(ic, "GET USER %s CITY\n", nick);
+ skype_printf(ic, "GET USER %s HOMEPAGE\n", nick);
+ skype_printf(ic, "GET USER %s ABOUT\n", nick);
+}
+
+static void skype_set_my_name(struct im_connection *ic, char *info)
+{
+ skype_set_display_name(set_find(&ic->acc->set, "display_name"), info);
+}
+
+static void skype_init(account_t *acc)
+{
+ set_t *s;
+
+ s = set_add(&acc->set, "server", SKYPE_DEFAULT_SERVER, set_eval_account,
+ acc);
+ s->flags |= ACC_SET_OFFLINE_ONLY;
+
+ s = set_add(&acc->set, "port", SKYPE_DEFAULT_PORT, set_eval_int, acc);
+ s->flags |= ACC_SET_OFFLINE_ONLY;
+
+ s = set_add(&acc->set, "display_name", NULL, skype_set_display_name,
+ acc);
+ s->flags |= ACC_SET_NOSAVE | ACC_SET_ONLINE_ONLY;
+
+ s = set_add(&acc->set, "call", NULL, skype_set_call, acc);
+ s->flags |= ACC_SET_NOSAVE | ACC_SET_ONLINE_ONLY;
+
+ s = set_add(&acc->set, "balance", NULL, skype_set_balance, acc);
+ s->flags |= ACC_SET_NOSAVE | ACC_SET_ONLINE_ONLY;
+
+ s = set_add(&acc->set, "skypeout_offline", "true", set_eval_bool, acc);
+
+ s = set_add(&acc->set, "skypeconsole", "false", set_eval_bool, acc);
+ s->flags |= ACC_SET_OFFLINE_ONLY;
+
+ s = set_add(&acc->set, "skypeconsole_receive", "false", set_eval_bool,
+ acc);
+ s->flags |= ACC_SET_OFFLINE_ONLY;
+
+ s = set_add(&acc->set, "auto_join", "false", set_eval_bool, acc);
+ s->flags |= ACC_SET_OFFLINE_ONLY;
+
+ s = set_add(&acc->set, "test_join", "false", set_eval_bool, acc);
+ s->flags |= ACC_SET_OFFLINE_ONLY;
+
+ s = set_add(&acc->set, "show_moods", "false", set_eval_bool, acc);
+
+ s = set_add(&acc->set, "edit_prefix", "EDIT:",
+ NULL, acc);
+}
+
+#if BITLBEE_VERSION_CODE > BITLBEE_VER(3, 0, 1)
+GList *skype_buddy_action_list(bee_user_t *bu)
+{
+ static GList *ret;
+
+ /* Unused parameter */
+ bu = bu;
+
+ if (ret == NULL) {
+ static const struct buddy_action ba[3] = {
+ {"CALL", "Initiate a call" },
+ {"HANGUP", "Hang up a call" },
+ };
+
+ ret = g_list_prepend(ret, (void *) ba + 0);
+ }
+
+ return ret;
+}
+
+void *skype_buddy_action(struct bee_user *bu, const char *action, char * const args[], void *data)
+{
+ /* Unused parameters */
+ args = args;
+ data = data;
+
+ if (!g_strcasecmp(action, "CALL"))
+ skype_call(bu->ic, bu->handle);
+ else if (!g_strcasecmp(action, "HANGUP"))
+ skype_hangup(bu->ic);
+
+ return NULL;
+}
+#endif
+
+void init_plugin(void)
+{
+ struct prpl *ret = g_new0(struct prpl, 1);
+
+ ret->name = "skype";
+ ret->login = skype_login;
+ ret->init = skype_init;
+ ret->logout = skype_logout;
+ ret->buddy_msg = skype_buddy_msg;
+ ret->get_info = skype_get_info;
+ ret->set_my_name = skype_set_my_name;
+ ret->away_states = skype_away_states;
+ ret->set_away = skype_set_away;
+ ret->add_buddy = skype_add_buddy;
+ ret->remove_buddy = skype_remove_buddy;
+ ret->chat_msg = skype_chat_msg;
+ ret->chat_leave = skype_chat_leave;
+ ret->chat_invite = skype_chat_invite;
+ ret->chat_with = skype_chat_with;
+ ret->handle_cmp = g_strcasecmp;
+ ret->chat_topic = skype_chat_topic;
+#if BITLBEE_VERSION_CODE > BITLBEE_VER(3, 0, 1)
+ ret->buddy_action_list = skype_buddy_action_list;
+ ret->buddy_action = skype_buddy_action;
+#endif
+ register_protocol(ret);
+}
diff --git a/protocols/skype/skyped.cnf b/protocols/skype/skyped.cnf
new file mode 100644
index 00000000..c7dc9098
--- /dev/null
+++ b/protocols/skype/skyped.cnf
@@ -0,0 +1,40 @@
+# create RSA certs - Server
+
+RANDFILE = skyped.rnd
+
+[ req ]
+default_bits = 1024
+encrypt_key = yes
+distinguished_name = req_dn
+x509_extensions = cert_type
+
+[ req_dn ]
+countryName = Country Name (2 letter code)
+countryName_default = HU
+countryName_min = 2
+countryName_max = 2
+
+stateOrProvinceName = State or Province Name (full name)
+stateOrProvinceName_default = Some-State
+
+localityName = Locality Name (eg, city)
+
+0.organizationName = Organization Name (eg, company)
+0.organizationName_default = Stunnel Developers Ltd
+
+organizationalUnitName = Organizational Unit Name (eg, section)
+#organizationalUnitName_default =
+
+0.commonName = Common Name (FQDN of your server)
+0.commonName_default = localhost
+
+# To create a certificate for more than one name uncomment:
+# 1.commonName = DNS alias of your server
+# 2.commonName = DNS alias of your server
+# ...
+# See http://home.netscape.com/eng/security/ssl_2.0_certificate.html
+# to see how Netscape understands commonName.
+
+[ cert_type ]
+nsCertType = server
+
diff --git a/protocols/skype/skyped.py b/protocols/skype/skyped.py
new file mode 100644
index 00000000..615d6835
--- /dev/null
+++ b/protocols/skype/skyped.py
@@ -0,0 +1,491 @@
+#!/usr/bin/env python2.7
+#
+# skyped.py
+#
+# Copyright (c) 2007, 2008, 2009, 2010, 2011 by Miklos Vajna <vmiklos@frugalware.org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+# USA.
+#
+
+import sys
+import os
+import signal
+import locale
+import time
+import socket
+import getopt
+import Skype4Py
+import hashlib
+from ConfigParser import ConfigParser, NoOptionError
+from traceback import print_exception
+import ssl
+
+__version__ = "0.1.1"
+
+try:
+ import gobject
+ hasgobject = True
+except ImportError:
+ import select
+ import threading
+ hasgobject = False
+
+def eh(type, value, tb):
+ global options
+
+ if type != KeyboardInterrupt:
+ print_exception(type, value, tb)
+ if hasgobject:
+ gobject.MainLoop().quit()
+ if options.conn:
+ options.conn.close()
+ # shut down client if it's running
+ try:
+ skype.skype.Client.Shutdown()
+ except NameError:
+ pass
+ sys.exit("Exiting.")
+
+sys.excepthook = eh
+
+def wait_for_lock(lock, timeout_to_print, timeout, msg):
+ start = time.time()
+ locked = lock.acquire(0)
+ while not(locked):
+ time.sleep(0.5)
+ if timeout_to_print and (time.time() - timeout_to_print > start):
+ dprint("%s: Waited %f seconds" % \
+ (msg, time.time() - start))
+ timeout_to_print = False
+ if timeout and (time.time() - timeout > start):
+ dprint("%s: Waited %f seconds, giving up" % \
+ (msg, time.time() - start))
+ return False
+ locked = lock.acquire(0)
+ return True
+
+def input_handler(fd, io_condition = None):
+ global options
+ global skype
+ if options.buf:
+ for i in options.buf:
+ skype.send(i.strip())
+ options.buf = None
+ if not hasgobject:
+ return True
+ else:
+ if not hasgobject:
+ close_socket = False
+ if wait_for_lock(options.lock, 3, 10, "input_handler"):
+ try:
+ input = fd.recv(1024)
+ options.lock.release()
+ except Exception, s:
+ dprint("Warning, receiving 1024 bytes failed (%s)." % s)
+ fd.close()
+ options.conn = False
+ options.lock.release()
+ return False
+ for i in input.split("\n"):
+ if i.strip() == "SET USERSTATUS OFFLINE":
+ close_socket = True
+ skype.send(i.strip())
+ return not(close_socket)
+ try:
+ input = fd.recv(1024)
+ except Exception, s:
+ dprint("Warning, receiving 1024 bytes failed (%s)." % s)
+ fd.close()
+ return False
+ for i in input.split("\n"):
+ skype.send(i.strip())
+ return True
+
+def skype_idle_handler(skype):
+ try:
+ c = skype.skype.Command("PING", Block=True)
+ skype.skype.SendCommand(c)
+ except Skype4Py.SkypeAPIError, s:
+ dprint("Warning, pinging Skype failed (%s)." % (s))
+ return True
+
+def send(sock, txt):
+ global options
+ from time import sleep
+ count = 1
+ done = False
+ if hasgobject:
+ while (not done) and (count < 10):
+ try:
+ sock.send(txt)
+ done = True
+ except Exception, s:
+ count += 1
+ dprint("Warning, sending '%s' failed (%s). count=%d" % (txt, s, count))
+ sleep(1)
+ if not done:
+ options.conn.close()
+ else:
+ while (not done) and (count < 10) and options.conn:
+ if wait_for_lock(options.lock, 3, 10, "socket send"):
+ try:
+ if options.conn: sock.send(txt)
+ options.lock.release()
+ done = True
+ except Exception, s:
+ options.lock.release()
+ count += 1
+ dprint("Warning, sending '%s' failed (%s). count=%d" % (txt, s, count))
+ sleep(1)
+ if not done:
+ if options.conn:
+ options.conn.close()
+ options.conn = False
+ return done
+
+def bitlbee_idle_handler(skype):
+ global options
+ done = False
+ if options.conn:
+ try:
+ e = "PING"
+ done = send(options.conn, "%s\n" % e)
+ except Exception, s:
+ dprint("Warning, sending '%s' failed (%s)." % (e, s))
+ if hasgobject:
+ options.conn.close()
+ else:
+ if options.conn: options.conn.close()
+ options.conn = False
+ done = False
+ if hasgobject:
+ return True
+ else:
+ return done
+ return True
+
+def server(host, port, skype = None):
+ global options
+ if ":" in host:
+ sock = socket.socket(socket.AF_INET6)
+ else:
+ sock = socket.socket()
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.bind((host, port))
+ sock.listen(1)
+ if hasgobject:
+ gobject.io_add_watch(sock, gobject.IO_IN, listener)
+ else:
+ dprint("Waiting for connection...")
+ listener(sock, skype)
+
+def listener(sock, skype):
+ global options
+ if not hasgobject:
+ if not(wait_for_lock(options.lock, 3, 10, "listener")): return False
+ rawsock, addr = sock.accept()
+ options.conn = ssl.wrap_socket(rawsock,
+ server_side=True,
+ certfile=options.config.sslcert,
+ keyfile=options.config.sslkey,
+ ssl_version=ssl.PROTOCOL_TLSv1)
+ if hasattr(options.conn, 'handshake'):
+ try:
+ options.conn.handshake()
+ except Exception:
+ if not hasgobject:
+ options.lock.release()
+ dprint("Warning, handshake failed, closing connection.")
+ return False
+ ret = 0
+ try:
+ line = options.conn.recv(1024)
+ if line.startswith("USERNAME") and line.split(' ')[1].strip() == options.config.username:
+ ret += 1
+ line = options.conn.recv(1024)
+ if line.startswith("PASSWORD") and hashlib.sha1(line.split(' ')[1].strip()).hexdigest() == options.config.password:
+ ret += 1
+ except Exception, s:
+ dprint("Warning, receiving 1024 bytes failed (%s)." % s)
+ options.conn.close()
+ if not hasgobject:
+ options.conn = False
+ options.lock.release()
+ return False
+ if ret == 2:
+ dprint("Username and password OK.")
+ options.conn.send("PASSWORD OK\n")
+ if hasgobject:
+ gobject.io_add_watch(options.conn, gobject.IO_IN, input_handler)
+ else:
+ options.lock.release()
+ serverloop(options, skype)
+ return True
+ else:
+ dprint("Username and/or password WRONG.")
+ options.conn.send("PASSWORD KO\n")
+ if not hasgobject:
+ options.conn.close()
+ options.conn = False
+ options.lock.release()
+ return False
+
+def dprint(msg):
+ from time import strftime
+ global options
+
+ now = strftime("%Y-%m-%d %H:%M:%S")
+
+ if options.debug:
+ try:
+ print now + ": " + msg
+ except Exception, s:
+ try:
+ sanitized = msg.encode("ascii", "backslashreplace")
+ except Error, s:
+ try:
+ sanitized = "hex [" + msg.encode("hex") + "]"
+ except Error, s:
+ sanitized = "[unable to print debug message]"
+ print now + "~=" + sanitized
+ sys.stdout.flush()
+ if options.log:
+ sock = open(options.log, "a")
+ sock.write("%s: %s\n" % (now, msg))
+ sock.close()
+
+class SkypeApi:
+ def __init__(self):
+ self.skype = Skype4Py.Skype()
+ self.skype.OnNotify = self.recv
+ self.skype.Client.Start()
+
+ def recv(self, msg_text):
+ global options
+ if msg_text == "PONG":
+ return
+ if "\n" in msg_text:
+ # crappy skype prefixes only the first line for
+ # multiline messages so we need to do so for the other
+ # lines, too. this is something like:
+ # 'CHATMESSAGE id BODY first line\nsecond line' ->
+ # 'CHATMESSAGE id BODY first line\nCHATMESSAGE id BODY second line'
+ prefix = " ".join(msg_text.split(" ")[:3])
+ msg_text = ["%s %s" % (prefix, i) for i in " ".join(msg_text.split(" ")[3:]).split("\n")]
+ else:
+ msg_text = [msg_text]
+ for i in msg_text:
+ try:
+ # Internally, BitlBee always uses UTF-8 and encodes/decodes as
+ # necessary to communicate with the IRC client; thus send the
+ # UTF-8 it expects
+ e = i.encode('UTF-8')
+ except:
+ # Should never happen, but it's better to send difficult to
+ # read data than crash because some message couldn't be encoded
+ e = i.encode('ascii', 'backslashreplace')
+ if options.conn:
+ dprint('<< ' + e)
+ try:
+ send(options.conn, e + "\n")
+ except Exception, s:
+ dprint("Warning, sending '%s' failed (%s)." % (e, s))
+ if options.conn: options.conn.close()
+ options.conn = False
+ else:
+ dprint('-- ' + e)
+
+ def send(self, msg_text):
+ if not len(msg_text) or msg_text == "PONG":
+ if msg_text == "PONG":
+ options.last_bitlbee_pong = time.time()
+ return
+ try:
+ # Internally, BitlBee always uses UTF-8 and encodes/decodes as
+ # necessary to communicate with the IRC client; thus decode the
+ # UTF-8 it sent us
+ e = msg_text.decode('UTF-8')
+ except:
+ # Should never happen, but it's better to send difficult to read
+ # data to Skype than to crash
+ e = msg_text.decode('ascii', 'backslashreplace')
+ dprint('>> ' + e)
+ try:
+ c = self.skype.Command(e, Block=True)
+ self.skype.SendCommand(c)
+ self.recv(c.Reply)
+ except Skype4Py.SkypeError:
+ pass
+ except Skype4Py.SkypeAPIError, s:
+ dprint("Warning, sending '%s' failed (%s)." % (e, s))
+
+class Options:
+ def __init__(self):
+ self.cfgpath = os.path.join(os.environ['HOME'], ".skyped", "skyped.conf")
+ # fall back to system-wide settings
+ self.syscfgpath = "/usr/local/etc/skyped/skyped.conf"
+ if os.path.exists(self.syscfgpath) and not os.path.exists(self.cfgpath):
+ self.cfgpath = self.syscfgpath
+ self.daemon = True
+ self.debug = False
+ self.help = False
+ self.host = "0.0.0.0"
+ self.log = None
+ self.port = None
+ self.version = False
+ # well, this is a bit hackish. we store the socket of the last connected client
+ # here and notify it. maybe later notify all connected clients?
+ self.conn = None
+ # this will be read first by the input handler
+ self.buf = None
+
+
+ def usage(self, ret):
+ print """Usage: skyped [OPTION]...
+
+skyped is a daemon that acts as a tcp server on top of a Skype instance.
+
+Options:
+ -c --config path to configuration file (default: %s)
+ -d --debug enable debug messages
+ -h --help this help
+ -H --host set the tcp host, supports IPv4 and IPv6 (default: %s)
+ -l --log set the log file in background mode (default: none)
+ -n --nofork don't run as daemon in the background
+ -p --port set the tcp port (default: %s)
+ -v --version display version information""" % (self.cfgpath, self.host, self.port)
+ sys.exit(ret)
+
+def serverloop(options, skype):
+ timeout = 1; # in seconds
+ skype_ping_period = 5
+ bitlbee_ping_period = 10
+ bitlbee_pong_timeout = 30
+ now = time.time()
+ skype_ping_start_time = now
+ bitlbee_ping_start_time = now
+ options.last_bitlbee_pong = now
+ in_error = []
+ handler_ok = True
+ while (len(in_error) == 0) and handler_ok and options.conn:
+ ready_to_read, ready_to_write, in_error = \
+ select.select([options.conn], [], [options.conn], \
+ timeout)
+ now = time.time()
+ handler_ok = len(in_error) == 0
+ if (len(ready_to_read) == 1) and handler_ok:
+ handler_ok = input_handler(ready_to_read.pop())
+ # don't ping bitlbee/skype if they already received data
+ now = time.time() # allow for the input_handler to take some time
+ bitlbee_ping_start_time = now
+ skype_ping_start_time = now
+ options.last_bitlbee_pong = now
+ if (now - skype_ping_period > skype_ping_start_time) and handler_ok:
+ handler_ok = skype_idle_handler(skype)
+ skype_ping_start_time = now
+ if now - bitlbee_ping_period > bitlbee_ping_start_time:
+ handler_ok = bitlbee_idle_handler(skype)
+ bitlbee_ping_start_time = now
+ if options.last_bitlbee_pong:
+ if (now - options.last_bitlbee_pong) > bitlbee_pong_timeout:
+ dprint("Bitlbee pong timeout")
+ # TODO is following line necessary? Should there be a options.conn.unwrap() somewhere?
+ # options.conn.shutdown()
+ if options.conn:
+ options.conn.close()
+ options.conn = False
+ else:
+ options.last_bitlbee_pong = now
+
+if __name__=='__main__':
+ options = Options()
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], "c:dhH:l:np:v", ["config=", "debug", "help", "host=", "log=", "nofork", "port=", "version"])
+ except getopt.GetoptError:
+ options.usage(1)
+ for opt, arg in opts:
+ if opt in ("-c", "--config"):
+ options.cfgpath = arg
+ elif opt in ("-d", "--debug"):
+ options.debug = True
+ elif opt in ("-h", "--help"):
+ options.help = True
+ elif opt in ("-H", "--host"):
+ options.host = arg
+ elif opt in ("-l", "--log"):
+ options.log = arg
+ elif opt in ("-n", "--nofork"):
+ options.daemon = False
+ elif opt in ("-p", "--port"):
+ options.port = int(arg)
+ elif opt in ("-v", "--version"):
+ options.version = True
+ if options.help:
+ options.usage(0)
+ elif options.version:
+ print "skyped %s" % __version__
+ sys.exit(0)
+ # parse our config
+ if not os.path.exists(options.cfgpath):
+ print "Can't find configuration file at '%s'." % options.cfgpath
+ print "Use the -c option to specify an alternate one."
+ sys.exit(1)
+ options.config = ConfigParser()
+ options.config.read(options.cfgpath)
+ options.config.username = options.config.get('skyped', 'username').split('#')[0]
+ options.config.password = options.config.get('skyped', 'password').split('#')[0]
+ options.config.sslkey = os.path.expanduser(options.config.get('skyped', 'key').split('#')[0])
+ options.config.sslcert = os.path.expanduser(options.config.get('skyped', 'cert').split('#')[0])
+ # hack: we have to parse the parameters first to locate the
+ # config file but the -p option should overwrite the value from
+ # the config file
+ try:
+ options.config.port = int(options.config.get('skyped', 'port').split('#')[0])
+ if not options.port:
+ options.port = options.config.port
+ except NoOptionError:
+ pass
+ if not options.port:
+ options.port = 2727
+ dprint("Parsing config file '%s' done, username is '%s'." % (options.cfgpath, options.config.username))
+ if options.daemon:
+ pid = os.fork()
+ if pid == 0:
+ nullin = file(os.devnull, 'r')
+ nullout = file(os.devnull, 'w')
+ os.dup2(nullin.fileno(), sys.stdin.fileno())
+ os.dup2(nullout.fileno(), sys.stdout.fileno())
+ os.dup2(nullout.fileno(), sys.stderr.fileno())
+ else:
+ print 'skyped is started on port %s, pid: %d' % (options.port, pid)
+ sys.exit(0)
+ else:
+ dprint('skyped is started on port %s' % options.port)
+ if hasgobject:
+ server(options.host, options.port)
+ try:
+ skype = SkypeApi()
+ except Skype4Py.SkypeAPIError, s:
+ sys.exit("%s. Are you sure you have started Skype?" % s)
+ if hasgobject:
+ gobject.timeout_add(2000, skype_idle_handler, skype)
+ gobject.timeout_add(60000, bitlbee_idle_handler, skype)
+ gobject.MainLoop().run()
+ else:
+ while 1:
+ options.conn = False
+ options.lock = threading.Lock()
+ server(options.host, options.port, skype)
diff --git a/protocols/skype/skyped.txt b/protocols/skype/skyped.txt
new file mode 100644
index 00000000..53f2626d
--- /dev/null
+++ b/protocols/skype/skyped.txt
@@ -0,0 +1,52 @@
+= skyped(1)
+
+== NAME
+
+skyped - allows remote control of the Skype GUI client
+
+== SYNOPSIS
+
+skyped [<options>]
+
+== DESCRIPTION
+
+Skype supports remote control of the GUI client only via X11 or DBus
+messages. This is hard in care you want remote control. This daemon
+listens on a TCP port and runs on the same machine where the GUI client
+runs. It passes all the input it gets to Skype directly, except for a
+few commands which is related to authentication. The whole communication
+is done via SSL.
+
+== CONFIGURATION
+
+See the README for information about how to configure this daemon.
+
+== OPTIONS
+
+-c, --config::
+ Path to configuration file (default: $HOME/.skyped/skyped.conf)
+
+-d, --debug::
+ Enable debug messages
+
+-h, --help::
+ Show short summary of options
+
+-H, --host::
+ Set the tcp host (default: 0.0.0.0)
+
+-l, --log::
+ Set the log file in background mode (default: none)
+
+-n, --nofork::
+ Don't run as daemon in the background
+
+-p, --port::
+ Set the tcp port (default: 2727)
+
+-v, --version::
+ Display version information
+
+== AUTHOR
+
+Written by Miklos Vajna <vmiklos@frugalware.org>
diff --git a/protocols/skype/t/Makefile b/protocols/skype/t/Makefile
new file mode 100644
index 00000000..9c5e95f9
--- /dev/null
+++ b/protocols/skype/t/Makefile
@@ -0,0 +1,33 @@
+PORT=9876
+BITLBEE=/usr/sbin/bitlbee
+
+export TEST_SKYPE_ID=user
+export TEST_SKYPE_PASSWORD=pass
+
+testfiles := $(wildcard irssi/*.test)
+tests := $(patsubst %.test,%,$(testfiles))
+
+.PHONY: $(tests)
+
+all: $(tests)
+ @echo "passed $$(echo $(testfiles)|wc -w) tests."
+
+$(tests): % : %.test
+ @echo "--- Running test $@ ---"; \
+ if [ -r "$(BITLBEE)" -a -x "$(BITLBEE)" ]; then \
+ bitlbee_binary="$(BITLBEE)"; \
+ else \
+ bitlbee_basename=`basename $(BITLBEE)`; \
+ bitlbee_binary=`which $$bitlbee_basename`; \
+ fi; \
+ if ! ./livetest-bitlbee.sh "$$bitlbee_binary" $(PORT) irssi/livetest-irssi.sh $< >$@.log; then \
+ echo Test failed, log: ;\
+ cat $@.log;\
+ exit 1;\
+ fi;\
+ echo "--- OK ---" ;\
+ sleep 1
+clean:
+ rm -r irssi/*.log bitlbeetest.pid dotirssi livetest
+
+
diff --git a/protocols/skype/t/bitlbee.conf b/protocols/skype/t/bitlbee.conf
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/protocols/skype/t/bitlbee.conf
diff --git a/protocols/skype/t/irssi/livetest-irssi.sh b/protocols/skype/t/irssi/livetest-irssi.sh
new file mode 100755
index 00000000..a8e136cf
--- /dev/null
+++ b/protocols/skype/t/irssi/livetest-irssi.sh
@@ -0,0 +1,109 @@
+#!/usr/bin/env bash
+ISCRIPT=$1
+OPT=$2
+
+[ -n "$ISCRIPT" ] || { echo Syntax: `basename "$0"` irssi-test-script; exit 1; }
+
+# Load variables from test
+eval `sed -e '1,/^###/!d;/^###/d' "$ISCRIPT"`
+
+#if [ "$OPT" == "checkvars" ]; then echo $TESTNEEDEDVARS; fi
+RET=0
+
+# Check if we have the neccessary environment variables for this test
+for var in $TESTNEEDEDVARS; do
+ if [ -z `eval echo \$\{$var\}` ]; then
+ if [ "$OPT" != "checkvars" ]; then
+ echo Need environment variable "$var" for this test.
+ exit 66
+ else
+ echo $var
+ RET=66
+ fi
+ fi
+done
+
+# if we got this far we're OK
+if [ "$OPT" == "checkvars" ]; then exit $RET; fi
+
+[ -n "$PORT" ] || { echo 'Need the bitlbee listening port as environment variable PORT'; exit 1; }
+
+# Setup the irssi dir
+(
+ rm -r dotirssi
+ mkdir -p dotirssi/scripts dotirssi/logs
+ cp "`dirname $0`"/trigger.pl dotirssi/scripts &&
+ echo 'script load trigger.pl' >dotirssi/startup
+) &>/dev/null || { echo Failed to setup irssi testdir; exit 1; }
+
+# write irssi config
+
+echo '
+
+aliases = {
+ runtest = "'`sed -e "1,/^###/d;s/@LOGIN@/$TESTLOGIN/;s/@PASSWORD@/$TESTPASSWORD/" "$ISCRIPT" | tr '\n' ';'`'";
+ expectbee = "/trigger add -publics -channels &bitlbee -regexp";
+ expectjoin = "/trigger add -joins -masks *!$0@* $1-";
+ expectmsg = "/trigger add -privmsgs -masks *!$0@* $1-";
+};
+
+servers = ( { address = "localhost"; chatnet = "local"; port = "'$PORT'"; autoconnect="yes";});
+
+settings = {
+ settings_autosave = "no";
+ core = { real_name = "bitlbee-test"; user_name = "bitlbee-test"; nick = "bitlbeetest"; };
+ "fe-text" = { actlist_sort = "refnum"; };
+};
+
+chatnets = { local = { type = "IRC"; autosendcmd = "/runtest"; }; };
+
+logs = {
+"dotirssi/logs/status.log" = { auto_open = "yes"; level = "ALL"; items = ( { type = "window"; name = "1"; } ); };
+"dotirssi/logs/control.log" = { auto_open = "yes"; level = "ALL"; items = ( { type = "target"; name = "&bitlbee"; } ); };
+' >dotirssi/config
+
+for nick in $TESTLOGNICKS; do
+ echo '
+ "dotirssi/logs/'$nick'.log" = { auto_open = "yes"; level = "ALL"; items = ( { type = "target"; name = "'$nick'"; } ); };
+ ' >>dotirssi/config
+done
+
+echo '};' >>dotirssi/config
+
+# Go!
+
+echo Running irssi...
+screen -D -m irssi --config=dotirssi/config --home=dotirssi/ &
+
+# output logs
+
+submitlogs() {
+ perl -p -i -e "s/$TESTLOGIN/---TESTLOGIN---/;s/$TESTPASSWORD/---TESTPASSWORD---/" dotirssi/logs/*.log
+
+ if [ "$OPT" == "tgz" ]; then
+ tar czf "`dirname $0`"/"`basename "$ISCRIPT"`".logs.tgz dotirssi/logs/*.log
+ elif [ "$OPT" == "ctest" ]; then
+ echo CTEST_FULL_OUTPUT
+ for log in dotirssi/logs/*.log; do
+ echo -n '<DartMeasurement name="'$log'" type="text/string"><![CDATA['
+ cat "$log"
+ echo "]]></DartMeasurement>"
+ done
+ else
+ echo Test logs: dotirssi/logs/*.log
+ fi
+}
+
+# timeout stuff
+
+t=$TESTDURATION
+intval=1
+while (( t >= intval )); do
+ sleep $intval
+ kill -0 $! &>/dev/null || { echo screen/irssi terminated.; submitlogs; bash -c "cd dotirssi/logs && $TESTCHECKRESULT" >/dev/null; exit $?; }
+ t=$(( t - $intval ))
+done
+echo Killing screen/irssi...
+kill $!
+submitlogs
+exit 22
diff --git a/protocols/skype/t/irssi/skype-call.test b/protocols/skype/t/irssi/skype-call.test
new file mode 100644
index 00000000..8f502a59
--- /dev/null
+++ b/protocols/skype/t/irssi/skype-call.test
@@ -0,0 +1,13 @@
+TESTNEEDEDVARS="TEST_SKYPE_ID TEST_SKYPE_PASSWORD"
+TESTDURATION=60
+TESTCHECKRESULT="grep '\[Test Passed\]' status.log"
+TESTLOGIN="$TEST_SKYPE_ID"
+TESTPASSWORD="$TEST_SKYPE_PASSWORD"
+### Test receiving call output
+/expectbee 'Welcome to the BitlBee' -command 'msg $$C register testing'
+/expectbee 'Account successfully created' -command 'msg $$C account add skype @LOGIN@ @PASSWORD@'
+/expectbee 'Account successfully added' -command 'msg $$C account 0 set test_join true'
+/expectbee 'test_join' -command 'msg $$C account 0 on'
+/expectjoin echo123 -command 'ctcp echo123 call'
+/expectbee 'You are currently ringing the user' -command 'ctcp echo123 hangup'
+/expectbee '(You cancelled the call|You finished the call)' -command 'quit Test Passed'
diff --git a/protocols/skype/t/irssi/skype-info.test b/protocols/skype/t/irssi/skype-info.test
new file mode 100644
index 00000000..e8507321
--- /dev/null
+++ b/protocols/skype/t/irssi/skype-info.test
@@ -0,0 +1,12 @@
+TESTNEEDEDVARS="TEST_SKYPE_ID TEST_SKYPE_PASSWORD"
+TESTDURATION=60
+TESTCHECKRESULT="grep '\[Test Passed\]' status.log"
+TESTLOGIN="$TEST_SKYPE_ID"
+TESTPASSWORD="$TEST_SKYPE_PASSWORD"
+### Test receiving info output
+/expectbee 'Welcome to the BitlBee' -command 'msg $$C register testing'
+/expectbee 'Account successfully created' -command 'msg $$C account add skype @LOGIN@ @PASSWORD@'
+/expectbee 'Account successfully added' -command 'msg $$C account 0 set test_join true'
+/expectbee 'test_join' -command 'msg $$C account 0 on'
+/expectjoin echo123 -command 'msg $$C info echo123'
+/expectbee 'Full Name: Echo / Sound Test Service' -command 'quit Test Passed'
diff --git a/protocols/skype/t/irssi/skype-login.test b/protocols/skype/t/irssi/skype-login.test
new file mode 100644
index 00000000..ca627002
--- /dev/null
+++ b/protocols/skype/t/irssi/skype-login.test
@@ -0,0 +1,10 @@
+TESTNEEDEDVARS="TEST_SKYPE_ID TEST_SKYPE_PASSWORD"
+TESTDURATION=10
+TESTCHECKRESULT="grep '\[Test Passed\]' status.log"
+TESTLOGIN="$TEST_SKYPE_ID"
+TESTPASSWORD="$TEST_SKYPE_PASSWORD"
+### Test login
+/expectbee 'Welcome to the BitlBee' -command 'msg $$C register testing'
+/expectbee 'Account successfully created' -command 'msg $$C account add skype @LOGIN@ @PASSWORD@'
+/expectbee 'Account successfully added' -command 'msg $$C account 0 on'
+/expectbee 'Logged in' -command 'quit Test Passed'
diff --git a/protocols/skype/t/irssi/skype-msg.test b/protocols/skype/t/irssi/skype-msg.test
new file mode 100644
index 00000000..d35615cd
--- /dev/null
+++ b/protocols/skype/t/irssi/skype-msg.test
@@ -0,0 +1,17 @@
+TESTNEEDEDVARS="TEST_SKYPE_ID TEST_SKYPE_PASSWORD"
+TESTDURATION=60
+TESTCHECKRESULT="grep '\[Test Passed\]' status.log"
+TESTLOGIN="$TEST_SKYPE_ID"
+TESTPASSWORD="$TEST_SKYPE_PASSWORD"
+### Test sending and receiving messages
+/expectbee 'Welcome to the BitlBee' -command 'msg $$C register testing'
+/expectbee 'Account successfully created' -command 'msg $$C account add skype @LOGIN@ @PASSWORD@'
+/expectbee 'Account successfully added' -command 'msg $$C account 0 set test_join true'
+/expectbee 'test_join' -command 'msg $$C account 0 on'
+# use builtin test service
+/expectjoin echo123 -command 'msg $$C echo123: ping, say pong'
+/expectbee 'pong' -command 'quit Test Passed'
+# use a public bot as well, just in case the above one would fail
+/expectjoin echo123 -command 'msg $$C add skype pam_bot'
+/expectjoin pam_bot -command 'msg $$C pam_bot: pambot help'
+/expectbee 'PamBot, thanks for chatting with me' -command 'quit Test Passed'
diff --git a/protocols/skype/t/irssi/trigger.pl b/protocols/skype/t/irssi/trigger.pl
new file mode 100644
index 00000000..02f8951f
--- /dev/null
+++ b/protocols/skype/t/irssi/trigger.pl
@@ -0,0 +1,1225 @@
+# trigger.pl - execute a command or replace text, triggered by an event in irssi
+# Do /TRIGGER HELP or look at http://wouter.coekaerts.be/irssi/ for help
+
+# Copyright (C) 2002-2006 Wouter Coekaerts <wouter@coekaerts.be>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+use strict;
+use Irssi 20020324 qw(command_bind command_runsub command signal_add_first signal_continue signal_stop signal_remove);
+use Text::ParseWords;
+use IO::File;
+use vars qw($VERSION %IRSSI);
+
+$VERSION = '1.0';
+%IRSSI = (
+ authors => 'Wouter Coekaerts',
+ contact => 'wouter@coekaerts.be',
+ name => 'trigger',
+ description => 'execute a command or replace text, triggered by an event in irssi',
+ license => 'GPLv2 or later',
+ url => 'http://wouter.coekaerts.be/irssi/',
+ changed => '$LastChangedDate: 2006-01-23 13:10:19 +0100 (Mon, 23 Jan 2006) $',
+);
+
+sub cmd_help {
+ Irssi::print (<<'SCRIPTHELP_EOF', MSGLEVEL_CLIENTCRAP);
+
+TRIGGER LIST
+TRIGGER SAVE
+TRIGGER RELOAD
+TRIGGER MOVE <number> <number>
+TRIGGER DELETE <number>
+TRIGGER CHANGE <number> ...
+TRIGGER ADD ...
+
+When to match:
+On which types of event to trigger:
+ These are simply specified by -name_of_the_type
+ The normal IRC event types are:
+ publics, %|privmsgs, pubactions, privactions, pubnotices, privnotices, joins, parts, quits, kicks, topics, invites, nick_changes, dcc_msgs, dcc_actions, dcc_ctcps
+ mode_channel: %|a mode on the (whole) channel (like +t, +i, +b)
+ mode_nick: %|a mode on someone in the channel (like +o, +v)
+ -all is an alias for all of those.
+ Additionally, there is:
+ rawin: %|raw text incoming from the server
+ send_command: %|commands you give to irssi
+ send_text: %|lines you type that aren't commands
+ beep: %|when irssi beeps
+ notify_join: %|someone in you notify list comes online
+ notify_part: %|someone in your notify list goes offline
+ notify_away: %|someone in your notify list goes away
+ notify_unaway: %|someone in your notify list goes unaway
+ notify_unidle: %|someone in your notify list stops idling
+
+Filters (conditions) the event has to satisfy. They all take one parameter.
+If you can give a list, seperate elements by space and use quotes around the list.
+ -pattern: %|The message must match the given pattern. ? and * can be used as wildcards
+ -regexp: %|The message must match the given regexp. (see man perlre)
+ %|if -nocase is given as an option, the regexp or pattern is matched case insensitive
+ -tags: %|The servertag must be in the given list of tags
+ -channels: %|The event must be in one of the given list of channels.
+ Examples: %|-channels '#chan1 #chan2' or -channels 'IRCNet/#channel'
+ %|-channels 'EFNet/' means every channel on EFNet and is the same as -tags 'EFNet'
+ -masks: %|The person who triggers it must match one of the given list of masks
+ -hasmode: %|The person who triggers it must have the give mode
+ Examples: %|'-o' means not opped, '+ov' means opped OR voiced, '-o&-v' means not opped AND not voiced
+ -hasflag: %|Only trigger if if friends.pl (friends_shasta.pl) or people.pl is loaded and the person who triggers it has the given flag in the script (same syntax as -hasmode)
+ -other_masks
+ -other_hasmode
+ -other_hasflag: %|Same as above but for the victim for kicks or mode_nick.
+
+What to do when it matches:
+ -command: Execute the given Irssi-command
+ %|You are able to use $1, $2 and so on generated by your regexp pattern.
+ %|For multiple commands ; (or $;) can be used as seperator
+ %|The following variables are also expanded:
+ $T: %|Server tag
+ $C: %|Channel name
+ $N: %|Nickname of the person who triggered this command
+ $A: %|His address (foo@bar.com),
+ $I: %|His ident (foo)
+ $H: %|His hostname (bar.com)
+ $M: %|The complete message
+ ${other}: %|The victim for kicks or mode_nick
+ ${mode_type}: %|The type ('+' or '-') for a mode_channel or mode_nick
+ ${mode_char}: %|The mode char ('o' for ops, 'b' for ban,...)
+ ${mode_arg} : %|The argument to the mode (if there is one)
+ %|$\X, with X being one of the above expands (e.g. $\M), escapes all non-alphanumeric characters, so it can be used with /eval or /exec. Don't use /eval or /exec without this, it's not safe.
+
+ -replace: %|replaces the matching part with the given replacement in the event (requires a -regexp or -pattern)
+ -once: %|remove the trigger if it is triggered, so it only executes once and then is forgotten.
+ -stop: %|stops the signal. It won't get displayed by Irssi. Like /IGNORE
+ -debug: %|print some debugging info
+
+Other options:
+ -disabled: %|Same as removing it, but keeps it in case you might need it later
+ -name: %|Give the trigger a name. You can refer to the trigger with this name in add/del/change commands
+
+Examples:
+ Knockout people who do a !list:
+ /TRIGGER ADD %|-publics -channels "#channel1 #channel2" -nocase -regexp ^!list -command "KN $N This is not a warez channel!"
+ React to !echo commands from people who are +o in your friends-script:
+ /TRIGGER ADD %|-publics -regexp '^!echo (.*)' -hasflag '+o' -command 'say echo: $1'
+ Ignore all non-ops on #channel:
+ /TRIGGER ADD %|-publics -actions -channels "#channel" -hasmode '-o' -stop
+ Send a mail to yourself every time a topic is changed:
+ /TRIGGER ADD %|-topics -command 'exec echo $\N changed topic of $\C to: $\M | mail you@somewhere.com -s topic'
+
+
+Examples with -replace:
+ %|Replace every occurence of shit with sh*t, case insensitive:
+ /TRIGGER ADD %|-all -nocase -regexp shit -replace sh*t
+ %|Strip all colorcodes from *!lamer@*:
+ /TRIGGER ADD %|-all -masks *!lamer@* -regexp '\x03\d?\d?(,\d\d?)?|\x02|\x1f|\x16|\x06' -replace ''
+ %|Never let *!bot1@foo.bar or *!bot2@foo.bar hilight you
+ %|(this works by cutting your nick in 2 different parts, 'myn' and 'ick' here)
+ %|you don't need to understand the -replace argument, just trust that it works if the 2 parts separately don't hilight:
+ /TRIGGER ADD %|-all masks '*!bot1@foo.bar *!bot2@foo.bar' -regexp '(myn)(ick)' -nocase -replace '$1\x02\x02$2'
+ %|Avoid being hilighted by !top10 in eggdrops with stats.mod (but show your nick in bold):
+ /TRIGGER ADD %|-publics -regexp '(Top.0\(.*\): 1.*)(my)(nick)' -replace '$1\x02$2\x02\x02$3\x02'
+ %|Convert a Windows-1252 Euro to an ISO-8859-15 Euro (same effect as euro.pl):
+ /TRIGGER ADD %|-regexp '\x80' -replace '\xA4'
+ %|Show tabs as spaces, not the inverted I (same effect as tab_stop.pl):
+ /TRIGGER ADD %|-all -regexp '\t' -replace ' '
+SCRIPTHELP_EOF
+} # /
+
+my @triggers; # array of all triggers
+my %triggers_by_type; # hash mapping types on triggers of that type
+my $recursion_depth = 0;
+my $changed_since_last_save = 0;
+
+###############
+### formats ###
+###############
+
+Irssi::theme_register([
+ 'trigger_header' => 'Triggers:',
+ 'trigger_line' => '%#$[-4]0 $1',
+ 'trigger_added' => 'Trigger $0 added: $1',
+ 'trigger_not_found' => 'Trigger {hilight $0} not found',
+ 'trigger_saved' => 'Triggers saved to $0',
+ 'trigger_loaded' => 'Triggers loaded from $0'
+]);
+
+#########################################
+### catch the signals & do your thing ###
+#########################################
+
+# trigger types with a message and a channel
+my @allchanmsg_types = qw(publics pubactions pubnotices pubctcps pubctcpreplies parts quits kicks topics);
+# trigger types with a message
+my @allmsg_types = (@allchanmsg_types, qw(privmsgs privactions privnotices privctcps privctcpreplies dcc_msgs dcc_actions dcc_ctcps));
+# trigger types with a channel
+my @allchan_types = (@allchanmsg_types, qw(mode_channel mode_nick joins invites));
+# trigger types in -all
+my @all_types = (@allmsg_types, qw(mode_channel mode_nick joins invites nick_changes));
+# trigger types with a server
+my @all_server_types = (@all_types, qw(rawin notify_join notify_part notify_away notify_unaway notify_unidle));
+# all trigger types
+my @trigger_types = (@all_server_types, qw(send_command send_text beep));
+#trigger types that are not in -all
+#my @notall_types = grep {my $a=$_; return (!grep {$_ eq $a} @all_types);} @trigger_types;
+my @notall_types = qw(rawin notify_join notify_part notify_away notify_unaway notify_unidle send_command send_text beep);
+
+my @signals = (
+# "message public", SERVER_REC, char *msg, char *nick, char *address, char *target
+{
+ 'types' => ['publics'],
+ 'signal' => 'message public',
+ 'sub' => sub {check_signal_message(\@_,1,$_[0],$_[4],$_[2],$_[3],'publics');},
+},
+# "message private", SERVER_REC, char *msg, char *nick, char *address
+{
+ 'types' => ['privmsgs'],
+ 'signal' => 'message private',
+ 'sub' => sub {check_signal_message(\@_,1,$_[0],undef,$_[2],$_[3],'privmsgs');},
+},
+# "message irc action", SERVER_REC, char *msg, char *nick, char *address, char *target
+{
+ 'types' => ['privactions','pubactions'],
+ 'signal' => 'message irc action',
+ 'sub' => sub {
+ if ($_[4] eq $_[0]->{nick}) {
+ check_signal_message(\@_,1,$_[0],undef,$_[2],$_[3],'privactions');
+ } else {
+ check_signal_message(\@_,1,$_[0],$_[4],$_[2],$_[3],'pubactions');
+ }
+ },
+},
+# "message irc notice", SERVER_REC, char *msg, char *nick, char *address, char *target
+{
+ 'types' => ['privnotices','pubnotices'],
+ 'signal' => 'message irc notice',
+ 'sub' => sub {
+ if ($_[4] eq $_[0]->{nick}) {
+ check_signal_message(\@_,1,$_[0],undef,$_[2],$_[3],'privnotices');
+ } else {
+ check_signal_message(\@_,1,$_[0],$_[4],$_[2],$_[3],'pubnotices');
+ }
+ }
+},
+# "message join", SERVER_REC, char *channel, char *nick, char *address
+{
+ 'types' => ['joins'],
+ 'signal' => 'message join',
+ 'sub' => sub {check_signal_message(\@_,-1,$_[0],$_[1],$_[2],$_[3],'joins');}
+},
+# "message part", SERVER_REC, char *channel, char *nick, char *address, char *reason
+{
+ 'types' => ['parts'],
+ 'signal' => 'message part',
+ 'sub' => sub {check_signal_message(\@_,4,$_[0],$_[1],$_[2],$_[3],'parts');}
+},
+# "message quit", SERVER_REC, char *nick, char *address, char *reason
+{
+ 'types' => ['quits'],
+ 'signal' => 'message quit',
+ 'sub' => sub {check_signal_message(\@_,3,$_[0],undef,$_[1],$_[2],'quits');}
+},
+# "message kick", SERVER_REC, char *channel, char *nick, char *kicker, char *address, char *reason
+{
+ 'types' => ['kicks'],
+ 'signal' => 'message kick',
+ 'sub' => sub {check_signal_message(\@_,5,$_[0],$_[1],$_[3],$_[4],'kicks',{'other'=>$_[2]});}
+},
+# "message topic", SERVER_REC, char *channel, char *topic, char *nick, char *address
+{
+ 'types' => ['topics'],
+ 'signal' => 'message topic',
+ 'sub' => sub {check_signal_message(\@_,2,$_[0],$_[1],$_[3],$_[4],'topics');}
+},
+# "message invite", SERVER_REC, char *channel, char *nick, char *address
+{
+ 'types' => ['invites'],
+ 'signal' => 'message invite',
+ 'sub' => sub {check_signal_message(\@_,-1,$_[0],$_[1],$_[2],$_[3],'invites');}
+},
+# "message nick", SERVER_REC, char *newnick, char *oldnick, char *address
+{
+ 'types' => ['nick_changes'],
+ 'signal' => 'message nick',
+ 'sub' => sub {check_signal_message(\@_,-1,$_[0],undef,$_[1],$_[3],'nick_changes');}
+},
+# "message dcc", DCC_REC *dcc, char *msg
+{
+ 'types' => ['dcc_msgs'],
+ 'signal' => 'message dcc',
+ 'sub' => sub {check_signal_message(\@_,1,$_[0]->{'server'},undef,$_[0]->{'nick'},undef,'dcc_msgs');
+ }
+},
+# "message dcc action", DCC_REC *dcc, char *msg
+{
+ 'types' => ['dcc_actions'],
+ 'signal' => 'message dcc action',
+ 'sub' => sub {check_signal_message(\@_,1,$_[0]->{'server'},undef,$_[0]->{'nick'},undef,'dcc_actions');}
+},
+# "message dcc ctcp", DCC_REC *dcc, char *cmd, char *data
+{
+ 'types' => ['dcc_ctcps'],
+ 'signal' => 'message dcc ctcp',
+ 'sub' => sub {check_signal_message(\@_,1,$_[0]->{'server'},undef,$_[0]->{'nick'},undef,'dcc_ctcps');}
+},
+# "server incoming", SERVER_REC, char *data
+{
+ 'types' => ['rawin'],
+ 'signal' => 'server incoming',
+ 'sub' => sub {check_signal_message(\@_,1,$_[0],undef,undef,undef,'rawin');}
+},
+# "send command", char *args, SERVER_REC, WI_ITEM_REC
+{
+ 'types' => ['send_command'],
+ 'signal' => 'send command',
+ 'sub' => sub {
+ sig_send_text_or_command(\@_,1);
+ }
+},
+# "send text", char *line, SERVER_REC, WI_ITEM_REC
+{
+ 'types' => ['send_text'],
+ 'signal' => 'send text',
+ 'sub' => sub {
+ sig_send_text_or_command(\@_,0);
+ }
+},
+# "beep"
+{
+ 'types' => ['beep'],
+ 'signal' => 'beep',
+ 'sub' => sub {check_signal_message(\@_,-1,undef,undef,undef,undef,'beep');}
+},
+# "event "<cmd>, SERVER_REC, char *args, char *sender_nick, char *sender_address
+{
+ 'types' => ['mode_channel', 'mode_nick'],
+ 'signal' => 'event mode',
+ 'sub' => sub {
+ my ($server, $event_args, $nickname, $address) = @_;
+ my ($target, $modes, $modeargs) = split(/ /, $event_args, 3);
+ return if (!$server->ischannel($target));
+ my (@modeargs) = split(/ /,$modeargs);
+ my ($pos, $type, $event_type, $arg) = (0, '+');
+ foreach my $char (split(//,$modes)) {
+ if ($char eq "+" || $char eq "-") {
+ $type = $char;
+ } else {
+ if ($char =~ /[Oovh]/) { # mode_nick
+ $event_type = 'mode_nick';
+ $arg = $modeargs[$pos++];
+ } elsif ($char =~ /[beIqdk]/ || ( $char =~ /[lfJ]/ && $type eq '+')) { # chan_mode with arg
+ $event_type = 'mode_channel';
+ $arg = $modeargs[$pos++];
+ } else { # chan_mode without arg
+ $event_type = 'mode_channel';
+ $arg = undef;
+ }
+ check_signal_message(\@_,-1,$server,$target,$nickname,$address,$event_type,{
+ 'mode_type' => $type,
+ 'mode_char' => $char,
+ 'mode_arg' => $arg,
+ 'other' => ($event_type eq 'mode_nick') ? $arg : undef
+ });
+ }
+ }
+ }
+},
+# "notifylist joined", SERVER_REC, char *nick, char *user, char *host, char *realname, char *awaymsg
+{
+ 'types' => ['notify_join'],
+ 'signal' => 'notifylist joined',
+ 'sub' => sub {check_signal_message(\@_, 5, $_[0], undef, $_[1], $_[2].'@'.$_[3], 'notify_join', {'realname' => $_[4]});}
+},
+{
+ 'types' => ['notify_part'],
+ 'signal' => 'notifylist left',
+ 'sub' => sub {check_signal_message(\@_, 5, $_[0], undef, $_[1], $_[2].'@'.$_[3], 'notify_left', {'realname' => $_[4]});}
+},
+{
+ 'types' => ['notify_unidle'],
+ 'signal' => 'notifylist unidle',
+ 'sub' => sub {check_signal_message(\@_, 5, $_[0], undef, $_[1], $_[2].'@'.$_[3], 'notify_unidle', {'realname' => $_[4]});}
+},
+{
+ 'types' => ['notify_away', 'notify_unaway'],
+ 'signal' => 'notifylist away changed',
+ 'sub' => sub {check_signal_message(\@_, 5, $_[0], undef, $_[1], $_[2].'@'.$_[3], ($_[5] ? 'notify_away' : 'notify_unaway'), {'realname' => $_[4]});}
+},
+# "ctcp msg", SERVER_REC, char *args, char *nick, char *addr, char *target
+{
+ 'types' => ['pubctcps', 'privctcps'],
+ 'signal' => 'ctcp msg',
+ 'sub' => sub {
+ my ($server, $args, $nick, $addr, $target) = @_;
+ if ($target eq $server->{'nick'}) {
+ check_signal_message(\@_, 1, $server, undef, $nick, $addr, 'privctcps');
+ } else {
+ check_signal_message(\@_, 1, $server, $target, $nick, $addr, 'pubctcps');
+ }
+ }
+},
+# "ctcp reply", SERVER_REC, char *args, char *nick, char *addr, char *target
+{
+ 'types' => ['pubctcpreplies', 'privctcpreplies'],
+ 'signal' => 'ctcp reply',
+ 'sub' => sub {
+ my ($server, $args, $nick, $addr, $target) = @_;
+ if ($target eq $server->{'nick'}) {
+ check_signal_message(\@_, 1, $server, undef, $nick, $addr, 'privctcps');
+ } else {
+ check_signal_message(\@_, 1, $server, $target, $nick, $addr, 'pubctcps');
+ }
+ }
+}
+);
+
+sub sig_send_text_or_command {
+ my ($signal, $iscommand) = @_;
+ my ($line, $server, $item) = @$signal;
+ my ($channelname,$nickname,$address) = (undef,undef,undef);
+ if ($item && (ref($item) eq 'Irssi::Irc::Channel' || ref($item) eq 'Irssi::Silc::Channel')) {
+ $channelname = $item->{'name'};
+ } elsif ($item && ref($item) eq 'Irssi::Irc::Query') { # TODO Silc query ?
+ $nickname = $item->{'name'};
+ $address = $item->{'address'}
+ }
+ # TODO pass context also for non-channels (queries and other stuff)
+ check_signal_message($signal,0,$server,$channelname,$nickname,$address,$iscommand ? 'send_command' : 'send_text');
+
+}
+
+my %filters = (
+'tags' => {
+ 'types' => \@all_server_types,
+ 'sub' => sub {
+ my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
+
+ if (!defined($server)) {
+ return 0;
+ }
+ my $matches = 0;
+ foreach my $tag (split(/ /,$param)) {
+ if (lc($server->{'tag'}) eq lc($tag)) {
+ $matches = 1;
+ last;
+ }
+ }
+ return $matches;
+ }
+},
+'channels' => {
+ 'types' => \@allchan_types,
+ 'sub' => sub {
+ my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
+
+ if (!defined($channelname) || !defined($server)) {
+ return 0;
+ }
+ my $matches = 0;
+ foreach my $trigger_channel (split(/ /,$param)) {
+ if (lc($channelname) eq lc($trigger_channel)
+ || lc($server->{'tag'}.'/'.$channelname) eq lc($trigger_channel)
+ || lc($server->{'tag'}.'/') eq lc($trigger_channel)) {
+ $matches = 1;
+ last; # this channel matches, stop checking channels
+ }
+ }
+ return $matches;
+ }
+},
+'masks' => {
+ 'types' => \@all_types,
+ 'sub' => sub {
+ my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
+ return (defined($nickname) && defined($address) && defined($server) && $server->masks_match($param, $nickname, $address));
+ }
+},
+'other_masks' => {
+ 'types' => ['kicks', 'mode_nick'],
+ 'sub' => sub {
+ my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
+ return 0 unless defined($extra->{'other'});
+ my $other_address = get_address($extra->{'other'}, $server, $channelname);
+ return defined($other_address) && $server->masks_match($param, $extra->{'other'}, $other_address);
+ }
+},
+'hasmode' => {
+ 'types' => \@all_types,
+ 'sub' => sub {
+ my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
+ return hasmode($param, $nickname, $server, $channelname);
+ }
+},
+'other_hasmode' => {
+ 'types' => ['kicks', 'mode_nick'],
+ 'sub' => sub {
+ my ($param,$signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
+ return defined($extra->{'other'}) && hasmode($param, $extra->{'other'}, $server, $channelname);
+ }
+},
+'hasflag' => {
+ 'types' => \@all_types,
+ 'sub' => sub {
+ my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
+ return 0 unless defined($nickname) && defined($address) && defined($server);
+ my $flags = get_flags ($server->{'chatnet'},$channelname,$nickname,$address);
+ return defined($flags) && check_modes($flags,$param);
+ }
+},
+'other_hasflag' => {
+ 'types' => ['kicks', 'mode_nick'],
+ 'sub' => sub {
+ my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
+ return 0 unless defined($extra->{'other'});
+ my $other_address = get_address($extra->{'other'}, $server, $channelname);
+ return 0 unless defined($other_address);
+ my $flags = get_flags ($server->{'chatnet'},$channelname,$extra->{'other'},$other_address);
+ return defined($flags) && check_modes($flags,$param);
+ }
+},
+'mode_type' => {
+ 'types' => ['mode_channel', 'mode_nick'],
+ 'sub' => sub {
+ my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
+ return (($param) eq $extra->{'mode_type'});
+ }
+},
+'mode_char' => {
+ 'types' => ['mode_channel', 'mode_nick'],
+ 'sub' => sub {
+ my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
+ return (($param) eq $extra->{'mode_char'});
+ }
+},
+'mode_arg' => {
+ 'types' => ['mode_channel', 'mode_nick'],
+ 'sub' => sub {
+ my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
+ return (($param) eq $extra->{'mode_arg'});
+ }
+}
+);
+
+sub get_address {
+ my ($nick, $server, $channel) = @_;
+ my $nickrec = get_nickrec($nick, $server, $channel);
+ return $nickrec ? $nickrec->{'host'} : undef;
+}
+sub get_nickrec {
+ my ($nick, $server, $channel) = @_;
+ return unless defined($server) && defined($channel) && defined($nick);
+ my $chanrec = $server->channel_find($channel);
+ return $chanrec ? $chanrec->nick_find($nick) : undef;
+}
+
+sub hasmode {
+ my ($param, $nickname, $server, $channelname) = @_;
+ my $nickrec = get_nickrec($nickname, $server, $channelname);
+ return 0 unless defined $nickrec;
+ my $modes =
+ ($nickrec->{'op'} ? 'o' : '')
+ . ($nickrec->{'voice'} ? 'v' : '')
+ . ($nickrec->{'halfop'} ? 'h' : '')
+ ;
+ return check_modes($modes, $param);
+}
+
+# list of all switches
+my @trigger_switches = (@trigger_types, qw(all nocase stop once debug disabled));
+# parameters (with an argument)
+my @trigger_params = qw(pattern regexp command replace name);
+# list of all options (including switches) for /TRIGGER ADD
+my @trigger_add_options = (@trigger_switches, @trigger_params, keys(%filters));
+# same for /TRIGGER CHANGE, this includes the -no<option>'s
+my @trigger_options = map(($_,'no'.$_) ,@trigger_add_options);
+
+# check the triggers on $signal's $parammessage parameter, for triggers with $condition set
+# on $server in $channelname, for $nickname!$address
+# set $parammessage to -1 if the signal doesn't have a message
+# for signal without channel, nick or address, set to undef
+sub check_signal_message {
+ my ($signal, $parammessage, $server, $channelname, $nickname, $address, $condition, $extra) = @_;
+ my ($changed, $stopped, $context, $need_rebuild);
+ my $message = ($parammessage == -1) ? '' : $signal->[$parammessage];
+
+ return if (!$triggers_by_type{$condition});
+
+ if ($recursion_depth > 10) {
+ Irssi::print("Trigger error: Maximum recursion depth reached, aborting trigger.", MSGLEVEL_CLIENTERROR);
+ return;
+ }
+ $recursion_depth++;
+
+TRIGGER:
+ foreach my $trigger (@{$triggers_by_type{$condition}}) {
+ # check filters
+ foreach my $trigfilter (@{$trigger->{'filters'}}) {
+ if (! ($trigfilter->[2]($trigfilter->[1], $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra))) {
+
+ next TRIGGER;
+ }
+ }
+
+ # check regexp (and keep matches in @- and @+, so don't make a this a {block})
+ next if ($trigger->{'compregexp'} && ($parammessage == -1 || $message !~ m/$trigger->{'compregexp'}/));
+
+ # if we got this far, it fully matched, and we need to do the replace/command/stop/once
+ my $expands = $extra;
+ $expands->{'M'} = $message,;
+ $expands->{'T'} = (defined($server)) ? $server->{'tag'} : '';
+ $expands->{'C'} = $channelname;
+ $expands->{'N'} = $nickname;
+ $expands->{'A'} = $address;
+ $expands->{'I'} = ((!defined($address)) ? '' : substr($address,0,index($address,'@')));
+ $expands->{'H'} = ((!defined($address)) ? '' : substr($address,index($address,'@')+1));
+ $expands->{'$'} = '$';
+ $expands->{';'} = ';';
+
+ if (defined($trigger->{'replace'})) { # it's a -replace
+ $message =~ s/$trigger->{'compregexp'}/do_expands($trigger->{'compreplace'},$expands,$message)/ge;
+ $changed = 1;
+ }
+
+ if ($trigger->{'command'}) { # it's a (nonempty) -command
+ my $command = $trigger->{'command'};
+ # $1 = the stuff behind the $ we want to expand: a number, or a character from %expands
+ $command = do_expands($command, $expands, $message);
+
+ if (defined($server)) {
+ if (defined($channelname) && $server->channel_find($channelname)) {
+ $context = $server->channel_find($channelname);
+ } else {
+ $context = $server;
+ }
+ } else {
+ $context = undef;
+ }
+
+ if (defined($context)) {
+ $context->command("eval $command");
+ } else {
+ Irssi::command("eval $command");
+ }
+ }
+
+ if ($trigger->{'debug'}) {
+ print("DEBUG: trigger $condition pmesg=$parammessage message=$message server=$server->{tag} channel=$channelname nick=$nickname address=$address " . join(' ',map {$_ . '=' . $extra->{$_}} keys(%$extra)));
+ }
+
+ if ($trigger->{'stop'}) {
+ $stopped = 1;
+ }
+
+ if ($trigger->{'once'}) {
+ # find this trigger in the real trigger list, and remove it
+ for (my $realindex=0; $realindex < scalar(@triggers); $realindex++) {
+ if ($triggers[$realindex] == $trigger) {
+ splice (@triggers,$realindex,1);
+ last;
+ }
+ }
+ $need_rebuild = 1;
+ }
+ }
+
+ if ($need_rebuild) {
+ rebuild();
+ $changed_since_last_save = 1;
+ }
+ if ($stopped) { # stopped with -stop
+ signal_stop();
+ } elsif ($changed) { # changed with -replace
+ $signal->[$parammessage] = $message;
+ signal_continue(@$signal);
+ }
+ $recursion_depth--;
+}
+
+# used in check_signal_message to expand $'s
+# $inthis is a string that can contain $ stuff (like 'foo$1bar$N')
+sub do_expands {
+ my ($inthis, $expands, $from) = @_;
+ # @+ and @- are copied because there are two s/// nested, and the inner needs the $1 and $2,... of the outer one
+ my @plus = @+;
+ my @min = @-;
+ my $p = \@plus; my $m = \@min;
+ $inthis =~ s/\$(\\*(\d+|[^0-9x{]|x[0-9a-fA-F][0-9a-fA-F]|{.*?}))/expand_and_escape($1,$expands,$m,$p,$from)/ge;
+ return $inthis;
+}
+
+# \ $ and ; need extra escaping because we use eval
+sub expand_and_escape {
+ my $retval = expand(@_);
+ $retval =~ s/([\\\$;])/\\\1/g;
+ return $retval;
+}
+
+# used in do_expands (via expand_and_escape), to_expand is the part after the $
+sub expand {
+ my ($to_expand, $expands, $min, $plus, $from) = @_;
+ if ($to_expand =~ /^\d+$/) { # a number => look up in $vars
+ # from man perlvar:
+ # $3 is the same as "substr $var, $-[3], $+[3] - $-[3])"
+ return ($to_expand > @{$min} ? '' : substr($from,$min->[$to_expand],$plus->[$to_expand]-$min->[$to_expand]));
+ } elsif ($to_expand =~ s/^\\//) { # begins with \, so strip that from to_expand
+ my $exp = expand($to_expand,$expands,$min,$plus,$from); # first expand without \
+ $exp =~ s/([^a-zA-Z0-9])/\\\1/g; # escape non-word chars
+ return $exp;
+ } elsif ($to_expand =~ /^x([0-9a-fA-F]{2})/) { # $xAA
+ return chr(hex($1));
+ } elsif ($to_expand =~ /^{(.*?)}$/) { # ${foo}
+ return expand($1, $expands, $min, $plus, $from);
+ } else { # look up in $expands
+ return $expands->{$to_expand};
+ }
+}
+
+sub check_modes {
+ my ($has_modes, $need_modes) = @_;
+ my $matches;
+ my $switch = 1; # if a '-' if found, will be 0 (meaning the modes should not be set)
+ foreach my $need_mode (split /&/, $need_modes) {
+ $matches = 0;
+ foreach my $char (split //, $need_mode) {
+ if ($char eq '-') {
+ $switch = 0;
+ } elsif ($char eq '+') {
+ $switch = 1;
+ } elsif ((index($has_modes, $char) != -1) == $switch) {
+ $matches = 1;
+ last;
+ }
+ }
+ if (!$matches) {
+ return 0;
+ }
+ }
+ return 1;
+}
+
+# get someones flags from people.pl or friends(_shasta).pl
+sub get_flags {
+ my ($chatnet, $channel, $nick, $address) = @_;
+ my $flags;
+ no strict 'refs';
+ if (defined %{ 'Irssi::Script::people::' }) {
+ if (defined ($channel)) {
+ $flags = (&{ 'Irssi::Script::people::find_local_flags' }($chatnet,$channel,$nick,$address));
+ } else {
+ $flags = (&{ 'Irssi::Script::people::find_global_flags' }($chatnet,$nick,$address));
+ }
+ $flags = join('',keys(%{$flags}));
+ } else {
+ my $shasta;
+ if (defined %{ 'Irssi::Script::friends_shasta::' }) {
+ $shasta = 'friends_shasta';
+ } elsif (defined &{ 'Irssi::Script::friends::get_idx' }) {
+ $shasta = 'friends';
+ } else {
+ return undef;
+ }
+ my $idx = (&{ 'Irssi::Script::'.$shasta.'::get_idx' }($nick, $address));
+ if ($idx == -1) {
+ return '';
+ }
+ $flags = (&{ 'Irssi::Script::'.$shasta.'::get_friends_flags' }($idx,undef));
+ if ($channel) {
+ $flags .= (&{ 'Irssi::Script::'.$shasta.'::get_friends_flags' }($idx,$channel));
+ }
+ }
+ return $flags;
+}
+
+########################################################
+### internal stuff called by manage, needed by above ###
+########################################################
+
+my %mask_to_regexp = ();
+foreach my $i (0..255) {
+ my $ch = chr $i;
+ $mask_to_regexp{$ch} = "\Q$ch\E";
+}
+$mask_to_regexp{'?'} = '(.)';
+$mask_to_regexp{'*'} = '(.*)';
+
+sub compile_trigger {
+ my ($trigger) = @_;
+ my $regexp;
+
+ if ($trigger->{'regexp'}) {
+ $regexp = $trigger->{'regexp'};
+ } elsif ($trigger->{'pattern'}) {
+ $regexp = $trigger->{'pattern'};
+ $regexp =~ s/(.)/$mask_to_regexp{$1}/g;
+ } else {
+ delete $trigger->{'compregexp'};
+ return;
+ }
+
+ if ($trigger->{'nocase'}) {
+ $regexp = '(?i)' . $regexp;
+ }
+
+ $trigger->{'compregexp'} = qr/$regexp/;
+
+ if(defined($trigger->{'replace'})) {
+ (my $replace = $trigger->{'replace'}) =~ s/\$/\$\$/g;
+ $trigger->{'compreplace'} = Irssi::parse_special($replace);
+ }
+}
+
+# rebuilds triggers_by_type and updates signal binds
+sub rebuild {
+ %triggers_by_type = ();
+ foreach my $trigger (@triggers) {
+ if (!$trigger->{'disabled'}) {
+ if ($trigger->{'all'}) {
+ # -all is an alias for all types in @all_types for which the filters can apply
+ALLTYPES:
+ foreach my $type (@all_types) {
+ # check if all filters can apply to $type
+ foreach my $filter (@{$trigger->{'filters'}}) {
+ if (! grep {$_ eq $type} $filters{$filter->[0]}->{'types'}) {
+ next ALLTYPES;
+ }
+ }
+ push @{$triggers_by_type{$type}}, ($trigger);
+ }
+ }
+
+ foreach my $type ($trigger->{'all'} ? @notall_types : @trigger_types) {
+ if ($trigger->{$type}) {
+ push @{$triggers_by_type{$type}}, ($trigger);
+ }
+ }
+ }
+ }
+
+ foreach my $signal (@signals) {
+ my $should_bind = 0;
+ foreach my $type (@{$signal->{'types'}}) {
+ if (defined($triggers_by_type{$type})) {
+ $should_bind = 1;
+ }
+ }
+ if ($should_bind && !$signal->{'bind'}) {
+ signal_add_first($signal->{'signal'}, $signal->{'sub'});
+ $signal->{'bind'} = 1;
+ } elsif (!$should_bind && $signal->{'bind'}) {
+ signal_remove($signal->{'signal'}, $signal->{'sub'});
+ $signal->{'bind'} = 0;
+ }
+ }
+}
+
+################################
+### manage the triggers-list ###
+################################
+
+my $trigger_file; # cached setting
+
+sub sig_setup_changed {
+ $trigger_file = Irssi::settings_get_str('trigger_file');
+}
+
+sub autosave {
+ cmd_save() if ($changed_since_last_save);
+}
+
+# TRIGGER SAVE
+sub cmd_save {
+ my $io = new IO::File $trigger_file, "w";
+ if (defined $io) {
+ $io->print("#Triggers file version $VERSION\n");
+ foreach my $trigger (@triggers) {
+ $io->print(to_string($trigger) . "\n");
+ }
+ $io->close;
+ }
+ Irssi::printformat(MSGLEVEL_CLIENTNOTICE, 'trigger_saved', $trigger_file);
+ $changed_since_last_save = 0;
+}
+
+# save on unload
+sub UNLOAD {
+ cmd_save();
+}
+
+# TRIGGER LOAD
+sub cmd_load {
+ sig_setup_changed(); # make sure we've read the trigger_file setting
+ my $converted = 0;
+ my $io = new IO::File $trigger_file, "r";
+ if (not defined $io) {
+ if (-e $trigger_file) {
+ Irssi::print("Error opening triggers file", MSGLEVEL_CLIENTERROR);
+ }
+ return;
+ }
+ if (defined $io) {
+ @triggers = ();
+ my $text;
+ $text = $io->getline;
+ my $file_version = '';
+ if ($text =~ /^#Triggers file version (.*)\n/) {
+ $file_version = $1;
+ }
+ if ($file_version lt '0.6.1+2') {
+ no strict 'vars';
+ $text .= $_ foreach ($io->getlines);
+ my $rep = eval "$text";
+ if (! ref $rep) {
+ Irssi::print("Error in triggers file");
+ return;
+ }
+ my @old_triggers = @$rep;
+
+ for (my $index=0;$index < scalar(@old_triggers);$index++) {
+ my $trigger = $old_triggers[$index];
+
+ if ($file_version lt '0.6.1') {
+ # convert old names: notices => pubnotices, actions => pubactions
+ foreach $oldname ('notices','actions') {
+ if ($trigger->{$oldname}) {
+ delete $trigger->{$oldname};
+ $trigger->{'pub'.$oldname} = 1;
+ $converted = 1;
+ }
+ }
+ }
+ if ($file_version lt '0.6.1+1' && $trigger->{'modifiers'}) {
+ if ($trigger->{'modifiers'} =~ /i/) {
+ $trigger->{'nocase'} = 1;
+ Irssi::print("Trigger: trigger ".($index+1)." had 'i' in it's modifiers, it has been converted to -nocase");
+ }
+ if ($trigger->{'modifiers'} !~ /^[ig]*$/) {
+ Irssi::print("Trigger: trigger ".($index+1)." had unrecognised modifier '". $trigger->{'modifiers'} ."', which couldn't be converted.");
+ }
+ delete $trigger->{'modifiers'};
+ $converted = 1;
+ }
+
+ if (defined($trigger->{'replace'}) && ! $trigger->{'regexp'}) {
+ Irssi::print("Trigger: trigger ".($index+1)." had -replace but no -regexp, removed it");
+ splice (@old_triggers,$index,1);
+ $index--; # nr of next trigger now is the same as this one was
+ }
+
+ # convert to text with compat, and then to new trigger hash
+ $text = to_string($trigger,1);
+ my @args = &shellwords($text . ' a');
+ my $trigger = parse_options({},@args);
+ if ($trigger) {
+ push @triggers, $trigger;
+ }
+ }
+ } else { # new format
+ while ( $text = $io->getline ) {
+ chop($text);
+ my @args = &shellwords($text . ' a');
+ my $trigger = parse_options({},@args);
+ if ($trigger) {
+ push @triggers, $trigger;
+ }
+ }
+ }
+ }
+ Irssi::printformat(MSGLEVEL_CLIENTNOTICE, 'trigger_loaded', $trigger_file);
+ if ($converted) {
+ Irssi::print("Trigger: Triggers file will be in new format next time it's saved.");
+ }
+ rebuild();
+}
+
+# escape for printing with to_string
+# <<abcdef>> => << 'abcdef' >>
+# <<abc'def>> => << "abc'def" >>
+# <<abc'def\x02>> => << 'abc'\''def\x02' >>
+sub param_to_string {
+ my ($text) = @_;
+ # avoid ugly escaping if we can use "-quotes without other escaping (no " or \)
+ if ($text =~ /^[^"\\]*'[^"\\]$/) {
+ return ' "' . $text . '" ';
+ }
+ # "'" signs without a (odd number of) \ in front of them, need be to escaped as '\''
+ # this is ugly :(
+ $text =~ s/(^|[^\\](\\\\)*)'/$1'\\''/g;
+ return " '$text' ";
+}
+
+# converts a trigger back to "-switch -options 'foo'" form
+# if $compat, $trigger is in the old format (used to convert)
+sub to_string {
+ my ($trigger, $compat) = @_;
+ my $string;
+
+ foreach my $switch (@trigger_switches) {
+ if ($trigger->{$switch}) {
+ $string .= '-'.$switch.' ';
+ }
+ }
+
+ if ($compat) {
+ foreach my $filter (keys(%filters)) {
+ if ($trigger->{$filter}) {
+ $string .= '-' . $filter . param_to_string($trigger->{$filter});
+ }
+ }
+ } else {
+ foreach my $trigfilter (@{$trigger->{'filters'}}) {
+ $string .= '-' . $trigfilter->[0] . param_to_string($trigfilter->[1]);
+ }
+ }
+
+ foreach my $param (@trigger_params) {
+ if ($trigger->{$param} || ($param eq 'replace' && defined($trigger->{'replace'}))) {
+ $string .= '-' . $param . param_to_string($trigger->{$param});
+ }
+ }
+ return $string;
+}
+
+# find a trigger (for REPLACE and DELETE), returns index of trigger, or -1 if not found
+sub find_trigger {
+ my ($data) = @_;
+ if ($data =~ /^[0-9]*$/ and defined($triggers[$data-1])) {
+ return $data-1;
+ } else {
+ for (my $i=0; $i < scalar(@triggers); $i++) {
+ if ($triggers[$i]->{'name'} eq $data) {
+ return $i;
+ }
+ }
+ }
+ Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'trigger_not_found', $data);
+ return -1; # not found
+}
+
+
+# TRIGGER ADD <options>
+sub cmd_add {
+ my ($data, $server, $item) = @_;
+ my @args = shellwords($data . ' a');
+
+ my $trigger = parse_options({}, @args);
+ if ($trigger) {
+ push @triggers, $trigger;
+ Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'trigger_added', scalar(@triggers), to_string($trigger));
+ rebuild();
+ $changed_since_last_save = 1;
+ }
+}
+
+# TRIGGER CHANGE <nr> <options>
+sub cmd_change {
+ my ($data, $server, $item) = @_;
+ my @args = shellwords($data . ' a');
+ my $index = find_trigger(shift @args);
+ if ($index != -1) {
+ if(parse_options($triggers[$index], @args)) {
+ Irssi::print("Trigger " . ($index+1) ." changed to: ". to_string($triggers[$index]));
+ }
+ rebuild();
+ $changed_since_last_save = 1;
+ }
+}
+
+# parses options for TRIGGER ADD and TRIGGER CHANGE
+# if invalid args returns undef, else changes $thetrigger and returns it
+sub parse_options {
+ my ($thetrigger,@args) = @_;
+ my ($trigger, $option);
+
+ if (pop(@args) ne 'a') {
+ Irssi::print("Syntax error, probably missing a closing quote", MSGLEVEL_CLIENTERROR);
+ return undef;
+ }
+
+ %$trigger = %$thetrigger; # make a copy to prevent changing the given trigger if args doesn't parse
+ARGS: for (my $arg = shift @args; $arg; $arg = shift @args) {
+ # expand abbreviated options, put in $option
+ $arg =~ s/^-//;
+ $option = undef;
+ foreach my $ioption (@trigger_options) {
+ if (index($ioption, $arg) == 0) { # -$opt starts with $arg
+ if ($option) { # another already matched
+ Irssi::print("Ambiguous option: $arg", MSGLEVEL_CLIENTERROR);
+ return undef;
+ }
+ $option = $ioption;
+ last if ($arg eq $ioption); # exact match is unambiguous
+ }
+ }
+ if (!$option) {
+ Irssi::print("Unknown option: $arg", MSGLEVEL_CLIENTERROR);
+ return undef;
+ }
+
+ # -<param> <value> or -no<param>
+ foreach my $param (@trigger_params) {
+ if ($option eq $param) {
+ $trigger->{$param} = shift @args;
+ next ARGS;
+ }
+ if ($option eq 'no'.$param) {
+ $trigger->{$param} = undef;
+ next ARGS;
+ }
+ }
+
+ # -[no]<switch>
+ foreach my $switch (@trigger_switches) {
+ # -<switch>
+ if ($option eq $switch) {
+ $trigger->{$switch} = 1;
+ next ARGS;
+ }
+ # -no<switch>
+ elsif ($option eq 'no'.$switch) {
+ $trigger->{$switch} = undef;
+ next ARGS;
+ }
+ }
+
+ # -<filter> <value>
+ if ($filters{$option}) {
+ push @{$trigger->{'filters'}}, [$option, shift @args, $filters{$option}->{'sub'}];
+ next ARGS;
+ }
+
+ # -<nofilter>
+ if ($option =~ /^no(.*)$/ && $filters{$1}) {
+ my $filter = $1;
+ # the new filters are the old grepped for everything except ones with name $filter
+ @{$trigger->{'filters'}} = grep( $_->[0] ne $filter, @{$trigger->{'filters'}} );
+ }
+ }
+
+ if (defined($trigger->{'replace'}) && ! $trigger->{'regexp'} && !$trigger->{'pattern'}) {
+ Irssi::print("Trigger error: Can't have -replace without -regexp", MSGLEVEL_CLIENTERROR);
+ return undef;
+ }
+
+ if ($trigger->{'pattern'} && $trigger->{'regexp'}) {
+ Irssi::print("Trigger error: Can't have -pattern and -regexp in same trigger", MSGLEVEL_CLIENTERROR);
+ return undef;
+ }
+
+ # remove types that are implied by -all
+ if ($trigger->{'all'}) {
+ foreach my $type (@all_types) {
+ delete $trigger->{$type};
+ }
+ }
+
+ # remove types for which the filters don't apply
+ foreach my $type (@trigger_types) {
+ if ($trigger->{$type}) {
+ foreach my $filter (@{$trigger->{'filters'}}) {
+ if (!grep {$_ eq $type} @{$filters{$filter->[0]}->{'types'}}) {
+ Irssi::print("Warning: the filter -" . $filter->[0] . " can't apply to an event of type -$type, so I'm removing that type from this trigger.");
+ delete $trigger->{$type};
+ }
+ }
+ }
+ }
+
+ # check if it has at least one type
+ my $has_a_type;
+ foreach my $type (@trigger_types) {
+ if ($trigger->{$type}) {
+ $has_a_type = 1;
+ last;
+ }
+ }
+ if (!$has_a_type && !$trigger->{'all'}) {
+ Irssi::print("Warning: this trigger doesn't trigger on any type of message. you probably want to add -publics or -all");
+ }
+
+ compile_trigger($trigger);
+ %$thetrigger = %$trigger; # copy changes to real trigger
+ return $thetrigger;
+}
+
+# TRIGGER DELETE <num>
+sub cmd_del {
+ my ($data, $server, $item) = @_;
+ my @args = shellwords($data);
+ my $index = find_trigger(shift @args);
+ if ($index != -1) {
+ Irssi::print("Deleted ". ($index+1) .": ". to_string($triggers[$index]));
+ splice (@triggers,$index,1);
+ rebuild();
+ $changed_since_last_save = 1;
+ }
+}
+
+# TRIGGER MOVE <num> <num>
+sub cmd_move {
+ my ($data, $server, $item) = @_;
+ my @args = &shellwords($data);
+ my $index = find_trigger(shift @args);
+ if ($index != -1) {
+ my $newindex = shift @args;
+ if ($newindex < 1 || $newindex > scalar(@triggers)) {
+ Irssi::print("$newindex is not a valid trigger number");
+ return;
+ }
+ Irssi::print("Moved from ". ($index+1) ." to $newindex: ". to_string($triggers[$index]));
+ $newindex -= 1; # array starts counting from 0
+ my $trigger = splice (@triggers,$index,1); # remove from old place
+ splice (@triggers,$newindex,0,($trigger)); # insert at new place
+ rebuild();
+ $changed_since_last_save = 1;
+ }
+}
+
+# TRIGGER LIST
+sub cmd_list {
+ Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'trigger_header');
+ my $i=1;
+ foreach my $trigger (@triggers) {
+ Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'trigger_line', $i++, to_string($trigger));
+ }
+}
+
+######################
+### initialisation ###
+######################
+
+command_bind('trigger help',\&cmd_help);
+command_bind('help trigger',\&cmd_help);
+command_bind('trigger add',\&cmd_add);
+command_bind('trigger change',\&cmd_change);
+command_bind('trigger move',\&cmd_move);
+command_bind('trigger list',\&cmd_list);
+command_bind('trigger delete',\&cmd_del);
+command_bind('trigger save',\&cmd_save);
+command_bind('trigger reload',\&cmd_load);
+command_bind 'trigger' => sub {
+ my ( $data, $server, $item ) = @_;
+ $data =~ s/\s+$//g;
+ command_runsub('trigger', $data, $server, $item);
+};
+
+Irssi::signal_add('setup saved', \&autosave);
+Irssi::signal_add('setup changed', \&sig_setup_changed);
+
+# This makes tab completion work
+Irssi::command_set_options('trigger add',join(' ',@trigger_add_options));
+Irssi::command_set_options('trigger change',join(' ',@trigger_options));
+
+Irssi::settings_add_str($IRSSI{'name'}, 'trigger_file', Irssi::get_irssi_dir()."/triggers");
+
+cmd_load();
diff --git a/protocols/skype/t/livetest-bitlbee.sh b/protocols/skype/t/livetest-bitlbee.sh
new file mode 100755
index 00000000..7cbfbf6e
--- /dev/null
+++ b/protocols/skype/t/livetest-bitlbee.sh
@@ -0,0 +1,116 @@
+#!/usr/bin/env bash
+
+start_skyped()
+{
+ python ../skyped.py "$@" > skypedtest.pid
+ while true
+ do
+ [ -e skypedtest.pid ] || break
+ pid=$(sed 's/.*: //' skypedtest.pid)
+ if [ -n "$(ps -p $pid -o pid=)" ]; then
+ sleep 5
+ else
+ start_skyped "$@"
+ break
+ fi
+ done
+}
+
+BITLBEE=$1
+typeset -ix PORT=`echo $2 | egrep '^[0-9]{1,5}$'`
+SCRIPT=$3
+shift 3
+
+[ -n "$SCRIPT" -a -n "$BITLBEE" -a -e "$SCRIPT" -a "$PORT" -ne 0 ] || { echo Syntax: `basename "$0"` bitlbee-executable listening-port test-script test-script-args; exit 1; }
+
+# Create or empty test dir
+mkdir livetest 2>/dev/null || rm livetest/bitlbeetest*.xml bitlbeetest.pid 2>/dev/null
+
+# Run the bee
+echo Running bitlbee...
+$VALGRIND $BITLBEE -n -c bitlbee.conf -d livetest/ -D -P bitlbeetest.pid -p $PORT 2>bitlbee.log &
+sleep 2
+
+# Check if it's really running
+kill -0 `cat bitlbeetest.pid 2>/dev/null ` 2>/dev/null || { echo Failed to run bitlbee daemon on port $PORT; exit 1; }
+
+if [ -z "$TUNNELED_MODE" ]; then
+ # Set up skyped
+
+ rm -rf etc
+ mkdir etc
+ cd etc
+ cp ../../skyped.cnf .
+ cp ~/.skyped/skyped.cert.pem .
+ cp ~/.skyped/skyped.key.pem .
+ cd ..
+ echo "[skyped]" > skyped.conf
+ echo "username = $TEST_SKYPE_ID" >> skyped.conf
+ SHA1=`which sha1sum`
+ if [ -z "$SHA1" ]; then
+ SHA1=`which sha1`
+ fi
+ if [ -z "$SHA1" ]; then
+ echo Test failed
+ echo "(Can't compute password for skyped.conf)"
+ exit 77
+ fi
+ echo "password = $(echo -n $TEST_SKYPE_PASSWORD|$SHA1|sed 's/ *-$//')" >> skyped.conf
+ # we use ~ here to test that resolve that syntax works
+ echo "cert = $(pwd|sed "s|$HOME|~|")/etc/skyped.cert.pem" >> skyped.conf
+ echo "key = $(pwd|sed "s|$HOME|~|")/etc/skyped.key.pem" >> skyped.conf
+ echo "port = 2727" >> skyped.conf
+
+ # Run skyped
+ start_skyped -c skyped.conf -l skypedtest.log &
+ sleep 2
+fi
+
+if [ "$TUNNELED_MODE" = "yes" ]; then
+ rm -f tunnel.pid
+ if [ -n "$TUNNEL_SCRIPT" ]; then
+ $TUNNEL_SCRIPT &
+ echo $! > tunnel.pid
+ sleep 5
+ fi
+fi
+
+# Run the test
+echo Running test script...
+"$SCRIPT" $*
+RET=$?
+
+if [ -z "$TUNNELED_MODE" ]; then
+ # skyped runs on another host: no means to kill it
+ # Kill skyped
+ killall -TERM skype
+ if [ -f skypedtest.pid ]; then
+ pid=$(sed 's/.*: //' skypedtest.pid)
+ rm skypedtest.pid
+ [ -n "$(ps -p $pid -o pid=)" ] && kill -TERM $pid
+ fi
+fi
+
+if [ "$TUNNELED_MODE" = "yes" ]; then
+ if [ -n "$TUNNEL_SCRIPT" ]; then
+ cat tunnel.pid >> /tmp/tunnel.pid
+ kill `cat tunnel.pid`
+ rm -f tunnel.pid
+ fi
+fi
+
+# Kill bee
+echo Killing bitlbee...
+kill `cat bitlbeetest.pid`
+
+if [ "$TUNNELED_MODE" = "yes" ]; then
+ # give the skyped a chance to timeout
+ sleep 30
+fi
+
+# Return test result
+[ $RET -eq 0 ] && echo Test passed
+[ $RET -ne 0 ] && echo Test failed
+[ $RET -eq 22 ] && echo '(timed out)'
+[ $RET -eq 66 ] && echo '(environment variables missing)'
+exit $RET