;;; eventful.el --- Eventful API client for Emacs ;; Copyright (C) 2005-2007 Eventful, Inc. ;; Author: Edward O'Connor ;; Maintainer: Eventful API Developers ;; Keywords: convenience ;; This file 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 2, or (at your option) ;; any later version. ;; This file 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 GNU Emacs; see the file COPYING. If not, write to ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, ;; Boston, MA 02110-1301, USA. ;;; Commentary: ;; eventful.el is an Eventful API client library for Emacs. ;;; History: ;; 2007-08-06: Renamed from evdb.el. ;;; Code: (require 'md5) (require 'url) ;; Tested with the URL package in CVS Emacs (require 'xml) ;; xml.el in CVS Emacs ;; Placate the byte-compiler. (defvar url-http-end-of-headers) ;; * User-serviceable parts. (defgroup eventful nil "Emacs interface to Eventful's REST API." :group 'processes :prefix "eventful-") (defcustom eventful-app-key nil "Your application's Eventful API key." :group 'eventful :type '(string)) ;; * General elisp utilities (defun eventful-md5 (string) "Return a lower-case MD5sum of STRING." (downcase (md5 string))) (defun eventful-string (object) "Return a string representation of OBJECT." (cond ((stringp object) object) ((symbolp object) (symbol-name object)) ((numberp object) (number-to-string object)) (t (format "%s" object)))) (defun eventful-format-url-parameters (alist) "Format ALIST as HTTP query parameters. \(eventful-format-url-parameters '((foo . 1) (bar . \"baz quux\"))) => \"foo=1&bar=baz+quux\"" (mapconcat (lambda (cons) (format "%s=%s" (url-hexify-string (eventful-string (car cons))) (url-hexify-string (eventful-string (cdr cons))))) alist "&")) (defun eventful-xml-node-text (xml &rest path) "Extract the text of XML's child named by PATH." (let ((node xml)) (while path (setq node (car (xml-get-children node (pop path))))) (mapconcat 'identity (xml-node-children node) " "))) ;; * Bleah. (defvar eventful-user-key nil) ;; * HTTP request/response handling (put 'eventful-error 'error-message "Eventful API error") (put 'eventful-error 'error-conditions '(eventful-error error)) (defvar eventful-debug nil) ;; http://api.eventful.com/docs/errors (defun eventful-check-error (response) "Check for an error in RESPONSE. If an error is found, signal the error." (let ((string (xml-get-attribute response 'string)) (description (eventful-xml-node-text response 'description))) (when (eq (xml-node-name response) 'error) (when eventful-debug (switch-to-buffer (current-buffer))) (signal 'eventful-error (list string description))))) (defun eventful-response (buffer &optional no-error) "Process the XML response from Eventful which resides in BUFFER. If optional argument NO-ERROR is non-nil, don't check for errors in the response." (unwind-protect (with-current-buffer buffer (save-excursion (goto-char url-http-end-of-headers) (let ((response (xml-parse-region (point) (point-max) nil nil nil))) (setq response (car response)) (unless no-error (eventful-check-error response)) response))) (unless eventful-debug (kill-buffer buffer)))) (defvar eventful-username nil "The username under which you're currently logged in.") (defun eventful-request (call args &optional http-method no-error) "Perform a Eventful API request to CALL with ARGS using HTTP-METHOD. If optional argument NO-ERROR is non-nil, don't check for errors in the response." (let ((url-package-name "eventful.el") (url-request-method (or http-method "GET"))) (setq args (cons (cons 'app_key eventful-app-key) args)) (when eventful-user-key (setq args (append `((user . ,eventful-username) (user_key . ,eventful-user-key)) args))) (eventful-response (url-retrieve-synchronously (let ((url (concat "http://api.eventful.com/rest" call "?" (eventful-format-url-parameters args)))) (message "eventful-equest: %s" url) url)) no-error))) ;; * Authentication (defun eventful-login (username password) "Login as USERNAME with PASSWORD. Read more here: http://api.eventful.com/docs/auth" (let* ((nonce (eventful-xml-node-text (let ((eventful-username nil) (eventful-user-key nil)) ;; We directly call `eventful-request' here because ;; `eventful-api/users/login won't do what we'd like it to. (eventful-request "/users/login" nil nil t)) 'nonce)) (response (eventful-md5 (concat nonce ":" (eventful-md5 password))))) (setq eventful-username username) (setq eventful-user-key (eventful-xml-node-text (eventful-api/users/login username nonce response) 'user_key)))) ;; * Tools for defining API method wrappers. (defmacro eventful-api-method (method required-args optional-args docstring &rest body) "Define an elisp binding for Eventful API method METHOD. Specify REQUIRED-ARGS and OPTIONAL-ARGS. Documentation in the form of a DOCSTRING is required. The forms of BODY are evaluated before anything else happens." (let ((method-name (intern (concat "eventful-api" method))) (all-args (append required-args optional-args))) `(defun ,method-name (,@required-args ,@(when optional-args (append '(&optional) optional-args))) ,(format "%s\n\nRead more here: http://api.eventful.com/docs%s\n" docstring method) ,@body (let ((args (list ,@(mapcar (lambda (arg) (list 'cons (list 'quote arg) arg)) required-args)))) ,@(mapcar (lambda (arg) `(when ,arg (push (cons ',arg ,arg) args))) optional-args) (eventful-request ,method args))))) (put 'eventful-api-method 'lisp-indent-function 3) (put 'eventful-api-method 'doc-string-elt 4) (defmacro eventful-api-undocumented (method) "Define an elisp binding for Eventful API method METHOD. METHOD is undocumented at , so we blindly pass through whatever you've passed into this method. Assumes the arguments are in the form of a plist." (let ((method-name (intern (concat "eventful-api" method)))) `(defun ,method-name (&rest args) ,(format (concat "Eventful API method %s is undocumented.\n\n" "ARGS should be a plist of parameter names and values.\n" "e.g., (eventful-api%s \"foo\" 1 \"bar\" 2)\n\n" "Read more here: http://api.eventful.com/docs%s\n") method method method) (let ((real-args (list))) (while props (add-to-list real-args (cons (car props) (cadr props))) (setq props (cddr props))) (eventful-request ,method real-args))))) ;; * Wrapper functions for each API method. (eventful-api-method "/calendars/delete" (id) () "Delete the calendar whose id is ID.") (eventful-api-method "/calendars/events/add" (calendar_id event_id) () "Add EVENT_ID to the calendar whose id is CALENDAR_ID.") (eventful-api-method "/calendars/events/list" (id modified_since) (sort_order sort_direction page_size page_number) "List this calendar's events.") (eventful-api-method "/calendars/events/remove" (calendar_id event_id) () "Remove the event whose id is EVENT_ID from the calendar CALENDAR_ID.") (eventful-api-method "/calendars/get" (id) () "Fetch the calendar identified by ID.") (eventful-api-undocumented "/calendars/images/add") (eventful-api-undocumented "/calendars/images/remove") (eventful-api-method "/calendars/modify" (id) (calendar_name description tags privacy where_query what_query notify_schedule) "Modify the calendar with id ID.") (eventful-api-method "/calendars/new" (calendar_name) (description tags privacy where_query what_query notify_schedule) "Create a new calendar named CALENDAR_NAME.") (eventful-api-method "/calendars/properties/add" (id name value) () "Add the property NAME=VALUE to the calendar ID.") (eventful-api-method "/calendars/properties/list" (id) () "List the properties of the calendar with id ID.") (eventful-api-method "/calendars/properties/remove" (id) (property_id name) "Remove a property from the calendar whose id is ID." (assert (or property_id name) nil "Must provide either `property_id' or `name'")) (eventful-api-undocumented "/calendars/rights/add") (eventful-api-undocumented "/calendars/rights/remove") (eventful-api-method "/calendars/search" () (keywords count_only sort_order sort_direction page_size page_number) "Search for calendars.") (eventful-api-method "/categories/get" (id) () "Fetch the category identified by ID.") (eventful-api-method "/categories/list" () () "List Eventful's categories.") (eventful-api-undocumented "/demands/comments/delete") (eventful-api-undocumented "/demands/comments/modify") (eventful-api-undocumented "/demands/comments/new") (eventful-api-undocumented "/demands/events/add") (eventful-api-undocumented "/demands/events/remove") (eventful-api-method "/demands/get" (id) () "Fetch the demand identified by ID.") (eventful-api-undocumented "/demands/images/add") (eventful-api-undocumented "/demands/images/remove") (eventful-api-undocumented "/demands/links/delete") (eventful-api-undocumented "/demands/links/new") (eventful-api-undocumented "/demands/locales/search") (eventful-api-method "/demands/modify" (id) (description) "Modify the demand identified by ID.") (eventful-api-method "/demands/new" (performer_id location) (description tags) "Create a demand for PERFORMER_ID in LOCATION.") (eventful-api-undocumented "/demands/restore") (eventful-api-method "/demands/search" () (keywords location count_only sort_order sort_direction page_size page_number) "Search for demands.") (eventful-api-undocumented "/demands/tags/add") (eventful-api-undocumented "/demands/tags/list") (eventful-api-undocumented "/demands/tags/remove") (eventful-api-method "/demands/withdraw" (id note) () "Withdraw the demand with id ID, for reason NOTE.") (eventful-api-undocumented "/events/categories/add") (eventful-api-undocumented "/events/categories/remove") (eventful-api-method "/events/comments/delete" (comment_id) () "Delete the comment with id COMMENT_ID.") (eventful-api-method "/events/comments/modify" (comment_id, comment) () "Modify the comment with id COMMENT_ID. COMMENT is the new text.") (eventful-api-method "/events/comments/new" (id comment) () "Add a comment to the event with id ID. COMMENT is the comment text.") (eventful-api-undocumented "/events/flags/add") (eventful-api-undocumented "/events/flags/list") (eventful-api-undocumented "/events/flags/remove") (eventful-api-method "/events/get" (id) () "Fetch the event with identified by ID.") (eventful-api-method "/events/going/list" (id) () "Get a list of users who are going to this event.") (eventful-api-method "/events/images/add" (id image_id) () "Add the image whose id is IMAGE_ID to the event with id ID.") (eventful-api-undocumented "/events/images/delete") (eventful-api-undocumented "/events/images/new") (eventful-api-method "/events/images/remove" (id image_id) () "Remove the image whose id is IMAGE_ID to the event with id ID.") (eventful-api-method "/events/links/delete" (link_id) () "Delete the link with id LINK_ID from the event.") (eventful-api-method "/events/links/new" (id link link_type_id) (description) "Add a new link to the event with id ID. LINK is the URL, and LINK_TYPE_ID must be one of the values in the documentation.") (eventful-api-method "/events/modify" (id) (title start_time stop_time tz_olson_path all_day description privacy tags free price venue_id parent_id) "Modify the event with id ID.") (eventful-api-method "/events/new" (title start_time) (stop_time tz_olson_path all_day description privacy tags free price venue_id parent_id) "Create a new event.") (eventful-api-method "/events/performers/add" (id performer_id) () "Add the performer with id PERFORMER_ID to the event with id ID.") (eventful-api-undocumented "/events/performers/list") (eventful-api-method "/events/performers/remove" (id performer_id) () "Remove the performer with id PERFORMER_ID from the event with id ID.") (eventful-api-method "/events/properties/add" (id name value) () "Add the property NAME=VALUE to the event with id ID.") (eventful-api-method "/events/properties/list" (id) () "List the properties of the event whose id is ID.") (eventful-api-method "/events/properties/remove" (id) (property_id name) "Remove a property from the event whose id is ID." (assert (or property_id name) nil "Must provide either `property_id' or `name'")) (eventful-api-undocumented "/events/recurrence/list") (eventful-api-method "/events/restore" (id) () "Restore the event with identified by ID.") (eventful-api-method "/events/rights/add" (id relation) () "Enables RELATION (friends/family/contacts) to edit the event with id ID.") (eventful-api-method "/events/rights/remove" (id realtion) () "Disables RELATION (friends/family/contacts) from editing the event with ID.") (eventful-api-method "/events/search" () (keywords location date within units count_only sort_order sort_direction page_size page_number) "Search for events.") (eventful-api-undocumented "/events/tags/add") (eventful-api-method "/events/tags/delete" (tags id) () "Delete TAGS from the event whose id is ID.") (eventful-api-method "/events/tags/list" (id) () "Fetch the tags of the event identified by ID.") (eventful-api-method "/events/tags/new" (tags id) () "Add TAGS to the event whose id is ID.") (eventful-api-undocumented "/events/tags/remove") (eventful-api-method "/events/tags/search" (tag) () "Search for events tagged TAG.") (eventful-api-method "/events/withdraw" (id) (note) "Withdraw the event identified by ID. NOTE, if non-null, should be a string explaining the withdrawal.") (eventful-api-undocumented "/groups/calendars/add") (eventful-api-undocumented "/groups/calendars/delete") (eventful-api-undocumented "/groups/comments/add") (eventful-api-undocumented "/groups/comments/list") (eventful-api-undocumented "/groups/comments/modify") (eventful-api-undocumented "/groups/comments/remove") (eventful-api-method "/groups/events/add" (id event_id) () "Add EVENT_ID to the group whose id is ID.") (eventful-api-undocumented "/groups/events/list") (eventful-api-undocumented "/groups/events/remove") (eventful-api-method "/groups/get" (id) () "Fetch the group identified by ID.") (eventful-api-undocumented "/groups/images/add") (eventful-api-undocumented "/groups/images/remove") (eventful-api-undocumented "/groups/links/add") (eventful-api-undocumented "/groups/links/list") (eventful-api-undocumented "/groups/links/remove") (eventful-api-undocumented "/groups/modify") (eventful-api-undocumented "/groups/new") (eventful-api-undocumented "/groups/search") (eventful-api-undocumented "/groups/tags/add") (eventful-api-undocumented "/groups/tags/list") (eventful-api-undocumented "/groups/tags/remove") (eventful-api-undocumented "/groups/users/add") (eventful-api-undocumented "/groups/users/delete") (eventful-api-method "/groups/users/list" (id) () "Fetches the list of members of the group whose id is ID.") (eventful-api-undocumented "/groups/users/remove") (eventful-api-undocumented "/groups/venues/add") (eventful-api-undocumented "/groups/venues/delete") (eventful-api-method "/groups/withdraw" (id note) () "Withdraw the group whose id is ID, with reason NOTE.") (eventful-api-undocumented "/images/delete") (eventful-api-undocumented "/images/list") (eventful-api-method "/images/new" () (image_file image_url caption) "Upload an image to Eventful." (assert (or image_file image_url) nil "Must provide either `image_file' or `image_url'")) (eventful-api-method "/links/types/list" () () "Returns the list of link types supported by Eventful.") (eventful-api-undocumented "/locales/search") (eventful-api-method "/metros/upcoming/search" (venue_id) () "Given a venue ID, returns a list of Upcoming.org metros the venue might possibly be in.") (eventful-api-undocumented "/performers/comments/delete") (eventful-api-undocumented "/performers/comments/modify") (eventful-api-undocumented "/performers/comments/new") (eventful-api-undocumented "/performers/demands/list") (eventful-api-undocumented "/performers/events/list") (eventful-api-method "/performers/get" (id) () "Fetch the performer identified by ID.") (eventful-api-method "/performers/images/add" (id image_id) () "Add IMAGE_ID to performer whose id is ID.") (eventful-api-method "/performers/images/remove" (id image_id) () "Remove IMAGE_ID from performer whose id is ID.") (eventful-api-undocumented "/performers/links/add") (eventful-api-undocumented "/performers/links/remove") (eventful-api-method "/performers/modify" (id) (name short_bio long_bio tags) "Edit the performer whose id is ID.") (eventful-api-method "/performers/new" (name short_bio) (long_bio tags) "Create a new performer, named NAME.") (eventful-api-undocumented "/performers/restore") (eventful-api-method "/performers/search" () (keywords count_only sort_order sort_direction page_size page_number) "Search for performers.") (eventful-api-undocumented "/performers/tags/add") (eventful-api-undocumented "/performers/tags/list") (eventful-api-undocumented "/performers/tags/remove") (eventful-api-undocumented "/performers/user/add") (eventful-api-undocumented "/performers/user/remove") (eventful-api-method "/performers/withdraw" (id note) () "Withdraw the performer whose id is ID, with reason NOTE.") (eventful-api-undocumented "/users/calendars/delete") (eventful-api-method "/users/calendars/events/list" (id) (page_size page_number interval offset) "Given a calendar ID, returns a list of its events.") (eventful-api-method "/users/calendars/get" (id) () "Returns the properties of calendar ID.") (eventful-api-method "/users/calendars/list" (owner) () "Returns a list of OWNER's calendars.") (eventful-api-undocumented "/users/demands/list") (eventful-api-undocumented "/users/events/list") (eventful-api-undocumented "/users/events/recent") (eventful-api-undocumented "/users/favorites/add") (eventful-api-undocumented "/users/favorites/check") (eventful-api-undocumented "/users/favorites/list") (eventful-api-undocumented "/users/favorites/remove") (eventful-api-method "/users/get" (username) () "Fetch the user named USERNAME.") (eventful-api-method "/users/going/add" (event_id) () "Marks the logged in user as going to EVENT_ID.") (eventful-api-method "/users/going/remove" (event_id) () "Removes the logged in user from EVENT_ID's going list.") (eventful-api-undocumented "/users/groups/calendars/list") (eventful-api-method "/users/groups/list" (id) () "Lists the groups of which the user ID is a member.") (eventful-api-undocumented "/users/groups/users") (eventful-api-undocumented "/users/images/add") (eventful-api-undocumented "/users/images/delete") (eventful-api-undocumented "/users/images/get") (eventful-api-undocumented "/users/images/new") (eventful-api-undocumented "/users/images/remove") (eventful-api-method "/users/locales/add" (id locale) () "Adds LOCALE to the user (ID)'s list of saved locales.") (eventful-api-method "/users/locales/delete" (id locale) () "Removes LOCALE from the user (ID)'s list of saved locales.") (eventful-api-method "/users/locales/list" (id) () "Returns a list of the user (ID)'s locales.") (eventful-api-method "/users/login" (user nonce response) () "Log in. THIS IS NOT THE USER-SERVICEABLE LOGIN INTERFACE. Please call `eventful-login' instead. Read more here: http://api.eventful.com/docs/auth") (eventful-api-undocumented "/users/prefs/delete") (eventful-api-undocumented "/users/prefs/get") (eventful-api-undocumented "/users/prefs/put") (eventful-api-method "/users/relations/add" (member_id relation) () "Adds MEMBER_ID as bearing RELATION to the logged-in user.") (eventful-api-method "/users/relations/list" (id members) () "Lists all relations associated with the user (whose id is ID). If members is non-null, returns the members of each relation as well.") (eventful-api-method "/users/relations/remove" (member_id relation) () "Removes MEMBER_ID from the logged in user's RELATION list.") (eventful-api-method "/users/search" (keywords) (sort_order count_only page_size page_number) "Searches for users.") (eventful-api-method "/users/venues/list" (id) () "Lists the venues added by the user whose id is ID.") (eventful-api-method "/venues/comments/delete" (comment_id) () "Delete the comment with id COMMENT_ID.") (eventful-api-method "/venues/comments/modify" (comment_id, comment) () "Modify the comment with id COMMENT_ID. COMMENT is the new text.") (eventful-api-method "/venues/comments/new" (id comment) () "Add a comment to the venue with id ID. COMMENT is the comment text.") (eventful-api-method "/venues/get" (id) () "Fetch the venue identified by ID.") (eventful-api-undocumented "/venues/images/add") (eventful-api-undocumented "/venues/images/delete") (eventful-api-undocumented "/venues/images/new") (eventful-api-undocumented "/venues/images/remove") (eventful-api-method "/venues/links/delete" (link_id) () "Delete the link with id LINK_ID from the venue.") (eventful-api-method "/venues/links/new" (id link link_type_id) (description) "Add a new link to the venue with id ID. LINK is the URL, and LINK_TYPE_ID must be one of the values in the documentation.") (eventful-api-method "/venues/modify" (id country venue_type) (name address city region postal_code description privacy parent_id) "Modify the venue whose id is ID.") (eventful-api-method "/venues/new" (name country venue_type) (address city region postal_code description privacy url url_type parent_id) "Create a new venue named NAME.") (eventful-api-method "/venues/properties/add" (id name value) () "Add the property NAME=VALUE to the venue ID.") (eventful-api-method "/venues/properties/list" (id) () "List the properties of the venue with id ID.") (eventful-api-method "/venues/properties/remove" (id) (property_id name) "Remove a property from the venue whose id is ID." (assert (or property_id name) nil "Must provide either `property_id' or `name'")) (eventful-api-method "/venues/restore" (id) () "Restore the venue whose id is ID.") (eventful-api-undocumented "/venues/rights/add") (eventful-api-undocumented "/venues/rights/remove") (eventful-api-method "/venues/search" (keywords) (location count_only page_size page_number) "Search for venues.") (eventful-api-method "/venues/tags/delete" (tags id) () "Delete TAGS from the venue whose id is ID.") (eventful-api-method "/venues/tags/list" (id) () "Fetch the tags of the venue identified by ID.") (eventful-api-method "/venues/tags/new" (tags id) () "Add TAGS to the venue whose id is ID.") (eventful-api-method "/venues/tags/search" (tag) (page_size page_number) "List venues associated with TAG.") (eventful-api-method "/venues/withdraw" (id note) () "Withdraw the venue whose id is ID, with reason NOTE.") (provide 'eventful) ;;; eventful.el ends here