comparison libpurple/protocols/jabber/buddy.c @ 26817:3912f55a1633

propagate from branch 'im.pidgin.pidgin' (head fbb4fe5da444943eecc76bdcd6c8ba967790b6c8) to branch 'im.pidgin.cpw.darkrain42.xmpp.bosh' (head 601bc627c9430320848361f0ed81c6c4c6ee53e0)
author Paul Aurich <paul@darkrain42.org>
date Tue, 28 Apr 2009 18:43:57 +0000
parents 538b4175fd24 80437c891f92
children 6028712210ca 98ff7c538e48
comparison
equal deleted inserted replaced
26743:de9816c970fe 26817:3912f55a1633
30 #include "buddy.h" 30 #include "buddy.h"
31 #include "chat.h" 31 #include "chat.h"
32 #include "jabber.h" 32 #include "jabber.h"
33 #include "iq.h" 33 #include "iq.h"
34 #include "presence.h" 34 #include "presence.h"
35 #include "useravatar.h"
35 #include "xdata.h" 36 #include "xdata.h"
36 #include "pep.h" 37 #include "pep.h"
37 #include "adhoccommands.h" 38 #include "adhoccommands.h"
38
39 #define MAX_HTTP_BUDDYICON_BYTES (200 * 1024)
40 39
41 typedef struct { 40 typedef struct {
42 long idle_seconds; 41 long idle_seconds;
43 } JabberBuddyInfoResource; 42 } JabberBuddyInfoResource;
44 43
96 if(!jb) 95 if(!jb)
97 return NULL; 96 return NULL;
98 97
99 for(l = jb->resources; l; l = l->next) 98 for(l = jb->resources; l; l = l->next)
100 { 99 {
101 if(!jbr && !resource) { 100 JabberBuddyResource *tmp = (JabberBuddyResource *) l->data;
102 jbr = l->data; 101 if (!jbr && !resource) {
103 } else if(!resource) { 102 jbr = tmp;
104 if(((JabberBuddyResource *)l->data)->priority > jbr->priority) 103 } else if (!resource) {
105 jbr = l->data; 104 if (tmp->priority > jbr->priority)
106 else if(((JabberBuddyResource *)l->data)->priority == jbr->priority) { 105 jbr = tmp;
106 else if (tmp->priority == jbr->priority) {
107 /* Determine if this resource is more available than the one we've currently chosen */ 107 /* Determine if this resource is more available than the one we've currently chosen */
108 switch(((JabberBuddyResource *)l->data)->state) { 108 switch(tmp->state) {
109 case JABBER_BUDDY_STATE_ONLINE: 109 case JABBER_BUDDY_STATE_ONLINE:
110 case JABBER_BUDDY_STATE_CHAT: 110 case JABBER_BUDDY_STATE_CHAT:
111 /* This resource is online/chatty. Prefer to one which isn't either. */ 111 /* This resource is online/chatty. Prefer to one which isn't either. */
112 if ((jbr->state != JABBER_BUDDY_STATE_ONLINE) && (jbr->state != JABBER_BUDDY_STATE_CHAT)) 112 if (((jbr->state != JABBER_BUDDY_STATE_ONLINE) && (jbr->state != JABBER_BUDDY_STATE_CHAT))
113 jbr = l->data; 113 || (jbr->idle && !tmp->idle)
114 || (jbr->idle && tmp->idle && tmp->idle > jbr->idle))
115 jbr = tmp;
114 break; 116 break;
115 case JABBER_BUDDY_STATE_AWAY: 117 case JABBER_BUDDY_STATE_AWAY:
116 case JABBER_BUDDY_STATE_DND: 118 case JABBER_BUDDY_STATE_DND:
117 /* This resource is away/dnd. Prefer to one which is extended away, unavailable, or unknown. */ 119 /* This resource is away/dnd. Prefer to one which is extended away, unavailable, or unknown. */
118 if ((jbr->state == JABBER_BUDDY_STATE_XA) || (jbr->state == JABBER_BUDDY_STATE_UNAVAILABLE) || 120 if (((jbr->state == JABBER_BUDDY_STATE_XA) || (jbr->state == JABBER_BUDDY_STATE_UNAVAILABLE) ||
119 (jbr->state == JABBER_BUDDY_STATE_UNKNOWN) || (jbr->state == JABBER_BUDDY_STATE_ERROR)) 121 (jbr->state == JABBER_BUDDY_STATE_UNKNOWN) || (jbr->state == JABBER_BUDDY_STATE_ERROR))
120 jbr = l->data; 122 || (jbr->idle && !tmp->idle)
123 || (jbr->idle && tmp->idle && tmp->idle > jbr->idle))
124 jbr = tmp;
121 break; 125 break;
122 case JABBER_BUDDY_STATE_XA: 126 case JABBER_BUDDY_STATE_XA:
123 /* This resource is extended away. That's better than unavailable or unknown. */ 127 /* This resource is extended away. That's better than unavailable or unknown. */
124 if ((jbr->state == JABBER_BUDDY_STATE_UNAVAILABLE) || (jbr->state == JABBER_BUDDY_STATE_UNKNOWN) || (jbr->state == JABBER_BUDDY_STATE_ERROR)) 128 if ((jbr->state == JABBER_BUDDY_STATE_UNAVAILABLE) || (jbr->state == JABBER_BUDDY_STATE_UNKNOWN) || (jbr->state == JABBER_BUDDY_STATE_ERROR))
125 jbr = l->data; 129 jbr = tmp;
126 break; 130 break;
127 case JABBER_BUDDY_STATE_UNAVAILABLE: 131 case JABBER_BUDDY_STATE_UNAVAILABLE:
128 /* This resource is unavailable. That's better than unknown. */ 132 /* This resource is unavailable. That's better than unknown. */
129 if ((jbr->state == JABBER_BUDDY_STATE_UNKNOWN) || (jbr->state == JABBER_BUDDY_STATE_ERROR)) 133 if ((jbr->state == JABBER_BUDDY_STATE_UNKNOWN) || (jbr->state == JABBER_BUDDY_STATE_ERROR))
130 jbr = l->data; 134 jbr = tmp;
131 break; 135 break;
132 case JABBER_BUDDY_STATE_UNKNOWN: 136 case JABBER_BUDDY_STATE_UNKNOWN:
133 case JABBER_BUDDY_STATE_ERROR: 137 case JABBER_BUDDY_STATE_ERROR:
134 /* These are never preferable. */ 138 /* These are never preferable. */
135 break; 139 break;
136 } 140 }
137 } 141 }
138 } else if(((JabberBuddyResource *)l->data)->name) { 142 } else if(tmp->name) {
139 if(!strcmp(((JabberBuddyResource *)l->data)->name, resource)) { 143 if(!strcmp(tmp->name, resource)) {
140 jbr = l->data; 144 jbr = tmp;
141 break; 145 break;
142 } 146 }
143 } 147 }
144 } 148 }
145 149
200 204
201 if(!jbr) 205 if(!jbr)
202 return; 206 return;
203 207
204 jabber_buddy_resource_free(jbr); 208 jabber_buddy_resource_free(jbr);
205 }
206
207 const char *jabber_buddy_get_status_msg(JabberBuddy *jb)
208 {
209 JabberBuddyResource *jbr;
210
211 if(!jb)
212 return NULL;
213
214 jbr = jabber_buddy_find_resource(jb, NULL);
215
216 if(!jbr)
217 return NULL;
218
219 return jbr->status;
220 } 209 }
221 210
222 /******* 211 /*******
223 * This is the old vCard stuff taken from the old prpl. vCards, by definition 212 * This is the old vCard stuff taken from the old prpl. vCards, by definition
224 * are a temporary thing until jabber can get its act together and come up 213 * are a temporary thing until jabber can get its act together and come up
485 474
486 if (vc_node != NULL) { 475 if (vc_node != NULL) {
487 iq = jabber_iq_new(js, JABBER_IQ_SET); 476 iq = jabber_iq_new(js, JABBER_IQ_SET);
488 xmlnode_insert_child(iq->node, vc_node); 477 xmlnode_insert_child(iq->node, vc_node);
489 jabber_iq_send(iq); 478 jabber_iq_send(iq);
479
480 /* Send presence to update vcard-temp:x:update */
481 jabber_presence_send(js, FALSE);
490 } 482 }
491 } 483 }
492 484
493 void jabber_set_buddy_icon(PurpleConnection *gc, PurpleStoredImage *img) 485 void jabber_set_buddy_icon(PurpleConnection *gc, PurpleStoredImage *img)
494 { 486 {
495 if(((JabberStream*)purple_connection_get_protocol_data(gc))->pep) { 487 PurpleAccount *account = purple_connection_get_account(gc);
496 /* XEP-0084: User Avatars */ 488
497 if(img) { 489 /* Publish the avatar as specified in XEP-0084 */
498 /* 490 jabber_avatar_set(gc->proto_data, img);
499 * TODO: This is pretty gross. The Jabber PRPL really shouldn't 491 /* Set the image in our vCard */
500 * do voodoo to try to determine the image type, height 492 jabber_set_info(gc, purple_account_get_user_info(account));
501 * and width. 493
502 */ 494 /* TODO: Fake image to ourselves, since a number of servers do not echo
503 /* A PNG header, including the IHDR, but nothing else */ 495 * back our presence to us. To do this without uselessly copying the data
504 const struct { 496 * of the image, we need purple_buddy_icons_set_for_user_image (i.e. takes
505 guchar signature[8]; /* must be hex 89 50 4E 47 0D 0A 1A 0A */ 497 * an existing icon/stored image). */
506 struct {
507 guint32 length; /* must be 0x0d */
508 guchar type[4]; /* must be 'I' 'H' 'D' 'R' */
509 guint32 width;
510 guint32 height;
511 guchar bitdepth;
512 guchar colortype;
513 guchar compression;
514 guchar filter;
515 guchar interlace;
516 } ihdr;
517 } *png = purple_imgstore_get_data(img); /* ATTN: this is in network byte order! */
518
519 /* check if the data is a valid png file (well, at least to some extend) */
520 if(png->signature[0] == 0x89 &&
521 png->signature[1] == 0x50 &&
522 png->signature[2] == 0x4e &&
523 png->signature[3] == 0x47 &&
524 png->signature[4] == 0x0d &&
525 png->signature[5] == 0x0a &&
526 png->signature[6] == 0x1a &&
527 png->signature[7] == 0x0a &&
528 ntohl(png->ihdr.length) == 0x0d &&
529 png->ihdr.type[0] == 'I' &&
530 png->ihdr.type[1] == 'H' &&
531 png->ihdr.type[2] == 'D' &&
532 png->ihdr.type[3] == 'R') {
533 /* parse PNG header to get the size of the image (yes, this is required) */
534 guint32 width = ntohl(png->ihdr.width);
535 guint32 height = ntohl(png->ihdr.height);
536 xmlnode *publish, *item, *data, *metadata, *info;
537 char *lengthstring, *widthstring, *heightstring;
538
539 /* compute the sha1 hash */
540 char *hash = jabber_calculate_data_sha1sum(purple_imgstore_get_data(img), purple_imgstore_get_size(img));
541 char *base64avatar;
542
543 publish = xmlnode_new("publish");
544 xmlnode_set_attrib(publish,"node",AVATARNAMESPACEDATA);
545
546 item = xmlnode_new_child(publish, "item");
547 xmlnode_set_attrib(item, "id", hash);
548
549 data = xmlnode_new_child(item, "data");
550 xmlnode_set_namespace(data,AVATARNAMESPACEDATA);
551
552 base64avatar = purple_base64_encode(purple_imgstore_get_data(img), purple_imgstore_get_size(img));
553 xmlnode_insert_data(data,base64avatar,-1);
554 g_free(base64avatar);
555
556 /* publish the avatar itself */
557 jabber_pep_publish((JabberStream*)purple_connection_get_protocol_data(gc), publish);
558
559 /* next step: publish the metadata */
560 publish = xmlnode_new("publish");
561 xmlnode_set_attrib(publish,"node",AVATARNAMESPACEMETA);
562
563 item = xmlnode_new_child(publish, "item");
564 xmlnode_set_attrib(item, "id", hash);
565
566 metadata = xmlnode_new_child(item, "metadata");
567 xmlnode_set_namespace(metadata,AVATARNAMESPACEMETA);
568
569 info = xmlnode_new_child(metadata, "info");
570 xmlnode_set_attrib(info, "id", hash);
571 xmlnode_set_attrib(info, "type", "image/png");
572 lengthstring = g_strdup_printf("%u", (unsigned)purple_imgstore_get_size(img));
573 xmlnode_set_attrib(info, "bytes", lengthstring);
574 g_free(lengthstring);
575 widthstring = g_strdup_printf("%u", width);
576 xmlnode_set_attrib(info, "width", widthstring);
577 g_free(widthstring);
578 heightstring = g_strdup_printf("%u", height);
579 xmlnode_set_attrib(info, "height", heightstring);
580 g_free(heightstring);
581
582 /* publish the metadata */
583 jabber_pep_publish((JabberStream*)purple_connection_get_protocol_data(gc), publish);
584
585 g_free(hash);
586 } else {
587 purple_debug_error("jabber", "jabber_set_buddy_icon received non-png data");
588 }
589 } else {
590 /* remove the metadata */
591 xmlnode *metadata, *item;
592 xmlnode *publish = xmlnode_new("publish");
593 xmlnode_set_attrib(publish,"node",AVATARNAMESPACEMETA);
594
595 item = xmlnode_new_child(publish, "item");
596
597 metadata = xmlnode_new_child(item, "metadata");
598 xmlnode_set_namespace(metadata,AVATARNAMESPACEMETA);
599
600 xmlnode_new_child(metadata, "stop");
601
602 /* publish the metadata */
603 jabber_pep_publish((JabberStream*)gc->proto_data, publish);
604 }
605 }
606
607 /* vCard avatars do not have an image type requirement so update our
608 * vCard avatar regardless of image type for those poor older clients
609 */
610 jabber_set_info(gc, purple_account_get_user_info(gc->account));
611
612 jabber_presence_send(gc->proto_data, FALSE);
613 } 498 }
614 499
615 /* 500 /*
616 * This is the callback from the "ok clicked" for "set vCard" 501 * This is the callback from the "ok clicked" for "set vCard"
617 * 502 *
1182 1067
1183 static void jabber_vcard_save_mine(JabberStream *js, const char *from, 1068 static void jabber_vcard_save_mine(JabberStream *js, const char *from,
1184 JabberIqType type, const char *id, 1069 JabberIqType type, const char *id,
1185 xmlnode *packet, gpointer data) 1070 xmlnode *packet, gpointer data)
1186 { 1071 {
1187 xmlnode *vcard; 1072 xmlnode *vcard, *photo, *binval;
1188 char *txt; 1073 char *txt, *vcard_hash = NULL;
1189 PurpleStoredImage *img;
1190 1074
1191 if (type == JABBER_IQ_ERROR) { 1075 if (type == JABBER_IQ_ERROR) {
1192 purple_debug_warning("jabber", "Server returned error while retrieving vCard"); 1076 purple_debug_warning("jabber", "Server returned error while retrieving vCard");
1193 return; 1077 return;
1194 } 1078 }
1204 /* if we have no vCard, then lets not overwrite what we might have locally */ 1088 /* if we have no vCard, then lets not overwrite what we might have locally */
1205 } 1089 }
1206 1090
1207 js->vcard_fetched = TRUE; 1091 js->vcard_fetched = TRUE;
1208 1092
1209 if(NULL != (img = purple_buddy_icons_find_account_icon(js->gc->account))) { 1093 if (vcard && (photo = xmlnode_get_child(vcard, "PHOTO")) &&
1210 jabber_set_buddy_icon(js->gc, img); 1094 (binval = xmlnode_get_child(photo, "BINVAL"))) {
1211 purple_imgstore_unref(img); 1095 gsize size;
1212 } 1096 char *bintext = xmlnode_get_data(binval);
1097 guchar *data = purple_base64_decode(bintext, &size);
1098 g_free(bintext);
1099
1100 if (data) {
1101 vcard_hash = jabber_calculate_data_sha1sum(data, size);
1102 g_free(data);
1103 }
1104 }
1105
1106 /* Republish our vcard if the photo is different than the server's */
1107 if (!purple_strequal(vcard_hash, js->initial_avatar_hash)) {
1108 PurpleAccount *account = purple_connection_get_account(js->gc);
1109 jabber_set_info(js->gc, purple_account_get_user_info(account));
1110 } else if (js->initial_avatar_hash) {
1111 /* Our photo is in the vcard, so advertise vcard-temp updates */
1112 js->avatar_hash = g_strdup(js->initial_avatar_hash);
1113 }
1114
1115 g_free(vcard_hash);
1213 } 1116 }
1214 1117
1215 void jabber_vcard_fetch_mine(JabberStream *js) 1118 void jabber_vcard_fetch_mine(JabberStream *js)
1216 { 1119 {
1217 JabberIq *iq = jabber_iq_new(js, JABBER_IQ_GET); 1120 JabberIq *iq = jabber_iq_new(js, JABBER_IQ_GET);
1455 g_free(bare_jid); 1358 g_free(bare_jid);
1456 1359
1457 jabber_buddy_info_show_if_ready(jbi); 1360 jabber_buddy_info_show_if_ready(jbi);
1458 } 1361 }
1459 1362
1460 typedef struct _JabberBuddyAvatarUpdateURLInfo {
1461 JabberStream *js;
1462 char *from;
1463 char *id;
1464 } JabberBuddyAvatarUpdateURLInfo;
1465
1466 static void do_buddy_avatar_update_fromurl(PurpleUtilFetchUrlData *url_data, gpointer user_data, const gchar *url_text, gsize len, const gchar *error_message) {
1467 JabberBuddyAvatarUpdateURLInfo *info = user_data;
1468 if(!url_text) {
1469 purple_debug(PURPLE_DEBUG_ERROR, "jabber",
1470 "do_buddy_avatar_update_fromurl got error \"%s\"", error_message);
1471 return;
1472 }
1473
1474 purple_buddy_icons_set_for_user(purple_connection_get_account(info->js->gc), info->from, (void*)url_text, len, info->id);
1475 g_free(info->from);
1476 g_free(info->id);
1477 g_free(info);
1478 }
1479
1480 static void do_buddy_avatar_update_data(JabberStream *js, const char *from, xmlnode *items) {
1481 xmlnode *item, *data;
1482 const char *checksum;
1483 char *b64data;
1484 void *img;
1485 size_t size;
1486 if(!items)
1487 return;
1488
1489 item = xmlnode_get_child(items, "item");
1490 if(!item)
1491 return;
1492
1493 data = xmlnode_get_child_with_namespace(item,"data",AVATARNAMESPACEDATA);
1494 if(!data)
1495 return;
1496
1497 checksum = xmlnode_get_attrib(item,"id");
1498 if(!checksum)
1499 return;
1500
1501 b64data = xmlnode_get_data(data);
1502 if(!b64data)
1503 return;
1504
1505 img = purple_base64_decode(b64data, &size);
1506 if(!img) {
1507 g_free(b64data);
1508 return;
1509 }
1510
1511 purple_buddy_icons_set_for_user(purple_connection_get_account(js->gc), from, img, size, checksum);
1512 g_free(b64data);
1513 }
1514
1515 void jabber_buddy_avatar_update_metadata(JabberStream *js, const char *from, xmlnode *items) {
1516 PurpleBuddy *buddy = purple_find_buddy(purple_connection_get_account(js->gc), from);
1517 const char *checksum;
1518 xmlnode *item, *metadata;
1519 if(!buddy)
1520 return;
1521
1522 checksum = purple_buddy_icons_get_checksum_for_user(buddy);
1523 item = xmlnode_get_child(items,"item");
1524 metadata = xmlnode_get_child_with_namespace(item, "metadata", AVATARNAMESPACEMETA);
1525 if(!metadata)
1526 return;
1527 /* check if we have received a stop */
1528 if(xmlnode_get_child(metadata, "stop")) {
1529 purple_buddy_icons_set_for_user(purple_connection_get_account(js->gc), from, NULL, 0, NULL);
1530 } else {
1531 xmlnode *info, *goodinfo = NULL;
1532 gboolean has_children = FALSE;
1533
1534 /* iterate over all info nodes to get one we can use */
1535 for(info = metadata->child; info; info = info->next) {
1536 if(info->type == XMLNODE_TYPE_TAG)
1537 has_children = TRUE;
1538 if(info->type == XMLNODE_TYPE_TAG && !strcmp(info->name,"info")) {
1539 const char *type = xmlnode_get_attrib(info,"type");
1540 const char *id = xmlnode_get_attrib(info,"id");
1541
1542 if(checksum && id && !strcmp(id, checksum)) {
1543 /* we already have that avatar, so we don't have to do anything */
1544 goodinfo = NULL;
1545 break;
1546 }
1547 /* We'll only pick the png one for now. It's a very nice image format anyways. */
1548 if(type && id && !goodinfo && !strcmp(type, "image/png"))
1549 goodinfo = info;
1550 }
1551 }
1552 if(has_children == FALSE) {
1553 purple_buddy_icons_set_for_user(purple_connection_get_account(js->gc), from, NULL, 0, NULL);
1554 } else if(goodinfo) {
1555 const char *url = xmlnode_get_attrib(goodinfo, "url");
1556 const char *id = xmlnode_get_attrib(goodinfo,"id");
1557
1558 /* the avatar might either be stored in a pep node, or on a HTTP/HTTPS URL */
1559 if(!url)
1560 jabber_pep_request_item(js, from, AVATARNAMESPACEDATA, id, do_buddy_avatar_update_data);
1561 else {
1562 PurpleUtilFetchUrlData *url_data;
1563 JabberBuddyAvatarUpdateURLInfo *info = g_new0(JabberBuddyAvatarUpdateURLInfo, 1);
1564 info->js = js;
1565
1566 url_data = purple_util_fetch_url_len(url, TRUE, NULL, TRUE,
1567 MAX_HTTP_BUDDYICON_BYTES,
1568 do_buddy_avatar_update_fromurl, info);
1569 if (url_data) {
1570 info->from = g_strdup(from);
1571 info->id = g_strdup(id);
1572 js->url_datas = g_slist_prepend(js->url_datas, url_data);
1573 } else
1574 g_free(info);
1575
1576 }
1577 }
1578 }
1579 }
1580
1581 static void jabber_buddy_info_resource_free(gpointer data) 1363 static void jabber_buddy_info_resource_free(gpointer data)
1582 { 1364 {
1583 JabberBuddyInfoResource *jbri = data; 1365 JabberBuddyInfoResource *jbri = data;
1584 g_free(jbri); 1366 g_free(jbri);
1585 } 1367 }
1648 if((query = xmlnode_get_child(packet, "query"))) { 1430 if((query = xmlnode_get_child(packet, "query"))) {
1649 seconds = xmlnode_get_attrib(query, "seconds"); 1431 seconds = xmlnode_get_attrib(query, "seconds");
1650 if(seconds) { 1432 if(seconds) {
1651 char *end = NULL; 1433 char *end = NULL;
1652 long sec = strtol(seconds, &end, 10); 1434 long sec = strtol(seconds, &end, 10);
1653 if(end != seconds) { 1435 JabberBuddy *jb = NULL;
1436 char *resource = NULL;
1437 char *buddy_name = NULL;
1438 JabberBuddyResource *jbr = NULL;
1439
1440 if(end != seconds) {
1654 JabberBuddyInfoResource *jbir = g_hash_table_lookup(jbi->resources, resource_name); 1441 JabberBuddyInfoResource *jbir = g_hash_table_lookup(jbi->resources, resource_name);
1655 if(jbir) { 1442 if(jbir) {
1656 jbir->idle_seconds = sec; 1443 jbir->idle_seconds = sec;
1657 } 1444 }
1658 } 1445 }
1446 /* Update the idle time of the buddy resource, if we got it.
1447 This will correct the value when a server doesn't mark
1448 delayed presence and we got the presence when signing on */
1449 jb = jabber_buddy_find(js, from, FALSE);
1450 if (jb) {
1451 resource = jabber_get_resource(from);
1452 buddy_name = jabber_get_bare_jid(from);
1453 /* if the resource already has an idle time set, we
1454 must have gotten it originally from a presence. In
1455 this case we update it. Otherwise don't update it, to
1456 avoid setting an idle and not getting informed about
1457 the resource getting unidle */
1458 if (resource && buddy_name) {
1459 jbr = jabber_buddy_find_resource(jb, resource);
1460
1461 if (jbr->idle) {
1462 if (sec) {
1463 jbr->idle = time(NULL) - sec;
1464 } else {
1465 jbr->idle = 0;
1466 }
1467
1468 if (jbr ==
1469 jabber_buddy_find_resource(jb, NULL)) {
1470 purple_prpl_got_user_idle(js->gc->account,
1471 buddy_name, jbr->idle, jbr->idle);
1472 }
1473 }
1474 }
1475 g_free(resource);
1476 g_free(buddy_name);
1477 }
1659 } 1478 }
1660 } 1479 }
1661 } 1480 }
1662 g_free(resource_name); 1481 g_free(resource_name);
1663 } 1482 }