summaryrefslogtreecommitdiff
path: root/sunshine
diff options
context:
space:
mode:
authorKrzysztof Klinikowski <kkszysiu@home.(none)>2010-01-16 18:43:46 +0100
committerKrzysztof Klinikowski <kkszysiu@home.(none)>2010-01-16 18:43:46 +0100
commit4663395479875da09bbd3f91b4e5b1ca175a5391 (patch)
tree246538b5efed9e27ea73e1c4c85e0a7db4c59a10 /sunshine
parent6588c3881896688e8ac4b48678bd85d330225df8 (diff)
Project name change.
Diffstat (limited to 'sunshine')
-rw-r--r--sunshine/Makefile.am14
-rw-r--r--sunshine/__init__.py2
-rw-r--r--sunshine/aliasing.py136
-rw-r--r--sunshine/avatars.py174
-rw-r--r--sunshine/capabilities.py87
-rw-r--r--sunshine/channel/Makefile.am5
-rw-r--r--sunshine/channel/__init__.py18
-rw-r--r--sunshine/channel/contact_list.py371
-rw-r--r--sunshine/channel/group.py223
-rw-r--r--sunshine/channel/text.py198
-rw-r--r--sunshine/channel_manager.py94
-rw-r--r--sunshine/connection.py641
-rw-r--r--sunshine/connection_manager.py87
-rw-r--r--sunshine/contacts.py93
-rw-r--r--sunshine/debug.py37
-rw-r--r--sunshine/handle.py132
-rw-r--r--sunshine/lqsoft/Makefile.am4
-rwxr-xr-xsunshine/lqsoft/__init__.py0
-rw-r--r--sunshine/lqsoft/cstruct/Makefile.am6
-rwxr-xr-xsunshine/lqsoft/cstruct/__init__.py0
-rwxr-xr-xsunshine/lqsoft/cstruct/common.py322
-rw-r--r--sunshine/lqsoft/cstruct/constraints.py188
-rw-r--r--sunshine/lqsoft/cstruct/fields/Makefile.am5
-rwxr-xr-xsunshine/lqsoft/cstruct/fields/__init__.py3
-rw-r--r--sunshine/lqsoft/cstruct/fields/complex.py140
-rwxr-xr-xsunshine/lqsoft/cstruct/fields/numeric.py64
-rwxr-xr-xsunshine/lqsoft/cstruct/fields/text.py78
-rw-r--r--sunshine/lqsoft/cstruct/test/Makefile.am5
-rwxr-xr-xsunshine/lqsoft/cstruct/test/__init__.py2
-rwxr-xr-xsunshine/lqsoft/cstruct/test/test_complex.py107
-rwxr-xr-xsunshine/lqsoft/cstruct/test/test_numeric.py171
-rwxr-xr-xsunshine/lqsoft/cstruct/test/test_strings.py80
-rw-r--r--sunshine/lqsoft/pygadu/Makefile.am8
-rwxr-xr-xsunshine/lqsoft/pygadu/__init__.py0
-rwxr-xr-xsunshine/lqsoft/pygadu/models.py302
-rwxr-xr-xsunshine/lqsoft/pygadu/network.py9
-rwxr-xr-xsunshine/lqsoft/pygadu/network_base.py110
-rwxr-xr-xsunshine/lqsoft/pygadu/network_v8.py196
-rwxr-xr-xsunshine/lqsoft/pygadu/packets.py54
-rwxr-xr-xsunshine/lqsoft/pygadu/twisted_protocol.py276
-rw-r--r--sunshine/lqsoft/utils/Makefile.am2
-rwxr-xr-xsunshine/lqsoft/utils/__init__.py27
-rw-r--r--sunshine/presence.py335
-rw-r--r--sunshine/util/Makefile.am3
-rw-r--r--sunshine/util/__init__.py0
-rw-r--r--sunshine/util/decorator.py126
46 files changed, 4935 insertions, 0 deletions
diff --git a/sunshine/Makefile.am b/sunshine/Makefile.am
new file mode 100644
index 0000000..ff421e8
--- /dev/null
+++ b/sunshine/Makefile.am
@@ -0,0 +1,14 @@
+SUBDIRS = channel lqsoft util
+
+sunshinedir = $(pythondir)/sunshine
+sunshine_PYTHON = aliasing.py \
+ avatars.py \
+ capabilities.py \
+ channel_manager.py \
+ connection_manager.py \
+ connection.py \
+ contacts.py \
+ debug.py \
+ handle.py \
+ __init__.py \
+ presence.py
diff --git a/sunshine/__init__.py b/sunshine/__init__.py
new file mode 100644
index 0000000..6c7c92f
--- /dev/null
+++ b/sunshine/__init__.py
@@ -0,0 +1,2 @@
+from connection_manager import *
+from debug import *
diff --git a/sunshine/aliasing.py b/sunshine/aliasing.py
new file mode 100644
index 0000000..d5fd250
--- /dev/null
+++ b/sunshine/aliasing.py
@@ -0,0 +1,136 @@
+# telepathy-sunshine is the GaduGadu connection manager for Telepathy
+#
+# Copyright (C) 2007 Ali Sabil <ali.sabil@gmail.com>
+# Copyright (C) 2010 Krzysztof Klinikowski <kkszysiu@gmail.com>
+#
+# 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 logging
+
+import telepathy
+import telepathy.constants
+
+from sunshine.handle import SunshineHandleFactory
+from sunshine.util.decorator import async
+
+__all__ = ['SunshineAliasing']
+
+logger = logging.getLogger('Sunshine.Aliasing')
+
+class SunshineAliasing(telepathy.server.ConnectionInterfaceAliasing):
+
+ def __init__(self):
+ telepathy.server.ConnectionInterfaceAliasing.__init__(self)
+ self.aliases = {}
+
+ def GetAliasFlags(self):
+ return telepathy.constants.CONNECTION_ALIAS_FLAG_USER_SET
+
+ def RequestAliases(self, contacts):
+ logger.debug("Called RequestAliases")
+ return [self._get_alias(handle_id) for handle_id in contacts]
+
+ def GetAliases(self, contacts):
+ logger.debug("Called GetAliases")
+
+ result = {}
+ for contact in contacts:
+ result[contact] = self._get_alias(contact)
+ return result
+
+ def SetAliases(self, aliases):
+ for handle_id, alias in aliases.iteritems():
+ handle = self.handle(telepathy.HANDLE_TYPE_CONTACT, handle_id)
+ if handle != SunshineHandleFactory(self, 'self'):
+ logger.debug("Called SetAliases for handle: %s, alias: %s" % (handle.name, alias))
+
+ if alias == handle.name:
+ alias = ''
+
+ new_alias = alias
+
+ try:
+ handle.contact.updateName(new_alias)
+ except:
+ pass
+
+ #alias = unicode(alias, 'utf-8')
+ logger.info("Contact %s alias changed to '%s'" % (unicode(handle.name), alias))
+ self.aliases[handle.name] = alias
+ self.AliasesChanged([(handle, alias)])
+ else:
+ logger.info("Self alias changed to '%s'" % alias)
+ self.AliasesChanged(((SunshineHandleFactory(self, 'self'), alias), ))
+
+# # papyon.event.ContactEventInterface
+# def on_contact_display_name_changed(self, contact):
+# self._contact_alias_changed(contact)
+#
+# # papyon.event.ContactEventInterface
+# def on_contact_infos_changed(self, contact, updated_infos):
+# alias = updated_infos.get(ContactGeneral.ANNOTATIONS, {}).\
+# get(ContactAnnotations.NICKNAME, None)
+#
+# if alias is not None or alias != "":
+# self._contact_alias_changed(contact)
+#
+# # papyon.event.ContactEventInterface
+# def on_contact_memberships_changed(self, contact):
+# handle = ButterflyHandleFactory(self, 'contact',
+# contact.account, contact.network_id)
+# if contact.is_member(papyon.Membership.FORWARD):
+# alias = handle.pending_alias
+# if alias is not None:
+# infos = {ContactGeneral.ANNOTATIONS : \
+# {ContactAnnotations.NICKNAME : alias.encode('utf-8')}
+# }
+# self.msn_client.address_book.\
+# update_contact_infos(contact, infos)
+# handle.pending_alias = None
+
+ def _get_alias(self, handle_id):
+ """Get the alias from one handle id"""
+ handle = self.handle(telepathy.HANDLE_TYPE_CONTACT, handle_id)
+ if handle == SunshineHandleFactory(self, 'self'):
+ alias = 'Ja'
+ else:
+ contact = handle.contact
+ #print str(self.aliases)
+ if self.aliases.has_key(handle.name):
+ alias = self.aliases[handle.name]
+ #del self.aliases[handle.name]
+ elif contact is None:
+ alias = handle.name
+ else:
+ alias = contact.ShowName
+ if alias == '' or alias is None:
+ alias = contact.uin
+ return alias
+
+# @async
+# def _contact_alias_changed(self, contact):
+# handle = GaduHandleFactory(self, 'contact',
+# contact.account, None)
+#
+# alias = contact.infos.get(ContactGeneral.ANNOTATIONS, {}).\
+# get(ContactAnnotations.NICKNAME, None)
+#
+# if alias == "" or alias is None:
+# alias = contact.display_name
+#
+# alias = unicode(alias, 'utf-8')
+# logger.info("Contact %s alias changed to '%s'" % (unicode(handle), alias))
+# self.AliasesChanged([(handle, alias)])
+#
diff --git a/sunshine/avatars.py b/sunshine/avatars.py
new file mode 100644
index 0000000..e230d8f
--- /dev/null
+++ b/sunshine/avatars.py
@@ -0,0 +1,174 @@
+# telepathy-sunshine is the GaduGadu connection manager for Telepathy
+#
+# Copyright (C) 2006-2007 Ali Sabil <ali.sabil@gmail.com>
+# Copyright (C) 2010 Krzysztof Klinikowski <kkszysiu@gmail.com>
+#
+# 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 logging
+import imghdr
+import hashlib
+import dbus
+
+import telepathy
+
+from twisted.web.client import getPage
+
+from xml.dom import minidom
+
+from sunshine.handle import SunshineHandleFactory
+from sunshine.util.decorator import async
+
+__all__ = ['SunshineAvatars']
+
+logger = logging.getLogger('Sunshine.Avatars')
+
+
+class SunshineAvatars(telepathy.server.ConnectionInterfaceAvatars):
+
+ def __init__(self):
+ print 'SunshineAvatars called.'
+ self._avatar_known = False
+ telepathy.server.ConnectionInterfaceAvatars.__init__(self)
+
+ def GetAvatarRequirements(self):
+ mime_types = ("image/png","image/jpeg","image/gif")
+ return (mime_types, 96, 96, 192, 192, 500 * 1024)
+
+ def GetKnownAvatarTokens(self, contacts):
+ result = {}
+ for handle_id in contacts:
+ handle = self.handle(telepathy.HANDLE_TYPE_CONTACT, handle_id)
+ if handle == self.GetSelfHandle():
+ #tutaj kiedys trzeba napisac kod odp za naszego avatara
+ contact = None
+ av_token = handle.name
+ #pass
+ else:
+ contact = handle.contact
+
+ if contact is not None:
+ av_token = str(contact.uin)
+ else:
+ av_token = None
+
+ if av_token is not None:
+ result[handle] = av_token
+ elif self._avatar_known:
+ result[handle] = ""
+ return result
+
+ def RequestAvatars(self, contacts):
+ for handle_id in contacts:
+ handle = self.handle(telepathy.HANDLE_TYPE_CONTACT, handle_id)
+ if handle == self.GetSelfHandle():
+ #msn_object = self.msn_client.profile.msn_object
+ #self._msn_object_retrieved(msn_object, handle)
+ pass
+ else:
+ contact = handle.contact
+ if contact is not None:
+ url = 'http://api.gadu-gadu.pl/avatars/%s/0.xml' % (str(contact.uin))
+ d = getPage(url, timeout=10)
+ d.addCallback(self.on_fetch_avatars_file_ok, url, handle_id)
+ d.addErrback(self.on_fetch_avatars_file_failed, url, handle_id)
+
+ def SetAvatar(self, avatar, mime_type):
+ pass
+# self._avatar_known = True
+# if not isinstance(avatar, str):
+# avatar = "".join([chr(b) for b in avatar])
+# msn_object = papyon.p2p.MSNObject(self.msn_client.profile,
+# len(avatar),
+# papyon.p2p.MSNObjectType.DISPLAY_PICTURE,
+# hashlib.sha1(avatar).hexdigest() + '.tmp',
+# "",
+# data=StringIO.StringIO(avatar))
+# self.msn_client.profile.msn_object = msn_object
+# avatar_token = msn_object._data_sha.encode("hex")
+# logger.info("Setting self avatar to %s" % avatar_token)
+# return avatar_token
+
+ def ClearAvatar(self):
+ pass
+# self.msn_client.profile.msn_object = None
+# self._avatar_known = True
+
+ def on_fetch_avatars_file_ok(self, result, url, handle_id):
+ try:
+ if result:
+ logger.info("Avatar file retrieved from %s" % (url))
+ e = minidom.parseString(result)
+ data = e.getElementsByTagName('bigAvatar')[0].firstChild.data
+
+ d = getPage(str(data), timeout=20)
+ d.addCallback(self.on_fetch_avatars_ok, data, handle_id)
+ d.addErrback(self.on_fetch_avatars_failed, data, handle_id)
+ except:
+ logger.info("Avatar file can't be retrieved from %s" % (url))
+
+ def on_fetch_avatars_file_failed(self, error, url, handle_id):
+ logger.info("Avatar file can't be retrieved from %s, error: %s" % (url, error.getErrorMessage()))
+
+ def on_fetch_avatars_ok(self, result, url, handle_id):
+ try:
+ handle = self.handle(telepathy.constants.HANDLE_TYPE_CONTACT, handle_id)
+ logger.info("Avatar retrieved for %s from %s" % (handle.name, url))
+ type = imghdr.what('', result)
+ if type is None: type = 'jpeg'
+ avatar = dbus.ByteArray(result)
+ h = hashlib.new('md5')
+ h.update(url)
+ token = h.hexdigest()
+ self.AvatarRetrieved(handle, token, avatar, 'image/' + type)
+ except:
+ logger.debug("Avatar retrieved but something went wrong.")
+
+ def on_fetch_avatars_failed(self, error, url, handle_id):
+ logger.debug("Avatar not retrieved, error: %s" % (error.getErrorMessage()))
+#
+# # papyon.event.ContactEventInterface
+# def on_contact_msn_object_changed(self, contact):
+# if contact.msn_object is not None:
+# avatar_token = contact.msn_object._data_sha.encode("hex")
+# else:
+# avatar_token = ""
+# handle = ButterflyHandleFactory(self, 'contact',
+# contact.account, contact.network_id)
+# self.AvatarUpdated(handle, avatar_token)
+#
+# # papyon.event.ProfileEventInterface
+# def on_profile_msn_object_changed(self):
+# msn_object = self.msn_client.profile.msn_object
+# if msn_object is not None:
+# avatar_token = msn_object._data_sha.encode("hex")
+# logger.info("Self avatar changed to %s" % avatar_token)
+# handle = ButterflyHandleFactory(self, 'self')
+# self.AvatarUpdated(handle, avatar_token)
+#
+# @async
+# def _msn_object_retrieved(self, msn_object, handle):
+# if msn_object is not None and msn_object._data is not None:
+# logger.info("Avatar retrieved %s" % msn_object._data_sha.encode("hex"))
+# msn_object._data.seek(0, 0)
+# avatar = msn_object._data.read()
+# msn_object._data.seek(0, 0)
+# type = imghdr.what('', avatar)
+# if type is None: type = 'jpeg'
+# avatar = dbus.ByteArray(avatar)
+# token = msn_object._data_sha.encode("hex")
+# self.AvatarRetrieved(handle, token, avatar, 'image/' + type)
+# else:
+# logger.info("Avatar retrieved but NULL")
diff --git a/sunshine/capabilities.py b/sunshine/capabilities.py
new file mode 100644
index 0000000..92dbd5e
--- /dev/null
+++ b/sunshine/capabilities.py
@@ -0,0 +1,87 @@
+# telepathy-sunshine is the GaduGadu connection manager for Telepathy
+#
+# Copyright (C) 2009 Collabora Ltd.
+# Copyright (C) 2010 Krzysztof Klinikowski <kkszysiu@gmail.com>
+#
+# 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 logging
+import dbus
+
+import telepathy
+
+from sunshine.handle import SunshineHandleFactory
+
+__all__ = ['SunshineCapabilities']
+
+logger = logging.getLogger('Sunshine.Capabilities')
+
+class SunshineCapabilities(telepathy.server.ConnectionInterfaceCapabilities):
+
+ def __init__(self):
+ telepathy.server.ConnectionInterfaceCapabilities.__init__(self)
+# papyon.event.ContactEventInterface.__init__(self, self.msn_client)
+ dbus_interface = telepathy.CONNECTION_INTERFACE_CAPABILITIES
+
+ def AdvertiseCapabilities(self, add, remove):
+# for caps, specs in add:
+# if caps == telepathy.CHANNEL_TYPE_STREAMED_MEDIA:
+# if specs & telepathy.CHANNEL_MEDIA_CAPABILITY_VIDEO:
+# self._self_handle.profile.client_id.has_webcam = True
+# self._self_handle.profile.client_id.supports_rtc_video = True
+# for caps in remove:
+# if caps == telepathy.CHANNEL_TYPE_STREAMED_MEDIA:
+# self._self_handle.profile.client_id.has_webcam = False
+
+ return telepathy.server.ConnectionInterfaceCapabilities.\
+ AdvertiseCapabilities(self, add, remove)
+
+# # papyon.event.ContactEventInterface
+# def on_contact_client_capabilities_changed(self, contact):
+# self._update_capabilities(contact)
+#
+# def _update_capabilities(self, contact):
+# handle = ButterflyHandleFactory(self, 'contact',
+# contact.account, contact.network_id)
+# ctype = telepathy.CHANNEL_TYPE_STREAMED_MEDIA
+#
+# new_gen, new_spec = self._get_capabilities(contact)
+# if handle in self._caps:
+# old_gen, old_spec = self._caps[handle][ctype]
+# else:
+# old_gen = 0
+# old_spec = 0
+#
+# if old_gen == new_gen and old_spec == new_spec:
+# return
+#
+# diff = (int(handle), ctype, old_gen, new_gen, old_spec, new_spec)
+# self.CapabilitiesChanged([diff])
+#
+# def _get_capabilities(self, contact):
+# gen_caps = 0
+# spec_caps = 0
+#
+# caps = contact.client_capabilities
+# if caps.supports_sip_invite:
+# gen_caps |= telepathy.CONNECTION_CAPABILITY_FLAG_CREATE
+# gen_caps |= telepathy.CONNECTION_CAPABILITY_FLAG_INVITE
+# spec_caps |= telepathy.CHANNEL_MEDIA_CAPABILITY_AUDIO
+# spec_caps |= telepathy.CHANNEL_MEDIA_CAPABILITY_NAT_TRAVERSAL_STUN
+#
+# if caps.has_webcam:
+# spec_caps |= telepathy.CHANNEL_MEDIA_CAPABILITY_VIDEO
+#
+# return gen_caps, spec_caps
diff --git a/sunshine/channel/Makefile.am b/sunshine/channel/Makefile.am
new file mode 100644
index 0000000..875550b
--- /dev/null
+++ b/sunshine/channel/Makefile.am
@@ -0,0 +1,5 @@
+channeldir = $(pythondir)/sunshine/channel
+channel_PYTHON = contact_list.py \
+ group.py \
+ __init__.py \
+ text.py
diff --git a/sunshine/channel/__init__.py b/sunshine/channel/__init__.py
new file mode 100644
index 0000000..7a6b8a1
--- /dev/null
+++ b/sunshine/channel/__init__.py
@@ -0,0 +1,18 @@
+# telepathy-sunshine is the GaduGadu connection manager for Telepathy
+#
+# Copyright (C) 2006-2007 Ali Sabil <ali.sabil@gmail.com>
+# Copyright (C) 2010 Krzysztof Klinikowski <kkszysiu@gmail.com>
+#
+# 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
diff --git a/sunshine/channel/contact_list.py b/sunshine/channel/contact_list.py
new file mode 100644
index 0000000..31b4a02
--- /dev/null
+++ b/sunshine/channel/contact_list.py
@@ -0,0 +1,371 @@
+# telepathy-sunshine is the GaduGadu connection manager for Telepathy
+#
+# Copyright (C) 2006-2007 Ali Sabil <ali.sabil@gmail.com>
+# Copyright (C) 2010 Krzysztof Klinikowski <kkszysiu@gmail.com>
+#
+# 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 logging
+import weakref
+
+import telepathy
+
+import xml.etree.ElementTree as ET
+
+from sunshine.util.decorator import async
+from sunshine.handle import SunshineHandleFactory
+
+from sunshine.lqsoft.pygadu.twisted_protocol import GaduClient
+from sunshine.lqsoft.pygadu.models import GaduProfile, GaduContact
+
+__all__ = ['SunshineContactListChannelFactory']
+
+logger = logging.getLogger('Sunshine.ContactListChannel')
+
+class HandleMutex(object):
+ def __init__(self):
+ self._handles = set()
+ self._keys = {}
+ self._callbacks = {}
+
+ def is_locked(self, handle):
+ return (handle in self._handles)
+
+ def is_owned(self, key, handle):
+ return (handle in self._handles and self._keys[handle] == key)
+
+ def lock(self, key, handle):
+ if self.is_locked(handle):
+ return False
+ self._handles.add(handle)
+ self._keys[handle] = key
+ return True
+
+ def unlock(self, key, handle):
+ if not self.is_owned(key, handle):
+ return
+ self._handles.remove(handle)
+ del self._keys[handle]
+ callbacks = self._callbacks.get(handle, [])[:]
+ self._callbacks[handle] = []
+ for callback in callbacks:
+ callback[0](*callback[1:])
+
+ def add_callback(self, key, handle, callback):
+ if self.is_owned(key, handle):
+ return
+ if not self.is_locked(handle):
+ callback[0](*callback[1:])
+ else:
+ self._callbacks.setdefault(handle, []).append(callback)
+
+class Lockable(object):
+ def __init__(self, mutex, key, cb_name):
+ self._mutex = mutex
+ self._key = key
+ self._cb_name = cb_name
+
+ def __call__(self, func):
+ def method(object, handle, *args, **kwargs):
+ def finished_cb(*user_data):
+ self._mutex.unlock(self._key, handle)
+
+ def unlocked_cb():
+ self._mutex.lock(self._key, handle)
+ kwargs[self._cb_name] = finished_cb
+ if func(object, handle, *args, **kwargs):
+ finished_cb()
+
+ self._mutex.add_callback(self._key, handle, (unlocked_cb,))
+
+ return method
+
+mutex = HandleMutex()
+
+
+def SunshineContactListChannelFactory(connection, manager, handle, props):
+ handle = connection.handle(
+ props[telepathy.CHANNEL_INTERFACE + '.TargetHandleType'],
+ props[telepathy.CHANNEL_INTERFACE + '.TargetHandle'])
+
+ if handle.get_name() == 'subscribe':
+ channel_class = SunshineSubscribeListChannel
+ #hacky & tricky
+# elif handle.get_name() == 'publish':
+# channel_class = SunshineSubscribeListChannel
+
+# elif handle.get_name() == 'publish':
+# channel_class = ButterflyPublishListChannel
+# elif handle.get_name() == 'hide':
+# channel_class = ButterflyHideListChannel
+# elif handle.get_name() == 'allow':
+# channel_class = ButterflyAllowListChannel
+# elif handle.get_name() == 'deny':
+# channel_class = ButterflyDenyListChannel
+ else:
+ raise TypeError("Unknown list type : " + handle.get_name())
+ return channel_class(connection, manager, props)
+
+
+class SunshineListChannel(
+ telepathy.server.ChannelTypeContactList,
+ telepathy.server.ChannelInterfaceGroup):
+ "Abstract Contact List channels"
+
+ def __init__(self, connection, manager, props):
+ self._conn_ref = weakref.ref(connection)
+ telepathy.server.ChannelTypeContactList.__init__(self, connection, manager, props)
+ telepathy.server.ChannelInterfaceGroup.__init__(self)
+ self._populate(connection)
+
+ def GetLocalPendingMembersWithInfo(self):
+ return []
+
+ # papyon.event.AddressBookEventInterface
+ def on_addressbook_contact_added(self, contact):
+ added = set()
+ local_pending = set()
+ remote_pending = set()
+
+ ad, lp, rp = self._filter_contact(contact)
+ if ad or lp or rp:
+ handle = ButterflyHandleFactory(self._conn_ref(), 'contact',
+ contact.account, contact.network_id)
+ if ad: added.add(handle)
+ if lp: local_pending.add(handle)
+ if rp: remote_pending.add(handle)
+ msg = contact.attributes.get('invite_message', '')
+ self.MembersChanged(msg, added, (), local_pending, remote_pending, 0,
+ telepathy.CHANNEL_GROUP_CHANGE_REASON_NONE)
+
+ # papyon.event.AddressBookEventInterface
+ def on_addressbook_contact_deleted(self, contact):
+ handle = ButterflyHandleFactory(self._conn_ref(), 'contact',
+ contact.account, contact.network_id)
+ ad, lp, rp = self._filter_contact(contact)
+ if self._contains_handle(handle) and not ad:
+ self.MembersChanged('', (), [handle], (), (), 0,
+ telepathy.CHANNEL_GROUP_CHANGE_REASON_NONE)
+
+ # papyon.event.AddressBookEventInterface
+ def on_addressbook_contact_blocked(self, contact):
+ pass
+
+ # papyon.event.AddressBookEventInterface
+ def on_addressbook_contact_unblocked(self, contact):
+ pass
+
+ @async
+ def _populate(self, connection):
+ added = set()
+ local_pending = set()
+ remote_pending = set()
+
+ for contact in connection.gadu_client.contacts:
+ #logger.info("New contact %s, name: %s added." % (contact.uin, contact.ShowName))
+ ad, lp, rp = self._filter_contact(contact)
+ if ad or lp or rp:
+ handle = SunshineHandleFactory(self._conn_ref(), 'contact',
+ contact.uin, None)
+ if ad: added.add(handle)
+ if lp: local_pending.add(handle)
+ if rp: remote_pending.add(handle)
+ self.MembersChanged('', added, (), local_pending, remote_pending, 0,
+ telepathy.CHANNEL_GROUP_CHANGE_REASON_NONE)
+
+ def _filter_contact(self, contact):
+ return (False, False, False)
+
+ def _contains_handle(self, handle):
+ members, local_pending, remote_pending = self.GetAllMembers()
+ return (handle in members) or (handle in local_pending) or \
+ (handle in remote_pending)
+
+
+class SunshineSubscribeListChannel(SunshineListChannel):
+ """Subscribe List channel.
+
+ This channel contains the list of contact to whom the current used is
+ 'subscribed', basically this list contains the contact for whom you are
+ supposed to receive presence notification."""
+
+ def __init__(self, connection, manager, props):
+ SunshineListChannel.__init__(self, connection, manager, props)
+ self.GroupFlagsChanged(telepathy.CHANNEL_GROUP_FLAG_CAN_ADD |
+ telepathy.CHANNEL_GROUP_FLAG_CAN_REMOVE, 0)
+
+ def AddMembers(self, contacts, message):
+ logger.info("Subscribe - AddMembers called")
+ for h in contacts:
+ handle = self._conn.handle(telepathy.constants.HANDLE_TYPE_CONTACT, h)
+ contact_xml = ET.Element("Contact")
+ ET.SubElement(contact_xml, "Guid").text = str(handle.name)
+ ET.SubElement(contact_xml, "GGNumber").text = str(handle.name)
+ ET.SubElement(contact_xml, "ShowName").text = str(handle.name)
+ ET.SubElement(contact_xml, "Groups")
+ c = GaduContact.from_xml(contact_xml)
+ self._conn_ref().gadu_client.addContact( c )
+ #config.addNewContact( c )
+ self._conn_ref().gadu_client.notifyAboutContact( c )
+ logger.info("Adding contact: %s" % (handle.name))
+ self.MembersChanged('', [handle], (), (), (), 0,
+ telepathy.CHANNEL_GROUP_CHANGE_REASON_INVITED)
+
+ #alias and group settings for new contacts are bit tricky
+ #try to set alias
+ handle.contact.ShowName = self._conn_ref().get_contact_alias(handle.id)
+ #and group
+ if self._conn_ref().pending_contacts_to_group.has_key(handle.name):
+ logger.info("Trying to add temporary group.")
+ print str(self._conn_ref().pending_contacts_to_group)
+ print str(self._conn_ref().pending_contacts_to_group[handle.name])
+ handle.contact.updateGroups(self._conn_ref().pending_contacts_to_group[handle.name])
+ logger.info("Contact added.")
+
+ def RemoveMembers(self, contacts, message):
+ for h in contacts:
+ self._remove(h)
+
+ def _filter_contact(self, contact):
+ return (True, False, False)
+ #return (contact.is_member(papyon.Membership.FORWARD) and not
+ # contact.is_member(papyon.Membership.PENDING), False, False)
+
+ #@Lockable(mutex, 'add_subscribe', 'finished_cb')
+# def _add(self, handle_id, message, finished_cb):
+# logger.info("Subscribe - Add Members called.")
+# handle = self._conn.handle(telepathy.HANDLE_TYPE_CONTACT, handle_id)
+# if handle.contact is not None and \
+# handle.contact.is_member(papyon.Membership.FORWARD):
+# return True
+#
+# account = handle.account
+# network = handle.network
+# groups = list(handle.pending_groups)
+# handle.pending_groups = set()
+# ab = self._conn.msn_client.address_book
+# ab.add_messenger_contact(account,
+# network_id=network,
+# auto_allow=False,
+# invite_message=message.encode('utf-8'),
+# groups=groups,
+# done_cb=(finished_cb,),
+# failed_cb=(finished_cb,))
+
+ @Lockable(mutex, 'rem_subscribe', 'finished_cb')
+ def _remove(self, handle_id, finished_cb):
+ handle = self._conn.handle(telepathy.HANDLE_TYPE_CONTACT, handle_id)
+ contact = handle.contact
+ if contact is None or not contact.is_member(papyon.Membership.FORWARD):
+ return True
+ ab = self._conn.msn_client.address_book
+ ab.delete_contact(contact, done_cb=(finished_cb,),
+ failed_cb=(finished_cb,))
+
+ # papyon.event.ContactEventInterface
+ def on_contact_memberships_changed(self, contact):
+ handle = ButterflyHandleFactory(self._conn_ref(), 'contact',
+ contact.account, contact.network_id)
+ if contact.is_member(papyon.Membership.FORWARD):
+ self.MembersChanged('', [handle], (), (), (), 0,
+ telepathy.CHANNEL_GROUP_CHANGE_REASON_INVITED)
+ if len(handle.pending_groups) > 0:
+ ab = self._conn.msn_client.address_book
+ for group in handle.pending_groups:
+ ab.add_contact_to_group(group, contact)
+ handle.pending_groups = set()
+
+#
+#class ButterflyPublishListChannel(ButterflyListChannel,
+# papyon.event.ContactEventInterface):
+#
+# def __init__(self, connection, manager, props):
+# ButterflyListChannel.__init__(self, connection, manager, props)
+# papyon.event.ContactEventInterface.__init__(self, connection.msn_client)
+# self.GroupFlagsChanged(0, 0)
+#
+# def AddMembers(self, contacts, message):
+# for handle_id in contacts:
+# self._add(handle_id, message)
+#
+# def RemoveMembers(self, contacts, message):
+# for handle_id in contacts:
+# self._remove(handle_id)
+#
+# def GetLocalPendingMembersWithInfo(self):
+# result = []
+# for contact in self._conn.msn_client.address_book.contacts:
+# if not contact.is_member(papyon.Membership.PENDING):
+# continue
+# handle = ButterflyHandleFactory(self._conn_ref(), 'contact',
+# contact.account, contact.network_id)
+# result.append((handle, handle,
+# telepathy.CHANNEL_GROUP_CHANGE_REASON_INVITED,
+# contact.attributes.get('invite_message', '')))
+# return result
+#
+# def _filter_contact(self, contact):
+# return (contact.is_member(papyon.Membership.ALLOW),
+# contact.is_member(papyon.Membership.PENDING),
+# False)
+#
+# @Lockable(mutex, 'add_publish', 'finished_cb')
+# def _add(self, handle_id, message, finished_cb):
+# handle = self._conn.handle(telepathy.HANDLE_TYPE_CONTACT, handle_id)
+# contact = handle.contact
+# if contact is not None and contact.is_member(papyon.Membership.ALLOW):
+# return True
+#
+# account = handle.account
+# network = handle.network
+# ab = self._conn.msn_client.address_book
+# if contact is not None and contact.is_member(papyon.Membership.PENDING):
+# ab.accept_contact_invitation(contact, False,
+# done_cb=(finished_cb,), failed_cb=(finished_cb,))
+# else:
+# ab.allow_contact(account, network,
+# done_cb=(finished_cb,), failed_cb=(finished_cb,))
+#
+# @Lockable(mutex, 'rem_publish', 'finished_cb')
+# def _remove(self, handle_id, finished_cb):
+# handle = self._conn.handle(telepathy.HANDLE_TYPE_CONTACT, handle_id)
+# contact = handle.contact
+# ab = self._conn.msn_client.address_book
+# if contact.is_member(papyon.Membership.PENDING):
+# ab.decline_contact_invitation(contact, False, done_cb=finished_cb,
+# failed_cb=finished_cb)
+# elif contact.is_member(papyon.Membership.ALLOW):
+# ab.disallow_contact(contact, done_cb=(finished_cb,),
+# failed_cb=(finished_cb,))
+# else:
+# return True
+#
+# # papyon.event.ContactEventInterface
+# def on_contact_memberships_changed(self, contact):
+# handle = ButterflyHandleFactory(self._conn_ref(), 'contact',
+# contact.account, contact.network_id)
+# if self._contains_handle(handle):
+# if contact.is_member(papyon.Membership.PENDING):
+# # Nothing worth our attention
+# return
+#
+# if contact.is_member(papyon.Membership.ALLOW):
+# # Contact accepted
+# self.MembersChanged('', [handle], (), (), (), 0,
+# telepathy.CHANNEL_GROUP_CHANGE_REASON_INVITED)
+# else:
+# # Contact rejected
+# self.MembersChanged('', (), [handle], (), (), 0,
+# telepathy.CHANNEL_GROUP_CHANGE_REASON_NONE)
diff --git a/sunshine/channel/group.py b/sunshine/channel/group.py
new file mode 100644
index 0000000..6094092
--- /dev/null
+++ b/sunshine/channel/group.py
@@ -0,0 +1,223 @@
+# telepathy-sunshine is the GaduGadu connection manager for Telepathy
+#
+# Copyright (C) 2006-2007 Ali Sabil <ali.sabil@gmail.com>
+# Copyright (C) 2007 Johann Prieur <johann.prieur@gmail.com>
+# Copyright (C) 2010 Krzysztof Klinikowski <kkszysiu@gmail.com>
+#
+# 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 logging, hashlib
+
+import telepathy
+
+import xml.etree.ElementTree as ET
+
+from sunshine.lqsoft.pygadu.models import GaduProfile, GaduContact, GaduContactGroup
+
+from sunshine.util.decorator import async
+from sunshine.handle import SunshineHandleFactory
+from sunshine.channel.contact_list import SunshineListChannel
+
+__all__ = ['SunshineGroupChannel']
+
+logger = logging.getLogger('Sunshine.GroupChannel')
+
+
+class SunshineGroupChannel(SunshineListChannel):
+
+ def __init__(self, connection, manager, props):
+ self.__pending_add = []
+ self.__pending_remove = []
+ self.conn = connection
+ self.groups = {}
+ SunshineListChannel.__init__(self, connection, manager, props)
+ self.GroupFlagsChanged(telepathy.CHANNEL_GROUP_FLAG_CAN_ADD |
+ telepathy.CHANNEL_GROUP_FLAG_CAN_REMOVE, 0)
+ @async
+ def create_group():
+ if self._handle.group is None:
+ name = self._handle.name
+ for group in self.conn.profile.groups:
+ if group.Name != name:
+ h = hashlib.md5()
+ h.update(name)
+
+ group_xml = ET.Element("Group")
+ ET.SubElement(group_xml, "Id").text = h.hexdigest()
+ ET.SubElement(group_xml, "Name").text = name
+ ET.SubElement(group_xml, "IsExpanded").text = str('True')
+ ET.SubElement(group_xml, "IsRemovable").text = str('True')
+
+ g = GaduContactGroup.from_xml(group_xml)
+ self.conn.profile.addGroup(g)
+
+ for group in self.conn.profile.groups:
+ self.groups[group.Id] = group.Name
+
+ for contact in self.conn.profile.contacts:
+ contact_groups = ET.fromstring(contact.Groups)
+ if contact.Groups:
+ for group in contact_groups.getchildren():
+ if self.groups.has_key(group.text):
+ if self.groups[group.text] == self._handle.group.Name:
+ self.add_contact_to_group(self._handle.group, contact, None)
+ create_group()
+
+
+ def AddMembers(self, contacts, message):
+ for contact_handle_id in contacts:
+ contact_handle = self._conn.handle(telepathy.HANDLE_TYPE_CONTACT,
+ contact_handle_id)
+ logger.info("Adding contact %s to group %s" %
+ (unicode(contact_handle), unicode(self._handle)))
+
+ contact = contact_handle.contact
+ group = self._handle.group
+
+ self.add_contact_to_group(group, contact, contact_handle)
+
+
+ def RemoveMembers(self, contacts, message):
+ for contact_handle_id in contacts:
+ contact_handle = self._conn.handle(telepathy.HANDLE_TYPE_CONTACT,
+ contact_handle_id)
+ logger.info("Removing contact %s from pending group %s" %
+ (unicode(contact_handle), unicode(self._handle)))
+
+ contact = contact_handle.contact
+ group = self._handle.group
+
+ self.delete_contact_from_group(group, contact, contact_handle)
+
+ def Close(self):
+ logger.debug("Deleting group %s" % self._handle.name)
+ del self.conn.profile.groups[self._handle.name]
+# ab = self._conn.msn_client.address_book
+# group = self._handle.group
+# ab.delete_group(group)
+
+# def _filter_contact(self, contact):
+# if contact.is_member(papyon.Membership.FORWARD):
+# for group in contact.groups:
+# if group.name.decode("utf-8") == self._handle.name:
+# return (True, False, False)
+# return (False, False, False)
+#
+# def on_addressbook_group_added(self, group):
+# if group.name.decode("utf-8") == self._handle.name:
+# self.AddMembers(self.__pending_add, None)
+# self.__pending_add = []
+# self.RemoveMembers(self.__pending_remove, None)
+# self.__pending_remove = []
+#
+# def on_addressbook_group_deleted(self, group):
+# if group.name.decode("utf-8") == self._handle.name:
+# self.Closed()
+# self._conn.remove_channel(self)
+#
+# def on_addressbook_group_contact_added(self, group, contact):
+# group_name = group.name.decode("utf-8")
+# if group_name == self._handle.name:
+# handle = ButterflyHandleFactory(self._conn_ref(), 'contact',
+# contact.account, contact.network_id)
+#
+# added = set()
+# added.add(handle)
+#
+# self.MembersChanged('', added, (), (), (), 0,
+# telepathy.CHANNEL_GROUP_CHANGE_REASON_NONE)
+#
+# logger.debug("Contact %s added to group %s" %
+# (handle.name, group_name))
+#
+# def on_addressbook_group_contact_deleted(self, group, contact):
+# group_name = group.name.decode("utf-8")
+# if group_name == self._handle.name:
+# handle = ButterflyHandleFactory(self._conn_ref(), 'contact',
+# contact.account, contact.network_id)
+#
+# removed = set()
+# removed.add(handle)
+#
+# self.MembersChanged('', (), removed, (), (), 0,
+# telepathy.CHANNEL_GROUP_CHANGE_REASON_NONE)
+#
+# logger.debug("Contact %s removed from group %s" %
+# (handle.name, group_name))
+#
+
+ @async
+ def add_contact_to_group(self, group, contact, contact_handle):
+ group_name = group.Name
+ if group_name == self._handle.name:
+ if hasattr(contact, 'uin'):
+ contact_uin = contact.uin
+ else:
+ contact_uin = contact_handle.name
+
+ handle = SunshineHandleFactory(self.conn, 'contact',
+ contact_uin, None)
+ added = set()
+ added.add(handle)
+
+ if group.Name and group.Id:
+ is_group = False
+
+ contact_groups_xml = ET.Element("Groups")
+ if hasattr(contact, 'Groups'):
+ contact_groups = ET.fromstring(contact.Groups)
+ for c_group in contact_groups.getchildren():
+ if c_group.text == group.Id:
+ is_group = True
+ ET.SubElement(contact_groups_xml, "GroupId").text = c_group.text
+ if is_group != True:
+ ET.SubElement(contact_groups_xml, "GroupId").text = group.Id
+ c_groups = ET.tostring(contact_groups_xml)
+
+ if hasattr(contact, 'updateGroups'):
+ contact.updateGroups(c_groups)
+ else:
+ self.conn.pending_contacts_to_group[contact_uin] = c_groups
+
+ self.MembersChanged('', added, (), (), (), 0,
+ telepathy.CHANNEL_GROUP_CHANGE_REASON_NONE)
+
+ logger.debug("Contact %s added to group %s" %
+ (handle.name, group_name))
+
+ @async
+ def delete_contact_from_group(self, group, contact, contact_handle):
+ group_name = group.Name
+ if group_name == self._handle.name:
+ handle = SunshineHandleFactory(self.conn, 'contact',
+ contact.uin, None)
+ removed = set()
+ removed.add(handle)
+
+ contact_groups_xml = ET.Element("Groups")
+ contact_groups = ET.fromstring(contact.Groups)
+ if contact.Groups:
+ for c_group in contact_groups.getchildren():
+ if c_group.text != group.Id:
+ ET.SubElement(contact_groups_xml, "GroupId").text = c_group.text
+ c_groups = ET.tostring(contact_groups_xml)
+
+ contact.updateGroups(c_groups)
+
+ self.MembersChanged('', (), removed, (), (), 0,
+ telepathy.CHANNEL_GROUP_CHANGE_REASON_NONE)
+
+ logger.debug("Contact %s removed from group %s" %
+ (handle.name, group_name))
diff --git a/sunshine/channel/text.py b/sunshine/channel/text.py
new file mode 100644
index 0000000..03ccc9e
--- /dev/null
+++ b/sunshine/channel/text.py
@@ -0,0 +1,198 @@
+# telepathy-sunshine is the GaduGadu connection manager for Telepathy
+#
+# Copyright (C) 2006-2007 Ali Sabil <ali.sabil@gmail.com>
+# Copyright (C) 2007 Johann Prieur <johann.prieur@gmail.com>
+# Copyright (C) 2010 Krzysztof Klinikowski <kkszysiu@gmail.com>
+#
+# 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 logging
+import weakref
+import time
+
+import telepathy
+
+from sunshine.util.decorator import async
+from sunshine.handle import SunshineHandleFactory
+
+__all__ = ['SunshineTextChannel']
+
+logger = logging.getLogger('Sunshine.TextChannel')
+
+
+class SunshineTextChannel(telepathy.server.ChannelTypeText):
+
+ def __init__(self, conn, manager, conversation, props):
+ _, surpress_handler, handle = manager._get_type_requested_handle(props)
+ self._recv_id = 0
+ self._conn_ref = weakref.ref(conn)
+ self.conn = conn
+
+ self.contact = handle.contact
+
+ telepathy.server.ChannelTypeText.__init__(self, conn, manager, props)
+
+ def Send(self, message_type, text):
+ if message_type == telepathy.CHANNEL_TEXT_MESSAGE_TYPE_NORMAL:
+ logger.info("Sending message : %s" % unicode(text))
+ msg = text.encode('windows-1250')
+ self.conn.gadu_client.sendTo(int(self.contact.uin), str(text), str(msg))
+ else:
+ raise telepathy.NotImplemented("Unhandled message type")
+ self.Sent(int(time.time()), message_type, text)
+
+ def Close(self):
+ telepathy.server.ChannelTypeText.Close(self)
+ self.remove_from_connection()
+
+ # Redefine GetSelfHandle since we use our own handle
+ # as Butterfly doesn't have channel specific handles
+ def GetSelfHandle(self):
+ return self._conn.GetSelfHandle()
+
+ # Rededefine AcknowledgePendingMessages to remove offline messages
+ # from the oim box.
+ def AcknowledgePendingMessages(self, ids):
+ telepathy.server.ChannelTypeText.AcknowledgePendingMessages(self, ids)
+# messages = []
+# for id in ids:
+# if id in self._pending_offline_messages.keys():
+# messages.append(self._pending_offline_messages[id])
+# del self._pending_offline_messages[id]
+# self._oim_box_ref().delete_messages(messages)
+
+ # Rededefine ListPendingMessages to remove offline messages
+ # from the oim box.
+ def ListPendingMessages(self, clear):
+ return telepathy.server.ChannelTypeText.ListPendingMessages(self, clear)
+
+
+# if clear:
+# messages = self._pending_offline_messages.values()
+# self._oim_box_ref().delete_messages(messages)
+# return telepathy.server.ChannelTypeText.ListPendingMessages(self, clear)
+#
+# # papyon.event.ConversationEventInterface
+# def on_conversation_user_joined(self, contact):
+# handle = ButterflyHandleFactory(self._conn_ref(), 'contact',
+# contact.account, contact.network_id)
+# logger.info("User %s joined" % unicode(handle))
+# if handle not in self._members:
+# self.MembersChanged('', [handle], [], [], [],
+# handle, telepathy.CHANNEL_GROUP_CHANGE_REASON_INVITED)
+#
+# # papyon.event.ConversationEventInterface
+# def on_conversation_user_left(self, contact):
+# handle = ButterflyHandleFactory(self._conn_ref(), 'contact',
+# contact.account, contact.network_id)
+# logger.info("User %s left" % unicode(handle))
+# # There was only us and we are leaving, is it necessary?
+# if len(self._members) == 1:
+# self.ChatStateChanged(handle, telepathy.CHANNEL_CHAT_STATE_GONE)
+# elif len(self._members) == 2:
+# # Add the last user who left as the offline contact so we may still send
+# # him offlines messages and destroy the conversation
+# self._conversation.leave()
+# self._conversation = None
+# self._offline_handle = handle
+# self._offline_contact = contact
+# else:
+# #If there is only us and a offline contact don't remove him from
+# #the members since we still send him messages
+# self.MembersChanged('', [], [handle], [], [],
+# handle, telepathy.CHANNEL_GROUP_CHANGE_REASON_NONE)
+#
+# # papyon.event.ConversationEventInterface
+# def on_conversation_user_typing(self, contact):
+# handle = ButterflyHandleFactory(self._conn_ref(), 'contact',
+# contact.account, contact.network_id)
+# logger.info("User %s is typing" % unicode(handle))
+# self.ChatStateChanged(handle, telepathy.CHANNEL_CHAT_STATE_COMPOSING)
+#
+# # papyon.event.ConversationEventInterface
+# def on_conversation_message_received(self, sender, message):
+# id = self._recv_id
+# timestamp = int(time.time())
+# handle = ButterflyHandleFactory(self._conn_ref(), 'contact',
+# sender.account, sender.network_id)
+# type = telepathy.CHANNEL_TEXT_MESSAGE_TYPE_NORMAL
+# message = message.content
+# logger.info("User %s sent a message" % unicode(handle))
+# self.Received(id, timestamp, handle, type, 0, message)
+# self._recv_id += 1
+#
+# # papyon.event.ConversationEventInterface
+# def on_conversation_nudge_received(self, sender):
+# id = self._recv_id
+# timestamp = int(time.time())
+# handle = ButterflyHandleFactory(self._conn_ref(), 'contact',
+# sender.account, sender.network_id)
+# type = telepathy.CHANNEL_TEXT_MESSAGE_TYPE_ACTION
+# text = unicode("sends you a nudge", "utf-8")
+# logger.info("User %s sent a nudge" % unicode(handle))
+# self.Received(id, timestamp, handle, type, 0, text)
+# self._recv_id += 1
+#
+# # papyon.event.ContactEventInterface
+# def on_contact_presence_changed(self, contact):
+# handle = ButterflyHandleFactory(self._conn_ref(), 'contact',
+# contact.account, contact.network_id)
+# # Recreate a conversation if our contact join
+# if self._offline_contact == contact and contact.presence != papyon.Presence.OFFLINE:
+# logger.info('Contact %s connected, inviting him to the text channel' % unicode(contact))
+# client = self._conn_ref().msn_client
+# self._conversation = papyon.Conversation(client, [contact])
+# papyon.event.ConversationEventInterface.__init__(self, self._conversation)
+# self._offline_contact = None
+# self._offline_handle = None
+# #FIXME : I really hope there is no race condition between the time
+# # the contact accept the invitation and the time we send him a message
+# # Can a user refuse an invitation? what happens then?
+#
+#
+# # Public API
+# def offline_message_received(self, message):
+# # @message a papyon.OfflineIM.OfflineMessage
+# id = self._recv_id
+# sender = message.sender
+# timestamp = time.mktime(message.date.timetuple())
+# text = message.text
+#
+# # Map the id to the offline message so we can remove it
+# # when acked by the client
+# self._pending_offline_messages[id] = message
+#
+# handle = ButterflyHandleFactory(self._conn_ref(), 'contact',
+# sender.account, sender.network_id)
+# type = telepathy.CHANNEL_TEXT_MESSAGE_TYPE_NORMAL
+# logger.info("User %r sent a offline message" % handle)
+# self.Received(id, timestamp, handle, type, 0, text)
+#
+# self._recv_id += 1
+#
+# @async
+# def __add_initial_participants(self):
+# handles = []
+# handles.append(self._conn.GetSelfHandle())
+# if self._conversation:
+# for participant in self._conversation.participants:
+# handle = ButterflyHandleFactory(self._conn_ref(), 'contact',
+# participant.account, participant.network_id)
+# handles.append(handle)
+# else:
+# handles.append(self._offline_handle)
+#
+# self.MembersChanged('', handles, [], [], [],
+# 0, telepathy.CHANNEL_GROUP_CHANGE_REASON_NONE)
diff --git a/sunshine/channel_manager.py b/sunshine/channel_manager.py
new file mode 100644
index 0000000..8cff4b9
--- /dev/null
+++ b/sunshine/channel_manager.py
@@ -0,0 +1,94 @@
+# telepathy-sunshine is the GaduGadu connection manager for Telepathy
+#
+# Copyright (C) 2006-2007 Ali Sabil <ali.sabil@gmail.com>
+# Copyright (C) 2010 Krzysztof Klinikowski <kkszysiu@gmail.com>
+#
+# 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 logging
+import weakref
+
+import dbus
+import telepathy
+
+from sunshine.channel.contact_list import SunshineContactListChannelFactory
+from sunshine.channel.group import SunshineGroupChannel
+from sunshine.channel.text import SunshineTextChannel
+#from sunshine.channel.media import SunshineMediaChannel
+from sunshine.handle import SunshineHandleFactory
+
+__all__ = ['SunshineChannelManager']
+
+logger = logging.getLogger('Sunshine.ChannelManager')
+
+class SunshineChannelManager(telepathy.server.ChannelManager):
+ def __init__(self, connection):
+ telepathy.server.ChannelManager.__init__(self, connection)
+
+ fixed = {telepathy.CHANNEL_INTERFACE + '.ChannelType': telepathy.CHANNEL_TYPE_TEXT,
+ telepathy.CHANNEL_INTERFACE + '.TargetHandleType': dbus.UInt32(telepathy.HANDLE_TYPE_CONTACT)}
+ self._implement_channel_class(telepathy.CHANNEL_TYPE_TEXT,
+ self._get_text_channel, fixed, [])
+
+ fixed = {telepathy.CHANNEL_INTERFACE + '.ChannelType': telepathy.CHANNEL_TYPE_CONTACT_LIST}
+ self._implement_channel_class(telepathy.CHANNEL_TYPE_CONTACT_LIST,
+ self._get_list_channel, fixed, [])
+
+# fixed = {telepathy.CHANNEL_INTERFACE + '.ChannelType': telepathy.CHANNEL_TYPE_STREAMED_MEDIA,
+# telepathy.CHANNEL_INTERFACE + '.TargetHandleType': dbus.UInt32(telepathy.HANDLE_TYPE_CONTACT)}
+# self._implement_channel_class(telepathy.CHANNEL_TYPE_STREAMED_MEDIA,
+# self._get_media_channel, fixed, [telepathy.CHANNEL_INTERFACE + '.TargetHandle'])
+
+ def _get_list_channel(self, props):
+ _, surpress_handler, handle = self._get_type_requested_handle(props)
+
+ if handle.get_type() == telepathy.HANDLE_TYPE_GROUP:
+ channel = SunshineGroupChannel(self._conn, self, props)
+ logger.debug('New group channel')
+ else:
+ channel = SunshineContactListChannelFactory(self._conn,
+ self, handle, props)
+ logger.debug('New contact list channel: %s' % (handle.name))
+ return channel
+
+ def _get_text_channel(self, props, conversation=None):
+ _, surpress_handler, handle = self._get_type_requested_handle(props)
+
+ if handle.get_type() != telepathy.HANDLE_TYPE_CONTACT:
+ raise telepathy.NotImplemented('Only contacts are allowed')
+
+ logger.debug('New text channel for handle, name: %s, id: %s, type: %s' % (handle.name, handle.id, handle.type))
+
+ channel = SunshineTextChannel(self._conn, self, conversation, props)
+ return channel
+
+# def _get_media_channel(self, props, call=None):
+# _, surpress_handler, handle = self._get_type_requested_handle(props)
+#
+# if handle.get_type() != telepathy.HANDLE_TYPE_CONTACT:
+# raise telepathy.NotImplemented('Only contacts are allowed')
+#
+# contact = handle.contact
+#
+## if contact.presence == papyon.Presence.OFFLINE:
+## raise telepathy.NotAvailable('Contact not available')
+#
+# logger.debug('New media channel')
+#
+# if call is None:
+# client = self._conn.msn_client
+# call = client.call_manager.create_call(contact)
+#
+# return GaduMediaChannel(self._conn, self, call, handle, props)
diff --git a/sunshine/connection.py b/sunshine/connection.py
new file mode 100644
index 0000000..86bd0c4
--- /dev/null
+++ b/sunshine/connection.py
@@ -0,0 +1,641 @@
+# telepathy-sunshine is the GaduGadu connection manager for Telepathy
+#
+# Copyright (C) 2010 Krzysztof Klinikowski <kkszysiu@gmail.com>
+#
+# 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 time
+import weakref
+import logging
+
+import xml.etree.ElementTree as ET
+
+from sunshine.lqsoft.pygadu.twisted_protocol import GaduClient
+from sunshine.lqsoft.pygadu.models import GaduProfile, GaduContact, GaduContactGroup
+
+from twisted.internet import reactor, protocol
+from twisted.web.client import getPage
+from twisted.python import log
+
+import dbus
+import telepathy
+
+from sunshine.presence import SunshinePresence
+from sunshine.aliasing import SunshineAliasing
+from sunshine.avatars import SunshineAvatars
+from sunshine.handle import SunshineHandleFactory
+from sunshine.capabilities import SunshineCapabilities
+from sunshine.contacts import SunshineContacts
+from sunshine.channel_manager import SunshineChannelManager
+from sunshine.util.decorator import async
+
+__all__ = ['SunshineConnection']
+
+logger = logging.getLogger('Sunshine.Connection')
+
+
+class SunshineConfig(object):
+ def __init__(self, uin):
+ self.uin = uin
+ self.path = None
+ self.contacts_count = 0
+
+ def check_dirs(self):
+ path = os.path.join(os.path.join(os.environ['HOME'], '.telepathy-sunshine'), str(self.uin))
+ try:
+ os.makedirs(path)
+ except:
+ pass
+ if os.path.isfile(os.path.join(path, 'profile.xml')):
+ pass
+ else:
+ contactbook_xml = ET.Element("ContactBook")
+
+ ET.SubElement(contactbook_xml, "Groups")
+ ET.SubElement(contactbook_xml, "Contacts")
+
+ main_xml = ET.ElementTree(contactbook_xml)
+ main_xml.write(os.path.join(path, 'profile.xml'), encoding="UTF-8")
+
+ self.path = os.path.join(path, 'profile.xml')
+ return os.path.join(path, 'profile.xml')
+
+ def get_contacts(self):
+ file = open(self.path, "r")
+ config_xml = ET.parse(file).getroot()
+
+ self.roster = {'groups':[], 'contacts':[]}
+
+ for elem in config_xml.find('Groups').getchildren():
+ self.roster['groups'].append(elem)
+
+ for elem in config_xml.find('Contacts').getchildren():
+ self.roster['contacts'].append(elem)
+
+ self.contacts_count = len(config_xml.find('Contacts').getchildren())
+
+ return self.roster
+
+ def make_contacts_file(self, groups, contacts):
+ contactbook_xml = ET.Element("ContactBook")
+
+ groups_xml = ET.SubElement(contactbook_xml, "Groups")
+ contacts_xml = ET.SubElement(contactbook_xml, "Contacts")
+
+ for group in groups:
+ #Id, Name, IsExpanded, IsRemovable
+ group_xml = ET.SubElement(groups_xml, "Group")
+ ET.SubElement(group_xml, "Id").text = group.Id
+ ET.SubElement(group_xml, "Name").text = group.Name
+ ET.SubElement(group_xml, "IsExpanded").text = str(group.IsExpanded)
+ ET.SubElement(group_xml, "IsRemovable").text = str(group.IsRemovable)
+
+ for contact in contacts:
+ #Guid, GGNumber, ShowName. MobilePhone. HomePhone, Email, WWWAddress, FirstName, LastName, Gender, Birth, City, Province, Groups, CurrentAvatar, Avatars
+ contact_xml = ET.SubElement(contacts_xml, "Contact")
+ ET.SubElement(contact_xml, "Guid").text = contact.Guid
+ ET.SubElement(contact_xml, "GGNumber").text = contact.GGNumber
+ ET.SubElement(contact_xml, "ShowName").text = contact.ShowName
+ contact_groups_xml = ET.SubElement(contact_xml, "Groups")
+ contact_groups = ET.fromstring(contact.Groups)
+ if contact.Groups:
+ for group in contact_groups.getchildren():
+ ET.SubElement(contact_groups_xml, "GroupId").text = group.text
+
+ main_xml = ET.ElementTree(contactbook_xml)
+ main_xml.write(self.path, encoding="UTF-8")
+
+ def get_contacts_count(self):
+ return self.contacts_count
+
+#class GaduClientFactory(protocol.ClientFactory, protocol.ReconnectingClientFactory):
+class GaduClientFactory(protocol.ClientFactory):
+ def __init__(self, config):
+ self.config = config
+
+ def buildProtocol(self, addr):
+ # connect using current selected profile
+ #self.resetDelay()
+ return GaduClient(self.config)
+
+ def startedConnecting(self, connector):
+ logger.info('Started to connect.')
+
+ def clientConnectionLost(self, connector, reason):
+ logger.info('Lost connection. Reason: %s' % (reason))
+ #protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
+ #connector.connect()
+ reactor.stop()
+
+ def clientConnectionFailed(self, connector, reason):
+ logger.info('Connection failed. Reason: %s' % (reason))
+ #protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
+ reactor.stop()
+
+class SunshineConnection(telepathy.server.Connection,
+ telepathy.server.ConnectionInterfaceRequests,
+ SunshinePresence,
+ SunshineAliasing,
+ SunshineAvatars,
+ SunshineCapabilities,
+ SunshineContacts
+ ):
+
+
+ _mandatory_parameters = {
+ 'account' : 's',
+ 'password' : 's'
+ }
+ _optional_parameters = {
+ 'server' : 's',
+ 'port' : 'q'
+ }
+ _parameter_defaults = {
+ 'server' : '91.197.13.67',
+ 'port' : 8074
+ }
+
+ def __init__(self, manager, parameters):
+ self.check_parameters(parameters)
+
+ try:
+ account = unicode(parameters['account'])
+ server = (parameters['server'], parameters['port'])
+
+ self._manager = weakref.proxy(manager)
+ self._account = (parameters['account'], parameters['password'])
+ self._server = (parameters['server'], parameters['port'])
+
+ self.profile = GaduProfile(uin= int(parameters['account']) )
+ self.profile.uin = int(parameters['account'])
+ self.profile.password = str(parameters['password'])
+ self.profile.status = 0x014
+ self.profile.onLoginSuccess = self.on_loginSuccess
+ self.profile.onLoginFailure = self.on_loginFailed
+ self.profile.onContactStatusChange = self.on_updateContact
+ self.profile.onMessageReceived = self.on_messageReceived
+ self.profile.onStatusNoticiesRecv = self.on_StatusNoticiesRecv
+
+ #lets try to make file with contacts etc ^^
+ self.configfile = SunshineConfig(int(parameters['account']))
+ self.configfile.check_dirs()
+ #lets get contacts from contacts config file
+ contacts_list = self.configfile.get_contacts()
+
+ for contact_from_list in contacts_list['contacts']:
+ c = GaduContact.from_xml(contact_from_list)
+ try:
+ c.uin
+ self.profile.addContact( c )
+ except:
+ pass
+
+ for group_from_list in contacts_list['groups']:
+ g = GaduContactGroup.from_xml(group_from_list)
+ if g.Name:
+ self.profile.addGroup(g)
+
+ logger.info("We have %s contacts in file." % (self.configfile.get_contacts_count()))
+
+
+ self.factory = GaduClientFactory(self.profile)
+ self._channel_manager = SunshineChannelManager(self)
+
+ self._recv_id = 0
+ self.pending_contacts_to_group = {}
+ self._status = None
+
+ # Call parent initializers
+ telepathy.server.Connection.__init__(self, 'gadugadu', account, 'sunshine')
+ telepathy.server.ConnectionInterfaceRequests.__init__(self)
+ SunshinePresence.__init__(self)
+ SunshineAliasing.__init__(self)
+ SunshineAvatars.__init__(self)
+ SunshineCapabilities.__init__(self)
+ SunshineContacts.__init__(self)
+
+
+ self.set_self_handle(SunshineHandleFactory(self, 'self'))
+
+ self.__disconnect_reason = telepathy.CONNECTION_STATUS_REASON_NONE_SPECIFIED
+ #small hack. We started to connnect with status invisible and just later we change status to client-like
+ self._initial_presence = 0x014
+ self._initial_personal_message = None
+
+ logger.info("Connection to the account %s created" % account)
+ except Exception, e:
+ import traceback
+ logger.exception("Failed to create Connection")
+ raise
+
+ @property
+ def manager(self):
+ return self._manager
+
+ @property
+ def gadu_client(self):
+ return self.profile
+
+ def handle(self, handle_type, handle_id):
+ self.check_handle(handle_type, handle_id)
+ return self._handles[handle_type, handle_id]
+
+ def get_contact_alias(self, handle_id):
+ return self._get_alias(handle_id)
+
+ def get_handle_id_by_name(self, handle_type, name):
+ """Returns a handle ID for the given type and name
+
+ Arguments:
+ handle_type -- Telepathy Handle_Type for all the handles
+ name -- username for the contact
+
+ Returns:
+ handle_id -- ID for the given username
+ """
+
+ handle_id = 0
+ for handle in self._handles.values():
+ if handle.get_name() == name:
+ handle_id = handle.get_id()
+ break
+
+ return handle_id
+
+ def Connect(self):
+ if self._status == telepathy.CONNECTION_STATUS_DISCONNECTED:
+ logger.info("Connecting")
+ self.StatusChanged(telepathy.CONNECTION_STATUS_CONNECTING,
+ telepathy.CONNECTION_STATUS_REASON_REQUESTED)
+ self.__disconnect_reason = telepathy.CONNECTION_STATUS_REASON_NONE_SPECIFIED
+ #reactor.connectTCP('91.197.13.83', 8074, self.factory)
+ #reactor.connectTCP(self._server[0], self._server[1], self.factory)
+ self.getServerAdress(self._account[0])
+
+ def Disconnect(self):
+ logger.info("Disconnecting")
+ self.StatusChanged(telepathy.CONNECTION_STATUS_DISCONNECTED,
+ telepathy.CONNECTION_STATUS_REASON_REQUESTED)
+ reactor.stop()
+
+ def RequestHandles(self, handle_type, names, sender):
+ logger.info("Method RequestHandles called, handle type: %s, names: %s" % (str(handle_type), str(names)))
+ self.check_connected()
+ self.check_handle_type(handle_type)
+
+ handles = []
+ for name in names:
+ if handle_type == telepathy.HANDLE_TYPE_CONTACT:
+ contact_name = name
+
+ handle_id = self.get_handle_id_by_name(telepathy.constants.HANDLE_TYPE_CONTACT, str(contact_name))
+
+ if handle_id != 0:
+ handle = self.handle(telepathy.constants.HANDLE_TYPE_CONTACT, handle_id)
+ else:
+ handle = SunshineHandleFactory(self, 'contact',
+ str(contact_name), None)
+
+# try:
+# #just check is contact_name is integer
+# stripped = str(int(contact_name))
+# except:
+# if self.profile.isContactExist(contact_name) == False:
+# contact_pseudo_xmled = ET.fromstring("""<Contact><Guid>%s</Guid><GGNumber>%s</GGNumber><ShowName>%s</ShowName></Contact>""" % (str(contact_name), str(contact_name), str(contact_name)))
+# c = GaduContact.from_xml(contact_pseudo_xmled)
+# #self.profile.addContact( c )
+# self.profile.addNewContact( c )
+# self.profile.notifyAboutContact( c )
+ #if len(name) > 1:
+ # network_id = int(name[1])
+ #else:
+ # network_id = papyon.NetworkID.MSN
+ #contacts = self.msn_client.address_book.contacts.\
+ # search_by_account(contact_name).\
+ # search_by_network_id(network_id)
+ #
+ #if len(contacts) > 0:
+ # contact = contacts[0]
+ # handle = GaduHandleFactory(self, 'contact',
+ # contact.account, contact.network_id)
+ #else:
+ # handle = GaduHandleFactory(self, 'contact',
+ # contact_name, network_id)
+ #print contact_name
+ #contact = self.profile.get_contact(int(contact_name))
+ #print str(contact)
+
+
+
+
+
+# print "contact name: %s" % (contact_name)
+# handle = GaduHandleFactory(self, 'contact',
+# str(contact_name), None)
+#
+#
+
+ elif handle_type == telepathy.HANDLE_TYPE_LIST:
+ handle = SunshineHandleFactory(self, 'list', name)
+ elif handle_type == telepathy.HANDLE_TYPE_GROUP:
+ handle = SunshineHandleFactory(self, 'group', name)
+ else:
+ raise telepathy.NotAvailable('Handle type unsupported %d' % handle_type)
+ handles.append(handle.id)
+ self.add_client_handle(handle, sender)
+ return handles
+
+ def _generate_props(self, channel_type, handle, suppress_handler, initiator_handle=None):
+ props = {
+ telepathy.CHANNEL_INTERFACE + '.ChannelType': channel_type,
+ telepathy.CHANNEL_INTERFACE + '.TargetHandle': 0 if handle is None else handle.get_id(),
+ telepathy.CHANNEL_INTERFACE + '.TargetHandleType': telepathy.HANDLE_TYPE_NONE if handle is None else handle.get_type(),
+ telepathy.CHANNEL_INTERFACE + '.Requested': suppress_handler
+ }
+
+ if initiator_handle is not None:
+ props[telepathy.CHANNEL_INTERFACE + '.InitiatorHandle'] = initiator_handle.id
+
+ return props
+
+ @dbus.service.method(telepathy.CONNECTION, in_signature='suub',
+ out_signature='o', async_callbacks=('_success', '_error'))
+ def RequestChannel(self, type, handle_type, handle_id, suppress_handler,
+ _success, _error):
+ self.check_connected()
+ channel_manager = self._channel_manager
+
+ if handle_id == 0:
+ handle = None
+ else:
+ handle = self.handle(handle_type, handle_id)
+ props = self._generate_props(type, handle, suppress_handler)
+ self._validate_handle(props)
+
+ channel = channel_manager.channel_for_props(props, signal=False)
+
+ _success(channel._object_path)
+ self.signal_new_channels([channel])
+
+
+ def get_handle_id_by_name(self, handle_type, name):
+ """Returns a handle ID for the given type and name
+
+ Arguments:
+ handle_type -- Telepathy Handle_Type for all the handles
+ name -- username for the contact
+
+ Returns:
+ handle_id -- ID for the given username
+ """
+ handle_id = 0
+ for handle in self._handles.values():
+ if handle.get_name() == name:
+ handle_id = handle.get_id()
+ break
+
+ return handle_id
+
+ def updateContactsFile(self):
+ """Method that updates contact file when it changes and in loop every 5 seconds."""
+ self.configfile.make_contacts_file(self.profile.groups, self.profile.contacts)
+ reactor.callLater(5, self.updateContactsFile)
+
+ @async
+ def makeTelepathyContactsChannel(self):
+ logger.debug("Method makeTelepathyContactsChannel called.")
+ handle = SunshineHandleFactory(self, 'list', 'subscribe')
+ props = self._generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST,
+ handle, False)
+ self._channel_manager.channel_for_props(props, signal=True)
+
+# handle = ButterflyHandleFactory(self, 'list', 'publish')
+# props = self._generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST,
+# handle, False)
+# self._channel_manager.channel_for_props(props, signal=True)
+
+ @async
+ def makeTelepathyGroupChannels(self):
+ logger.debug("Method makeTelepathyGroupChannels called.")
+ for group in self.profile.groups:
+ handle = SunshineHandleFactory(self, 'group',
+ group.Name)
+ props = self._generate_props(
+ telepathy.CHANNEL_TYPE_CONTACT_LIST, handle, False)
+ self._channel_manager.channel_for_props(props, signal=True)
+
+ def getServerAdress(self, uin):
+ logger.info("Fetching GG server adress.")
+ url = 'http://appmsg.gadu-gadu.pl/appsvc/appmsg_ver8.asp?fmnumber=%s&lastmsg=0&version=8.0.0.9103' % (str(uin))
+ d = getPage(url, timeout=10)
+ d.addCallback(self.on_server_adress_fetched, uin)
+ d.addErrback(self.on_server_adress_fetched_failed, uin)
+
+ def on_server_adress_fetched(self, result, uin):
+ try:
+ result = result.replace('\n', '')
+ a = result.split(' ')
+ if a[0] == '0' and a[-1:][0] != 'notoperating':
+ logger.info("GG server adress fetched, IP: %s" % (a[-1:][0]))
+ reactor.connectTCP(a[-1:][0], 8074, self.factory)
+ else:
+ raise Exception()
+ except:
+ logger.debug("Cannot get GG server IP adress. Trying again...")
+ self.getServerAdress(uin)
+
+ def on_server_adress_fetched_failed(self, error, uin):
+ logger.info("Failed to get page with server IP adress.")
+ self.getServerAdress(uin)
+
+ @async
+ def on_contactsImported(self):
+ logger.info("No contacts in the XML contacts file yet. Contacts imported.")
+
+ self.configfile.make_contacts_file(self.profile.groups, self.profile.contacts)
+ reactor.callLater(5, self.updateContactsFile)
+
+ self.makeTelepathyContactsChannel()
+ self.makeTelepathyGroupChannels()
+
+ self._status = telepathy.CONNECTION_STATUS_CONNECTED
+ self.StatusChanged(telepathy.CONNECTION_STATUS_CONNECTED,
+ telepathy.CONNECTION_STATUS_REASON_REQUESTED)
+
+ @async
+ def on_loginSuccess(self):
+ logger.info("Connected")
+
+ #if its a first run or we dont have any contacts in contacts file yet then try to import contacts from server
+ if self.configfile.get_contacts_count() == 0:
+ self.profile.importContacts(self.on_contactsImported)
+ else:
+ self.configfile.make_contacts_file(self.profile.groups, self.profile.contacts)
+ reactor.callLater(5, self.updateContactsFile)
+
+ self.makeTelepathyContactsChannel()
+ self.makeTelepathyGroupChannels()
+
+ self._status = telepathy.CONNECTION_STATUS_CONNECTED
+ self.StatusChanged(telepathy.CONNECTION_STATUS_CONNECTED,
+ telepathy.CONNECTION_STATUS_REASON_REQUESTED)
+
+ def on_StatusNoticiesRecv(self):
+ logger.info("Status noticies received.")
+
+ @async
+ def on_loginFailed(self):
+ logger.info("Method on_loginFailed called.")
+ self._status = telepathy.CONNECTION_STATUS_DISCONNECTED
+ self.StatusChanged(telepathy.CONNECTION_STATUS_DISCONNECTED,
+ telepathy.CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED)
+ reactor.stop()
+
+ @async
+ def on_updateContact(self, contact):
+ handle_id = self.get_handle_id_by_name(telepathy.constants.HANDLE_TYPE_CONTACT, str(contact.uin))
+ handle = self.handle(telepathy.constants.HANDLE_TYPE_CONTACT, handle_id)
+ logger.info("Method on_updateContact called, status changed for UIN: %s, id: %s, status: %s, description: %s" % (contact.uin, handle.id, contact.status, contact.get_desc()))
+ self._presence_changed(handle, contact.status, contact.get_desc())
+
+ @async
+ def on_messageReceived(self, msg):
+ handle_id = self.get_handle_id_by_name(telepathy.constants.HANDLE_TYPE_CONTACT,
+ str(msg.sender))
+ if handle_id != 0:
+ handle = self.handle(telepathy.constants.HANDLE_TYPE_CONTACT, handle_id)
+ else:
+ handle = SunshineHandleFactory(self, 'contact',
+ str(msg.sender), None)
+
+ timestamp = int(time.time())
+ type = telepathy.CHANNEL_TEXT_MESSAGE_TYPE_NORMAL
+ logger.info("User %s sent a message" % handle.name)
+
+ logger.info("Msg from %r %d %d [%r] [%r]" % (msg.sender, msg.content.offset_plain, msg.content.offset_attrs, msg.content.plain_message, msg.content.html_message))
+
+ props = self._generate_props(telepathy.CHANNEL_TYPE_TEXT,
+ handle, False)
+ channel = self._channel_manager.channel_for_props(props,
+ signal=True, conversation=None)
+ message = "%s" % unicode(str(msg.content.plain_message).replace('\x00', '').decode('windows-1250').encode('utf-8'))
+ #print 'message: ', message
+ channel.Received(self._recv_id, timestamp, handle, type, 0, message)
+ self._recv_id += 1
+
+
+ # papyon.event.ClientEventInterface
+ def on_client_state_changed(self, state):
+ if state == papyon.event.ClientState.CONNECTING:
+ self.StatusChanged(telepathy.CONNECTION_STATUS_CONNECTING,
+ telepathy.CONNECTION_STATUS_REASON_REQUESTED)
+ elif state == papyon.event.ClientState.SYNCHRONIZED:
+ handle = ButterflyHandleFactory(self, 'list', 'subscribe')
+# props = self._generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST,
+# handle, False)
+# self._channel_manager.channel_for_props(props, signal=True)
+#
+# handle = ButterflyHandleFactory(self, 'list', 'publish')
+# props = self._generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST,
+# handle, False)
+# self._channel_manager.channel_for_props(props, signal=True)
+
+ #handle = ButterflyHandleFactory(self, 'list', 'hide')
+ #props = self._generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST,
+ # handle, False)
+ #self._channel_manager.channel_for_props(props, signal=True)
+
+ #handle = ButterflyHandleFactory(self, 'list', 'allow')
+ #props = self._generate_propstelepathy.CHANNEL_TYPE_CONTACT_LIST,
+ # handle, False)
+ #self._channel_manager.channel_for_props(props, signal=True)
+
+ #handle = ButterflyHandleFactory(self, 'list', 'deny')
+ #props = self._generate_props(telepathy.CHANNEL_TYPE_CONTACT_LIST,
+ # handle, False)
+ #self._channel_manager.channel_for_props(props, signal=True)
+
+ for group in self.msn_client.address_book.groups:
+ handle = ButterflyHandleFactory(self, 'group',
+ group.name.decode("utf-8"))
+ props = self._generate_props(
+ telepathy.CHANNEL_TYPE_CONTACT_LIST, handle, False)
+ self._channel_manager.channel_for_props(props, signal=True)
+ elif state == papyon.event.ClientState.OPEN:
+ self.StatusChanged(telepathy.CONNECTION_STATUS_CONNECTED,
+ telepathy.CONNECTION_STATUS_REASON_REQUESTED)
+ presence = self._initial_presence
+ message = self._initial_personal_message
+ if presence is not None:
+ self._client.profile.presence = presence
+ if message is not None:
+ self._client.profile.personal_message = message
+ self._client.profile.end_point_name = "PAPYON"
+
+ if (presence is not None) or (message is not None):
+ self._presence_changed(ButterflyHandleFactory(self, 'self'),
+ self._client.profile.presence,
+ self._client.profile.personal_message)
+ elif state == papyon.event.ClientState.CLOSED:
+ self.StatusChanged(telepathy.CONNECTION_STATUS_DISCONNECTED,
+ self.__disconnect_reason)
+ #FIXME
+ self._channel_manager.close()
+ self._advertise_disconnected()
+
+ # papyon.event.ClientEventInterface
+ def on_client_error(self, type, error):
+ if type == papyon.event.ClientErrorType.NETWORK:
+ self.__disconnect_reason = telepathy.CONNECTION_STATUS_REASON_NETWORK_ERROR
+ elif type == papyon.event.ClientErrorType.AUTHENTICATION:
+ self.__disconnect_reason = telepathy.CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED
+ elif type == papyon.event.ClientErrorType.PROTOCOL and \
+ error == papyon.event.ProtocolError.OTHER_CLIENT:
+ self.__disconnect_reason = telepathy.CONNECTION_STATUS_REASON_NAME_IN_USE
+ else:
+ self.__disconnect_reason = telepathy.CONNECTION_STATUS_REASON_NONE_SPECIFIED
+
+ # papyon.event.InviteEventInterface
+ def on_invite_conversation(self, conversation):
+ logger.debug("Conversation invite")
+ #FIXME: get rid of this crap and implement group support
+ participants = conversation.participants
+ for p in participants:
+ participant = p
+ break
+ handle = ButterflyHandleFactory(self, 'contact',
+ participant.account, participant.network_id)
+
+ props = self._generate_props(telepathy.CHANNEL_TYPE_TEXT,
+ handle, False, initiator_handle=handle)
+ channel = self._channel_manager.channel_for_props(props,
+ signal=True, conversation=conversation)
+
+ # papyon.event.InviteEventInterface
+ def on_invite_conference(self, call):
+ logger.debug("Call invite")
+ handle = ButterflyHandleFactory(self, 'contact', call.peer.account,
+ call.peer.network_id)
+
+ props = self._generate_props(telepathy.CHANNEL_TYPE_STREAMED_MEDIA,
+ handle, False, initiator_handle=handle)
+
+ channel = self._channel_manager.channel_for_props(props,
+ signal=True, call=call)
+
+ def _advertise_disconnected(self):
+ self._manager.disconnected(self)
diff --git a/sunshine/connection_manager.py b/sunshine/connection_manager.py
new file mode 100644
index 0000000..3a408bd
--- /dev/null
+++ b/sunshine/connection_manager.py
@@ -0,0 +1,87 @@
+# telepathy-sunshine is the GaduGadu connection manager for Telepathy
+#
+# Copyright (C) 2010 Krzysztof Klinikowski <kkszysiu@gmail.com>
+#
+# 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 telepathy
+import gobject
+import dbus
+import logging
+
+from sunshine.connection import SunshineConnection
+
+__all__ = ['SunshineConnectionManager']
+
+logger = logging.getLogger('Sunshine.ConnectionManager')
+
+
+class SunshineConnectionManager(telepathy.server.ConnectionManager):
+ """Sunshine connection manager
+
+ Implements the org.freedesktop.Telepathy.ConnectionManager interface"""
+
+ def __init__(self, shutdown_func=None):
+ "Initializer"
+ telepathy.server.ConnectionManager.__init__(self, 'sunshine')
+
+ self._protos['gadugadu'] = SunshineConnection
+ self._shutdown = shutdown_func
+ logger.info("Connection manager created")
+
+ def GetParameters(self, proto):
+ "Returns the mandatory and optional parameters for the given proto."
+ if proto not in self._protos:
+ raise telepathy.NotImplemented('unknown protocol %s' % proto)
+
+ result = []
+ connection_class = self._protos[proto]
+ mandatory_parameters = connection_class._mandatory_parameters
+ optional_parameters = connection_class._optional_parameters
+ default_parameters = connection_class._parameter_defaults
+
+ for parameter_name, parameter_type in mandatory_parameters.iteritems():
+ param = (parameter_name,
+ telepathy.CONN_MGR_PARAM_FLAG_REQUIRED,
+ parameter_type,
+ '')
+ result.append(param)
+
+ for parameter_name, parameter_type in optional_parameters.iteritems():
+ if parameter_name in default_parameters:
+ param = (parameter_name,
+ telepathy.CONN_MGR_PARAM_FLAG_HAS_DEFAULT,
+ parameter_name,
+ default_parameters[parameter_name])
+ else:
+ param = (parameter_name, 0, parameter_name, '')
+ result.append(param)
+
+ return result
+
+ def disconnected(self, conn):
+ def shutdown():
+ if self._shutdown is not None and \
+ len(self._connections) == 0:
+ self._shutdown()
+ return False
+ result = telepathy.server.ConnectionManager.disconnected(self, conn)
+ gobject.timeout_add(5000, shutdown)
+
+ def quit(self):
+ "Terminates all connections. Must be called upon quit"
+ for connection in self._connections:
+ connection.Disconnect()
+ logger.info("Connection manager quitting")
diff --git a/sunshine/contacts.py b/sunshine/contacts.py
new file mode 100644
index 0000000..6dd3d0a
--- /dev/null
+++ b/sunshine/contacts.py
@@ -0,0 +1,93 @@
+# telepathy-sunshine is the GaduGadu connection manager for Telepathy
+#
+# Copyright (C) 2009 Olivier Le Thanh Duong <olivier@lethanh.be>
+# Copyright (C) 2010 Krzysztof Klinikowski <kkszysiu@gmail.com>
+#
+# 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 logging
+import time
+
+import telepathy
+import telepathy.errors
+import dbus
+
+__all__ = ['SunshineContacts']
+
+logger = logging.getLogger('Sunshine.Contacts')
+
+class SunshineContacts(telepathy.server.ConnectionInterfaceContacts):
+
+ attributes = {
+ telepathy.CONNECTION : 'contact-id',
+ telepathy.CONNECTION_INTERFACE_SIMPLE_PRESENCE : 'presence',
+ telepathy.CONNECTION_INTERFACE_ALIASING : 'alias',
+ telepathy.CONNECTION_INTERFACE_AVATARS : 'token',
+ telepathy.CONNECTION_INTERFACE_CAPABILITIES : 'caps'
+ }
+
+ def __init__(self):
+ telepathy.server.ConnectionInterfaceContacts.__init__(self)
+
+ dbus_interface = telepathy.CONNECTION_INTERFACE_CONTACTS
+
+ self._implement_property_get(dbus_interface, \
+ {'ContactAttributeInterfaces' : self.get_contact_attribute_interfaces})
+
+ # Overwrite the dbus attribute to get the sender argument
+ @dbus.service.method(telepathy.CONNECTION_INTERFACE_CONTACTS, in_signature='auasb',
+ out_signature='a{ua{sv}}', sender_keyword='sender')
+ def GetContactAttributes(self, handles, interfaces, hold, sender):
+ #InspectHandle already checks we're connected, the handles and handle type.
+ for interface in interfaces:
+ if interface not in self.attributes:
+ raise telepathy.errors.InvalidArgument(
+ 'Interface %s is not supported by GetContactAttributes' % (interface))
+
+ handle_type = telepathy.HANDLE_TYPE_CONTACT
+ ret = {}
+ for handle in handles:
+ ret[handle] = {}
+
+ functions = {
+ telepathy.CONNECTION :
+ lambda x: zip(x, self.InspectHandles(handle_type, x)),
+ telepathy.CONNECTION_INTERFACE_SIMPLE_PRESENCE :
+ lambda x: self.GetPresences(x).items(),
+ telepathy.CONNECTION_INTERFACE_ALIASING :
+ lambda x: self.GetAliases(x).items(),
+ telepathy.CONNECTION_INTERFACE_AVATARS :
+ lambda x: self.GetKnownAvatarTokens(x).items(),
+ telepathy.CONNECTION_INTERFACE_CAPABILITIES :
+ lambda x: self.GetCapabilities(x).items()
+ }
+
+ #Hold handles if needed
+ if hold:
+ self.HoldHandles(handle_type, handles, sender)
+
+ # Attributes from the interface org.freedesktop.Telepathy.Connection
+ # are always returned, and need not be requested explicitly.
+ interfaces = set(interfaces + [telepathy.CONNECTION])
+ for interface in interfaces:
+ interface_attribute = interface + '/' + self.attributes[interface]
+ results = functions[interface](handles)
+ for handle, value in results:
+ ret[int(handle)][interface_attribute] = value
+ return ret
+
+ def get_contact_attribute_interfaces(self):
+ return self.attributes.keys()
diff --git a/sunshine/debug.py b/sunshine/debug.py
new file mode 100644
index 0000000..57cc9f9
--- /dev/null
+++ b/sunshine/debug.py
@@ -0,0 +1,37 @@
+# telepathy-sunshine is the GaduGadu connection manager for Telepathy
+#
+# Copyright (C) 2010 Krzysztof Klinikowski <kkszysiu@gmail.com>
+#
+# 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 telepathy
+
+class SunshineDebug(telepathy.server.Debug):
+ """Sunshine debug interface
+
+ Implements the org.freedesktop.Telepathy.Debug interface"""
+
+ def __init__(self, conn_manager):
+ telepathy.server.Debug.__init__(self, conn_manager)
+
+ def get_record_name(self, record):
+ name = record.name
+ if name.startswith("Sunshine."):
+ domain, category = name.split('.', 1)
+ else:
+ domain = "sunshine"
+ category = name
+ name = domain.lower() + "/" + category.lower()
+ return name
diff --git a/sunshine/handle.py b/sunshine/handle.py
new file mode 100644
index 0000000..accbfe4
--- /dev/null
+++ b/sunshine/handle.py
@@ -0,0 +1,132 @@
+# telepathy-sunshine is the GaduGadu connection manager for Telepathy
+#
+# Copyright (C) 2007 Ali Sabil <ali.sabil@gmail.com>
+# Copyright (C) 2010 Krzysztof Klinikowski <kkszysiu@gmail.com>
+#
+# 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 logging
+import weakref
+
+import telepathy
+
+__all__ = ['SunshineHandleFactory']
+
+logger = logging.getLogger('Sunshine.Handle')
+
+
+def SunshineHandleFactory(connection, type, *args):
+ mapping = {'self': SunshineSelfHandle,
+ 'contact': SunshineContactHandle,
+ 'list': SunshineListHandle,
+ 'group': SunshineGroupHandle}
+ handle = mapping[type](connection, *args)
+ connection._handles[handle.get_type(), handle.get_id()] = handle
+ return handle
+
+
+class SunshineHandleMeta(type):
+ def __call__(cls, connection, *args):
+ obj, newly_created = cls.__new__(cls, connection, *args)
+ if newly_created:
+ obj.__init__(connection, connection.get_handle_id(), *args)
+ logger.info("New Handle %s" % unicode(obj))
+ return obj
+
+
+class SunshineHandle(telepathy.server.Handle):
+ __metaclass__ = SunshineHandleMeta
+
+ instances = weakref.WeakValueDictionary()
+ def __new__(cls, connection, *args):
+ key = (cls, connection._account[0], args)
+ if key not in cls.instances.keys():
+ instance = object.__new__(cls, connection, *args)
+ cls.instances[key] = instance # TRICKY: instances is a weakdict
+ return instance, True
+ return cls.instances[key], False
+
+ def __init__(self, connection, id, handle_type, name):
+ telepathy.server.Handle.__init__(self, id, handle_type, name)
+ self._conn = weakref.proxy(connection)
+
+ def __unicode__(self):
+ type_mapping = {telepathy.HANDLE_TYPE_CONTACT : 'Contact',
+ telepathy.HANDLE_TYPE_ROOM : 'Room',
+ telepathy.HANDLE_TYPE_LIST : 'List',
+ telepathy.HANDLE_TYPE_GROUP : 'Group'}
+ type_str = type_mapping.get(self.type, '')
+ return "<Sunshine%sHandle id=%u name='%s'>" % \
+ (type_str, self.id, self.name)
+
+ id = property(telepathy.server.Handle.get_id)
+ type = property(telepathy.server.Handle.get_type)
+ name = property(telepathy.server.Handle.get_name)
+
+
+class SunshineSelfHandle(SunshineHandle):
+ instance = None
+
+ def __init__(self, connection, id):
+ handle_type = telepathy.HANDLE_TYPE_CONTACT
+ handle_name = connection._account[0]
+ self._connection = connection
+ SunshineHandle.__init__(self, connection, id, handle_type, handle_name)
+
+ @property
+ def profile(self):
+ #TODO: thats not required, it should be removed someday :)
+ return False
+
+
+class SunshineContactHandle(SunshineHandle):
+ #TODO: GG using just UIN to indenrify user so we need just contact_uin instead of contact_account and contact_network)
+ def __init__(self, connection, id, contact_account, contact_network):
+ handle_type = telepathy.HANDLE_TYPE_CONTACT
+ handle_name = str(contact_account)
+ self.account = str(contact_account)
+ self.network = contact_network
+ self.pending_groups = set()
+ self.pending_alias = None
+ self._connection = connection
+ SunshineHandle.__init__(self, connection, id, handle_type, handle_name)
+
+ @property
+ def contact(self):
+ result = self._connection.gadu_client.get_contact(int(self.account))
+ return result
+
+
+class SunshineListHandle(SunshineHandle):
+ def __init__(self, connection, id, list_name):
+ handle_type = telepathy.HANDLE_TYPE_LIST
+ handle_name = list_name
+ SunshineHandle.__init__(self, connection, id, handle_type, handle_name)
+
+
+class SunshineGroupHandle(SunshineHandle):
+ def __init__(self, connection, id, group_name):
+ handle_type = telepathy.HANDLE_TYPE_GROUP
+ handle_name = group_name
+ self._connection = connection
+ self.handle_name = group_name
+ SunshineHandle.__init__(self, connection, id, handle_type, handle_name)
+
+ @property
+ def group(self):
+ for group in self._connection.gadu_client.groups:
+ if group.Name == self.handle_name:
+ return group
+ return None
diff --git a/sunshine/lqsoft/Makefile.am b/sunshine/lqsoft/Makefile.am
new file mode 100644
index 0000000..021cc4a
--- /dev/null
+++ b/sunshine/lqsoft/Makefile.am
@@ -0,0 +1,4 @@
+SUBDIRS = cstruct pygadu utils
+
+lqsoftdir = $(pythondir)/sunshine/lqsoft
+lqsoft_PYTHON = __init__.py
diff --git a/sunshine/lqsoft/__init__.py b/sunshine/lqsoft/__init__.py
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/sunshine/lqsoft/__init__.py
diff --git a/sunshine/lqsoft/cstruct/Makefile.am b/sunshine/lqsoft/cstruct/Makefile.am
new file mode 100644
index 0000000..056ff96
--- /dev/null
+++ b/sunshine/lqsoft/cstruct/Makefile.am
@@ -0,0 +1,6 @@
+SUBDIRS = fields test
+
+cstructdir = $(pythondir)/sunshine/lqsoft/cstruct
+cstruct_PYTHON = common.py \
+ constraints.py \
+ __init__.py
diff --git a/sunshine/lqsoft/cstruct/__init__.py b/sunshine/lqsoft/cstruct/__init__.py
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/sunshine/lqsoft/cstruct/__init__.py
diff --git a/sunshine/lqsoft/cstruct/common.py b/sunshine/lqsoft/cstruct/common.py
new file mode 100755
index 0000000..b4824e9
--- /dev/null
+++ b/sunshine/lqsoft/cstruct/common.py
@@ -0,0 +1,322 @@
+#!/usr/bin/env python
+# -*- coding: utf-8
+
+__author__="lreqc"
+__date__ ="$2009-07-19 07:48:34$"
+
+import sunshine.lqsoft.cstruct.constraints as const
+import struct,sys
+
+def log(msg):
+ sys.stderr.write(msg+'\n')
+
+
+class ICField(object):
+
+ def add_constraint(self, constr):
+ pass
+
+ def before_pack(self, obj, offset, **opts):
+ pass
+
+ def pack(self, obj, offset, **opts):
+ pass
+
+ def unpack(self, obj, data, pos):
+ pass
+
+ def get_value(self, obj, current_value):
+ pass
+
+ def set_value(self, obj, new_value):
+ pass
+
+
+class CField(ICField):
+
+ KEYWORDS = {
+ 'offset': const.OffsetConstraint,
+ 'prefix': const.PrefixConstraint,
+ }
+
+ def __init__(self, idx, default=None, **kwargs):
+ self.idx = idx
+ self.default = default
+ self.constraints = []
+ self.ommit = []
+
+ for (key,value) in kwargs.iteritems():
+ #if key == 'nullable':
+ # continue
+
+ if key.endswith('__ommit'):
+ key = key[:-7]
+ self.ommit.append(key)
+
+ constr = self.KEYWORDS[key](value)
+ constr.keyword = key
+ self.add_constraint(constr)
+
+ # if there is an ommit field, the field is nullable
+ # if there is no ommit field, the field can't be null
+ self.nullable = bool(self.ommit)
+
+ def add_constraint(self, constr):
+ # add constraint
+ # lame version - should be a balanced tree
+ index = 0
+ length = len(self.constraints)
+ while index < length \
+ and self.constraints[index].priority <= constr.priority:
+ index += 1
+ self.constraints.insert(index, constr)
+
+ def _before_unpack(self, opts):
+ """Prepare the data for unpacking."""
+ for c in self.constraints:
+ if not c.before_unpack(opts):
+ if c.keyword in self.ommit:
+ opts['__ommit'] = True
+ break
+ raise UnpackException('Data buffer failed to satisfy constraint: ' + str(c), c)
+
+
+ def before_pack(self, obj, offset, **opts):
+ """Pack dry-run, so that the field can update depencies"""
+ value = getattr(obj, self.name)
+ opts.update({'field': self, 'obj': obj, 'value': value, 'offset': offset })
+
+ if (value == None) and self.nullable:
+ return 0 # field is ommited
+
+ for c in reversed(self.constraints):
+ c.before_pack(opts)
+
+ return struct.calcsize(self._format_string(opts))
+
+ def pack(self, obj, offset, **opts):
+ """Pack the field into a byte array"""
+ value = getattr(obj, self.name)
+
+ if (value == None) and self.nullable:
+ return '' # field is ommited
+
+ opts.update({'field': self, 'obj': obj, 'value': value, 'offset': offset})
+ for c in reversed(self.constraints):
+ c.pack(opts)
+
+ return struct.pack( self._format_string(opts), value)
+
+ def unpack(self, obj, data, pos):
+ """Unpack the given byte buffer into this field, starting at pos"""
+ # before we unpack we need to check things like:
+ # * is the field at given offset ? (yes, this always comes first)
+ # * does the field prefix match ?
+ # * any other stuff the user wants to check
+ opts = {'obj': obj, 'data': data, 'offset': pos}
+ self._before_unpack(opts)
+
+ if not opts.get('__ommit', False):
+ return self._retrieve_value(opts)
+ else:
+ return (None, pos)
+
+ def _format_string(self, opts):
+ """The format string for the default retrieval method"""
+ return ''
+
+ def _retrieve_value(self, opts):
+ #print opts
+ fmt = self._format_string(opts)
+ fmt_len = struct.calcsize(fmt)
+ v = struct.unpack_from(fmt, opts['data'], opts['offset'])
+ return (v, opts['offset'] + fmt_len)
+
+ def get_value(self, obj, current_value):
+ return current_value
+
+ def set_value(self, obj, new_value):
+ # enable niling the field
+ if (new_value == None) and self.nullable:
+ return None
+
+ # the new value is not yet set on the object
+ opts = {'field': self, 'obj': obj, 'value': new_value}
+
+ # trigger constraints
+ for constr in self.constraints:
+ constr.on_value_set(opts)
+
+ return opts['value']
+
+ def __str__(self):
+ return str(self.name)
+
+class MetaStruct(type):
+ def __new__(cls, name, bases, cdict):
+ fields = []
+ #internal_dict = {}
+ ndict = {}
+ # log('Constructing class: ' + name)
+
+ for (field_name, field_value) in cdict.iteritems():
+ if isinstance(field_value, CField):
+ field_value.name = field_name
+ fields.append(field_value)
+ #internal_dict[field_name] = field_value
+ ndict[field_name] = property( \
+ MetaStruct.getter_for(field_value), \
+ MetaStruct.setter_for(field_value) )
+ else:
+ ndict[field_name] = field_value
+
+ klass = type.__new__(cls, name, bases, ndict)
+
+ #old_dict = getattr(klass, '_internal', {})
+ #internal_dict.update(old_dict)
+ #setattr(klass, '_internal', internal_dict)
+
+ fields.sort(key= lambda item: item.idx)
+
+ order = getattr(klass, '_field_order', [])
+ order = order + fields
+ setattr(klass, '_field_order', order)
+ return klass
+
+
+ @staticmethod
+ def getter_for(field):
+ def getter(self):
+ return field.get_value(self, getattr(self, '_' + field.name))
+ return getter
+
+ @staticmethod
+ def setter_for(field):
+ def setter(self, value):
+ # log("Called setter on %r with %r" % (self, name))
+ return setattr(self, '_' + field.name, field.set_value(self, value))
+ return setter
+
+class CStruct(object):
+ __metaclass__ = MetaStruct
+
+ def __init__(self, **kwargs):
+ for field in self._field_order:
+ setattr(self, field.name, kwargs.get(field.name,field.default))
+
+ def _before_pack(self, offset=0):
+ for field in self._field_order:
+ offset += field.before_pack(self, offset)
+ return offset
+
+ def _pack(self, off=0):
+ s = ''
+ for field in self._field_order:
+ data = field.pack(self, off)
+ off += len(data)
+ s += data
+ return s
+
+ def pack(self, offset=0):
+ self._before_pack(offset)
+ return self._pack(offset)
+
+ @classmethod
+ def unpack(cls, data, offset=0):
+ print "Unpacking class %s: offset=%d, total_buf=%d" % (cls.__name__, offset, len(data))
+ dict = {}
+ dp = ItemWrapper(dict)
+
+ for field in cls._field_order:
+ print "Unpacking field @%d: %s" % (offset, field.name)
+ value, next_offset = field.unpack(dp, data, offset)
+ dict[field.name] = value
+ offset = next_offset
+ print "Unpacked: " + repr(value)
+
+ instance = cls(**dict)
+ print "Unpacked: " + str(instance)
+ return instance, offset
+
+ def __field_value(self, field, default=None):
+ return field.get_value(self, getattr(self, '_' + field.name, default))
+
+ def __str__(self):
+ buf = "CStruct("
+ buf += ','.join( "%s = %r" % (field.name, self.__field_value(field)) \
+ for field in self._field_order )
+ buf += ")"
+ return buf
+
+class ItemWrapper(object):
+ """Wraps the given object (usually a dict or a list) with
+ accessor methods that turn attribute calls to index calls.
+ Also provides a handy way, to attach set/get triggers."""
+
+ def __init__(self, obj):
+ self._object = obj
+
+ def __getattribute__(self, name):
+ # play nice with names startign with '_'
+ if name.startswith('_'):
+ try:
+ return object.__getattribute__(self, name)
+ except AttributeError:
+ return object.__getattribute__(self._object, name[1:])
+
+ return self._get_action(self, name, self._object[name])
+
+ def __setattr__(self, name, value):
+ # play nice :)
+ if name.startswith('_'):
+ return object.__setattr__(self, name, value)
+
+ self._object[name] = self._set_action(self, name, value)
+
+ def __getitem__(self, name):
+ return self._get_action(self, name, self._object[name])
+
+ def __setitem__(self, name, value):
+ self._object[name] = self._set_action(self, name, value)
+
+ def __len__(self):
+ return self._object.__len__()
+
+ def _set_action(self, me, name, value):
+ return value
+
+ def _get_action(self, me, name, value):
+ return value
+
+ def __str__(self):
+ return str(self._object)
+
+ def __eq__(self, other):
+ return self._object.__eq__(other)
+
+
+class ListItemWrapper(ItemWrapper):
+
+ def __getattribute__(self, name):
+ try:
+ key = int(name)
+ return self._get_action(self, name, self._object[key])
+ except ValueError:
+ return ItemWrapper.__getattribute__(self, name)
+
+ def __setattr__(self, name, value):
+ try:
+ key = int(name)
+ self._object[key] = self._set_action(self, name, value)
+ except ValueError:
+ return ItemWrapper.__setattr__(self, name, value)
+
+class UnpackException(Exception):
+ def __init__(self, msg, constraint):
+ Exception.__init__(self, msg)
+ self.constraint = constraint
+
+class PackingException(Exception):
+ def __init__(self, msg, constraint):
+ Exception.__init__(self, msg)
+ self.constraint = constraint
diff --git a/sunshine/lqsoft/cstruct/constraints.py b/sunshine/lqsoft/cstruct/constraints.py
new file mode 100644
index 0000000..0cd056a
--- /dev/null
+++ b/sunshine/lqsoft/cstruct/constraints.py
@@ -0,0 +1,188 @@
+# -*- coding: utf-8
+
+import types
+
+# default priorities
+PRIO_OFFSET = 100
+PRIO_PREFIX = 500
+PRIO_TYPE = 600
+PRIO_LENGTH = 700
+PRIO_MAXLENGTH = 750
+PRIO_NBOUNDS = 800
+
+class IConstraint(object):
+ def __init__(self, priority):
+ self.priority = priority
+
+ def __str__(self):
+ return self.__class__.__name__
+
+ def before_unpack(self, opts):
+ return True
+
+ def pack(self, opts):
+ return True
+
+ def before_pack(self, opts):
+ return True
+
+ def on_value_set(self, opts):
+ pass
+
+class PrefixConstraint(IConstraint):
+ def __init__(self, param, priority=PRIO_PREFIX):
+ IConstraint.__init__(self, priority)
+
+ if not isinstance(param, str):
+ raise ValueError("Prefix constraints takes a byte array as an argument")
+ self.prefix = param
+
+ def match(self, data, pos):
+ l = len(data)
+ for char in self.prefix:
+ if pos >= l: # prefix exceeds the data
+ return False
+
+ if data[pos] != char: # characters don't match
+ return False
+ pos += 1
+ return True
+
+ def before_unpack(self, opts):
+ return self.match(opts['data'], opts['offset'])
+
+class OffsetConstraint(IConstraint):
+
+ def __init__(self, param, priority=PRIO_OFFSET):
+ IConstraint.__init__(self, priority)
+
+ if isinstance(param, str):
+ self.before_upack = self.before_upack_field
+ elif isinstance(param, int):
+ self.before_upack = self.before_upack_number
+ else:
+ raise ValueError("Offset constraint must contain a number or a valid field name.")
+
+ self.__offset = param
+
+ def before_upack_number(self, options):
+ if self.__offset != options['offset']:
+ return False
+ return True
+
+ def before_upack_field(self, options):
+ off_field = getattr(options['obj'], self.__offset)
+ if not isinstance(off_field, NumericField):
+ raise ValueError("Field offset can only be attached \
+ to a numeric field.")
+ if getattr(options['obj'], self.__offset) != options['offset']:
+ return False
+ return True
+
+ def before_pack(self, options):
+ if isinstance(self.__offset, str):
+ setattr(options['obj'], self.__offset, options['offset'])
+
+ def pack(self, options):
+ if isinstance(self.__offset, int) and (options['offset'] != self.__offset):
+ raise PackingException("Explicit offset of field %s was set, but position doesn't match" % \
+ options['field'].name )
+
+class ValueTypeConstraint(IConstraint):
+
+ def __init__(self, typeklass, priority=PRIO_TYPE):
+ IConstraint.__init__(self, priority)
+
+ if not isinstance(typeklass, type):
+ raise ValueError("This constraint must contain a type class.")
+ self._klass = typeklass
+
+ def on_value_set(self, opts):
+ if not isinstance(opts['value'], self._klass):
+ raise ValueError("Field %s accepts only instances of %s as value."\
+ % (opts['field'].name, self._klass.__name__) )
+
+class NumericBounds(IConstraint):
+ BOUND_FOR_CTYPE = {
+ 'int': (-(2**31)+1 , 2**31),
+ 'uint': (0 , 2**32-1),
+ 'short': (-(2**15)+1 , 2**15),
+ 'ushort': (0 , 2**16-1),
+ 'byte': (-127, 128),
+ 'ubyte': (0, 255),
+ }
+
+ def __init__(self, lower_bound = None, upper_bound = None, ctype=None, \
+ priority=PRIO_NBOUNDS):
+ IConstraint.__init__(self, priority)
+
+ if ctype != None:
+ self._lbound, self._ubound = self.BOUND_FOR_CTYPE[ctype]
+ elif lower_bound == None or upper_bound == None:
+ raise ValueError("You need to specify bounds or a ctype.")
+ else:
+ self._lbound = lower_bound
+ self._ubound = upper_bound
+
+ def on_value_set(self, opts):
+ if not (self._lbound <= opts['value'] <= self._ubound):
+ raise ValueError("Field %s - value %s out of bounds."\
+ % (opts['field'].name, opts['value']) )
+
+class LengthConstraint(IConstraint):
+ def __init__(self, length, padding_func, priority=PRIO_LENGTH, opt_name='length'):
+ IConstraint.__init__(self, priority)
+
+ if isinstance(length, property):
+ self.before_unpack = self.before_unpack_prop
+ elif isinstance(length, str):
+ self.before_unpack = self.before_unpack_field
+ elif isinstance(length, int):
+ self.before_unpack = self.before_unpack_number
+ else:
+ raise ValueError("Length constraint must contain a number or a field name.")
+
+ self._opt_name = opt_name
+ self.__length = length
+ self.__padding_func = padding_func
+
+ def on_value_set(self, opts):
+ L = len(opts['value'])
+ if isinstance(self.__length, property):
+ return self.__length.__set__(opts, L)
+ if isinstance(self.__length, str):
+ return setattr(opts['obj'], self.__length, L)
+ if self.__length < 0:
+ return # do nothing
+
+ if L > self.__length:
+ raise ValueError("Field %s has limited length of %d." % (opts['field'].name, self.__length) )
+
+ if self.__padding_func:
+ opts['padding'] = (self.__length - L)
+ self.__padding_func(opts)
+
+ def before_pack(self, opts):
+ # the value is about to be packed
+ # nothing to do here, 'cause we ensure proper length in the trigger
+ opts[self._opt_name] = len(opts['value'])
+
+ def pack(self, opts):
+ # value is being packed - add our property
+ opts[self._opt_name] = len(opts['value'])
+
+ def before_unpack_prop(self, opts):
+ opts[self._opt_name] = self.__length.__get__(opts)
+ return True
+
+ def before_unpack_number(self, opts):
+ opts[self._opt_name] = self.__length
+ return True
+
+ def before_unpack_field(self, opts):
+ opts[self._opt_name] = getattr(opts['obj'], self.__length)
+ return True
+
+class MaxLengthConstraint(LengthConstraint):
+ def __init__(self, length, priority=PRIO_MAXLENGTH):
+ LengthConstraint.__init__(self, length, None, priority, opt_name='max_length') \ No newline at end of file
diff --git a/sunshine/lqsoft/cstruct/fields/Makefile.am b/sunshine/lqsoft/cstruct/fields/Makefile.am
new file mode 100644
index 0000000..a622fd6
--- /dev/null
+++ b/sunshine/lqsoft/cstruct/fields/Makefile.am
@@ -0,0 +1,5 @@
+fieldsdir = $(pythondir)/sunshine/lqsoft/cstruct/fields
+fields_PYTHON = complex.py \
+ __init__.py \
+ numeric.py \
+ text.py
diff --git a/sunshine/lqsoft/cstruct/fields/__init__.py b/sunshine/lqsoft/cstruct/fields/__init__.py
new file mode 100755
index 0000000..357a126
--- /dev/null
+++ b/sunshine/lqsoft/cstruct/fields/__init__.py
@@ -0,0 +1,3 @@
+__author__="lreqc"
+__date__ ="$2009-07-20 15:30:15$"
+
diff --git a/sunshine/lqsoft/cstruct/fields/complex.py b/sunshine/lqsoft/cstruct/fields/complex.py
new file mode 100644
index 0000000..e610758
--- /dev/null
+++ b/sunshine/lqsoft/cstruct/fields/complex.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python
+# -*- coding: utf-8
+
+__author__ = "Łukasz Rekucki"
+__date__ = "$2009-07-19 07:46:52$"
+
+from sunshine.lqsoft.cstruct.common import ListItemWrapper, CField
+from sunshine.lqsoft.cstruct.constraints import *
+
+
+def array_padder(opts):
+ pad = opts['padding']
+ opts['value']._extend(None for _ in xrange(0, pad))
+ # setattr(opts['obj'], '_' + opts['field'].name, value)
+
+class ArrayField(CField):
+ KEYWORDS = dict(CField.KEYWORDS,
+ length= lambda lv: LengthConstraint(\
+ length=lv, padding_func=array_padder) )
+
+ def __init__(self, idx, subfield, default=[], length=0, **kwargs):
+ CField.__init__(self, idx, default, **dict(kwargs, length=length) )
+ self.__subfield = subfield
+
+ # packing
+ def before_pack(self, obj, offset, **opts):
+ value = getattr(obj, self.name)
+
+ if (value == None) and self.nullable:
+ return 0 # field is ommit
+
+ opts.update({'field': self, 'obj': obj, 'value': value})
+ for c in reversed(self.constraints):
+ c.before_pack(opts)
+
+ data_len = 0
+ off = offset
+ for i in xrange(0, opts['length']):
+ # map the field to index i
+ self.__subfield.name = str(i)
+ sf_len = self.__subfield.before_pack(value, off)
+ data_len += sf_len
+ off += sf_len
+
+ return data_len
+
+ def pack(self, obj, offset, **opts):
+ value = getattr(obj, self.name)
+
+ if (value == None) and self.nullable:
+ return '' # field is ommit
+
+ opts.update({'field': self, 'obj': obj, 'value': value})
+ for c in reversed(self.constraints):
+ c.pack(opts)
+
+ # all constraints to this field applied
+
+ buffer = ''
+ off = offset
+ for i in xrange(0, opts['length']):
+ # map the field to index i
+ self.__subfield.name = str(i)
+ data = self.__subfield.pack(value, off)
+ off += len(data)
+ buffer += data
+ return buffer
+
+
+ # unpacking
+ def _retrieve_value(self, opts):
+ l = []
+ data_len = len(opts['data'])
+ array_len = opts['length']
+ offset = opts['offset']
+
+ i = 0
+ while (array_len < 0 and offset < data_len) or (0 <= i < array_len):
+ self.__subfield.name = str(i)
+ v, offset = self.__subfield.unpack(opts['obj'], opts['data'], offset)
+ l.append(v)
+ i += 1
+
+ return (l, offset)
+
+ def item_set_value(self, wrapper, item_name, new_value):
+ # let the subfield se the value - this validates
+ # print self, wrapper, item_name, new_value
+ self.__subfield.name = item_name
+ return self.__subfield.set_value(wrapper, new_value)
+
+ def item_get_value(self, wrapper, item_name, current_value):
+ # let the subfield se the value - this validates
+ self.__subfield.name = item_name
+ return self.__subfield.get_value(wrapper, current_value)
+
+ # override set, to wrap the value
+ def set_value(self, obj, value):
+ wrapper = ListItemWrapper(value)
+ wrapper._set_action = self.item_set_value
+ wrapper._get_action = self.item_get_value
+ return CField.set_value(self, obj, wrapper)
+
+ # no need to wrap the get
+
+
+class StructField(CField):
+ """Field containing a sub-structure - useful for defining common field groups."""
+
+ KEYWORDS = dict(CField.KEYWORDS,
+ inner = lambda cvalue: const.ValueTypeConstraint(cvalue) )
+
+ def __init__(self, idx, struct, default=None, **kwargs):
+ CField.__init__(self, idx, default, **kwargs)
+ self._struct_klass = struct
+
+ def before_pack(self, obj, offset, **opts):
+ value = getattr(obj, self.name)
+ if (value == None) and self.nullable:
+ return 0 # field is ommited
+
+ opts.update({'field': self, 'obj': obj, 'value': value, 'offset': offset})
+ for c in reversed(self.constraints):
+ c.before_pack(opts)
+
+ return value._before_pack(offset)
+
+ def pack(self, obj, offset, **opts):
+ value = getattr(obj, self.name)
+ if (value == None) and self.nullable:
+ return '' # field is ommited
+
+ opts.update({'field': self, 'obj': obj, 'value': value, 'offset': offset})
+ for c in reversed(self.constraints):
+ c.pack(opts)
+
+ return value._pack(offset)
+
+ def _retrieve_value(self, opts):
+ return self._struct_klass.unpack(opts['data'], opts['offset'])
diff --git a/sunshine/lqsoft/cstruct/fields/numeric.py b/sunshine/lqsoft/cstruct/fields/numeric.py
new file mode 100755
index 0000000..2c55318
--- /dev/null
+++ b/sunshine/lqsoft/cstruct/fields/numeric.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+# -*- coding: utf-8
+
+__author__ = "Łukasz Rekucki"
+__date__ = "$2009-07-19 07:46:52$"
+
+import numbers
+
+from sunshine.lqsoft.cstruct.common import *
+import sunshine.lqsoft.cstruct.constraints as const
+
+class NumericField(CField):
+
+ KEYWORDS = dict(CField.KEYWORDS,
+ ctype = lambda ctype_value: const.NumericBounds(ctype=ctype_value) )
+
+ def __init__(self, idx, default=0, **kwargs):
+ CField.__init__(self, idx, default, **kwargs)
+ self.add_constraint( const.ValueTypeConstraint(numbers.Real) )
+ self.__ctype = kwargs.get('ctype', 'int')
+
+ FMT_STRING = {
+ 'int': 'i',
+ 'uint': 'I',
+ 'short': 'h',
+ 'ushort': 'H',
+ 'byte': 'b',
+ 'ubyte': 'B',
+ }
+
+ def _format_string(self, opts):
+ return '<' + NumericField.FMT_STRING[self.__ctype]
+
+ def _retrieve_value(self, opts):
+ (v, offset) = CField._retrieve_value(self, opts)
+ return (v[0], offset)
+
+# some usefull shorthands
+class IntField(NumericField):
+ def __init__(self, idx, default=0, **kwargs):
+ NumericField.__init__(self, idx, default, **dict(kwargs, ctype='int'))
+
+class UIntField(NumericField):
+ def __init__(self, idx, default=0, **kwargs):
+ NumericField.__init__(self, idx, default, **dict(kwargs, ctype='uint'))
+
+class ShortField(NumericField):
+ def __init__(self, idx, default=0, **kwargs):
+ NumericField.__init__(self, idx, default, **dict(kwargs, ctype='short'))
+
+class UShortField(NumericField):
+ def __init__(self, idx, default=0, **kwargs):
+ NumericField.__init__(self, idx, default, **dict(kwargs, ctype='ushort'))
+
+class ByteField(NumericField):
+ def __init__(self, idx, default=0, **kwargs):
+ NumericField.__init__(self, idx, default, **dict(kwargs, ctype='byte'))
+
+class UByteField(NumericField):
+ def __init__(self, idx, default=0, **kwargs):
+ NumericField.__init__(self, idx, default, **dict(kwargs, ctype='ubyte'))
+
+if __name__ == "__main__":
+ print "Hello";
diff --git a/sunshine/lqsoft/cstruct/fields/text.py b/sunshine/lqsoft/cstruct/fields/text.py
new file mode 100755
index 0000000..dbf2cfc
--- /dev/null
+++ b/sunshine/lqsoft/cstruct/fields/text.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python
+# -*- coding: utf-8
+
+__author__ = "Łukasz Rekucki"
+__date__ = "$2009-07-19 09:50:34$"
+
+from sunshine.lqsoft.cstruct.common import CField, CStruct
+from sunshine.lqsoft.cstruct.fields.numeric import UIntField
+from sunshine.lqsoft.cstruct.fields.complex import StructField
+
+from sunshine.lqsoft.cstruct.constraints import *
+
+def string_padder(opts):
+ pad = opts['padding']
+ opts['value'] += pad*'\x00'
+
+class StringField(CField):
+ KEYWORDS = dict(CField.KEYWORDS,
+ length= lambda lv: LengthConstraint(\
+ length=lv, padding_func=string_padder) )
+
+ def __init__(self, idx, default='', length=0, **kwargs):
+ CField.__init__(self, idx, default, **dict(kwargs, length=length) )
+
+ def _format_string(self, opts):
+ if opts['length'] == -1:
+ opts['length'] = len(opts['data']) - opts['offset']
+
+ return '<'+str(opts['length'])+'s'
+
+ def _retrieve_value(self, opts):
+ (v, offset) = CField._retrieve_value(self, opts)
+ return (v[0], offset)
+
+
+class NullStringField(CField):
+ KEYWORDS = dict(CField.KEYWORDS,
+ max_length= lambda lv: MaxLengthConstraint(length=lv) )
+
+ def _format_string(self, opts):
+ return '<'+str(opts['length'])+'s'
+
+ def _before_unpack(self, opts):
+ CField._before_unpack(self, opts)
+ try:
+ opts['length'] = opts['data'].index('\0', opts['offset']) - opts['offset'] + 1
+ if opts.has_key('max_length'):
+ opts['length'] = min(opts['max_length'], opts['length'])
+ except ValueError:
+ raise UnpackException("Unterminated null string occured.")
+
+ def before_pack(self, obj, offset, **opts):
+ value = getattr(obj, self.name)
+ return CField.before_pack(self,obj, offset, length=len(value), **opts)
+
+ def pack(self, obj, offset, **opts):
+ value = getattr(obj, self.name)
+ return CField.pack(self, obj, offset, length=len(value), **opts)
+
+ def _retrieve_value(self, opts):
+ (v, offset) = CField._retrieve_value(self, opts)
+ return (v[0], offset)
+
+ def set_value(self, obj, value):
+ if not isinstance(value, str) or value[-1] != '\0':
+ raise ValueError("NullStringField value must a string with last character == '\\0'.")
+
+ return CField.set_value(self, obj, value)
+
+class CStruct_VarString(CStruct):
+ length = UIntField(0)
+ text = StringField(1, length='length')
+
+class VarcharField(StructField):
+ def __init__(self, idx, default='', **opts):
+ if isinstance(default, str):
+ default = CStruct_VarString(text=default)
+ StructField.__init__(self, idx, CStruct_VarString, default, **opts)
diff --git a/sunshine/lqsoft/cstruct/test/Makefile.am b/sunshine/lqsoft/cstruct/test/Makefile.am
new file mode 100644
index 0000000..df3f6d4
--- /dev/null
+++ b/sunshine/lqsoft/cstruct/test/Makefile.am
@@ -0,0 +1,5 @@
+testdir = $(pythondir)/sunshine/lqsoft/cstruct/test
+test_PYTHON = __init__.py \
+ test_complex.py \
+ test_numeric.py \
+ test_strings.py
diff --git a/sunshine/lqsoft/cstruct/test/__init__.py b/sunshine/lqsoft/cstruct/test/__init__.py
new file mode 100755
index 0000000..c546d36
--- /dev/null
+++ b/sunshine/lqsoft/cstruct/test/__init__.py
@@ -0,0 +1,2 @@
+__author__="lreqc"
+__date__ ="$2009-07-20 15:35:31$"
diff --git a/sunshine/lqsoft/cstruct/test/test_complex.py b/sunshine/lqsoft/cstruct/test/test_complex.py
new file mode 100755
index 0000000..bbb8776
--- /dev/null
+++ b/sunshine/lqsoft/cstruct/test/test_complex.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python
+# -*- coding: utf-8
+
+import unittest
+import struct
+
+from lqsoft.cstruct.common import CStruct
+from lqsoft.cstruct.fields.complex import *
+from lqsoft.cstruct.fields.numeric import IntField, UIntField
+from lqsoft.cstruct.fields.text import NullStringField
+from lqsoft.cstruct.constraints import *
+
+__author__="lreqc"
+__date__ ="$2009-07-21 00:43:44$"
+
+class ArrayFieldTest(unittest.TestCase):
+
+ def setUp(self):
+ self.svalue = [1,1,2,3,5,8]
+ self.slen = len(self.svalue)
+ self.sdata = struct.pack("<"+str(self.slen)+"i", *self.svalue)
+ # self.sdata_ext = struct.pack("<I", self.slen) + self.sdata
+
+ def testArrayPack(self):
+ class TestStruct(CStruct):
+ array = ArrayField(0, length = self.slen, subfield=IntField(0))
+
+ s = TestStruct(array=self.svalue)
+ try:
+ s.array[1] = 'ala ma kota' # this should fail
+ self.fail('Integer array accepted a string')
+ except ValueError:
+ pass
+
+ self.assertEqual( s.array , self.svalue)
+ self.assertEqual( s.pack(), self.sdata)
+
+ def testArrayUnpack(self):
+ class TestStruct(CStruct):
+ array = ArrayField(0, length = self.slen, subfield=IntField(0))
+
+ s, offset = TestStruct.unpack(self.sdata)
+
+ self.assertEqual(s.array, self.svalue)
+ for i in xrange(0, self.slen):
+ self.assertEqual( s.array[i], self.svalue[i])
+
+class StructFieldTest(unittest.TestCase):
+
+ def setUp(self):
+ class InnerStruct(CStruct):
+ one = IntField(0, default=13)
+ two = IntField(1, default=42)
+
+ class OuterStruct(CStruct):
+ pad = NullStringField(0, default='KOT\0')
+ inner = StructField(1, struct=InnerStruct)
+ post = UIntField(2, default=0xbebafeca)
+
+ self.OuterStruct = OuterStruct
+ self.InnerStruct = InnerStruct
+
+ self.inner = InnerStruct()
+ self.inner_data = self.inner.pack()
+
+ def testAccess(self):
+ s = self.OuterStruct(inner=self.inner)
+ self.assertEqual(s.pad, 'KOT\0')
+ self.assertEqual(s.post, 0xbebafeca)
+ self.assertEqual(s.inner, self.inner)
+
+ def testPack(self):
+ s = self.OuterStruct(inner=self.inner)
+ data = s.pack()
+ print repr(data)
+ self.assertEqual( data[4:-4], self.inner_data )
+
+ def testGaduMsgOut(self):
+ from lqsoft.pygadu.network import *
+ import time
+
+ html_text = """<span style="color:#000000; font-family:'MS Shell Dlg 2'; font-size:12pt; ">ala</span>"""
+ rcpt = 1849224
+ klass = MessageOutPacket
+ attrs = StructMsgAttrs()
+ attrs.richtext = StructRichText()
+
+ payload = StructMessage(klass=StructMessage.CLASS.CHAT, \
+ html_message=html_text, plain_message='ala\0', \
+ attrs = attrs)
+
+ packet = klass( recipient=rcpt, seq=int(time.time()), content=payload)
+ data = packet.as_packet()
+ print
+ for (i, b) in enumerate(data):
+ print '%02x' % ord(b),
+ if (i+1)%8 == 0: print
+
+ print packet.content.offset_plain
+ print repr(data[8+packet.content.offset_plain:])
+ print packet.content.offset_attrs
+
+if __name__ == '__main__':
+ import lqsoft.cstruct.test.test_complex
+
+ suite = unittest.TestLoader().loadTestsFromName('StructFieldTest.testGaduMsgOut', lqsoft.cstruct.test.test_complex)
+ unittest.TextTestRunner(verbosity=2).run(suite) \ No newline at end of file
diff --git a/sunshine/lqsoft/cstruct/test/test_numeric.py b/sunshine/lqsoft/cstruct/test/test_numeric.py
new file mode 100755
index 0000000..2b821e4
--- /dev/null
+++ b/sunshine/lqsoft/cstruct/test/test_numeric.py
@@ -0,0 +1,171 @@
+#!/usr/bin/env python
+# -*- coding: utf-8
+
+import unittest
+import struct
+
+from lqsoft.cstruct.common import CStruct
+from lqsoft.cstruct.fields.numeric import *
+from lqsoft.cstruct.constraints import *
+
+__author__ = "lreqc"
+__date__ = "$2009-07-21 00:43:44$"
+
+class NumericFieldTest(unittest.TestCase):
+
+ def setUp(self):
+ self.ivalue = 42
+ self.idata = struct.pack('<i', self.ivalue)
+
+ def test0Pack(self):
+ class TestStruct(CStruct):
+ intField = NumericField(0, ctype='int')
+
+ s = TestStruct(intField=self.ivalue)
+ self.assert_(s.intField == self.ivalue)
+ self.assert_( s.pack() == self.idata )
+
+ def test0Unpack(self):
+ class TestStruct(CStruct):
+ intField = NumericField(0, ctype='int')
+
+ s, offset = TestStruct.unpack(self.idata)
+ self.assert_(s.intField == self.ivalue)
+
+ def testPrefixMatch(self):
+ class TestStruct(CStruct):
+ intField = NumericField(0, ctype='int', prefix=self.idata[0:2])
+
+ v, offset = TestStruct.unpack(self.idata)
+ self.assert_(v.intField == self.ivalue)
+
+ def testPrefixMismatch(self):
+ class TestStruct(CStruct):
+ intField = NumericField(0, ctype='int', prefix=chr(ord(self.idata[0])+13%255))
+
+ try:
+ v, offset = TestStruct.unpack(self.idata)
+ self.fail('Invalid prefix accepted by field during unpack.')
+ except UnpackException, e:
+ if not isinstance(e.constraint, PrefixConstraint):
+ self.fail(e.getMessage())
+ # otherwise we succeded
+
+ def testPrefixTooLong(self):
+ class TestStruct(CStruct):
+ intField = NumericField(0, ctype='int', prefix=(self.idata+'\x00') )
+
+ try:
+ v, offset = TestStruct.unpack(self.idata)
+ self.fail('Too long prefix accepted by field during unpack.')
+ except UnpackException, e:
+ if not isinstance(e.constraint, PrefixConstraint):
+ self.fail(e.getMessage())
+ # otherwise we succeded
+
+ def testPrefixEmpty(self):
+ class TestStruct(CStruct):
+ intField = NumericField(0, ctype='int', prefix='' )
+
+ v, offset = TestStruct.unpack(self.idata)
+ self.assert_(v.intField == self.ivalue)
+
+ def testByteAndShort(self):
+ class TestStruct(CStruct):
+ byteField = ByteField(0)
+ shortField = ShortField(1)
+ intField = IntField(2)
+
+ s = TestStruct(byteField=42, shortField=-30000, intField=4000000)
+ s2, offset = TestStruct.unpack( s.pack() )
+
+ self.assert_(s2.intField == s.intField)
+ self.assert_(s2.byteField == s.byteField)
+ self.assert_(s2.shortField == s.shortField)
+
+ def testOffset(self):
+ class TestStruct(CStruct):
+ f1 = IntField(0, offset=0)
+ f2 = IntField(1, offset=4)
+
+ data = [chr(x) for x in xrange(64)]
+ data[16:20] = self.idata
+ data = "".join(data)
+
+ v, offset = TestStruct.unpack(data, 16)
+ self.assert_(v.f1 == self.ivalue)
+
+ def testOmmitUnpack(self):
+ class TestStruct(CStruct):
+ f1 = UIntField(0, prefix__ommit='\x00')
+ f2 = UIntField(1)
+ f3 = IntField(2)
+
+ d = struct.pack("<Ii", 0xcafebabe, 2)
+ v, offset = TestStruct.unpack(d)
+ self.assertEqual(offset, 8)
+ self.assertEqual(v.f1, None)
+ self.assertEqual(v.f2, 0xcafebabe)
+ self.assertEqual(v.f3, 2)
+
+ def testOmmitPack(self):
+ class TestStruct(CStruct):
+ f1 = UIntField(0, prefix__ommit='\x00')
+ f2 = UIntField(1)
+ f3 = IntField(2)
+
+ s = TestStruct(f1=None, f2=2, f3=3)
+ self.assertEqual(s.f2, 2)
+ self.assertEqual(s.f3, 3)
+ self.assertEqual( s.pack(), '\x02\x00\x00\x00\x03\x00\x00\x00')
+
+ def testOmmitPackWithOffset(self):
+ class TestStruct(CStruct):
+ offset_field = UIntField(0)
+ not_important = IntField(1, prefix__ommit='\x02')
+ data = IntField(2, offset='offset_field')
+
+ # s = TestStruct(data=0xcafe)
+ # s.pack()
+ # self.assertEqual(s.offset_field, 8)
+
+ s = TestStruct(not_important=None, data=0xcafe)
+ s.pack()
+ self.assertEqual(s.offset_field, 4)
+
+
+
+ def testBoundViolation(self):
+ class A(CStruct):
+ f = ByteField(0)
+
+ class B(CStruct):
+ f = ShortField(1)
+
+ class C(CStruct):
+ f = UIntField(2)
+
+ try:
+ A(b=5442)
+ self.fail("Illegal values accepted for byte field.")
+ except:
+ return
+
+ try:
+ B(f=5645442)
+ self.fail("Illegal values accepted for short field.")
+ except:
+ return
+
+ try:
+ C(f=-1)
+ self.fail("Illegal values accepted for unsigned integer.")
+ except:
+ return
+
+
+if __name__ == '__main__':
+ import lqsoft.cstruct.test.test_numeric
+
+ suite = unittest.TestLoader().loadTestsFromName('NumericFieldTest.testOmmitPackWithOffset', lqsoft.cstruct.test.test_numeric)
+ unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/sunshine/lqsoft/cstruct/test/test_strings.py b/sunshine/lqsoft/cstruct/test/test_strings.py
new file mode 100755
index 0000000..9b3aaa7
--- /dev/null
+++ b/sunshine/lqsoft/cstruct/test/test_strings.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+# -*- coding: utf-8
+
+import unittest
+import struct
+
+from lqsoft.cstruct.common import CStruct
+from lqsoft.cstruct.fields.text import *
+from lqsoft.cstruct.fields.numeric import IntField
+from lqsoft.cstruct.constraints import *
+
+__author__="lreqc"
+__date__ ="$2009-07-21 00:43:44$"
+
+class StringFieldTest(unittest.TestCase):
+
+ def setUp(self):
+ self.svalue = "Hello World!"
+ self.slen = len(self.svalue)
+ self.sdata = struct.pack("<"+str(self.slen)+"s", self.svalue)
+ self.sdata_ext = struct.pack("<I", self.slen) + self.sdata
+
+ def testNegativeLength(self):
+ class TestStruct(CStruct):
+ text = StringField(0, length=-1)
+
+ s, offset = TestStruct.unpack(self.sdata)
+ self.assertEqual(s.text, self.svalue)
+ self.assertEqual(offset, self.slen)
+
+ def test0PackWithPadding(self):
+ class TestStruct(CStruct):
+ text = StringField(0, length=32)
+
+ s = TestStruct(text=self.svalue)
+ self.assertTrue(self.slen < 32)
+ self.assertEqual(s.text, self.svalue + (32-self.slen)*'\x00')
+ self.assertEqual(s.pack(), s.text)
+
+ def test0Pack(self):
+ class TestStruct(CStruct):
+ text = StringField(0, length=self.slen)
+
+ s = TestStruct(text=self.svalue)
+ self.assertEqual(s.text, self.svalue)
+ self.assertEqual(s.pack(), self.sdata)
+
+ def test0Unpack(self):
+ class TestStruct(CStruct):
+ text = StringField(0, length=self.slen)
+
+ s, offset = TestStruct.unpack(self.sdata)
+ self.assertEqual(offset, self.slen)
+ self.assertEqual(s.text, self.svalue)
+
+ def testLengthAsField(self):
+ class TestStruct(CStruct):
+ tlen = IntField(0)
+ text = StringField(1, length='tlen')
+
+ s = TestStruct(text=self.svalue)
+ self.assertEqual(s.text, self.svalue)
+ self.assertEqual(s.tlen, self.slen)
+ self.assertEqual(s.pack(), self.sdata_ext)
+
+ s, offset = TestStruct.unpack(self.sdata_ext)
+ self.assertEqual(s.text, self.svalue)
+ self.assertEqual(s.tlen, self.slen)
+
+ def test0PackNullString(self):
+ class TestStruct(CStruct):
+ text = NullStringField(0)
+ checksum = IntField(1, default=0x7afebabe)
+
+ s = TestStruct(text='Ala ma kota\0')
+ self.assertEqual(s.pack(), 'Ala ma kota\0' + struct.pack("<i", 0x7afebabe))
+
+
+if __name__ == "__main__":
+ import lqsoft.cstruct.test.test_strings \ No newline at end of file
diff --git a/sunshine/lqsoft/pygadu/Makefile.am b/sunshine/lqsoft/pygadu/Makefile.am
new file mode 100644
index 0000000..16ddb3f
--- /dev/null
+++ b/sunshine/lqsoft/pygadu/Makefile.am
@@ -0,0 +1,8 @@
+pygadudir = $(pythondir)/sunshine/lqsoft/pygadu
+pygadu_PYTHON = __init__.py \
+ models.py \
+ network_base.py \
+ network.py \
+ network_v8.py \
+ packets.py \
+ twisted_protocol.py
diff --git a/sunshine/lqsoft/pygadu/__init__.py b/sunshine/lqsoft/pygadu/__init__.py
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/sunshine/lqsoft/pygadu/__init__.py
diff --git a/sunshine/lqsoft/pygadu/models.py b/sunshine/lqsoft/pygadu/models.py
new file mode 100755
index 0000000..1095b31
--- /dev/null
+++ b/sunshine/lqsoft/pygadu/models.py
@@ -0,0 +1,302 @@
+# -*- coding: utf-8
+__author__="lreqc"
+__date__ ="$2009-07-14 07:33:27$"
+
+from sunshine.lqsoft.pygadu.network_base import StructNotice
+import xml.etree.ElementTree as ET
+import hashlib
+import zlib
+
+class GaduProfile(object):
+
+ def __init__(self, uin):
+ self.uin = uin
+ self.__status = None
+ self.__hashelem = None
+ self.__contacts = {}
+ self.__groups = {}
+ self.__connection = None
+
+ def __set_password(self, value):
+ self.__hashelem = hashlib.new('sha1')
+ self.__hashelem.update(value)
+
+ def __get_hashelem(self):
+ return self.__hashelem.copy()
+
+ def __set_status(self, value):
+ self.__status = value
+
+ def __get_status(self):
+ return self.__status
+
+ password = property(__get_hashelem, __set_password)
+ status = property(__get_status, __set_status)
+
+ def _updateContact(self, notify):
+ # notify is of class GGStruct_Status80
+ if notify.uin != self.uin:
+ if self.__contacts.has_key(notify.uin):
+ contact = self.__contacts[notify.uin]
+ else:
+ contact_xml = ET.Element("Contact")
+ ET.SubElement(contact_xml, "Guid").text = notify.uin
+ ET.SubElement(contact_xml, "GGNumber").text = notify.uin
+ ET.SubElement(contact_xml, "ShowName").text = "Unknown User"
+ ET.SubElement(contact_xml, "Groups")
+ c = GaduContact.from_xml(contact_xml)
+ self.addContact( c )
+ #contact = GaduContact.simple_make(self, notify.uin, "Unknown User")
+
+ contact.status = notify.status
+ contact.description = notify.description
+ self.onContactStatusChange(contact)
+
+ def _creditials(self, result, *args, **kwargs):
+ """Called by protocol, to get creditials, result will be passed to login
+ procedure. It should be a 2-tuple with (uin, hash_elem)"""
+ return self.onCreditialsNeeded()
+
+ def _loginSuccess(self, conn, *args, **kwargs):
+ self.__connection = conn
+ self.onLoginSuccess()
+ return self
+
+ # high-level interface
+ @property
+ def connected(self):
+ """Is the profile currently used in an active connection"""
+ return self.__connection is not None
+
+ def addContact(self, contact):
+ if self.__contacts.has_key(contact.uin):
+ raise ValueError("Contact with UIN %d already exists." % contact.uin)
+
+ self.__contacts[contact.uin] = contact
+ if self.connected:
+ self.setNotifyState(contact.uin, contact.notify_flags)
+
+ def notifyAboutContact(self, contact):
+ """Notify GG server when new GG contact is added to the contacts list."""
+ self.__connection.addNewContact(contact)
+
+ def addGroup(self, group):
+ if self.__groups.has_key(group.Id):
+ raise ValueError("Group %d already exists." % group.Id)
+ self.__groups[group.Id] = group
+
+ # stuff that user can use
+ def setNotifyState(self, uin, new_state):
+ pass
+
+ def sendTextMessage(self, text):
+ pass
+
+ def setMyState(self, new_state, new_description=''):
+ if not self.connected:
+ raise RuntimeError("You need to be connected, to import contact list from the server.")
+
+ self.__connection.changeStatus(new_state, new_description)
+
+ def sendTo(self, uin, html_message, plain_message):
+ if not self.connected:
+ raise RuntimeError("You need to be connected, to send messages.")
+ self.__connection.sendHTMLMessage(uin, html_message + '\0', plain_message + '\0')
+
+ def importContacts(self, callback):
+ """Issue an import request. This is non-blocking and returns no data."""
+ if not self.connected:
+ raise RuntimeError("You need to be connected, to import contact list from the server.")
+
+ def parse_xml(data):
+ #print zlib.decompress(data)
+ book = ET.fromstring(zlib.decompress(data))
+ self._flushContacts()
+
+ for elem in book.find('Groups').getchildren():
+ self.addGroup( GaduContactGroup.from_xml(elem) )
+
+ for elem in book.find('Contacts').getchildren():
+ contact = GaduContact.from_xml(elem)
+ self.addContact( contact )
+ self.__connection.addNewContact(contact)
+
+ callback()
+
+ self.__connection.sendImportRequest(parse_xml)
+
+ def _flushContacts(self):
+ self.__contacts = {}
+ self.__groups = {}
+
+ def exportContacts(self):
+ pass
+
+ def disconnect(self):
+ self.__connection.transport.loseConnection()
+
+ # stuff that should be implemented by user
+ def onCreditialsNeeded(self, *args, **kwargs):
+ return (self.uin, self.__hashelem, self.__status)
+
+ def onLoginSuccess(self):
+ """Called when login is completed."""
+ pass
+
+ def onLoginFailure(self, reason):
+ """Called after an unsuccessful login."""
+ pass
+
+ def onContactStatusChange(self, contact):
+ """Called when a status of a contact has changed."""
+ pass
+
+ def onMessageReceived(self, message):
+ """Called when a message had been received"""
+ pass
+
+ def onStatusNoticiesRecv(self):
+ """Called when a contact list notify was sent"""
+ pass
+
+ def isContactExist(self, uin):
+ return self.__contacts.has_key(uin)
+
+ def get_contact(self, uin):
+ if self.__contacts.has_key(uin):
+ return self.__contacts[uin]
+ else:
+ return None
+
+ @property
+ def contacts(self):
+ return self.__contacts.itervalues()
+
+ @property
+ def groups(self):
+ return self.__groups.itervalues()
+
+class Def(object):
+ def __init__(self, type, default_value, required=False, exportable=True, init=lambda x: x):
+ self.type = type
+ self.default = default_value
+ self.required = required
+ self.exportable = exportable
+ self._init = init
+
+ def init(self, value):
+ return self.type( self._init(value) )
+
+def mkdef(*args):
+ return Def(*args)
+
+
+class FlatXMLObject(object):
+
+ def __init__(self, **kwargs):
+ for (k, v) in self.SCHEMA.iteritems():
+ if v.required and not kwargs.has_key(k):
+ raise ValueError("You must supply a %s field." % k)
+
+ setattr(self, k, kwargs.get(k, v.default))
+ if not isinstance(getattr(self, k), v.type):
+ raise ValueError("Field %s has to be of class %s." % (k, v.type.__name__))
+
+ @classmethod
+ def from_xml(cls, element):
+ dict = {}
+
+ for (k, v) in cls.SCHEMA.iteritems():
+ elem = element.find("./"+k)
+ if not v.exportable:
+ continue
+
+ if v.required and elem is None:
+ raise ValueError("Invalid element - need child element %s to unpack." % k)
+
+ dict[k] = v.type(elem.text if elem is not None and elem.text else v.default)
+ if k == 'Groups':
+ dict[k] = v.type(ET.tostring(elem) if elem is not None else v.default)
+ return cls(**dict)
+
+class GaduContactGroup(FlatXMLObject):
+ SCHEMA = {
+ "Id": mkdef(str, '', True),
+ "Name": mkdef(str, '', True),
+ "IsExpanded": mkdef(bool, True),
+ "IsRemovable": mkdef(bool, True),
+ }
+
+
+class GaduContact(FlatXMLObject):
+ """Single contact as seen in catalog (person we are watching) - conforming to GG8.0"""
+ SCHEMA = {
+ 'Guid': mkdef(str, '', True),
+ 'GGNumber': mkdef(str, '', True),
+ 'ShowName': mkdef(str, '', True),
+ 'MobilePhone': mkdef(str, ''),
+ 'HomePhone': mkdef(str, ''),
+ 'Email': mkdef(str, 'someone@somewhere.moc'),
+ 'WWWAddress': mkdef(str, ''),
+ 'FirstName': mkdef(str, ''),
+ 'LastName': mkdef(str, ''),
+ 'Gender': mkdef(int, 0),
+ 'Birth': mkdef(str, ''),
+ 'City': mkdef(str, ''),
+ 'Province': mkdef(str, ''),
+ 'Groups': mkdef(str, ''),
+ 'CurrentAvatar': mkdef(int, 0),
+ # 'Avatars': mkdef(list, []),
+ 'UserActivatedInMG':mkdef(bool, False),
+ 'FlagBuddy': mkdef(bool, False),
+ 'FlagNormal': mkdef(bool, False),
+ 'FlagFriend': mkdef(bool, False),
+ 'FlagIgnored': mkdef(bool, False),
+ # state variables
+ 'description': mkdef(str, '', False, False),
+ 'status': mkdef(int, 0, False, False),
+ }
+
+ @classmethod
+ def simple_make(cls, profile, uin, name):
+ return cls(profile, Guid=str(uin), GGNumber=str(uin), ShowName=name)
+
+ def __str__(self):
+ return "[%s,%d: %s]" % (self.GGNumber, self.status, self.description)
+
+ @property
+ def uin(self):
+ return int(self.GGNumber)
+
+ @property
+ def notify_flags(self):
+ return int(self.FlagBuddy and StructNotice.TYPE.BUDDY) \
+ & int(self.FlagFriend and StructNotice.TYPE.FRIEND) \
+ & int(self.FlagIgnored and StructNotice.TYPE.IGNORE)
+
+ def updateStatus(self, status, desc=None):
+ self.status = status
+ if desc: self.description = desc
+
+ def updateName(self, name):
+ self.ShowName = name
+
+ def updateGroups(self, groups):
+ self.Groups = groups
+
+ def get_desc(self):
+ #print 'Tak to get_desc, desctiption.text zwraca: %s a samo description: %s' % (self.description.text, self.description)
+ if self.description.text:
+ return self.description.text
+ else:
+ return ''
+
+# @classmethod
+# def from_request_string(cls, rqs):
+# dict = {}
+# for (fmt, value) in zip(cls.RQ_STRING_FORMAT, rqs.split(';')):
+# dict[fmt[0]] = fmt[1](value)
+# return cls(**dict)
+#
+# def request_string(self):
+# return ";".join( [ str(self.__getattribute__(fmt[0])) for fmt in self.RQ_STRING_FORMAT] )
diff --git a/sunshine/lqsoft/pygadu/network.py b/sunshine/lqsoft/pygadu/network.py
new file mode 100755
index 0000000..653b88c
--- /dev/null
+++ b/sunshine/lqsoft/pygadu/network.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8
+#
+
+__author__= "Łukasz Rekucki"
+__date__ = "$2009-07-23 19:35:08$"
+
+
+from sunshine.lqsoft.pygadu.network_base import *
+from sunshine.lqsoft.pygadu.network_v8 import *
diff --git a/sunshine/lqsoft/pygadu/network_base.py b/sunshine/lqsoft/pygadu/network_base.py
new file mode 100755
index 0000000..e3471fc
--- /dev/null
+++ b/sunshine/lqsoft/pygadu/network_base.py
@@ -0,0 +1,110 @@
+# -*- coding: utf-8
+__author__="lreqc"
+__date__ ="$2009-07-14 01:07:32$"
+
+from sunshine.lqsoft.pygadu.packets import inpacket, outpacket
+
+from sunshine.lqsoft.cstruct.common import CStruct
+from sunshine.lqsoft.cstruct.fields import complex, numeric
+
+from sunshine.lqsoft.utils import Enum
+
+PACKET_HEADER_LENGTH = 8
+
+class GaduPacketHeader(CStruct):
+ """Struktura opisująca nagłówek pakietu w GG"""
+ msg_type = numeric.UIntField(0)
+ msg_length = numeric.UIntField(1)
+
+ def __str__(self):
+ return '[GGHDR: type=%d, length %d]' % (self.msg_type, self.msg_length)
+
+class GaduPacket(CStruct):
+ """Wspólna nadklasa dla wszystkich wiadomości w GG"""
+ def as_packet(self):
+ data = self.pack()
+ hdr = GaduPacketHeader(msg_type=self.packet_id, msg_length=len(data))
+ return hdr.pack() + data
+
+ def __str__(self):
+ return self.__class__.__name__
+
+#
+# INCOMING PACKETS
+#
+@inpacket(0x01)
+class WelcomePacket(GaduPacket):
+ seed = numeric.IntField(0)
+
+@inpacket(0x05)
+class MessageAckPacket(GaduPacket): #SendMsgAck
+ MSG_STATUS = Enum({
+ 'BLOCKED': 0x0001, 'DELIVERED': 0x0002,
+ 'QUEUED': 0x0003, 'MBOXFULL': 0x0004,
+ 'NOT_DELIVERED': 0x0006
+ })
+
+ msg_status = numeric.IntField(0)
+ recipient = numeric.IntField(1)
+ seq = numeric.IntField(2)
+
+@inpacket(0x09)
+class LoginFailedPacket(GaduPacket):
+ pass
+
+@inpacket(0x0b)
+class DisconnectPacket(GaduPacket):
+ pass
+
+@inpacket(0x14)
+class NeedEmailPacket(GaduPacket):
+ pass
+
+@inpacket(0x0d)
+class UnavailbleAckPacket(GaduPacket):
+ pass
+
+@inpacket(0x07)
+class PongPacket(GaduPacket):
+ pass
+
+#
+# OUTGOING PACKETS
+#
+
+class StructNotice(CStruct): # Notify
+ TYPE = Enum({
+ 'BUDDY': 0x01,
+ 'FRIEND': 0x02,
+ 'IGNORE': 0x04
+ })
+
+ uin = numeric.UIntField(0)
+ type = numeric.UByteField(1, default=0x03)
+
+ def __str__(self):
+ return "%d[%d]" (self.uin, self.type)
+
+@outpacket(0x0f)
+class NoticeFirstPacket(GaduPacket): #NotifyFirst
+ contacts = complex.ArrayField(0, complex.StructField(0, struct=StructNotice), length=-1)
+
+@outpacket(0x10)
+class NoticeLastPacket(GaduPacket): #NotifyLast
+ contacts = complex.ArrayField(0, complex.StructField(0, struct=StructNotice), length=-1)
+
+@outpacket(0x12)
+class NoNoticesPacket(GaduPacket):
+ pass
+
+@outpacket(0x0d)
+class AddNoticePacket(GaduPacket):
+ contact = complex.StructField(0, struct=StructNotice)
+
+@outpacket(0x0e)
+class RemoveNoticePacker(GaduPacket):
+ contact = complex.StructField(0, struct=StructNotice)
+
+@outpacket(0x08)
+class PingPacket(GaduPacket):
+ pass
diff --git a/sunshine/lqsoft/pygadu/network_v8.py b/sunshine/lqsoft/pygadu/network_v8.py
new file mode 100755
index 0000000..7921159
--- /dev/null
+++ b/sunshine/lqsoft/pygadu/network_v8.py
@@ -0,0 +1,196 @@
+# -*- coding: utf-8
+#
+__author__= "Łukasz Rekucki (lqc)"
+__date__ = "$2009-07-13 17:06:21$"
+__doc__ = """Packet structures for Gadu-Gadu 8.0"""
+
+# standard library stuff
+import hashlib
+import struct
+
+# Cstruct stuff
+from sunshine.lqsoft.cstruct.common import CStruct
+from sunshine.lqsoft.cstruct.fields.numeric import *
+from sunshine.lqsoft.cstruct.fields.text import *
+from sunshine.lqsoft.cstruct.fields.complex import *
+
+# useful helpers
+from sunshine.lqsoft.utils import Enum
+
+# base protocol
+from sunshine.lqsoft.pygadu.packets import inpacket, outpacket
+from sunshine.lqsoft.pygadu.network_base import GaduPacket
+
+#
+# COMMON STRUCTURES
+#
+class StructStatus(CStruct):
+ uin = IntField(0)
+ status = IntField(1)
+ flags = IntField(2)
+ remote_ip = IntField(3)
+ remote_port = ShortField(4)
+ image_size = ByteField(5)
+ reserved01 = ByteField(6)
+ reserved02 = IntField(7)
+ description = VarcharField(8)
+
+class StructConference(CStruct):
+ attr_type = ByteField(0, default=0x01)
+ rcp_count = IntField(1)
+ recipients = ArrayField(2, length='rcp_count', subfield=IntField(0))
+
+class StructRichText(CStruct):
+ attr_type = ByteField(0, default=0x02)
+ length = UShortField(1)
+ format = StringField(2, length='length', default='\x00\x00\x08\x00\x00\x00')
+
+class StructMsgAttrs(CStruct):
+ conference = StructField(0, struct=StructConference, prefix__ommit="\x01")
+
+ # additional formating for the plain_message version
+ richtext = StructField(1, struct=StructRichText, prefix__ommit="\x02")
+
+
+class StructMessage(CStruct):
+ CLASS = Enum({
+ 'QUEUED': 0x0001,
+ 'MESSAGE': 0x0004,
+ 'CHAT': 0x0008,
+ 'CTCP': 0x0010,
+ 'NOACK': 0x0020,
+ })
+
+ klass = IntField(0)
+ offset_plain = IntField(1) # tekst
+ offset_attrs = IntField(2) # atrybuty
+
+ # the message in HTML (the server sometimes forgets to place the
+ # ending '\0' char, so just cut-off the message at the plain_message offset
+ # encoding: utf-8
+ html_message = StringField(3, length=property(\
+ lambda opts: opts['obj'].offset_plain - opts['offset'],\
+ lambda opts, new_value: new_value ) )
+
+ # the message in plain text
+ # NOTE: encoding is cp1250
+ plain_message = NullStringField(4, offset='offset_plain')
+
+ # if the message is part of a conference, this includes useful data
+ attrs = StructField(5, struct=StructMsgAttrs, offset='offset_attrs')
+
+#
+# PACKETS
+#
+@outpacket(0x31)
+class LoginPacket(GaduPacket):
+ uin = UIntField(0)
+ language = StringField(1, length=2, default='pl')
+ hash_type = UByteField(2, default=0x02)
+ login_hash = StringField(3, length=64)
+ status = UIntField(4, default=0x02)
+ flags = UIntField(5, default=0x03)
+ features = UIntField(6, default=0x37)
+ local_ip = IntField(7)
+ local_port = ShortField(8)
+ external_ip = IntField(9)
+ external_port = ShortField(10)
+ image_size = UByteField(11, default=0xff)
+ unknown01 = UByteField(12, default=0x64)
+ version = VarcharField(13, default="Gadu-Gadu Client build 8.0.0.9103")
+ description = VarcharField(14)
+
+ def update_hash(self, password, seed):
+ """Uaktualnij login_hash używając algorytmu SHA1"""
+ hash = hashlib.new('sha1')
+ hash.update(password)
+ hash.update(struct.pack('<i', seed))
+ self.login_hash = hash.digest()
+
+@inpacket(0x35)
+class LoginOKPacket(GaduPacket): #LoginOk80
+ reserved = IntField(0, True)
+
+@inpacket(0x2e)
+class MessageInPacket(GaduPacket): #RecvMsg80
+ sender = IntField(0)
+ seq = IntField(1)
+ time = IntField(2)
+ content = StructField(3, struct=StructMessage)
+
+@outpacket(0x2d)
+class MessageOutPacket(GaduPacket):
+ recipient = IntField(0, default=None)
+ seq = IntField(1)
+ content = StructField(2, struct=StructMessage)
+
+@outpacket(0x38)
+class ChangeStatusPacket(GaduPacket): #NewStatus80
+ STATUS = Enum({
+ 'NOT_AVAILABLE': 0x0001,
+ 'NOT_AVAILABLE_DESC': 0x0015,
+ 'FFC': 0x0017,
+ 'FFC_DESC': 0x0018,
+ 'AVAILABLE': 0x0002,
+ 'AVAILABLE_DESC': 0x0004,
+ 'BUSY': 0x0003,
+ 'BUSY_DESC': 0x0005,
+ 'DND': 0x0021,
+ 'DND_DESC': 0x0022,
+ 'HIDDEN': 0x0014,
+ 'HIDDEN_DESC': 0x0016,
+ 'DND': 0x0021,
+ 'BLOCKED': 0x0006,
+ 'MASK_FRIEND': 0x8000,
+ 'MASK_GFX': 0x0100,
+ 'MASK_STATUS': 0x4000,
+ })
+
+ status = IntField(0)
+ flags = IntField(1)
+ #description = VarcharField(2, default='test')
+ description_size = UIntField(2)
+ description = StringField(3, length='description_size')
+
+@inpacket(0x36)
+class StatusUpdatePacket(GaduPacket): # Status80
+ contact = StructField(0, struct=StructStatus)
+
+@inpacket(0x37)
+class StatusNoticiesPacket(GaduPacket): # NotifyReply80
+ contacts = ArrayField(0, length=-1, subfield=StructField(0, struct=StructStatus))
+
+#
+# Contact database altering packets
+#
+@outpacket(0x2f)
+class ULRequestPacket(GaduPacket): # UserListReq80
+ """Import contact list from the server"""
+ TYPE = Enum({
+ 'PUT': 0x00,
+ 'PUT_MORE': 0x01,
+ 'GET': 0x02,
+ })
+
+ type = ByteField(0)
+ data = StringField(1, length=-1)
+
+@inpacket(0x30)
+class ULReplyPacket(GaduPacket): # UserListReply80
+ TYPE = Enum({
+ 'PUT_REPLY': 0x00,
+ 'PUT_REPLY_MORE': 0x02,
+ 'GET_REPLY_MORE': 0x04,
+ 'GET_REPLY': 0x06,
+ })
+
+ type = ByteField(0)
+ data = StringField(1, length=-1)
+
+ @property
+ def is_get(self):
+ return (self.type & self.TYPE.GET_REPLY_MORE)
+
+ @property
+ def is_final(self):
+ return (self.type & 0x02)
diff --git a/sunshine/lqsoft/pygadu/packets.py b/sunshine/lqsoft/pygadu/packets.py
new file mode 100755
index 0000000..eb261bc
--- /dev/null
+++ b/sunshine/lqsoft/pygadu/packets.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8
+__author__="lreqc"
+__date__ ="$2009-07-14 01:04:28$"
+
+class Resolver(object):
+ __by_ID_in = {}
+ __by_ID_out = {}
+ __by_class = {}
+ __by_name = {}
+
+ @classmethod
+ def packet(cls, id, is_out):
+ def decorator(pcls):
+ dict = cls.__by_ID_out if is_out else cls.__by_ID_in
+ conflict = dict.get(id, None)
+
+ if conflict is not None:
+ raise ValueError("ID %d for packet %s conflicts with %s" \
+ % (id,pcls.__name__,conflict.__name__) )
+ if dict.has_key(pcls):
+ raise ValueError("Every class can have only ONE ID.")
+
+ dict[id] = pcls
+ cls.__by_class[pcls] = (id, is_out)
+ cls.__by_name[pcls.__name__] = pcls
+ pcls.packet_id = id
+ return pcls
+
+ return decorator
+
+ @classmethod
+ def list_packets(cls):
+ print "Listing packets:"
+ for (klass, (id, is_out)) in sorted(cls.__by_class.iteritems(), key=lambda k: (k[1][1],k[1][0])):
+ print klass.__name__, hex(id), "(%s)" % (is_out and "OUT" or "IN")
+
+ @classmethod
+ def by_name(cls, name):
+ return cls.__by_name[name]
+
+ @classmethod
+ def by_IDi(cls, id):
+ return cls.__by_ID_in[id]
+
+ @classmethod
+ def by_IDo(cls, id):
+ return cls.__by_ID_out[id]
+
+
+def inpacket(id):
+ return Resolver.packet(id, False)
+
+def outpacket(id):
+ return Resolver.packet(id, True) \ No newline at end of file
diff --git a/sunshine/lqsoft/pygadu/twisted_protocol.py b/sunshine/lqsoft/pygadu/twisted_protocol.py
new file mode 100755
index 0000000..3c5839c
--- /dev/null
+++ b/sunshine/lqsoft/pygadu/twisted_protocol.py
@@ -0,0 +1,276 @@
+# -*- coding: utf-8
+__author__="lreqc"
+__date__ ="$2009-07-14 05:51:00$"
+
+from twisted.internet.defer import Deferred
+from twisted.internet.protocol import Protocol
+from twisted.internet import task
+import twisted.python.log as tlog
+
+from sunshine.lqsoft.pygadu.network import *
+from sunshine.lqsoft.pygadu.packets import Resolver
+
+import struct, time
+
+class GaduClient(Protocol):
+
+ def __init__(self, profile):
+ self.user_profile = profile # the user connected to this client
+ self.doLogin = Deferred()
+ self.doLogin.addCallbacks(profile._creditials, self._onInvalidCreditials)
+ self.doLogin.addCallbacks(self._doLogin, self._onLoginFailed)
+ self.doLogin.addErrback(self._onLoginFailed)
+
+ self.loginSuccess = Deferred()
+ self.loginSuccess.addCallbacks(self._sendAllContacts, self._onLoginFailed)
+ self.loginSuccess.addCallbacks(profile._loginSuccess, self._onLoginFailed)
+ self.loginSuccess.addErrback(self._onLoginFailed)
+
+ self.importrq_cb = None
+ self.__pingThread = None
+
+ def connectionMade(self):
+ self.__buffer = ''
+ self.__chdr = None
+ # Nie trzeba tu nic robi�, bo to server pierwszy wysyła nam wiadomość
+
+ def connectionLost(self, reason):
+ if self.__pingThread:
+ self.__pingThread.stop()
+ self.__pingThread = None
+
+ Protocol.connectionLost(self, reason)
+
+ def __pop_data(self, n):
+ data, self.__buffer = self.__buffer[:n], self.__buffer[n:]
+ return data
+
+ def dataReceived(self, data):
+ self.__buffer += data
+
+ while True:
+ if self.__chdr is not None:
+ # if we are inside of a message
+ hdr = self.__chdr
+
+ if len(self.__buffer) < hdr.msg_length:
+ # not yet
+ break
+
+ try:
+ msg_class = Resolver.by_IDi(hdr.msg_type)
+ except KeyError, e:
+ self.__pop_data(hdr.msg_length)
+ self._log('Ommiting message with type %d.' % hdr.msg_type)
+ else:
+ msg, _ = msg_class.unpack( self.__pop_data(hdr.msg_length) )
+ self._messageReceived(hdr, msg)
+ finally:
+ self.__chdr = None
+ else:
+ # we're waiting for a header
+ if len(self.__buffer) < PACKET_HEADER_LENGTH:
+ # no header yet
+ break
+
+ self.__chdr, _ = GaduPacketHeader.unpack(\
+ self.__pop_data(PACKET_HEADER_LENGTH))
+ # continue normally
+
+ # end of data loop
+
+ def _sendPacket(self, msg):
+ # wrap the packet with a transport header
+ self.transport.write( msg.as_packet() )
+
+ def _messageReceived(self, hdr, msg):
+ """Called when a full GG message has been received"""
+ self._log("Calling action: " + msg.__class__.__name__)
+ getattr(self, '_handle' + msg.__class__.__name__, self._log)(msg)
+
+ # handlers
+ def _handleWelcomePacket(self, msg):
+ self._log("Welcome seed is: " + str(msg.seed))
+ self.__seed = msg.seed
+ self.doLogin.callback(self.__seed)
+
+ def _doLogin(self, result, *args, **kwargs):
+ self._log("Sending creditials to the server.")
+ login_klass = Resolver.by_name('LoginPacket')
+ result[1].update( struct.pack("<i", self.__seed) )
+ login = login_klass(uin=result[0], login_hash=result[1].digest(), status=result[2])
+ self._sendPacket(login)
+ return True
+
+ def _onLoginFailed(self, failure, *args, **kwargs):
+ print 'Login failed.'
+ failure.printTraceback()
+ self.user_profile.onLoginFailure(failure)
+ return failure
+
+ def _onInvalidCreditials(self, failure, *args, **kwargs):
+ print "User didn't provide necessary info"
+ failure.printTraceback()
+ self._onLoginFailed(failure, *args, **kawrgs)
+
+ def _handleLoginOKPacket(self, msg):
+ print 'Login almost done - send the notify list.'
+ self.__pingThread = task.LoopingCall(self.sendPing)
+ self.__pingThread.start(180.0)
+ self.loginSuccess.callback(self)
+
+ def _handleLoginFailedPacket(self, msg):
+ print 'Server sent - login failed'
+ self.loginSuccess.errback(None)
+
+ def _handleStatusUpdatePacket(self, msg):
+ self.user_profile._updateContact(msg.contact)
+
+ def _handleStatusNoticiesPacket(self, msg):
+ print 'Server sent - noticies packet'
+ self.user_profile.onStatusNoticiesRecv()
+ for struct in msg.contacts:
+ self.user_profile._updateContact(struct)
+
+ def _handleMessageInPacket(self, msg):
+ self.user_profile.onMessageReceived(msg)
+
+ def _handleMessageAckPacket(self, msg):
+ print "MSG_Status=%x, recipient=%d, seq=%d" % (msg.msg_status, msg.recipient, msg.seq)
+
+ def _handleDisconnectPacket(self, msg):
+ print 'Server sent - disconnect packet'
+ Protocol.connectionLost(self, None)
+ #self.loseConnection()
+
+ def _sendAllContacts(self, result, *args, **kwargs):
+ contacts = list( self.user_profile.contacts )
+
+ if len(contacts) == 0:
+ self._sendPacket( Resolver.by_name('NoNoticesPacket')() )
+ return self
+
+ nl_class = Resolver.by_name('NoticeLastPacket')
+ nf_class = Resolver.by_name('NoticeFirstPacket')
+
+ while len(contacts) > 400:
+ batch, contacts = constacts[:400], contacts[400:]
+ self._sendPacket( nf_class(contacts= \
+ [StructNotice(uin=uin) for (uin, _) in batch]) )
+
+# self._sendPacket( nl_class(contacts= \
+# [StructNotice(uin=uin) for (uin, _) in contacts]) )
+ self._sendPacket( nl_class(contacts= \
+ [StructNotice(uin=contacts[i].uin) for i in range(len(contacts))]) )
+ self._log("Sent all contacts.")
+ return self
+
+ def sendPing(self):
+ self._sendPacket( Resolver.by_name('PingPacket')() )
+
+ def sendHTMLMessage(self, rcpt, html_text, plain_message):
+ klass = Resolver.by_name('MessageOutPacket')
+
+ attrs = StructMsgAttrs()
+ attrs.richtext = StructRichText()
+
+ payload = StructMessage(klass=StructMessage.CLASS.CHAT, \
+ html_message=html_text, plain_message=plain_message, \
+ attrs = attrs)
+
+ self._sendPacket( klass( recipient=rcpt, seq=int(time.time()), content=payload) )
+
+ def sendImportRequest(self, callback):
+ if self.importrq_cb is not None:
+ raise RuntimeError("There can be only one import request pending.")
+
+ self.importrq_cb = Deferred()
+ self.importrq_cb.addCallbacks(lambda result, *args, **kwargs: callback(result), self._log_failure)
+ self.importrq_cb.addErrback(self._log_failure)
+ self.import_buf = ''
+
+ klass = Resolver.by_name('ULRequestPacket')
+ self._sendPacket( klass(type=klass.TYPE.GET, data='') )
+
+ def addNewContact(self, contact):
+ add_class = Resolver.by_name('AddNoticePacket')
+
+ self._sendPacket( add_class(contact= \
+ StructNotice(uin=contact.uin)) )
+ self._log("New contact %s added." % contact.uin)
+ return self
+
+ def changeStatus(self, status, desc=''):
+ change_status_class = Resolver.by_name('ChangeStatusPacket')
+
+ print ChangeStatusPacket.STATUS.FFC
+
+ if status == 'NOT_AVAILABLE':
+ if desc == '' or desc == None:
+ gg_status = change_status_class.STATUS.NOT_AVAILABLE
+ else:
+ gg_status = change_status_class.STATUS.NOT_AVAILABLE_DESC
+ elif status == 'FFC':
+ if desc == '' or desc == None:
+ gg_status = change_status_class.STATUS.FFC
+ else:
+ gg_status = change_status_class.STATUS.FFC_DESC
+ elif status == 'AVAILABLE':
+ if desc == '' or desc == None:
+ gg_status = change_status_class.STATUS.AVAILABLE
+ else:
+ gg_status = change_status_class.STATUS.AVAILABLE_DESC
+ elif status == 'BUSY':
+ if desc == '' or desc == None:
+ gg_status = change_status_class.STATUS.BUSY
+ else:
+ gg_status = change_status_class.STATUS.BUSY_DESC
+ elif status == 'DND':
+ if desc == '' or desc == None:
+ gg_status = change_status_class.STATUS.DND
+ else:
+ gg_status = change_status_class.STATUS.DND_DESC
+ elif status == 'HIDDEN':
+ if desc == '' or desc == None:
+ gg_status = change_status_class.STATUS.HIDDEN
+ else:
+ gg_status = change_status_class.STATUS.HIDDEN_DESC
+ else:
+ gg_status = change_status_class.STATUS.NOT_AVAILABLE
+
+ if desc == '' or desc == None:
+ self._sendPacket( change_status_class(status=gg_status, flags=0x00000001) )
+ else:
+ desc_len = len(desc)
+ self._sendPacket( change_status_class(status=gg_status, flags=0x00000001, description_size=desc_len, description=desc) )
+ self._log("Status changed")
+ return True
+
+ def _handleULReplyPacket(self, msg):
+ if msg.is_get:
+ if not self.importrq_cb:
+ self._warn("Unexpected UL_GET reply")
+ return
+
+ self.import_buf += msg.data
+
+ if msg.is_final:
+ cb = self.importrq_cb
+ self.importrq_cb = None
+ cb.callback(self.import_buf)
+ else:
+ self._log("UL_PUT reply")
+
+ #
+ # High-level interface callbacks
+ #
+
+ def _warn(self, obj):
+ tlog.warning( str(obj) )
+
+ def _log(self, obj):
+ tlog.msg( str(obj) )
+
+ def _log_failure(self, failure, *args, **kwargs):
+ print "Failure:"
+ failure.printTraceback()
diff --git a/sunshine/lqsoft/utils/Makefile.am b/sunshine/lqsoft/utils/Makefile.am
new file mode 100644
index 0000000..2224cc4
--- /dev/null
+++ b/sunshine/lqsoft/utils/Makefile.am
@@ -0,0 +1,2 @@
+utilsdir = $(pythondir)/sunshine/lqsoft/utils
+utils_PYTHON = __init__.py
diff --git a/sunshine/lqsoft/utils/__init__.py b/sunshine/lqsoft/utils/__init__.py
new file mode 100755
index 0000000..47817ea
--- /dev/null
+++ b/sunshine/lqsoft/utils/__init__.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8
+
+class Enum(object):
+ def __init__(self, kwargs):
+ self.__dict__['__dict'] = kwargs
+ self.__dict__['__reverse_dict'] = {}
+ for (k,v) in kwargs.iteritems():
+ self.__dict__['__reverse_dict'][v] = k
+
+ def key_for(self, name):
+ return self.__reverse_dict[name]
+
+ def __getattr__(self, name):
+ try:
+ return self.__dict__['__dict'][name]
+ except KeyError:
+ raise AttributeError('No attribute named ' + name)
+
+ def __setattr__(self, name, value):
+ raise AttributeError('This class in not mutable')
+
+
+def reverse_dict(mapping):
+ d = {}
+ for (k,v) in mapping.iteritems():
+ d[v] = k
+ return mapping, d \ No newline at end of file
diff --git a/sunshine/presence.py b/sunshine/presence.py
new file mode 100644
index 0000000..9630952
--- /dev/null
+++ b/sunshine/presence.py
@@ -0,0 +1,335 @@
+# telepathy-sunshine is the GaduGadu connection manager for Telepathy
+#
+# Copyright (C) 2006-2007 Ali Sabil <ali.sabil@gmail.com>
+# Copyright (C) 2010 Krzysztof Klinikowski <kkszysiu@gmail.com>
+#
+# 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
+
+# Implementation of the SimplePresence specification at :
+# http://telepathy.freedesktop.org/spec.html#org.freedesktop.Telepathy.Connection.Interface.SimplePresence
+
+import logging
+import time
+
+import dbus
+import telepathy
+import telepathy.constants
+import telepathy.errors
+
+from sunshine.handle import SunshineHandleFactory
+from sunshine.util.decorator import async
+
+__all__ = ['SunshinePresence']
+
+logger = logging.getLogger('Sunshine.Presence')
+
+
+class SunshinePresenceMapping(object):
+ ONLINE = 'available'
+ FFC = 'free_for_chat'
+ BUSY = 'busy'
+ IDLE = 'dnd'
+ INVISIBLE = 'hidden'
+ OFFLINE = 'offline'
+
+ to_gg = {
+ ONLINE: 'AVAILABLE',
+ BUSY: 'BUSY',
+ IDLE: 'DND',
+ INVISIBLE: 'HIDDEN',
+ OFFLINE: 'NOT_AVAILABLE'
+ }
+
+ to_telepathy = {
+ 'AVAILABLE': ONLINE,
+ 'FFC': FFC,
+ 'BUSY': BUSY,
+ 'DND': IDLE,
+ 'HIDDEN': INVISIBLE,
+ 'NOT_AVAILABLE': OFFLINE
+ }
+
+ from_gg_to_tp = {
+ # 'NOT_AVAILABLE': 0x0001,
+ # 'NOT_AVAILABLE_DESC': 0x0015,
+ # 'FFC': 0x0017,
+ # 'FFC_DESC': 0x0018,
+ # 'AVAILABLE': 0x0002,
+ # 'AVAILABLE_DESC': 0x0004,
+ # 'BUSY': 0x0003,
+ # 'BUSY_DESC': 0x0005,
+ # 'DND': 0x0021,
+ # 'DND_DESC': 0x0022,
+ # 'HIDDEN': 0x0014,
+ # 'HIDDEN_DESC': 0x0016,
+ # 'DND': 0x0021,
+ # 'BLOCKED': 0x0006,
+ # 'MASK_FRIEND': 0x8000,
+ # 'MASK_GFX': 0x0100,
+ # 'MASK_STATUS': 0x4000,
+ 0: OFFLINE,
+ 0x0001: OFFLINE,
+ 0x4015: OFFLINE,
+ 0x0017: ONLINE,
+ 0x4018: ONLINE,
+ 0x0002: ONLINE,
+ 0x4004: ONLINE,
+ 0x0003: BUSY,
+ 0x4005: BUSY,
+ 0x0021: IDLE,
+ 0x4022: IDLE,
+ 0x0014: INVISIBLE,
+ 0x4016: INVISIBLE,
+ #Opisy graficzne
+ #Z tego co mi sie wydaje one zawsze maja maske "z opisem"
+ 0x4115: OFFLINE,
+ 0x4118: ONLINE,
+ 0x4104: ONLINE,
+ 0x4105: BUSY,
+ 0x4122: IDLE,
+ 0x4116: INVISIBLE
+ }
+
+ to_presence_type = {
+ ONLINE: telepathy.constants.CONNECTION_PRESENCE_TYPE_AVAILABLE,
+ FFC: telepathy.constants.CONNECTION_PRESENCE_TYPE_AVAILABLE,
+ BUSY: telepathy.constants.CONNECTION_PRESENCE_TYPE_BUSY,
+ IDLE: telepathy.constants.CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY,
+ INVISIBLE: telepathy.constants.CONNECTION_PRESENCE_TYPE_HIDDEN,
+ OFFLINE: telepathy.constants.CONNECTION_PRESENCE_TYPE_OFFLINE
+ }
+
+class SunshinePresence(telepathy.server.ConnectionInterfacePresence,
+ telepathy.server.ConnectionInterfaceSimplePresence):
+
+ def __init__(self):
+ telepathy.server.ConnectionInterfacePresence.__init__(self)
+ telepathy.server.ConnectionInterfaceSimplePresence.__init__(self)
+
+ dbus_interface = 'org.freedesktop.Telepathy.Connection.Interface.SimplePresence'
+
+ self._implement_property_get(dbus_interface, {'Statuses' : self.get_statuses})
+
+
+ def GetStatuses(self):
+ # the arguments are in common to all on-line presences
+ arguments = {'message' : 's'}
+
+ # you get one of these for each status
+ # {name:(type, self, exclusive, {argument:types}}
+ return {
+ SunshinePresenceMapping.ONLINE:(
+ telepathy.CONNECTION_PRESENCE_TYPE_AVAILABLE,
+ True, True, arguments),
+ SunshinePresenceMapping.BUSY:(
+ telepathy.CONNECTION_PRESENCE_TYPE_BUSY,
+ True, True, arguments),
+ SunshinePresenceMapping.IDLE:(
+ telepathy.CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY,
+ True, True, arguments),
+ SunshinePresenceMapping.INVISIBLE:(
+ telepathy.CONNECTION_PRESENCE_TYPE_HIDDEN,
+ True, True, {}),
+ SunshinePresenceMapping.OFFLINE:(
+ telepathy.CONNECTION_PRESENCE_TYPE_OFFLINE,
+ True, True, {})
+ }
+
+ def RequestPresence(self, contacts):
+ presences = self.get_presences(contacts)
+ self.PresenceUpdate(presences)
+
+ def GetPresence(self, contacts):
+ return self.get_presences(contacts)
+
+ def SetStatus(self, statuses):
+ status, arguments = statuses.items()[0]
+ if status == SunshinePresenceMapping.OFFLINE:
+ self.Disconnect()
+
+ presence = SunshinePresenceMapping.to_gg[status]
+ message = arguments.get('message', u'')
+
+ logger.info("Setting Presence to '%s'" % presence)
+ logger.info("Setting Personal message to '%s'" % message)
+
+ message = message.encode('UTF-8')
+
+ if self._status == telepathy.CONNECTION_STATUS_CONNECTED:
+ self._self_presence_changed(SunshineHandleFactory(self, 'self'), presence, message)
+ self.profile.setMyState(presence, message)
+ else:
+ self._self_presence_changed(SunshineHandleFactory(self, 'self'), presence, message)
+
+ def get_presences(self, contacts):
+ presences = {}
+ for handle_id in contacts:
+ handle = self.handle(telepathy.HANDLE_TYPE_CONTACT, handle_id)
+# try:
+# contact = handle.contact
+# except AttributeError:
+# contact = handle.profile
+#
+# if contact is not None:
+# presence = ButterflyPresenceMapping.to_telepathy[contact.presence]
+# personal_message = unicode(contact.personal_message, "utf-8")
+# else:
+# presence = SunshinePresenceMapping.OFFLINE
+# personal_message = u""
+ try:
+ contact = handle.profile
+ presence = SunshinePresenceMapping.OFFLINE
+ personal_message = u""
+ except AttributeError:
+ #I dont know what to do here. Do I really need this? :P
+ contact = handle.contact
+ if contact is not None:
+ presence = SunshinePresenceMapping.to_telepathy[contact.status]
+ #thats bad, mkay?
+ personal_message = unicode(contact.get_desc(), "utf-8")
+ else:
+ presence = SunshinePresenceMapping.OFFLINE
+ personal_message = u""
+
+ arguments = {}
+ if personal_message:
+ arguments = {'message' : personal_message}
+
+ presences[handle] = (0, {presence : arguments}) # TODO: Timestamp
+ return presences
+
+
+ # SimplePresence
+
+ def GetPresences(self, contacts):
+ return self.get_simple_presences(contacts)
+
+ def SetPresence(self, status, message):
+ if status == SunshinePresenceMapping.OFFLINE:
+ self.Disconnect()
+
+ try:
+ presence = SunshinePresenceMapping.to_gg[status]
+ except KeyError:
+ raise telepathy.errors.InvalidArgument
+
+ logger.info("Setting Presence to '%s'" % presence)
+ logger.info("Setting Personal message to '%s'" % message)
+
+ message = message.encode('UTF-8')
+
+ if self._status == telepathy.CONNECTION_STATUS_CONNECTED:
+ self._self_presence_changed(SunshineHandleFactory(self, 'self'), presence, message)
+ self.profile.setMyState(presence, message)
+ else:
+ self._self_presence_changed(SunshineHandleFactory(self, 'self'), presence, message)
+
+ def get_simple_presences(self, contacts):
+ presences = {}
+ for handle_id in contacts:
+ handle = self.handle(telepathy.HANDLE_TYPE_CONTACT, handle_id)
+ if handle == SunshineHandleFactory(self, 'self'):
+ contact = handle.profile
+ presence = SunshinePresenceMapping.OFFLINE
+ personal_message = u""
+ else:
+ #I dont know what to do here. Do I really need this? :P
+ contact = handle.contact
+ if contact is not None:
+ presence = SunshinePresenceMapping.from_gg_to_tp[contact.status]
+ #that bad, I dont know but when I set desc there I didnt get any contacts :/
+ #personal_message = str(contact.get_desc())
+ personal_message = str('')
+ else:
+ presence = SunshinePresenceMapping.OFFLINE
+ personal_message = u""
+
+ presence_type = SunshinePresenceMapping.to_presence_type[presence]
+
+ presences[handle] = (presence_type, presence, personal_message)
+ return presences
+
+ def get_statuses(self):
+ # you get one of these for each status
+ # {name:(Type, May_Set_On_Self, Can_Have_Message}
+ return dbus.Dictionary({
+ SunshinePresenceMapping.ONLINE:(
+ telepathy.CONNECTION_PRESENCE_TYPE_AVAILABLE,
+ True, True),
+ SunshinePresenceMapping.BUSY:(
+ telepathy.CONNECTION_PRESENCE_TYPE_BUSY,
+ True, True),
+ SunshinePresenceMapping.IDLE:(
+ telepathy.CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY,
+ True, True),
+ SunshinePresenceMapping.INVISIBLE:(
+ telepathy.CONNECTION_PRESENCE_TYPE_HIDDEN,
+ True, True),
+ SunshinePresenceMapping.OFFLINE:(
+ telepathy.CONNECTION_PRESENCE_TYPE_OFFLINE,
+ True, True)
+ }, signature='s(ubb)')
+
+# # papyon.event.ContactEventInterface
+# def on_contact_presence_changed(self, contact):
+# handle = ButterflyHandleFactory(self, 'contact',
+# contact.account, contact.network_id)
+# logger.info("Contact %s presence changed to '%s'" % (unicode(handle),
+# contact.presence))
+# self._presence_changed(handle, contact.presence, contact.personal_message)
+#
+# # papyon.event.ContactEventInterface
+# on_contact_personal_message_changed = on_contact_presence_changed
+#
+# # papyon.event.ProfileEventInterface
+# def on_profile_presence_changed(self):
+# profile = self.msn_client.profile
+# self._presence_changed(ButterflyHandleFactory(self, 'self'),
+# profile.presence, profile.personal_message)
+#
+# # papyon.event.ProfileEventInterface
+# on_profile_personal_message_changed = on_profile_presence_changed
+
+ @async
+ def _presence_changed(self, handle, presence, personal_message):
+ try:
+ presence = SunshinePresenceMapping.from_gg_to_tp[presence]
+ except KeyError:
+ presence = SunshinePresenceMapping.from_gg_to_tp[0]
+ presence_type = SunshinePresenceMapping.to_presence_type[presence]
+ personal_message = unicode(str(personal_message), "utf-8")
+
+ self.PresencesChanged({handle: (presence_type, presence, personal_message)})
+
+ arguments = {}
+ if personal_message:
+ arguments = {'message' : personal_message}
+
+ self.PresenceUpdate({handle: (int(time.time()), {presence:arguments})})
+
+ @async
+ def _self_presence_changed(self, handle, presence, personal_message):
+
+ presence = SunshinePresenceMapping.to_telepathy[presence]
+ presence_type = SunshinePresenceMapping.to_presence_type[presence]
+ personal_message = unicode(str(personal_message), "utf-8")
+
+ self.PresencesChanged({handle: (presence_type, presence, personal_message)})
+
+ arguments = {}
+ if personal_message:
+ arguments = {'message' : personal_message}
+
+ self.PresenceUpdate({handle: (int(time.time()), {presence:arguments})})
diff --git a/sunshine/util/Makefile.am b/sunshine/util/Makefile.am
new file mode 100644
index 0000000..f4cd81a
--- /dev/null
+++ b/sunshine/util/Makefile.am
@@ -0,0 +1,3 @@
+utildir = $(pythondir)/sunshine/util
+util_PYTHON = decorator.py \
+ __init__.py
diff --git a/sunshine/util/__init__.py b/sunshine/util/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/sunshine/util/__init__.py
diff --git a/sunshine/util/decorator.py b/sunshine/util/decorator.py
new file mode 100644
index 0000000..250e2d2
--- /dev/null
+++ b/sunshine/util/decorator.py
@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Ali Sabil <ali.sabil@gmail.com>
+#
+# 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
+#
+
+"""Useful decorators"""
+
+import sys
+import warnings
+import time
+
+import gobject
+
+__all__ = ['decorator', 'rw_property', 'deprecated', 'unstable', 'async',
+ 'throttled']
+
+
+def decorator(function):
+ """decorator to be used on decorators, it preserves the docstring and
+ function attributes of functions to which it is applied."""
+ def new_decorator(f):
+ g = function(f)
+ g.__name__ = f.__name__
+ g.__doc__ = f.__doc__
+ g.__dict__.update(f.__dict__)
+ return g
+ new_decorator.__name__ = function.__name__
+ new_decorator.__doc__ = function.__doc__
+ new_decorator.__dict__.update(function.__dict__)
+ return new_decorator
+
+
+def rw_property(function):
+ """This decorator implements read/write properties, as follow:
+
+ @rw_property
+ def my_property():
+ "Documentation"
+ def fget(self):
+ return self._my_property
+ def fset(self, value):
+ self._my_property = value
+ return locals()
+ """
+ return property(**function())
+
+@decorator
+def deprecated(func):
+ """This is a decorator which can be used to mark functions as deprecated.
+ It will result in a warning being emitted when the function is used."""
+ def new_function(*args, **kwargs):
+ warnings.warn("Call to deprecated function %s." % func.__name__,
+ category=DeprecationWarning)
+ return func(*args, **kwargs)
+ return new_function
+
+@decorator
+def unstable(func):
+ """This is a decorator which can be used to mark functions as unstable API
+ wise. It will result in a warning being emitted when the function is used."""
+ def new_function(*args, **kwargs):
+ warnings.warn("Call to unstable API function %s." % func.__name__,
+ category=FutureWarning)
+ return func(*args, **kwargs)
+ return new_function
+
+@decorator
+def async(func):
+ """Make a function mainloop friendly. the function will be called at the
+ next mainloop idle state."""
+ def new_function(*args, **kwargs):
+ def async_function():
+ func(*args, **kwargs)
+ return False
+ gobject.idle_add(async_function)
+ return new_function
+
+class throttled(object):
+ """Throttle the calls to a function by queueing all the calls that happen
+ before the minimum delay."""
+
+ def __init__(self, min_delay, queue):
+ self._min_delay = min_delay
+ self._queue = queue
+ self._last_call_time = None
+
+ def __call__(self, func):
+ def process_queue():
+ if len(self._queue) != 0:
+ func, args, kwargs = self._queue.pop(0)
+ self._last_call_time = time.time() * 1000
+ func(*args, **kwargs)
+ return False
+
+ def new_function(*args, **kwargs):
+ now = time.time() * 1000
+ if self._last_call_time is None or \
+ now - self._last_call_time >= self._min_delay:
+ self._last_call_time = now
+ func(*args, **kwargs)
+ else:
+ self._queue.append((func, args, kwargs))
+ last_call_delta = now - self._last_call_time
+ process_queue_timeout = int(self._min_delay * len(self._queue) - last_call_delta)
+ gobject.timeout_add(process_queue_timeout, process_queue)
+
+ new_function.__name__ = func.__name__
+ new_function.__doc__ = func.__doc__
+ new_function.__dict__.update(func.__dict__)
+ return new_function
+
+