From: Florian Zeitz <florob@babelmonkeys.de>
Date: Wed, 5 Jun 2013 22:51:50 +0000 (+0200)
Subject: Add User Avatar support
X-Git-Url: https://git.babelmonkeys.de/?a=commitdiff_plain;h=fa3c174db5c04176d891b91f68491e6ac8e22c22;p=jubjub.git

Add User Avatar support
---

diff --git a/data/gtk/roster.ui b/data/gtk/roster.ui
index cadb855..a2dc350 100644
--- a/data/gtk/roster.ui
+++ b/data/gtk/roster.ui
@@ -45,6 +45,8 @@
       <column type="gchararray"/>
       <!-- column-name status -->
       <column type="gchararray"/>
+      <!-- column-name avatar -->
+      <column type="GdkPixbuf"/>
     </columns>
   </object>
   <object class="GtkTreeModelFilter" id="RosterTreeModelFilter">
@@ -255,6 +257,17 @@
                     </child>
                   </object>
                 </child>
+                <child>
+                  <object class="GtkTreeViewColumn" id="RosterTreeViewColumn3">
+                    <property name="title" translatable="yes">Avatar</property>
+                    <child>
+                      <object class="GtkCellRendererPixbuf" id="cellrendererpixbuf1"/>
+                      <attributes>
+                        <attribute name="pixbuf">4</attribute>
+                      </attributes>
+                    </child>
+                  </object>
+                </child>
               </object>
             </child>
           </object>
