diff options
author | Wilmer van der Gaast <wilmer@gaast.net> | 2011-07-24 13:51:00 +0100 |
---|---|---|
committer | Wilmer van der Gaast <wilmer@gaast.net> | 2011-07-24 13:51:00 +0100 |
commit | c8b8c83fb6a0ae1b59de71ac796a78d73e444988 (patch) | |
tree | 9fdc9d1393353e663132b34cff0b8c16fb1e014c | |
parent | a01049810d7101b8c0014aa7b36826fc95f944a5 (diff) | |
parent | 17f6079a06eaed2fba1da999332e9e5ac1dbc2a6 (diff) |
Merging Skype plugin. Many thanks to Miklos Vajna for all his work on this,
having this distributed together with BitlBee will hopefully make up for the
fact that BitlBee has poor binary API backward compatibility.
-rw-r--r-- | Makefile | 23 | ||||
-rwxr-xr-x | configure | 12 | ||||
-rw-r--r-- | protocols/skype/.bzrignore | 19 | ||||
-rw-r--r-- | protocols/skype/.mailmap | 1 | ||||
-rw-r--r-- | protocols/skype/HACKING | 26 | ||||
-rw-r--r-- | protocols/skype/Makefile | 107 | ||||
-rw-r--r-- | protocols/skype/NEWS | 131 | ||||
-rw-r--r-- | protocols/skype/README | 488 | ||||
-rw-r--r-- | protocols/skype/asciidoc.conf | 21 | ||||
-rw-r--r-- | protocols/skype/client.sh | 1 | ||||
-rw-r--r-- | protocols/skype/skype.c | 1565 | ||||
-rw-r--r-- | protocols/skype/skyped.cnf | 40 | ||||
-rw-r--r-- | protocols/skype/skyped.py | 491 | ||||
-rw-r--r-- | protocols/skype/skyped.txt | 52 | ||||
-rw-r--r-- | protocols/skype/t/Makefile | 33 | ||||
-rw-r--r-- | protocols/skype/t/bitlbee.conf | 0 | ||||
-rwxr-xr-x | protocols/skype/t/irssi/livetest-irssi.sh | 109 | ||||
-rw-r--r-- | protocols/skype/t/irssi/skype-call.test | 13 | ||||
-rw-r--r-- | protocols/skype/t/irssi/skype-info.test | 12 | ||||
-rw-r--r-- | protocols/skype/t/irssi/skype-login.test | 10 | ||||
-rw-r--r-- | protocols/skype/t/irssi/skype-msg.test | 17 | ||||
-rw-r--r-- | protocols/skype/t/irssi/trigger.pl | 1225 | ||||
-rwxr-xr-x | protocols/skype/t/livetest-bitlbee.sh | 116 |
23 files changed, 4510 insertions, 2 deletions
@@ -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 $@ @@ -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/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 |