summaryrefslogblamecommitdiffstats
path: root/emacsconf.el
blob: 5f054eccbf35dace99dc43abac6e7591e99649b8 (plain) (tree)



























                                                                                            



                                   
                                     
                      

                   
                                


                                               


                                                                    



                                                        



                                              
 
                                                           


                   
                                                                                                                                                                                             





                                                                                
                                              

                                        
                                                                     
                                                

                   
                                                    

                                                                  
                                                                                                        


                                                                    
                                                          







                                             



                                          


                                                                                    
 
                                                             
                                                          


                                                                                                            
                                                                                                   
                                                                                           

                                                                       










                                                                             

                                                                                                                                                           



                                                                   







                                                       
 


                                         
                                

                                     
                                   

                                        

                                                                                             


                                    










                                                                                              

 

















                                                                                                       

                                       


                                                                         
                       


                                                                                                          


                                                                           


                                        







                                                                                                                                                               
                                                                       
 













                                                                                                        
 


                                                                                 
                                    
                                                             









                                                                                                





                                                                                                              
 








                                                                                                              
                                                 
                             
                                                
                                                                       

                                        
                                                          
                                                




                                                                                                 
 
                                                           








                                                                                                                     
                                                                               

                                                           
                                                        





                                                            



                                                             
                                                        


                                                                                

                 
                                                                                        
                                           

                                                                    
                 
 



                                 


                                                                        

                                                                                                                                                                                           
 
                                               

                                             
                




                                                                                                 

                                                                                                                                                                                

                                                                                                                





                                                                    
 


                                                                   
                                              



                                                                            
                                                                                                              

                                        

                                    
                                                   
                                                
                                
         


















                                                                                   
               
 



                                                   





                                                         
                              

                                                             
                                                










                                                                              
                                                           
 






                                                             

                                                  

                                          
                                       
                                     

                                                                
                                       


                                                     

                                             
                                                     
                                           
                                             
                                   
                                                                              


                                                                                                                           
                                                                                                                     










                                                                        
                                                                        

                                                                                  
                                                     

                                                                          
                                                                                                                   


                                                     
                                                           
                                    
                                             
                                         

                                                         
                                    

                                                 
                                                                                                                       


                                                                                
                                           



                                                                                                   



            
                    
                                                             
                          
                               


                                                               
                                                                           














                                                                                                      
                                                         








                                                                                                             






                                                                                                                                
 
                                                                                                  









                                                                              

                                                                      
























                                                                                                               
 















                                                                                          





                                                                                                       


                                                                                  
 

                                    


                                                                   




                                                                                                                                                       


                              



                                                    














                                                                                                                                                                                                                                                                                             
 
















                                                                              

                                           
                                 
                                            
                                          
                             
                              
                                      
                                        
                             



                                                                      
                                                                               


                                                                                      
 


                                          
                                                         
                                                                    











                                                                                                                                     


                                                                                                                                     

                                                                                                                                     


                                                                                                         

                                                                                                          

                                     



                                                                                                                                                                                                                                                       

    
                        
                                  

                                                                            


                                                                                                                                                                                 



                                                             
                                                                    

                                                           
                                                                                                         






                                                                                                                                                                                                                                                                                     
                                                                       
                                                                                                                              
                                                                                                                                
         



                                                  
                                                                                                       
                                                        
                                                                                           
                                                 

                                                                                                   
                                                                                                                


                                                                                                                




                                                             

                                       
                                                                                                        




                                                                                                 
       
 



                                                         

                                                                           
                        







                                               
 
                                             
                                                                                        


                                           



                                           

                        

                                      
                                                                       

                                 

                                                              











                                                                 






                                                    











                                                                       








                                                             
                                                 
                                                    




                                                                        
 











                                                                                                                                                        
                                                   
                              
                                                                
 











                                                                                                     















                                                                        


                                               


                                            
















                                                                                                                 
                           
            





                                                                                                         

            


                                                       
                                                                   





                                                                        




                                                            


                                                 

                                 
                                                



                                                               
                                           
                                                


                                                                  

                                                               
                                         
                                              









                                                     
                                    
                                                


                                         

                                                                             







                                                                                              
                                                                                                     
                                                                                   
                                                                                                               
 
            



                                                                            
                                       


                                                                               













                                                                                             
 
              
                                                      

                                                                                                       





                                                                                                                                                                                                                                                   
                                             
                                                                                       













                                                                         




                                                                                                 
            
                                                     




                                                              
                                                                      



                                   
                           


                                         








                                                         

                                                                                             


                                                
                                                              







                                                                        
                                              
                                    
 

              




                                                                                                                        
      




                                                                       

      
 








                                                













                                                                                                  


                                                                             
 










                                                                                                
 











                                                                                                                               
                                                                                                                                      

                                                                                                  










                                                                            

                                                                                
                                                                                               
          
                        
                                                                          

                                                                                       
                                                                                                                                   

                                                                

                                                                                                                         
                                                                                                    
                                      
                            
                           
                             

                                                                           
                                                                                                                                                          

                                                                                                   

                                                                                                                        
                                                                                          
                                                               
                                     
                           

                              

                                 


                                                    




                                                                                                                                                                                       


                                                                                                                                                                                                          
                              
                      
 







                                                                         




                                                                                         
 







                                                                                               
 



                                                                                        

















                                                                                                                                                            
                        
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     





                                                                              


                                                                       

























                                                                                                      
                                                                                                                                                    
                                                                                                     
                                       






                                                                                          

                                                                         






                                                                
 















                                                                                                                
                                                                   



                                                                               






                                                              









                                                                 
                                                    
                                                      
                 
                                                      







                                                                                 
             


                           
                                              
                                                                 





                                                           
                             















                                                                       
                     


                                                                                                                                                  



                                           


                               
 





                                                                                       













                                                                                                

                                                            
                                                        
                                  

                                               
                                                                                    
                                                                                 
                             
     

                                                

                                               











                                                                 






                                          






                                                     
                                                                 



                                                                                      




                                                                                                                        
                                                               
                                                                                                                  

                                                                   

                         
 




                                                                                     















                                                                       



                                                                                                              
 










                                                                                                                    



                                                                                               












                                                                                                                   



                                                      
                                                                











                                                                                                                                                                                                                                                                                
 

















































                                                                                                                 






                                                                                                                                                                                       

                          
;;; emacsconf.el --- Core functions and variables for EmacsConf  -*- lexical-binding: t; -*-

;; Copyright (C) 2021  Sacha Chua

;; Author: Sacha Chua <sacha@sachachua.com>
;; Keywords: multimedia

;; 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:

;; 

;;; Code:

