Mercurial > pidgin.yaz
comparison libpurple/protocols/jabber/caps.c @ 17609:f88b3a093cba
Implemented ad-hoc commands for the buddy action menu (untested), implemented the receiving end of XEP-0115: Entity Capabilities. Note that this seems not to be reliable right now, since some clients seem to have a very broken [read: completely non-functional] implementation (most notably Gajim and the py-transports).
author | Andreas Monitzer <pidgin@monitzer.com> |
---|---|
date | Sat, 23 Jun 2007 02:57:21 +0000 |
parents | |
children | 7c79957207c3 |
comparison
equal
deleted
inserted
replaced
17608:a8b1159fd95b | 17609:f88b3a093cba |
---|---|
1 /* | |
2 * purple - Jabber Protocol Plugin | |
3 * | |
4 * Copyright (C) 2007, Andreas Monitzer <andy@monitzer.com> | |
5 * | |
6 * This program is free software; you can redistribute it and/or modify | |
7 * it under the terms of the GNU General Public License as published by | |
8 * the Free Software Foundation; either version 2 of the License, or | |
9 * (at your option) any later version. | |
10 * | |
11 * This program is distributed in the hope that it will be useful, | |
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 * GNU General Public License for more details. | |
15 * | |
16 * You should have received a copy of the GNU General Public License | |
17 * along with this program; if not, write to the Free Software | |
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |
19 * | |
20 */ | |
21 | |
22 #include "caps.h" | |
23 #include <string.h> | |
24 #include "internal.h" | |
25 #include "util.h" | |
26 #include "iq.h" | |
27 | |
28 #define JABBER_CAPS_FILENAME "xmpp-caps.xml" | |
29 | |
30 static GHashTable *capstable = NULL; /* JabberCapsKey -> JabberCapsValue */ | |
31 | |
32 typedef struct _JabberCapsKey { | |
33 char *node; | |
34 char *ver; | |
35 } JabberCapsKey; | |
36 | |
37 typedef struct _JabberCapsValueExt { | |
38 GList *identities; /* JabberCapsIdentity */ | |
39 GList *features; /* char * */ | |
40 } JabberCapsValueExt; | |
41 | |
42 typedef struct _JabberCapsValue { | |
43 GList *identities; /* JabberCapsIdentity */ | |
44 GList *features; /* char * */ | |
45 GHashTable *ext; /* char * -> JabberCapsValueExt */ | |
46 } JabberCapsValue; | |
47 | |
48 static guint jabber_caps_hash(gconstpointer key) { | |
49 const JabberCapsKey *name = key; | |
50 guint nodehash = g_str_hash(name->node); | |
51 guint verhash = g_str_hash(name->ver); | |
52 | |
53 return nodehash ^ verhash; | |
54 } | |
55 | |
56 static gboolean jabber_caps_compare(gconstpointer v1, gconstpointer v2) { | |
57 const JabberCapsKey *name1 = v1; | |
58 const JabberCapsKey *name2 = v2; | |
59 | |
60 return strcmp(name1->node,name2->node) == 0 && strcmp(name1->ver,name2->ver) == 0; | |
61 } | |
62 | |
63 static void jabber_caps_destroy_key(gpointer key) { | |
64 JabberCapsKey *keystruct = key; | |
65 g_free(keystruct->node); | |
66 g_free(keystruct->ver); | |
67 g_free(keystruct); | |
68 } | |
69 | |
70 static void jabber_caps_destroy_value(gpointer value) { | |
71 JabberCapsValue *valuestruct = value; | |
72 while(valuestruct->identities) { | |
73 JabberCapsIdentity *id = valuestruct->identities->data; | |
74 g_free(id->category); | |
75 g_free(id->type); | |
76 g_free(id->name); | |
77 g_free(id); | |
78 | |
79 valuestruct->identities = g_list_delete_link(valuestruct->identities,valuestruct->identities); | |
80 } | |
81 while(valuestruct->features) { | |
82 g_free(valuestruct->features->data); | |
83 valuestruct->features = g_list_delete_link(valuestruct->features,valuestruct->features); | |
84 } | |
85 g_hash_table_destroy(valuestruct->ext); | |
86 g_free(valuestruct); | |
87 } | |
88 | |
89 static void jabber_caps_ext_destroy_value(gpointer value) { | |
90 JabberCapsValueExt *valuestruct = value; | |
91 while(valuestruct->identities) { | |
92 JabberCapsIdentity *id = valuestruct->identities->data; | |
93 g_free(id->category); | |
94 g_free(id->type); | |
95 g_free(id->name); | |
96 g_free(id); | |
97 | |
98 valuestruct->identities = g_list_delete_link(valuestruct->identities,valuestruct->identities); | |
99 } | |
100 while(valuestruct->features) { | |
101 g_free(valuestruct->features->data); | |
102 valuestruct->features = g_list_delete_link(valuestruct->features,valuestruct->features); | |
103 } | |
104 g_free(valuestruct); | |
105 } | |
106 | |
107 static void jabber_caps_load(void); | |
108 | |
109 void jabber_caps_init(void) { | |
110 capstable = g_hash_table_new_full(jabber_caps_hash, jabber_caps_compare, jabber_caps_destroy_key, jabber_caps_destroy_value); | |
111 jabber_caps_load(); | |
112 } | |
113 | |
114 static void jabber_caps_load(void) { | |
115 xmlnode *capsdata = purple_util_read_xml_from_file(JABBER_CAPS_FILENAME, "XMPP capabilities cache"); | |
116 xmlnode *client; | |
117 if(!capsdata || strcmp(capsdata->name, "capabilities")) | |
118 return; | |
119 | |
120 for(client = capsdata->child; client; client = client->next) { | |
121 if(client->type != XMLNODE_TYPE_TAG) | |
122 continue; | |
123 if(!strcmp(client->name, "client")) { | |
124 JabberCapsKey *key = g_new0(JabberCapsKey, 1); | |
125 JabberCapsValue *value = g_new0(JabberCapsValue, 1); | |
126 xmlnode *child; | |
127 key->node = g_strdup(xmlnode_get_attrib(client,"node")); | |
128 key->ver = g_strdup(xmlnode_get_attrib(client,"ver")); | |
129 value->ext = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, jabber_caps_ext_destroy_value); | |
130 for(child = client->child; child; child = child->next) { | |
131 if(child->type != XMLNODE_TYPE_TAG) | |
132 continue; | |
133 if(!strcmp(child->name,"feature")) { | |
134 const char *var = xmlnode_get_attrib(child, "var"); | |
135 if(!var) | |
136 continue; | |
137 value->features = g_list_append(value->features,g_strdup(var)); | |
138 } else if(!strcmp(child->name,"identity")) { | |
139 const char *category = xmlnode_get_attrib(child, "category"); | |
140 const char *type = xmlnode_get_attrib(child, "type"); | |
141 const char *name = xmlnode_get_attrib(child, "name"); | |
142 | |
143 JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1); | |
144 id->category = g_strdup(category); | |
145 id->type = g_strdup(type); | |
146 id->name = g_strdup(name); | |
147 | |
148 value->identities = g_list_append(value->identities,id); | |
149 } else if(!strcmp(child->name,"ext")) { | |
150 const char *identifier = xmlnode_get_attrib(child, "identifier"); | |
151 if(identifier) { | |
152 xmlnode *extchild; | |
153 | |
154 JabberCapsValueExt *extvalue = g_new0(JabberCapsValueExt, 1); | |
155 | |
156 for(extchild = child->child; extchild; extchild = extchild->next) { | |
157 if(extchild->type != XMLNODE_TYPE_TAG) | |
158 continue; | |
159 if(!strcmp(extchild->name,"feature")) { | |
160 const char *var = xmlnode_get_attrib(extchild, "var"); | |
161 if(!var) | |
162 continue; | |
163 extvalue->features = g_list_append(extvalue->features,g_strdup(var)); | |
164 } else if(!strcmp(extchild->name,"identity")) { | |
165 const char *category = xmlnode_get_attrib(extchild, "category"); | |
166 const char *type = xmlnode_get_attrib(extchild, "type"); | |
167 const char *name = xmlnode_get_attrib(extchild, "name"); | |
168 | |
169 JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1); | |
170 id->category = g_strdup(category); | |
171 id->type = g_strdup(type); | |
172 id->name = g_strdup(name); | |
173 | |
174 extvalue->identities = g_list_append(extvalue->identities,id); | |
175 } | |
176 } | |
177 g_hash_table_replace(value->ext, g_strdup(identifier), extvalue); | |
178 } | |
179 } | |
180 } | |
181 g_hash_table_replace(capstable, key, value); | |
182 } | |
183 } | |
184 } | |
185 | |
186 static void jabber_caps_store_ext(gpointer key, gpointer value, gpointer user_data) { | |
187 const char *extname = key; | |
188 JabberCapsValueExt *props = value; | |
189 xmlnode *root = user_data; | |
190 xmlnode *ext = xmlnode_new_child(root,"ext"); | |
191 GList *iter; | |
192 | |
193 xmlnode_set_attrib(ext,"identifier",extname); | |
194 | |
195 for(iter = props->identities; iter; iter = g_list_next(iter)) { | |
196 JabberCapsIdentity *id = iter->data; | |
197 xmlnode *identity = xmlnode_new_child(ext, "identity"); | |
198 xmlnode_set_attrib(identity, "category", id->category); | |
199 xmlnode_set_attrib(identity, "type", id->type); | |
200 xmlnode_set_attrib(identity, "name", id->name); | |
201 } | |
202 | |
203 for(iter = props->features; iter; iter = g_list_next(iter)) { | |
204 const char *feat = iter->data; | |
205 xmlnode *feature = xmlnode_new_child(ext, "feature"); | |
206 xmlnode_set_attrib(feature, "var", feat); | |
207 } | |
208 } | |
209 | |
210 static void jabber_caps_store_client(gpointer key, gpointer value, gpointer user_data) { | |
211 JabberCapsKey *clientinfo = key; | |
212 JabberCapsValue *props = value; | |
213 xmlnode *root = user_data; | |
214 xmlnode *client = xmlnode_new_child(root,"client"); | |
215 GList *iter; | |
216 | |
217 xmlnode_set_attrib(client,"node",clientinfo->node); | |
218 xmlnode_set_attrib(client,"ver",clientinfo->ver); | |
219 | |
220 for(iter = props->identities; iter; iter = g_list_next(iter)) { | |
221 JabberCapsIdentity *id = iter->data; | |
222 xmlnode *identity = xmlnode_new_child(client, "identity"); | |
223 xmlnode_set_attrib(identity, "category", id->category); | |
224 xmlnode_set_attrib(identity, "type", id->type); | |
225 xmlnode_set_attrib(identity, "name", id->name); | |
226 } | |
227 | |
228 for(iter = props->features; iter; iter = g_list_next(iter)) { | |
229 const char *feat = iter->data; | |
230 xmlnode *feature = xmlnode_new_child(client, "feature"); | |
231 xmlnode_set_attrib(feature, "var", feat); | |
232 } | |
233 | |
234 g_hash_table_foreach(props->ext,jabber_caps_store_ext,client); | |
235 } | |
236 | |
237 static void jabber_caps_store(void) { | |
238 xmlnode *root = xmlnode_new("capabilities"); | |
239 g_hash_table_foreach(capstable, jabber_caps_store_client, root); | |
240 purple_util_write_data_to_file(JABBER_CAPS_FILENAME, xmlnode_to_formatted_str(root, NULL), -1); | |
241 } | |
242 | |
243 /* this function assumes that all information is available locally */ | |
244 static JabberCapsClientInfo *jabber_caps_collect_info(const char *node, const char *ver, GList *ext) { | |
245 JabberCapsClientInfo *result = g_new0(JabberCapsClientInfo, 1); | |
246 JabberCapsKey *key = g_new0(JabberCapsKey, 1); | |
247 JabberCapsValue *caps; | |
248 GList *iter; | |
249 | |
250 key->node = g_strdup(node); | |
251 key->ver = g_strdup(ver); | |
252 | |
253 caps = g_hash_table_lookup(capstable,key); | |
254 | |
255 g_free(key->node); | |
256 g_free(key->ver); | |
257 g_free(key); | |
258 | |
259 /* join all information */ | |
260 for(iter = caps->identities; iter; iter = g_list_next(iter)) { | |
261 JabberCapsIdentity *id = iter->data; | |
262 JabberCapsIdentity *newid = g_new0(JabberCapsIdentity, 1); | |
263 newid->category = g_strdup(id->category); | |
264 newid->type = g_strdup(id->type); | |
265 newid->name = g_strdup(id->name); | |
266 | |
267 result->identities = g_list_append(result->identities,newid); | |
268 } | |
269 for(iter = caps->features; iter; iter = g_list_next(iter)) { | |
270 const char *feat = iter->data; | |
271 char *newfeat = g_strdup(feat); | |
272 | |
273 result->features = g_list_append(result->features,newfeat); | |
274 } | |
275 | |
276 for(iter = ext; iter; iter = g_list_next(iter)) { | |
277 const char *ext = iter->data; | |
278 JabberCapsValueExt *extinfo = g_hash_table_lookup(caps->ext,ext); | |
279 | |
280 if(extinfo) { | |
281 for(iter = extinfo->identities; iter; iter = g_list_next(iter)) { | |
282 JabberCapsIdentity *id = iter->data; | |
283 JabberCapsIdentity *newid = g_new0(JabberCapsIdentity, 1); | |
284 newid->category = g_strdup(id->category); | |
285 newid->type = g_strdup(id->type); | |
286 newid->name = g_strdup(id->name); | |
287 | |
288 result->identities = g_list_append(result->identities,newid); | |
289 } | |
290 for(iter = extinfo->features; iter; iter = g_list_next(iter)) { | |
291 const char *feat = iter->data; | |
292 char *newfeat = g_strdup(feat); | |
293 | |
294 result->features = g_list_append(result->features,newfeat); | |
295 } | |
296 } | |
297 } | |
298 return result; | |
299 } | |
300 | |
301 void jabber_caps_free_clientinfo(JabberCapsClientInfo *clientinfo) { | |
302 if(!clientinfo) | |
303 return; | |
304 while(clientinfo->identities) { | |
305 JabberCapsIdentity *id = clientinfo->identities->data; | |
306 g_free(id->category); | |
307 g_free(id->type); | |
308 g_free(id->name); | |
309 g_free(id); | |
310 | |
311 clientinfo->identities = g_list_remove_link(clientinfo->identities,clientinfo->identities); | |
312 } | |
313 while(clientinfo->features) { | |
314 char *feat = clientinfo->features->data; | |
315 g_free(feat); | |
316 | |
317 clientinfo->features = g_list_remove_link(clientinfo->features,clientinfo->features); | |
318 } | |
319 | |
320 g_free(clientinfo); | |
321 } | |
322 | |
323 typedef struct _jabber_caps_cbplususerdata { | |
324 jabber_caps_get_info_cb cb; | |
325 gpointer user_data; | |
326 | |
327 char *who; | |
328 char *node; | |
329 char *ver; | |
330 GList *ext; | |
331 unsigned extOutstanding; | |
332 } jabber_caps_cbplususerdata; | |
333 | |
334 static void jabber_caps_get_info_check_completion(jabber_caps_cbplususerdata *userdata) { | |
335 if(userdata->extOutstanding == 0) { | |
336 userdata->cb(jabber_caps_collect_info(userdata->node, userdata->ver, userdata->ext), userdata->user_data); | |
337 g_free(userdata->who); | |
338 g_free(userdata->node); | |
339 g_free(userdata->ver); | |
340 while(userdata->ext) { | |
341 g_free(userdata->ext->data); | |
342 userdata->ext = g_list_remove_link(userdata->ext,userdata->ext); | |
343 } | |
344 g_free(userdata); | |
345 } | |
346 } | |
347 | |
348 static void jabber_caps_ext_iqcb(JabberStream *js, xmlnode *packet, gpointer data) { | |
349 /* collect data and fetch all exts */ | |
350 xmlnode *query = xmlnode_get_child_with_namespace(packet,"query","http://jabber.org/protocol/disco#info"); | |
351 xmlnode *child; | |
352 jabber_caps_cbplususerdata *userdata = data; | |
353 JabberCapsKey *clientkey = g_new0(JabberCapsKey, 1); | |
354 JabberCapsValue *client; | |
355 JabberCapsValueExt *value = g_new0(JabberCapsValueExt, 1); | |
356 const char *node = xmlnode_get_attrib(query, "node"); | |
357 const char *key; | |
358 | |
359 --userdata->extOutstanding; | |
360 | |
361 if(node) { | |
362 clientkey->node = g_strdup(userdata->node); | |
363 clientkey->ver = g_strdup(userdata->ver); | |
364 | |
365 client = g_hash_table_lookup(capstable,clientkey); | |
366 | |
367 g_free(clientkey->node); | |
368 g_free(clientkey->ver); | |
369 g_free(clientkey); | |
370 | |
371 /* split node by #, key either points to \0 or the correct ext afterwards */ | |
372 for(key = node; key[0] != '\0'; ++key) { | |
373 if(key[0] == '#') { | |
374 ++key; | |
375 break; | |
376 } | |
377 } | |
378 | |
379 for(child = query->child; child; child = child->next) { | |
380 if(child->type != XMLNODE_TYPE_TAG) | |
381 continue; | |
382 if(!strcmp(child->name,"feature")) { | |
383 const char *var = xmlnode_get_attrib(child, "var"); | |
384 if(!var) | |
385 continue; | |
386 value->features = g_list_append(value->features,g_strdup(var)); | |
387 } else if(!strcmp(child->name,"identity")) { | |
388 const char *category = xmlnode_get_attrib(child, "category"); | |
389 const char *type = xmlnode_get_attrib(child, "type"); | |
390 const char *name = xmlnode_get_attrib(child, "name"); | |
391 | |
392 JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1); | |
393 id->category = g_strdup(category); | |
394 id->type = g_strdup(type); | |
395 id->name = g_strdup(name); | |
396 | |
397 value->identities = g_list_append(value->identities,id); | |
398 } | |
399 } | |
400 g_hash_table_replace(client->ext, g_strdup(key), value); | |
401 | |
402 jabber_caps_store(); | |
403 } | |
404 | |
405 jabber_caps_get_info_check_completion(userdata); | |
406 } | |
407 | |
408 static void jabber_caps_client_iqcb(JabberStream *js, xmlnode *packet, gpointer data) { | |
409 /* collect data and fetch all exts */ | |
410 xmlnode *query = xmlnode_get_child_with_namespace(packet,"query","http://jabber.org/protocol/disco#info"); | |
411 xmlnode *child; | |
412 GList *iter; | |
413 jabber_caps_cbplususerdata *userdata = data; | |
414 JabberCapsKey *key = g_new0(JabberCapsKey, 1); | |
415 JabberCapsValue *value = g_new0(JabberCapsValue, 1); | |
416 key->node = g_strdup(userdata->node); | |
417 key->ver = g_strdup(userdata->ver); | |
418 | |
419 value->ext = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, jabber_caps_ext_destroy_value); | |
420 | |
421 for(child = query->child; child; child = child->next) { | |
422 if(child->type != XMLNODE_TYPE_TAG) | |
423 continue; | |
424 if(!strcmp(child->name,"feature")) { | |
425 const char *var = xmlnode_get_attrib(child, "var"); | |
426 if(!var) | |
427 continue; | |
428 value->features = g_list_append(value->features,g_strdup(var)); | |
429 } else if(!strcmp(child->name,"identity")) { | |
430 const char *category = xmlnode_get_attrib(child, "category"); | |
431 const char *type = xmlnode_get_attrib(child, "type"); | |
432 const char *name = xmlnode_get_attrib(child, "name"); | |
433 | |
434 JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1); | |
435 id->category = g_strdup(category); | |
436 id->type = g_strdup(type); | |
437 id->name = g_strdup(name); | |
438 | |
439 value->identities = g_list_append(value->identities,id); | |
440 } | |
441 } | |
442 g_hash_table_replace(capstable, key, value); | |
443 | |
444 /* fetch all exts */ | |
445 for(iter = userdata->ext; iter; iter = g_list_next(iter)) { | |
446 JabberIq *iq = jabber_iq_new_query(js,JABBER_IQ_GET,"http://jabber.org/protocol/disco#info"); | |
447 xmlnode *query = xmlnode_get_child_with_namespace(iq->node,"query","http://jabber.org/protocol/disco#info"); | |
448 char *node = g_strdup_printf("%s#%s", node, (const char*)iter->data); | |
449 xmlnode_set_attrib(query, "node", node); | |
450 g_free(node); | |
451 xmlnode_set_attrib(iq->node, "to", userdata->who); | |
452 | |
453 jabber_iq_set_callback(iq,jabber_caps_ext_iqcb,userdata); | |
454 jabber_iq_send(iq); | |
455 } | |
456 | |
457 jabber_caps_store(); | |
458 | |
459 jabber_caps_get_info_check_completion(userdata); | |
460 } | |
461 | |
462 void jabber_caps_get_info(JabberStream *js, const char *who, const char *node, const char *ver, const char *ext, jabber_caps_get_info_cb cb, gpointer user_data) { | |
463 JabberCapsValue *client; | |
464 JabberCapsKey *key = g_new0(JabberCapsKey, 1); | |
465 char *originalext = g_strdup(ext); | |
466 char *oneext, *ctx; | |
467 jabber_caps_cbplususerdata *userdata = g_new0(jabber_caps_cbplususerdata, 1); | |
468 userdata->cb = cb; | |
469 userdata->user_data = user_data; | |
470 userdata->who = g_strdup(who); | |
471 userdata->node = g_strdup(node); | |
472 userdata->ver = g_strdup(ver); | |
473 | |
474 if(originalext) | |
475 for(oneext = strtok_r(originalext, " ", &ctx); oneext; oneext = strtok_r(NULL, " ", &ctx)) { | |
476 userdata->ext = g_list_append(userdata->ext,g_strdup(oneext)); | |
477 ++userdata->extOutstanding; | |
478 } | |
479 g_free(originalext); | |
480 | |
481 key->node = g_strdup(node); | |
482 key->ver = g_strdup(ver); | |
483 | |
484 client = g_hash_table_lookup(capstable, key); | |
485 | |
486 g_free(key->node); | |
487 g_free(key->ver); | |
488 g_free(key); | |
489 | |
490 if(!client) { | |
491 JabberIq *iq = jabber_iq_new_query(js,JABBER_IQ_GET,"http://jabber.org/protocol/disco#info"); | |
492 xmlnode *query = xmlnode_get_child_with_namespace(iq->node,"query","http://jabber.org/protocol/disco#info"); | |
493 char *nodever = g_strdup_printf("%s#%s", node, ver); | |
494 xmlnode_set_attrib(query, "node", nodever); | |
495 g_free(nodever); | |
496 xmlnode_set_attrib(iq->node, "to", who); | |
497 | |
498 jabber_iq_set_callback(iq,jabber_caps_client_iqcb,userdata); | |
499 jabber_iq_send(iq); | |
500 } else { | |
501 GList *iter; | |
502 /* fetch unknown exts only */ | |
503 for(iter = userdata->ext; iter; iter = g_list_next(iter)) { | |
504 JabberCapsValueExt *extvalue = g_hash_table_lookup(client->ext, (const char*)iter->data); | |
505 JabberIq *iq; | |
506 xmlnode *query; | |
507 char *nodever; | |
508 | |
509 if(extvalue) { | |
510 /* we already have this ext, don't bother with it */ | |
511 --userdata->extOutstanding; | |
512 continue; | |
513 } | |
514 | |
515 iq = jabber_iq_new_query(js,JABBER_IQ_GET,"http://jabber.org/protocol/disco#info"); | |
516 query = xmlnode_get_child_with_namespace(iq->node,"query","http://jabber.org/protocol/disco#info"); | |
517 nodever = g_strdup_printf("%s#%s", node, (const char*)iter->data); | |
518 xmlnode_set_attrib(query, "node", nodever); | |
519 g_free(nodever); | |
520 xmlnode_set_attrib(iq->node, "to", who); | |
521 | |
522 jabber_iq_set_callback(iq,jabber_caps_ext_iqcb,userdata); | |
523 jabber_iq_send(iq); | |
524 } | |
525 /* maybe we have all data available anyways? This is the ideal case where no network traffic is necessary */ | |
526 jabber_caps_get_info_check_completion(userdata); | |
527 } | |
528 } | |
529 |