--- /dev/null
+#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