(defgroup emacsconf nil "EmacsConf" :group 'multimedia)

(defcustom emacsconf-id "emacsconf"
  "ID of conference"
  :group 'emacsconf
  :type 'string)
(defcustom emacsconf-name "EmacsConf"
  "Name of conference"
  :group 'emacsconf
  :type 'string)
(defcustom emacsconf-year "2023"
  "Conference year. String for easy inclusion."
  :group 'emacsconf
  :type 'string)
(defcustom emacsconf-date "2023-12-03" "Starting date of EmacsConf."
	:group 'emacsconf
	:type 'string)
(defcustom emacsconf-directory "~/vendor/emacsconf-wiki"
  "Directory where the wiki files are."
  :group 'emacsconf
  :type 'directory)
(defcustom emacsconf-ansible-directory nil
  "Directory where the Ansible repository is."
  :group 'emacsconf
  :type 'directory)

(defcustom emacsconf-timezone "US/Eastern" "Main timezone."
  :group 'emacsconf
  :type 'string)

(defcustom emacsconf-timezones '("US/Eastern" "US/Central" "US/Mountain" "US/Pacific" "UTC" "Europe/Paris" "Europe/Athens" "Asia/Kolkata" "Asia/Singapore" "Asia/Tokyo") "List of timezones."
  :group 'emacsconf
  :type '(repeat string))

(defcustom emacsconf-base-url "https://emacsconf.org/" "Includes trailing slash"
  :group 'emacsconf
  :type 'string)
(defcustom emacsconf-publishing-phase 'program
  "Controls what information to include.
'program - don't include times
'schedule - include times; use this leading up to the emacsconference
'resources - after EmacsConf, don't need status"
  :group 'emacsconf
  :type '(choice
          (const :tag "CFP: include invitation" cfp)
          (const :tag "Program: Don't include times" program)
          (const :tag "Schedule: Include detailed times" schedule)
					(const :tag "Conference: Show IRC and watching info" conference)
          (const :tag "Resources: Don't include status" resources)))

(defcustom emacsconf-org-file nil
  "Path to the Org file with emacsconference information."
  :type 'file
  :group 'emacsconf)

(defcustom emacsconf-upcoming-file nil
  "Path to the Org file with upcoming talks."
  :type 'file
  :group 'emacsconf)

(defcustom emacsconf-emergency-contact nil
  "Emergency contact information."
  :type 'string
  :group 'emacsconf)
(defcustom emacsconf-review-days 7 "Number of days for review for early acceptance."
	:type 'natnum
	:group 'emacsconf)

(defvar emacsconf-stream-base "https://live0.emacsconf.org/")
(defvar emacsconf-chat-base "https://chat.emacsconf.org/")
(defvar emacsconf-backstage-dir "/ssh:orga@media.emacsconf.org:/var/www/media.emacsconf.org/2022/backstage")
(defvar emacsconf-upload-dir "/ssh:orga@media.emacsconf.org:/srv/upload")
(defvar emacsconf-res-dir (format "/ssh:orga@res.emacsconf.org:/data/emacsconf/%s" emacsconf-year))
(defvar emacsconf-media-extensions '("webm" "mkv" "mp4" "webm" "avi" "ts" "ogv" "wav" "ogg" "mp3"))
(defvar emacsconf-ftp-upload-dir "/ssh:orga@media.emacsconf.org:/srv/ftp/anon/upload-here")
(defvar emacsconf-backstage-user "emacsconf")
(defvar emacsconf-backstage-password nil "Password for backstage area")
(defvar emacsconf-notebook
  (expand-file-name
   "index.org"
   (expand-file-name "organizers-notebook"
                     (expand-file-name emacsconf-year emacsconf-directory))))

(defun emacsconf-prep-agenda ()
  (interactive)
  (let* ((org-agenda-custom-commands
         `(("a" "Agenda"
            ((tags-todo "-PRIORITY=\"C\"-SCHEDULED={.}-nextyear"
                        ((org-agenda-files (list ,emacsconf-notebook))
												 (org-agenda-sorting-strategy '(priority-down effort-up))))
             (agenda ""
                     ((org-agenda-files (list ,emacsconf-notebook))
                      (org-agenda-span 7)))
             )))))
    (org-agenda nil "a")))

(defun emacsconf-talk-agenda ())

(defun emacsconf-notebook-goto-custom-id (id)
	(interactive "MID: ")
	(find-file-other-window emacsconf-notebook)
	(goto-char (org-find-property "CUSTOM_ID" id)))

(defun emacsconf-ftp-upload-dired ()
  (interactive)
  (dired emacsconf-ftp-upload-dir "-tl"))
(defun emacsconf-upload-dired ()
  (interactive)
  (dired emacsconf-upload-dir "-tl"))
(defun emacsconf-backstage-dired ()
  (interactive)
  (dired emacsconf-backstage-dir "-tl"))
(defun emacsconf-res-dired () (interactive) (dired emacsconf-res-dir "-tl"))
(defun emacsconf-media-dired () (interactive) (dired emacsconf-public-media-directory "-tl"))
(defun emacsconf-cache-dired ()
  (interactive)
  (dired emacsconf-cache-dir "-tl"))
(defun emacsconf-slugify (s)
  (replace-regexp-in-string " +" "-" (replace-regexp-in-string "[^a-z0-9 ]" "" (downcase s))))

(defun emacsconf-video-slug (talk)
  (concat "emacsconf-" emacsconf-year "-" (plist-get talk :slug) "--"
          (emacsconf-slugify (plist-get talk :title))
          (if (plist-get talk :speakers)
              (concat"--"
                     (emacsconf-slugify (plist-get talk :speakers)))
            "")))



(defun emacsconf-set-video-slug-if-needed (o)
  (interactive (list (emacsconf-complete-talk-info)))
  (unless (plist-get o :video-slug)
    (let ((video-slug
           (read-string "Set video slug: " (emacsconf-video-slug o))))
      (save-window-excursion
        (emacsconf-with-talk-heading (plist-get o :slug)
          (org-entry-put (point) "VIDEO_SLUG" video-slug)))
      (plist-put o :video-slug video-slug)))
  (plist-get o :video-slug))

(defun emacsconf-set-video-slugs ()
  (interactive)
  (org-map-entries
   (lambda ()
     (org-entry-put (point) "VIDEO_SLUG" (emacsconf-video-slug (emacsconf-get-talk-info-for-subtree))))
   "SLUG={.}-VIDEO_SLUG={.}"))

(defun emacsconf-upload-to-backstage ()
  (interactive)
  (mapc (lambda (file)
          (copy-file file (expand-file-name (file-name-nondirectory file)
                                            emacsconf-backstage-dir)
                     t)
          (when (and emacsconf-cache-dir
                     (not (string= (expand-file-name (file-name-nondirectory file) default-directory)
                                   (expand-file-name (file-name-nondirectory file) emacsconf-cache-dir))))
            (copy-file file (expand-file-name (file-name-nondirectory file)
                                              emacsconf-cache-dir)
                       t)))
        (or (dired-get-marked-files)
            (list (buffer-file-name)))))

(defun emacsconf-get-srv2-and-upload-to-backstage (talk)
  (interactive (list (emacsconf-complete-talk-info (seq-filter (lambda (o) (plist-get o :youtube-url)) (emacsconf-get-talk-info)))))
  (let ((filename (make-temp-file nil nil "srv2"))
        (buf (get-buffer-create "*test*")))
    (when (plist-get talk :youtube-url)
      (call-process "yt-dlp" nil buf t "--write-sub" "--write-auto-sub" "--no-warnings" "--sub-lang" "en" "--skip-download" "--sub-format" "srv2" "-o" filename
                    (plist-get talk :youtube-url))
      (with-current-buffer (find-file-noselect (concat filename ".en.srv2"))
        (emacsconf-upload-to-backstage-and-rename talk "main--srt")))))

(defun emacsconf-upload-to-backstage-and-rename (talk &optional filename)
  (interactive (list (emacsconf-complete-talk-info)))
  (mapc (lambda (file)
          (let ((new-file (or filename (read-string (format "Filename (%s): " (file-name-base file))))))
            (copy-file file
                       (expand-file-name (concat (plist-get talk :video-slug)
                                                 (if (string= new-file "")
                                                     ""
                                                   (concat "--" new-file))
                                                 "."
                                                 (file-name-extension file))
                                         emacsconf-backstage-dir)
                       t)))
        (or (dired-get-marked-files) (list (buffer-file-name)))))

(defun emacsconf-upload-copy-from-json (talk key filename)
  (interactive (let-alist (json-parse-string (buffer-string) :object-type 'alist)
                 (list (emacsconf-complete-talk-info)
                       .metadata.key
                       (read-string (format "Filename: ")))))
  (let ((new-filename (concat (plist-get talk :video-slug)
                              (if (string= filename "")
                                  filename
                                (concat "--" filename))
                              "."
                              (let-alist (json-parse-string (buffer-string) :object-type 'alist)
                                (file-name-extension .metadata.name)))))
    (copy-file key
               (expand-file-name new-filename
                                 emacsconf-backstage-dir) t)
    ;; (copy-file key (expand-file-name new-filename emacsconf-cache-dir))
    ;; (unless (file-directory-p (expand-file-name (plist-get talk :slug) emacsconf-res-dir))
    ;;   (make-directory (expand-file-name (plist-get talk :slug) emacsconf-res-dir)))
    ;; (copy-file (expand-file-name new-filename emacsconf-cache-dir)
    ;;            (expand-file-name new-filename (expand-file-name (plist-get talk :slug) emacsconf-res-dir)))
    ))

(defcustom emacsconf-download-directory "~/Downloads"
  "Directory to check for downloaded files."
  :type 'directory
  :group 'emacsconf)

(defun emacsconf-latest-file (path &optional filter)
  "Return the newest file in PATH. Optionally filter by FILTER."
  (car (sort (seq-remove #'file-directory-p (directory-files path 'full filter t)) #'file-newer-than-file-p)))

(defun emacsconf-find-captions-from-slug (search)
	"Edit captions file."
  (interactive (list (emacsconf-complete-talk)))
  (emacsconf-with-talk-heading search (emacsconf-subed-find-captions)))

(defun emacsconf-edit-wiki-page (search)
	"Open the wiki page for the talk matching SEARCH."
  (interactive (list (emacsconf-complete-talk)))
  (setq search (if (stringp search) (emacsconf-get-slug-from-string search)
								 (plist-get search :slug)))
  (find-file (expand-file-name
							(concat search ".md")
              (expand-file-name "talks" (expand-file-name emacsconf-year emacsconf-directory)))))

(defun emacsconf-find-caption-directives-from-slug (search)
  (interactive (list (emacsconf-complete-talk)))
  (setq search (emacsconf-get-slug-from-string search))
  (find-file (expand-file-name (concat search ".md")
                               (expand-file-name "captions" (expand-file-name emacsconf-year emacsconf-directory)))))


(defun emacsconf-browse-wiki-page (search)
  (interactive (list (emacsconf-complete-talk)))
  (setq search (emacsconf-get-slug-from-string search))
  (browse-url (concat emacsconf-base-url emacsconf-year "/talks/" search "/")))

(defun emacsconf-set-property-from-slug (search prop value)
  (interactive (list (emacsconf-complete-talk) nil nil))
  (save-window-excursion
    (emacsconf-with-talk-heading search
      (setq prop (or prop (org-read-property-name)))
      (setq value (or value (org-read-property-value prop)))
      (org-entry-put (point) prop value))))


(defun emacsconf-complete-slug ()
  (emacsconf-get-slug-from-string (emacsconf-complete-talk)))

(defun emacsconf-export-slug (link description format _)
  (let* ((path (format "https://emacsconf.org/%s/talks/%s" emacsconf-year link))
         (talk (emacsconf-resolve-talk link))
				 (desc (or description link)))
    (pcase format
      (`html
       (format "<a href=\"#%s\" title=\"%s\">%s</a>" link (plist-get talk :title) desc))
      (`ascii (format "%s (%s)" desc path))
      (`md
       (format "[%s](%s \"%s\")" desc path (plist-get talk :title)))
      (_ path))))

(with-eval-after-load 'org
  (org-link-set-parameters
   "emacsconf"
   :follow #'emacsconf-go-to-talk
   :complete (lambda () (concat "emacsconf:" (emacsconf-complete-slug)))
   :export #'emacsconf-export-slug))

(defvar emacsconf-complete-talk-cache nil)
;; (setq emacsconf-complete-talk-cache (mapcar (lambda (o) (string-join (delq nil (mapcar (lambda (f) (plist-get o f)) '(:slug :title :speakers :irc))) " - ")) (emacsconf-get-talk-info)))

(defun emacsconf-complete-talk (&optional info)
	"Offer talks for completion.
If INFO is specified, limit it to that list."
  (let ((choices
				 (if (and (null info) emacsconf-complete-talk-cache)
						 emacsconf-complete-talk-cache
					 (mapcar (lambda (o)
										 (string-join
											(delq nil
														(mapcar (lambda (f) (plist-get o f))
																		'(:slug :title :speakers :irc)))
											" - "))
									 (or info (emacsconf-get-talk-info))))))
    (completing-read
     "Talk: " 
     (lambda (string predicate action)
       (if (eq action 'metadata)
           '(metadata (category . emacsconf))
         (complete-with-action action choices string predicate))))))

(defun emacsconf-complete-talk-info (&optional info)
  (emacsconf-search-talk-info (emacsconf-complete-talk info) info))

(defun emacsconf-get-slug-from-string (search)
  (when (listp search) (setq search (car search)))
  (cond
	 ((and search (stringp search) (string-match "\\(.*?\\) - " search))
		(match-string 1 search))
	 ((and (stringp search) (string-match (concat emacsconf-id "-" emacsconf-year "-\\(.+?\\)--") search))
		(match-string 1 search))
   (t search)))

(defun emacsconf-go-to-talk (search)
	"Jump to the talk heading matching SEARCH."
  (interactive (list (emacsconf-complete-talk)))
  (find-file emacsconf-org-file)
  (widen)
  (cond
   ((plist-get search :slug)
    (goto-char (org-find-property "SLUG" (plist-get search :slug))))
   ((emacsconf-get-slug-from-string search)
    (goto-char (org-find-property "SLUG" (emacsconf-get-slug-from-string search))))
   (t
    (goto-char
     (catch 'found
       (org-map-entries
        (lambda ()
          (when (string-match search
                              (cons
                               (concat (org-entry-get (point) "SLUG") " - "
                                       (org-entry-get (point) "ITEM") " - "
                                       (org-entry-get (point) "NAME") " - "
                                       (org-entry-get (point) "EMAIL"))
                               (point)))
            (throw 'found (point))))
        "SLUG={.}")))))
  (org-reveal))

(defmacro emacsconf-for-each-talk (&rest body)
  (declare (indent 0) (debug t))
  `(org-map-entries (lambda () ,@body) "SLUG={.}"))

(defmacro emacsconf-with-talk-heading (search &rest body)
  (declare (indent 1) (debug t))
  `(progn
     (emacsconf-go-to-talk ,search)
     ,@body))

(defvar emacsconf-status-types
  '(("WAITING_FOR_PREREC" . "Waiting for video from speaker")
    ("TO_PROCESS" . "Processing uploaded video")
    ("PROCESSING" . "Processing uploaded video")
    ("TO_AUTOCAP" . "Processing uploaded video")
    ("TO_ASSIGN" . "Waiting for a caption volunteer")
    ("TO_CAPTION" . "Processing uploaded video")
    ("TO_STREAM" . "Talk captioned")
    ("PLAYING" . "Now playing on the conference livestream")
    ("CLOSED_Q" . "Q&A starting (not yet open for joining)")
    ("OPEN_Q" . "Q&A open for participation")
    ("UNSTREAMED_Q" . "Q&A continues off the stream")
    ("TO_ARCHIVE" . "Q&A finished, IRC and pad will be archived on this page")
    ("TO_EXTRACT" . "Q&A to be extracted from the room recordings")
    ("DONE" . "All done")
    ("CANCELLED" . "Sorry, this talk has been cancelled")))

(defun emacsconf-get-talk-categories (o)
  (org-narrow-to-subtree)
  (let (list)
    (while (re-search-forward "Category[^ \t\n]+" nil t)
      (setq list (cons (match-string-no-properties 0) list)))
    (plist-put o :categories (reverse list))))

(defun emacsconf-get-talk-info-from-properties (o)
  (let ((heading (org-heading-components))
        (field-props '(                 
                       ;; Initial creation
                       (:track "TRACK")
                       (:slug "SLUG")
                       (:speakers "NAME")                       
                       (:speakers-short "NAME_SHORT")
                       (:email "EMAIL")
                       (:public-email "PUBLIC_EMAIL")
                       (:emergency "EMERGENCY")
                       (:buffer "BUFFER")
                       (:min-time "MIN_TIME")
                       (:max-time "MAX_TIME")
                       (:availability "AVAILABILITY")
                       (:q-and-a "Q_AND_A")
                       (:timezone "TIMEZONE")
                       (:irc "IRC")
                       (:pronunciation "PRONUNCIATION")                       
                       (:pronouns "PRONOUNS")
											 (:date-submitted "DATE_SUBMITTED")
											 (:date-to-notify "DATE_TO_NOTIFY")
											 (:email-notes "EMAIL_NOTES")
                       ;; Scheduling
                       (:scheduled "SCHEDULED")
                       (:time "TIME")
                       (:fixed-time "FIXED_TIME")
                       ;; Coordination
                       (:prerec-info "PREREC_INFO")
                       ;; Prep
                       (:bbb-room "ROOM")                       
                       ;; Processing
                       (:video-slug "VIDEO_SLUG")
                       (:video-file "VIDEO_FILE")                       
                       (:video-time "VIDEO_TIME")                       
                       (:video-file-size "VIDEO_FILE_SIZE")                       
                       (:video-duration "VIDEO_DURATION")
                       (:stream-files "STREAM_FILES")
                       (:youtube-url "YOUTUBE_URL")                       
                       (:toobnix-url "TOOBNIX_URL")
											 (:intro-time "INTRO_TIME")
                       ;; Captioning
                       (:captioner "CAPTIONER")
                       (:caption-note "CAPTION_NOTE")
                       (:captions-edited "CAPTIONS_EDITED")
                       ;; Conference
                       (:check-in "CHECK_IN")
                       (:public "PUBLIC")
                       (:intro-note "INTRO_NOTE")
                       (:hyperlist-note "HYPERLIST_NOTE")
                       ;; Extraction
                       (:qa-youtube "QA_YOUTUBE")
                       (:qa-toobnix "QA_TOOBNIX")
											 (:bbb-playback "BBB_PLAYBACK")
                       ;; Old
                       (:alternate-apac "ALTERNATE_APAC")                       
                       (:extra-live-time "EXTRA_LIVE_TIME")
                       (:present "PRESENT")
                       (:talk-id "TALK_ID") ; use slug instead
                       (:qa-public "QA_PUBLIC") ; now tracked by the OPEN_Q and UNSTREAMED_Q status
                       (:uuid "UUID")           ; Pentabarf export
                       )))
    (apply
     'append
     o
     (list
      :point (point)
			:title (org-entry-get (point) "ITEM")
      :year emacsconf-year
      :conf-year emacsconf-year
      :type (if (org-entry-get (point) "SLUG") 'talk 'headline)
      :status (elt heading 2)
      :level (car heading)
      :url (concat emacsconf-year "/talks/" (org-entry-get (point) "SLUG"))
      :schedule-group (org-entry-get-with-inheritance "SCHEDULE_GROUP")
      :wiki-file-path
      (expand-file-name 
       (concat (org-entry-get (point) "SLUG") ".md")
       (expand-file-name "captions" (expand-file-name emacsconf-year emacsconf-directory)))
      :start-time
      (when (org-entry-get (point) "SCHEDULED")
		    (date-to-time
		     (concat
		      (format-time-string "%Y-%m-%dT%H:%M:%S"
					                    (org-timestamp-to-time
					                     (org-timestamp-split-range
					                      (org-timestamp-from-string
					                       (org-entry-get (point) "SCHEDULED")))))
		      emacsconf-timezone-offset)))
      :end-time (when (org-entry-get (point) "SCHEDULED")
		              (date-to-time
		               (concat
		                (format-time-string "%Y-%m-%dT%H:%M:%S"
					                              (org-timestamp-to-time
					                               (org-timestamp-split-range
					                                (org-timestamp-from-string
					                                 (org-entry-get (point) "SCHEDULED"))
					                                t)))
		                emacsconf-timezone-offset))))
     (mapcar 
      (lambda (prop)
				(list
				 (or (car (rassoc (list (car prop)) field-props))
						 (intern (concat ":" (replace-regexp-in-string "_" "-" (downcase (car prop))))))
				 (cdr prop)))
			(org-entry-properties)))))

(defvar emacsconf-abstract-heading-regexp "abstract" "Regexp matching heading for talk abstract.")

(defun emacsconf-get-subtree-entry (heading-regexp)
  (car
   (delq nil
         (org-map-entries
          (lambda ()
            (when (string-match heading-regexp (org-entry-get (point) "ITEM"))
              (org-get-entry)))
          nil 'tree))))

(defun emacsconf-get-talk-abstract-from-subtree (o)
  "Add the abstract from a subheading with a title matching Abstract."
  (plist-put o :abstract (substring-no-properties (or (emacsconf-get-subtree-entry "abstract") ""))))


(defun emacsconf-get-talk-comments-from-subtree (o)
  (setq o (plist-put o :comments
                     (apply 'append
                            (org-map-entries
                             (lambda ()
                               (org-end-of-meta-data)
                               (mapcar (lambda (item)
                                         (string-trim
                                          (replace-regexp-in-string
                                           " *\n *"
                                           " "
                                           (buffer-substring-no-properties (+ (car item) (length (elt item 2)))
                                                                           (min (point-max) (elt item 6))))))
                                       (org-element-property  :structure (org-element-at-point)))
                               )
                             "ITEM={comments}" 'tree))))
  (plist-put o :acceptance-comment
             (car (delq nil (mapcar
                             (lambda (o)
                               (when (string-match "For the [^ ]+ speakers?: " o)
                                 (replace-match "" t t o)))
                             (plist-get o :comments))))))

(defun emacsconf-convert-talk-abstract-to-markdown (o)
  (plist-put o :abstract-md (org-export-string-as (or (plist-get o :abstract) "") 'md t)))

(defun emacsconf-summarize-times (time timezones)
  (let (prev-day)
    (mapconcat
     (lambda (tz)
       (let ((cur-day (format-time-string "%a %b %-e" time tz))
             (cur-time (format-time-string "%H%MH %Z" time tz)))
         (if (equal prev-day cur-day)
             cur-time
           (setq prev-day cur-day)
           (concat cur-day " " cur-time))))
     timezones
     " / ")))

(defun emacsconf-add-timezone-conversions (o)
  (plist-put o :scheduled-tzs
             (concat (org-timestamp-format (plist-get o :start-time) "%a %b %e %l:%M%p Toronto time (")
                     (emacsconf-summarize-times (plist-get o :start-time) emacsconf-timezones)
                     ")")))

(defun emacsconf-get-abstract-from-wiki (o)
  (plist-put o :markdown (emacsconf-talk-markdown-from-wiki (plist-get o :slug))))


(defun emacsconf-add-talk-status (o)
  (plist-put o :status-label
             (or (assoc-default (plist-get o :status) 
                                emacsconf-status-types 'string= "")
                 (plist-get o :status)))
  (if (or
			 (member (plist-get o :status)
							 (split-string "PLAYING CLOSED_Q OPEN_Q UNSTREAMED_Q TO_ARCHIVE TO_EXTRACT TO_FOLLOW_UP DONE"))
			 (time-less-p (plist-get o :start-time)
										(current-time)))
      (plist-put o :public t))
  o)

(defun emacsconf-talk-live-p (talk)
  "Return non-nil if TALK is ready to be published."
  (plist-get talk :public))

(defun emacsconf-test-public-states ()
  (let ((states (split-string (replace-regexp-in-string "(.*?)" "" "TODO(t) TO_REVIEW TO_ACCEPT TO_CONFIRM WAITING_FOR_PREREC(w) TO_PROCESS(p) TO_AUTOCAP(y) TO_ASSIGN(a) TO_CAPTION(c) TO_STREAM(s) PLAYING(m) CLOSED_Q(q) OPEN_Q(o) UNSTREAMED_Q(u) TO_ARCHIVE TO_EXTRACT TO_FOLLOW_UP"))))
    (mapc (lambda (state) (assert (null (plist-get (emacsconf-add-talk-status
                                                    (list :status state))
                                                   :public))))
          (subseq states
                  0
                  (seq-position
                   states "PLAYING")))
    (mapc (lambda (state) (assert (plist-get (emacsconf-add-talk-status (list
                                                                         :status
                                                                         state))
                                             :public)))
          (subseq states (seq-position
                          states "PLAYING")))))

;; https://stackoverflow.com/questions/55855621/org-mode-getting-logbook-notes
(defun emacsconf-get-logbook-notes ()
	(save-excursion
    (unless (org-at-heading-p)
      (outline-previous-heading))
    (when (re-search-forward ":LOGBOOK:" (save-excursion
                                           (outline-next-heading)
                                           (point))
                             t)
      (let* ((elt (org-element-property-drawer-parser nil))
             (beg (org-element-property :contents-begin elt))
             (end (org-element-property :contents-end elt)))
        (buffer-substring-no-properties beg end)))))

(defun emacsconf-get-talk-logbook (o)
	(plist-put o :logbook (emacsconf-get-logbook-notes)))

(defvar emacsconf-talk-info-functions
  '(emacsconf-get-talk-info-from-properties
    emacsconf-get-talk-categories
    emacsconf-get-talk-abstract-from-subtree
		emacsconf-get-talk-logbook
    emacsconf-add-talk-status
    emacsconf-add-checkin-time
    emacsconf-add-timezone-conversions
    emacsconf-add-speakers-with-pronouns
    emacsconf-add-live-info))
(defun emacsconf-add-speakers-with-pronouns (o)
  (plist-put o :speakers-with-pronouns
             (cond
              ((null (plist-get o :pronouns)) (plist-get o :speakers))
              ((string= (plist-get o :pronouns) "nil") (plist-get o :speakers))
              ((string-match "(" (plist-get o :pronouns)) (plist-get o :pronouns))
              (t (format "%s (%s)" (plist-get o :speakers) (plist-get o :pronouns)))))
  o)

(defun emacsconf-add-checkin-time (o)
  (unless (or (null (plist-get o :status))
              (null (plist-get o :email))
              (string= (plist-get o :status) "CANCELLED")
              (string-match "after" (or (plist-get o :q-and-a) "")))
    (if (null (plist-get o :video-file))
				(progn
					(plist-put o :live-time (plist-get o :start-time))
					(plist-put o :qa-time (plist-get o :live-time))
					(plist-put
					 o :checkin-label
					 "1 hour before the scheduled start of your talk, since you don't have a pre-recorded video")
					(plist-put
					 o :checkin-time
					 (seconds-to-time (time-subtract (plist-get o :start-time) (seconds-to-time 3600)))))
			(plist-put o :live-time
                 (seconds-to-time
									(+
									 (time-to-seconds (plist-get o :start-time))
									 (* 60 (string-to-number (or (plist-get o :video-time) "0")))
									 (* 60 (string-to-number (or (plist-get o :intro-time) "0")))
									 )))
			(unless (string-match "none\\|after" (or (plist-get o :q-and-a) "none"))
				(plist-put o :qa-time
									 (plist-get o :live-time)))      
      (plist-put o :checkin-label
                 "30 minutes before the scheduled start of your Q&A, since you have a pre-recorded video")
      (when (plist-get o :video-time)
        (plist-put o :checkin-time
                   (seconds-to-time
										(time-subtract (time-add (plist-get o :start-time)
																						 (seconds-to-time (* 60 (string-to-number (plist-get o :video-time)))))
																	 (seconds-to-time (/ 3600 2))))))))
  o)

(require 'emacsconf-pad)
(defun emacsconf-add-live-info (o)
  (plist-put o :absolute-url (concat emacsconf-base-url (plist-get o :url)))
  (plist-put o :in-between-url (format "%s%s/in-between/%s.png"
																			 emacsconf-media-base-url
																			 emacsconf-year
																			 (plist-get o :slug)))
  (plist-put o :qa-slide-url (format "%s%s/in-between/%s.png"
                                     emacsconf-media-base-url
                                     emacsconf-year
                                     (plist-get o :slug)))
	(plist-put o :intro-expanded (emacsconf-pad-expand-intro o))
  (let ((track (emacsconf-get-track (plist-get o :track))))
    (when track
      (plist-put o :watch-url (concat emacsconf-base-url emacsconf-year "/watch/" (plist-get track :id)))
      (plist-put o :webchat-url
								 (concat emacsconf-chat-base "?join=emacsconf"
												 (if (eq emacsconf-publishing-phase 'conference)
														 (concat ","
																		 (replace-regexp-in-string "#" ""
																															 (plist-get track :channel)))
													 "")))
			(plist-put o :track-id (plist-get track :id))) 
		(plist-put o :channel (if (eq emacsconf-publishing-phase 'conference) (plist-get track :channel) "emacsconf"))
    (plist-put o :bbb-backstage (concat emacsconf-media-base-url emacsconf-year "/backstage/current/room/" (plist-get o :slug)))
    (cond
     ((string= (or (plist-get o :q-and-a) "") "")
      (plist-put o :qa-info "none")
      (plist-put o :qa-link "none"))
     ((string-match "live" (plist-get o :q-and-a))
      (plist-put o :bbb-redirect (format "https://emacsconf.org/current/%s/room/" (plist-get o :slug)))
      (plist-put o :qa-info (plist-get o :bbb-redirect))
      (plist-put o :qa-link (format "<a href=\"%s\">BBB</a>" (plist-get o :bbb-redirect))))
     ((string-match "IRC" (plist-get o :q-and-a))
      (plist-put o :qa-info (concat "#" (plist-get o :channel) 
                                    (emacsconf-surround ", speaker nick: " (plist-get o :irc) "")))
      (plist-put o :qa-link (format "<a href=\"%s\">%s</a>" (plist-get o :webchat-url) (plist-get o :qa-info))))
     ((string-match "Mumble" (plist-get o :q-and-a))
      (plist-put o :qa-info "Moderated via Mumble, ask questions via pad or IRC")
      (plist-put o :qa-link (format "<a href=\"%s\">%s</a>" (plist-get o :webchat-url) (plist-get o :qa-info))))
     ((string-match "pad" (plist-get o :q-and-a))
      (plist-put o :qa-info "Etherpad")
      (plist-put o :qa-link (format "<a href=\"%s\">%s</a>"
                                    (plist-get o :pad-url)
                                    (plist-get o :qa-info))))
     (t (plist-put o :qa-info "none")
        (plist-put o :qa-link "none")))
    (plist-put o :pad-url (format "https://pad.emacsconf.org/%s-%s" emacsconf-year (plist-get o :slug)))
    (plist-put o :recorded-intro
               (let ((filename
                      (expand-file-name (concat (plist-get o :slug) ".webm")
                                        (expand-file-name "intros" emacsconf-stream-asset-dir))))
                 (and (file-exists-p filename) filename)))
    o))

(defun emacsconf-search-talk-info (search &optional info)
  (setq info (or info (emacsconf-get-talk-info)))
  (or
   (seq-find (lambda (o) (string= (plist-get o :slug)
                                  (emacsconf-get-slug-from-string search)))
             info)
   (seq-find (lambda (o)
               (string-match
                search
                (format "%s - %s - %s - %s"
                        (plist-get o :slug)
                        (plist-get o :title)
                        (plist-get o :speakers)
                        (plist-get o :email))))
             info)))

(defun emacsconf-get-talk-info-for-subtree ()
  (seq-reduce (lambda (prev val) (save-excursion (save-restriction (funcall val prev))))
              emacsconf-talk-info-functions
              nil))

(defun emacsconf-sort-by-scheduled (a b)
  (let ((time-a (plist-get a :start-time))
        (time-b (plist-get b :start-time)))
    (cond
     ((null time-b) t)
     ((null time-a) nil)
     ((time-less-p time-a time-b) t)
     ((time-less-p time-b time-a) nil)
     (t (< (or (plist-get a :point) 0) (or (plist-get b :point) 0))))))

(defun emacsconf-get-talk-info ()
  (with-current-buffer (find-file-noselect emacsconf-org-file)
    (save-excursion
      (save-restriction
        (widen)
        (let (results)
          (org-map-entries
           (lambda ()
             (when (or (org-entry-get (point) "TIME")
                       (org-entry-get (point) "SLUG")
                       (org-entry-get (point) "INCLUDE_IN_INFO"))
               (setq results
                     (cons (emacsconf-get-talk-info-for-subtree)
                           results)))))
          (nreverse results))))))

(defun emacsconf-filter-talks (list)
  "Return only talk info in LIST."
  (seq-filter
   (lambda (talk) (eq (plist-get talk :type) 'talk))
   list))

(defun emacsconf-collect-field-for-status (status field &optional info)
  (seq-map
   (lambda (o)
     (plist-get o field))
   (seq-filter
    (lambda (o)
      (if (listp status)
          (member (plist-get o :status) status)
        (string= status (plist-get o :status))))
    (emacsconf-filter-talks (or info (emacsconf-get-talk-info))))))


(defun emacsconf-get-talk-info-from-file (&optional filename)
  (with-temp-buffer
    (insert-file-contents (or filename "conf.org"))
    (org-mode)
    (org-show-all)
    (goto-char (point-min))
    (goto-char (org-find-property "ID" "talks"))
    (emacsconf-get-talk-info 'wiki)))

(defun emacsconf-include-next-talks (info number)
  (let* ((info (emacsconf-prepare-for-display info))
         (cur-list info))
    ;; add links to the next talks
    (while cur-list
      (plist-put (pop cur-list) :next-talks (seq-take cur-list number)))
    info))

(defun emacsconf-previous-talk (talk &optional info)
	(setq info (emacsconf-prepare-for-display (or info (emacsconf-get-talk-info))))
	(let* ((pos (seq-position info talk))
				 (prev (and pos
										(> pos 0)
										(elt info (1- pos)))))
		(and prev
				 (string= (format-time-string "%Y-%m-%d" (plist-get prev :start-time) emacsconf-timezone)
									(format-time-string "%Y-%m-%d" (plist-get talk :start-time) emacsconf-timezone))
				 prev)))
;; (emacsconf-previous-talk (emacsconf-resolve-talk "lspbridge"))

(defun emacsconf-resolve-talk (talk &optional info)
  "Return the plist for TALK."
  (if (stringp talk) (emacsconf-find-talk-info talk info) talk))

(defun emacsconf-find-talk-info (filter &optional info)
  (setq info (or info (emacsconf-filter-talks (emacsconf-get-talk-info))))
  (when (stringp filter) (setq filter (list filter)))
  (or (seq-find (lambda (o) (string= (plist-get o :slug) (car filter))) info)
      (seq-find (lambda (o)
                  (let ((case-fold-search t)
                        (all (mapconcat (lambda (f) (plist-get o f)) '(:title :speakers :slug) " ")))
                    (null (seq-contains-p
                           (mapcar (lambda (condition) (string-match condition all)) filter)
                           nil))))
                info)))

(defun emacsconf-combine-plist (list-of-talks separator)
  (let (result entry)
    (while list-of-talks
      (setq entry (car list-of-talks))
      (while entry
        (unless (equal (plist-get result (car entry))
                         (cadr entry))
          (setq result
                (plist-put result
                           (car entry)
                           (cons (cadr entry)
                                 (or (plist-get result (car entry)))))))
        (setq entry (cddr entry)))
      (setq list-of-talks (cdr list-of-talks)))
    result))

(defun emacsconf-goto-talk-id (id)
  (goto-char (org-find-property "TALK_ID" id)))

(defun emacsconf-goto-slug (slug)
  (goto-char (org-find-property "SLUG" id)))

(defun emacsconf-talk-markdown-from-wiki (slug)
  "Return the markdown from SLUG."
  (when (file-exists-p (expand-file-name (format "%s/talks/%s.md" emacsconf-year slug) emacsconf-directory))
    (with-temp-buffer
      (insert-file-contents (expand-file-name (format "%s/talks/%s.md" emacsconf-year slug) emacsconf-directory))
      (goto-char (point-min))
      (while (re-search-forward "<!--" nil t)
        (let ((start (match-beginning 0)))
          (when (re-search-forward "-->" nil t)
            (delete-region start (match-end 0)))))
      (goto-char (point-min))
      (while (re-search-forward "\\[\\[![^]]+\\]\\]" nil t)
        (replace-match ""))
      (string-trim (buffer-string)))))

(defun emacsconf-replace-plist-in-string (attrs string)
  "Replace ${keyword} from ATTRS in STRING."
  (let ((a attrs) name val)
    (while a
      (setq name (pop a) val (pop a))
      (when (stringp val)
        (setq string
              (replace-regexp-in-string (regexp-quote (concat "${" (substring (symbol-name name) 1) "}"))
                                        (or val "")
                                        string t t))))
    string))

(defun emacsconf-public-talks (info)
  (seq-filter (lambda (f) (plist-get f :public)) info))

(defun emacsconf-format-short-time (string &optional omit-end-time)
  (if (stringp string) (setq string (org-timestamp-from-string string)))
  (downcase
   (concat (format-time-string "~%l:%M%p"
                               (org-timestamp-to-time
                                (org-timestamp-split-range
                                 string)))
           (if omit-end-time ""
             (format-time-string "-%l:%M%p"
                                 (org-timestamp-to-time
                                  (org-timestamp-split-range
                                   string t)))))))

(defvar emacsconf-focus 'time "'time or 'status")

;;; Embark
(defun emacsconf-embark-finder ()
	"Identify when we're on a talk subtree."
  (when (and (derived-mode-p 'org-mode)
             (org-entry-get-with-inheritance "SLUG"))
    (cons 'emacsconf (org-entry-get-with-inheritance "SLUG"))))

(defun emacsconf-insert-talk-title (search)
	"Insert the talk title matching SEARCH."
  (interactive (list (emacsconf-complete-talk)))
  (insert (plist-get (emacsconf-search-talk-info search) :title)))

(with-eval-after-load 'embark
  (add-to-list 'embark-target-finders 'emacsconf-embark-finder)
  (defvar-keymap embark-emacsconf-actions
    :doc "Keymap for emacsconf-related things"
    "a" #'emacsconf-announce
    "c" #'emacsconf-find-captions-from-slug
    "d" #'emacsconf-find-caption-directives-from-slug
    "p" #'emacsconf-set-property-from-slug
    "w" #'emacsconf-edit-wiki-page
    "s" #'emacsconf-set-start-time-for-slug
    "W" #'emacsconf-browse-wiki-page
    "u" #'emacsconf-update-talk
    "t" #'emacsconf-insert-talk-title
    "m" #'emacsconf-mail-speaker-from-slug
    "M" #'emacsconf-mail-insert-info
    "n" #'emacsconf-mail-notmuch-search-for-talk
    "f" #'org-forward-heading-same-level
    "b" #'org-backward-heading-same-level
    "RET" #'emacsconf-go-to-talk)
  (add-to-list 'embark-keymap-alist '(emacsconf . embark-emacsconf-actions)))

;;; Status updates

(defun emacsconf-status-update ()
  (interactive)
  (let ((emacsconf-info (emacsconf-get-talk-info)))
    (kill-new
     (format "%d captioned (%d minutes), %d received and waiting to be captioned (%d minutes)"
             (length (emacsconf-collect-field-for-status "CAPTIONED" :title))
             (apply '+ (seq-map 'string-to-number (conf-collect-field-for-status "CAPTIONED" :time)))
             (length (emacsconf-collect-field-for-status "PREREC_RECEIVED" :title))
             (apply '+ (seq-map 'string-to-number (conf-collect-field-for-status "PREREC_RECEIVED" :time)))))))

;; Timezones
(defvar emacsconf-timezone-offset
  (format-time-string "%z" (date-to-time emacsconf-date) emacsconf-timezone)
  "Timezone offset for `emacsconf-timezone' on `emacsconf-date'.")

(defun emacsconf-timezone-string (o tz)
  (let* ((timestamp (org-timestamp-from-string (plist-get o :scheduled)))
         (start (org-timestamp-to-time (org-timestamp-split-range timestamp)))
         (end (org-timestamp-to-time (org-timestamp-split-range timestamp t))))
    (if (string= tz "UTC")
        (format "%s - %s "
                (format-time-string "%A, %b %-e %Y, ~%-l:%M %p"
                                    start tz)
                (format-time-string "%-l:%M %p %Z"
                                    end tz))
      (format "%s - %s (%s)"
              (format-time-string "%A, %b %-e %Y, ~%-l:%M %p"
                                  start tz)
              (format-time-string "%-l:%M %p %Z"
                                  end tz)
              tz))))
(defun emacsconf-timezone-strings (o &optional timezones)
  (mapcar (lambda (tz) (emacsconf-timezone-string o tz)) (or timezones emacsconf-timezones)))

;;;###autoload
(defun emacsconf-convert-from-timezone (timezone time)
  (interactive (list (progn
											 (require 'tzc)
											 (if (org-entry-get (point) "TIMEZONE")
													 (completing-read (format "From zone (%s): "
																										(org-entry-get (point) "TIMEZONE"))
																						tzc-time-zones nil nil nil nil
																						(org-entry-get (point) "TIMEZONE"))
												 (completing-read "From zone: " tzc-time-zones nil t)))
                     (read-string "Time: ")))
  (let* ((from-offset (format-time-string "%z" (date-to-time emacsconf-date) timezone))
         (time
          (date-to-time
           (concat emacsconf-date "T" (string-pad time 5 ?0 t)  ":00.000"
                   from-offset))))
    (message "%s = %s"
             (format-time-string
              "%b %d %H:%M %z"
              time
              timezone)
             (format-time-string
              "%b %d %H:%M %z"
              time
              emacsconf-timezone))))

(defun emacsconf-timezone-set (timezone)
	"Set the timezone for the current Org entry."
	(interactive (list (progn (require 'tzc) (completing-read "Timezone: " tzc-time-zones))))
	(org-entry-put (point) "TIMEZONE" timezone))

;;; Etherpad
(defvar emacsconf-review-comments-heading "Comments")
(defun emacsconf-import-comments-from-etherpad-text (filename)
  (interactive "FEtherpad text export: ")
  (with-temp-buffer
    (insert-file-contents filename)
    (goto-char (point-min))
    (while (re-search-forward "^[\t ]+Comments for \\([^:]+\\)" nil t)
      (let ((slug (match-string 1))
            comments)
        (forward-line 1)
        (setq comments
              (split-string
                (replace-regexp-in-string
                "\t\t\\*[ \t]*"
                ""
                (buffer-substring-no-properties
                 (point)
                 (if (re-search-forward "^[^\t]" nil t)
                     (match-beginning 0)
                   (point-max))))
               "\n"))
        (save-window-excursion
          (emacsconf-with-talk-heading slug
            ;; Do we already have a heading for comments?
            (if (re-search-forward (concat "^\\(\\*+\\) +" emacsconf-review-comments-heading)
                                   (save-excursion (org-end-of-subtree)) t)
                (org-end-of-meta-data)
              (org-end-of-subtree)
              (org-insert-heading-after-current)
              (insert emacsconf-review-comments-heading "\n"))
            ;; Are these comments already included?
            (save-restriction
              (org-narrow-to-subtree)
              (mapc (lambda (o)
                      (goto-char (point-min))
                      (unless (re-search-forward (regexp-quote o) nil t)
                        (goto-char (point-max))
                        (unless (bolp) (insert "\n"))
                        (insert "- " o "\n")))
                    comments))))))))

;;; Validation

(defun emacsconf-validate-all-talks-have-comments-for-speakers ()
  (interactive)
  (emacsconf-for-each-talk
    (unless (re-search-forward "^\\(- \\)?For \\(the \\)?[^ ]+ speaker" (save-excursion (org-end-of-subtree) (point)) t)
      (error "Could not find comment for %s" (org-entry-get (point) "SLUG"))))
  nil)

(defun emacsconf-validate-all-talks-have-field (field)
  (emacsconf-for-each-talk
    (when (string= (or (org-entry-get (point) field) "") "")
      (error "%s is missing %s" (org-entry-get (point) "SLUG") field)))
  nil)



(defvar emacsconf-time-constraints
  '(("saturday morning break" "10:00" "11:30")
    ("saturday lunch" "11:30" "13:30")
    ("saturday closing remarks" "16:30" "17:30")
    ("sunday morning break" "10:00" "11:30")
    ("sunday lunch" "11:30" "13:30")
    ("sunday closing remarks" "16:30" "17:30")))

(defun emacsconf-validate-no-overlaps (&optional info)
  (let (results))
  (while (cdr info)
    (when (and (plist-get (car info) :slug)
               (time-less-p (plist-get (cadr info) :start-time) (plist-get (car info) :end-time)))
      (setq results (cons "%s overlaps with %s (ends %s, starts %s)"
                          (or (plist-get (car info) :slug)
                              (plist-get (car info) :title))
                          (or (plist-get (cadr info) :slug)
                              (plist-get (cadr info) :title))
                          (format-time-string "%H:%M" (plist-get (car info) :end-time))
                          (format-time-string "%H:%M" (plist-get (cadr info) :start-time)))))
    (setq info (cdr info))))

(defun emacsconf-active-talks (list)
  "Remove CANCELLED talks from the list."
  (seq-remove (lambda (o) (string= (plist-get o :status) "CANCELLED")) list))

(defun emacsconf-validate-talk-subtree ()
  "Report an error if required properties are missing."
  (interactive)
  (let* ((props (org-entry-properties))
         (missing (seq-remove
                  (lambda (o) (assoc-default (symbol-name o) props))
                  '(CUSTOM_ID SLUG NAME NAME_SHORT EMAIL AVAILABILITY Q_AND_A TRACK MAX_TIME))))
    (when missing
      (if (called-interactively-p 'any)
          (message "Missing %s"  (mapconcat #'symbol-name missing ", "))
        (format "Missing %s"  (mapconcat #'symbol-name missing ", "))))))

;;; Ansible support
(defun emacsconf-ansible-export-talks ()
  (interactive)
  (when emacsconf-ansible-directory
    (with-temp-file (expand-file-name "talks.json" emacsconf-ansible-directory)
      (insert (json-encode (list :talks
                                 (mapcar (lambda (o)
                                           (apply 'list
                                                  (cons :start-time (format-time-string "%FT%T%z" (plist-get o :start-time) t))
                                                  (cons :end-time (format-time-string "%FT%T%z" (plist-get o :end-time) t))
                                                  (mapcar (lambda (field)
                                                            (cons field (plist-get o field)))
                                                          '(:slug :title :speakers :pronouns :pronunciation :url :track :video-slug)))
                                           )
                                         (emacsconf-filter-talks (emacsconf-get-talk-info)))))))))

(defun emacsconf-ansible-load-vars (file)
  (interactive (list (read-file-name "File: " emacsconf-ansible-directory)))
  (with-temp-buffer
    (insert-file-contents file)
    (let ((vars
           (yaml-parse-string (buffer-string))))
      (mapc (lambda (var)
              (set (car var) (gethash (cdr var) vars))
              )
            '((emacsconf-pad-api-key . etherpad_api_key)
              (emacsconf-pad-base . etherpad_url)
              (emacsconf-backstage-password . emacsconf_backstage_password))))))
;; (emacsconf-ansible-load-vars (expand-file-name "prod-vars.yml" emacsconf-ansible-directory))
;;; Tracks
(defvar emacsconf-tracks
  `((:name "General" :color "peachpuff" :id "gen" :channel "emacsconf-gen"
           :watch "https://live.emacsconf.org/2022/watch/gen/"
				   :tramp "/ssh:emacsconf-gen@res.emacsconf.org#46668:"
           :webchat-url "https://chat.emacsconf.org/?join=emacsconf,emacsconf-org,emacsconf-accessible,emacsconf-dev,emacsconf-gen"
           :stream ,(concat emacsconf-stream-base "gen.webm")
           :480p ,(concat emacsconf-stream-base "gen-480p.webm")
					 :youtube-url "https://www.youtube.com/watch?v=UEJ88c7MJq0"
					 :youtube-studio-url "https://studio.youtube.com/video/UEJ88c7MJq0/livestreaming"
					 :toobnix-url "https://toobnix.org/w/7t9X8eXuSby8YpyEKTb4aj"
           :start "09:00" :end "17:00"
           :vnc-display ":5"
           :vnc-port "5905"
           :status "offline")
   (:name "Development" :color "skyblue" :id "dev" :channel "emacsconf-dev"
          :watch "https://live.emacsconf.org/2022/watch/dev/"
				  :webchat-url "https://chat.emacsconf.org/?join=emacsconf,emacsconf-org,emacsconf-accessible,emacsconf-gen,emacsconf-dev"
          :tramp "/ssh:emacsconf-dev@res.emacsconf.org#46668:"
					:toobnix-url "https://toobnix.org/w/w6K77y3bNMo8xsNuqQeCcD"
					:youtube-url "https://www.youtube.com/watch?v=PMaoF-xa1b4"
					:youtube-studio-url "https://studio.youtube.com/video/PMaoF-xa1b4/livestreaming"
					:stream ,(concat emacsconf-stream-base "dev.webm")
          :480p ,(concat emacsconf-stream-base "dev-480p.webm")
          :start "10:00" :end "17:00"
          :vnc-display ":6"
          :vnc-port "5906"
          :status "offline")))

(defun emacsconf-get-track (name)
  (when (and (listp name) (plist-get name :track))
		(setq name (plist-get name :track)))
	(if (stringp name)
			(or
			 (seq-find (lambda (track) (or (string= name (plist-get track :name))
																		 (string= name (plist-get track :id))))
								 emacsconf-tracks)
			 (let ((talk (emacsconf-resolve-talk name)))
				 (seq-find (lambda (track) (or (string= (plist-get talk :track) (plist-get track :name))
																		 (string= (plist-get talk :track) (plist-get track :id))))
									 emacsconf-tracks))
			 name)
		name))

(defun emacsconf-by-track (info)
  (mapcar (lambda (track)
            (seq-filter
             (lambda (talk)
               (string= (plist-get talk :track) (plist-get track :name)))
             info))
          emacsconf-tracks))

(defun emacsconf-complete-track (&optional prompt tracks)
  (emacsconf-get-track
	 (completing-read
		(or prompt "Track: ")
		(mapcar (lambda (o) (plist-get o :name)) (or tracks emacsconf-tracks)))))

(defun emacsconf-by-day (info)
  (seq-group-by (lambda (o)
                  (format-time-string "%Y-%m-%d" (plist-get o :start-time) emacsconf-timezone))
                (sort (seq-filter (lambda (o)
                                    (or (plist-get o :slug)
                                        (plist-get o :include-in-info)))
                                  info)
                      #'emacsconf-sort-by-scheduled)))

(defun emacsconf-filter-talks-by-track (track info)
  (when (stringp track) (setq track (emacsconf-get-track track)))
  (seq-filter (lambda (o) (string= (plist-get o :track) (plist-get track :name))) info))

(defun emacsconf-filter-talks-by-slugs (slugs &optional info)
	(setq info (or info (emacsconf-get-talk-info)))
	(if slugs
			(seq-filter (lambda (o)
										(member (plist-get o :slug)
														slugs))
									info)
		info))

(defun emacsconf-filter-talks-by-logbook (text &optional info)
	(setq info (or info (emacsconf-get-talk-info)))
	(if text
			(seq-remove (lambda (o)
										(and (plist-get o :logbook)
												 (string-match (regexp-quote text) (plist-get o :logbook))))
									info)
		info))

(defvar emacsconf-shifts
	(list (list :id "sat-am-gen" :track "General" :start "2022-12-03T08:00:00-0500" :end "2022-12-03T12:00:00-0500" :host "zaeph" :streamer "sachac" :checkin "corwin" :irc "dto" :pad "publicvoit" :coord "sachac") (list :id "sat-pm-gen" :track "General" :start "2022-12-03T13:00:00-0500" :end "2022-12-03T18:00:00-0500" :host "zaeph" :streamer "sachac" :checkin "FlowyCoder" :irc "bandali" :pad "publicvoit" :coord "sachac") (list :id "sat-am-dev" :track "Development" :start "2022-12-03T08:00:00-0500" :end "2022-12-03T12:00:00-0500" :host "bandali" :streamer "sachac" :checkin "corwin" :irc "dto" :coord "sachac") (list :id "sat-pm-dev" :track "Development" :start "2022-12-03T13:00:00-0500" :end "2022-12-03T18:00:00-0500" :host "bandali" :streamer "sachac" :checkin "FlowyCoder" :irc "bandali" :coord "sachac") (list :id "sun-am-gen" :track "General" :start "2022-12-04T08:00:00-0500" :end "2022-12-04T12:00:00-0500" :host "zaeph" :streamer "sachac" :checkin "corwin" :irc "dto" :pad "publicvoit" :coord "sachac") (list :id "sun-pm-gen" :track "General" :start "2022-12-04T13:00:00-0500" :end "2022-12-04T18:00:00-0500" :host "zaeph" :streamer "jman" :checkin "FlowyCoder" :irc "bandali" :pad "publicvoit" :coord "sachac") (list :id "sun-am-dev" :track "Development" :start "2022-12-04T08:00:00-0500" :end "2022-12-04T12:00:00-0500" :host "bandali" :streamer "sachac" :checkin "corwin" :irc "dto" :coord "sachac") (list :id "sun-pm-dev" :track "Development" :start "2022-12-04T13:00:00-0500" :end "2022-12-04T18:00:00-0500" :host "bandali" :streamer "sachac" :checkin "FlowyCoder" :irc "bandali" :coord "sachac")))

(defun emacsconf-filter-talks-by-time (start-time end-time info)
  "Return talks that are between START-TIME and END-TIME (inclusive) in INFO."
  (when (stringp start-time) (setq start-time (date-to-time start-time)))
  (when (stringp end-time) (setq end-time (date-to-time end-time)))
  (seq-filter (lambda (o)
                (and (plist-get o :start-time)
                     (time-less-p (plist-get o :start-time) end-time) 
                     (time-less-p start-time (plist-get o :end-time))))
              info))

(defun emacsconf-get-shift (time)
  "Return the shift that TIME is in."
  (unless (stringp time)
    (setq time (format-time-string "%Y-%m-%dT%H:%M:%S%z" time emacsconf-timezone)))
  (seq-find (lambda (shift)
              (and (not (string> time (plist-get shift :end)))
                   (not (string> (plist-get shift :start) time))))
            emacsconf-shifts))

(defun emacsconf-filter-talks-by-shift (time track info)
  "Return a list of talks that are in the shift specified by TIME.
Filter by TRACK if given.  Use INFO as the list of talks."
  (let* ((shift (emacsconf-get-shift time))
         (list (emacsconf-filter-talks-by-time (plist-get shift :start) (plist-get shift :end) info)))
    (if track
        (emacsconf-filter-talks-by-track track info)
      list)))

(defun emacsconf-talk-all-done-p (talk)
  (member (plist-get talk :status) (split-string "TO_ARCHIVE TO_EXTRACT TO_FOLLOW_UP DONE")))

(defun emacsconf-bbb-status (talk)
  (let ((states
         '((open . "OPEN_Q UNSTREAMED_Q")
           (before . "TODO TO_REVIEW TO_ACCEPT WAITING_FOR_PREREC TO_PROCESS PROCESSING TO_AUTOCAP TO_ASSIGN TO_CAPTION TO_STREAM PLAYING CLOSED_Q")
           (after . "TO_ARCHIVE TO_EXTRACT TO_REVIEW_QA TO_INDEX_QA TO_CAPTION_QA TO_FOLLOW_UP DONE")
           (cancelled . "CANCELLED"))))
    (if (string-match "live" (or (plist-get talk :q-and-a) ""))
        (or (car (seq-find (lambda (state)
                             (member (plist-get talk :status) (split-string (cdr state))))
                           states))
            (throw 'error "Unknown talk BBB state"))
      'irc)))

(defun emacsconf-captions-edited-p (filename)
  "Return non-nil if FILENAME has been edited and is okay for inclusion."
  (and
	 filename
	 (file-exists-p filename)
   (with-temp-buffer
     (insert-file-contents filename)
     (goto-char (point-min))
     (re-search-forward "captioned by" (line-end-position) t))))

(defvar emacsconf-bbb-base-url "https://bbb.emacsverse.org/" "Include trailing slash.")
(defun emacsconf-bbb-room-title-list (&optional info)
  (delq nil
        (mapcar
         (lambda (o)
           (when (car o)
             (concat "ec" (substring emacsconf-year 2)
                     "-" (plist-get (emacsconf-get-shift (plist-get (cadr o) :start-time)) :id)
                     "-" (plist-get (emacsconf-get-track (plist-get (cadr o) :track)) :id)
                     " " (car o)
                     " ("
                     (mapconcat (lambda (talk) (plist-get talk :slug)) (cdr o) ", ")
                     ")")))
         (seq-group-by (lambda (o) (plist-get o :speakers))
                       (or info (emacsconf-active-talks (emacsconf-filter-talks (emacsconf-get-talk-info))))))))

(defun emacsconf-surround (before text after &optional alternative)
  "Concat BEFORE, TEXT, and AFTER if TEXT is specified, or return ALTERNATIVE."
  (if (and text (not (string= text "")))
      (concat (or before "") text (or after ""))
    alternative))

;;; Volunteer management
(defun emacsconf-get-volunteer-info ()
  (with-current-buffer (find-file-noselect emacsconf-org-file)
    (org-map-entries (lambda () (org-entry-properties))
                     "volunteer+EMAIL={.}")))

(defun emacsconf-volunteer-emails-for-completion (&optional info)
  (mapcar (lambda (o)
            (emacsconf-surround
             (if (assoc-default "ITEM" o)
                 (concat (assoc-default "ITEM" o) " <")
               "<")
             (assoc-default "EMAIL" o)
             ">" ""))
          (or info (emacsconf-get-volunteer-info))))

(defun emacsconf-complete-volunteer (&optional info)
  (setq info (or info (emacsconf-get-volunteer-info)))
  (let* ((choices
          (emacsconf-volunteer-emails-for-completion))
         (choice (completing-read
                  "Volunteer: " 
                  (lambda (string predicate action)
                    (if (eq action 'metadata)
                        '(metadata (category . volunteer))
                      (complete-with-action action choices string predicate))))))
    (elt info (seq-position choices choice))))

;;; Reflowing
(defun emacsconf-reflow ()
  "Help reflow text files."
  (interactive)
  (let (input last-input (case-fold-search t))
    (while (not (string= "" (setq input (read-string "Word: "))))
      (when (string= input "!")
        (delete-backward-char 1)
        (insert " ")
        (end-of-line)
        (re-search-forward (regexp-quote last-input) nil t)
        (setq input last-input))
      (if (string= input "'")
          (progn
            (end-of-line)          
            (unless (looking-back " ")
              (insert " "))          
            (delete-char 1))
        (forward-word)
        (cond
         ((string= input ",")
          (re-search-forward ", " nil t)
          (goto-char (match-end 0)))
         ((string= input ".")
          (re-search-forward "\\. " nil t)
          (goto-char (match-end 0)))
         (t
          (re-search-forward (concat "\\<" (regexp-quote input)) nil t)
          (goto-char (match-beginning 0))))
        (insert "\n")
        (when (< (+ (- (line-end-position) (point))
										(save-excursion (- (line-end-position) (line-beginning-position)))
										1) fill-column)
          (save-excursion
            (goto-char (line-end-position))
            (insert " ")
            (delete-char 1)))
        (setq last-input input)
        (recenter)
        (undo-boundary)))))

(defmacro emacsconf-with-todo-hooks (&rest body)
  "Run BODY with the Emacsconf todo hooks."
  `(with-current-buffer (find-file-noselect emacsconf-org-file)
     (let ((org-after-todo-state-change-hook '(emacsconf-org-after-todo-state-change)))
       ,@body)))

(defun emacsconf-add-org-after-todo-state-change-hook ()
  "Add FUNC to `org-after-todo-stage-change-hook'."
  (interactive)
  (with-current-buffer (find-buffer-visiting emacsconf-org-file)
    (add-hook 'org-after-todo-state-change-hook #'emacsconf-org-after-todo-state-change nil t)))

(defun emacsconf-remove-org-after-todo-state-change-hook ()
  "Remove FUNC from `org-after-todo-stage-change-hook'."
  (interactive)
  (with-current-buffer (find-buffer-visiting emacsconf-org-file)
    (remove-hook 'org-after-todo-state-change-hook
                 #'emacsconf-org-after-todo-state-change  t)))

(defvar emacsconf-todo-hooks
  '((emacsconf-stream-play-talk-on-change "gen" "dev")
    (emacsconf-stream-open-qa-windows-on-change "gen" "dev")
    emacsconf-erc-announce-on-change ;; announce via ERC
    emacsconf-publish-bbb-redirect
    emacsconf-stream-update-talk-info-on-change
    emacsconf-publish-media-files-on-change
    ;; emacsconf-publish-update-talk  ;; skipping this for now, I'll do this locally
    emacsconf-publish-backstage-org-on-state-change ;; update the backstage index
    ;; write to the talk text
    )
  "Functions to run when the todo state changes.
They will be called with TALK.")

(defun emacsconf-org-after-todo-state-change ()
  "Run all the hooks in `emacsconf-todo-hooks'.
If an `emacsconf-todo-hooks' entry is a list, run it only for the
tracks with the ID in the cdr of that list."
  (let* ((talk (emacsconf-get-talk-info-for-subtree))
         (track (emacsconf-get-track (plist-get talk :track))))
    (mapc
     (lambda (hook-entry)
       (cond
        ((symbolp hook-entry) (funcall hook-entry talk))
        ((member (plist-get track :id) (cdr hook-entry))
         (funcall (car hook-entry) talk))))
     emacsconf-todo-hooks)))

(defun emacsconf-broadcast (message)
  (interactive "MMessage: ")
  (when (not (string= (or message "") ""))
    (erc-cmd-BROADCAST message))
  (emacsconf-stream-broadcast message))

(defun emacsconf-agenda ()
  (interactive)
  (let ((org-agenda-files (list emacsconf-org-file)))
    (org-agenda-list nil emacsconf-date 2)))

(defun emacsconf-track-agenda (track)
  (interactive (list (emacsconf-complete-track)))
  (when (stringp track) (setq track (emacsconf-get-track track)))
  (let ((org-agenda-files (list emacsconf-org-file))
        (org-agenda-category-filter-preset (list (concat "+" (plist-get track :id)))))
    (org-agenda-list nil emacsconf-date 2)))

(defun emacsconf-update-talk-status-with-hooks (slug from-states to-state)
	(interactive (list (emacsconf-complete-talk) "." (completing-read "To: " (mapcar 'car emacsconf-status-types))))
	(emacsconf-with-todo-hooks
	 (emacsconf-update-talk-status slug from-states to-state)))

(defun emacsconf-update-talk-status (slug from-states to-state)
  (interactive (list (emacsconf-complete-talk) "." (completing-read "To: " (mapcar 'car emacsconf-status-types))))
  (emacsconf-with-talk-heading slug
    (when (string-match from-states (org-entry-get (point) "TODO"))
      (org-todo to-state)
      (save-buffer))))

;; copied from org-ascii--indent-string
(defun emacsconf-indent-string (s width)
  "Indent S by WIDTH spaces."
  (replace-regexp-in-string "\\(^\\)[ \t]*\\S-" (make-string width ?\s) s nil nil 1))

(defun emacsconf-add-to-logbook (note)
  "Add NOTE as a logbook entry for the current subtree."
  (move-marker org-log-note-return-to (point))
  (move-marker org-log-note-marker (point))
  (with-temp-buffer
    (insert note)
    (let ((org-log-note-purpose 'note))
      (org-store-log-note))))

(defun emacsconf-add-to-talk-logbook (talk note)
  "Add NOTE as a logbook entry for TALK."
  (interactive (list (emacsconf-complete-talk) (read-string "Note: ")))
  (save-excursion
    (emacsconf-with-talk-heading talk
      (emacsconf-add-to-logbook note))))

(defun emacsconf-reload ()
  "Reload the emacsconf-el modules."
  (interactive)
  (mapc #'load-library '("emacsconf" "emacsconf-erc" "emacsconf-publish" "emacsconf-stream" "emacsconf-pad")))

(defun emacsconf-find-talk-file-in-cache (talk filename)
  (interactive (let ((talk (emacsconf-complete-talk-info)))
                 (list
                  talk
                  (completing-read "File: " (directory-files emacsconf-cache-dir t (plist-get talk :video-slug))))))
  (find-file filename))

(defun emacsconf-cache-find-file (filename)
  (interactive (list (read-file-name "File: " (expand-file-name "./" emacsconf-cache-dir) nil t)))
  (find-file (expand-file-name filename emacsconf-cache-dir)))

(defun emacsconf-format-seconds (seconds)
	(concat (format-seconds "%.2m:%.2s" (floor seconds))
					"." (format "%03d" (% (floor (* 1000 seconds)) 1000))))

(defun emacsconf-insert-time-for-speaker (talk)
	(interactive (list (emacsconf-complete-talk-info)))
	(insert
	 (format-time-string "%-I:%M %P %Z" (plist-get talk :start-time) emacsconf-timezone)
	 " (" emacsconf-timezone ")"
	 (if (string= (format-time-string "%z" (plist-get talk :start-time) (plist-get talk :timezone))
								emacsconf-timezone-offset)
			 ""
		 (concat
			" which should be the same as "
			(format-time-string "%-I:%M %P %Z" (plist-get talk :start-time) (plist-get talk :timezone))
			" in "
			(plist-get talk :timezone)))))

(defun emacsconf-collect-prop (prop list)
	(mapcar (lambda (o) (plist-get o prop)) list))

(defun emacsconf-talk-file (talk suffix &optional always source)
	(let ((wiki-filename
				 (expand-file-name (concat (plist-get talk :video-slug) suffix)
													 (expand-file-name "captions"
																						 (expand-file-name (plist-get talk :year)
																															 emacsconf-directory))))
				(cache-filename
				 (expand-file-name (concat (plist-get talk :video-slug) suffix)
													 emacsconf-cache-dir)))
		(cond
		 ((and (file-exists-p wiki-filename) (not (eq source 'cache))) wiki-filename)
		 ((and (file-exists-p cache-filename) (not (eq source 'wiki-captions))) cache-filename)
		 (always cache-filename))))

(with-eval-after-load 'org
	(defun emacsconf-el-complete ()
		"Complete a file from the Emacsconf Elisp library."
		(concat "emacsconf-el:"
						(file-name-base
						 (read-file-name
							"File: "
							(file-name-directory (locate-library "emacsconf.el"))))))
	(defun emacsconf-el-open (link _)
		"Visit a file from the Emacsconf Elisp library."
		(find-file
		 (expand-file-name
			link
			(file-name-directory (locate-library "emacsconf.el")))))

	(defun emacsconf-el-export (link description format _)
		"Export link to emacsconf-el file."
		(format "<a href=\"https://git.emacsconf.org/emacsconf-el/tree/%s\">%s</a>"
						link (or description link)))
	
	(org-link-set-parameters
	 "emacsconf-el"
	 :complete #'emacsconf-el-complete
	 :export #'emacsconf-el-export
	 :follow #'emacsconf-el-open)

	(defun emacsconf-ansible-complete ()
		"Complete a file from the Emacsconf Elisp library."
		(concat "emacsconf-el:"
						(file-name-base
						 (read-file-name
							"File: "
							emacsconf-ansible-directory))))
	(defun emacsconf-ansible-open (link _)
		"Visit a file from the Emacsconf Elisp library."
		(find-file
		 (expand-file-name
			link
			emacsconf-ansible-directory)))

	(defun emacsconf-ansible-export (link description format _)
		"Export link to emacsconf-el file."
		(format "<a href=\"https://git.emacsconf.org/emacsconf-ansible/tree/%s\">%s</a>"
						link (or description link)))
	
	(org-link-set-parameters
	 "emacsconf-ansible"
	 :complete #'emacsconf-ansible-complete
	 :export #'emacsconf-ansible-export
	 :follow #'emacsconf-ansible-open))

(defun emacsconf-end-of-week (date)
	"Useful for analyzing data. Assumes week ends Sunday."
	(let ((d (decode-time (date-to-time date))))
		(format-time-string "%Y-%m-%d"
												(encode-time
												 (decoded-time-add d (make-decoded-time :day (% (- 7 (decoded-time-weekday d)) 7)))))))
(provide 'emacsconf)
;;; emacsconf.el ends here