Mercurial > pidgin
annotate libfaim/aim_im.c @ 312:3069be4c291e
[gaim-migrate @ 322]
I don't know why I did this. I have homework due in 15 hours that I haven't
started yet, and it's in a language I don't know and it's a project I don't
understand. If my teacher knew about this, he would be pissed. He looks
pissed all the time, even when he's not. When he smiles he looks devilish.
Maybe I only think that because literally half the class flunked the midterm.
I am not joking about that. More people got F's than A, B, and C combined.
It's 2 am and the homework's due at 5 tomorrow so what do I do? Get chat to
work. Wow. That's going to look good on my resume. "Why did you flunk this
class?" "Because I was getting chat in Instant Messenger to work." Not that
that's not something to be proud of, but I wonder which is more important to
employers. The big battle, experience versus education. Just because you
got good grades in college doesn't mean you're smarter than someone who
flunked, it just means you put in the effort necessary to get a better grade
and the other person didn't. Maybe the person who flunked was working on
real honest-to-god actually *used* software, as opposed to some stupid tree
that only gets used for a fringe branch of computer science that doesn't
offer much more than a normal heap or binary search tree offers. Maybe the
person was out there reverse-engineering protocols and allowing cross-
platform communication to occur, creating interoperability and causing a
greater demand not only for the product, but for the platform it runs on!
Given the choices, who would you pick? Someone who was told how to code a
tree and managed to get it to work, or someone who increases your userbase
and marketability?
Enough of my rant for a while. I've had waaaaay too much sugar (gummy candy is
deadly).
committer: Tailor Script <tailor@pidgin.im>
author | Eric Warmenhoven <eric@warmenhoven.org> |
---|---|
date | Fri, 02 Jun 2000 09:11:48 +0000 |
parents | bafaf1b68f9a |
children | 9d258a0aa560 |
rev | line source |
---|---|
2 | 1 /* |
2 * aim_im.c | |
3 * | |
4 * The routines for sending/receiving Instant Messages. | |
5 * | |
6 */ | |
7 | |
283
0f14e6d8a51b
[gaim-migrate @ 293]
Eric Warmenhoven <eric@warmenhoven.org>
parents:
237
diff
changeset
|
8 #include <faim/aim.h> |
2 | 9 |
10 /* | |
11 * Send an ICBM (instant message). | |
12 * | |
13 * | |
14 * Possible flags: | |
15 * AIM_IMFLAGS_AWAY -- Marks the message as an autoresponse | |
16 * AIM_IMFLAGS_ACK -- Requests that the server send an ack | |
17 * when the message is received (of type 0x0004/0x000c) | |
18 * | |
19 */ | |
237 | 20 u_long aim_send_im(struct aim_session_t *sess, |
21 struct aim_conn_t *conn, | |
22 char *destsn, u_int flags, char *msg) | |
2 | 23 { |
24 | |
237 | 25 int curbyte,i; |
26 struct command_tx_struct *newpacket; | |
2 | 27 |
283
0f14e6d8a51b
[gaim-migrate @ 293]
Eric Warmenhoven <eric@warmenhoven.org>
parents:
237
diff
changeset
|
28 if (strlen(msg) >= MAXMSGLEN) |
0f14e6d8a51b
[gaim-migrate @ 293]
Eric Warmenhoven <eric@warmenhoven.org>
parents:
237
diff
changeset
|
29 return -1; |
0f14e6d8a51b
[gaim-migrate @ 293]
Eric Warmenhoven <eric@warmenhoven.org>
parents:
237
diff
changeset
|
30 |
0f14e6d8a51b
[gaim-migrate @ 293]
Eric Warmenhoven <eric@warmenhoven.org>
parents:
237
diff
changeset
|
31 if (!(newpacket = aim_tx_new(0x0002, conn, strlen(msg)+256))) |
237 | 32 return -1; |
2 | 33 |
237 | 34 newpacket->lock = 1; /* lock struct */ |
2 | 35 |
36 curbyte = 0; | |
237 | 37 curbyte += aim_putsnac(newpacket->data+curbyte, |
38 0x0004, 0x0006, 0x0000, sess->snac_nextid); | |
39 | |
40 /* | |
41 * Generate a random message cookie | |
42 * | |
43 * We could cache these like we do SNAC IDs. (In fact, it | |
44 * might be a good idea.) In the message error functions, | |
45 * the 8byte message cookie is returned as well as the | |
46 * SNAC ID. | |
47 * | |
48 */ | |
49 for (i=0;i<8;i++) | |
50 curbyte += aimutil_put8(newpacket->data+curbyte, (u_char) random()); | |
2 | 51 |
237 | 52 /* |
53 * Channel ID | |
54 */ | |
55 curbyte += aimutil_put16(newpacket->data+curbyte,0x0001); | |
56 | |
57 /* | |
58 * Destination SN (prepended with byte length) | |
59 */ | |
60 curbyte += aimutil_put8(newpacket->data+curbyte,strlen(destsn)); | |
61 curbyte += aimutil_putstr(newpacket->data+curbyte, destsn, strlen(destsn)); | |
62 | |
63 /* | |
64 * metaTLV start. | |
65 */ | |
66 curbyte += aimutil_put16(newpacket->data+curbyte, 0x0002); | |
67 curbyte += aimutil_put16(newpacket->data+curbyte, strlen(msg) + 0x0d); | |
2 | 68 |
237 | 69 /* |
70 * Flag data? | |
71 */ | |
72 curbyte += aimutil_put16(newpacket->data+curbyte, 0x0501); | |
73 curbyte += aimutil_put16(newpacket->data+curbyte, 0x0001); | |
74 curbyte += aimutil_put16(newpacket->data+curbyte, 0x0101); | |
75 curbyte += aimutil_put8 (newpacket->data+curbyte, 0x01); | |
76 | |
77 /* | |
78 * Message block length. | |
79 */ | |
80 curbyte += aimutil_put16(newpacket->data+curbyte, strlen(msg) + 0x04); | |
81 | |
82 /* | |
83 * Character set data? | |
84 */ | |
85 curbyte += aimutil_put16(newpacket->data+curbyte, 0x0000); | |
86 curbyte += aimutil_put16(newpacket->data+curbyte, 0x0000); | |
2 | 87 |
237 | 88 /* |
89 * Message. Not terminated. | |
90 */ | |
91 curbyte += aimutil_putstr(newpacket->data+curbyte,msg, strlen(msg)); | |
2 | 92 |
237 | 93 /* |
94 * Set the Request Acknowledge flag. | |
95 */ | |
96 if (flags & AIM_IMFLAGS_ACK) { | |
97 curbyte += aimutil_put16(newpacket->data+curbyte,0x0003); | |
98 curbyte += aimutil_put16(newpacket->data+curbyte,0x0000); | |
99 } | |
100 | |
101 /* | |
102 * Set the Autoresponse flag. | |
103 */ | |
104 if (flags & AIM_IMFLAGS_AWAY) { | |
105 curbyte += aimutil_put16(newpacket->data+curbyte,0x0004); | |
106 curbyte += aimutil_put16(newpacket->data+curbyte,0x0000); | |
107 } | |
108 | |
109 newpacket->commandlen = curbyte; | |
110 newpacket->lock = 0; | |
111 | |
112 aim_tx_enqueue(sess, newpacket); | |
2 | 113 |
114 #ifdef USE_SNAC_FOR_IMS | |
115 { | |
116 struct aim_snac_t snac; | |
117 | |
237 | 118 snac.id = sess->snac_nextid; |
2 | 119 snac.family = 0x0004; |
120 snac.type = 0x0006; | |
121 snac.flags = 0x0000; | |
122 | |
123 snac.data = malloc(strlen(destsn)+1); | |
124 memcpy(snac.data, destsn, strlen(destsn)+1); | |
125 | |
237 | 126 aim_newsnac(sess, &snac); |
2 | 127 } |
128 | |
237 | 129 aim_cleansnacs(sess, 60); /* clean out all SNACs over 60sec old */ |
2 | 130 #endif |
131 | |
237 | 132 return (sess->snac_nextid++); |
2 | 133 } |
134 | |
237 | 135 /* |
136 * It can easily be said that parsing ICBMs is THE single | |
137 * most difficult thing to do in the in AIM protocol. In | |
138 * fact, I think I just did say that. | |
139 * | |
140 * Below is the best damned solution I've come up with | |
141 * over the past sixteen months of battling with it. This | |
142 * can parse both away and normal messages from every client | |
143 * I have access to. Its not fast, its not clean. But it works. | |
144 * | |
145 * We should also support at least minimal parsing of | |
146 * Channel 2, so that we can at least know the name of the | |
147 * room we're invited to, but obviously can't attend... | |
148 * | |
149 */ | |
150 int aim_parse_incoming_im_middle(struct aim_session_t *sess, | |
151 struct command_rx_struct *command) | |
2 | 152 { |
237 | 153 u_int i = 0,z; |
154 rxcallback_t userfunc = NULL; | |
155 u_char cookie[8]; | |
156 int channel; | |
157 struct aim_tlvlist_t *tlvlist; | |
158 struct aim_userinfo_s userinfo; | |
159 u_short wastebits; | |
160 | |
161 memset(&userinfo, 0x00, sizeof(struct aim_userinfo_s)); | |
162 | |
163 i = 10; /* Skip SNAC header */ | |
2 | 164 |
237 | 165 /* |
166 * Read ICBM Cookie. And throw away. | |
167 */ | |
168 for (z=0; z<8; z++,i++) | |
169 cookie[z] = command->data[i]; | |
2 | 170 |
237 | 171 /* |
172 * Channel ID. | |
173 * | |
174 * Channel 0x0001 is the message channel. There are | |
175 * other channels for things called "rendevous" | |
176 * which represent chat and some of the other new | |
177 * features of AIM2/3/3.5. | |
178 * | |
179 * Channel 0x0002 is the Rendevous channel, which | |
180 * is where Chat Invitiations and various client-client | |
181 * connection negotiations come from. | |
182 * | |
183 */ | |
184 channel = aimutil_get16(command->data+i); | |
185 i += 2; | |
2 | 186 |
237 | 187 /* |
188 * | |
189 */ | |
190 if ((channel != 0x01) && (channel != 0x02)) | |
191 { | |
192 printf("faim: icbm: ICBM received on an unsupported channel. Ignoring.\n (chan = %04x)", channel); | |
193 return 1; | |
194 } | |
195 | |
196 /* | |
197 * Source screen name. | |
198 */ | |
199 memcpy(userinfo.sn, command->data+i+1, (int)command->data[i]); | |
200 userinfo.sn[(int)command->data[i]] = '\0'; | |
201 i += 1 + (int)command->data[i]; | |
202 | |
203 /* | |
204 * Warning Level | |
205 */ | |
206 userinfo.warnlevel = aimutil_get16(command->data+i); /* guess */ | |
207 i += 2; | |
208 | |
209 /* | |
210 * Number of TLVs that follow. Not needed. | |
211 */ | |
212 wastebits = aimutil_get16(command->data+i); | |
2 | 213 i += 2; |
214 | |
237 | 215 /* |
216 * Read block of TLVs. All further data is derived | |
217 * from what is parsed here. | |
218 */ | |
219 tlvlist = aim_readtlvchain(command->data+i, command->commandlen-i); | |
2 | 220 |
237 | 221 /* |
222 * From here on, its depends on what channel we're on. | |
223 */ | |
224 if (channel == 1) | |
225 { | |
226 u_int j = 0, y = 0, z = 0; | |
227 char *msg = NULL; | |
228 u_int icbmflags = 0; | |
229 struct aim_tlv_t *msgblocktlv, *tmptlv; | |
230 u_char *msgblock; | |
231 u_short flag1,flag2; | |
232 | |
233 /* | |
234 * Check Autoresponse status. If it is an autoresponse, | |
235 * it will contain a second type 0x0004 TLV, with zero length. | |
236 */ | |
237 if (aim_gettlv(tlvlist, 0x0004, 2)) | |
238 icbmflags |= AIM_IMFLAGS_AWAY; | |
239 | |
240 /* | |
241 * Check Ack Request status. | |
242 */ | |
243 if (aim_gettlv(tlvlist, 0x0003, 2)) | |
244 icbmflags |= AIM_IMFLAGS_ACK; | |
245 | |
246 /* | |
247 * Extract the various pieces of the userinfo struct. | |
248 */ | |
249 /* Class. */ | |
250 if ((tmptlv = aim_gettlv(tlvlist, 0x0001, 1))) | |
251 userinfo.class = aimutil_get16(tmptlv->value); | |
252 /* Member-since date. */ | |
253 if ((tmptlv = aim_gettlv(tlvlist, 0x0002, 1))) | |
254 { | |
255 /* If this is larger than 4, its probably the message block, skip */ | |
256 if (tmptlv->length <= 4) | |
257 userinfo.membersince = aimutil_get32(tmptlv->value); | |
258 } | |
259 /* On-since date */ | |
260 if ((tmptlv = aim_gettlv(tlvlist, 0x0003, 1))) | |
261 userinfo.onlinesince = aimutil_get32(tmptlv->value); | |
262 /* Idle-time */ | |
263 if ((tmptlv = aim_gettlv(tlvlist, 0x0004, 1))) | |
264 userinfo.idletime = aimutil_get16(tmptlv->value); | |
265 /* Session Length (AIM) */ | |
266 if ((tmptlv = aim_gettlv(tlvlist, 0x000f, 1))) | |
267 userinfo.sessionlen = aimutil_get16(tmptlv->value); | |
268 /* Session Length (AOL) */ | |
269 if ((tmptlv = aim_gettlv(tlvlist, 0x0010, 1))) | |
270 userinfo.sessionlen = aimutil_get16(tmptlv->value); | |
271 | |
272 /* | |
273 * Message block. | |
274 * | |
275 * XXX: Will the msgblock always be the second 0x0002? | |
276 */ | |
277 msgblocktlv = aim_gettlv(tlvlist, 0x0002, 1); | |
278 if (!msgblocktlv) | |
279 { | |
280 printf("faim: icbm: major error! no message block TLV found!\n"); | |
281 aim_freetlvchain(&tlvlist); | |
282 return 1; | |
283 } | |
284 | |
285 /* | |
286 * Extracting the message from the unknown cruft. | |
287 * | |
288 * This is a bit messy, and I'm not really qualified, | |
289 * even as the author, to comment on it. At least | |
290 * its not as bad as a while loop shooting into infinity. | |
291 * | |
292 * "Do you believe in magic?" | |
293 * | |
294 */ | |
295 msgblock = msgblocktlv->value; | |
296 j = 0; | |
297 | |
298 wastebits = aimutil_get8(msgblock+j++); | |
299 wastebits = aimutil_get8(msgblock+j++); | |
300 | |
301 y = aimutil_get16(msgblock+j); | |
302 j += 2; | |
303 for (z = 0; z < y; z++) | |
304 wastebits = aimutil_get8(msgblock+j++); | |
305 wastebits = aimutil_get8(msgblock+j++); | |
306 wastebits = aimutil_get8(msgblock+j++); | |
307 | |
308 /* | |
309 * Message string length, including flag words. | |
310 */ | |
311 i = aimutil_get16(msgblock+j); | |
312 j += 2; | |
313 | |
314 /* | |
315 * Flag words. | |
316 * | |
317 * Its rumored that these can kick in some funky | |
318 * 16bit-wide char stuff that used to really kill | |
319 * libfaim. Hopefully the latter is no longer true. | |
320 * | |
321 * Though someone should investiagte the former. | |
322 * | |
323 */ | |
324 flag1 = aimutil_get16(msgblock+j); | |
325 j += 2; | |
326 flag2 = aimutil_get16(msgblock+j); | |
327 j += 2; | |
328 | |
329 if (flag1 || flag2) | |
330 printf("faim: icbm: **warning: encoding flags are being used! {%04x, %04x}\n", flag1, flag2); | |
331 | |
332 /* | |
333 * Message string. | |
334 */ | |
335 i -= 4; | |
336 msg = (char *)malloc(i+1); | |
337 memcpy(msg, msgblock+j, i); | |
338 msg[i] = '\0'; | |
339 | |
340 /* | |
341 * Call client. | |
342 */ | |
343 userfunc = aim_callhandler(command->conn, 0x0004, 0x0007); | |
344 if (userfunc) | |
345 i = userfunc(sess, command, channel, &userinfo, msg, icbmflags, flag1, flag2); | |
346 else | |
347 i = 0; | |
348 | |
349 free(msg); | |
350 } | |
351 else if (channel == 0x0002) | |
352 { | |
353 int rendtype; | |
354 struct aim_tlv_t *block1; | |
355 struct aim_tlvlist_t *list2; | |
356 struct aim_tlv_t *tmptlv; | |
357 int a; | |
358 | |
359 /* Class. */ | |
360 if ((tmptlv = aim_gettlv(tlvlist, 0x0001, 1))) | |
361 userinfo.class = aimutil_get16(tmptlv->value); | |
362 /* On-since date */ | |
363 if ((tmptlv = aim_gettlv(tlvlist, 0x0003, 1))) | |
364 userinfo.onlinesince = aimutil_get32(tmptlv->value); | |
365 /* Idle-time */ | |
366 if ((tmptlv = aim_gettlv(tlvlist, 0x0004, 1))) | |
367 userinfo.idletime = aimutil_get16(tmptlv->value); | |
368 /* Session Length (AIM) */ | |
369 if ((tmptlv = aim_gettlv(tlvlist, 0x000f, 1))) | |
370 userinfo.sessionlen = aimutil_get16(tmptlv->value); | |
371 /* Session Length (AOL) */ | |
372 if ((tmptlv = aim_gettlv(tlvlist, 0x0010, 1))) | |
373 userinfo.sessionlen = aimutil_get16(tmptlv->value); | |
2 | 374 |
237 | 375 /* |
376 * There's another block of TLVs embedded in the type 5 here. | |
377 */ | |
378 block1 = aim_gettlv(tlvlist, 0x0005, 1); | |
379 if (!block1) | |
380 return 1; /* major problem */ | |
381 | |
382 a = 0x1a; /* skip -- not sure what this information is! */ | |
383 | |
384 /* | |
385 * XXX: Ignore if there's no data, only cookie information. | |
386 * | |
387 * Its probably just an accepted invitation or something. | |
388 * | |
389 */ | |
390 if (block1->length <= 0x1a) | |
391 { | |
392 aim_freetlvchain(&tlvlist); | |
393 return 1; | |
394 } | |
395 | |
396 list2 = aim_readtlvchain(block1->value+a, block1->length-a); | |
397 | |
398 if (aim_gettlv(list2, 0x0004, 1) /* start connection */ || | |
399 aim_gettlv(list2, 0x000b, 1) /* close conncetion */) | |
400 { | |
401 rendtype = 1; /* voice request */ | |
2 | 402 |
237 | 403 /* |
404 * Call client. | |
405 */ | |
406 userfunc = aim_callhandler(command->conn, 0x0004, 0x0007); | |
407 if (userfunc) | |
408 i = userfunc(sess, | |
409 command, | |
410 channel, | |
411 rendtype, | |
412 &userinfo); | |
413 else | |
414 i = 0; | |
415 } | |
2 | 416 else |
237 | 417 { |
418 struct aim_chat_roominfo roominfo; | |
419 char *msg=NULL,*encoding=NULL,*lang=NULL; | |
420 | |
421 rendtype = 0; /* chat invite */ | |
422 if (aim_gettlv(list2, 0x2711, 1)) | |
423 { | |
424 struct aim_tlv_t *nametlv; | |
425 | |
426 nametlv = aim_gettlv(list2, 0x2711, 1); | |
427 aim_chat_readroominfo(nametlv->value, &roominfo); | |
428 } | |
429 | |
430 if (aim_gettlv(list2, 0x000c, 1)) | |
431 msg = aim_gettlv_str(list2, 0x000c, 1); | |
432 | |
433 if (aim_gettlv(list2, 0x000d, 1)) | |
434 encoding = aim_gettlv_str(list2, 0x000d, 1); | |
435 | |
436 if (aim_gettlv(list2, 0x000e, 1)) | |
437 lang = aim_gettlv_str(list2, 0x000e, 1); | |
438 | |
439 /* | |
440 * Call client. | |
441 */ | |
442 userfunc = aim_callhandler(command->conn, 0x0004, 0x0007); | |
443 if (userfunc) | |
444 i = userfunc(sess, | |
445 command, | |
446 channel, | |
447 rendtype, | |
448 &userinfo, | |
449 &roominfo, | |
450 msg, | |
451 encoding?encoding+1:NULL, | |
452 lang?lang+1:NULL); | |
453 else | |
454 i = 0; | |
455 | |
456 free(roominfo.name); | |
457 free(msg); | |
458 free(encoding); | |
459 free(lang); | |
460 } | |
461 aim_freetlvchain(&list2); | |
2 | 462 } |
463 | |
237 | 464 /* |
465 * Free up the TLV chain. | |
466 */ | |
467 aim_freetlvchain(&tlvlist); | |
2 | 468 |
469 | |
470 return i; | |
471 } | |
237 | 472 |
473 /* | |
474 * Not real sure what this does, nor does anyone I've talk to. | |
475 * | |
476 * Didn't use to send it. But now I think it might be a good | |
477 * idea. | |
478 * | |
479 */ | |
480 u_long aim_seticbmparam(struct aim_session_t *sess, | |
481 struct aim_conn_t *conn) | |
482 { | |
483 struct command_tx_struct *newpacket; | |
484 int curbyte; | |
485 | |
486 if(!(newpacket = aim_tx_new(0x0002, conn, 10+16))) | |
487 return -1; | |
488 | |
489 newpacket->lock = 1; | |
490 | |
491 curbyte = aim_putsnac(newpacket->data, 0x0004, 0x0002, 0x0000, sess->snac_nextid); | |
492 curbyte += aimutil_put16(newpacket->data+curbyte, 0x0000); | |
493 curbyte += aimutil_put32(newpacket->data+curbyte, 0x00000003); | |
494 curbyte += aimutil_put8(newpacket->data+curbyte, 0x1f); | |
495 curbyte += aimutil_put8(newpacket->data+curbyte, 0x40); | |
496 curbyte += aimutil_put8(newpacket->data+curbyte, 0x03); | |
497 curbyte += aimutil_put8(newpacket->data+curbyte, 0xe7); | |
498 curbyte += aimutil_put8(newpacket->data+curbyte, 0x03); | |
499 curbyte += aimutil_put8(newpacket->data+curbyte, 0xe7); | |
500 curbyte += aimutil_put16(newpacket->data+curbyte, 0x0000); | |
501 curbyte += aimutil_put16(newpacket->data+curbyte, 0x0000); | |
502 | |
503 newpacket->lock = 0; | |
504 aim_tx_enqueue(sess, newpacket); | |
505 | |
506 return (sess->snac_nextid++); | |
507 } | |
508 | |
509 int aim_parse_msgerror_middle(struct aim_session_t *sess, | |
510 struct command_rx_struct *command) | |
511 { | |
512 u_long snacid = 0x000000000; | |
513 struct aim_snac_t *snac = NULL; | |
514 int ret = 0; | |
515 rxcallback_t userfunc = NULL; | |
516 | |
517 /* | |
518 * Get SNAC from packet and look it up | |
519 * the list of unrepliedto/outstanding | |
520 * SNACs. | |
521 * | |
522 * After its looked up, the SN that the | |
523 * message should've gone to will be | |
524 * in the ->data element of the snac struct. | |
525 * | |
526 */ | |
527 snacid = aimutil_get32(command->data+6); | |
528 snac = aim_remsnac(sess, snacid); | |
529 | |
284
bafaf1b68f9a
[gaim-migrate @ 294]
Eric Warmenhoven <eric@warmenhoven.org>
parents:
283
diff
changeset
|
530 if (!snac) { |
bafaf1b68f9a
[gaim-migrate @ 294]
Eric Warmenhoven <eric@warmenhoven.org>
parents:
283
diff
changeset
|
531 printf("faim: msgerr: got an ICBM-failed error on an unknown SNAC ID! (%08lx)\n", snacid); |
bafaf1b68f9a
[gaim-migrate @ 294]
Eric Warmenhoven <eric@warmenhoven.org>
parents:
283
diff
changeset
|
532 } |
237 | 533 |
534 /* | |
535 * Call client. | |
536 */ | |
537 userfunc = aim_callhandler(command->conn, 0x0004, 0x0001); | |
538 if (userfunc) | |
539 ret = userfunc(sess, command, (snac)?snac->data:"(UNKNOWN)"); | |
540 else | |
541 ret = 0; | |
542 | |
284
bafaf1b68f9a
[gaim-migrate @ 294]
Eric Warmenhoven <eric@warmenhoven.org>
parents:
283
diff
changeset
|
543 if (snac) { |
bafaf1b68f9a
[gaim-migrate @ 294]
Eric Warmenhoven <eric@warmenhoven.org>
parents:
283
diff
changeset
|
544 free(snac->data); |
bafaf1b68f9a
[gaim-migrate @ 294]
Eric Warmenhoven <eric@warmenhoven.org>
parents:
283
diff
changeset
|
545 free(snac); |
bafaf1b68f9a
[gaim-migrate @ 294]
Eric Warmenhoven <eric@warmenhoven.org>
parents:
283
diff
changeset
|
546 } |
237 | 547 |
548 return ret; | |
549 } | |
550 | |
551 |