1 | package goagi |
||
2 | |||
3 | import ( |
||
4 | "fmt" |
||
5 | "regexp" |
||
6 | ) |
||
7 | |||
8 | // Command sends command as string to the AGI and returns response valus with |
||
9 | // text response |
||
10 | func (agi *AGI) Command(cmd string) (code int, result int, respStr string, err error) { |
||
11 | resp, err := agi.execute(cmd) |
||
12 | if err != nil { |
||
13 | return -1, -1, "", err |
||
14 | } |
||
15 | return resp.code, int(resp.result), resp.raw, nil |
||
16 | } |
||
17 | |||
18 | // Answer executes AGI command "ANSWER" |
||
19 | // Answers channel if not already in answer state. |
||
20 | func (agi *AGI) Answer() (bool, error) { |
||
21 | resp, err := agi.execute("ANSWER") |
||
22 | if err != nil { |
||
23 | return false, err |
||
24 | } |
||
25 | return resp.isOk(), nil |
||
26 | } |
||
27 | |||
28 | // AsyncAGIBreak Interrupts Async AGI |
||
29 | // Interrupts expected flow of Async AGI commands and returns control |
||
30 | // to previous source (typically, the PBX dialplan). |
||
31 | func (agi *AGI) AsyncAGIBreak() (bool, error) { |
||
32 | resp, err := agi.execute("ASYNCAGI BREAK") |
||
33 | if err != nil { |
||
34 | return false, err |
||
35 | } |
||
36 | // Asterisk res_agi always returns 200 result=0 |
||
37 | // but for the future try to check response. |
||
38 | return resp.isOk(), nil |
||
39 | } |
||
40 | |||
41 | // ChannelStatus returns status of the connected channel. |
||
42 | // |
||
43 | // If no channel name is given (empty line) then returns the status of the current channel. |
||
44 | // |
||
45 | //Return values: |
||
46 | // 0 - Channel is down and available. |
||
47 | // 1 - Channel is down, but reserved. |
||
48 | // 2 - Channel is off hook. |
||
49 | // 3 - Digits (or equivalent) have been dialed. |
||
50 | // 4 - Line is ringing. |
||
51 | // 5 - Remote end is ringing. |
||
52 | // 6 - Line is up. |
||
53 | // 7 - Line is busy. |
||
54 | func (agi *AGI) ChannelStatus(channel string) (int, error) { |
||
55 | resp, err := agi.execute("CHANNEL STATUS " + channel) |
||
56 | if err != nil { |
||
57 | return -1, err |
||
58 | } |
||
59 | if resp.result == -1 { |
||
60 | return -1, errorNew("No channel name matched the argument given.") |
||
61 | } |
||
62 | return int(resp.result), nil |
||
63 | } |
||
64 | |||
65 | // ControlStreamFile sends audio file on channel and allows the listener |
||
66 | // to control the stream. |
||
67 | // Send the given file, allowing playback to be controlled by the given digits, if any. |
||
68 | // Use double quotes for the digits if you wish none to be permitted. If offsetms |
||
69 | // is provided then the audio will seek to offsetms before play starts. |
||
70 | // Returns 0 if playback completes without a digit being pressed, or the ASCII numerical |
||
71 | // value of the digit if one was pressed, or -1 on error or if the channel was |
||
72 | // disconnected. |
||
73 | // Returns the position where playback was terminated as endpos. |
||
74 | // Example: |
||
75 | // agi.ControlStreamFile("prompt_en", "19", "3000", "#", "0", "#", "1600") |
||
76 | // agi.ControlStreamFile("prompt_en", "") |
||
77 | // agi.ControlStreamFile("prompt_en", "19", "", "", "", "#", "1600") |
||
78 | func (agi *AGI) ControlStreamFile(filename, digits string, args ...interface{}) (int32, error) { |
||
79 | cmd := "CONTROL STREAM FILE " + filename |
||
80 | if len(digits) > 0 { |
||
81 | cmd += " " + digits |
||
82 | } else { |
||
83 | cmd += " \"\"" |
||
84 | } |
||
85 | resp, err := agi.execute(cmd, args...) |
||
86 | if err != nil { |
||
87 | return 0, err |
||
88 | } |
||
89 | if resp.result == -1 { |
||
90 | return resp.result, errorNew("Error or channel disconnected.") |
||
91 | } |
||
92 | |||
93 | return resp.endpos, nil |
||
94 | } |
||
95 | |||
96 | // DatabaseDel deletes an entry in the Asterisk database for a given family and key. |
||
97 | // Returns status and error if fails. |
||
98 | func (agi *AGI) DatabaseDel(family, key string) (bool, error) { |
||
99 | resp, err := agi.execute("DATABASE DELETE", family, key) |
||
100 | if err != nil { |
||
101 | return false, err |
||
102 | } |
||
103 | ok := resp.code == 200 && resp.result == 1 |
||
104 | return ok, nil |
||
105 | } |
||
106 | |||
107 | // DatabaseDelTree deletes a family or specific keytree within a family in the Asterisk database. |
||
108 | func (agi *AGI) DatabaseDelTree(family, keytree string) (bool, error) { |
||
109 | resp, err := agi.execute("DATABASE DELTREE", family, keytree) |
||
110 | if err != nil { |
||
111 | return false, err |
||
112 | } |
||
113 | ok := resp.code == 200 && resp.result == 1 |
||
114 | return ok, nil |
||
115 | } |
||
116 | |||
117 | // DatabaseGet Retrieves an entry in the Asterisk database for a given family and key. |
||
118 | // Returns value as string or error if failed or value not set |
||
119 | func (agi *AGI) DatabaseGet(family, key string) (string, error) { |
||
120 | resp, err := agi.execute("DATABASE GET", family, key) |
||
121 | if err != nil { |
||
122 | return "", err |
||
123 | } |
||
124 | if resp.result == 0 { |
||
125 | return "", errorNew("Value not set.") |
||
126 | } |
||
127 | return resp.value, nil |
||
128 | } |
||
129 | |||
130 | // DatabasePut adds or updates an entry in the Asterisk database for |
||
131 | // a given family, key, and value. |
||
132 | func (agi *AGI) DatabasePut(family, key, val string) (bool, error) { |
||
133 | resp, err := agi.execute("DATABASE PUT", family, key, val) |
||
134 | if err != nil { |
||
135 | return false, err |
||
136 | } |
||
137 | ok := resp.code == 200 && resp.result == 1 |
||
138 | return ok, nil |
||
139 | } |
||
140 | |||
141 | // Exec executes application with given options. |
||
142 | func (agi *AGI) Exec(app, opts string) (int, error) { |
||
143 | opts = `"` + opts + `"` |
||
144 | resp, err := agi.execute("EXEC", app, opts) |
||
145 | if err != nil { |
||
146 | return -1, err |
||
147 | } |
||
148 | |||
149 | if resp.result == -2 { |
||
150 | return -2, errorNew("Could not find application " + app) |
||
151 | } |
||
152 | return int(resp.result), nil |
||
153 | } |
||
154 | |||
155 | // GetData Stream the given file, and receive DTMF data. |
||
156 | func (agi *AGI) GetData(file string, args ...interface{}) (digit string, timeout bool, err error) { |
||
157 | cmd := "GET DATA " + file |
||
158 | resp, err := agi.execute(cmd, args...) |
||
159 | if err != nil { |
||
160 | return "", false, err |
||
161 | } |
||
162 | re := regexp.MustCompile(`200 result=([\d*]+)`) |
||
163 | result := re.FindStringSubmatch(resp.raw) |
||
164 | |||
165 | if len(result) > 1 { |
||
166 | return result[1], false, nil |
||
167 | } |
||
168 | |||
169 | if resp.result < 0 { |
||
170 | return "", false, errorNew("Failed get data.") |
||
171 | } |
||
172 | timeout = resp.value == "timeout" |
||
173 | digit = "" //string(resp.result) |
||
174 | return |
||
175 | } |
||
176 | |||
177 | // GetFullVariable evaluates a channel expression |
||
178 | func (agi *AGI) GetFullVariable(name string, channel ...string) (string, error) { |
||
179 | cmd := "GET FULL VARIABLE " + name |
||
180 | var resp *agiResp |
||
181 | var err error |
||
182 | if len(channel) > 0 { |
||
183 | resp, err = agi.execute(cmd, channel[0]) |
||
184 | } else { |
||
185 | resp, err = agi.execute(cmd) |
||
186 | } |
||
187 | if err != nil { |
||
188 | return "", err |
||
189 | } |
||
190 | if resp.result == 0 { |
||
191 | return "", errorNew("Variable is not set.") |
||
192 | } |
||
193 | |||
194 | return resp.value, nil |
||
195 | } |
||
196 | |||
197 | // GetOption Stream file, prompt for DTMF, with timeout. |
||
198 | // Behaves similar to STREAM FILE but used with a timeout option. |
||
199 | // Returns digit pressed, offset and error |
||
200 | func (agi *AGI) GetOption(filename, digits string, timeout int32) (int, int32, error) { |
||
201 | cmd := "GET OPTION " + filename |
||
202 | resp, err := agi.execute(cmd, digits, timeout) |
||
203 | if err != nil { |
||
204 | return -1, 0, err |
||
205 | } |
||
206 | |||
207 | if resp.result == -1 { |
||
208 | return -1, 0, errorNew("Command failure") |
||
209 | } |
||
210 | |||
211 | if resp.result == 0 && resp.endpos == 0 { |
||
212 | return -1, 0, errorNew("Failure on open") |
||
213 | } |
||
214 | |||
215 | return int(resp.result), resp.endpos, nil |
||
216 | } |
||
217 | |||
218 | // GetVariable Gets a channel variable. |
||
219 | func (agi *AGI) GetVariable(name string) (string, error) { |
||
220 | resp, err := agi.execute("GET VARIABLE", name) |
||
221 | if err != nil { |
||
222 | return "", err |
||
223 | } |
||
224 | if resp.result == 0 { |
||
225 | return "", errorNew("Variable is not set.") |
||
226 | } |
||
227 | |||
228 | return resp.value, nil |
||
229 | } |
||
230 | |||
231 | // GetVariable Gets a channel variable. |
||
0 ignored issues
–
show
introduced
by
![]() |
|||
232 | func (agi *AGI) GoSub(ctx, ext, prio, args string) (bool, error) { |
||
233 | resp, err := agi.execute("GOSUB", ctx, ext, prio, args) |
||
234 | if err != nil { |
||
235 | return false, err |
||
236 | } |
||
237 | if resp.result == -1 { |
||
238 | return false, errorNew("Failed Gosub") |
||
239 | } |
||
240 | if resp.result == 0 && resp.code == 100 { |
||
241 | chStr, chErr := agi.read() |
||
242 | select { |
||
243 | case str := <-chStr: |
||
244 | resp, err = parseResponse(str) |
||
245 | if err != nil { |
||
246 | return false, err |
||
247 | } |
||
248 | return resp.code == 200, nil |
||
249 | case err := <-chErr: |
||
250 | return false, err |
||
251 | } |
||
252 | } |
||
253 | return true, nil |
||
254 | } |
||
255 | |||
256 | // Hangup a channel. |
||
257 | // Hangs up the specified channel. If no channel name is given, hangs up the current channel |
||
258 | func (agi *AGI) Hangup(channel ...string) (bool, error) { |
||
259 | cmd := "HANGUP" |
||
260 | if len(channel) > 0 { |
||
261 | cmd += " " + channel[0] |
||
262 | } |
||
263 | resp, err := agi.execute(cmd) |
||
264 | if err != nil { |
||
265 | return false, err |
||
266 | } |
||
267 | if resp.result == -1 { |
||
268 | return false, errorNew("Failed hangup") |
||
269 | } |
||
270 | return true, nil |
||
271 | } |
||
272 | |||
273 | // Noop Does nothing. |
||
274 | func (agi *AGI) Noop() error { |
||
275 | _, err := agi.execute("NOOP") |
||
276 | return err |
||
277 | } |
||
278 | |||
279 | // ReceiveChar Receives one character from channels supporting it. |
||
280 | // Most channels do not support the reception of text. Returns the decimal value of |
||
281 | // the character if one is received, or 0 if the channel does not support text reception. |
||
282 | // timeout - The maximum time to wait for input in milliseconds, or 0 for infinite. Most channels |
||
283 | // Returns -1 only on error/hangup. |
||
284 | func (agi *AGI) ReceiveChar(timeout int32) (int, error) { |
||
285 | resp, err := agi.execute("RECEIVE CHAR", timeout) |
||
286 | if err != nil { |
||
287 | return -1, err |
||
288 | } |
||
289 | if resp.result == -1 { |
||
290 | return -1, errorNew("Channel error or hangup.") |
||
291 | } |
||
292 | if resp.result == 0 { |
||
293 | return -1, errorNew("Channel does not support text reception.") |
||
294 | } |
||
295 | return int(resp.result), nil |
||
296 | } |
||
297 | |||
298 | // ReceiveText Receives text from channels supporting it. |
||
299 | // timeout - The timeout to be the maximum time to wait for input in milliseconds, or 0 for infinite. |
||
300 | func (agi *AGI) ReceiveText(timeout int32) (string, error) { |
||
301 | resp, err := agi.execute("RECEIVE TEXT", timeout) |
||
302 | if err != nil { |
||
303 | return "", err |
||
304 | } |
||
305 | if resp.result == -1 { |
||
306 | return "", errorNew("Failure, hangup or timeout.") |
||
307 | } |
||
308 | return resp.value, nil |
||
309 | } |
||
310 | |||
311 | // RecordFile Record to a file until a given dtmf digit in the sequence is received. |
||
312 | // The format will specify what kind of file will be recorded. The timeout is the |
||
313 | // maximum record time in milliseconds, or -1 for no timeout. |
||
314 | // offset samples is optional, and, if provided, will seek to the offset without |
||
315 | // exceeding the end of the file. |
||
316 | // beep causes Asterisk to play a beep to the channel that is about to be recorded. |
||
317 | // silence is the number of seconds of silence allowed before the function returns |
||
318 | // despite the lack of dtmf digits or reaching timeout. |
||
319 | // silence is the number of seconds of silence that are permitted before the |
||
320 | // recording is terminated, regardless of the escape_digits or timeout arguments |
||
321 | func (agi *AGI) RecordFile(file, format, escDigits string, |
||
322 | timeout, offset int, beep bool, silence int) error { |
||
323 | |||
324 | cmd := "RECORD FILE" |
||
325 | cmd = fmt.Sprintf("%s %s %s %s", cmd, file, format, escDigits) |
||
326 | if beep { |
||
327 | cmd = fmt.Sprintf("%s BEEP", cmd) |
||
328 | } |
||
329 | if silence > 0 { |
||
330 | cmd = fmt.Sprintf("%s s=%d", cmd, silence) |
||
331 | } |
||
332 | resp, err := agi.execute(cmd) |
||
333 | if err != nil { |
||
334 | return err |
||
335 | } |
||
336 | if resp.result <= 0 { |
||
337 | return errorNew("Failed record file") |
||
338 | } |
||
339 | return nil |
||
340 | } |
||
341 | |||
342 | func (agi *AGI) say(cmd string, args ...interface{}) error { |
||
343 | resp, err := agi.execute("SAY "+cmd, args...) |
||
344 | if err != nil { |
||
345 | return err |
||
346 | } |
||
347 | if resp.result < 0 { |
||
348 | return errorNew("Failure") |
||
349 | } |
||
350 | return nil |
||
351 | } |
||
352 | |||
353 | // SayAlpha says a given character string, returning early if any of the given |
||
354 | // DTMF digits are received on the channel. |
||
355 | func (agi *AGI) SayAlpha(number, escDigits string) error { |
||
356 | return agi.say("ALPHA", number, escDigits) |
||
357 | } |
||
358 | |||
359 | // SayDate say a given date, returning early if any of the given DTMF digits |
||
360 | // are received on the channel |
||
361 | func (agi *AGI) SayDate(date, escDigits string) error { |
||
362 | return agi.say("DATE", date, escDigits) |
||
363 | } |
||
364 | |||
365 | // SayDatetime say a given time, returning early if any of the given DTMF |
||
366 | // digits are received on the channel |
||
367 | func (agi *AGI) SayDatetime(time, escDigits, format, timezone string) error { |
||
368 | return agi.say("DATETIME", time, escDigits, format, timezone) |
||
369 | } |
||
370 | |||
371 | // SayDigits say a given digit string, returning early if any of the given |
||
372 | // DTMF digits are received on the channel |
||
373 | func (agi *AGI) SayDigits(number, escDigits string) error { |
||
374 | return agi.say("DIGITS", number, escDigits) |
||
375 | } |
||
376 | |||
377 | // SayNumber say a given digit string, returning early if any of the given |
||
378 | // DTMF digits are received on the channel |
||
379 | func (agi *AGI) SayNumber(number, escDigits string) error { |
||
380 | return agi.say("NUMBER", number, escDigits) |
||
381 | } |
||
382 | |||
383 | // SayPhonetic say a given character string with phonetics, returning early |
||
384 | // if any of the given DTMF digits are received on the channel |
||
385 | func (agi *AGI) SayPhonetic(str, escDigits string) error { |
||
386 | return agi.say("PHONETIC", str, escDigits) |
||
387 | } |
||
388 | |||
389 | // SayTime say a given time, returning early if any of the given DTMF digits |
||
390 | // are received on the channel |
||
391 | func (agi *AGI) SayTime(time, escDigits string) error { |
||
392 | return agi.say("TIME", time, escDigits) |
||
393 | } |
||
394 | |||
395 | // SendImage Sends the given image on a channel. Most channels do not support |
||
396 | // the transmission of images. |
||
397 | func (agi *AGI) SendImage(image string) error { |
||
398 | resp, err := agi.execute("SEND IMAGE", image) |
||
399 | if err != nil { |
||
400 | return err |
||
401 | } |
||
402 | if resp.result < 0 { |
||
403 | return errorNew("Failure or hangup.") |
||
404 | } |
||
405 | return nil |
||
406 | } |
||
407 | |||
408 | // SendText Sends the given text on a channel. Most channels do not support |
||
409 | // the transmission of text. |
||
410 | func (agi *AGI) SendText(text string) error { |
||
411 | text = fmt.Sprintf("\"%s\"", text) |
||
412 | resp, err := agi.execute("SEND TEXT", text) |
||
413 | if err != nil { |
||
414 | return err |
||
415 | } |
||
416 | if resp.result < 0 { |
||
417 | return errorNew("Failure or hangup.") |
||
418 | } |
||
419 | return nil |
||
420 | } |
||
421 | |||
422 | // SetAutoHangup Cause the channel to automatically hangup at time seconds in the future. |
||
423 | // Setting to 0 will cause the autohangup feature to be disabled on this channel. |
||
424 | func (agi *AGI) SetAutoHangup(seconds int) error { |
||
425 | resp, err := agi.execute("SET AUTOHANGUP", seconds) |
||
426 | if err != nil { |
||
427 | return err |
||
428 | } |
||
429 | if resp.result != 0 { |
||
430 | return errorNew("Failure or hangup.") |
||
431 | } |
||
432 | return nil |
||
433 | } |
||
434 | |||
435 | // SetCallerid Changes the callerid of the current channel. |
||
436 | func (agi *AGI) SetCallerid(clid string) error { |
||
437 | resp, err := agi.execute("SET CALLERID", clid) |
||
438 | if err != nil { |
||
439 | return err |
||
440 | } |
||
441 | if resp.result != 1 { |
||
442 | return errorNew("Failure or hangup.") |
||
443 | } |
||
444 | return nil |
||
445 | } |
||
446 | |||
447 | // SetContext Sets the context for continuation upon exiting the application. |
||
448 | func (agi *AGI) SetContext(ctx string) error { |
||
449 | resp, err := agi.execute("SET CONTEXT", ctx) |
||
450 | if err != nil { |
||
451 | return err |
||
452 | } |
||
453 | if resp.result != 0 { |
||
454 | return errorNew("Failure or hangup.") |
||
455 | } |
||
456 | return nil |
||
457 | } |
||
458 | |||
459 | // SetExtension Changes the extension for continuation upon exiting the application. |
||
460 | func (agi *AGI) SetExtension(ext string) error { |
||
461 | resp, err := agi.execute("SET EXTENSION", ext) |
||
462 | if err != nil { |
||
463 | return err |
||
464 | } |
||
465 | if resp.result != 0 { |
||
466 | return errorNew("Failure or hangup.") |
||
467 | } |
||
468 | return nil |
||
469 | } |
||
470 | |||
471 | // SetMusic Enables/Disables the music on hold generator. If class is not specified, |
||
472 | // then the default music on hold class will be used. |
||
473 | // Parameters: opt is "on" or "off", and music class as string |
||
474 | func (agi *AGI) SetMusic(opt string, class ...string) error { |
||
475 | if opt != "on" && opt != "off" { |
||
476 | return errorNew("Invalid opt: '" + opt + "'. Must be 'on' or 'off'.") |
||
477 | } |
||
478 | |||
479 | if class != nil { |
||
480 | opt = fmt.Sprintf("%s %s", opt, class[0]) |
||
481 | } |
||
482 | |||
483 | resp, err := agi.execute("SET MUSIC", opt) |
||
484 | if err != nil { |
||
485 | return err |
||
486 | } |
||
487 | if resp.result != 0 { |
||
488 | return errorNew("Failure or hangup.") |
||
489 | } |
||
490 | return nil |
||
491 | } |
||
492 | |||
493 | // SetPriority Changes the priority for continuation upon exiting the application. |
||
494 | // The priority must be a valid priority or label. |
||
495 | func (agi *AGI) SetPriority(priority string) error { |
||
496 | resp, err := agi.execute("SET PRIORITY", priority) |
||
497 | if err != nil { |
||
498 | return err |
||
499 | } |
||
500 | if resp.result != 0 { |
||
501 | return errorNew("Failure or hangup.") |
||
502 | } |
||
503 | return nil |
||
504 | } |
||
505 | |||
506 | // SetVariable Sets a variable to the current channel. |
||
507 | func (agi *AGI) SetVariable(name, value string) error { |
||
508 | value = fmt.Sprintf("\"%s\"", value) |
||
509 | resp, err := agi.execute("SET VARIABLE", name, value) |
||
510 | if err != nil { |
||
511 | return err |
||
512 | } |
||
513 | if resp.result != 1 { |
||
514 | return errorNew("Failure or hangup.") |
||
515 | } |
||
516 | return nil |
||
517 | } |
||
518 | |||
519 | // StreamFile Send the given file, allowing playback to be interrupted by the given |
||
520 | // digits, if any. |
||
521 | func (agi *AGI) StreamFile(file, escDigits string, offset int) (int, error) { |
||
522 | resp, err := agi.execute("STREAM FILE", file, escDigits, offset) |
||
523 | if err != nil { |
||
524 | return -1, err |
||
525 | } |
||
526 | if resp.result == -1 { |
||
527 | return -1, errorNew("Failure or hangup.") |
||
528 | } |
||
529 | return int(resp.result), nil |
||
530 | } |
||
531 | |||
532 | // TDDMode Enable/Disable TDD transmission/reception on a channel. |
||
533 | // Modes: on, off, mate, tdd |
||
534 | func (agi *AGI) TDDMode(mode string) error { |
||
535 | resp, err := agi.execute("TDD MODE", mode) |
||
536 | if err != nil { |
||
537 | return err |
||
538 | } |
||
539 | if resp.result != 1 { |
||
540 | return errorNew("Failure or hangup.") |
||
541 | } |
||
542 | return nil |
||
543 | } |
||
544 | |||
545 | // Verbose Sends message to the console via verbose message system. |
||
546 | // level is the verbose level (1-4) |
||
547 | func (agi *AGI) Verbose(msg string, level ...int) error { |
||
548 | var err error |
||
549 | msg = fmt.Sprintf("\"%s\"", msg) |
||
550 | if level == nil { |
||
551 | _, err = agi.execute("VERBOSE", msg) |
||
552 | return err |
||
553 | } |
||
554 | |||
555 | lvl := level[0] |
||
556 | if lvl < 1 && lvl > 4 { |
||
557 | lvl = 1 |
||
558 | } |
||
559 | _, err = agi.execute("VERBOSE", msg, lvl) |
||
560 | return err |
||
561 | } |
||
562 | |||
563 | // WaitForDigit Waits up to timeout *milliseconds* for channel to receive a DTMF digit. |
||
564 | // Use -1 for the timeout value if you desire the call to block indefinitely. |
||
565 | // Return digit pressed as string or error |
||
566 | func (agi *AGI) WaitForDigit(timeout int) (string, error) { |
||
567 | resp, err := agi.execute("WAIT FOR DIGIT", timeout) |
||
568 | if err != nil { |
||
569 | return "", err |
||
570 | } |
||
571 | if resp.result == -1 { |
||
572 | return "", errorNew("Failed run command") |
||
573 | } |
||
574 | if resp.result == 0 { |
||
575 | return "", nil |
||
576 | } |
||
577 | return string(resp.result), nil |
||
578 | } |
||
579 |