]> code.delx.au - gnu-emacs-elpa/blob - packages/debbugs/debbugs.el
Decode xsd:base64Binary values in debbugs.el
[gnu-emacs-elpa] / packages / debbugs / debbugs.el
1 ;;; debbugs.el --- SOAP library to access debbugs servers
2
3 ;; Copyright (C) 2011-2016 Free Software Foundation, Inc.
4
5 ;; Author: Michael Albinus <michael.albinus@gmx.de>
6 ;; Keywords: comm, hypermedia
7 ;; Package: debbugs
8 ;; Version: 0.9
9 ;; Package-Requires: ((async "1.6"))
10
11 ;; This file is not part of GNU Emacs.
12
13 ;; This program is free software: you can redistribute it and/or modify
14 ;; it under the terms of the GNU General Public License as published by
15 ;; the Free Software Foundation, either version 3 of the License, or
16 ;; (at your option) any later version.
17
18 ;; This program is distributed in the hope that it will be useful,
19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ;; GNU General Public License for more details.
22
23 ;; You should have received a copy of the GNU General Public License
24 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
25
26 ;;; Commentary:
27
28 ;; This package provides basic functions to access a Debbugs SOAP
29 ;; server (see <http://wiki.debian.org/DebbugsSoapInterface>).
30
31 ;; The function "get_versions" is not implemented (yet). "search_est"
32 ;; is an extension on <http://debbugs.gnu.org>.
33
34 ;;; Code:
35
36 ;(setq soap-debug t message-log-max t)
37 (require 'soap-client)
38 (eval-when-compile (require 'cl))
39
40 (declare-function soap-invoke-async "soap-client")
41 (declare-function async-start "async")
42 (declare-function async-get "async")
43
44 (defgroup debbugs nil
45 "Debbugs library"
46 :group 'hypermedia)
47
48 (defcustom debbugs-servers
49 '(("gnu.org"
50 :wsdl "http://debbugs.gnu.org/cgi/soap.cgi?WSDL"
51 :bugreport-url "http://debbugs.gnu.org/cgi/bugreport.cgi")
52 ("debian.org"
53 :wsdl "http://bugs.debian.org/cgi-bin/soap.cgi?WSDL"
54 :bugreport-url "http://bugs.debian.org/cgi-bin/bugreport.cgi"))
55 "*List of Debbugs server specifiers.
56 Each entry is a list that contains a string identifying the port
57 name and the server parameters in keyword-value form. Allowed
58 keywords are:
59
60 `:wsdl' -- Location of WSDL. The value is a string with URL that
61 should return the WSDL specification of Debbugs/SOAP service.
62
63 `:bugreport-url' -- URL of the server script that returns mboxes
64 with bug logs.
65
66 The list initially contains two predefined and configured Debbugs
67 servers: \"gnu.org\" and \"debian.org\"."
68 :group 'debbugs
69 :link '(custom-manual "(debbugs)Debbugs server specifiers")
70 :type '(choice
71 (const nil)
72 (repeat
73 (cons :tag "Server"
74 (string :tag "Port name")
75 (checklist :tag "Options" :greedy t
76 (group :inline t
77 (const :format "" :value :wsdl)
78 (string :tag "WSDL"))
79 (group :inline t
80 (const :format "" :value :bugreport-url)
81 (string :tag "Bugreport URL")))))))
82
83 (defcustom debbugs-port "gnu.org"
84 "The port instance to be applied from `debbugs-wsdl'.
85 This corresponds to the Debbugs server to be accessed, either
86 \"gnu.org\", or \"debian.org\", or user defined port name."
87 ;; Maybe we should create an own group?
88 :group 'debbugs
89 :type '(choice :tag "Debbugs server" (const "gnu.org") (const "debian.org")
90 (string :tag "user defined port name")))
91
92 ;; It would be nice if we could retrieve it from the debbugs server.
93 ;; Not supported yet.
94 (defconst debbugs-wsdl
95 (soap-load-wsdl
96 (expand-file-name
97 "Debbugs.wsdl"
98 (if load-in-progress
99 (file-name-directory load-file-name)
100 default-directory)))
101 "The WSDL object to be used describing the SOAP interface.")
102
103 ;; Please do not increase this value, otherwise we would run into
104 ;; performance problems on the server. Maybe we need to change this a
105 ;; server specific value.
106 (defconst debbugs-max-hits-per-request 500
107 "The max number of bugs or results per soap invocation.")
108
109 (defvar debbugs-cache-data
110 (make-hash-table :test 'equal :size debbugs-max-hits-per-request)
111 "Hash table of retrieved bugs.")
112
113 (defcustom debbugs-cache-expiry (* 60 60)
114 "How many seconds debbugs query results are cached.
115 `t' or 0 disables caching, `nil' disables expiring."
116 :group 'debbugs
117 :type '(choice (const :tag "Always" t)
118 (const :tag "Never" nil)
119 (integer :tag "Seconds")))
120
121 (defvar debbugs-soap-invoke-async-object nil
122 "The object manipulated by `debbugs-soap-invoke-async'.")
123
124 (defun debbugs-soap-invoke-async (operation-name &rest parameters)
125 "Invoke the SOAP connection asynchronously.
126 If possible, it uses `soap-invoke-async' from soapclient 3.0.
127 Otherwise, `async-start' from the async package is used."
128 (if (fboundp 'soap-invoke-async)
129 ;; This is soap-client 3.0.
130 (apply
131 'soap-invoke-async
132 (lambda (response &rest args)
133 (setq debbugs-soap-invoke-async-object
134 (append debbugs-soap-invoke-async-object (car response))))
135 nil
136 debbugs-wsdl debbugs-port operation-name parameters)
137 ;; Fallback with async.
138 (async-start
139 `(lambda ()
140 (load ,(locate-library "soap-client"))
141 (apply
142 'soap-invoke
143 (soap-load-wsdl
144 ,(expand-file-name
145 "Debbugs.wsdl"
146 (file-name-directory (locate-library "debbugs"))))
147 ,debbugs-port ,operation-name ',parameters)))))
148
149 (defun debbugs-get-bugs (&rest query)
150 "Return a list of bug numbers which match QUERY.
151
152 QUERY is a sequence of keyword-value pairs where the values are
153 strings, i.e. :KEYWORD \"VALUE\" [:KEYWORD \"VALUE\"]*
154
155 The keyword-value pair is a subquery. The keywords are allowed to
156 have multiple occurrence within the query at any place. The
157 subqueries with the same keyword form the logical subquery, which
158 returns the union of bugs of every subquery it contains.
159
160 The result of the QUERY is an intersection of results of all
161 subqueries.
162
163 Valid keywords are:
164
165 :package -- The value is the name of the package a bug belongs
166 to, like \"emacs\", \"coreutils\", \"gnus\", or \"tramp\".
167
168 :src -- This is used to retrieve bugs that belong to source
169 with given name.
170
171 :severity -- This is the severity of the bug. The exact set of
172 allowed values depends on the Debbugs port. Examples are
173 \"normal\", \"minor\", \"wishlist\" etc.
174
175 :tag -- An arbitrary string the bug is annotated with.
176 Usually, this is used to mark the status of the bug, like
177 \"fixed\", \"moreinfo\", \"notabug\", \"patch\",
178 \"unreproducible\" or \"wontfix\". The exact set of tags
179 depends on the Debbugs port.
180
181 :owner -- This is used to identify bugs by the owner's email
182 address. The special email address \"me\" is used as pattern,
183 replaced with `user-mail-address'.
184
185 :submitter -- With this keyword it is possible to filter bugs
186 by the submitter's email address. The special email address
187 \"me\" is used as pattern, replaced with `user-mail-address'.
188
189 :maint -- This is used to find bugs of the packages which are
190 maintained by the person with the given email address. The
191 special email address \"me\" is used as pattern, replaced with
192 `user-mail-address'.
193
194 :correspondent -- This allows to find bug reports where the
195 person with the given email address has participated. The
196 special email address \"me\" is used as pattern, replaced with
197 `user-mail-address'.
198
199 :affects -- With this keyword it is possible to find bugs which
200 affect the package with the given name. The bugs are chosen by
201 the value of field `affects' in bug's status. The returned bugs
202 do not necessary belong to this package.
203
204 :status -- Status of bug. Valid values are \"done\",
205 \"forwarded\" and \"open\".
206
207 :archive -- A keyword to filter for bugs which are already
208 archived, or not. Valid values are \"0\" (not archived),
209 \"1\" (archived) or \"both\". If this keyword is not given in
210 the query, `:archive \"0\"' is assumed by default.
211
212 Example. Get all opened and forwarded release critical bugs for
213 the packages which are maintained by \"me\" and which have a
214 patch:
215
216 \(debbugs-get-bugs :maint \"me\" :tag \"patch\"
217 :severity \"critical\"
218 :status \"open\"
219 :severity \"grave\"
220 :status \"forwarded\"
221 :severity \"serious\")"
222
223 (let (vec kw key val)
224 ;; Check query.
225 (while (and (consp query) (<= 2 (length query)))
226 (setq kw (pop query)
227 val (pop query))
228 (unless (and (keywordp kw) (stringp val))
229 (error "Wrong query: %s %s" kw val))
230 (setq key (substring (symbol-name kw) 1))
231 (case kw
232 ((:package :severity :tag :src :affects)
233 ;; Value shall be one word.
234 (if (string-match "\\`\\S-+\\'" val)
235 (setq vec (vconcat vec (list key val)))
236 (error "Wrong %s: %s" key val)))
237 ((:owner :submitter :maint :correspondent)
238 ;; Value is an email address.
239 (if (string-match "\\`\\S-+\\'" val)
240 (progn
241 (when (string-equal "me" val)
242 (setq val user-mail-address))
243 (when (string-match "<\\(.+\\)>" val)
244 (setq val (match-string 1 val)))
245 (setq vec (vconcat vec (list key val))))
246 (error "Wrong %s: %s" key val)))
247 (:status
248 ;; Possible values: "done", "forwarded" and "open"
249 (if (string-match "\\`\\(done\\|forwarded\\|open\\)\\'" val)
250 (setq vec (vconcat vec (list key val)))
251 (error "Wrong %s: %s" key val)))
252 (:archive
253 ;; Value is `0' or `1' or `both'.
254 (if (string-match "\\`\\(0\\|1\\|both\\)\\'" val)
255 (setq vec (vconcat vec (list key val)))
256 (error "Wrong %s: %s" key val)))
257 (t (error "Unknown key: %s" kw))))
258
259 (unless (null query)
260 (error "Unknown key: %s" (car query)))
261 (sort (car (soap-invoke debbugs-wsdl debbugs-port "get_bugs" vec)) '<)))
262
263 (defun debbugs-newest-bugs (amount)
264 "Return the list of bug numbers, according to AMOUNT (a number) latest bugs."
265 (sort (car (soap-invoke debbugs-wsdl debbugs-port "newest_bugs" amount)) '<))
266
267 (defun debbugs-convert-soap-value-to-string (string-value)
268 "If STRING-VALUE is unibyte, decode its contents as a UTF-8 string.
269 If STRING-VALUE is a multibyte string, then `soap-client'
270 received an xsd:string for this value, and will have decoded it
271 already.
272
273 If STRING-VALUE is a unibyte string, then `soap-client' received
274 an xsd:base64Binary, and ran `base64-decode-string' on it to
275 produce a unibyte string of bytes.
276
277 For some reason, the Debbugs server code base64-encodes strings
278 that contain UTF-8 characters, and returns them as
279 xsd:base64Binary, instead of just returning them as xsd:string.
280 Therefore, when STRING-VALUE is a unibyte string, we assume its
281 bytes represent a UTF-8 string and decode them accordingly."
282 (if (stringp string-value)
283 (if (not (multibyte-string-p string-value))
284 (decode-coding-string string-value 'utf-8)
285 string-value)
286 (error "Invalid string value")))
287
288 (defun debbugs-get-status (&rest bug-numbers)
289 "Return a list of status entries for the bugs identified by BUG-NUMBERS.
290
291 Every returned entry is an association list with the following attributes:
292
293 `bug_num': The bug number.
294
295 `package': A list of package names the bug belongs to.
296
297 `severity': The severity of the bug report. This can be
298 \"critical\", \"grave\", \"serious\", \"important\",
299 \"normal\", \"minor\" or \"wishlist\".
300
301 `tags': The status of the bug report, a list of strings. This
302 can be \"fixed\", \"notabug\", \"wontfix\", \"unreproducible\",
303 \"moreinfo\" or \"patch\".
304
305 `pending': The string \"pending\", \"forwarded\" or \"done\".
306
307 `subject': Subject/Title of the bugreport.
308
309 `originator': Submitter of the bugreport.
310
311 `mergedwith': A list of bug numbers this bug was merged with.
312 If it is a single bug, then this attribute contains just a
313 number.
314
315 `source': Source package name of the bug report.
316
317 `date': Date of bug creation.
318
319 `log_modified', `last_modified': Date of last update.
320
321 `found_date', `fixed_date': Date of bug report / bug fix
322 \(empty for now).
323
324 `done': The email address of the worker who has closed the bug (if done).
325
326 `archived': `t' if the bug is archived, `nil' otherwise.
327
328 `unarchived': The date the bug has been unarchived, if ever.
329
330 `found_versions', `fixed_versions': List of version strings.
331
332 `forwarded': A URL or an email address.
333
334 `blocks': A list of bug numbers this bug blocks.
335
336 `blockedby': A list of bug numbers this bug is blocked by.
337
338 `msgid': The message id of the initial bug report.
339
340 `owner': Who is responsible for fixing.
341
342 `location': Always the string \"db-h\" or \"archive\".
343
344 `affects': A list of package names.
345
346 `summary': Arbitrary text.
347
348 Example:
349
350 \(debbugs-get-status 10)
351
352 => ;; Attributes with empty values are not shown
353 \(\(\(bug_num . 10)
354 \(source . \"unknown\")
355 \(date . 1203606305.0)
356 \(msgid . \"<87zltuz7eh.fsf@freemail.hu>\")
357 \(severity . \"wishlist\")
358 \(owner . \"Magnus Henoch <mange@freemail.hu>\")
359 \(log_modified . 1261079402.0)
360 \(location . \"db-h\")
361 \(subject . \"url-gw should support HTTP CONNECT proxies\")
362 \(originator . \"Magnus Henoch <mange@freemail.hu>\")
363 \(last_modified . 1271200046.0)
364 \(pending . \"pending\")
365 \(package \"emacs\")))"
366 (let (cached-bugs)
367 ;; Check for cached bugs.
368 (setq bug-numbers (delete-dups bug-numbers)
369 bug-numbers
370 (delete
371 nil
372 (mapcar
373 (lambda (bug)
374 (let ((status (gethash bug debbugs-cache-data)))
375 (if (and
376 status
377 (or
378 (null debbugs-cache-expiry)
379 (and
380 (natnump debbugs-cache-expiry)
381 (> (cdr (assoc 'cache_time status))
382 (- (float-time)) debbugs-cache-expiry))))
383 (progn
384 (setq cached-bugs (append cached-bugs (list status)))
385 nil)
386 bug)))
387 bug-numbers)))
388
389 ;; Retrieve the data.
390 (setq debbugs-soap-invoke-async-object nil)
391 (when bug-numbers
392 ;; Retrieve bugs asynchronously.
393 (let ((bug-ids bug-numbers)
394 results)
395 (while bug-ids
396 (setq results
397 (append
398 results
399 (list
400 (debbugs-soap-invoke-async
401 "get_status"
402 (apply
403 'vector
404 (butlast
405 bug-ids (- (length bug-ids)
406 debbugs-max-hits-per-request))))))
407
408 bug-ids
409 (last bug-ids (- (length bug-ids)
410 debbugs-max-hits-per-request))))
411
412 (dolist (res results)
413 (if (bufferp res)
414 ;; This is soap-client 3.0.
415 (while (buffer-live-p res)
416 (accept-process-output (get-buffer-process res) 0.1))
417 ;; Fallback with async.
418 (dolist (status (async-get res))
419 (setq debbugs-soap-invoke-async-object
420 (append debbugs-soap-invoke-async-object status)))))))
421
422 (append
423 cached-bugs
424 ;; Massage results.
425 (mapcar
426 (lambda (x)
427 (let (y)
428 ;; "archived" is the number 1 or 0.
429 (setq y (assoc 'archived (cdr (assoc 'value x))))
430 (setcdr y (= (cdr y) 1))
431 ;; "found_versions" and "fixed_versions" are lists,
432 ;; containing strings or numbers.
433 (dolist (attribute '(found_versions fixed_versions))
434 (setq y (assoc attribute (cdr (assoc 'value x))))
435 (setcdr y (mapcar
436 (lambda (z) (if (numberp z) (number-to-string z) z))
437 (cdr y))))
438 ;; "mergedwith", "blocks" and "blockedby are strings,
439 ;; containing blank separated bug numbers.
440 (dolist (attribute '(mergedwith blocks blockedby))
441 (setq y (assoc attribute (cdr (assoc 'value x))))
442 (when (stringp (cdr y))
443 (setcdr y (mapcar
444 'string-to-number (split-string (cdr y) " " t)))))
445 ;; "originator" may be an xsd:base64Binary value containing
446 ;; a UTF-8-encoded string.
447 (dolist (attribute '(originator))
448 (setq y (assoc attribute (cdr (assoc 'value x))))
449 (setcdr y (debbugs-convert-soap-value-to-string (cdr y))))
450 ;; "package" is a string, containing comma separated
451 ;; package names. "keywords" and "tags" are strings,
452 ;; containing blank separated package names.
453 (dolist (attribute '(package keywords tags))
454 (setq y (assoc attribute (cdr (assoc 'value x))))
455 (when (stringp (cdr y))
456 (setcdr y (split-string (cdr y) ",\\| " t))))
457 ;; Cache the result, and return.
458 (if (and debbugs-cache-expiry (natnump debbugs-cache-expiry))
459 (puthash
460 (cdr (assoc 'key x))
461 ;; Put also a time stamp.
462 (cons (cons 'cache_time (floor (float-time)))
463 (cdr (assoc 'value x)))
464 debbugs-cache-data)
465 ;; Don't cache.
466 (cdr (assoc 'value x)))))
467 debbugs-soap-invoke-async-object))))
468
469 (defun debbugs-get-usertag (&rest query)
470 "Return a list of bug numbers which match QUERY.
471
472 QUERY is a sequence of keyword-value pairs where the values are
473 strings, i.e. :KEYWORD \"VALUE\" [:KEYWORD \"VALUE\"]*
474
475 Valid keywords are:
476
477 :user -- The value is the name of the package a bug belongs to,
478 like \"emacs\", \"coreutils\", \"gnus\", or \"tramp\". It can
479 also be an email address of a user who has applied a user tag.
480 The special email address \"me\" is used as pattern, replaced
481 with `user-mail-address'. There must be at least one such
482 entry; it is recommended to have exactly one.
483
484 :tag -- A string applied as user tag. Often, it is a
485 subproduct identification, like \"cedet\" or \"tramp\" for the
486 package \"emacs\".
487
488 If there is no :tag entry, no bug numbers will be returned but a list of
489 existing user tags for :user.
490
491 Example:
492
493 \(debbugs-get-usertag :user \"emacs\")
494
495 => (\"www\" \"solaris\" \"ls-lisp\" \"cygwin\")
496
497 \(debbugs-get-usertag :user \"emacs\" :tag \"www\" :tag \"cygwin\")
498
499 => (807 1223 5637)"
500
501 (let (user tags kw key val object result)
502 ;; Check query.
503 (while (and (consp query) (<= 2 (length query)))
504 (setq kw (pop query)
505 val (pop query))
506 (unless (and (keywordp kw) (stringp val))
507 (error "Wrong query: %s %s" kw val))
508 (setq key (substring (symbol-name kw) 1))
509 (case kw
510 ((:user)
511 ;; Value shall be one word. Extract email address, if existing.
512 (if (string-match "\\`\\S-+\\'" val)
513 (progn
514 (when (string-equal "me" val)
515 (setq val user-mail-address))
516 (when (string-match "<\\(.+\\)>" val)
517 (setq val (match-string 1 val)))
518 (pushnew val user :test #'equal))
519 (error "Wrong %s: %s" key val)))
520 ((:tag)
521 ;; Value shall be one word.
522 (if (string-match "\\`\\S-+\\'" val)
523 (pushnew val tags :test #'equal)
524 (error "Wrong %s: %s" key val)))
525 (t (error "Unknown key: %s" kw))))
526
527 (unless (null query)
528 (error "Unknown key: %s" (car query)))
529 (unless (= (length user) 1)
530 (error "There must be exactly one :user entry"))
531
532 (setq
533 object
534 (car (soap-invoke debbugs-wsdl debbugs-port "get_usertag" (car user))))
535
536 (if (null tags)
537 ;; Return the list of existing tags.
538 (mapcar (lambda (x) (symbol-name (car x))) object)
539
540 ;; Return bug numbers.
541 (dolist (elt object result)
542 (when (member (symbol-name (car elt)) tags)
543 (setq result (append (cdr elt) result)))))))
544
545 (defun debbugs-get-bug-log (bug-number)
546 "Return a list of messages related to BUG-NUMBER.
547
548 Every message is an association list with the following attributes:
549
550 `msg_num': The number of the message inside the bug log. The
551 numbers are ascending, newer messages have a higher number.
552
553 `header': The message header lines, as arrived at the bug tracker.
554
555 `body': The message body.
556
557 `attachments' A list of possible attachments, or `nil'. Not
558 implemented yet server side."
559 (car (soap-invoke debbugs-wsdl debbugs-port "get_bug_log" bug-number)))
560
561 (defun debbugs-search-est (&rest query)
562 "Return the result of a full text search according to QUERY.
563
564 QUERY is a sequence of lists of keyword-value pairs where the
565 values are strings or numbers, i.e. :KEYWORD \"VALUE\" [:KEYWORD
566 VALUE]*
567
568 Every sublist of the QUERY forms a hyperestraier condition. A
569 detailed description of hyperestraier conditions can be found at
570 URL `http://fallabs.com/hyperestraier/uguide-en.html#searchcond'.
571
572 The following conditions are possible:
573
574 \[:phrase SEARCH-PHRASE :skip NUMBER :max NUMBER\]
575
576 The string SEARCH-PHRASE forms the search on the database. It
577 contains words to be searched for, combined by operators like
578 AND, ANDNOT and OR. If there is no operator between the words,
579 AND is used by default. The phrase keyword and value can also
580 be omitted, this is useful in combination with other conditions.
581
582 :skip and :max are optional. They specify, how many hits are
583 skipped, and how many maximal hits are returned. This can be
584 used for paged results. Per default, :skip is 0 and all
585 possible hits are returned.
586
587 There must be exactly one such condition.
588
589 \[ATTRIBUTE VALUE+ :operation OPERATION :order ORDER\]
590
591 ATTRIBUTE is one of the following keywords:
592
593 :status -- Status of bug. Valid values are \"done\",
594 \"forwarded\" and \"open\".
595
596 :subject, :@title -- The subject of a message or the title of
597 the bug, a string.
598
599 :date, :@cdate -- The submission or modification dates of a
600 message, a number.
601
602 :submitter, :@author -- The email address of the submitter of a
603 bug or the author of a message belonging to this bug, a string.
604 The special email address \"me\" is used as pattern, replaced
605 with `user-mail-address'.
606
607 :package -- The value is the name of the package a bug belongs
608 to, like \"emacs\", \"coreutils\", \"gnus\", or \"tramp\".
609
610 :tags -- An arbitrary string the bug is annotated with.
611
612 :severity -- This is the severity of the bug. The exact set of
613 allowed values depends on the Debbugs port. Examples are
614 \"normal\", \"minor\", \"wishlist\" etc.
615
616 :operator defines the comparison operator to be applied to
617 ATTRIBUTE. For string attributes this could be \"STREQ\" \(is
618 equal to the string), \"STRNE\" \(is not equal to the string),
619 \"STRINC\" \(includes the string), \"STRBW\" \(begins with the
620 string), \"STREW\" \(ends with the string), \"STRAND\"
621 \(includes all tokens in the string), \"STROR\" \(includes at
622 least one token in the string), \"STROREQ\" \(is equal to at
623 least one token in the string) or \"STRRX\" \(matches regular
624 expressions of the string). For operators with tokens, several
625 values for ATTRIBUTE shall be used.
626
627 Numbers can be compared by the operators \"NUMEQ\" \(is equal
628 to the number), \"NUMNE\" \(is not equal to the number),
629 \"NUMGT\" \(is greater than the number), \"NUMGE\" \(is greater
630 than or equal to the number), \"NUMLT\" \(is less than the
631 number), \"NUMLE\" \(is less than or equal to the number) or
632 \"NUMBT\" \(is between the two numbers). In the last case,
633 there must be two values for ATTRIBUTE.
634
635 If an operator is leaded by \"!\", the meaning is inverted. If
636 a string operator is leaded by \"I\", the case of the value is
637 ignored.
638
639 The optional :order can be specified only in one condition. It
640 means, that ATTRIBUTE is used for sorting the results. The
641 following order operators exist: \"STRA\" \(ascending by
642 string), \"STRD\" \(descending by string), \"NUMA\" \(ascending
643 by number) or \"NUMD\" \(descending by number).
644
645 A special case is an :order, where there is no corresponding
646 attribute value and no operator. In this case, ATTRIBUTE is
647 not used for the search.
648
649 The result of the QUERY is a list of association lists with the
650 same attributes as in the conditions. Additional attributes are
651
652 `id': The bug number.
653
654 `msg_num': The number of the message inside the bug log.
655
656 `snippet': The surrounding text found by the search. For the
657 syntax of the snippet, consult the hyperestraier user guide.
658
659 Examples:
660
661 \(debbugs-search-est
662 '\(:phrase \"armstrong AND debbugs\" :skip 10 :max 2)
663 '\(:severity \"normal\" :operator \"STRINC\")
664 '\(:date :order \"NUMA\"))
665
666 => \(\(\(msg_num . 21)
667 \(date . 1229208302)
668 \(@author . \"Glenn Morris <rgm@gnu.org>\")
669 \(@title . \"Re: bug#1567: Mailing an archived bug\")
670 \(id . 1567)
671 \(severity . \"normal\")
672 \(@cdate . \"Wed, 17 Dec 2008 14:34:50 -0500\")
673 \(snippet . \"...\")
674 \(subject . \"Mailing an archived bug\")
675 \(package . \"debbugs.gnu.org\"))
676 ...)
677
678 ;; Show all messages from me between 2011-08-01 and 2011-08-31.
679 \(debbugs-search-est
680 '\(:max 20)
681 '\(:@author \"me\" :operator \"ISTRINC\")
682 `\(:date
683 ,\(floor \(float-time \(encode-time 0 0 0 1 8 2011)))
684 ,\(floor \(float-time \(encode-time 0 0 0 31 8 2011)))
685 :operator \"NUMBT\"))"
686
687 (let ((phrase (assoc :phrase query))
688 args result)
689 (if (and phrase (not (member :skip phrase)) (not (member :skip phrase)))
690 ;; We loop, until we have all results.
691 (let ((skip 0)
692 (query (delete phrase query))
693 result1)
694 (while skip
695 (setq result1
696 (apply
697 'debbugs-search-est
698 (append
699 (list
700 (append
701 phrase `(:skip ,skip)
702 `(:max ,debbugs-max-hits-per-request)))
703 query))
704 skip (and (= (length result1) debbugs-max-hits-per-request)
705 (+ skip debbugs-max-hits-per-request))
706 result (append result result1)))
707 result)
708
709 ;; Compile search arguments.
710 (dolist (elt query)
711 (let (vec kw key val
712 phrase-cond attr-cond)
713
714 ;; Phrase is mandatory, even if empty.
715 (when (and (or (member :skip elt) (member :max elt))
716 (not (member :phrase elt)))
717 (setq vec (vector "phrase" "")))
718
719 ;; Parse condition.
720 (while (consp elt)
721 (setq kw (pop elt))
722 (unless (keywordp kw)
723 (error "Wrong keyword: %s" kw))
724 (setq key (substring (symbol-name kw) 1))
725 (cl-case kw
726 ;; Phrase condition.
727 (:phrase
728 ;; It shouldn't happen in an attribute condition.
729 (if attr-cond
730 (error "Wrong keyword: %s" kw))
731 (setq phrase-cond t val (pop elt))
732 ;; Value is a string.
733 (if (stringp val)
734 (setq vec (vconcat vec (list key val)))
735 (error "Wrong %s: %s" key val)))
736
737 ((:skip :max)
738 ;; It shouldn't happen in an attribute condition.
739 (if attr-cond
740 (error "Wrong keyword: %s" kw))
741 (setq phrase-cond t val (pop elt))
742 ;; Value is a number.
743 (if (numberp val)
744 (setq vec (vconcat vec (list key (number-to-string val))))
745 (error "Wrong %s: %s" key val)))
746
747 ;; Attribute condition.
748 ((:submitter :@author)
749 ;; It shouldn't happen in a phrase condition.
750 (if phrase-cond
751 (error "Wrong keyword: %s" kw))
752 (if (not (stringp (car elt)))
753 (setq vec (vconcat vec (list key "")))
754 ;; Value is an email address.
755 (while (and (stringp (car elt))
756 (string-match "\\`\\S-+\\'" (car elt)))
757 (when (string-equal "me" (car elt))
758 (setcar elt user-mail-address))
759 (when (string-match "<\\(.+\\)>" (car elt))
760 (setcar elt (match-string 1 (car elt))))
761 (let ((x (pop elt)))
762 (unless (member x val)
763 (setq val (append val (list x))))))
764 (setq vec
765 (vconcat vec (list key (mapconcat 'identity val " "))))))
766
767 (:status
768 ;; It shouldn't happen in a phrase condition.
769 (if phrase-cond
770 (error "Wrong keyword: %s" kw))
771 (setq attr-cond t)
772 (if (not (stringp (car elt)))
773 (setq vec (vconcat vec (list key "")))
774 ;; Possible values: "done", "forwarded" and "open"
775 (while (and (stringp (car elt))
776 (string-match
777 "\\`\\(done\\|forwarded\\|open\\)\\'" (car elt)))
778 (let ((x (pop elt)))
779 (unless (member x val)
780 (setq val (append val (list x))))))
781 (setq vec
782 (vconcat vec (list key (mapconcat 'identity val " "))))))
783
784 ((:subject :package :tags :severity :@title)
785 ;; It shouldn't happen in a phrase condition.
786 (if phrase-cond
787 (error "Wrong keyword: %s" kw))
788 (setq attr-cond t)
789 (if (not (stringp (car elt)))
790 (setq vec (vconcat vec (list key "")))
791 ;; Just a string.
792 (while (stringp (car elt))
793 (let ((x (pop elt)))
794 (unless (member x val)
795 (setq val (append val (list x))))))
796 (setq vec
797 (vconcat vec (list key (mapconcat 'identity val " "))))))
798
799 ((:date :@cdate)
800 ;; It shouldn't happen in a phrase condition.
801 (if phrase-cond
802 (error "Wrong keyword: %s" kw))
803 (setq attr-cond t)
804 (if (not (numberp (car elt)))
805 (setq vec (vconcat vec (list key "")))
806 ;; Just a number.
807 (while (numberp (car elt))
808 (let ((x (pop elt)))
809 (unless (member x val)
810 (setq val (append val (list x))))))
811 (setq vec
812 (vconcat
813 vec (list key (mapconcat 'number-to-string val " "))))))
814
815 ((:operator :order)
816 ;; It shouldn't happen in a phrase condition.
817 (if phrase-cond
818 (error "Wrong keyword: %s" kw))
819 (setq attr-cond t val (pop elt))
820 ;; Value is a number.
821 (if (stringp val)
822 (setq vec (vconcat vec (list key val)))
823 (error "Wrong %s: %s" key val)))
824
825 (t (error "Unknown key: %s" kw))))
826
827 (setq args (vconcat args (list vec)))))
828
829 (setq result
830 (car (soap-invoke debbugs-wsdl debbugs-port "search_est" args)))
831 ;; The result contains lists (key value). We transform it into
832 ;; cons cells (key . value).
833 (dolist (elt1 result result)
834 (dolist (elt2 elt1)
835 (setcdr elt2 (cadr elt2)))))))
836
837 (defun debbugs-get-attribute (bug-or-message attribute)
838 "Return the value of key ATTRIBUTE.
839
840 BUG-OR-MESSAGE must be list element returned by either
841 `debbugs-get-status' or `debbugs-get-bug-log'.
842
843 Example: Return the originator of last submitted bug.
844
845 \(debbugs-get-attribute
846 \(car \(apply 'debbugs-get-status \(debbugs-newest-bugs 1))) 'originator)"
847 (cdr (assoc attribute bug-or-message)))
848
849 (defun debbugs-get-message-numbers (messages)
850 "Return the message numbers of MESSAGES.
851 MESSAGES must be the result of a `debbugs-get-bug-log' call."
852 (mapcar (lambda (x) (debbugs-get-attribute x 'msg_num)) messages))
853
854 (defun debbugs-get-message (messages message-number)
855 "Return the message MESSAGE-NUMBER of MESSAGES.
856 MESSAGES must be the result of a `debbugs-get-bug-log' call.
857
858 The returned message is a list of strings. The first element are
859 the header lines of the message, the second element is the body
860 of the message. Further elements of the list, if any, are
861 attachments of the message.
862
863 If there is no message with MESSAGE-NUMBER, the function returns `nil'.
864
865 Example: Return the first message of last submitted bug.
866
867 \(let \(\(messages \(apply 'debbugs-get-bug-log \(debbugs-newest-bugs 1))))
868 \(debbugs-get-message messages
869 \(car \(debbugs-get-message-numbers messages))))"
870 (while (and messages
871 (/= (debbugs-get-attribute (car messages) 'msg_num)
872 message-number))
873 (setq messages (cdr messages)))
874 (when messages
875 (append (list (debbugs-get-attribute (car messages) 'header)
876 (debbugs-get-attribute (car messages) 'body))
877 (debbugs-get-attribute (car messages) 'attachments))))
878
879 (defun debbugs-get-mbox (bug-number mbox-type &optional filename)
880 "Download mbox with messages of bug BUG-NUMBER from Debbugs server.
881 BUG-NUMBER is a number of bug. It must be of integer type.
882
883 MBOX-TYPE specifies a type of mbox and can be one of the
884 following symbols:
885
886 `mboxfolder': Download mbox folder.
887
888 `mboxmaint': Download maintainer's mbox.
889
890 `mboxstat', `mboxstatus': Download status mbox. The use of
891 either symbol depends on actual Debbugs server configuration.
892 For gnu.org, use the former; for debian.org - the latter.
893
894 FILENAME, if non-`nil', is the name of file to store mbox. If
895 FILENAME is `nil', the downloaded mbox is inserted into the
896 current buffer."
897 (let (url (mt "") bn)
898 (unless (setq url (plist-get
899 (cdr (assoc debbugs-port debbugs-servers))
900 :bugreport-url))
901 (error "URL of bugreport script for port %s is not specified"
902 debbugs-port))
903 (setq bn (format "bug=%s;" (number-to-string bug-number)))
904 (unless (eq mbox-type 'mboxfolder)
905 (if (memq mbox-type '(mboxmaint mboxstat mboxstatus))
906 (setq mt (concat (symbol-name mbox-type) "=yes;"))
907 (error "Unknown mbox type: %s" mbox-type)))
908 (setq url (concat url (format "?%s%smbox=yes" bn mt)))
909 (if filename
910 (url-copy-file url filename t)
911 (url-insert-file-contents url))))
912
913 (provide 'debbugs)
914
915 ;;; TODO:
916
917 ;; * SOAP interface extensions (wishlist).
918 ;; - Server-side sorting.
919 ;; - Regexp and/or wildcards search.
920 ;; - Returning message attachments.
921
922 ;;; debbugs.el ends here