ae6459f1eb872f12c5760d6a43f034f5994f71a2
[slh] / lj.el
1 ;; lj.el --- LiveJournal meets SXEmacs   -*- Emacs-Lisp -*-
2
3 ;; Copyright (C) 2008, 2009 Steve Youngs
4
5 ;; Author:     Steve Youngs <steve@sxemacs.org>
6 ;; Maintainer: Steve Youngs <steve@sxemacs.org>
7 ;; Created:    <2008-06-15>
8 ;; Based On:   jwz-lj.el by Jamie Zawinski <jwz@jwz.org>
9 ;; Keywords:   blog, lj, livejournal
10 ;; Homepage:   <http://www.sxemacs.org/~steve/lj/lj.el>
11
12 ;; This file is part of SLH (Steve's Lisp Hacks).
13
14 ;; Redistribution and use in source and binary forms, with or without
15 ;; modification, are permitted provided that the following conditions
16 ;; are met:
17 ;;
18 ;; 1. Redistributions of source code must retain the above copyright
19 ;;    notice, this list of conditions and the following disclaimer.
20 ;;
21 ;; 2. Redistributions in binary form must reproduce the above copyright
22 ;;    notice, this list of conditions and the following disclaimer in the
23 ;;    documentation and/or other materials provided with the distribution.
24 ;;
25 ;; 3. Neither the name of the author nor the names of any contributors
26 ;;    may be used to endorse or promote products derived from this
27 ;;    software without specific prior written permission.
28 ;;
29 ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
30 ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
31 ;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
32 ;; DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
33 ;; FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
34 ;; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
35 ;; SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
36 ;; BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
37 ;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
38 ;; OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
39 ;; IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40
41 ;;; Commentary:
42 ;; 
43 ;;    First up, let me say that this would not have been possible if
44 ;;    it weren't for JWZ's jwz-lj.el.  In fact, large portions of
45 ;;    lj.el were lifted directly from jwz-lj.el.  So, thank you very
46 ;;    much, Jamie!
47 ;;
48 ;;; *** IMPORTANT BIT ***
49 ;;
50 ;;    You MUST compose your LJ posts in raw HTML (XHTML 1.0 Transitional
51 ;;    if you plan on validating the markup before you submit the post).
52 ;;    Don't bitch and complain about how hard or inconvenient that is.
53 ;;    I'm not listening.  The buffer where you write your posts is in a
54 ;;    derivative of html-mode so you've got everything you need right at
55 ;;    your fingertips.  And anyway, writing your LJ posts in raw HTML
56 ;;    gives you much more control over what the finished post will look
57 ;;    like.
58 ;;
59 ;;    Another important step before you can use lj.el is to log into
60 ;;    your LiveJournal a/c with your web browser and check the "remember
61 ;;    me" box on the login page.  This is the only way to have your
62 ;;    password stored in your cookies.  Don't worry, it is encrypted.
63 ;;
64 ;;; A Note about validating your markup:
65 ;;
66 ;;    Before you can validate your posts (with psgml) you need to
67 ;;    ensure that...
68 ;;
69 ;;        o You have both, the psgml, and psgml-dtds XEmacs packages
70 ;;          installed.  Well, duh!
71 ;;
72 ;;        o You have a working sgml toolchain.  It is way beyond the
73 ;;          scope of this lib (or my patience) to show you how.
74 ;;          Instead, do what the pros do and see:
75 ;;
76 ;;          http://www.linuxfromscratch.org/blfs/view/svn/pst/sgml.html
77 ;;
78 ;;        o psgml needs to know where your catalog files are.  The
79 ;;          easiest way to do that is to set SGML_CATALOG_FILES in
80 ;;          your shell environment...
81 ;;
82 ;;          export SGML_CATALOG_FILES=/etc/sgml/catalog:/path/to/psgml-dtds/CATALOG
83 ;;
84 ;;; Install/Set Up:
85 ;;
86 ;;    Whack this lib into your load-path somewhere and...
87 ;;     (require 'lj)
88 ;;     (setq lj-user-id "your_lj_id")
89 ;;
90 ;;    When you want to compose a new LJ entry... M-x lj RET
91 ;;
92 ;;    There's nothing hard or overly complicated here.  Take a look at
93 ;;    describe-mode (`C-h m') which will show you the keybindings
94 ;;    available.  All of the "lj-mode specific" interactive commands
95 ;;    have a binding.  There are 3 "global" commands that don't...
96 ;;
97 ;;         #'lj
98 ;;         #'lj-blog-buffer
99 ;;         #'lj-blog-region
100 ;;
101 ;;    The only reason they don't have keybindings is that I think it'd
102 ;;    be bad form on my part to set global keys for you.  Assign them
103 ;;    to keys if you want.
104 ;;
105 ;;    All of the "headers" have completion too.  A couple of tips
106 ;;    about the completion...
107 ;;
108 ;;      - By default iso-left-tab (that's shift-tab for the clueless)
109 ;;        will cycle backwards.
110 ;;
111 ;;      - The trick to getting multiple tags is to type a comma (`,')
112 ;;        plus the first letter or two of the next tag you wanna use
113 ;;        after the last inserted tag.
114 ;;
115 ;;; Twitter: (broken, don't use)
116 ;;
117 ;;    This is currently broken because twitter turned off Basic Auth
118 ;;
119 ;;    You can optionally post the subject header of your blog entry as
120 ;;    a status update to Twitter, along with a URL to the entry on
121 ;;    livejournal.com.  To do so, you must set...
122 ;;
123 ;;        `lj-twitter-flag'
124 ;;        `lj-twitter-username'
125 ;;        `lj-twitter-password'
126 ;;
127 ;;    The down side to this is that your twitter username and password
128 ;;    are stored in clear text.  I'll work on a way to make that safer
129 ;;    later.
130 ;;
131 ;;    Have fun with it!
132
133 ;;; Todo:
134 ;;
135 ;;    o Make "Writer's Block" a bit friendlier.  Add the ability to
136 ;;      choose different qotd's after one has been selected.  Also, be
137 ;;      able to view older qotd's.
138 ;;
139 ;;    o Rewrite the twitter code to use OAuth.
140 ;;
141
142 ;;; Bugs:
143 ;;
144 ;;    I've tried to make this compatible with XEmacs 21.5 and 21.4,
145 ;;    but I don't have either of those installed so I'm not 100%
146 ;;    certain.  As for GNU/Emacs... absolutely no idea, but I'd doubt
147 ;;    that this is anywhere near compatible.
148 ;;
149 ;;; History:
150 ;; 
151 ;;    See the tla logs
152 ;;
153
154 ;;; Version:
155 (defconst lj-version 1.25
156   "Version number of SXEmacs/LJ.")
157
158 ;;; Code:
159 (eval-when-compile
160   (autoload #'html-mode "psgml-html" nil t)
161   (autoload #'executable-find "executable")
162   (autoload #'completing-read-multiple "crm")
163   (autoload #'sgml-indent-or-tab "psgml" nil t)
164   (autoload #'sgml-parse-prolog "psgml-parse" nil t)
165   (autoload #'sgml-validate "psgml" nil t)
166   (autoload #'sgml-default-validate-command "psgml")
167   (autoload #'browse-url-of-buffer "browse-url" nil t)
168   (autoload #'customize-apropos "cus-edit" nil t)
169   (autoload #'customize-group "cus-edit" nil t)
170   (autoload #'regexp-opt "regexp-opt")
171   (autoload #'sqlite-open "ffi-sqlite")
172   (autoload #'sqlite-rows "ffi-sqlite")
173   (autoload #'sqlite-close "ffi-sqlite")
174   (autoload #'url-cookie-retrieve "url-cookie")
175   (autoload #'url-cookie-name "url-cookie")
176   (autoload #'url-cookie-value "url-cookie")
177   (defvar sxemacs-codename)
178   (defvar xemacs-codename)
179   (defvar url-cookie-secure-storage)
180   (defvar url-cookie-file))
181
182 (eval-and-compile
183   (require 'hm--html-configuration)
184   (require 'psgml-html)
185   (require 'font-lock)
186   (unless (fboundp #'when-fboundp)
187     (require 'bytedecl))
188   (autoload #'mm-url-insert "mm-url"))
189
190 (defgroup lj nil
191   "LiveJournal"
192   :prefix "lj-"
193   :link '(url-link "http://www.livejournal.com/")
194   :group 'hypermedia)
195
196 (defgroup lj-twitter nil
197   "LiveJournal meets Twitter"
198   :prefix "lj-twitter-"
199   :link '(url-link "http://www.livejournal.com/")
200   :link '(url-link "http://twitter.com/")
201   :group 'lj)
202
203 (defun lj-customise-faces ()
204   "Customise the lj.el faces."
205   (interactive)
206   (customize-apropos "^lj-" 'faces))
207
208 (defun lj-customise-group ()
209   "Customise lj.el user options."
210   (interactive)
211   (customize-group "lj"))
212
213 (defcustom lj-user-id (user-login-name)
214   "*Your LJ user ID."
215   :type 'string
216   :group 'lj)
217
218 (defcustom lj-cookie-flavour 'auto
219   "*The default cookie flavour \(browser\) to search for cookies."
220   :type '(choice
221           (symbol :tag "Automatic" :value auto)
222           (symbol :tag "Chrome" :value chrome)
223           (symbol :tag "Firefox" :value firefox)
224           (symbol :tag "Seamonkey" :value seamonkey)
225           (symbol :tag "Mozilla" :value mozilla)
226           (symbol :tag "Galeon" :value galeon)
227           (symbol :tag "Safari" :value safari)
228           (symbol :tag "Netscape" :value netscape)
229           (symbol :tag "Midori" :value midori)
230           (symbol :tag "Emacs-W3" :value w3))
231   :group 'lj)
232
233 (defcustom lj-default-security-level "public"
234   "*The default security level LJ posts will have."
235   :type '(choice
236           (string :tag "Public" :value "public")
237           (string :tag "Private" :value "private")
238           (string :tag "All Friends" :value "usemask")
239           (string :tag "Group..."))
240   :group 'lj)
241
242 (defcustom lj-directory (paths-construct-path
243                          (list (user-home-directory) ".lj"))
244   "*Directory for storing tags and archiving posts."
245   :type 'directory
246   :group 'lj)
247
248 (defcustom lj-tags-file (expand-file-name "ljtags" lj-directory)
249   "*File to store list of LJ tags."
250   :type 'file
251   :group 'lj)
252
253 (defcustom lj-groups-file (expand-file-name "ljgrps" lj-directory)
254   "*File to store list of LJ friends groups."
255   :type 'file
256   :group 'lj)
257
258 (defcustom lj-moods-file (expand-file-name "ljmoods" lj-directory)
259   "*File to store list of LJ \"moods\"."
260   :type 'file
261   :group 'lj)
262
263 (defcustom lj-pickws-file (expand-file-name "pickws" lj-directory)
264   "*File to store list of LJ user picture keywords."
265   :type 'file
266   :group 'lj)
267
268 (defcustom lj-userpic-directory
269   (file-name-as-directory
270    (expand-file-name "images" lj-directory))
271   "*Directory to store LJ userpic files."
272   :type 'directory
273   :group 'lj)
274
275 (defcustom lj-drafts-directory
276   (file-name-as-directory
277    (expand-file-name "drafts" lj-directory))
278   "*Directory where post drafts are stored."
279   :type 'directory
280   :group 'lj)
281
282 (defvar lj-tags nil
283   "A list of LJ tags.")
284
285 (defvar lj-groups nil
286   "A list of LJ friends groups.")
287
288 (defvar lj-moods nil
289   "LiveJournal \"moods\".")
290
291 (defvar lj-pickws nil
292   "A list of LJ userpic keywords.")
293
294 (defvar lj-default-pickw nil
295   "The default LJ userpic keyword.")
296
297 ;; See mpd.el in the same repo as lj.el
298 (defvar **mpd-var-Title* nil)
299 (defvar **mpd-var-Artist* nil)
300 (defun lj-music-mpd ()
301   "Return the current song title/artist from mpd."
302   (let ((song (if **mpd-var-Title*
303                   (format "%s --- [%s]"
304                           **mpd-var-Title*
305                           **mpd-var-Artist*)
306                 "The Sounds of Silence --- [Marcel Marceau]")))
307     song))
308
309 (defcustom lj-music (and (featurep 'mpd) #'lj-music-mpd)
310   "*A function to retrieve current song for LJ music header.
311 This function should return a formatted string, or nil."
312   :type 'function
313   :group 'lj)
314
315 (defcustom lj-archive-posts nil
316   "*Keep an archive copy of LJ posts when non-nil."
317   :type 'boolean
318   :group 'lj)
319
320 (defcustom lj-archive-directory
321   (file-name-as-directory
322    (expand-file-name "archive" lj-directory))
323   "*Directory where LJ posts are archived."
324   :type 'directory
325   :group 'lj)
326
327 (defcustom lj-bcc-address nil
328   "*Email address to send a copy of LJ posts to.
329 Set to nil to disable."
330   :type 'sexp
331   :group 'lj)
332
333 (defcustom lj-default-location nil
334   "*Default for the Location header."
335   :type 'sexp
336   :group 'lj)
337
338 (defcustom lj-before-preview-hook nil
339   "*Hook run before previewing a post."
340   :type 'hook
341   :group 'lj)
342
343 (defcustom lj-after-preview-hook nil
344   "*Hook run as the last thing from `lj-preview'."
345   :type 'hook
346   :group 'lj)
347
348 (defcustom lj-before-validate-hook nil
349   "*Hook run before validating a post."
350   :type 'hook
351   :group 'lj)
352
353 (defcustom lj-after-validate-hook nil
354   "*Hook run as the last thing from `lj-validate'."
355   :type 'hook
356   :group 'lj)
357
358 (defcustom lj-init-hook nil
359   "*Hook run before anything else is done when starting lj."
360   :type 'hook
361   :group 'lj)
362
363 (defcustom lj-before-post-hook nil
364   "*Hook run before posting."
365   :type 'hook
366   :group 'lj)
367
368 (defcustom lj-after-post-hook nil
369   "*Hook run after posting."
370   :type 'hook
371   :group 'lj)
372
373 (defcustom lj-cut-hook nil
374   "*Hooks run after inserting an LJ-CUT."
375   :type 'hook
376   :group 'lj)
377
378 (defcustom lj-poll-hook nil
379   "*Hooks run after inserting a LJ Poll."
380   :type 'hook
381   :group 'lj)
382
383 (defcustom lj-journal-hook nil
384   "*Hooks run after inserting a LJ Journal link."
385   :type 'hook
386   :group 'lj)
387
388 (defcustom lj-youtube-hook nil
389   "*Hooks run after inserting a youtube/google video."
390   :type 'hook
391   :group 'lj)
392
393 (defcustom lj-twitter-flag nil
394   "*Non-nil means to update your twitter status.
395
396 The subject header and a URL to the last blog entry is posted to
397 twitter as a status update if this is set."
398   :type 'boolean
399   :group 'lj-twitter)
400
401 (defcustom lj-twitter-username (user-login-name)
402   "*Your twitter username."
403   :type 'string
404   :group 'lj-twitter)
405
406 ;;; FIXME: Can we store this in something OTHER than plain text.  Do
407 ;;; we even need it at all with OAuth?
408 (defcustom lj-twitter-password "secret"
409   "*Your twitter password."
410   :type 'string
411   :group 'lj-twitter)
412
413 (defconst lj-clientversion
414   (concat (when (featurep 'sxemacs) "S")
415           "XEmacs-"
416           emacs-program-version
417           (format "/LJ: %.2f" lj-version))
418   "Client version string.")
419
420 (defconst lj-useragent
421   (concat "("
422           (when (featurep 'sxemacs) "S")
423           "XEmacs/"
424           emacs-program-version
425           (format " [%s]:LJ-%.2f; steve@sxemacs.org)"
426                   (if (featurep 'sxemacs)
427                       sxemacs-codename
428                     xemacs-codename)
429                   lj-version))
430   "Useragent string sent to livejournal.com.")
431
432 (defconst lj-validate-header
433   "<?xml version=\"1.0\" encoding=\"utf-8\"?>
434 <!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"
435  \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">
436
437 <html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">
438   <head>
439     <title>LJ Post Preview</title>
440
441     <style type=\"text/css\">
442       div.ljhead {
443         background: rgb(204,204,255);
444         padding: 0.5em;
445         border: ridge;
446         borderwidth: thin;
447         font-family: times new roman, verdana, helvetica, sans-serif;
448         font-size: 12pt;
449         font-weight: bold;
450       }
451       div.lj {
452         background: rgb(255,235,205);
453         padding: 0.5em;
454         border: none;
455       }
456       div.ljpoll {
457         color: red;
458         font-weight: bold;
459       }
460       div.ljcut {
461         background: white;
462         padding: 0.5em;
463         border: solid;
464         borderwidth: thin;
465       }
466     </style>
467
468   </head>
469   <body>
470
471 "
472   "Header used to construct HTML doc for previewing and validating LJ posts.")
473
474 (defconst lj-validate-footer
475   "
476   </body>
477 </html>
478
479 <!-- Leave this comment at the end of this file
480 Local variables:
481 sgml-validate-command:\"onsgmls -E0 -wall -wno-unused-param -wfully-tagged -wfully-declared -wtype-valid -wintegral -s %s %s\"
482 sgml-omittag:nil
483 sgml-shorttag:nil
484 sgml-namecase-general:nil
485 sgml-general-insert-case:lower
486 sgml-minimize-attributes:nil
487 sgml-always-quote-attributes:t
488 sgml-indent-step:2
489 sgml-indent-data:t
490 sgml-parent-document:nil
491 sgml-exposed-tags:nil
492 sgml-local-catalogs:nil
493 sgml-local-ecat-files:nil
494 End:
495 -->
496 "
497   "Footer used to construct HTML doc for previewing and validating LJ posts.")
498
499 (defconst lj-base-url
500   "http://www.livejournal.com/interface/flat"
501   "The base URL where LJ posts are submitted etc.")
502
503 (defvar lj-last-entry-btime nil
504   "The date/time of the last posted entry as a big integer.")
505
506 (defun lj-parse-time-string (string)
507   "Parse a time STRING of the format \"YYYY-MM-DD HH:MM:SS\".
508
509 The seconds field can be ommitted and in that case 0 is used.
510
511 Returns a list suitable for passing to `encode-time' or `encode-btime'."
512   (let ((regexp (concat "^\\([12][0-9]+\\)-\\([0-9]+\\)-\\([0-9]+\\)\\s-"
513                         "\\([0-9]+\\):\\([0-9]+\\)\\(:\\([0-9]+\\)\\)?$")))
514     (if (string-match regexp string)
515         (let ((year (string-to-int (substring string
516                                               (match-beginning 1)
517                                               (match-end 1))))
518               (month (string-to-int (substring string
519                                                (match-beginning 2)
520                                                (match-end 2))))
521               (day (string-to-int (substring string
522                                              (match-beginning 3)
523                                              (match-end 3))))
524               (hour (string-to-int (substring string
525                                               (match-beginning 4)
526                                               (match-end 4))))
527               (min (string-to-int (substring string
528                                              (match-beginning 5)
529                                              (match-end 5))))
530               (sec (if (eq (length string) 19)
531                        (string-to-int (substring string (match-beginning 7)
532                                                  (match-end 7)))
533                      0)))
534           (unless (and (>= year 1970)
535                        (<= year 2099))
536             (error 'invalid-argument year))
537           (unless (and (>= month 1)
538                        (<= month 12))
539             (error 'invalid-argument month))
540           (unless (and (>= day 1)
541                        (<= day 31))
542             (error 'invalid-argument day))
543           (unless (and (>= hour 0)
544                        (<= hour 23))
545             (error 'invalid-argument hour))
546           (unless (and (>= min 0)
547                        (<= min 59))
548             (error 'invalid-argument min))
549           (unless (and (>= sec 0)
550                        (<= sec 59))
551             (error 'invalid-argument sec))
552           (list sec min hour day month year))
553       (error 'invalid-argument string))))
554
555 ;; Probably should set up a proper prefix
556 (defvar lj-mode-map
557   (let ((map (make-sparse-keymap)))
558     (set-keymap-name map 'lj-mode-map)
559     (define-key map [(control ?c) (control return)] #'lj-post)
560     (define-key map [(control ?c) ?F] #'lj-customise-faces)
561     (define-key map [(control ?c) ?G] #'lj-customise-group)
562     (define-key map [(control ?c) ?P] #'lj-insert-poll)
563     (define-key map [(control ?c) ?c] #'lj-cut-region)
564     (define-key map [(control ?c) ?j] #'lj-insert-journal)
565     (define-key map [(control ?c) ?p] #'lj-preview)
566     (define-key map [(control ?c) ?w] #'lj-writers-block)
567     (define-key map [(control ?c) ?y] #'lj-insert-youtube)
568     (define-key map [(control ?c) (control ?f) ?M] #'lj-goto-mood)
569     (define-key map [(control ?c) (control ?f) ?S] #'lj-goto-security)
570     (define-key map [(control ?c) (control ?f) ?b] #'lj-goto-bcc)
571     (define-key map [(control ?c) (control ?f) ?c] #'lj-goto-community)
572     (define-key map [(control ?c) (control ?f) ?f] #'lj-goto-fcc)
573     (define-key map [(control ?c) (control ?f) ?l] #'lj-goto-location)
574     (define-key map [(control ?c) (control ?f) ?m] #'lj-goto-music)
575     (define-key map [(control ?c) (control ?f) ?s] #'lj-goto-subject)
576     (define-key map [(control ?c) (control ?f) ?t] #'lj-goto-tags)
577     (define-key map [(control ?c) (control ?f) ?u] #'lj-goto-userpic)
578     (define-key map [(control ?c) (control ?b)] #'lj-goto-body)
579     (define-key map [(control meta ?v)] #'lj-validate)
580     (define-key map [tab] #'lj-sgml-indent-tab-or-complete)
581     (define-key map [iso-left-tab] #'lj-complete-header-backwards)
582     map))
583
584 (defvar lj-header-separator "--text follows this line--"
585   "Text to denote the end of the headers and beginning of the message.")
586
587 ;; Faces (defaults are probably crap for a light background)
588 (defun lj-face-p (face)
589   "Call facep on FACE."
590   (facep (find-face face)))
591
592 (make-face 'lj-header-name "Face used for LJ headers.")
593 (set-face-parent 'lj-header-name (or (and (lj-face-p 'message-header-name)
594                                           'message-header-name)
595                                      (and (lj-face-p 'message-header-name-face)
596                                           'message-header-name-face)
597                                      'bold))
598
599 (make-face 'lj-header-subject "Face used for LJ Subject header content.")
600 (set-face-parent 'lj-header-subject
601                  (or (and (lj-face-p 'message-header-subject)
602                           'message-header-subject)
603                      (and (lj-face-p 'message-header-subject-face)
604                           'message-header-subject-face)
605                      'default))
606
607 (make-face 'lj-header-fcc "Face used for LJ FCC header content.")
608 (set-face-parent 'lj-header-fcc 'font-lock-comment-face)
609 (make-face 'lj-header-bcc "Face used for LJ BCC header content.")
610 (set-face-parent 'lj-header-bcc (or (and (lj-face-p 'message-header-cc)
611                                          'message-header-cc)
612                                     (and (lj-face-p 'message-header-cc-face)
613                                          'message-header-cc-face)
614                                     'lj-header-fcc))
615
616 (make-face 'lj-header-security "Face used for LJ Security header content.")
617 (set-face-parent 'lj-header-security 'font-lock-warning-face)
618
619 (make-face 'lj-header-music "Face used for LJ Music header content.")
620 (set-face-parent 'lj-header-music
621                  (or (and (lj-face-p 'message-header-xheader)
622                           'message-header-xheader)
623                      (and (lj-face-p 'message-header-xheader-face)
624                           'message-header-xheader-face)
625                      'font-lock-builtin-face))
626
627 (make-face 'lj-header-mood "Face used for LJ Mood header content.")
628 (set-face-parent 'lj-header-mood
629                  (or (and (lj-face-p 'message-header-other)
630                           'message-header-other)
631                      (and (lj-face-p 'message-header-other-face)
632                           'message-header-other-face)
633                      'font-lock-function-name-face))
634
635 (make-face 'lj-header-userpic "Face used for LJ Userpic header content.")
636 (set-face-parent 'lj-header-userpic
637                  (or (and (lj-face-p 'message-header-other)
638                           'message-header-other)
639                      (and (lj-face-p 'message-header-other-face)
640                           'message-header-other-face)
641                      'font-lock-function-name-face))
642
643 (make-face 'lj-header-tags "Face used for LJ Tags header content.")
644 (set-face-parent 'lj-header-tags
645                  (or (and (lj-face-p 'message-header-newsgroups)
646                           'message-header-newsgroups)
647                      (and (lj-face-p 'message-header-newsgroups-face)
648                           'message-header-newsgroups-face)
649                      'font-lock-keyword-face))
650
651 (make-face 'lj-header-community "Face used for LJ Community header content.")
652 (set-face-parent 'lj-header-community 'lj-header-userpic)
653
654 (make-face 'lj-header-location "Face used for LJ Location header content.")
655 (set-face-parent 'lj-header-location 'lj-header-userpic)
656
657 (make-face 'lj-separator "Face used for the LJ header separator.")
658 (copy-face 'bold 'lj-separator)
659 (set-face-foreground 'lj-separator "red")
660
661 ;; compatibility hoohar
662 (unless (featurep 'sxemacs)
663   (fset #'defregexp #'defvar))
664
665 (defun lj-utf-emacs-p ()
666   "Return non-nil if this S?XEmacs has utf-8 coding-system."
667   (and (featurep 'mule)
668        (declare-fboundp (find-coding-system 'utf-8))))
669
670 (defregexp lj-header-regexp
671   (let ((headers '("Subject" "FCC" "BCC" "Security" "Community"
672                    "Location" "Mood" "Music" "Userpic" "Tags")))
673     (concat (regexp-opt headers t) ":"))
674   "Regular expression matching LJ headers.")
675
676 (defvar lj-font-lock-keywords
677   (append
678    `((,lj-header-regexp 0 lj-header-name)
679      ("^Subject: \\(.*$\\)" 1 lj-header-subject)
680      ("^FCC: \\(.*$\\)" 1 lj-header-fcc)
681      ("^BCC: \\(.*$\\)" 1 lj-header-bcc)
682      ("^Security: \\(.*$\\)" 1 lj-header-security)
683      ("^Community: \\(.*$\\)" 1 lj-header-community)
684      ("^Music: \\(.*$\\)" 1 lj-header-music)
685      ("^Mood: \\(.*$\\)" 1 lj-header-mood)
686      ("^Location: \\(.*$\\)" 1 lj-header-location)
687      ("^Userpic: \\(.*$\\)" 1 lj-header-userpic)
688      ("^Tags: \\(.*$\\)" 1 lj-header-tags)
689      (,(regexp-quote lj-header-separator) 0 lj-separator))
690    hm--html-font-lock-keywords
691    html-font-lock-keywords)
692   "Font lock keywords for `lj-mode'.")
693
694 ;; kill/yank'd from jwz-lj.el
695 (defconst lj-entity-table
696   '(("iexcl"  . ?\¡) ("cent"   . ?\¢) ("pound"  . ?\£) ("euro"   . ?\~)
697     ("curren" . ?\¤) ("yen"    . ?\¥) ("brvbar" . ?\¦) ("sect"   . ?\§)
698     ("uml"    . ?\¨) ("copy"   . ?\©) ("ordf"   . ?\ª) ("laquo"  . ?\«)
699     ("not"    . ?\¬) ("shy"    . ?\­) ("reg"    . ?\®) ("macr"   . ?\¯)
700     ("deg"    . ?\°) ("plusmn" . ?\±) ("sup2"   . ?\²) ("sup3"   . ?\³)
701     ("acute"  . ?\´) ("micro"  . ?\µ) ("para"   . ?\¶) ("middot" . ?\·)
702     ("cedil"  . ?\¸) ("sup1"   . ?\¹) ("ordm"   . ?\º) ("raquo"  . ?\»)
703     ("frac14" . ?\¼) ("frac12" . ?\½) ("frac34" . ?\¾) ("iquest" . ?\¿)
704     ("Agrave" . ?\À) ("Aacute" . ?\Á) ("Acirc"  . ?\Â) ("Atilde" . ?\Ã)
705     ("Auml"   . ?\Ä) ("Aring"  . ?\Å) ("AElig"  . ?\Æ) ("Ccedil" . ?\Ç)
706     ("Egrave" . ?\È) ("Eacute" . ?\É) ("Ecirc"  . ?\Ê) ("Euml"   . ?\Ë)
707     ("Igrave" . ?\Ì) ("Iacute" . ?\Í) ("Icirc"  . ?\Î) ("Iuml"   . ?\Ï)
708     ("ETH"    . ?\Ð) ("Ntilde" . ?\Ñ) ("Ograve" . ?\Ò) ("Oacute" . ?\Ó)
709     ("Ocirc"  . ?\Ô) ("Otilde" . ?\Õ) ("Ouml"   . ?\Ö) ("times"  . ?\×)
710     ("Oslash" . ?\Ø) ("Ugrave" . ?\Ù) ("Uacute" . ?\Ú) ("Ucirc"  . ?\Û)
711     ("Uuml"   . ?\Ü) ("Yacute" . ?\Ý) ("THORN"  . ?\Þ) ("szlig"  . ?\ß)
712     ("agrave" . ?\à) ("aacute" . ?\á) ("acirc"  . ?\â) ("atilde" . ?\ã)
713     ("auml"   . ?\ä) ("aring"  . ?\å) ("aelig"  . ?\æ) ("ccedil" . ?\ç)
714     ("egrave" . ?\è) ("eacute" . ?\é) ("ecirc"  . ?\ê) ("euml"   . ?\ë)
715     ("igrave" . ?\ì) ("iacute" . ?\í) ("icirc"  . ?\î) ("iuml"   . ?\ï)
716     ("eth"    . ?\ð) ("ntilde" . ?\ñ) ("ograve" . ?\ò) ("oacute" . ?\ó)
717     ("ocirc"  . ?\ô) ("otilde" . ?\õ) ("ouml"   . ?\ö) ("divide" . ?\÷)
718     ("oslash" . ?\ø) ("ugrave" . ?\ù) ("uacute" . ?\ú) ("ucirc"  . ?\û)
719     ("uuml"   . ?\ü) ("yacute" . ?\ý) ("thorn"  . ?\þ) ("yuml"   . ?\ÿ)
720     ("plusmn" . ?\±))
721   "HTML entities to Latin1 characters.")
722
723 ;; adapted from jwz-lj.el
724 (defun lj-entify-region (beg end)
725   "Convert non-ASCII chars in the region BEG - END to HTML entities."
726   (let ((regex (if (featurep 'sxemacs)
727                    "[^[:ascii:]]"
728                  ;; ho-hum, life would be simpler if XEmacs enabled
729                  ;; char classes
730                  (concat "[" (mapconcat
731                               #'(lambda (c)
732                                   (make-string 1 (cdr c)))
733                               lj-entity-table nil)
734                          "]")))
735         (case-fold-search nil))
736     (save-excursion
737       (goto-char beg)
738       (setq end (copy-marker end))
739       (while (re-search-forward regex end t)
740         (let* ((char (preceding-char))
741                (entity (or (car (rassq char lj-entity-table))
742                            (error "No entity %c" char))))
743           (delete-char -1)
744           (insert-before-markers "&" entity ";")))))
745   (and-fboundp #'charsets-in-region
746     (delq 'ascii (charsets-in-region beg end))
747     (error "Non-ASCII characters exist in this buffer")))
748
749 (defconst lj-unreserved-chars
750   '(?a ?b ?c ?d ?e ?f ?g ?h ?i ?j ?k ?l ?m
751        ?n ?o ?p ?q ?r ?s ?t ?u ?v ?w ?x ?y ?z
752        ?A ?B ?C ?D ?E ?F ?G ?H ?I ?J ?K ?L ?M
753        ?N ?O ?P ?Q ?R ?S ?T ?U ?V ?W ?X ?Y ?Z
754        ?0 ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9
755        ?- ?_ ?. ?! ?~ ?* ?' ?\( ?\))
756   "A list of characters that are _NOT_ reserved in the URL spec.
757 This is taken from RFC 2396.")
758
759 (defun lj-hexify-string (str &optional http-entify)
760   "Escape characters STR so STR can be used in a URL.
761
762 With non-nil HTTP-ENTIFY, convert non-ASCII characters to HTTP
763 entities."
764   (with-temp-buffer
765     (insert str)
766     (and http-entify
767          (lj-entify-region (point-min) (point-max)))
768     (mapconcat
769      #'(lambda (char)
770          (if (not (memq char lj-unreserved-chars))
771              (if (< char 16)
772                  (format "%%0%X" char)
773                (if (> char 255)
774                    (error "Hexifying multibyte character %s" str))
775                (format "%%%X" char))
776            (char-to-string char)))
777      (buffer-string) "")))
778
779 (when-fboundp #'ffi-defun
780   (ignore-errors
781     (require 'ffi-sqlite)
782     (require 'ffi-curl)))
783
784 ;; adapted from jwz-lj.el
785 (defun lj-extract-sql-cookies (file chromep)
786   "Extract LJ cookie data from SQL cookies FILE.
787
788 Non-nil CHROMEP forces a Google Chrome compatible sql query."
789   (let ((sql (if chromep
790                  ;; chrome sql cookies
791                  (concat "SELECT name,value FROM cookies "
792                          "WHERE host_key=\".www.livejournal.com\" "
793                          "OR host_key=\".livejournal.com\"")
794                ;; mozilla based sql cookies
795                (concat "SELECT name,value FROM moz_cookies "
796                        "WHERE host=\".www.livejournal.com\" "
797                        "OR host=\".livejournal.com\""))))
798     (if (featurep 'ffi-sqlite)
799         ;; Try SXEmacs' sexy ffi-sqlite if it's available
800         (let* ((db (sqlite-open file))
801                (rows (sqlite-rows db sql)))
802           (sqlite-close db)
803           (when (listp rows)
804             (concat "Cookie: "
805                     (mapconcat
806                      #'(lambda (c)
807                          (concat (car c) "=" (cadr c)))
808                      rows "; ")
809                     "\r\n")))
810       ;; The old fashioned way
811       (unless (executable-find "sqlite3")
812         (error "Can't find sqlite3"))
813       (let* ((sql (shell-command-to-string
814                    (concat "sqlite3 " file "'" sql ";'")))
815              (slist (butlast
816                      (split-string-by-char
817                       (replace-regexp-in-string "\n" "|" sql) ?|)))
818              cookies)
819         (while slist
820           (push (cons (car slist) (cadr slist)) cookies)
821           (setq slist (cddr slist)))
822         (when cookies
823           (concat "Cookie: "
824                   (mapconcat
825                    #'(lambda (c)
826                        (concat (car c) "=" (cdr c)))
827                    (reverse cookies) "; ")
828                   "\r\n"))))))
829
830 ;;; FIXME: redo this with #'xml-parse-file.  Just as soon as I can get
831 ;; me grubby little hands on one of these xml cookie files.
832 ;; kill/yank'd from jwz-lj.el
833 ;;; FIXME:  this will now be broken!
834 (defun lj-extract-xml-cookies (file)
835   "Extract LJ data from XML cookies FILE."
836   (save-excursion
837     (save-restriction
838       (narrow-to-region (point) (point))
839       (insert-file-contents file nil nil nil t)
840       (goto-char (point-min))
841       (search-forward "<dict>")
842       (let ((result "")
843             (start (point))
844             end
845             domain path name value)
846         (while (search-forward "</dict>" nil t)
847           (setq end (point))
848           (goto-char start)
849           (cond
850            ((search-forward "livejournal.com" end t)  ; bail fast
851
852             (goto-char start)
853             (re-search-forward (concat "<key>Domain</key>[ \t\n\r]*"
854                                        "<string>\\([^<>]+\\)</string>")
855                                end)
856             (setq domain (match-string 1))
857             (goto-char start)
858             (re-search-forward (concat "<key>Path</key>[ \t\n\r]*"
859                                        "<string>\\([^<>]+\\)</string>")
860                                end)
861             (setq path (match-string 1))
862             (goto-char start)
863             (re-search-forward (concat "<key>Name</key>[ \t\n\r]*"
864                                        "<string>\\([^<>]+\\)</string>")
865                                end)
866             (setq name (match-string 1))
867             (goto-char start)
868             (re-search-forward (concat "<key>Value</key>[ \t\n\r]*"
869                                        "<string>\\([^<>]+\\)</string>")
870                                end)
871             (setq value (match-string 1))
872             (if (string-match "\\blivejournal\\.com$" domain)
873                 (setq result
874                       (concat domain "\tTRUE\t" path "\tFALSE\t0\t"
875                               name "\t" value
876                               "\n" result)))))
877           (goto-char end)
878           (setq start end))
879         (delete-region (point-min) (point-max))
880         (insert result))))
881   nil)
882
883 (defun lj-extract-text-cookies (file)
884   "Extract LJ data from text based cookies FILE."
885   (let ((host-match "\\.livejournal\\.com$")
886         cookies)
887     (with-temp-buffer
888       (insert-file-contents-literally file)
889       (goto-char (point-min))
890       (while (not (eobp))
891         (when (looking-at (concat "^\\([^\t\r\n]+\\)\t"  ; 1 host
892                                   "\\([^\t\r\n]+\\)\t"  ; 2 bool
893                                   "\\([^\t\r\n]+\\)\t"  ; 3 path
894                                   "\\([^\t\r\n]+\\)\t"  ; 4 bool
895                                   "\\([^\t\r\n]+\\)\t"  ; 5 time_t
896                                   "\\([^\t\r\n]+\\)\t"  ; 6 key
897                                   "\\([^\t\r\n]+\\)$")) ; 7 val
898           (let ((host (match-string 1))
899                 (key (match-string 6))
900                 (val (match-string 7)))
901             (when (and (string-match host-match host)
902                        (not (assoc key cookies)))
903               (setq cookies (cons (cons key val) cookies))))
904           (forward-line 1))))
905     (when cookies
906       (concat "Cookie: "
907               (mapconcat
908                #'(lambda (c)
909                    (concat (car c) "=" (cdr c)))
910                (nreverse cookies) "; ")
911               "\r\n"))))
912
913 (defun lj-extract-w3-cookies ()
914   "Extract LJ cookie data from Emacs-W3 cookies."
915   (let* ((secure (and url-cookie-secure-storage t))
916          (w3cookies (remove-duplicates
917                      (append (url-cookie-retrieve
918                               "www.livejournal.com" "/" secure)
919                              (url-cookie-retrieve
920                               ".livejournal.com" "/" secure))
921                      :test #'equal)))
922     (when w3cookies
923       (replace-regexp-in-string
924        "; \r\n" "\r\n"
925        (concat "Cookie: "
926                (mapconcat
927                 #'(lambda (c)
928                     (unless (string-equal (url-cookie-name c) "HttpOnly")
929                       (concat (url-cookie-name c) "=" (url-cookie-value c))))
930                 (nreverse w3cookies) "; ")
931                "\r\n")))))
932
933 ;; adapted from jwz-lj.el, but rewritten from scratch
934 (defun lj-get-cookies (flavour)
935   "Return a string of LJ cookie data suitable for HTTP POST'ing.
936
937 Argument FLAVOUR specifies which browser's cookies to check.  If it is
938 the symbol `auto' \(the default\) all browsers will be searched in the
939 following order...
940
941 Google Chrome, Firefox, SeaMonkey, Mozilla, Galeon, Safari, Nescape,
942 Midori, and Emacs-W3 for cookie data."
943   (catch 'found
944     (let (cookies)
945
946       ;; Google Chrome
947       (when (or (eq flavour 'chrome)
948                 (eq flavour 'auto))
949         (let ((dir (paths-construct-path
950                     '(".config" "google-chrome" "Default")
951                     (user-home-directory))))
952           (and (file-exists-p (expand-file-name "Cookies" dir))
953                (setq cookies (lj-extract-sql-cookies
954                               (expand-file-name "Cookies" dir) t))))
955         (if cookies
956             (throw 'found cookies)
957           (when (not (eq flavour 'auto))
958             (lj-get-cookies 'auto))))
959
960       ;; Firefox 1-3 (works for sqlite cookies, plain text cookies are
961       ;; untested)
962       (when (or (eq flavour 'firefox)
963                 (eq flavour 'auto))
964         (let ((d1 (paths-construct-path
965                    '("Library" "Application Support" "Firefox" "Profiles")
966                    (user-home-directory)))
967               (d2 (paths-construct-path
968                    '(".mozilla" "firefox") (user-home-directory)))
969               dir)
970           (if (file-directory-p d1)
971               (setq dir (car (directory-files d1 t "\\.default$" nil 'dirs)))
972             (setq dir (car (directory-files d2 t "\\.default$" nil 'dirs))))
973           (or (and (file-exists-p (expand-file-name "cookies.txt" dir))
974                    (setq cookies (lj-extract-text-cookies
975                                   (expand-file-name "cookies.txt" dir))))
976               (and (file-exists-p (expand-file-name "cookies.sqlite" dir))
977                    (setq cookies (lj-extract-sql-cookies
978                                   (expand-file-name "cookies.sqlite" dir) nil)))))
979         (if cookies
980             (throw 'found cookies)
981           (when (not (eq flavour 'auto))
982             (lj-get-cookies 'auto))))
983
984       ;; SeaMonkey (untested)
985       (when (or (eq flavour 'seamonkey)
986                 (eq flavour 'auto))
987         (when (file-directory-p (expand-file-name ".mozilla/seamonkey"
988                                                   (user-home-directory)))
989           (let ((dir (car (directory-files "~/.mozilla/seamonkey"
990                                            t "\\.default$" nil 'dirs))))
991             (or (and (file-exists-p (expand-file-name "cookies.txt" dir))
992                      (setq cookies (lj-extract-text-cookies
993                                     (expand-file-name "cookies.txt" dir))))
994                 (and (file-exists-p (expand-file-name "cookies.sqlite" dir))
995                      (setq cookies (lj-extract-sql-cookies
996                                     (expand-file-name "cookies.sqlite" dir) nil))))))
997         (if cookies
998             (throw 'found cookies)
999           (when (not (eq flavour 'auto))
1000             (lj-get-cookies 'auto))))
1001
1002       ;; Mozilla (untested)
1003       (when (or (eq flavour 'mozilla)
1004                 (eq flavour 'auto))
1005         (when (file-directory-p (expand-file-name ".mozilla"
1006                                                   (user-home-directory)))
1007           (let ((d1 (paths-construct-path '(".mozilla" "default")
1008                                           (user-home-directory)))
1009                 (d2 (paths-construct-path `(".mozilla" ,(user-login-name))
1010                                           (user-home-directory)))
1011                 dir)
1012             (if (file-directory-p d1)
1013                 (setq dir (car (directory-files d1 t "\\.slt$" nil 'dirs)))
1014               (setq dir (car (directory-files d2 t "\\.slt$" nil 'dirs))))
1015             (and (file-exists-p (expand-file-name "cookies.txt" dir))
1016                  (setq cookies (lj-extract-text-cookies
1017                                 (expand-file-name "cookies.txt" dir))))))
1018         (if cookies
1019             (throw 'found cookies)
1020           (when (not (eq flavour 'auto))
1021             (lj-get-cookies 'auto))))
1022
1023       ;; Galeon (untested)
1024       (when (or (eq flavour 'galeon)
1025                 (eq flavour 'auto))
1026         (let ((dir (paths-construct-path
1027                     '(".galeon" "mozilla" "galeon") (user-home-directory))))
1028           (and (file-exists-p (expand-file-name "cookies.txt" dir))
1029                (setq cookies (lj-extract-text-cookies
1030                               (expand-file-name "cookies.txt" dir)))))
1031         (if cookies
1032             (throw 'found cookies)
1033           (when (not (eq flavour 'auto))
1034             (lj-get-cookies 'auto))))
1035
1036       ;; Safari (untested)
1037       (when (or (eq flavour 'safari)
1038                 (eq flavour 'auto))
1039         (let ((dir (paths-construct-path '("Library" "Cookies")
1040                                          (user-home-directory))))
1041           (and (file-exists-p (expand-file-name "Cookies.plist" dir))
1042                (setq cookies (lj-extract-xml-cookies
1043                               (expand-file-name "Cookies.plist" dir)))))
1044         (if cookies
1045             (throw 'found cookies)
1046           (when (not (eq flavour 'auto))
1047             (lj-get-cookies 'auto))))
1048
1049       ;; Netscape (untested)
1050       (when (or (eq flavour 'netscape)
1051                 (eq flavour 'auto))
1052         (let ((dir (paths-construct-path '(".netscape")
1053                                          (user-home-directory))))
1054           (and (file-exists-p (expand-file-name "cookies" dir))
1055                (setq cookies (lj-extract-text-cookies
1056                               (expand-file-name "cookies" dir)))))
1057         (if cookies
1058             (throw 'found cookies)
1059           (when (not (eq flavour 'auto))
1060             (lj-get-cookies 'auto))))
1061
1062       ;; Midori (works, but Midori doesn't take too much care about
1063       ;; invalid or broken cookies so YMMV here)
1064       (when (or (eq flavour 'midori)
1065                 (eq flavour 'auto))
1066         (let ((dir (paths-construct-path '(".config" "midori")
1067                                          (user-home-directory))))
1068           (and (file-exists-p (expand-file-name "cookies.txt" dir))
1069                (setq cookies (lj-extract-text-cookies
1070                               (expand-file-name "cookies.txt" dir)))))
1071         (if cookies
1072             (throw 'found cookies)
1073           (when (not (eq flavour 'auto))
1074             (lj-get-cookies 'auto))))
1075
1076       ;; Emacs-W3 URL (works with W3 in XE packages)
1077       (when (or (eq flavour 'w3)
1078                 (eq flavour 'auto))
1079         (and (file-exists-p url-cookie-file)
1080              (setq cookies (lj-extract-w3-cookies)))
1081         (if cookies
1082             (throw 'found cookies)
1083           (when (not (eq flavour 'auto))
1084             (lj-get-cookies 'auto))))
1085
1086       ;; Gah! no cookes anywhere!
1087       (error 'search-failed "LJ Cookie data"))))
1088
1089 (defvar lj-cookies (lj-get-cookies lj-cookie-flavour)
1090   "Alist of cookie data to send to LJ.")
1091
1092 ;; adapted from jwz-lj.el
1093 (defun lj-http-post (url cookies parser)
1094   "Sends a HTTP POST to URL with COOKIES.
1095
1096 Argument PARSER is a function to handle parsing the output received."
1097   (unless (string-match "\\`https?://\\([^/]+\\)\\([^?&]+\\)\\?\\(.*\\)\\'" url)
1098     (error "Unparsable url: %s" url))
1099   (let* ((host (match-string 1 url))
1100          (port 80)
1101          (path (match-string 2 url))
1102          (args (match-string 3 url))
1103          (post-cmd
1104           (concat "POST " path " HTTP/1.0\r\n"
1105                   "Content-Type: application/x-www-form-urlencoded\r\n"
1106                   "Content-Length: " (int-to-string (length args)) "\r\n"
1107                   "Host: " host "\r\n"
1108                   "X-LJ-Auth: cookie\r\n"
1109                   cookies
1110                   "\r\n"
1111                   args))
1112          (buf (generate-new-buffer " *LJ-process*"))
1113          proc)
1114     (setq proc (open-network-stream "LiveJournal" buf host port))
1115     (when (lj-utf-emacs-p)
1116       (set-process-coding-system proc 'utf-8 'utf-8))
1117     (process-send-string proc post-cmd)
1118     (message "HTTP POST sent to %s" host)
1119     (while (eq (process-status proc) 'open)
1120       (unless (accept-process-output proc 60)
1121         (delete-process proc)
1122         (error "[LJ] Server error: timeout")))
1123     (funcall parser buf)))
1124
1125 (defun lj-proc-success ()
1126   "Return t when LJ processes are successful.
1127
1128 By \"successful\" we mean that livejournal.com didn't complain about
1129 anything we sent it."
1130   (let ((regex "^success\n\\(.*$\\)")
1131         result)
1132     (save-excursion
1133       (goto-char (point-min))
1134       (if (re-search-forward regex nil t)
1135           (setq result (match-string 1))
1136         (error "[LJ] Server error: try again later"))
1137       (cond ((string= result "OK")
1138              t)
1139             ((string= result "FAIL")
1140              (let ((ereg "^errmsg\n\\(.*$\\)"))
1141                (save-excursion
1142                  (goto-char (point-min))
1143                  (re-search-forward ereg)
1144                  (if (string-match "Incorrect time value" (match-string 1))
1145                      (lj-post 'out-of-order)
1146                    (error "[LJ]: %s" (match-string 1))))))
1147             (t
1148              (error "[LJ]: Unknown error"))))))
1149
1150 (defun lj-friends-proc-parser (buf)
1151   "Processes the output from `lj-get-friends-groups'.
1152 Argument BUF is the process buffer used."
1153   (let ((regexp "^frgrp_\\([0-9]+\\)_name\n\\(.*$\\)")
1154         groups)
1155     (with-current-buffer buf
1156       (when (lj-proc-success)
1157         (goto-char (point-min))
1158         (while (re-search-forward regexp nil t)
1159           (setq groups
1160                 (cons (cons (match-string 2) (string-to-int (match-string 1)))
1161                       groups)))
1162         (kill-buffer nil))
1163       (when groups
1164         (or (file-directory-p lj-directory)
1165             (make-directory-path lj-directory))
1166         (with-current-buffer (find-file-noselect lj-groups-file)
1167           (erase-buffer)
1168           (insert ";;; Automatically generated DO NOT EDIT -*- Emacs-Lisp -*-\n"
1169                   (format "(setq lj-groups (quote %S))" groups))
1170           (save-buffer)
1171           (eval-current-buffer nil)
1172           (kill-buffer nil))))))
1173
1174 (defun lj-get-friends-groups ()
1175   "Retrieve an alist of groups/groupids from Livejournal."
1176   (let ((cookies (or lj-cookies
1177                      (error "No LJ cookies found")))
1178         (url (concat lj-base-url
1179                      "?mode=getfriendgroups"
1180                      "&user=" lj-user-id
1181                      "&auth_method=cookie"
1182                      (format "&ver=%d" (if (lj-utf-emacs-p) 1 0)))))
1183     (lj-http-post url cookies #'lj-friends-proc-parser)))
1184
1185 (defun lj-tags-proc-parser (buf)
1186   "Process the output from `lj-get-tags'.
1187 Argument BUF is the process buffer used."
1188   (let ((regexp "tag_[0-9]+_name\n\\(.*$\\)")
1189         tags)
1190     (with-current-buffer buf
1191       (when (lj-proc-success)
1192         (goto-char (point-min))
1193         (while (re-search-forward regexp nil t)
1194           (push (match-string 1) tags))
1195         (kill-buffer nil))
1196       (when tags
1197         (or (file-directory-p lj-directory)
1198             (make-directory-path lj-directory))
1199         (with-current-buffer (find-file-noselect lj-tags-file)
1200           (erase-buffer)
1201           (insert ";;; Automatically generated DO NOT EDIT -*- Emacs-Lisp -*-\n"
1202                   (format "(setq lj-tags (quote %S))" tags))
1203           (save-buffer)
1204           (eval-current-buffer nil)
1205           (kill-buffer nil))))))
1206
1207 (defun lj-get-tags ()
1208   "Retrieve a list of defined tags from Livejournal."
1209   (let ((cookies (or lj-cookies
1210                      (error "No LJ cookies found")))
1211         (url (concat lj-base-url
1212                      "?mode=getusertags"
1213                      "&user=" lj-user-id
1214                      "&auth_method=cookie"
1215                      (format "&ver=%d" (if (lj-utf-emacs-p) 1 0)))))
1216     (lj-http-post url cookies #'lj-tags-proc-parser)))
1217
1218 (defun lj-get-userpic-noffi (url file)
1219   "Download userpic from URL to FILE."
1220   (if (executable-find "curl")
1221       (shell-command (concat "curl " url " -so " file) nil)
1222     (error 'unimplemented "non-FFI leeching")))
1223
1224 (defun lj-get-userpics ()
1225   "Leech your userpics from livejournal.com."
1226   (unless (file-directory-p lj-userpic-directory)
1227     (make-directory-path lj-userpic-directory))
1228   (let ((pics (mapcar #'car lj-pickws)))
1229     (mapcar
1230      #'(lambda (p)
1231          (unless (file-exists-p
1232                   (expand-file-name p lj-userpic-directory))
1233            (let ((file (expand-file-name p lj-userpic-directory))
1234                  (url (cdr (assoc p lj-pickws))))
1235              (if (featurep '(and sxemacs ffi-curl))
1236                  (declare-fboundp (curl:download url file))
1237                (lj-get-userpic-noffi url file)))))
1238      pics)))
1239
1240 (defun lj-pickws-proc-parser (buf)
1241   "Process the output from `lj-get-pickws'.
1242 Argument BUF is the process buffer used."
1243   (let ((msg "^message\n\\(.*$\\)")
1244         defaultk defaultu keywords)
1245     (with-current-buffer buf
1246       (when (lj-proc-success)
1247         (goto-char (point-min))
1248         (save-excursion
1249           (re-search-forward "defaultpicurl\n\\(.*$\\)")
1250           (setq defaultu (match-string 1))
1251           (let ((defidx
1252                   (and
1253                    (re-search-forward (concat "^pickwurl_\\([0-9]+\\)\n"
1254                                               defaultu) nil t)
1255                    (match-string 1))))
1256             (goto-char (point-min))
1257             (re-search-forward (concat "^pickw_" defidx "\n\\(.*$\\)") nil t)
1258             (setq defaultk (match-string 1))
1259             (setq keywords (cons (cons defaultk defaultu) keywords))))
1260         (save-excursion
1261           (while (re-search-forward "^pickw_\\([0-9]+\\)\n\\(.*$\\)" nil t)
1262             (let* ((key (match-string 2))
1263                    (url (save-excursion
1264                           (goto-char (point-min))
1265                           (and (re-search-forward
1266                                 (concat "pickwurl_"
1267                                         (match-string 1) "\n\\(.*$\\)") nil t)
1268                                (match-string 1)))))
1269               (unless (string= key defaultk)
1270                 (setq keywords (cons (cons key url) keywords))))))
1271         (when (re-search-forward msg nil t)
1272           (pop-to-buffer (get-buffer-create "*LJ Message*"))
1273           (insert "Important Message From LiveJournal:\n"
1274                   "==================================\n\n")
1275           (insert (match-string 1)))
1276         (kill-buffer buf))
1277       (when keywords
1278         (or (file-directory-p lj-directory)
1279             (make-directory-path lj-directory))
1280         (with-current-buffer (find-file-noselect lj-pickws-file)
1281           (erase-buffer)
1282           (insert ";;; Automatically generated DO NOT EDIT -*- Emacs-Lisp -*-\n"
1283                   (format "(setq lj-pickws (quote %S))" keywords))
1284           (insert (format "\n(setq lj-default-pickw %S)" defaultk))
1285           (save-buffer)
1286           (eval-current-buffer nil)
1287           (kill-buffer nil))))
1288     (lj-get-userpics)))
1289
1290 (defun lj-get-pickws ()
1291   "Retieve an alist of userpic keyword/url pairs."
1292   (let ((cookies (or lj-cookies
1293                      (error "No LJ cookies found")))
1294         (url (concat lj-base-url
1295                      "?mode=login"
1296                      "&user=" lj-user-id
1297                      "&auth_method=cookie"
1298                      (format "&ver=%d" (if (lj-utf-emacs-p) 1 0))
1299                      "&clientversion=" lj-clientversion
1300                      "&getpickws=1"
1301                      "&getpickwurls=1")))
1302     (lj-http-post url cookies #'lj-pickws-proc-parser)))
1303
1304 (defun lj-moods-proc-parser (buf)
1305   "Process the output from `lj-get-moods'.
1306 Argument BUF is the process buffer used."
1307   (let ((regexp "mood_[0-9]+_id\n\\(.*\\)\nmood_[0-9]+_name\n\\(.*\\)")
1308         (msg "^message\n\\(.*$\\)")
1309         moods)
1310     (with-current-buffer buf
1311       (when (lj-proc-success)
1312         (goto-char (point-min))
1313         (save-excursion
1314           (while (re-search-forward regexp nil t)
1315             (setq moods (cons (cons (match-string 2)
1316                                     (string-to-int (match-string 1)))
1317                               moods))))
1318         (when (re-search-forward msg nil t)
1319           (pop-to-buffer (get-buffer-create "*LJ Message*"))
1320           (insert "Important Message From LiveJournal:\n"
1321                   "==================================\n\n")
1322           (insert (match-string 1)))
1323         (kill-buffer buf))
1324       (or (file-directory-p lj-directory)
1325           (make-directory-path lj-directory))
1326       (with-current-buffer (find-file-noselect lj-moods-file)
1327         (erase-buffer)
1328         (insert ";;; Automatically generated DO NOT EDIT -*- Emacs-Lisp -*-\n"
1329                 (format "(setq lj-moods (quote %S))" moods))
1330         (save-buffer)
1331         (eval-current-buffer nil)
1332         (kill-buffer nil)))))
1333
1334 (defun lj-get-moods ()
1335   "Retieve an alist of mood/moodid pairs."
1336   (let ((cookies (or lj-cookies
1337                      (error "No LJ cookies found")))
1338         (url (concat lj-base-url
1339                      "?mode=login"
1340                      "&user=" lj-user-id
1341                      "&auth_method=cookie"
1342                      (format "&ver=%d" (if (lj-utf-emacs-p) 1 0))
1343                      "&clientversion=" lj-clientversion
1344                      "&getmoods=0")))
1345     (lj-http-post url cookies #'lj-moods-proc-parser)))
1346
1347 (defvar lj-last-user-set-time nil)
1348 (defvar lj-qotd 0)
1349 ;; adapted from jwz-lj.el
1350 (defun lj-construct-url (subject body user
1351                                 &optional security tags community
1352                                 auto-format no-comments mood location
1353                                 music pickw date backdated)
1354   "Construct a URL to use for posting to LiveJournal.
1355
1356 Argument SUBJECT, a string, which is the title of the post.
1357 Argument BODY, a string, is the body of the post.
1358 Argument USER, a string, is the LJ userid to post as.
1359
1360 Optional argument SECURITY, a string, is the security level this post
1361 will have.  The default is `lj-default-security-level'.
1362
1363 Optional argument TAGS, a string, which is a comma delimited list of
1364 tags to add to this post.
1365
1366 Optional argument COMMUNITY, a string, which is the name of a LJ forum
1367 to send this post to instead of the user's blog.
1368
1369 Optional argument AUTO-FORMAT, when non-nil request that the LJ server
1370 automatically formats the post.  The default is nil, which means the
1371 post should NOT be auto formatted by LJ.
1372
1373 Optional argument NO-COMMENTS, when non-nil means to turn off comments
1374 on the post.
1375
1376 Optional argument MOOD, a string or an integer, is the post's \"mood\"
1377 header.  If it is an integer, it is a \"mood id\" which is mapped to a
1378 string by LJ.
1379
1380 Optional argument LOCATION, a string, free-form text describing your
1381 current location.  Livejournal turns it into a search URL to google
1382 maps.
1383
1384 Optional argument MUSIC, a string, of the currently playing mp3/ogg.
1385
1386 Optional argument PICKW, a string, of the userpic keyword to use.  If
1387 omitted, your default LJ userpic will be used.
1388
1389 Optional argument DATE, an internal time value as returned by
1390 `encode-time'.  Used to set a date/time on a post, if omitted the
1391 current time is used.
1392
1393 Optional boolean argument BACKDATED, causes the \"backdated\" flag to be
1394 set which will prevent the post from showing up on friends pages."
1395   (let* ((friends-mask nil)
1396          (tl (split-string-by-char
1397                 (format-time-string "%Y,%m,%d,%H,%M"
1398                                     (or date (current-time))) ?,))
1399          (year (first tl))
1400          (month (second tl))
1401          (day (third tl))
1402          (hour (fourth tl))
1403          (minute (fifth tl))
1404          (ctime (apply #'encode-btime (lj-parse-time-string
1405                                        (format "%s-%s-%s %s:%s"
1406                                                year month day hour minute))))
1407          (ltime (or lj-last-entry-btime (and (lj-get-last-entry-btime)
1408                                              lj-last-entry-btime)))
1409          url)
1410     ;; save custom date in case something goes wrong
1411     (if date
1412         (setq lj-last-user-set-time date)
1413       (setq lj-last-user-set-time nil))
1414     (setq subject (lj-hexify-string subject t))
1415     (setq body (lj-hexify-string body t))
1416     ;; security level
1417     (if (not (string-match "p\\(ublic\\|rivate\\)" security))
1418         (if (string= "usemask" security)
1419             (setq friends-mask 1)
1420           (let* ((groups (or lj-groups (lj-get-friends-groups)))
1421                  (id (cdr (assoc security groups))))
1422             (if id
1423                 (setq security "usemask"
1424                       friends-mask (lsh 1 id))
1425               (error "Unknown friends group: %s" security)))))
1426     (setq security (lj-hexify-string security t))
1427     ;; tags
1428     (if (> (length tags) 0)
1429         (setq tags (lj-hexify-string tags t))
1430       (setq tags nil))
1431     ;; mood
1432     (cond ((cdr (assoc mood lj-moods))
1433            (setq mood (cdr (assoc mood lj-moods))))
1434           ((and (stringp mood)
1435                 (> (length mood) 0))
1436            (setq mood (lj-hexify-string mood t)))
1437           ((integerp mood) nil)
1438           (t
1439            (setq mood nil)))
1440     ;; music
1441     (if (> (length music) 0)
1442         (setq music (lj-hexify-string music t))
1443       (setq music nil))
1444     ;; userpic
1445     (if (> (length pickw) 0)
1446         (setq pickw (lj-hexify-string pickw t))
1447       (setq pickw nil))
1448     ;; community
1449     (if (> (length community) 0)
1450         (setq community (lj-hexify-string community t))
1451       (setq community nil))
1452     ;; location
1453     (if (> (length location) 0)
1454         (setq location (lj-hexify-string location t))
1455       (setq location nil))
1456     ;; maybe force opt_backdated
1457     (when (> ltime ctime)
1458       (setq backdated t))
1459     ;; the final url
1460     (setq url (concat
1461                lj-base-url
1462                "?mode=postevent"
1463                "&user=" user
1464                "&auth_method=cookie"
1465                (format "&ver=%d" (if (lj-utf-emacs-p) 1 0))
1466                "&subject=" subject
1467                "&security=" security
1468                (when friends-mask
1469                  (format "&allowmask=%d" friends-mask))
1470                (when tags
1471                  (format "&prop_taglist=%s" tags))
1472                (when community
1473                  (format "&usejournal=%s" community))
1474                "&year=" year
1475                "&mon=" month
1476                "&day=" day
1477                "&hour=" hour
1478                "&min=" minute
1479                (when mood
1480                  (if (integerp mood)
1481                      (format "&prop_current_moodid=%d" mood)
1482                    (format "&prop_current_mood=%s" mood)))
1483                (when music
1484                  (format "&prop_current_music=%s" music))
1485                (when location
1486                  (format "&prop_current_location=%s" location))
1487                (when pickw
1488                  (format "&prop_picture_keyword=%s" pickw))
1489                (unless (zerop lj-qotd)
1490                  (format "&prop_qotdid=%d" lj-qotd))
1491                "&prop_opt_backdated=" (if backdated "1" "0")
1492                "&prop_opt_preformatted=" (if auto-format "0" "1")
1493                "&prop_opt_nocomments=" (if no-comments "1" "0")
1494                "&prop_useragent=" (lj-hexify-string lj-useragent)
1495                "&event=" body))
1496     url))
1497
1498 (defun lj-cut-toggle-top ()
1499   "Toggle view of LJ CUT text."
1500   (interactive)
1501   (save-excursion
1502     (goto-char (point-at-eol))
1503     (forward-char 1)
1504     (set-extent-property
1505      (extent-at (point) nil 'ljcut)
1506      'invisible (not (extent-property
1507                       (extent-at (point) nil 'ljcut) 'invisible)))))
1508
1509
1510 (defun lj-cut-mouse-toggle-top (event)
1511   "Toggle view of LJ CUT text under EVENT."
1512   (interactive "e")
1513   (let ((epoint (event-point event)))
1514     (save-excursion
1515       (goto-char epoint)
1516       (goto-char (point-at-eol))
1517       (forward-char 1)
1518       (set-extent-property
1519        (extent-at (point) nil 'ljcut)
1520        'invisible (not (extent-property
1521                         (extent-at (point) nil 'ljcut) 'invisible))))))
1522
1523 (defun lj-cut-toggle-bottom ()
1524   "Toggle view of LJ CUT text."
1525   (interactive)
1526   (save-excursion
1527     (goto-char (point-at-bol))
1528     (backward-char 1)
1529     (set-extent-property
1530      (extent-at (point) nil 'ljcut)
1531      'invisible (not (extent-property
1532                       (extent-at (point) nil 'ljcut) 'invisible)))))
1533
1534 (defun lj-cut-mouse-toggle-bottom (event)
1535   "Toggle view of LJ CUT text under EVENT."
1536   (interactive "e")
1537   (let ((epoint (event-point event)))
1538     (save-excursion
1539       (goto-char epoint)
1540       (goto-char (point-at-bol))
1541       (backward-char 1)
1542       (set-extent-property
1543        (extent-at (point) nil 'ljcut)
1544        'invisible (not (extent-property
1545                         (extent-at (point) nil 'ljcut) 'invisible))))))
1546
1547 (defvar lj-cut-keymap-top
1548   (let ((map (make-sparse-keymap)))
1549     (set-keymap-name map 'lj-cut-keymap-top)
1550     (define-key map [return] #'lj-cut-toggle-top)
1551     (define-key map [button2] #'lj-cut-mouse-toggle-top)
1552     map)
1553   "Keymap for LJ CUT extents.")
1554
1555 (defvar lj-cut-keymap-bottom
1556   (let ((map (make-sparse-keymap)))
1557     (set-keymap-name map 'lj-cut-keymap-bottom)
1558     (define-key map [return] #'lj-cut-toggle-bottom)
1559     (define-key map [button2] #'lj-cut-mouse-toggle-bottom)
1560     map)
1561   "Keymap for LJ CUT extents.")
1562
1563 (defun lj-cut-region (b e)
1564   "Mark text in the region B to E as an LJ CUT.
1565
1566 The text that is to be hidden behind the LJ CUT is made invisible in
1567 the buffer.  The visibility can be toggled with Return or Button2 on
1568 either of the lj-cut delimiters."
1569   (interactive "r")
1570   (let ((echo "Ret / Button2 Toggle View")
1571         ext)
1572     (save-restriction
1573       (narrow-to-region b e)
1574       (lj-text-to-html (point-min) (point-max))
1575       (set-extent-properties
1576        (setq ext (make-extent (point-min) (point-max)))
1577        '(start-open t end-open t invisible t ljcut t))
1578       (goto-char (point-min))
1579       (set-extent-properties
1580        (insert-face "<lj-cut text=\"---More---\">" 'widget-button-face)
1581        `(help-echo ,echo
1582                    balloon-help ,echo
1583                    keymap ,lj-cut-keymap-top
1584                    mouse-face font-lock-warning-face))
1585       (insert "\n")
1586       (goto-char (point-max))
1587       (set-extent-properties
1588        (insert-face "</lj-cut>" 'widget-button-face)
1589        `(help-echo ,echo
1590                    balloon-help ,echo
1591                    keymap ,lj-cut-keymap-bottom
1592                    mouse-face font-lock-warning-face))
1593       (insert "\n"))
1594     (set-extent-properties
1595      ext
1596      '(start-open nil end-open nil))
1597     (run-hooks 'lj-cut-hook)))
1598
1599 (defvar lj-poll-types '("radio" "check" "drop" "text" "scale")
1600   "LJ poll types.")
1601
1602 (defun lj-insert-poll (name type question)
1603   "Insert a poll into a LJ post.
1604 Argument NAME is the title of the poll.
1605 Argument TYPE is the type of poll \(see `lj-poll-types'\).
1606 Argument QUESTION is the poll question, or \"topic\"."
1607   (interactive
1608    (list (read-string "Poll Title: " nil nil "unnamed poll")
1609          (completing-read "Poll Type (default \"radio\"): "
1610                           (mapcar #'list lj-poll-types)
1611                           nil t nil nil "radio")
1612          (read-string "Poll Question: ")))
1613   (let ((voters (completing-read "Who can vote (default \"all\"): "
1614                                  (mapcar #'list '("all" "friends"))
1615                                  nil t nil nil "all"))
1616         (viewers (completing-read "Who can view results (default \"all\"): "
1617                                   (mapcar #'list '("all" "friends" "none"))
1618                                   nil t nil nil "all"))
1619         (p (point)))
1620     (insert
1621      (format "\n<lj-poll name=\"%s\" whovote=\"%s\" whoview=\"%s\">"
1622              name voters viewers)
1623      (format "\n<lj-pq type=\"%s\"" type))
1624     (cond
1625       ((string= "scale" type)
1626        (let ((low (read-number "Scale low mark (int): " t "1"))
1627              (high (read-number "Scale high mark (int): " t "10"))
1628              (step (read-number "Stepping: " t "1")))
1629          (insert (format " from=\"%d\" to=\"%d\" by=\"%d\">"
1630                          low high step)
1631                  (format "\n%s" question))))
1632       ((string= "text" type)
1633        (let* ((size (read-number "Text box size: " t "50"))
1634               (max (read-number "Max answer length: " t
1635                                 (number-to-string (1- size)))))
1636          (insert (format " size=\"%d\" maxlength=\"%d\">" size max)
1637                  (format "\n%s" question))))
1638       (t (insert (format ">\n%s" question))))
1639     (unless (string-match "scale\\|text" type)
1640       (let ((x "x"))
1641         (while (not (zerop (length x)))
1642           (setq x (read-string "Poll Answer (RET to finish): "))
1643           (or (zerop (length x))
1644               (insert (format "\n<lj-pi>%s</lj-pi>" x))))))
1645     (insert "\n</lj-pq>"
1646             "\n</lj-poll>")
1647     (indent-region p (point) nil)
1648     (run-hooks 'lj-poll-hook)))
1649
1650 ;; Apparantly the _VALID_ markup that this function produces causes
1651 ;; some (all?) versions of M$ Internet Exploiter to buffer the entire
1652 ;; movie before beginning playback.  Hey, lets call it a FEATURE!
1653 (defun lj-insert-youtube (url)
1654   "Insert a Google or Youtube video URL into a LJ post."
1655   (interactive "sVideo URL: ")
1656   (let* ((googlep (string-match "^http://video\\.google\\.com/.*$" url))
1657          (youtubep (string-match "^http://\\(www\\.\\)?youtube\\.com/.*$" url))
1658          (w (if googlep 420 400))
1659          (h (if googlep 352 338))
1660          (p (point)))
1661     (unless (or googlep youtubep)
1662       (error "Invalid Google/Youtube URL: %s" url))
1663     (insert (format "\n<object width=\"%d\" height=\"%d\"" w h)
1664             "\ntype=\"application/x-shockwave-flash\"")
1665     (if googlep
1666         (setq url (replace-regexp-in-string "/videoplay\\?"
1667                                             "/googleplayer.swf?" url))
1668       (setq url (replace-regexp-in-string "\\(/watch\\)?\\?v=" "/v/" url)))
1669     (setq url (replace-regexp-in-string "&.*$" "" url))
1670     (insert (format "\ndata=\"%s\">" url)
1671             "\n<param name=\"movie\""
1672             (format "\nvalue=\"%s\" />" url)
1673             "\n</object>")
1674     (indent-region p (point) nil)
1675     (run-hooks 'lj-youtube-hook)))
1676
1677 (defun lj-insert-journal (name &optional community)
1678   "Insert a link to NAME journal or LJ community into an LJ post.
1679
1680 Optional prefix argument, COMMUNITY means the link is to a LJ community
1681 instead of a LJ user's journal."
1682   (interactive "sUser or Community name: \nP")
1683   (let ((type (if current-prefix-arg "comm" "user"))
1684         (p (point)))
1685     (insert (format "\n<lj %s=\"%s\" />\n" type name))
1686     (indent-region p (point) nil)
1687     (run-hooks 'lj-journal-hook)))
1688
1689 (defvar lj-abbrev-table nil
1690   "Abbrev table to use in `lj-mode'.")
1691 (define-abbrev-table 'lj-abbrev-table ())
1692
1693 (define-derived-mode lj-mode html-mode "LJ"
1694   "This is a mode for composing LiveJournal posts.
1695 Its parent modes are `html-mode' and `sgml-mode' so everything you
1696 need to construct good clean HTML should be right at your fingertips.
1697
1698 LJ specific bindings:
1699
1700   \\[lj-post]\tSubmit post to LiveJournal
1701   \\[lj-preview]\t\tPreview post in web browser
1702   \\[lj-validate]\t\tValidate the markup in the post
1703
1704   \\[lj-writers-block]\t\tAnswer a LJ \"Writer's Block\" question
1705
1706   \\[lj-cut-region]\t\tHide text behind a LJ \"cut\"
1707   \\[lj-insert-journal]\t\tInsert a journal link
1708   \\[lj-insert-poll]\t\tInsert a poll
1709   \\[lj-insert-youtube]\t\tInsert a Google or YouTube Video
1710
1711   \\[lj-goto-subject]\tMove to the Subject header
1712   \\[lj-goto-fcc]\tMove to the FCC header
1713   \\[lj-goto-bcc]\tMove to the BCC header
1714   \\[lj-goto-community]\tMove to the Community header
1715   \\[lj-goto-music]\tMove to the Music header
1716   \\[lj-goto-security]\tMove to the Security header
1717   \\[lj-goto-mood]\tMove to the Mood header
1718   \\[lj-goto-location]\tMove to the Location header
1719   \\[lj-goto-userpic]\tMove to the Userpic header
1720   \\[lj-goto-tags]\tMove to the Tags header
1721   \\[lj-goto-body]\tMove to the post body
1722
1723   \\[lj-customise-faces]\t\tSet the header faces
1724   \\[lj-customise-group]\t\tSet the user options
1725
1726
1727 General bindings:
1728 \\{lj-mode-map}"
1729   :group 'lj
1730   :syntax-table nil
1731   :abbrev-table 'lj-abbrev-table
1732   (auto-save-mode 1))
1733
1734 (add-hook 'lj-mode-hook #'font-lock-mode)
1735
1736 (defun lj-make-archive-name ()
1737   "Compute a filename for archiving LJ posts.
1738
1739 The filenames are of the format... `ljp-YYYYMMDDHHMM'."
1740   (let ((file (format-time-string "ljp-%Y%m%d%H%M"))
1741         (dir lj-archive-directory))
1742     (expand-file-name file dir)))
1743
1744 (defun lj-generate-new-buffer ()
1745   "Create a new buffer for writing a new LJ post."
1746   (or (file-directory-p lj-drafts-directory)
1747       (make-directory-path lj-drafts-directory))
1748   (switch-to-buffer
1749    (find-file-noselect
1750     (expand-file-name (format-time-string "ljd-%Y%m%d%H%M")
1751                       lj-drafts-directory)))
1752   (rename-buffer "*LJ-Post*" 'unique)
1753   (when (lj-utf-emacs-p)
1754     (set-buffer-file-coding-system 'utf-8))
1755   (insert " \n")
1756   (make-extent (point-min) (point-at-eol))
1757   (insert "Subject: \n")
1758   (when lj-archive-posts
1759     (or (file-directory-p lj-archive-directory)
1760         (make-directory-path lj-archive-directory))
1761     (insert (format "FCC: %s\n" (lj-make-archive-name))))
1762   (when (stringp lj-bcc-address)
1763     (insert (format "BCC: %s\n" lj-bcc-address)))
1764   (insert (format "Security: %s\n" lj-default-security-level))
1765   (when (stringp lj-default-location)
1766     (insert (format "Location: %s\n" lj-default-location)))
1767   (when (functionp (symbol-value 'lj-music))
1768     (insert (format "Music: %s\n" (funcall lj-music))))
1769   (insert "Mood: \n")
1770   (when-boundp 'lj-default-pickw
1771     (insert (format "Userpic: %s\n" lj-default-pickw))
1772     (lj-update-userpic-glyph (expand-file-name lj-default-pickw
1773                                                lj-userpic-directory)))
1774   (insert "Tags: \n")
1775   ;; fool html mode
1776   (set-extent-property
1777    (insert-face "</head>\n" 'default) 'invisible t)
1778   (insert lj-header-separator "\n")
1779   (lj-mode))
1780
1781 (defun lj-goto-subject (&optional nocreate)
1782   "Move to the Subject header of an LJ post buffer.
1783
1784 The header is created if it doesn't exist, unless optional argument
1785 NOCREATE is non-nil."
1786   (interactive)
1787   (goto-char (point-min))
1788   (or (re-search-forward "^Subject: " nil 'missing)
1789       (unless nocreate
1790         (goto-char (point-min))
1791         (insert "Subject: ")
1792         (backward-char 1))))
1793
1794 (defun lj-goto-fcc (&optional nocreate)
1795   "Move to the FCC header of an LJ post buffer.
1796
1797 The header is created if it doesn't exist, unless optional argument
1798 NOCREATE is non-nil."
1799   (interactive)
1800   (goto-char (point-min))
1801   (or (re-search-forward "^FCC: " nil 'missing)
1802       (unless nocreate
1803         (goto-char (point-min))
1804         (insert "FCC: \n")
1805         (backward-char 1))))
1806
1807 (defun lj-goto-bcc (&optional nocreate)
1808   "Move to the BCC header of an LJ post buffer.
1809
1810 The header is created if it doesn't exist, unless optional argument
1811 NOCREATE is non-nil."
1812   (interactive)
1813   (goto-char (point-min))
1814   (or (re-search-forward "^BCC: " nil 'missing)
1815       (unless nocreate
1816         (goto-char (point-min))
1817         (insert "BCC: \n")
1818         (backward-char 1))))
1819
1820 (defun lj-goto-security (&optional nocreate)
1821   "Move to the Security header of an LJ post buffer.
1822
1823 The header is created if it doesn't exist, unless optional argument
1824 NOCREATE is non-nil."
1825   (interactive)
1826   (goto-char (point-min))
1827   (or (re-search-forward "^Security: " nil 'missing)
1828       (unless nocreate
1829         (goto-char (point-min))
1830         (insert "Security: \n")
1831         (backward-char 1))))
1832
1833 (defun lj-goto-community (&optional nocreate)
1834   "Move to the Community header of an LJ post buffer.
1835
1836 The header is created if it doesn't exist, unless optional argument
1837 NOCREATE is non-nil."
1838   (interactive)
1839   (goto-char (point-min))
1840   (or (re-search-forward "^Community: " nil 'missing)
1841       (unless nocreate
1842         (goto-char (point-min))
1843         (insert "Community: \n")
1844         (backward-char 1))))
1845
1846 (defun lj-goto-location (&optional nocreate)
1847   "Move to the Location header of an LJ post buffer.
1848
1849 The header is created if it doesn't exist, unless optional argument
1850 NOCREATE is non-nil."
1851   (interactive)
1852   (goto-char (point-min))
1853   (or (re-search-forward "^Location: " nil 'missing)
1854       (unless nocreate
1855         (goto-char (point-min))
1856         (insert "Location: \n")
1857         (backward-char 1))))
1858
1859 (defun lj-goto-mood (&optional nocreate)
1860   "Move to the Mood header of an LJ post buffer.
1861
1862 The header is created if it doesn't exist, unless optional argument
1863 NOCREATE is non-nil."
1864   (interactive)
1865   (goto-char (point-min))
1866   (or (re-search-forward "^Mood: " nil 'missing)
1867       (unless nocreate
1868         (goto-char (point-min))
1869         (insert "Mood: \n")
1870         (backward-char 1))))
1871
1872 (defun lj-goto-music (&optional nocreate)
1873   "Move to the Music header of an LJ post buffer.
1874
1875 The header is created if it doesn't exist, unless optional argument
1876 NOCREATE is non-nil."
1877   (interactive)
1878   (goto-char (point-min))
1879   (or (re-search-forward "^Music: " nil 'missing)
1880       (unless nocreate
1881         (goto-char (point-min))
1882         (insert "Music: \n")
1883         (backward-char 1))))
1884
1885 (defun lj-goto-userpic (&optional nocreate)
1886   "Move to the Userpic header of an LJ post buffer.
1887
1888 The header is created if it doesn't exist, unless optional argument
1889 NOCREATE is non-nil."
1890   (interactive)
1891   (goto-char (point-min))
1892   (or (re-search-forward "^Userpic: " nil 'missing)
1893       (unless nocreate
1894         (goto-char (point-min))
1895         (insert "Userpic: \n")
1896         (backward-char 1))))
1897
1898 (defun lj-goto-tags (&optional nocreate)
1899   "Move to the Tags header of an LJ post buffer.
1900 The header is created if it doesn't exist unless NOCREATE is non-nil."
1901   (interactive)
1902   (goto-char (point-min))
1903   (or (re-search-forward "^Tags: " nil 'missing)
1904       (unless nocreate
1905         (goto-char (point-min))
1906         (insert "Tags: \n")
1907         (backward-char 1))))
1908
1909 (defun lj-goto-body ()
1910   "Move to the body of an LJ post buffer."
1911   (interactive)
1912   (goto-char (point-min))
1913   (re-search-forward (regexp-quote lj-header-separator) nil t)
1914   (forward-line 1)
1915   (goto-char (point-at-bol)))
1916
1917 (defun lj-current-header ()
1918   "Return the name of the LJ header on the current line, or nil."
1919   (let ((hregex lj-header-regexp)
1920         (separator (regexp-quote lj-header-separator)))
1921     (if (save-excursion (re-search-forward separator nil t))
1922         (save-restriction
1923           (narrow-to-region (point-at-bol) (point-at-eol))
1924           (string-match hregex (buffer-string))
1925           (substring (buffer-string) (match-beginning 1) (match-end 1)))
1926       nil)))
1927
1928 (defun lj-header-content (header)
1929   "Return the content of HEADER as a string."
1930   (let ((goto (intern-soft (concat "lj-goto-" (downcase header)))))
1931     (save-excursion
1932       (funcall goto 'nocreate)
1933       (buffer-substring-no-properties (point) (point-at-eol)))))
1934
1935 (defun lj-update-userpic-glyph (glyph)
1936   "Update the userpic, GLYPH, displayed in the LJ-Post buffer."
1937   (let ((ext (extent-at (point-min)))
1938         (type (if (featurep '(and sxemacs ffi-magic))
1939                   (downcase (car (split-string-by-char
1940                                   (declare-fboundp
1941                                    (magic:file-type glyph)) ?\ )))
1942                 (downcase
1943                  (cadr (split-string-by-char
1944                         (shell-command-to-string (concat "file " glyph))
1945                         ?\ ))))))
1946     (set-extent-begin-glyph
1947      ext (make-glyph
1948           (list (vector (intern-soft type)
1949                         :data (with-temp-buffer
1950                                 (insert-file-contents-literally glyph)
1951                                 (buffer-string))))))))
1952
1953 ;; Header completion
1954 (defvar lj-completion-time 3
1955   "Time in seconds before completion list is reset.")
1956
1957 (defvar lj-completion-timer (make-itimer)
1958   "Completion timer.")
1959
1960 (defvar lj-completion-list nil
1961   "Completion list.")
1962
1963 (defvar lj-header-syntax-table
1964   (let ((table (copy-syntax-table text-mode-syntax-table)))
1965     (modify-syntax-entry ?~  "w " table)
1966     (modify-syntax-entry ?`  "w " table)
1967     (modify-syntax-entry ?-  "w " table)
1968     (modify-syntax-entry ?_  "w " table)
1969     (modify-syntax-entry ?+  "w " table)
1970     (modify-syntax-entry ?{  "w " table)
1971     (modify-syntax-entry ?[  "w " table)
1972     (modify-syntax-entry ?}  "w " table)
1973     (modify-syntax-entry ?]  "w " table)
1974     (modify-syntax-entry ?\\ "w " table)
1975     (modify-syntax-entry ?|  "w " table)
1976     (modify-syntax-entry ?\; "w " table)
1977     (modify-syntax-entry ?'  "w " table)
1978     (modify-syntax-entry ?<  "w " table)
1979     (modify-syntax-entry ?>  "w " table)
1980     (modify-syntax-entry ?#  "w " table)
1981     (modify-syntax-entry ?\  "w " table)
1982     (modify-syntax-entry ?.  "w " table)
1983     table)
1984   "Syntax table used in funky header cycling completion.")
1985
1986 (defun lj-init-completion-timer ()
1987   "Initialise the completion timer."
1988   (let ((timer lj-completion-timer))
1989     (set-itimer-function timer #'(lambda ()
1990                                    (setq lj-completion-list nil)))
1991     (set-itimer-value timer lj-completion-time)))
1992 (add-hook 'lj-init-hook #'lj-init-completion-timer)
1993
1994 (defsubst lj-cycle-list (list &optional reverse)
1995   "Return a list of head of LIST, and LIST rotated 1 place forward.
1996
1997 If optional argument, REVERSE is non-nil, rotate the list in the other
1998 direction."
1999   (if (featurep 'sxemacs)
2000       (let ((list (apply #'dllist list))
2001             name)
2002         (if reverse
2003             (dllist-rrotate list)
2004           (dllist-lrotate list))
2005         (setq name (dllist-car list))
2006         (list name (dllist-to-list list)))
2007     ;; XEmacs
2008     (if reverse
2009         (let* ((name (car (last list)))
2010                (l1 (cdr (reverse list)))
2011                (l2 (reverse l1)))
2012           (push name l2)
2013           (list name l2))
2014       (let* ((name (cadr list))
2015              (oldcar (car list))
2016              (list (cdr list))
2017              (list (append list (list oldcar))))
2018         (list name list)))))
2019
2020 (defsubst lj-set-completion-timer ()
2021   "(Re)set completion timer's value."
2022   (let ((timer lj-completion-timer))
2023     (and (itimerp timer)
2024          (set-itimer-value timer lj-completion-time))))
2025
2026 (defun lj-complete-header-backwards ()
2027   "Complete header, cycling backwards."
2028   (interactive)
2029   (and (lj-current-header)
2030        (lj-complete-header 'reverse)
2031        (when (string-match (lj-current-header) "Userpic")
2032          (when (file-exists-p (expand-file-name
2033                                (lj-header-content "userpic")
2034                                lj-userpic-directory))
2035            (lj-update-userpic-glyph
2036             (expand-file-name (lj-header-content "userpic")
2037                               lj-userpic-directory))))))
2038
2039 (defun lj-complete-header (&optional reverse)
2040   "Completion for LJ headers.
2041
2042 This completion does not pop up any completion buffers, instead it
2043 cycles through the possible completions \"in-place\" with each
2044 successive TAB.
2045
2046 With non-nil optional argument, REVERSE, the cycling goes in the other
2047 direction."
2048   (interactive)
2049   (unless lj-completion-list
2050     (unless (itimer-live-p lj-completion-timer)
2051       (lj-set-completion-timer)
2052       (activate-itimer lj-completion-timer))
2053     (let* ((completion-ignore-case t)
2054            (type (lj-current-header))
2055            (table (cond ((string= type "Security")
2056                          (let ((groups (copy-sequence
2057                                         (append lj-groups
2058                                                 '(("public" . ?a)
2059                                                   ("private" . ?b)
2060                                                   ("usemask" . ?c))))))
2061                            (sort* groups #'string-lessp :key #'car)))
2062                         ((string= type "Mood")
2063                          (let ((moods (copy-sequence lj-moods)))
2064                            (sort* moods #'string-lessp :key #'car)))
2065                         ((string= type "Userpic")
2066                          (let ((pics (copy-sequence lj-pickws)))
2067                            (sort* pics #'string-lessp :key #'car)))
2068                         ((string= type "Tags")
2069                          (let ((tags (mapcar #'(lambda (e) (cons e ?a))
2070                                              lj-tags)))
2071                            (sort* tags #'string-lessp :key #'car)))
2072                         (t (error 'invalid-argument type))))
2073            (current (if (string-match (current-word) type)
2074                         ""
2075                       (current-word)))
2076            (completion (try-completion current table))
2077            (all (all-completions current table)))
2078       (if (null completion)
2079           (message "Can't find completion for \"%s\"" current)
2080         (setq lj-completion-list all))))
2081   (when lj-completion-list
2082     (multiple-value-bind (completion newlist)
2083         (lj-cycle-list lj-completion-list reverse)
2084       (setq lj-completion-list newlist)
2085       (with-syntax-table lj-header-syntax-table
2086         (unless (string= "" (current-word))
2087           (unless (eolp)
2088             (forward-word))
2089           (unless (string-match (lj-current-header) (current-word))
2090             (backward-delete-word)))
2091         (insert " " completion)))
2092     (lj-set-completion-timer)))
2093
2094 (defun lj-sgml-indent-tab-or-complete (&optional refresh)
2095   "Does completion if in LJ headers, `sgml-indent-or-tab' otherwise.
2096
2097 If point is after the header separator, this function simply calls
2098 `sgml-indent-or-tab'.  If point is in the headers section it will do
2099 completion relevent to the header on the current line.
2100
2101 Please note that this is \"inline\" completion, that means you won't
2102 be prompted for anything in the minibuffer.  The completions will
2103 cycle directly in the LJ-post buffer.
2104
2105 The different header completions are:
2106
2107    Subject: Sweet bugger all.  Sorry, haven't perfected read-mind-mode
2108             yet.
2109
2110        FCC: Computes a new archive filename.
2111
2112        BCC: BBDB email addresses
2113
2114   Security: Completes valid security levels.  With prefix arg REFRESH,
2115             update your list of friends groups from livejournal.com
2116
2117  Community: No completion, just insert a TAB.
2118
2119      Music: Refreshes to the currently current song
2120
2121       Mood: Completes moods.  With prefix arg REFRESH, update the list
2122             of moods from livejournal.com.
2123
2124   Location: No completion.
2125
2126    Userpic: Completes list of LJ userpic keywords you have defined.
2127             With prefix arg REFRESH, update you list of userpic
2128             keywords.
2129
2130       Tags: Multiple completion from your list of previously used tags.
2131             With prefix arg REFRESH, update your list of tags from
2132             livejournal.com."
2133   (interactive "P")
2134   (let ((header (lj-current-header)))
2135     (if header
2136         (cond ((string= header "Subject")
2137                (error "Sorry, me crystal ball is in for repairs"))
2138               ((string= header "FCC")
2139                (let ((new (lj-make-archive-name)))
2140                  (goto-char (point-at-bol))
2141                  (re-search-forward "^FCC: " (point-at-eol))
2142                  (delete-region (point) (point-at-eol))
2143                  (insert new)))
2144               ((string= header "BCC")
2145                (if-fboundp #'bbdb-complete-name
2146                    (progn
2147                      (goto-char (point-at-eol))
2148                      (bbdb-complete-name))
2149                  (expand-abbrev)))
2150               ((string= header "Security")
2151                (when (or refresh (not lj-groups))
2152                  (lj-get-friends-groups))
2153                  (goto-char (point-at-bol))
2154                  (re-search-forward "^Security: " (point-at-eol))
2155                  (lj-complete-header))
2156               ((string= header "Community")
2157                (goto-char (point-at-eol))
2158                (insert "\t"))
2159               ((string= header "Music")
2160                (let ((current (and (functionp (symbol-value 'lj-music))
2161                                    (funcall lj-music))))
2162                  (when current
2163                    (goto-char (point-at-bol))
2164                    (re-search-forward "^Music: " (point-at-eol))
2165                    (delete-region (point) (point-at-eol))
2166                    (insert current))))
2167               ((string= header "Mood")
2168                (when (or refresh (not lj-moods))
2169                  (lj-get-moods))
2170                (goto-char (point-at-bol))
2171                  (re-search-forward "^Mood: " (point-at-eol))
2172                  (lj-complete-header))
2173               ((string= header "Location")
2174                (error "If you don't know, I can't help you"))
2175               ((string= header "Userpic")
2176                (when (or refresh (not lj-pickws))
2177                  (lj-get-pickws))
2178                  (goto-char (point-at-bol))
2179                  (re-search-forward "^Userpic: " (point-at-eol))
2180                  (lj-complete-header)
2181                  (when (file-exists-p (expand-file-name
2182                                        (lj-header-content "userpic")
2183                                        lj-userpic-directory))
2184                    (lj-update-userpic-glyph
2185                     (expand-file-name (lj-header-content "userpic")
2186                                       lj-userpic-directory))))
2187               ((string= header "Tags")
2188                (when (or refresh (not lj-tags))
2189                  (lj-get-tags))
2190                (lj-complete-header))
2191               (t
2192                (error "Unknown LJ header: %s" header)))
2193       (sgml-indent-or-tab))))
2194
2195 (defregexp lj-url-regexp
2196   (concat "\\(https?://\\|s?ftp://\\|gopher://\\|telnet://"
2197           "\\|wais://\\|file:/\\|s?news:\\)"
2198           "[^]\t\n \"'()<>[^`{}]*[^]\t\n \"'()<>[^`{}.,;\\(&gt\\)]+")
2199   "A regular expression matching URL's.")
2200
2201 (defregexp lj-email-regexp
2202   "[-a-zA-Z0-9._]+@\\([-a-zA-z0-9_]+\\.\\)+[a-zA-Z0-9]+"
2203   "A regular expression matching email addresses.")
2204
2205 (defun lj-text-to-html (beg end &optional nopbr)
2206   "Convert the plain text in the region BEG - END to html.
2207
2208 With optional argument, NOPBR, don't add <p>..</p> or <br /> tags.
2209
2210 This is an extremely basic converter.  All it really does is wrap
2211 paragraphs in <p>...</p>, and add <br /> to the end of each non-blank
2212 line.  It will also convert old 70's style text highlighting to the
2213 HTML equivalent.  e.g. _text_ -> <u>text</u>, *text* -> <b>text</b>.
2214 It also converts non-ASCII to HTML entities, and converts URL's and
2215 email addresses to hyperlinks.  Email addresses are obfuscated in an
2216 attempt to protect against spam harvesters.
2217
2218 Apart from the bold, underline, and hyperlink stuff, that's all the
2219 eye-candy you'll get.  Forget fonts, colours, tables, and lists.
2220 That's not what this is about.  The idea is to keep the text as close
2221 to \"as-is\" without resorting to using <pre>...</pre> tags.
2222
2223 Calling this function on text that contains \"<lj*>\" will break those
2224 tags.  So take note of what you are doing."
2225   (let ((replacements '(("&" . "&amp;")
2226                         ("\\.\\.\\." . "&hellip;")
2227                         ("<" . "&lt;")
2228                         (">" . "&gt;")
2229                         ("\"" . "&quot;")
2230                         ("_\\(.*\\)_" . "<u>\\1</u>")
2231                         ("\\*\\(.*\\)\\*" . "<b>\\1</b>")))
2232         (url lj-url-regexp)
2233         (email lj-email-regexp))
2234     (unless nopbr
2235       (add-to-list 'replacements
2236                    (cons (if (featurep 'sxemacs)
2237                              "\\([[:alnum:][:punct:]]\\)\n"
2238                            "\\([a-zA-Z0-9]\\|\\s.\\)\n")
2239                          "\\1<br />\n") 'append))
2240     (save-restriction
2241       (narrow-to-region beg end)
2242       (goto-char (point-min))
2243       ;; html quoting
2244       (mapcar
2245        #'(lambda (rep)
2246            (save-excursion
2247              (while (re-search-forward (car rep) nil t)
2248                (replace-match (cdr rep) t))))
2249        replacements)
2250       ;; paragraphs
2251       (unless nopbr
2252         (save-excursion
2253           (while (not (eobp))
2254             (save-restriction
2255               (mark-paragraph)
2256               (narrow-to-region (point) (mark))
2257               (goto-char (point-min))
2258               (insert "<p>")
2259               (goto-char (point-max))
2260               (insert "</p>\n"))
2261             (forward-paragraph))))
2262       ;; urls
2263       (save-excursion
2264         (while (re-search-forward url nil t)
2265           (replace-match "<a href=\"\\&\">\\&</a>")))
2266       ;; emails
2267       (save-excursion
2268         (while (re-search-forward email nil t)
2269           (replace-match "<a href=\"mailto:\\&\">\\&</a>"))
2270         (while (search-backward "@" nil t)
2271           (replace-match "&#64;" nil t)))
2272       ;; entities
2273       (save-excursion
2274         (lj-entify-region (point-min) (point-max))))
2275     (when (region-exists-p)
2276       (zmacs-deactivate-region))))
2277
2278 (defun lj-ljtags-to-html ()
2279   "Convert \"<lj-*>\" tags to something resembling HTML.
2280
2281 This function is used so that the markup in a post can be validated
2282 before it is submitted, and also so the post can be previewed before
2283 it is submitted.  Do not expect anything fancy."
2284   (goto-char (point-min))
2285   ;; polls
2286   (save-excursion
2287     (while (re-search-forward "<lj-\\(poll\\)" nil t)
2288       (let ((p (point-at-bol)))
2289         (search-forward (concat "</lj-" (match-string 1) ">") nil t)
2290         (save-restriction
2291           (narrow-to-region p (point))
2292           (lj-text-to-html (point-min) (point-max) 'nopbr)
2293           (goto-char (point-min))
2294           (insert "<div class=\"ljpoll\">\n<pre>\n")
2295           (goto-char (point-max))
2296           (insert "\n</pre>\n</div>")))))
2297   ;; cuts
2298   (save-excursion
2299     (while (re-search-forward "^</?lj-cut\\( text=\"---More---\"\\)?>$" nil t)
2300       (lj-text-to-html (match-beginning 0) (match-end 0) 'nopbr)))
2301   (save-excursion
2302     (while (re-search-forward "&lt;lj-cut" nil t)
2303       (replace-match "<div class=\"ljcut\">\n\\&")
2304       (re-search-forward "&lt;/lj-cut&gt;" nil t)
2305       (replace-match "\\&\n</div>")))
2306   ;; journal links
2307   (save-excursion
2308     (while (re-search-forward "<lj user=\"\\(.*\\)\" />" nil t)
2309       (replace-match "<a href=\"http://\\1.livejournal.com/profile\">
2310   <img src=\"http://p-stat.livejournal.com/img/userinfo.gif\"
2311     alt=\"[info]\" width=\"17\" height=\"17\"
2312     style=\"vertical-align: bottom; border: 0; padding-right: 1px;\" />
2313 </a>
2314 <a href=\"http://\\1.livejournal.com/\"><b>\\1</b></a>")))
2315   ;; writer's block
2316   (save-excursion
2317     (while (re-search-forward "<lj-template name=\"qotd\" id=\"[0-9]+\" />"
2318                               nil t)
2319       (replace-match "<h3>Writer's Block Answer</h3>" t))))
2320
2321 (defun lj-validate ()
2322   "Check the markup in a LJ post.
2323
2324 Please note that livejournal.com is quite forgiving when it comes to
2325 HTML in journal entries, lj.el, on the other hand... isn't.  For
2326 your entry to pass this validation it needs to be valid XHTML 1.0
2327 Transitional."
2328   (interactive)
2329   (run-hooks 'lj-before-validate-hook)
2330   (let ((vf (make-temp-name (expand-file-name "LJ-" (temp-directory))))
2331         (pb (current-buffer)))
2332     (with-current-buffer (get-buffer-create vf)
2333       (erase-buffer)
2334       (insert lj-validate-header)
2335       (insert
2336        (save-excursion
2337          (set-buffer pb)
2338          (lj-goto-body)
2339          (save-restriction
2340            (narrow-to-region (point) (point-max))
2341            (buffer-string))))
2342       (insert lj-validate-footer)
2343       (lj-ljtags-to-html)
2344       (write-region (point-min) (point-max) vf))
2345     (unwind-protect
2346         (progn
2347           (find-file vf)
2348           (sgml-parse-prolog)
2349           (sleep-for 5)
2350           (sgml-validate (sgml-default-validate-command))
2351           (let ((proc (get-buffer-process "*sgml validation*")))
2352             (while (process-live-p proc)
2353               (sit-for 0.1)
2354               (message "Validating markup, please wait..."))
2355             (message "Validation complete!")
2356             (when (> (process-exit-status proc) 0)
2357               (error 'syntax-error (process-name proc)))))
2358       (kill-buffer vf)
2359       (delete-file vf)
2360       (switch-to-buffer pb))
2361     (run-hooks 'lj-after-validate-hook)))
2362
2363 (defun lj-preview-headers (buf)
2364   "Add htmlised LJ headers in buffer, BUF for `lj-preview'."
2365   (let (text pic)
2366     (save-excursion
2367       (save-restriction
2368         (set-buffer buf)
2369         (setq pic (lj-header-content "userpic"))
2370         (lj-goto-body)
2371         (narrow-to-region (point-min) (point))
2372         (setq text (buffer-substring-no-properties)))
2373       (widen)
2374       (with-temp-buffer
2375         (insert text)
2376         (lj-text-to-html (point-min) (point-max))
2377         (goto-char (point-min))
2378         (insert "<div class=\"ljhead\">\n")
2379         (and (search-forward "<p>")
2380              (insert (format "<img src=\"%s\" align=\"right\" alt=\"Userpic\" />"
2381                              (cdr (assoc pic lj-pickws)))))
2382         (while (search-forward "&lt;/head&gt;" nil t)
2383           (replace-match "" nil t))
2384         (goto-char (point-max))
2385         (insert "\n</div>\n\n")
2386         (buffer-string)))))
2387
2388 (defun lj-preview ()
2389   "Preview the LJ post in a web browser.
2390
2391 Please note that this is far from a true representation of what the
2392 thing will look like once it has been submitted to LiveJournal.  But
2393 it should give you a rough idea."
2394   (interactive)
2395   (run-hooks 'lj-before-preview-hook)
2396   (let ((vf (make-temp-name (expand-file-name "LJ-" (temp-directory))))
2397         (pb (current-buffer)))
2398     (with-current-buffer (get-buffer-create vf)
2399       (erase-buffer)
2400       (insert lj-validate-header)
2401       (insert (lj-preview-headers pb))
2402       (insert "<div class=\"lj\">\n")
2403       (insert
2404        (save-excursion
2405          (set-buffer pb)
2406          (lj-goto-body)
2407          (save-restriction
2408            (narrow-to-region (point) (point-max))
2409            (buffer-string))))
2410       (insert "\n</div>")
2411       (insert lj-validate-footer)
2412       (lj-ljtags-to-html)
2413       (browse-url-of-buffer))
2414     (when (region-exists-p)
2415       (zmacs-deactivate-region))
2416     (run-hooks 'lj-after-preview-hook)))
2417
2418 (defvar lj-last-url "No URL yet, got nothing to blog about?"
2419   "The URL to your last posted blog entry on LiveJournal.")
2420
2421 (defun lj-post-proc-parser (buf)
2422   "Process parser for `lj-post'.
2423 Argument BUF is the process buffer used."
2424   (let ((url "^url\n\\(.*$\\)"))
2425     (with-current-buffer buf
2426       (when (lj-proc-success)
2427         (setq lj-last-user-set-time nil)
2428         (goto-char (point-min))
2429         (if (re-search-forward url nil t)
2430             (setq lj-last-url (match-string 1))
2431           (setq lj-last-url "NO URL RETURNED FROM LiveJournal"))
2432         (kill-buffer nil)))))
2433
2434 (defun lj-archive-post (archive)
2435   "Archive the current post to ARCHIVE."
2436   (let ((buf (current-buffer)))
2437     (with-current-buffer (find-file-noselect archive)
2438       (insert-buffer buf)
2439       (goto-char (point-max))
2440       (insert (format "\n\n<a href\"%s\">View Online</a>\n" lj-last-url))
2441       (save-buffer)
2442       (kill-buffer nil))))
2443
2444 (defun lj-send-bcc (subject security tags comm mood music location body)
2445   "Send a copy of a LJ post via email to `lj-bcc-address'.
2446
2447 Argument SUBJECT is the subject header from the post.
2448
2449 Argument SECURITY is the security level from the post, it is added to
2450 them mail as X-LJ-Auth header.
2451
2452 Argument TAGS are the tags from the post, added as Keywords header.
2453
2454 Argument COMM is the community from the post, added as X-LJ-Community
2455 header
2456
2457 Argument MOOD is the mood from the post, added as X-LJ-Mood header.
2458
2459 Argument MUSIC is the music from the post, added as X-Now-Playing
2460 header.
2461
2462 Argument LOCATION is the location from the post, added as X-LJ-Location
2463 header
2464
2465 Argument BODY is of course the post's body."
2466   (let* ((from (concat user-full-name
2467                        " <" lj-user-id "@livejournal.com>"))
2468          (headers `(("From" . ,from)
2469                     ("Keywords" . ,tags)
2470                     ("X-LJ-Auth" . ,security)
2471                     ("X-LJ-Community" . ,comm)
2472                     ("X-LJ-Location" . ,location)
2473                     ("X-LJ-Mood" . ,mood)
2474                     ("X-Now-Playing" . ,music)
2475                     ("X-URL" . ,lj-last-url)
2476                     ("MIME-Version" . "1.0")
2477                     ("Content-Type" . "text/html")))
2478          (mail-user-agent 'sendmail-user-agent))
2479     (compose-mail lj-bcc-address subject headers)
2480     (goto-char (point-max))
2481     (insert body)
2482     (declare-fboundp (mail-send-and-exit nil))))
2483
2484 (defun lj-last-entry-proc-parser (buf)
2485   "Process the output from `lj-get-last-entry-btime'.
2486 Argument BUF is the process buffer used."
2487   (let ((regexp "^events_1_eventtime\n\\(.*$\\)"))
2488     (with-current-buffer buf
2489       (when (lj-proc-success)
2490         (goto-char (point-min))
2491         (re-search-forward regexp nil t)
2492         (setq lj-last-entry-btime
2493               (apply #'encode-btime (lj-parse-time-string
2494                                      (match-string 1))))
2495         (kill-buffer nil)))))
2496
2497 (defun lj-get-last-entry-btime ()
2498   "Leech the last entry from LJ to get it's date/time."
2499   (let ((cookies (or lj-cookies
2500                      (error "No LJ cookies found")))
2501         (url (concat lj-base-url
2502                      "?mode=getevents"
2503                      "&user=" lj-user-id
2504                      "&auth_method=cookie"
2505                      (format "&ver=%d" (if (lj-utf-emacs-p) 1 0))
2506                      "&noprops=1"
2507                      "&selecttype=lastn"
2508                      "&howmany=1")))
2509     (lj-http-post url cookies #'lj-last-entry-proc-parser)))
2510
2511 (defun lj-set-date/time ()
2512   "Return an internal time value to use as post date/time.
2513
2514 This will prompt for a date string of the format yyyy-mm-dd, and a
2515 time string in the format HH:MM \(24hr\).  If either are given a null
2516 string the current date/time are used.
2517
2518 The value returned is that same as from `encode-time'."
2519   (let* ((date (read-string (format-time-string "New date [%Y-%m-%d]: ")
2520                             nil nil
2521                             (format-time-string "%Y-%m-%d")))
2522          (time (read-string (format-time-string "New time [%H:%M]: ")
2523                             nil nil
2524                             (format-time-string "%H:%M")))
2525          (timestr (concat date " " time))
2526          (btime (apply #'encode-btime (lj-parse-time-string timestr))))
2527     (btime-to-time btime)))
2528
2529 (defun lj-twitter-compress-url (url)
2530   "Compress URL using tinyurl.com."
2531   (with-temp-buffer
2532     (mm-url-insert
2533      (concat "http://tinyurl.com/api-create.php?url="
2534              (lj-hexify-string url t)))
2535     (buffer-string)))
2536
2537 (defun lj-twitter-sentinel (process status)
2538   "Sentinel for `lj-twitter-update-status' PROCESS STATUS."
2539   (if (equal status "finished\n")
2540       (message "Sending to Twitter...done")
2541     (message "Sending to Twitter...failed: %s"
2542              (substring status 0 (1- (length status))))))
2543
2544 ;;; FIXME: Twitter no longer supports Basic Auth, so must rewrite this
2545 ;;; using OAuth.
2546 (defun lj-twitter-update-status (user pass status url)
2547   "Update twitter status.
2548
2549 Argument USER is your twitter username.
2550 Argument PASS is your twitter password.
2551 Argument STATUS is the subject header from your LJ post.
2552 Argument URL is the URL to the post on livejournal.com."
2553   (let* ((userpass (format "%s:%s" user pass))
2554          (turl (lj-twitter-compress-url url))
2555          (twit (concat "status="
2556                        (lj-hexify-string
2557                         (concat status "  See: " turl) t)))
2558          (twiturl "http://twitter.com/statuses/update.json")
2559          proc)
2560     (if (<= (length twit) 147)          ; twitter's max + "status="
2561         (progn
2562           (setq proc
2563                 (apply #'start-process
2564                        "LJcurl" nil "curl"
2565                        (list "-u" userpass
2566                              "-d" twit
2567                              "-s" twiturl
2568                              "-H" "X-Twitter-Client: SXEmacs_LJ"
2569                              "-H" (format "X-Twitter-Client-Version: %s"
2570                                           lj-version)
2571                              "-H" (concat
2572                                    "X-Twitter-Client-URL: "
2573                                    "http://www.sxemacs.org/~steve/lj/lj.xml")
2574                              "-d" "source=lj.el")))
2575           (set-process-sentinel proc #'lj-twitter-sentinel))
2576       (warn "LJ subject too long for Twitter"))))
2577
2578 (defun lj-post (&optional out-of-order)
2579   "Submit a new post to LiveJournal.
2580
2581 With a single prefix argument, OUT-OF-ORDER, prompt for a date/time to
2582 use for the post.
2583
2584 With two prefix args, also set a \"date out of order\" flag."
2585   (interactive "i")
2586   (run-hooks 'lj-before-post-hook)
2587   (let ((subject (lj-header-content "subject"))
2588         (body (and (lj-goto-body)
2589                    (buffer-substring-no-properties (point) (point-max))))
2590         (user lj-user-id)
2591         (security (lj-header-content "security"))
2592         (tags (lj-header-content "tags"))
2593         (comm (lj-header-content "community"))
2594         (mood (lj-header-content "mood"))
2595         (location (lj-header-content "location"))
2596         (music (lj-header-content "music"))
2597         (pickw (lj-header-content "userpic"))
2598         (cookies (or lj-cookies
2599                      (error "No LJ cookies found")))
2600         (backdated nil)
2601         (date nil)
2602         (draftid (buffer-file-name))
2603         url)
2604     (when (and out-of-order
2605                (null current-prefix-arg))
2606       (setq backdated t
2607             date lj-last-user-set-time))
2608     (cond ((eq (car current-prefix-arg) 4)
2609            (setq date (lj-set-date/time)))
2610           ((eq (car current-prefix-arg) 16)
2611            (setq date (lj-set-date/time)
2612                  backdated t)))
2613     (setq url (lj-construct-url subject body user security tags comm nil nil
2614                                 mood location music pickw date backdated))
2615     ;; lets save the draft out to disc just in case something goes wrong
2616     (save-buffer)
2617     (lj-http-post url cookies #'lj-post-proc-parser)
2618     (and lj-archive-posts
2619          (lj-archive-post (lj-header-content "fcc")))
2620     (and lj-bcc-address
2621          (lj-send-bcc subject security tags comm mood music location body))
2622 ;;; FIXME: twitter updates are broken, needs OAuth
2623 ;;    (and lj-twitter-flag
2624 ;;       (lj-twitter-update-status lj-twitter-username lj-twitter-password
2625 ;;                                 subject lj-last-url))
2626     (delete-file draftid)
2627     (run-hooks 'lj-after-post-hook)))
2628
2629 ;; keep track of the date of the last entry for backdating purposes
2630 (add-hook 'lj-after-post-hook #'lj-get-last-entry-btime)
2631
2632 ;;; Writer's Block
2633 (defvar lj-qotd-buffer "*LJ Writer's Block*"
2634   "Buffer displaying a list of LJ Writer's Block questions.")
2635
2636 (defun lj-parse-qotd-archive ()
2637   "Leech the qotd archive and make it presentable for human consumption."
2638   (let ((buf (get-buffer-create lj-qotd-buffer))
2639         (bregexp "<!-- Content -->")
2640         (eregexp "<p class='skiplinks'>")
2641         (qregexp "<p class='qotd-archive-item-question'>\\(.*\\)</p><p")
2642         (dregexp
2643          (concat "<div class=\"b-qotd-question\">"
2644                  "<p class='qotd-archive-item-date'>\\(.*[0-9]+\\)</p>"))
2645         (idregexp "^.*qotd=\\([0-9]+\\).*\n.*$")
2646         (url "http://www.livejournal.com/misc/qotdarchive.bml")
2647         b e)
2648     (with-current-buffer buf
2649       (when (lj-utf-emacs-p)
2650         (set-buffer-file-coding-system 'utf-8))
2651       (erase-buffer)
2652       (mm-url-insert url)
2653       (goto-char (point-min))
2654       (setq b (and (search-forward bregexp nil t)
2655                    (forward-line 3)
2656                    (point-at-bol))
2657             e (and (search-forward eregexp nil t)
2658                    (point-at-bol)))
2659       (narrow-to-region b e)
2660       (goto-char (point-min))
2661       (insert (make-string 72 ?=) "\n")
2662       (save-excursion
2663         (while (re-search-forward dregexp nil t)
2664           (replace-match (format "%s:\n\n" (match-string 1)) t)))
2665       (save-excursion
2666         (while (re-search-forward qregexp nil t)
2667           (replace-match (format "QOTD: %s\n" (match-string 1)) t)))
2668       (save-excursion
2669         (while (re-search-forward "^QOTD:" nil t)
2670           (fill-paragraph nil)))
2671       (save-excursion
2672         (while (re-search-forward idregexp nil t)
2673           (replace-match (concat (format "\nWriter's Block ID: %s\n"
2674                                          (match-string 1))
2675                                  (make-string 72 ?=)) t))))))
2676
2677 (defun lj-qotd-subject (qotd)
2678   "Update the Subject header with title of QOTD."
2679   (let ((url (format "http://www.livejournal.com/update.bml?qotd=%d"
2680                      qotd))
2681         (subject))
2682     (with-temp-buffer
2683       (mm-url-insert url)
2684       (goto-char (point-min))
2685       (when (re-search-forward "Writer&#39;s Block: \\(.*\\)\" name="
2686                                nil t)
2687         (setq subject (match-string 1))))
2688     (lj-goto-subject)
2689     (goto-char (point-at-eol))
2690     (insert subject)
2691     (goto-char (point-max))))
2692
2693 (defun lj-narrow-to-qotd (qotd)
2694   "Narrow Writer's Block buffer to a single QOTD."
2695   (let ((delim (make-string 72 ?=))
2696         b e)
2697     (goto-char (point-max))
2698     (setq e (and (search-backward (format "ID: %d" qotd))
2699                  (point-at-eol))
2700           b (search-backward delim))
2701     (narrow-to-region b e)
2702     (shrink-window-if-larger-than-buffer)
2703     (other-window 1)
2704     (unless (eq major-mode 'lj-mode)
2705       (switch-to-buffer "*LJ-Post*"))
2706     (unless (zerop lj-qotd)
2707       (insert (format "<lj-template name=\"qotd\" id=\"%d\" />\n\n"
2708                       lj-qotd))
2709       (lj-qotd-subject lj-qotd))))
2710
2711 (defun lj-cleanup-qotd ()
2712   "Reset `lj-qotd' to zero and kill the qotd buffer."
2713   (progn
2714     (setq lj-qotd 0)
2715     (delete-other-windows)
2716     (when (buffer-live-p (get-buffer lj-qotd-buffer))
2717       (kill-buffer lj-qotd-buffer))))
2718
2719 (defun lj-qotd-quit ()
2720   "Cancel a LJ \"Writer's Block\" composition."
2721   (interactive)
2722   (other-window 1)
2723   (unless (eq major-mode 'lj-mode)
2724     (switch-to-buffer "*LJ-Post*"))
2725   (lj-cleanup-qotd)
2726   (and (lj-goto-subject)
2727        (kill-region (point) (point-at-eol)))
2728   (and (lj-goto-tags)
2729        (kill-region (point) (point-at-eol)))
2730   (lj-goto-body)
2731   (remove-hook 'lj-after-post-hook #'lj-cleanup-qotd))
2732
2733 (defun lj-writers-block ()
2734   "Compose an answer to a LJ \"Writer's Block\" question."
2735   (interactive)
2736   (and (lj-goto-subject)
2737        (kill-region (point) (point-at-eol))
2738        (insert "Writer's Block: "))
2739   (and (lj-goto-tags)
2740        (kill-region (point) (point-at-eol))
2741        (insert "writer's block"))
2742   (and (lj-goto-body)
2743        (kill-region (point) (point-max)))
2744   (lj-sgml-indent-tab-or-complete)
2745   (lj-parse-qotd-archive)
2746   (pop-to-buffer lj-qotd-buffer)
2747   (local-set-key [space] #'scroll-up)
2748   (local-set-key [delete] #'scroll-down)
2749   (local-set-key [return]
2750                  #'(lambda ()
2751                      (interactive)
2752                      (setq lj-qotd (read-number "Select Writer's Block ID: " t))
2753                      (lj-narrow-to-qotd lj-qotd)))
2754   (local-set-key [q] #'lj-qotd-quit)
2755   (message "[SPC]/[DEL] to scroll, [q] to cancel, [RET] to enter QOTD ID")
2756   (add-one-shot-hook 'lj-after-post-hook #'lj-cleanup-qotd 'append))
2757
2758 (defun lj-session-auto-save-files ()
2759   "Return a list of auto-save files in `lj-drafts-directory'."
2760   (directory-files lj-drafts-directory nil
2761                    #'auto-save-file-name-p 'list t))
2762
2763 (defun lj-recover-drafts (files)
2764   "Recover auto-saved FILES in `lj-drafts-directory'."
2765   (let ((default-directory lj-drafts-directory))
2766     (while files
2767       (recover-file (auto-save-original-name (car files)))
2768       (lj-edit-draft (auto-save-original-name (car files)))
2769       (setq files (cdr files)))))
2770
2771 ;;; Globals
2772
2773 (defun lj ()
2774   "Compose a new LiveJournal entry."
2775   (interactive)
2776   (run-hooks 'lj-init-hook)
2777   ;; Maybe update tags, groups, moods, pic keywords
2778   (or lj-tags (lj-get-tags))
2779   (or lj-groups (lj-get-friends-groups))
2780   (or lj-moods (lj-get-moods))
2781   (or lj-default-pickw (lj-get-pickws))
2782   (let ((auto-saves (lj-session-auto-save-files)))
2783     (if (and auto-saves
2784              (y-or-n-p "Auto saved drafts exist, do you wish to recover "))
2785         (lj-recover-drafts auto-saves)
2786       (lj-generate-new-buffer))))
2787
2788 (defun lj-blog-buffer (buffer &optional noformat)
2789   "Use contents of BUFFER to compose LJ entry.
2790
2791 With optional prefix arg, NOFORMAT, don't attempt to convert the text
2792 to HTML."
2793   (interactive "bBuffer to blog: \nP")
2794   (let ((blog (with-temp-buffer
2795                 (insert-buffer buffer)
2796                 (unless current-prefix-arg
2797                   (lj-text-to-html (point-min) (point-max)))
2798                 (buffer-substring-no-properties))))
2799     (lj)
2800     (insert blog)))
2801
2802 (defun lj-blog-region (beg end &optional noformat)
2803   "Compose LJ entry using content of region BEG - END.
2804
2805 With optional prefix arg, NOFORMAT, dont' attempt to convert the text
2806 to HTML."
2807   (interactive "r\nP")
2808   (let ((blog (buffer-substring beg end)))
2809     (unless current-prefix-arg
2810       (with-temp-buffer
2811         (insert blog)
2812         (lj-text-to-html (point-min) (point-max))
2813         (setq blog (buffer-substring-no-properties))))
2814     (lj)
2815     (insert blog)))
2816
2817 (defun lj-edit-draft (draft)
2818   "Edit an existing draft previously saved from lj.el."
2819   (interactive (list
2820                 (read-file-name "Edit draft: "
2821                                 lj-drafts-directory "" t)))
2822   (if (or (zerop (length draft))
2823           (not (file-readable-p (expand-file-name draft))))
2824       (error 'invalid-argument draft)
2825     (switch-to-buffer (find-file-noselect (expand-file-name draft)))
2826     (rename-buffer "*LJ-draft*" 'unique)
2827     (goto-char (point-min))
2828     (make-extent (point) (point-at-eol))
2829     (lj-update-userpic-glyph
2830      (expand-file-name (lj-header-content "userpic")
2831                        lj-userpic-directory))
2832     (re-search-forward lj-header-separator nil t)
2833     (forward-line -1)
2834     (set-extent-property
2835      (make-extent (point-at-bol) (1+ (point-at-eol))) 'invisible t)
2836     (lj-goto-body)
2837     (lj-mode)))
2838
2839 (provide 'lj)
2840
2841 ;; On-load actions
2842 (and (file-exists-p lj-tags-file)
2843      (load-file lj-tags-file))
2844 (and (file-exists-p lj-groups-file)
2845      (load-file lj-groups-file))
2846 (and (file-exists-p lj-moods-file)
2847      (load-file lj-moods-file))
2848 (and (file-exists-p lj-pickws-file)
2849      (load-file lj-pickws-file))
2850 ;;; lj.el ends here