diff --git a/src/core/JubAvatarManager.h b/src/core/JubAvatarManager.h
new file mode 100644
index 0000000..9565f39
--- /dev/null
+++ b/src/core/JubAvatarManager.h
@@ -0,0 +1,23 @@
+#import <ObjFW/ObjFW.h>
+#import <ObjXMPP/ObjXMPP.h>
+
+@class JubChatClient;
+@class XMPPContact;
+
+@protocol JubAvatarManagerDelegate
+- (void)contact: (XMPPContact*)contact
+   didSetAvatar: (OFString*)avatarFile;
+@end
+
+@interface JubAvatarManager : OFObject <XMPPConnectionDelegate>
+{
+	JubChatClient *_client;
+	id<JubAvatarManagerDelegate> _delegate;
+	OFMutableString *cachePath;
+}
+@property (assign) id<JubAvatarManagerDelegate> delegate;
+
+- initWithClient: (JubChatClient*)client;
+- (void)Jub_connection: (XMPPConnection*)connection
+      receivedAvatarIQ: (XMPPIQ*)IQ;
+@end
diff --git a/src/core/JubAvatarManager.m b/src/core/JubAvatarManager.m
new file mode 100644
index 0000000..7d2fe8f
--- /dev/null
+++ b/src/core/JubAvatarManager.m
@@ -0,0 +1,180 @@
+#import "JubAvatarManager.h"
+#import "JubChatClient.h"
+
+#define JUB_NS_PUBSUB @"http://jabber.org/protocol/pubsub"
+#define JUB_NS_PUBSUB_EVENT @"http://jabber.org/protocol/pubsub#event"
+#define JUB_NS_AVATAR_DATA @"urn:xmpp:avatar:data"
+#define JUB_NS_AVATAR_METADATA @"urn:xmpp:avatar:metadata"
+
+@implementation JubAvatarManager
+@synthesize delegate = _delegate;
+
+- initWithClient: (JubChatClient*)client
+{
+	self = [super init];
+
+	@try {
+		_client = client;
+		[_client.discoEntity
+		    addFeature: JUB_NS_AVATAR_METADATA @"+notify"];
+		[_client.connection addDelegate: self];
+
+		// Determine cache path
+		OFDictionary *env = [OFApplication environment];
+		OFString * xdgCache = env[@"XDG_CACHE_HOME"];
+
+		cachePath = [OFMutableString new];
+		if (xdgCache == nil) {
+			OFString *home = env[@"HOME"];
+			if (home == nil)
+				[cachePath appendString: @"/tmp"];
+			else
+				[cachePath appendString: home];
+			[cachePath appendString: @"/.cache"];
+		} else
+			[cachePath appendString: xdgCache];
+
+		[cachePath appendString: @"/jubjub"];
+	} @catch (id e) {
+		[self release];
+		@throw e;
+	}
+
+	return self;
+}
+
+- (void)dealloc
+{
+	// TODO: Remove feature
+	[cachePath release];
+	[_client.connection removeDelegate: self];
+	[super dealloc];
+}
+
+- (void)connection: (XMPPConnection*)connection
+      didReceiveMessage: (XMPPMessage*)message
+{
+	OFXMLElement *event = [message elementForName: @"event"
+					    namespace: JUB_NS_PUBSUB_EVENT];
+	if (event == nil)
+		return;
+
+	OFXMLElement *items = [event elementForName: @"items"
+					  namespace: JUB_NS_PUBSUB_EVENT];
+	if (items == nil ||
+	    ![[[items attributeForName: @"node"] stringValue]
+	      isEqual: JUB_NS_AVATAR_METADATA])
+		return;
+
+	OFXMLElement *item = [items elementForName: @"item"
+					 namespace: JUB_NS_PUBSUB_EVENT];
+	if (item == nil)
+		return;
+
+	OFString *ID = [[item attributeForName: @"id"] stringValue];
+	if (ID == nil)
+		return;
+
+	OFXMLElement *metadata = [item elementForName: @"metadata"
+					    namespace: JUB_NS_AVATAR_METADATA];
+	if (metadata == nil)
+		return;
+
+	OFXMLElement *info = [metadata elementForName: @"info"
+					    namespace: JUB_NS_AVATAR_METADATA];
+	if (info == nil)
+		return;
+
+	OFString *avatarPath =
+	    [cachePath stringByAppendingFormat: @"/%@.png", ID];
+	if ([OFFile fileExistsAtPath: avatarPath]) {
+		OFString *contactJID = [message.from bareJID];
+		XMPPContact *contact =
+		    [_client.contactManager.contacts objectForKey: contactJID];
+		if (contact == nil) // Avatar for unknown contact
+			return;
+		[_delegate contact: contact
+		      didSetAvatar: avatarPath];
+		return;
+	}
+
+	XMPPIQ *queryIQ =
+	    [XMPPIQ IQWithType: @"get"
+			    ID: [_client.connection generateStanzaID]];
+	queryIQ.to = message.from;
+
+	OFXMLElement *pubsub = [OFXMLElement elementWithName: @"pubsub"
+						   namespace: JUB_NS_PUBSUB];
+	[queryIQ addChild: pubsub];
+
+	OFXMLElement *queryItems =
+	    [OFXMLElement elementWithName: @"items"
+				namespace: JUB_NS_PUBSUB];
+	[queryItems addAttributeWithName: @"node"
+			     stringValue: JUB_NS_AVATAR_DATA];
+	[pubsub addChild: queryItems];
+
+	OFXMLElement *queryItem =
+	    [OFXMLElement elementWithName: @"item"
+				namespace: JUB_NS_PUBSUB];
+	[queryItem addAttributeWithName: @"id"
+			    stringValue: ID];
+	[queryItems addChild: queryItem];
+
+	[_client.connection sendIQ: queryIQ
+		    callbackTarget: self
+			  selector: @selector(Jub_connection:
+					    receivedAvatarIQ:)];
+}
+
+- (void)Jub_connection: (XMPPConnection*)connection
+      receivedAvatarIQ: (XMPPIQ*)IQ
+{
+	OFXMLElement *pubsub = [IQ elementForName: @"pubsub"
+					namespace: JUB_NS_PUBSUB];
+	if (pubsub == nil)
+		return;
+
+	OFXMLElement *items = [pubsub elementForName: @"items"
+					   namespace: JUB_NS_PUBSUB];
+	if (items == nil ||
+	    ![[[items attributeForName: @"node"] stringValue]
+	      isEqual: JUB_NS_AVATAR_DATA])
+		return;
+
+	OFXMLElement *item = [items elementForName: @"item"
+					 namespace: JUB_NS_PUBSUB];
+	if (item == nil)
+		return;
+
+	OFString *ID = [[item attributeForName: @"id"] stringValue];
+	if (ID == nil)
+		return;
+
+	OFXMLElement *data = [item elementForName: @"data"
+					namespace: JUB_NS_AVATAR_DATA];
+	if (data == nil)
+		return;
+
+	OFDataArray *avatar =
+	    [OFDataArray dataArrayWithBase64EncodedString: [data stringValue]];
+
+	if (![OFFile directoryExistsAtPath: cachePath])
+		[OFFile createDirectoryAtPath: cachePath
+				createParents: true];
+
+	OFString *filename =
+	    [cachePath stringByAppendingFormat: @"/%@.png", ID];
+	[avatar writeToFile: filename];
+
+	OFString *contactJID = [IQ.from bareJID];
+
+	XMPPContact *contact =
+	    [_client.contactManager.contacts objectForKey: contactJID];
+	if (contact == nil) // Avatar for unknown contact
+		return;
+
+	[_delegate contact: contact
+	      didSetAvatar: filename];
+}
+@end
diff --git a/src/core/JubChatClient.h b/src/core/JubChatClient.h
index 57cfd0b..168fa69 100644
--- a/src/core/JubChatClient.h
+++ b/src/core/JubChatClient.h
@@ -5,6 +5,8 @@
 #import "JubChatUI.h"
 #import "JubConfig.h"
 
