From fa3c174db5c04176d891b91f68491e6ac8e22c22 Mon Sep 17 00:00:00 2001 From: Florian Zeitz Date: Thu, 6 Jun 2013 00:51:50 +0200 Subject: [PATCH] Add User Avatar support --- data/gtk/roster.ui | 13 +++ src/core/JubAvatarManager.h | 23 +++++ src/core/JubAvatarManager.m | 180 +++++++++++++++++++++++++++++++++++ src/core/JubChatClient.h | 4 + src/core/JubChatClient.m | 31 +++--- src/core/Makefile | 5 +- src/core/main.m | 3 +- src/gui/gtk/JubGtkRosterUI.h | 4 +- src/gui/gtk/JubGtkRosterUI.m | 35 +++++++ 9 files changed, 280 insertions(+), 18 deletions(-) create mode 100644 src/core/JubAvatarManager.h create mode 100644 src/core/JubAvatarManager.m 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 @@ + + @@ -255,6 +257,17 @@ + + + Avatar + + + + 4 + + + + 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 +#import + +@class JubChatClient; +@class XMPPContact; + +@protocol JubAvatarManagerDelegate +- (void)contact: (XMPPContact*)contact + didSetAvatar: (OFString*)avatarFile; +@end + +@interface JubAvatarManager : OFObject +{ + JubChatClient *_client; + id _delegate; + OFMutableString *cachePath; +} +@property (assign) id 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 { @@ -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 #import "JubChatClient.h" +#import "JubAvatarManager.h" @class JubGtkChatUI; -@interface JubGtkRosterUI: OFObject +@interface JubGtkRosterUI: + OFObject { 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 #include +#include #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 { -- 2.39.2