emacs/lisp/gnus/gnus-icalendar.el

992 lines
38 KiB
EmacsLisp
Raw Normal View History

;;; gnus-icalendar.el --- reply to iCalendar meeting requests -*- lexical-binding:t -*-
;; Copyright (C) 2013-2019 Free Software Foundation, Inc.
;; Author: Jan Tatarik <Jan.Tatarik@gmail.com>
;; Keywords: mail, icalendar, org
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; To install:
;; (require 'gnus-icalendar)
;; (gnus-icalendar-setup)
;; to enable optional iCalendar->Org sync functionality
;; NOTE: both the capture file and the headline(s) inside must already exist
;; (setq gnus-icalendar-org-capture-file "~/org/notes.org")
;; (setq gnus-icalendar-org-capture-headline '("Calendar"))
;; (gnus-icalendar-org-setup)
;;; Code:
(require 'icalendar)
(require 'eieio)
(require 'gmm-utils)
(require 'mm-decode)
(require 'gnus-sum)
(require 'gnus-art)
gnus: replace cl with cl-lib * lisp/gnus/gnus-agent.el, lisp/gnus/gnus-art.el: * lisp/gnus/gnus-async.el, lisp/gnus/gnus-cache.el: * lisp/gnus/gnus-demon.el, lisp/gnus/gnus-group.el: * lisp/gnus/gnus-icalendar.el, lisp/gnus/gnus-logic.el: * lisp/gnus/gnus-msg.el, lisp/gnus/gnus-picon.el: * lisp/gnus/gnus-registry.el, lisp/gnus/gnus-salt.el: * lisp/gnus/gnus-score.el, lisp/gnus/gnus-spec.el: * lisp/gnus/gnus-srvr.el, lisp/gnus/gnus-start.el: * lisp/gnus/gnus-sum.el, lisp/gnus/gnus-topic.el: * lisp/gnus/gnus-util.el, lisp/gnus/gnus-uu.el, lisp/gnus/gnus-win.el: * lisp/gnus/mail-source.el, lisp/gnus/mm-decode.el: * lisp/gnus/mm-encode.el, lisp/gnus/mm-url.el, lisp/gnus/mm-view.el: * lisp/gnus/mml-smime.el, lisp/gnus/mml.el, lisp/gnus/mml2015.el: * lisp/gnus/nnbabyl.el, lisp/gnus/nndoc.el, lisp/gnus/nneething.el: * lisp/gnus/nnheader.el, lisp/gnus/nnimap.el, lisp/gnus/nnmail.el: * lisp/gnus/nnmaildir.el, lisp/gnus/nnoo.el, lisp/gnus/nnrss.el: * lisp/gnus/nnspool.el, lisp/gnus/nntp.el, lisp/gnus/nnvirtual.el: * lisp/gnus/nnweb.el, lisp/gnus/spam.el: Replace cl with cl-lib. * lisp/gnus/canlock.el, lisp/gnus/gnus-bcklg.el: * lisp/gnus/gnus-cite.el, lisp/gnus/gnus-cloud.el: * lisp/gnus/gnus-draft.el, lisp/gnus/gnus-dup.el: * lisp/gnus/gnus-fun.el, lisp/gnus/gnus-html.el: * lisp/gnus/gnus-int.el, lisp/gnus/gnus-kill.el, lisp/gnus/gnus-ml.el: * lisp/gnus/gnus-mlspl.el, lisp/gnus/gnus-range.el: * lisp/gnus/gnus-undo.el, lisp/gnus/gnus-vm.el: * lisp/gnus/mm-partial.el, lisp/gnus/mm-uu.el, lisp/gnus/mml1991.el: * lisp/gnus/nnagent.el, lisp/gnus/nndiary.el, lisp/gnus/nndir.el: * lisp/gnus/nndraft.el, lisp/gnus/nnfolder.el, lisp/gnus/nngateway.el: * lisp/gnus/nnmairix.el, lisp/gnus/nnmbox.el, lisp/gnus/nnmh.el: * lisp/gnus/nnml.el, lisp/gnus/score-mode.el, lisp/gnus/smiley.el: No need for cl.
2018-03-23 16:13:09 -04:00
(eval-when-compile (require 'cl-lib))
(defun gnus-icalendar-find-if (pred seq)
(catch 'found
(while seq
(when (funcall pred (car seq))
(throw 'found (car seq)))
(pop seq))))
;;;
;;; ical-event
;;;
(defclass gnus-icalendar-event ()
((organizer :initarg :organizer
:accessor gnus-icalendar-event:organizer
:initform ""
:type (or null string))
(summary :initarg :summary
:accessor gnus-icalendar-event:summary
:initform ""
:type (or null string))
(description :initarg :description
:accessor gnus-icalendar-event:description
:initform ""
:type (or null string))
(location :initarg :location
:accessor gnus-icalendar-event:location
:initform ""
:type (or null string))
(start-time :initarg :start-time
:accessor gnus-icalendar-event:start-time
:initform ""
:type (or null t))
(end-time :initarg :end-time
:accessor gnus-icalendar-event:end-time
:initform ""
:type (or null t))
(recur :initarg :recur
:accessor gnus-icalendar-event:recur
:initform ""
:type (or null string))
(uid :initarg :uid
:accessor gnus-icalendar-event:uid
:type string)
(method :initarg :method
:accessor gnus-icalendar-event:method
:initform "PUBLISH"
:type (or null string))
(rsvp :initarg :rsvp
:accessor gnus-icalendar-event:rsvp
:initform nil
:type (or null boolean))
(participation-type :initarg :participation-type
:accessor gnus-icalendar-event:participation-type
:initform 'non-participant
:type (or null t))
(req-participants :initarg :req-participants
:accessor gnus-icalendar-event:req-participants
:initform nil
:type (or null t))
(opt-participants :initarg :opt-participants
:accessor gnus-icalendar-event:opt-participants
:initform nil
:type (or null t)))
"generic iCalendar Event class")
(defclass gnus-icalendar-event-request (gnus-icalendar-event)
nil
"iCalendar class for REQUEST events")
(defclass gnus-icalendar-event-cancel (gnus-icalendar-event)
nil
"iCalendar class for CANCEL events")
(defclass gnus-icalendar-event-reply (gnus-icalendar-event)
nil
"iCalendar class for REPLY events")
(cl-defmethod gnus-icalendar-event:recurring-p ((event gnus-icalendar-event))
"Return t if EVENT is recurring."
(not (null (gnus-icalendar-event:recur event))))
(cl-defmethod gnus-icalendar-event:recurring-freq ((event gnus-icalendar-event))
"Return recurring frequency of EVENT."
(let ((rrule (gnus-icalendar-event:recur event)))
(string-match "FREQ=\\([[:alpha:]]+\\)" rrule)
(match-string 1 rrule)))
(cl-defmethod gnus-icalendar-event:recurring-interval ((event gnus-icalendar-event))
"Return recurring interval of EVENT."
(let ((rrule (gnus-icalendar-event:recur event))
(default-interval 1))
(string-match "INTERVAL=\\([[:digit:]]+\\)" rrule)
(or (match-string 1 rrule)
default-interval)))
(cl-defmethod gnus-icalendar-event:start ((event gnus-icalendar-event))
(format-time-string "%Y-%m-%d %H:%M" (gnus-icalendar-event:start-time event)))
(defun gnus-icalendar-event--decode-datefield (event field zone-map)
(let* ((dtdate (icalendar--get-event-property event field))
(dtdate-zone (icalendar--find-time-zone
(icalendar--get-event-property-attributes
event field) zone-map))
(dtdate-dec (icalendar--decode-isodatetime dtdate nil dtdate-zone)))
Simplify use of encode-time Most uses of (apply #'encode-time foo) can now be replaced with (encode-time foo). Make similar simplifications. * lisp/calendar/time-date.el (date-to-time): * lisp/calendar/timeclock.el (timeclock-when-to-leave) (timeclock-day-base, timeclock-generate-report): * lisp/emacs-lisp/timer.el (timer-set-idle-time): * lisp/eshell/esh-util.el (eshell-parse-ange-ls): * lisp/gnus/gnus-art.el (article-make-date-line): * lisp/gnus/gnus-delay.el (gnus-delay-article) (gnus-delay-send-queue): * lisp/gnus/gnus-icalendar.el (gnus-icalendar-event--decode-datefield): * lisp/gnus/gnus-logic.el (gnus-advanced-date): * lisp/gnus/message.el (message-make-expires-date): * lisp/gnus/nndiary.el (nndiary-compute-reminders): * lisp/mail/ietf-drums.el (ietf-drums-parse-date): * lisp/net/tramp-adb.el (tramp-adb-ls-output-time-less-p): * lisp/org/org-agenda.el (org-agenda-get-timestamps) (org-agenda-get-progress, org-agenda-show-clocking-issues): * lisp/org/org-capture.el (org-capture-set-target-location): * lisp/org/org-clock.el (org-clock-get-sum-start, org-clock-sum) (org-clocktable-steps): * lisp/org/org-colview.el (org-colview-construct-allowed-dates) * lisp/org/org-macro.el (org-macro--vc-modified-time): * lisp/org/org-table.el (org-table-eval-formula): * lisp/org/org.el (org-current-time, org-store-link) (org-time-today, org-read-date, org-read-date-display) (org-display-custom-time, org-time-string-to-time) (org-timestamp-change, org-timestamp--to-internal-time): * lisp/url/url-dav.el (url-dav-process-date-property): * lisp/vc/vc-cvs.el (vc-cvs-annotate-current-time) (vc-cvs-parse-entry): Simplify use of encode-time. * lisp/org/org-clock.el (org-clock-get-clocked-time): (org-clock-resolve, org-resolve-clocks, org_clock_out) (org-clock-update-time-maybe): Avoid some rounding problems with encode-time and float-time. * lisp/org/org-clock.el (org-clock-in, org-clock-update-time-maybe): * lisp/org/org-colview.el (org-columns--age-to-minutes): * lisp/org/org.el (org-get-scheduled-time, org-get-deadline-time) (org-add-planning-info, org-2ft, org-time-string-to-absolute) (org-closest-date): Use org-time-string-to-time instead of doing it by hand with encode-time. * lisp/org/org.el (org-current-time): Simplify rounding. (org-read-date): Avoid extra trip through encode-time.
2019-02-10 20:25:22 -08:00
(encode-time dtdate-dec)))
(defun gnus-icalendar-event--find-attendee (ical name-or-email)
(let* ((event (car (icalendar--all-events ical)))
(event-props (caddr event)))
(cl-labels ((attendee-name (att) (plist-get (cadr att) 'CN))
(attendee-email
(att)
(replace-regexp-in-string "^.*MAILTO:" "" (caddr att)))
(attendee-prop-matches-p
(prop)
(and (eq (car prop) 'ATTENDEE)
(or (member (attendee-name prop) name-or-email)
(let ((att-email (attendee-email prop)))
(gnus-icalendar-find-if
(lambda (email)
(string-match email att-email))
name-or-email))))))
(gnus-icalendar-find-if #'attendee-prop-matches-p event-props))))
(defun gnus-icalendar-event--get-attendee-names (ical)
(let* ((event (car (icalendar--all-events ical)))
(attendee-props (seq-filter
(lambda (p) (eq (car p) 'ATTENDEE))
(caddr event))))
(cl-labels
((attendee-role (prop) (plist-get (cadr prop) 'ROLE))
(attendee-name
(prop)
(or (plist-get (cadr prop) 'CN)
(replace-regexp-in-string "^.*MAILTO:" "" (caddr prop))))
(attendees-by-type (type)
(seq-filter
(lambda (p) (string= (attendee-role p) type))
attendee-props))
(attendee-names-by-type
(type)
(mapcar #'attendee-name (attendees-by-type type))))
(list
(attendee-names-by-type "REQ-PARTICIPANT")
(attendee-names-by-type "OPT-PARTICIPANT")))))
(defun gnus-icalendar-event-from-ical (ical &optional attendee-name-or-email)
(let* ((event (car (icalendar--all-events ical)))
(organizer (replace-regexp-in-string
"^.*MAILTO:" ""
(or (icalendar--get-event-property event 'ORGANIZER) "")))
(prop-map '((summary . SUMMARY)
(description . DESCRIPTION)
(location . LOCATION)
(recur . RRULE)
(uid . UID)))
(method (caddr (assoc 'METHOD (caddr (car (nreverse ical))))))
(attendee (when attendee-name-or-email
(gnus-icalendar-event--find-attendee ical attendee-name-or-email)))
(attendee-names (gnus-icalendar-event--get-attendee-names ical))
(role (plist-get (cadr attendee) 'ROLE))
(participation-type (pcase role
("REQ-PARTICIPANT" 'required)
("OPT-PARTICIPANT" 'optional)
(_ 'non-participant)))
(zone-map (icalendar--convert-all-timezones ical))
(args (list :method method
:organizer organizer
:start-time (gnus-icalendar-event--decode-datefield event 'DTSTART zone-map)
:end-time (gnus-icalendar-event--decode-datefield event 'DTEND zone-map)
:rsvp (string= (plist-get (cadr attendee) 'RSVP) "TRUE")
:participation-type participation-type
:req-participants (car attendee-names)
:opt-participants (cadr attendee-names)))
(event-class (cond
((string= method "REQUEST") 'gnus-icalendar-event-request)
((string= method "CANCEL") 'gnus-icalendar-event-cancel)
((string= method "REPLY") 'gnus-icalendar-event-reply)
(t 'gnus-icalendar-event))))
(cl-labels
((map-property
(prop)
(let ((value (icalendar--get-event-property event prop)))
(when value
;; ugly, but cannot get
;;replace-regexp-in-string work with "\\" as
;;REP, plus we should also handle "\\;"
(replace-regexp-in-string
"\\\\," ","
(replace-regexp-in-string
"\\\\n" "\n" (substring-no-properties value))))))
(accumulate-args
(mapping)
gnus: replace cl with cl-lib * lisp/gnus/gnus-agent.el, lisp/gnus/gnus-art.el: * lisp/gnus/gnus-async.el, lisp/gnus/gnus-cache.el: * lisp/gnus/gnus-demon.el, lisp/gnus/gnus-group.el: * lisp/gnus/gnus-icalendar.el, lisp/gnus/gnus-logic.el: * lisp/gnus/gnus-msg.el, lisp/gnus/gnus-picon.el: * lisp/gnus/gnus-registry.el, lisp/gnus/gnus-salt.el: * lisp/gnus/gnus-score.el, lisp/gnus/gnus-spec.el: * lisp/gnus/gnus-srvr.el, lisp/gnus/gnus-start.el: * lisp/gnus/gnus-sum.el, lisp/gnus/gnus-topic.el: * lisp/gnus/gnus-util.el, lisp/gnus/gnus-uu.el, lisp/gnus/gnus-win.el: * lisp/gnus/mail-source.el, lisp/gnus/mm-decode.el: * lisp/gnus/mm-encode.el, lisp/gnus/mm-url.el, lisp/gnus/mm-view.el: * lisp/gnus/mml-smime.el, lisp/gnus/mml.el, lisp/gnus/mml2015.el: * lisp/gnus/nnbabyl.el, lisp/gnus/nndoc.el, lisp/gnus/nneething.el: * lisp/gnus/nnheader.el, lisp/gnus/nnimap.el, lisp/gnus/nnmail.el: * lisp/gnus/nnmaildir.el, lisp/gnus/nnoo.el, lisp/gnus/nnrss.el: * lisp/gnus/nnspool.el, lisp/gnus/nntp.el, lisp/gnus/nnvirtual.el: * lisp/gnus/nnweb.el, lisp/gnus/spam.el: Replace cl with cl-lib. * lisp/gnus/canlock.el, lisp/gnus/gnus-bcklg.el: * lisp/gnus/gnus-cite.el, lisp/gnus/gnus-cloud.el: * lisp/gnus/gnus-draft.el, lisp/gnus/gnus-dup.el: * lisp/gnus/gnus-fun.el, lisp/gnus/gnus-html.el: * lisp/gnus/gnus-int.el, lisp/gnus/gnus-kill.el, lisp/gnus/gnus-ml.el: * lisp/gnus/gnus-mlspl.el, lisp/gnus/gnus-range.el: * lisp/gnus/gnus-undo.el, lisp/gnus/gnus-vm.el: * lisp/gnus/mm-partial.el, lisp/gnus/mm-uu.el, lisp/gnus/mml1991.el: * lisp/gnus/nnagent.el, lisp/gnus/nndiary.el, lisp/gnus/nndir.el: * lisp/gnus/nndraft.el, lisp/gnus/nnfolder.el, lisp/gnus/nngateway.el: * lisp/gnus/nnmairix.el, lisp/gnus/nnmbox.el, lisp/gnus/nnmh.el: * lisp/gnus/nnml.el, lisp/gnus/score-mode.el, lisp/gnus/smiley.el: No need for cl.
2018-03-23 16:13:09 -04:00
(cl-destructuring-bind (slot . ical-property) mapping
(setq args (append (list
(intern (concat ":" (symbol-name slot)))
(map-property ical-property))
args)))))
(mapc #'accumulate-args prop-map)
(apply #'make-instance event-class args))))
(defun gnus-icalendar-event-from-buffer (buf &optional attendee-name-or-email)
"Parse RFC5545 iCalendar in buffer BUF and return an event object.
Return a gnus-icalendar-event object representing the first event
contained in the invitation. Return nil for calendars without an event entry.
ATTENDEE-NAME-OR-EMAIL is a list of strings that will be matched
against the event's attendee names and emails. Invitation rsvp
status will be retrieved from the first matching attendee record."
(let ((ical (with-current-buffer (icalendar--get-unfolded-buffer (get-buffer buf))
(goto-char (point-min))
(icalendar--read-element nil nil))))
(when ical
(gnus-icalendar-event-from-ical ical attendee-name-or-email))))
;;;
;;; gnus-icalendar-event-reply
;;;
(defun gnus-icalendar-event--build-reply-event-body (ical-request status identities)
(let ((summary-status (capitalize (symbol-name status)))
(attendee-status (upcase (symbol-name status)))
reply-event-lines)
(cl-labels
((update-summary
(line)
(if (string-match "^[^:]+:" line)
(replace-match (format "\\&%s: " summary-status) t nil line)
line))
(update-dtstamp ()
(format-time-string "DTSTAMP:%Y%m%dT%H%M%SZ" nil t))
(attendee-matches-identity
(line)
(gnus-icalendar-find-if (lambda (name) (string-match-p name line))
identities))
(update-attendee-status
(line)
(when (and (attendee-matches-identity line)
(string-match "\\(PARTSTAT=\\)[^;]+" line))
(replace-match (format "\\1%s" attendee-status) t nil line)))
(process-event-line
(line)
(when (string-match "^\\([^;:]+\\)" line)
(let* ((key (match-string 0 line))
;; NOTE: not all of the below fields are mandatory,
;; but they are often present in other clients'
;; replies. Can be helpful for debugging, too.
(new-line
(cond
((string= key "ATTENDEE") (update-attendee-status line))
((string= key "SUMMARY") (update-summary line))
((string= key "DTSTAMP") (update-dtstamp))
((member key '("ORGANIZER" "DTSTART" "DTEND"
"LOCATION" "DURATION" "SEQUENCE"
"RECURRENCE-ID" "UID"))
line)
(t nil))))
(when new-line
(push new-line reply-event-lines))))))
(mapc #'process-event-line (split-string ical-request "\n"))
(unless (gnus-icalendar-find-if (lambda (x) (string-match "^ATTENDEE" x))
reply-event-lines)
(error "Could not find an event attendee matching given identity"))
(mapconcat #'identity `("BEGIN:VEVENT"
,@(nreverse reply-event-lines)
"END:VEVENT")
"\n"))))
(defun gnus-icalendar-event-reply-from-buffer (buf status identities)
"Build a calendar event reply for request contained in BUF.
The reply will have STATUS (`accepted', `tentative' or `declined').
The reply will be composed for attendees matching any entry
on the IDENTITIES list."
(cl-labels
((extract-block
(blockname)
(save-excursion
(let ((block-start-re (format "^BEGIN:%s" blockname))
(block-end-re (format "^END:%s" blockname))
start)
(when (re-search-forward block-start-re nil t)
(setq start (line-beginning-position))
(re-search-forward block-end-re)
(buffer-substring-no-properties start (line-end-position)))))))
(let (zone event)
(with-current-buffer (icalendar--get-unfolded-buffer (get-buffer buf))
(goto-char (point-min))
(setq zone (extract-block "VTIMEZONE")
event (extract-block "VEVENT")))
(when event
(let ((contents (list "BEGIN:VCALENDAR"
"METHOD:REPLY"
"PRODID:Gnus"
"VERSION:2.0"
zone
(gnus-icalendar-event--build-reply-event-body event status identities)
"END:VCALENDAR")))
(mapconcat #'identity (delq nil contents) "\n"))))))
;;;
;;; gnus-icalendar-org
;;
;; TODO: this is an optional feature, and it's only available with org-mode
;; 7+, so will need to properly handle emacsen with no/outdated org-mode
(require 'org)
(require 'org-capture)
(defgroup gnus-icalendar-org nil
"Settings for Calendar Event gnus/org integration."
:version "24.4"
:group 'gnus-icalendar
:prefix "gnus-icalendar-org-")
(defcustom gnus-icalendar-org-capture-file nil
"Target Org file for storing captured calendar events."
:type '(choice (const nil) file))
(defcustom gnus-icalendar-org-capture-headline nil
"Target outline in `gnus-icalendar-org-capture-file' for storing captured events."
:type '(repeat string))
(defcustom gnus-icalendar-org-template-name "used by gnus-icalendar-org"
"Org-mode template name."
:type '(string))
(defcustom gnus-icalendar-org-template-key "#"
"Org-mode template hotkey."
:type '(string))
(defvar gnus-icalendar-org-enabled-p nil)
(cl-defmethod gnus-icalendar-event:org-repeat ((event gnus-icalendar-event))
"Return `org-mode' timestamp repeater string for recurring EVENT.
Return nil for non-recurring EVENT."
(when (gnus-icalendar-event:recurring-p event)
(let* ((freq-map '(("HOURLY" . "h")
("DAILY" . "d")
("WEEKLY" . "w")
("MONTHLY" . "m")
("YEARLY" . "y")))
(org-freq (cdr (assoc (gnus-icalendar-event:recurring-freq event) freq-map))))
(when org-freq
(format "+%s%s" (gnus-icalendar-event:recurring-interval event) org-freq)))))
(cl-defmethod gnus-icalendar-event:org-timestamp ((event gnus-icalendar-event))
"Build `org-mode' timestamp from EVENT start/end dates and recurrence info."
(let* ((start (gnus-icalendar-event:start-time event))
(end (gnus-icalendar-event:end-time event))
(start-date (format-time-string "%Y-%m-%d" start))
(start-time (format-time-string "%H:%M" start))
(start-at-midnight (string= start-time "00:00"))
(end-date (format-time-string "%Y-%m-%d" end))
(end-time (format-time-string "%H:%M" end))
(end-at-midnight (string= end-time "00:00"))
(start-end-date-diff
Avoid some double-rounding of Lisp timestamps Also, simplify some time-related Lisp timestamp code while we’re in the neighborhood. * lisp/battery.el (battery-linux-proc-acpi) (battery-linux-sysfs, battery-upower, battery-bsd-apm): * lisp/calendar/timeclock.el (timeclock-seconds-to-string) (timeclock-log, timeclock-last-period) (timeclock-entry-length, timeclock-entry-list-span) (timeclock-find-discrep, timeclock-generate-report): * lisp/cedet/ede/detect.el (ede-detect-qtest): * lisp/completion.el (cmpl-hours-since-origin): * lisp/ecomplete.el (ecomplete-decay-1): * lisp/emacs-lisp/ert.el (ert--results-update-stats-display) (ert--results-update-stats-display-maybe): * lisp/emacs-lisp/timer-list.el (list-timers): * lisp/emacs-lisp/timer.el (timer-until) (timer-event-handler): * lisp/erc/erc-backend.el (erc-server-send-ping) (erc-server-send-queue, erc-handle-parsed-server-response) (erc-handle-unknown-server-response): * lisp/erc/erc-track.el (erc-buffer-visible): * lisp/erc/erc.el (erc-lurker-cleanup, erc-lurker-p) (erc-cmd-PING, erc-send-current-line): * lisp/eshell/em-pred.el (eshell-pred-file-time): * lisp/eshell/em-unix.el (eshell-show-elapsed-time): * lisp/gnus/gnus-icalendar.el (gnus-icalendar-event:org-timestamp): * lisp/gnus/gnus-int.el (gnus-backend-trace): * lisp/gnus/gnus-sum.el (gnus-user-date): * lisp/gnus/mail-source.el (mail-source-delete-crash-box): * lisp/gnus/nnmaildir.el (nnmaildir--scan): * lisp/ibuf-ext.el (ibuffer-mark-old-buffers): * lisp/gnus/nnmaildir.el (nnmaildir--scan): * lisp/mouse.el (mouse--down-1-maybe-follows-link) (mouse--click-1-maybe-follows-link): * lisp/mpc.el (mpc--faster-toggle): * lisp/net/rcirc.el (rcirc-handler-ctcp-KEEPALIVE) (rcirc-sentinel): * lisp/net/tramp-cache.el (tramp-get-file-property): * lisp/net/tramp-sh.el (tramp-sh-handle-file-newer-than-file-p) (tramp-maybe-open-connection): * lisp/net/tramp-smb.el (tramp-smb-maybe-open-connection): * lisp/org/org-clock.el (org-clock-resolve): (org-resolve-clocks, org-clock-in, org-clock-out, org-clock-sum): * lisp/org/org-timer.el (org-timer-start) (org-timer-pause-or-continue, org-timer-seconds): * lisp/org/org.el (org-evaluate-time-range): * lisp/org/ox-publish.el (org-publish-cache-ctime-of-src): * lisp/pixel-scroll.el (pixel-scroll-in-rush-p): * lisp/play/hanoi.el (hanoi-move-ring): * lisp/proced.el (proced-format-time): * lisp/progmodes/cpp.el (cpp-progress-message): * lisp/progmodes/flymake.el (flymake--handle-report): * lisp/progmodes/js.el (js--wait-for-matching-output): * lisp/subr.el (progress-reporter-do-update): * lisp/term/xterm.el (xterm--read-event-for-query): * lisp/time.el (display-time-update, emacs-uptime): * lisp/tooltip.el (tooltip-delay): * lisp/url/url-cookie.el (url-cookie-parse-file-netscape): * lisp/url/url-queue.el (url-queue-prune-old-entries): * lisp/url/url.el (url-retrieve-synchronously): * lisp/xt-mouse.el (xterm-mouse-event): Avoid double-rounding of time-related values. Simplify. * lisp/calendar/icalendar.el (icalendar--decode-isodatetime): When hoping for the best (unlikely), use a better decoded time. (icalendar--convert-sexp-to-ical): Avoid unnecessary encode-time. * lisp/calendar/timeclock.el (timeclock-when-to-leave): * lisp/cedet/ede/detect.el (ede-detect-qtest): * lisp/desktop.el (desktop-create-buffer): * lisp/emacs-lisp/benchmark.el (benchmark-elapse): * lisp/gnus/gnus-art.el (article-lapsed-string): * lisp/gnus/gnus-group.el (gnus-group-timestamp-delta): * lisp/gnus/nnmail.el (nnmail-expired-article-p): * lisp/gnus/nnmaildir.el (nnmaildir-request-expire-articles): * lisp/nxml/rng-maint.el (rng-time-function): * lisp/org/org-clock.el (org-clock-get-clocked-time) (org-clock-resolve, org-resolve-clocks, org-resolve-clocks-if-idle): * lisp/org/org-habit.el (org-habit-insert-consistency-graphs): * lisp/progmodes/vhdl-mode.el (vhdl-update-progress-info) (vhdl-fix-case-region-1): Use time-since instead of open-coding most of it. * lisp/erc/erc-dcc.el (erc-dcc-get-sentinel): * lisp/erc/erc.el (erc-string-to-emacs-time, erc-time-gt): Now obsolete. All uses changed. (erc-time-diff): Accept all Lisp time values. All uses changed. * lisp/gnus/gnus-demon.el (gnus-demon-idle-since): * lisp/gnus/gnus-score.el (gnus-score-headers): * lisp/gnus/nneething.el (nneething-make-head): * lisp/gnus/nnheader.el (nnheader-message-maybe): * lisp/gnus/nnimap.el (nnimap-keepalive): * lisp/image.el (image-animate-timeout): * lisp/mail/feedmail.el (feedmail-rfc822-date): * lisp/net/imap.el (imap-wait-for-tag): * lisp/net/newst-backend.el (newsticker--image-get): * lisp/net/rcirc.el (rcirc-handler-317, rcirc-handler-333): * lisp/obsolete/xesam.el (xesam-refresh-entry): * lisp/org/org-agenda.el (org-agenda-show-clocking-issues) (org-agenda-check-clock-gap, org-agenda-to-appt): * lisp/org/org-capture.el (org-capture-set-target-location): * lisp/org/org-clock.el (org-clock-resolve-clock) (org-clocktable-steps): * lisp/org/org-colview.el (org-columns-edit-value) (org-columns, org-agenda-columns): * lisp/org/org-duration.el (org-duration-from-minutes): * lisp/org/org-element.el (org-element-cache-sync-duration) (org-element-cache-sync-break) (org-element--cache-interrupt-p, org-element--cache-sync): * lisp/org/org-habit.el (org-habit-get-faces) * lisp/org/org-indent.el (org-indent-add-properties): * lisp/org/org-table.el (org-table-sum): * lisp/org/org-timer.el (org-timer-show-remaining-time) (org-timer-set-timer): * lisp/org/org.el (org-babel-load-file, org-today) (org-auto-repeat-maybe, org-2ft, org-time-stamp) (org-read-date-analyze, org-time-stamp-to-now) (org-small-year-to-year, org-goto-calendar): * lisp/org/ox.el (org-export-insert-default-template): * lisp/ses.el (ses--time-check): * lisp/type-break.el (type-break-time-warning) (type-break-statistics, type-break-demo-boring): * lisp/url/url-cache.el (url-cache-expired) (url-cache-prune-cache): * lisp/vc/vc-git.el (vc-git-stash-snapshot): * lisp/erc/erc-match.el (erc-log-matches-come-back): Simplify.
2019-02-22 18:32:31 -08:00
(time-to-number-of-days (time-subtract
(org-time-string-to-time end-date)
(org-time-string-to-time start-date))))
(org-repeat (gnus-icalendar-event:org-repeat event))
(repeat (if org-repeat (concat " " org-repeat) ""))
Avoid some double-rounding of Lisp timestamps Also, simplify some time-related Lisp timestamp code while we’re in the neighborhood. * lisp/battery.el (battery-linux-proc-acpi) (battery-linux-sysfs, battery-upower, battery-bsd-apm): * lisp/calendar/timeclock.el (timeclock-seconds-to-string) (timeclock-log, timeclock-last-period) (timeclock-entry-length, timeclock-entry-list-span) (timeclock-find-discrep, timeclock-generate-report): * lisp/cedet/ede/detect.el (ede-detect-qtest): * lisp/completion.el (cmpl-hours-since-origin): * lisp/ecomplete.el (ecomplete-decay-1): * lisp/emacs-lisp/ert.el (ert--results-update-stats-display) (ert--results-update-stats-display-maybe): * lisp/emacs-lisp/timer-list.el (list-timers): * lisp/emacs-lisp/timer.el (timer-until) (timer-event-handler): * lisp/erc/erc-backend.el (erc-server-send-ping) (erc-server-send-queue, erc-handle-parsed-server-response) (erc-handle-unknown-server-response): * lisp/erc/erc-track.el (erc-buffer-visible): * lisp/erc/erc.el (erc-lurker-cleanup, erc-lurker-p) (erc-cmd-PING, erc-send-current-line): * lisp/eshell/em-pred.el (eshell-pred-file-time): * lisp/eshell/em-unix.el (eshell-show-elapsed-time): * lisp/gnus/gnus-icalendar.el (gnus-icalendar-event:org-timestamp): * lisp/gnus/gnus-int.el (gnus-backend-trace): * lisp/gnus/gnus-sum.el (gnus-user-date): * lisp/gnus/mail-source.el (mail-source-delete-crash-box): * lisp/gnus/nnmaildir.el (nnmaildir--scan): * lisp/ibuf-ext.el (ibuffer-mark-old-buffers): * lisp/gnus/nnmaildir.el (nnmaildir--scan): * lisp/mouse.el (mouse--down-1-maybe-follows-link) (mouse--click-1-maybe-follows-link): * lisp/mpc.el (mpc--faster-toggle): * lisp/net/rcirc.el (rcirc-handler-ctcp-KEEPALIVE) (rcirc-sentinel): * lisp/net/tramp-cache.el (tramp-get-file-property): * lisp/net/tramp-sh.el (tramp-sh-handle-file-newer-than-file-p) (tramp-maybe-open-connection): * lisp/net/tramp-smb.el (tramp-smb-maybe-open-connection): * lisp/org/org-clock.el (org-clock-resolve): (org-resolve-clocks, org-clock-in, org-clock-out, org-clock-sum): * lisp/org/org-timer.el (org-timer-start) (org-timer-pause-or-continue, org-timer-seconds): * lisp/org/org.el (org-evaluate-time-range): * lisp/org/ox-publish.el (org-publish-cache-ctime-of-src): * lisp/pixel-scroll.el (pixel-scroll-in-rush-p): * lisp/play/hanoi.el (hanoi-move-ring): * lisp/proced.el (proced-format-time): * lisp/progmodes/cpp.el (cpp-progress-message): * lisp/progmodes/flymake.el (flymake--handle-report): * lisp/progmodes/js.el (js--wait-for-matching-output): * lisp/subr.el (progress-reporter-do-update): * lisp/term/xterm.el (xterm--read-event-for-query): * lisp/time.el (display-time-update, emacs-uptime): * lisp/tooltip.el (tooltip-delay): * lisp/url/url-cookie.el (url-cookie-parse-file-netscape): * lisp/url/url-queue.el (url-queue-prune-old-entries): * lisp/url/url.el (url-retrieve-synchronously): * lisp/xt-mouse.el (xterm-mouse-event): Avoid double-rounding of time-related values. Simplify. * lisp/calendar/icalendar.el (icalendar--decode-isodatetime): When hoping for the best (unlikely), use a better decoded time. (icalendar--convert-sexp-to-ical): Avoid unnecessary encode-time. * lisp/calendar/timeclock.el (timeclock-when-to-leave): * lisp/cedet/ede/detect.el (ede-detect-qtest): * lisp/desktop.el (desktop-create-buffer): * lisp/emacs-lisp/benchmark.el (benchmark-elapse): * lisp/gnus/gnus-art.el (article-lapsed-string): * lisp/gnus/gnus-group.el (gnus-group-timestamp-delta): * lisp/gnus/nnmail.el (nnmail-expired-article-p): * lisp/gnus/nnmaildir.el (nnmaildir-request-expire-articles): * lisp/nxml/rng-maint.el (rng-time-function): * lisp/org/org-clock.el (org-clock-get-clocked-time) (org-clock-resolve, org-resolve-clocks, org-resolve-clocks-if-idle): * lisp/org/org-habit.el (org-habit-insert-consistency-graphs): * lisp/progmodes/vhdl-mode.el (vhdl-update-progress-info) (vhdl-fix-case-region-1): Use time-since instead of open-coding most of it. * lisp/erc/erc-dcc.el (erc-dcc-get-sentinel): * lisp/erc/erc.el (erc-string-to-emacs-time, erc-time-gt): Now obsolete. All uses changed. (erc-time-diff): Accept all Lisp time values. All uses changed. * lisp/gnus/gnus-demon.el (gnus-demon-idle-since): * lisp/gnus/gnus-score.el (gnus-score-headers): * lisp/gnus/nneething.el (nneething-make-head): * lisp/gnus/nnheader.el (nnheader-message-maybe): * lisp/gnus/nnimap.el (nnimap-keepalive): * lisp/image.el (image-animate-timeout): * lisp/mail/feedmail.el (feedmail-rfc822-date): * lisp/net/imap.el (imap-wait-for-tag): * lisp/net/newst-backend.el (newsticker--image-get): * lisp/net/rcirc.el (rcirc-handler-317, rcirc-handler-333): * lisp/obsolete/xesam.el (xesam-refresh-entry): * lisp/org/org-agenda.el (org-agenda-show-clocking-issues) (org-agenda-check-clock-gap, org-agenda-to-appt): * lisp/org/org-capture.el (org-capture-set-target-location): * lisp/org/org-clock.el (org-clock-resolve-clock) (org-clocktable-steps): * lisp/org/org-colview.el (org-columns-edit-value) (org-columns, org-agenda-columns): * lisp/org/org-duration.el (org-duration-from-minutes): * lisp/org/org-element.el (org-element-cache-sync-duration) (org-element-cache-sync-break) (org-element--cache-interrupt-p, org-element--cache-sync): * lisp/org/org-habit.el (org-habit-get-faces) * lisp/org/org-indent.el (org-indent-add-properties): * lisp/org/org-table.el (org-table-sum): * lisp/org/org-timer.el (org-timer-show-remaining-time) (org-timer-set-timer): * lisp/org/org.el (org-babel-load-file, org-today) (org-auto-repeat-maybe, org-2ft, org-time-stamp) (org-read-date-analyze, org-time-stamp-to-now) (org-small-year-to-year, org-goto-calendar): * lisp/org/ox.el (org-export-insert-default-template): * lisp/ses.el (ses--time-check): * lisp/type-break.el (type-break-time-warning) (type-break-statistics, type-break-demo-boring): * lisp/url/url-cache.el (url-cache-expired) (url-cache-prune-cache): * lisp/vc/vc-git.el (vc-git-stash-snapshot): * lisp/erc/erc-match.el (erc-log-matches-come-back): Simplify.
2019-02-22 18:32:31 -08:00
(time-1-day 86400))
;; NOTE: special care is needed with appointments ending at midnight
;; (typically all-day events): the end time has to be changed to 23:59 to
;; prevent org agenda showing the event on one additional day
(cond
;; start/end midnight
;; A 0:0 - A+1 0:0 -> A
;; A 0:0 - A+n 0:0 -> A - A+n-1
((and start-at-midnight end-at-midnight) (if (> start-end-date-diff 1)
(let ((end-ts (format-time-string "%Y-%m-%d" (time-subtract end time-1-day))))
(format "<%s>--<%s>" start-date end-ts))
(format "<%s%s>" start-date repeat)))
;; end midnight
;; A .:. - A+1 0:0 -> A .:.-23:59
;; A .:. - A+n 0:0 -> A .:. - A_n-1
(end-at-midnight (if (= start-end-date-diff 1)
(format "<%s %s-23:59%s>" start-date start-time repeat)
(let ((end-ts (format-time-string "%Y-%m-%d" (time-subtract end time-1-day))))
(format "<%s %s>--<%s>" start-date start-time end-ts))))
;; start midnight
;; A 0:0 - A .:. -> A 0:0-.:. (default 1)
;; A 0:0 - A+n .:. -> A - A+n .:.
((and start-at-midnight
gnus: replace cl with cl-lib * lisp/gnus/gnus-agent.el, lisp/gnus/gnus-art.el: * lisp/gnus/gnus-async.el, lisp/gnus/gnus-cache.el: * lisp/gnus/gnus-demon.el, lisp/gnus/gnus-group.el: * lisp/gnus/gnus-icalendar.el, lisp/gnus/gnus-logic.el: * lisp/gnus/gnus-msg.el, lisp/gnus/gnus-picon.el: * lisp/gnus/gnus-registry.el, lisp/gnus/gnus-salt.el: * lisp/gnus/gnus-score.el, lisp/gnus/gnus-spec.el: * lisp/gnus/gnus-srvr.el, lisp/gnus/gnus-start.el: * lisp/gnus/gnus-sum.el, lisp/gnus/gnus-topic.el: * lisp/gnus/gnus-util.el, lisp/gnus/gnus-uu.el, lisp/gnus/gnus-win.el: * lisp/gnus/mail-source.el, lisp/gnus/mm-decode.el: * lisp/gnus/mm-encode.el, lisp/gnus/mm-url.el, lisp/gnus/mm-view.el: * lisp/gnus/mml-smime.el, lisp/gnus/mml.el, lisp/gnus/mml2015.el: * lisp/gnus/nnbabyl.el, lisp/gnus/nndoc.el, lisp/gnus/nneething.el: * lisp/gnus/nnheader.el, lisp/gnus/nnimap.el, lisp/gnus/nnmail.el: * lisp/gnus/nnmaildir.el, lisp/gnus/nnoo.el, lisp/gnus/nnrss.el: * lisp/gnus/nnspool.el, lisp/gnus/nntp.el, lisp/gnus/nnvirtual.el: * lisp/gnus/nnweb.el, lisp/gnus/spam.el: Replace cl with cl-lib. * lisp/gnus/canlock.el, lisp/gnus/gnus-bcklg.el: * lisp/gnus/gnus-cite.el, lisp/gnus/gnus-cloud.el: * lisp/gnus/gnus-draft.el, lisp/gnus/gnus-dup.el: * lisp/gnus/gnus-fun.el, lisp/gnus/gnus-html.el: * lisp/gnus/gnus-int.el, lisp/gnus/gnus-kill.el, lisp/gnus/gnus-ml.el: * lisp/gnus/gnus-mlspl.el, lisp/gnus/gnus-range.el: * lisp/gnus/gnus-undo.el, lisp/gnus/gnus-vm.el: * lisp/gnus/mm-partial.el, lisp/gnus/mm-uu.el, lisp/gnus/mml1991.el: * lisp/gnus/nnagent.el, lisp/gnus/nndiary.el, lisp/gnus/nndir.el: * lisp/gnus/nndraft.el, lisp/gnus/nnfolder.el, lisp/gnus/nngateway.el: * lisp/gnus/nnmairix.el, lisp/gnus/nnmbox.el, lisp/gnus/nnmh.el: * lisp/gnus/nnml.el, lisp/gnus/score-mode.el, lisp/gnus/smiley.el: No need for cl.
2018-03-23 16:13:09 -04:00
(cl-plusp start-end-date-diff)) (format "<%s>--<%s %s>" start-date end-date end-time))
;; default
;; A .:. - A .:. -> A .:.-.:.
;; A .:. - B .:.
((zerop start-end-date-diff) (format "<%s %s-%s%s>" start-date start-time end-time repeat))
(t (format "<%s %s>--<%s %s>" start-date start-time end-date end-time)))))
(defun gnus-icalendar--format-summary-line (summary &optional location)
(if location
(format "%s (%s)" summary location)
(format "%s" summary)))
(defun gnus-icalendar--format-participant-list (participants)
(mapconcat #'identity participants ", "))
;; TODO: make the template customizable
(cl-defmethod gnus-icalendar-event->org-entry ((event gnus-icalendar-event) reply-status)
"Return string with new `org-mode' entry describing EVENT."
(with-temp-buffer
(org-mode)
(with-slots (organizer summary description location
recur uid) event
(let* ((reply (if reply-status (capitalize (symbol-name reply-status))
"Not replied yet"))
(props `(("ICAL_EVENT" . "t")
("ID" . ,uid)
("ORGANIZER" . ,(gnus-icalendar-event:organizer event))
("LOCATION" . ,(gnus-icalendar-event:location event))
("PARTICIPATION_TYPE" . ,(symbol-name (gnus-icalendar-event:participation-type event)))
("REQ_PARTICIPANTS" . ,(gnus-icalendar--format-participant-list (gnus-icalendar-event:req-participants event)))
("OPT_PARTICIPANTS" . ,(gnus-icalendar--format-participant-list (gnus-icalendar-event:opt-participants event)))
("RRULE" . ,(gnus-icalendar-event:recur event))
("REPLY" . ,reply))))
(insert (format "* %s\n\n"
(gnus-icalendar--format-summary-line summary location)))
(mapc (lambda (prop)
(org-entry-put (point) (car prop) (cdr prop)))
props))
(when description
(save-restriction
(narrow-to-region (point) (point))
(insert (gnus-icalendar-event:org-timestamp event)
"\n\n"
description)
(indent-region (point-min) (point-max) 2)
(fill-region (point-min) (point-max))))
(buffer-string))))
(defun gnus-icalendar--deactivate-org-timestamp (ts)
(replace-regexp-in-string "[<>]"
(lambda (m) (cond ((string= m "<") "[")
((string= m ">") "]")))
ts))
(defun gnus-icalendar-find-org-event-file (event &optional org-file)
"Return the name of the file containing EVENT org entry.
Return nil when not found.
All org agenda files are searched for the EVENT entry. When
the optional ORG-FILE argument is specified, only that one file
is searched."
(let ((uid (gnus-icalendar-event:uid event))
(files (or org-file (org-agenda-files t 'ifmode))))
(cl-labels
((find-event-in
(file)
(org-check-agenda-file file)
(with-current-buffer (find-file-noselect file)
(let ((event-pos (org-find-entry-with-id uid)))
(when (and event-pos
(string= (cdr (assoc "ICAL_EVENT"
(org-entry-properties event-pos)))
"t"))
(throw 'found file))))))
(gnus-icalendar-find-if #'find-event-in files))))
(defun gnus-icalendar--show-org-event (event &optional org-file)
(let ((file (gnus-icalendar-find-org-event-file event org-file)))
(when file
(switch-to-buffer (find-file file))
(goto-char (org-find-entry-with-id (gnus-icalendar-event:uid event)))
(org-show-entry))))
(defun gnus-icalendar--update-org-event (event reply-status &optional org-file)
(let ((file (gnus-icalendar-find-org-event-file event org-file)))
(when file
(with-current-buffer (find-file-noselect file)
(with-slots (uid summary description organizer location recur
participation-type req-participants opt-participants) event
(let ((event-pos (org-find-entry-with-id uid)))
(when event-pos
(goto-char event-pos)
;; update the headline, keep todo, priority and tags, if any
(save-excursion
(let* ((priority (org-entry-get (point) "PRIORITY"))
(headline (delq nil (list
(org-entry-get (point) "TODO")
(when priority (format "[#%s]" priority))
(gnus-icalendar--format-summary-line summary location)
(org-entry-get (point) "TAGS")))))
(re-search-forward "^\\*+ " (line-end-position))
(delete-region (point) (line-end-position))
(insert (mapconcat #'identity headline " "))))
;; update props and description
(let ((entry-end (org-entry-end-position))
(entry-outline-level (org-outline-level)))
;; delete body of the entry, leave org drawers intact
(save-restriction
(org-narrow-to-element)
(goto-char entry-end)
(re-search-backward "^[\t ]*:END:")
(forward-line)
(delete-region (point) entry-end))
;; put new event description in the entry body
(when description
(save-restriction
(narrow-to-region (point) (point))
(insert "\n"
(gnus-icalendar-event:org-timestamp event)
"\n\n"
(replace-regexp-in-string "[\n]+$" "\n" description)
"\n")
(indent-region (point-min) (point-max) (1+ entry-outline-level))
(fill-region (point-min) (point-max))))
;; update entry properties
(cl-labels
((update-org-entry
(position property value)
(if (or (null value)
(string= value ""))
(org-entry-delete position property)
(org-entry-put position property value))))
(update-org-entry event-pos "ORGANIZER" organizer)
(update-org-entry event-pos "LOCATION" location)
(update-org-entry event-pos "PARTICIPATION_TYPE"
(symbol-name participation-type))
(update-org-entry event-pos "REQ_PARTICIPANTS"
(gnus-icalendar--format-participant-list
req-participants))
(update-org-entry event-pos "OPT_PARTICIPANTS"
(gnus-icalendar--format-participant-list
opt-participants))
(update-org-entry event-pos "RRULE" recur)
(update-org-entry
event-pos "REPLY"
(if reply-status (capitalize (symbol-name reply-status))
"Not replied yet")))
(save-buffer)))))))))
(defun gnus-icalendar--cancel-org-event (event &optional org-file)
(let ((file (gnus-icalendar-find-org-event-file event org-file)))
(when file
(with-current-buffer (find-file-noselect file)
(let ((event-pos (org-find-entry-with-id (gnus-icalendar-event:uid event))))
(when event-pos
(let ((ts (org-entry-get event-pos "DT")))
(when ts
(org-entry-put event-pos "DT" (gnus-icalendar--deactivate-org-timestamp ts))
(save-buffer)))))))))
(defun gnus-icalendar--get-org-event-reply-status (event &optional org-file)
(let ((file (gnus-icalendar-find-org-event-file event org-file)))
(when file
(save-excursion
(with-current-buffer (find-file-noselect file)
(let ((event-pos (org-find-entry-with-id (gnus-icalendar-event:uid event))))
(org-entry-get event-pos "REPLY")))))))
(defun gnus-icalendar-insinuate-org-templates ()
(unless (gnus-icalendar-find-if (lambda (x) (string= (cadr x) gnus-icalendar-org-template-name))
org-capture-templates)
(setq org-capture-templates
(append `((,gnus-icalendar-org-template-key
,gnus-icalendar-org-template-name
entry
(file+olp ,gnus-icalendar-org-capture-file ,@gnus-icalendar-org-capture-headline)
"%i"
:immediate-finish t))
org-capture-templates))
;; hide the template from interactive template selection list
;; (org-capture)
;; NOTE: doesn't work when capturing from string
;; (when (boundp 'org-capture-templates-contexts)
;; (push `(,gnus-icalendar-org-template-key "" ((in-mode . "gnus-article-mode")))
;; org-capture-templates-contexts))
))
(defun gnus-icalendar:org-event-save (event reply-status)
(with-temp-buffer
(org-capture-string (gnus-icalendar-event->org-entry event reply-status)
gnus-icalendar-org-template-key)))
(defun gnus-icalendar-show-org-agenda (event)
(let* ((time-delta (time-subtract (gnus-icalendar-event:end-time event)
(gnus-icalendar-event:start-time event)))
(duration-days (1+ (floor (encode-time time-delta 'integer) 86400))))
(org-agenda-list nil (gnus-icalendar-event:start event) duration-days)))
(cl-defmethod gnus-icalendar-event:sync-to-org ((event gnus-icalendar-event-request) reply-status)
(if (gnus-icalendar-find-org-event-file event)
(gnus-icalendar--update-org-event event reply-status)
(gnus-icalendar:org-event-save event reply-status)))
(cl-defmethod gnus-icalendar-event:sync-to-org ((event gnus-icalendar-event-cancel) _reply-status)
(when (gnus-icalendar-find-org-event-file event)
(gnus-icalendar--cancel-org-event event)))
(defun gnus-icalendar-org-setup ()
(if (and gnus-icalendar-org-capture-file gnus-icalendar-org-capture-headline)
(progn
(gnus-icalendar-insinuate-org-templates)
(setq gnus-icalendar-org-enabled-p t))
(message "Cannot enable Calendar->Org: missing capture file, headline")))
;;;
;;; gnus-icalendar
;;;
(defgroup gnus-icalendar nil
"Settings for inline display of iCalendar invitations."
:version "24.4"
:group 'gnus-article
:prefix "gnus-icalendar-")
(defcustom gnus-icalendar-reply-bufname "*CAL*"
"Buffer used for building iCalendar invitation reply."
:type '(string))
(defcustom gnus-icalendar-additional-identities nil
"We need to know your identity to make replies to calendar requests work.
Gnus will only offer you the Accept/Tentative/Decline buttons for
calendar events if any of your identities matches at least one
RSVP participant.
Your identity is guessed automatically from the variables
`user-full-name', `user-mail-address',
`gnus-ignored-from-addresses' and `message-alternative-emails'.
If you need even more aliases you can define them here. It really
only makes sense to define names or email addresses."
:type '(repeat string))
(defvar-local gnus-icalendar-reply-status nil)
(defvar-local gnus-icalendar-event nil)
(defvar-local gnus-icalendar-handle nil)
(defun gnus-icalendar-identities ()
"Return list of regexp-quoted names and email addresses belonging to the user.
These will be used to retrieve the RSVP information from ical events."
(apply #'append
(mapcar
(lambda (x) (if (listp x) x (list x)))
(list user-full-name (regexp-quote user-mail-address)
;; NOTE: these can be lists
gnus-ignored-from-addresses ; already regexp-quoted
(unless (functionp message-alternative-emails) ; String or function.
message-alternative-emails)
(mapcar #'regexp-quote gnus-icalendar-additional-identities)))))
;; TODO: make the template customizable
(cl-defmethod gnus-icalendar-event->gnus-calendar ((event gnus-icalendar-event) &optional reply-status)
"Format an overview of EVENT details."
(cl-labels
((format-header (x)
(format "%-12s%s"
(propertize (concat (car x) ":") 'face 'bold)
(cadr x))))
(with-slots (organizer summary description location recur uid
method rsvp participation-type)
event
(let ((headers `(("Summary" ,summary)
("Location" ,(or location ""))
("Time" ,(gnus-icalendar-event:org-timestamp event))
("Organizer" ,organizer)
("Attendance" ,(if (eq participation-type 'non-participant)
"You are not listed as an attendee"
(capitalize (symbol-name participation-type))))
("Method" ,method))))
(when (and (not (gnus-icalendar-event-reply-p event)) rsvp)
(setq headers (append headers
`(("Status" ,(or reply-status "Not replied yet"))))))
(concat
(mapconcat #'format-header headers "\n")
"\n\n"
description)))))
(defmacro gnus-icalendar-with-decoded-handle (handle &rest body)
"Execute BODY in buffer containing the decoded contents of HANDLE."
(let ((charset (make-symbol "charset")))
`(let ((,charset (cdr (assoc 'charset (mm-handle-type ,handle)))))
(with-temp-buffer
(mm-insert-part ,handle)
(when (string= ,charset "utf-8")
(decode-coding-region (point-min) (point-max) 'utf-8))
,@body))))
(defun gnus-icalendar-event-from-handle (handle &optional attendee-name-or-email)
(gnus-icalendar-with-decoded-handle handle
(gnus-icalendar-event-from-buffer (current-buffer) attendee-name-or-email)))
(defun gnus-icalendar-insert-button (text callback data)
;; FIXME: the gnus-mime-button-map keymap does not make sense for this kind
;; of button.
(let ((start (point)))
(add-text-properties
start
(progn
(insert "[ " text " ]")
(point))
`(gnus-callback
,callback
keymap ,gnus-mime-button-map
face ,gnus-article-button-face
gnus-data ,data))
(widget-convert-button 'link start (point)
:action 'gnus-widget-press-button)))
(defun gnus-icalendar-send-buffer-by-mail (buffer-name subject)
(let ((message-signature nil))
(with-current-buffer gnus-summary-buffer
(gnus-summary-reply)
(message-goto-body)
(mml-insert-multipart "alternative")
(mml-insert-empty-tag 'part 'type "text/plain")
(mml-attach-buffer buffer-name "text/calendar; method=REPLY; charset=UTF-8")
(message-goto-subject)
(delete-region (line-beginning-position) (line-end-position))
(insert "Subject: " subject)
(message-send-and-exit))))
(defun gnus-icalendar-reply (data)
(let* ((handle (car data))
(status (cadr data))
(event (caddr data))
(reply (gnus-icalendar-with-decoded-handle handle
(gnus-icalendar-event-reply-from-buffer
(current-buffer) status (gnus-icalendar-identities)))))
(when reply
(cl-labels
((fold-icalendar-buffer
()
(goto-char (point-min))
(while (re-search-forward "^\\(.\\{72\\}\\)\\(.+\\)$" nil t)
(replace-match "\\1\n \\2")
(goto-char (line-beginning-position)))))
(let ((subject (concat (capitalize (symbol-name status))
": " (gnus-icalendar-event:summary event))))
(with-current-buffer (get-buffer-create gnus-icalendar-reply-bufname)
(delete-region (point-min) (point-max))
(insert reply)
(fold-icalendar-buffer)
(gnus-icalendar-send-buffer-by-mail (buffer-name) subject))
;; Back in article buffer
(setq-local gnus-icalendar-reply-status status)
(when gnus-icalendar-org-enabled-p
(gnus-icalendar--update-org-event event status)
;; refresh article buffer to update the reply status
(with-current-buffer gnus-summary-buffer
(gnus-summary-show-article))))))))
(defun gnus-icalendar-sync-event-to-org (event)
(gnus-icalendar-event:sync-to-org event gnus-icalendar-reply-status))
(cl-defmethod gnus-icalendar-event:inline-reply-buttons ((event gnus-icalendar-event) handle)
(when (gnus-icalendar-event:rsvp event)
`(("Accept" gnus-icalendar-reply (,handle accepted ,event))
("Tentative" gnus-icalendar-reply (,handle tentative ,event))
("Decline" gnus-icalendar-reply (,handle declined ,event)))))
(cl-defmethod gnus-icalendar-event:inline-reply-buttons ((_event gnus-icalendar-event-reply) _handle)
"No buttons for REPLY events."
nil)
(cl-defmethod gnus-icalendar-event:inline-reply-status ((event gnus-icalendar-event))
(or (when gnus-icalendar-org-enabled-p
(gnus-icalendar--get-org-event-reply-status event))
"Not replied yet"))
(cl-defmethod gnus-icalendar-event:inline-reply-status ((_event gnus-icalendar-event-reply))
"No reply status for REPLY events."
nil)
(cl-defmethod gnus-icalendar-event:inline-org-buttons ((event gnus-icalendar-event))
(let* ((org-entry-exists-p (gnus-icalendar-find-org-event-file event))
(export-button-text (if org-entry-exists-p "Update Org Entry" "Export to Org")))
(delq nil (list
`("Show Agenda" gnus-icalendar-show-org-agenda ,event)
(when (gnus-icalendar-event-request-p event)
`(,export-button-text gnus-icalendar-sync-event-to-org ,event))
(when org-entry-exists-p
`("Show Org Entry" gnus-icalendar--show-org-event ,event))))))
(cl-defmethod gnus-icalendar-event:inline-org-buttons ((event gnus-icalendar-event-cancel))
(let ((org-entry-exists-p (gnus-icalendar-find-org-event-file event)))
(delq nil (list
`("Show Agenda" gnus-icalendar-show-org-agenda ,event)
(when org-entry-exists-p
`("Update Org Entry" gnus-icalendar-sync-event-to-org ,event))
(when org-entry-exists-p
`("Show Org Entry" gnus-icalendar--show-org-event ,event))))))
;;;###autoload
(defun gnus-icalendar-mm-inline (handle)
(let ((event (gnus-icalendar-event-from-handle handle (gnus-icalendar-identities))))
(setq gnus-icalendar-reply-status nil)
(when event
(cl-labels
((insert-button-group
(buttons)
(when buttons
(mapc (lambda (x)
(apply #'gnus-icalendar-insert-button x)
(insert " "))
buttons)
(insert "\n\n"))))
(insert-button-group
(gnus-icalendar-event:inline-reply-buttons event handle))
(when gnus-icalendar-org-enabled-p
(insert-button-group (gnus-icalendar-event:inline-org-buttons event)))
(setq gnus-icalendar-event event
gnus-icalendar-handle handle)
(insert (gnus-icalendar-event->gnus-calendar
event
(gnus-icalendar-event:inline-reply-status event)))))))
(defun gnus-icalendar-save-part (handle)
(let (event)
(when (and (equal (car (mm-handle-type handle)) "text/calendar")
(setq event (gnus-icalendar-event-from-handle handle (gnus-icalendar-identities))))
(gnus-icalendar-event:sync-to-org event))))
(defun gnus-icalendar-save-event ()
"Save the Calendar event in the text/calendar part under point."
(interactive)
(gnus-article-check-buffer)
(let ((data (get-text-property (point) 'gnus-data)))
(when data
(gnus-icalendar-save-part data))))
(defun gnus-icalendar-reply-accept ()
"Accept invitation in the current article."
(interactive)
(with-current-buffer gnus-article-buffer
(gnus-icalendar-reply (list gnus-icalendar-handle 'accepted gnus-icalendar-event))
(setq-local gnus-icalendar-reply-status 'accepted)))
(defun gnus-icalendar-reply-tentative ()
"Send tentative response to invitation in the current article."
(interactive)
(with-current-buffer gnus-article-buffer
(gnus-icalendar-reply (list gnus-icalendar-handle 'tentative gnus-icalendar-event))
(setq-local gnus-icalendar-reply-status 'tentative)))
(defun gnus-icalendar-reply-decline ()
"Decline invitation in the current article."
(interactive)
(with-current-buffer gnus-article-buffer
(gnus-icalendar-reply (list gnus-icalendar-handle 'declined gnus-icalendar-event))
(setq-local gnus-icalendar-reply-status 'declined)))
(defun gnus-icalendar-event-export ()
"Export calendar event to `org-mode', or update existing agenda entry."
(interactive)
(with-current-buffer gnus-article-buffer
(gnus-icalendar-sync-event-to-org gnus-icalendar-event))
;; refresh article buffer in case the reply had been sent before initial org
;; export
(with-current-buffer gnus-summary-buffer
(gnus-summary-show-article)))
(defun gnus-icalendar-event-show ()
"Display `org-mode' agenda entry related to the calendar event."
(interactive)
(gnus-icalendar--show-org-event
(with-current-buffer gnus-article-buffer
gnus-icalendar-event)))
(defun gnus-icalendar-event-check-agenda ()
"Display `org-mode' agenda for days between event start and end dates."
(interactive)
(gnus-icalendar-show-org-agenda
(with-current-buffer gnus-article-buffer gnus-icalendar-event)))
(defvar gnus-mime-action-alist) ; gnus-art
(defun gnus-icalendar-setup ()
;; FIXME: Get rid of this!
;; The three add-to-list are now redundant (good), but I think the rest
;; is still not automatically setup.
(add-to-list 'mm-inlined-types "text/calendar")
(add-to-list 'mm-automatic-display "text/calendar")
(add-to-list 'mm-inline-media-tests '("text/calendar" gnus-icalendar-mm-inline identity))
(gnus-define-keys (gnus-summary-calendar-map "i" gnus-summary-mode-map)
"a" gnus-icalendar-reply-accept
"t" gnus-icalendar-reply-tentative
"d" gnus-icalendar-reply-decline
"c" gnus-icalendar-event-check-agenda
"e" gnus-icalendar-event-export
"s" gnus-icalendar-event-show)
(require 'gnus-art)
(add-to-list 'gnus-mime-action-alist
(cons "save calendar event" #'gnus-icalendar-save-event)
t))
(provide 'gnus-icalendar)
;;; gnus-icalendar.el ends here