+@class JubAvatarManager;
+
 @interface JubChatClient : OFObject
     <XMPPConnectionDelegate, XMPPRosterDelegate, XMPPContactManagerDelegate>
 {
@@ -12,6 +14,7 @@
 	XMPPConnection *_connection;
 	XMPPRoster *_roster;
 	XMPPStreamManagement *_streamManagement;
+	JubAvatarManager *_avatarManager;
 	XMPPContactManager *_contactManager;
 	XMPPDiscoEntity *_discoEntity;
 	XMPPPresence *_presence;
@@ -19,6 +22,7 @@
 }
 @property (readonly) XMPPConnection *connection;
 @property (readonly) XMPPRoster *roster;
+@property (readonly) JubAvatarManager *avatarManager;
 @property (readonly) XMPPContactManager *contactManager;
 @property (readonly) XMPPDiscoEntity *discoEntity;
 @property (readonly) XMPPPresence *presence;
diff --git a/src/core/JubChatClient.m b/src/core/JubChatClient.m
index 1d7874a..3184b89 100644
--- a/src/core/JubChatClient.m
+++ b/src/core/JubChatClient.m
@@ -1,11 +1,14 @@
 #import "JubChatClient.h"
 #import "ObjXMPP/namespaces.h"
 
+#import "JubAvatarManager.h"
+
 #define JUB_CLIENT_URI @"http://babelmonkeys.de/jubjub"
 
 @implementation JubChatClient
 @synthesize connection = _connection;
 @synthesize roster = _roster;
+@synthesize avatarManager = _avatarManager;
 @synthesize contactManager = _contactManager;
 @synthesize discoEntity = _discoEntity;
 @synthesize presence = _presence;
@@ -28,6 +31,20 @@
 		_roster = [[XMPPRoster alloc] initWithConnection: _connection];
 		[_roster addDelegate: self];
 
+		_discoEntity =
+		    [[XMPPDiscoEntity alloc] initWithConnection: _connection
+						       capsNode: JUB_CLIENT_URI];
+
+		XMPPDiscoIdentity *identity =
+		    [XMPPDiscoIdentity identityWithCategory: @"client"
+						       type: @"pc"
+						       name: @"JubJub"];
+		[_discoEntity addIdentity: identity];
+		[_discoEntity addFeature: XMPP_NS_CAPS];
+
+		_avatarManager =
+		    [[JubAvatarManager alloc] initWithClient: self];
+
 		_contactManager = [[XMPPContactManager alloc]
 		    initWithConnection: _connection
 				roster: _roster];
@@ -53,6 +70,7 @@
 	[_contactManager release];
 	[_discoEntity release];
 	[_streamManagement release];
+	[_avatarManager release];
 	[_connection release];
 	[_presence release];
 	[_chatMap release];
@@ -133,19 +151,6 @@
 - (void)connection: (XMPPConnection*)connection
      wasBoundToJID: (XMPPJID*)jid
 {
-	of_log(@"Bound to JID: %@", [jid fullJID]);
-
-	_discoEntity =
-	    [[XMPPDiscoEntity alloc] initWithConnection: connection
-					       capsNode: JUB_CLIENT_URI];
-
-	XMPPDiscoIdentity *identity =
-	    [XMPPDiscoIdentity identityWithCategory: @"client"
-					       type: @"pc"
-					       name: @"JubJub"];
-	[_discoEntity addIdentity: identity];
-	[_discoEntity addFeature: XMPP_NS_CAPS];
-
 	[_roster requestRoster];
 }
 
