aboutsummaryrefslogtreecommitdiff
path: root/src/plugins/General/scrobbler
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/General/scrobbler')
-rw-r--r--src/plugins/General/scrobbler/scrobbler.cpp259
-rw-r--r--src/plugins/General/scrobbler/scrobbler.h75
-rw-r--r--src/plugins/General/scrobbler/scrobbler.pro41
-rw-r--r--src/plugins/General/scrobbler/scrobblerfactory.cpp59
-rw-r--r--src/plugins/General/scrobbler/scrobblerfactory.h45
-rw-r--r--src/plugins/General/scrobbler/settingsdialog.cpp51
-rw-r--r--src/plugins/General/scrobbler/settingsdialog.h48
-rw-r--r--src/plugins/General/scrobbler/settingsdialog.ui92
8 files changed, 670 insertions, 0 deletions
diff --git a/src/plugins/General/scrobbler/scrobbler.cpp b/src/plugins/General/scrobbler/scrobbler.cpp
new file mode 100644
index 000000000..7713161b7
--- /dev/null
+++ b/src/plugins/General/scrobbler/scrobbler.cpp
@@ -0,0 +1,259 @@
+/***************************************************************************
+ * Copyright (C) 2008 by Ilya Kotov *
+ * forkotov02@hotmail.ru *
+ * *
+ * 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. *
+ ***************************************************************************/
+
+#include <QMenu>
+#include <QHttp>
+#include <QByteArray>
+#include <QCryptographicHash>
+#include <QUrl>
+#include <QTime>
+#include <QSettings>
+#include <QDir>
+
+#include "scrobbler.h"
+
+#define SCROBBLER_HS_URL "post.audioscrobbler.com"
+#define PROTOCOL_VER "1.2"
+#define CLIENT_ID "qmm"
+#define CLIENT_VER "0.1"
+
+Scrobbler::Scrobbler(QObject *parent)
+ : General(parent)
+{
+ m_http = new QHttp(this);
+ m_http->setHost(SCROBBLER_HS_URL, 80);
+ m_state = General::Stopped;
+ QSettings settings(QDir::homePath()+"/.qmmp/qmmprc", QSettings::IniFormat);
+ settings.beginGroup("Scrobbler");
+ m_login = settings.value("login").toString();
+ m_passw = settings.value("password").toString();
+ settings.endGroup();
+ m_disabled = m_login.isEmpty() || m_passw.isEmpty();
+ m_passw = QString(QCryptographicHash::hash(m_passw.toAscii(), QCryptographicHash::Md5).toHex());
+ connect(m_http, SIGNAL(requestFinished (int, bool)), SLOT(processResponse(int, bool)));
+ connect(m_http, SIGNAL(readyRead (const QHttpResponseHeader&)),
+ SLOT(readResponse(const QHttpResponseHeader&)));
+ m_time = new QTime();
+ m_submitedSongs = 0;
+ m_handshakeid = 0;
+ m_submitid = 0;
+ if (!m_disabled)
+ handshake();
+ //TODO proxy support
+}
+
+
+Scrobbler::~Scrobbler()
+{
+ delete m_time;
+}
+
+void Scrobbler::setState(const uint &state)
+{
+ m_state = state;
+ if (m_disabled)
+ return;
+ switch ((uint) state)
+ {
+ case General::Playing:
+ {
+ m_start_ts = time(NULL);
+ m_time->restart();
+ if (!isReady() && m_handshakeid == 0)
+ handshake();
+ break;
+ }
+ case General::Paused:
+ {
+ break;
+ }
+ case General::Stopped:
+ {
+ if (!m_song.isEmpty()
+ && ((m_time->elapsed ()/1000 > 240)
+ || (m_time->elapsed ()/1000 > int(m_song.length()/2)))
+ && (m_time->elapsed ()/1000 > 60))
+ {
+ m_songCache << m_song;
+ m_timeStamps << m_start_ts;
+ }
+
+ m_song.clear();
+ if (m_songCache.isEmpty())
+ break;
+
+ if (m_http->error() != QHttp::NoError)
+ m_http->clearPendingRequests ();
+
+ if (isReady() && m_submitid == 0)
+ {
+ submit();
+ }
+ break;
+ }
+ }
+}
+
+void Scrobbler::setSongInfo(const SongInfo &song)
+{
+ if (m_state == General::Playing
+ && !song.title().isEmpty() //skip empty tags
+ && !song.artist().isEmpty()
+ && !song.isStream() //skip stream
+ && !song.artist().contains("&") //skip tags with special symbols
+ && !song.title().contains("&")
+ && !song.album().contains("&")
+ && !song.artist().contains("=")
+ && !song.title().contains("=")
+ && !song.album().contains("="))
+ {
+ m_song = song;
+ }
+}
+
+void Scrobbler::processResponse(int id, bool error)
+{
+ if (error)
+ {
+ qWarning("Scrobbler: %s", qPrintable(m_http->errorString ()));
+ //TODO hard failure handling
+
+ if (id == m_submitid)
+ m_submitid = 0;
+ else if (id == m_handshakeid)
+ m_handshakeid = 0;
+ return;
+ }
+ QString str(m_array);
+ QStringList strlist = str.split("\n");
+
+ if (id == m_handshakeid)
+ {
+ m_handshakeid = 0;
+ if (!strlist[0].contains("OK") || strlist.size() < 4)
+ {
+ qWarning("Scrobbler: handshake phase error: %s", qPrintable(strlist[0]));
+ //TODO badtime handling
+ return;
+ }
+ if (strlist.size() > 3) //process handshake response
+ {
+ qDebug("Scrobbler: reading handshake response");
+ qDebug("Scrobbler: Session ID: %s",qPrintable(strlist[1]));
+ qDebug("Scrobbler: Now-Playing URL: %s",qPrintable(strlist[2]));
+ qDebug("Scrobbler: Submission URL: %s",qPrintable(strlist[3]));
+ m_submitUrl = strlist[3];
+ m_session = strlist[1];
+ return;
+ }
+ }
+ else if (id == m_submitid)
+ {
+ m_submitid = 0;
+ if (!strlist[0].contains("OK"))
+ {
+ qWarning("Scrobbler: submit error: %s", qPrintable(strlist[0]));
+ //TODO badsession handling
+ return;
+ }
+ qWarning("Scrobbler: submited %d song(s)", m_submitedSongs);
+ while (m_submitedSongs)
+ {
+ m_submitedSongs--;
+ m_timeStamps.removeFirst ();
+ m_songCache.removeFirst ();
+ }
+ }
+ m_array.clear();
+}
+
+void Scrobbler::readResponse(const QHttpResponseHeader &header)
+{
+ if (header.statusCode () != 200)
+ {
+ qWarning("Scrobbler: error: %s",qPrintable(header.reasonPhrase ()));
+ //TODO Failure Handling
+ return;
+ }
+ m_array = m_http->readAll();
+}
+
+void Scrobbler::handshake()
+{
+ qDebug("Scrobbler::handshake()");
+ time_t ts = time(NULL);
+ qDebug("Scrobbler: current time stamp %ld",ts);
+ QString auth_tmp = QString("%1%2").arg(m_passw).arg(ts);
+ QByteArray auth = QCryptographicHash::hash(auth_tmp.toAscii (), QCryptographicHash::Md5);
+ auth = auth.toHex();
+
+ QString url = QString("%1?hs=true&p=%2&c=%3&v=%4&u=%5&t=%6&a=%7")
+ .arg("/")
+ .arg(PROTOCOL_VER)
+ .arg(CLIENT_ID)
+ .arg(CLIENT_VER)
+ .arg(m_login)
+ .arg(ts)
+ .arg(QString(auth));
+
+ qDebug("Scrobbler: request url: %s",qPrintable(url));
+ m_http->setHost(SCROBBLER_HS_URL, 80);
+ m_handshakeid = m_http->get(url);
+}
+
+void Scrobbler::submit()
+{
+ qDebug("Scrobbler::submit()");
+ if (m_songCache.isEmpty())
+ return;
+ m_submitedSongs = m_songCache.size();
+ QString body = QString("s=%1").arg(m_session);
+ for (int i = 0; i < qMin(m_songCache.size(), 25); ++i)
+ {
+ SongInfo info = m_songCache[i];
+ body += QString("&a[%9]=%1&t[%9]=%2&i[%9]=%3&o[%9]=%4&r[%9]=%5&l[%9]=%6&b[%9]=%7&n[%9]=%8&m[%9]=")
+ .arg(info.artist())
+ .arg(info.title())
+ .arg( m_timeStamps[i])
+ .arg("P")
+ .arg("")
+ .arg(info.length())
+ .arg(info.album())
+ .arg(info.track())
+ .arg(i);
+ }
+ QUrl url(m_submitUrl);
+ m_http->setHost(url.host(), url.port());
+ QHttpRequestHeader header("POST", url.path());
+ header.setContentType("application/x-www-form-urlencoded");
+ header.setValue("User-Agent","iScrobbler/1.5.1qmmp-plugins/0.2");
+ header.setValue("Host",url.host());
+ header.setValue("Accept", "*/*");
+ header.setContentLength(QUrl::toPercentEncoding(body,":/[]&=").size());
+ qDebug("Scrobbler: submit request header");
+ qDebug(qPrintable(header.toString()));
+ qDebug("*****************************");
+ m_submitid = m_http->request(header, QUrl::toPercentEncoding(body,":/[]&="));
+}
+
+bool Scrobbler::isReady()
+{
+ return !m_submitUrl.isEmpty() && !m_session.isEmpty();
+}
diff --git a/src/plugins/General/scrobbler/scrobbler.h b/src/plugins/General/scrobbler/scrobbler.h
new file mode 100644
index 000000000..25faa13fd
--- /dev/null
+++ b/src/plugins/General/scrobbler/scrobbler.h
@@ -0,0 +1,75 @@
+/***************************************************************************
+ * Copyright (C) 2008 by Ilya Kotov *
+ * forkotov02@hotmail.ru *
+ * *
+ * 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. *
+ ***************************************************************************/
+#ifndef SCROBBLER_H
+#define SCROBBLER_H
+
+#include <QHttpResponseHeader>
+#include <qmmpui/general.h>
+#include <time.h>
+
+class QHttp;
+class QTime;
+
+
+
+/**
+ @author Ilya Kotov <forkotov02@hotmail.ru>
+*/
+
+class Scrobbler : public General
+{
+Q_OBJECT
+public:
+ Scrobbler(QObject *parent = 0);
+
+ ~Scrobbler();
+
+ void setState(const uint& state);
+ void setSongInfo(const SongInfo &song);
+
+private slots:
+ void processResponse(int, bool);
+ void processResponseHeader(const QHttpResponseHeader &);
+ void readResponse(const QHttpResponseHeader&);
+
+private:
+ void handshake();
+ void submit();
+ bool isReady();
+ time_t m_start_ts;
+ SongInfo m_song;
+ QHttp *m_http;
+ uint m_state;
+ QString m_login;
+ QString m_passw;
+ QString m_submitUrl;
+ QString m_session;
+ QList <time_t> m_timeStamps;
+ QList <SongInfo> m_songCache;
+ QTime* m_time;
+ int m_submitedSongs;
+ int m_handshakeid;
+ int m_submitid;
+ QByteArray m_array;
+ bool m_disabled;
+
+};
+
+#endif
diff --git a/src/plugins/General/scrobbler/scrobbler.pro b/src/plugins/General/scrobbler/scrobbler.pro
new file mode 100644
index 000000000..787cb9f37
--- /dev/null
+++ b/src/plugins/General/scrobbler/scrobbler.pro
@@ -0,0 +1,41 @@
+include(../../plugins.pri)
+
+CONFIG += release \
+ warn_on \
+ plugin
+
+TEMPLATE = lib
+QMAKE_LIBDIR += ../../../../lib
+
+TARGET=$$PLUGINS_PREFIX/General/scrobbler
+QMAKE_CLEAN =$$PLUGINS_PREFIX/General/libscrobbler.so
+
+
+#TRANSLATIONS = translations/ffmpeg_plugin_ru.ts
+#RESOURCES = translations/translations.qrc
+
+isEmpty(LIB_DIR){
+ LIB_DIR = /lib
+}
+target.path = $$LIB_DIR/qmmp/General
+INSTALLS += target
+#FORMS += settingsdialog.ui
+
+#RESOURCES += images/images.qrc
+
+
+HEADERS += scrobblerfactory.h \
+ scrobbler.h \
+ settingsdialog.h
+SOURCES += scrobblerfactory.cpp \
+ scrobbler.cpp \
+ settingsdialog.cpp
+QT += network
+
+
+INCLUDEPATH += ../../../
+
+LIBS += -lqmmpui
+
+FORMS += settingsdialog.ui
+
diff --git a/src/plugins/General/scrobbler/scrobblerfactory.cpp b/src/plugins/General/scrobbler/scrobblerfactory.cpp
new file mode 100644
index 000000000..77bbb0f2b
--- /dev/null
+++ b/src/plugins/General/scrobbler/scrobblerfactory.cpp
@@ -0,0 +1,59 @@
+/***************************************************************************
+ * Copyright (C) 2008 by Ilya Kotov *
+ * forkotov02@hotmail.ru *
+ * *
+ * 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. *
+ ***************************************************************************/
+
+#include <QtGui>
+
+#include "scrobbler.h"
+#include "settingsdialog.h"
+#include "scrobblerfactory.h"
+
+const GeneralProperties ScrobblerFactory::properties() const
+{
+ GeneralProperties properties;
+ properties.name = tr("Scrobbler Plugin");
+ properties.hasAbout = TRUE;
+ properties.hasSettings = TRUE;
+ return properties;
+}
+
+General *ScrobblerFactory::create(QObject *parent)
+{
+ return new Scrobbler(parent);
+}
+
+void ScrobblerFactory::showSettings(QWidget *parent)
+{
+ SettingsDialog *dialog = new SettingsDialog(parent);
+ dialog->show();
+}
+
+void ScrobblerFactory::showAbout(QWidget *parent)
+{
+ QMessageBox::about (parent, tr("About Scrobbler Plugin"),
+ tr("Qmmp AudioScrobbler Plugin")+"\n"+
+ tr("Writen by: Ilya Kotov <forkotov02@hotmail.ru>"));
+}
+
+QTranslator *ScrobblerFactory::createTranslator(QObject *parent)
+{
+ return 0;
+}
+
+Q_EXPORT_PLUGIN(ScrobblerFactory)
diff --git a/src/plugins/General/scrobbler/scrobblerfactory.h b/src/plugins/General/scrobbler/scrobblerfactory.h
new file mode 100644
index 000000000..240749714
--- /dev/null
+++ b/src/plugins/General/scrobbler/scrobblerfactory.h
@@ -0,0 +1,45 @@
+/***************************************************************************
+ * Copyright (C) 2008 by Ilya Kotov *
+ * forkotov02@hotmail.ru *
+ * *
+ * 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. *
+ ***************************************************************************/
+#ifndef SCROBBLERFACTORY_H
+#define SCROBBLERFACTORY_H
+
+/**
+ @author Ilya Kotov <forkotov02@hotmail.ru>
+*/
+#include <QObject>
+#include <QTranslator>
+
+#include <qmmpui/general.h>
+#include <qmmpui/generalfactory.h>
+
+class ScrobblerFactory : public QObject, public GeneralFactory
+{
+Q_OBJECT
+Q_INTERFACES(GeneralFactory);
+public:
+ const GeneralProperties properties() const;
+ General *create(QObject *parent);
+ void showSettings(QWidget *parent);
+ void showAbout(QWidget *parent);
+ QTranslator *createTranslator(QObject *parent);
+
+};
+
+#endif
diff --git a/src/plugins/General/scrobbler/settingsdialog.cpp b/src/plugins/General/scrobbler/settingsdialog.cpp
new file mode 100644
index 000000000..8dc2dfe22
--- /dev/null
+++ b/src/plugins/General/scrobbler/settingsdialog.cpp
@@ -0,0 +1,51 @@
+/***************************************************************************
+ * Copyright (C) 2008 by Ilya Kotov *
+ * forkotov02@hotmail.ru *
+ * *
+ * 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. *
+ ***************************************************************************/
+
+#include <QSettings>
+#include <QDir>
+
+#include "settingsdialog.h"
+
+SettingsDialog::SettingsDialog(QWidget *parent)
+ : QDialog(parent)
+{
+ ui.setupUi(this);
+ setAttribute(Qt::WA_DeleteOnClose);
+ QSettings settings(QDir::homePath()+"/.qmmp/qmmprc", QSettings::IniFormat);
+ settings.beginGroup("Scrobbler");
+ ui.userLineEdit->setText(settings.value("login").toString());
+ ui.passwordLineEdit->setText(settings.value("password").toString());
+ settings.endGroup();
+ connect(ui.okButton, SIGNAL(clicked()), SLOT(writeSettings()));
+}
+
+
+SettingsDialog::~SettingsDialog()
+{}
+
+void SettingsDialog::writeSettings()
+{
+ QSettings settings(QDir::homePath()+"/.qmmp/qmmprc", QSettings::IniFormat);
+ settings.beginGroup("Scrobbler");
+ settings.setValue("login",ui.userLineEdit->text());
+ settings.setValue("password",ui.passwordLineEdit->text());
+ settings.endGroup();
+ accept();
+}
diff --git a/src/plugins/General/scrobbler/settingsdialog.h b/src/plugins/General/scrobbler/settingsdialog.h
new file mode 100644
index 000000000..0d5b21d52
--- /dev/null
+++ b/src/plugins/General/scrobbler/settingsdialog.h
@@ -0,0 +1,48 @@
+/***************************************************************************
+ * Copyright (C) 2008 by Ilya Kotov *
+ * forkotov02@hotmail.ru *
+ * *
+ * 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. *
+ ***************************************************************************/
+#ifndef SETTINGSDIALOG_H
+#define SETTINGSDIALOG_H
+
+#include <QDialog>
+
+
+#include "ui_settingsdialog.h"
+
+/**
+ @author Ilya Kotov <forkotov02@hotmail.ru>
+*/
+class SettingsDialog : public QDialog
+{
+Q_OBJECT
+public:
+ SettingsDialog(QWidget *parent = 0);
+
+ ~SettingsDialog();
+
+
+private slots:
+ void writeSettings();
+
+private:
+ Ui::SettingsDialog ui;
+
+};
+
+#endif
diff --git a/src/plugins/General/scrobbler/settingsdialog.ui b/src/plugins/General/scrobbler/settingsdialog.ui
new file mode 100644
index 000000000..ec446bba2
--- /dev/null
+++ b/src/plugins/General/scrobbler/settingsdialog.ui
@@ -0,0 +1,92 @@
+<ui version="4.0" >
+ <class>SettingsDialog</class>
+ <widget class="QDialog" name="SettingsDialog" >
+ <property name="geometry" >
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>250</width>
+ <height>123</height>
+ </rect>
+ </property>
+ <property name="windowTitle" >
+ <string>Scrobbler Plugin Settings</string>
+ </property>
+ <layout class="QGridLayout" >
+ <item row="0" column="0" >
+ <widget class="QLabel" name="label" >
+ <property name="text" >
+ <string>User name:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1" >
+ <widget class="QLineEdit" name="userLineEdit" />
+ </item>
+ <item row="1" column="0" >
+ <widget class="QLabel" name="label_2" >
+ <property name="text" >
+ <string>Password:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1" >
+ <widget class="QLineEdit" name="passwordLineEdit" >
+ <property name="echoMode" >
+ <enum>QLineEdit::Password</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0" colspan="2" >
+ <layout class="QHBoxLayout" >
+ <item>
+ <spacer>
+ <property name="orientation" >
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" >
+ <size>
+ <width>61</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="okButton" >
+ <property name="text" >
+ <string>OK</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="canselButton" >
+ <property name="text" >
+ <string>Cansel</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>canselButton</sender>
+ <signal>clicked()</signal>
+ <receiver>SettingsDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel" >
+ <x>204</x>
+ <y>90</y>
+ </hint>
+ <hint type="destinationlabel" >
+ <x>26</x>
+ <y>102</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>