]> cgit.babelmonkeys.de Git - jubjub.git/commitdiff
Add User Avatar support
authorFlorian Zeitz <florob@babelmonkeys.de>
Wed, 5 Jun 2013 22:51:50 +0000 (00:51 +0200)
committerFlorian Zeitz <florob@babelmonkeys.de>
Thu, 6 Jun 2013 21:41:15 +0000 (23:41 +0200)
data/gtk/roster.ui
src/core/JubAvatarManager.h [new file with mode: 0644]
src/core/JubAvatarManager.m [new file with mode: 0644]
src/core/JubChatClient.h
src/core/JubChatClient.m
src/core/Makefile
src/core/main.m
src/gui/gtk/JubGtkRosterUI.h
src/gui/gtk/JubGtkRosterUI.m

index cadb855dedbbd1a0a9fc57c10216a165f1688f45..a2dc3504303555c62ca856b5a0bd903542377ece 100644 (file)
@@ -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">
                     </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 (file)
index 0000000..9565f39
--- /dev/null
@@ -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 (file)
index 0000000..7d2fe8f
--- /dev/null
@@ -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
index 57cfd0b5782f29732863e3631aac10ed3fcfa9c1..168fa696aef080d9b2fbb528c5eaf91bd0c07b3f 100644 (file)
@@ -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;
index 1d7874ad563d26c044f5b9d100651c0505deb94f..3184b8970dd08e5e914aaa61c7d5bfef29d72de4 100644 (file)
@@ -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;
                _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];
 - (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];
 }
 
index 9b1d30392af704820d911837080236014b6d22f7..234aac6bfd2a60ffb52acf308ee1612a51780bda 100644 (file)
@@ -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
index fda8c5f96b5d59b229afed59b7f745fe9b75eb05..6de6785be8ec4378c4dfaa02bff19294a789038d 100644 (file)
@@ -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
index 40214f76797598d70321cffe7286b8ca46c3a8e5..9769c761ea4f47c972674af4fd82ddf77ae51bb6 100644 (file)
@@ -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;
index a805adf5129c80ae270688fbaa02271addac75d7..667760b0734cff566e6f9ed3a23791cc6340159b 100644 (file)
@@ -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
 {