diff --git a/src/core/Makefile b/src/core/Makefile
index 9b1d303..234aac6 100644
--- a/src/core/Makefile
+++ b/src/core/Makefile
@@ -1,6 +1,7 @@
 STATIC_LIB_NOINST = core.a
-SRCS = main.m		\
-       JubChatClient.m	\
+SRCS = main.m			\
+       JubAvatarManager.m 	\
+       JubChatClient.m		\
        JubConfig.m
 
 include ../../buildsys.mk
diff --git a/src/core/main.m b/src/core/main.m
index fda8c5f..6de6785 100644
--- a/src/core/main.m
+++ b/src/core/main.m
@@ -36,9 +36,8 @@ OF_APPLICATION_DELEGATE(AppDelegate)
 	_client.ui = _ui;
 	[_client.connection addDelegate: self];
 
-	[_client.connection asyncConnectAndHandle];
-
 	[_ui startUIThread];
+	[_client.connection asyncConnectAndHandle];
 }
 
 -  (void)connection: (XMPPConnection*)connection
diff --git a/src/gui/gtk/JubGtkRosterUI.h b/src/gui/gtk/JubGtkRosterUI.h
index 40214f7..9769c76 100644
--- a/src/gui/gtk/JubGtkRosterUI.h
+++ b/src/gui/gtk/JubGtkRosterUI.h
@@ -3,10 +3,12 @@
 #include <gtk/gtk.h>
 
 #import "JubChatClient.h"
+#import "JubAvatarManager.h"
 
 @class JubGtkChatUI;
 
-@interface JubGtkRosterUI: OFObject <XMPPContactManagerDelegate>
+@interface JubGtkRosterUI:
+	OFObject <XMPPContactManagerDelegate, JubAvatarManagerDelegate>
 {
 	GtkWidget *_roster_window;
 	GtkTreeStore *_roster_model;
diff --git a/src/gui/gtk/JubGtkRosterUI.m b/src/gui/gtk/JubGtkRosterUI.m
index a805adf..667760b 100644
--- a/src/gui/gtk/JubGtkRosterUI.m
+++ b/src/gui/gtk/JubGtkRosterUI.m
@@ -1,5 +1,6 @@
 #import <ObjXMPP/namespaces.h>
 #include <string.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
 
 #import "JubGtkRosterUI.h"
 #import "JubGObjectMap.h"
@@ -78,6 +79,7 @@ static gboolean filter_roster_by_presence(GtkTreeModel *model,
 		_client = [client retain];
 
 		[_client.contactManager addDelegate: self];
+		[_client.avatarManager setDelegate: self];
 
 		builder = gtk_builder_new();
 		gtk_builder_add_from_file(builder, "data/gtk/roster.ui", NULL);
@@ -121,6 +123,7 @@ static gboolean filter_roster_by_presence(GtkTreeModel *model,
 
 - (void)dealloc
 {
+	[_client.avatarManager setDelegate: nil];
 	[_client.contactManager removeDelegate: self];
 	[_groupMap release];
 	[_contactMap release];
@@ -364,6 +367,38 @@ static gboolean filter_roster_by_presence(GtkTreeModel *model,
 	});
 }
 
+- (void)contact: (XMPPContact*)contact
+   didSetAvatar: (OFString*)avatarFile
+{
+	of_log(@"Got an avatar from %@", contact.rosterItem.JID);
+	g_idle_add_block(^{
+		GtkTreeIter iter;
+		GtkTreePath *path;
+		GtkTreeRowReference *ref;
+		OFString *bareJID = [contact.rosterItem.JID bareJID];
+		OFMapTable *contactRows = [_contactMap objectForKey: bareJID];
+		OFArray *groups = contact.rosterItem.groups;;
+
+		GdkPixbuf *avatar =
+		    gdk_pixbuf_new_from_file([avatarFile UTF8String], NULL);
+
+		if (groups == nil)
+			groups = @[ @"General" ];
+
+		for (OFString *group in groups) {
+			ref = [contactRows valueForKey: group];
+			path = gtk_tree_row_reference_get_path(ref);
+			gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model),
+			    &iter, path);
+			gtk_tree_path_free(path);
+
+			gtk_tree_store_set(_roster_model, &iter,
+			    4, avatar, -1);
+		}
+		g_object_unref(G_OBJECT(avatar));
+	});
+}
+
 -      (void)client: (JubChatClient*)client_
   didChangePresence: (XMPPPresence*)presence
 {