330320900a15800765b7ae89794857864c5a918d
[elisp/mixi.git] / mixi.el
1 ;; mixi.el --- API library for accessing to mixi
2
3 ;; Copyright (C) 2005,2006 OHASHI Akira
4
5 ;; Author: OHASHI Akira <bg66@koka-in.org>
6 ;; Keywords: hypermedia
7
8 ;; This file is *NOT* a part of Emacs.
9
10 ;; This program is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 2, or (at your option)
13 ;; any later version.
14
15 ;; This program is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with this program; if not, you can either send email to this
22 ;; program's maintainer or write to: The Free Software Foundation,
23 ;; Inc.; 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
24
25 ;;; Commentary:
26
27 ;; API for getting contents:
28 ;;
29 ;;  * mixi-get-friends
30 ;;  * mixi-get-favorites
31 ;;  * mixi-get-logs
32 ;;  * mixi-get-diaries
33 ;;  * mixi-get-new-diaries
34 ;;  * mixi-get-communities
35 ;;  * mixi-get-topics
36 ;;  * mixi-get-new-topics
37 ;;  * mixi-get-comments
38 ;;  * mixi-get-new-comments
39
40 ;; Example:
41 ;;
42 ;; Display only the first page of new diaries like a mail format.
43 ;;
44 ;; (let ((mixi-new-diary-max-pages 1)
45 ;;       (buffer (generate-new-buffer "*temp*"))
46 ;;       (format "%Y/%m/%d %H:%M"))
47 ;;   (pop-to-buffer buffer)
48 ;;   (mapc (lambda (diary)
49 ;;        (let ((subject (mixi-diary-title diary))
50 ;;              ;; Don't get owner's nick at first for omitting a useless
51 ;;              ;; retrieval.
52 ;;              (from (mixi-friend-nick (mixi-diary-owner diary)))
53 ;;              (date (format-time-string format (mixi-diary-time diary)))
54 ;;              (body (mixi-diary-content diary)))
55 ;;          (insert "From: " from "\n"
56 ;;                  "Subject: " subject "\n"
57 ;;                  "Date: " date "\n\n"
58 ;;                  body "\n\n")))
59 ;;      (mixi-get-new-diaries))
60 ;;   (set-buffer-modified-p nil)
61 ;;   (setq buffer-read-only t)
62 ;;   (goto-char (point-min)))
63 ;;
64 ;; Display only the first page of new diaries including all comments like a
65 ;; mail format.  Comments are displayed like a reply mail.
66 ;;
67 ;; (let ((mixi-new-diary-max-pages 1)
68 ;;       (buffer (generate-new-buffer "*temp*"))
69 ;;       (format "%Y/%m/%d %H:%M"))
70 ;;   (pop-to-buffer buffer)
71 ;;   (mapc (lambda (diary)
72 ;;        (let ((subject (mixi-diary-title diary))
73 ;;              ;; Don't get owner's nick at first for omitting a useless
74 ;;              ;; retrieval.
75 ;;              (from (mixi-friend-nick (mixi-diary-owner diary)))
76 ;;              (date (format-time-string format (mixi-diary-time diary)))
77 ;;              (body (mixi-diary-content diary)))
78 ;;          (insert "From: " from "\n"
79 ;;                  "Subject: " subject "\n"
80 ;;                  "Date: " date "\n\n"
81 ;;                  body "\n\n")
82 ;;          (mapc (lambda (comment)
83 ;;                  (let ((from (mixi-friend-nick
84 ;;                               (mixi-comment-owner comment)))
85 ;;                        (subject (concat "Re: " subject))
86 ;;                        (date (format-time-string
87 ;;                               format (mixi-comment-time comment)))
88 ;;                        (body (mixi-comment-content comment)))
89 ;;                    (insert "From: " from "\n"
90 ;;                            "Subject: " subject "\n"
91 ;;                            "Date: " date "\n\n"
92 ;;                            body "\n\n")))
93 ;;                (mixi-get-comments diary))))
94 ;;      (mixi-get-new-diaries))
95 ;;   (set-buffer-modified-p nil)
96 ;;   (setq buffer-read-only t)
97 ;;   (goto-char (point-min)))
98
99 ;;; Code:
100
101 (condition-case nil
102     (require 'url "url")
103   (error))
104
105 (condition-case nil
106     (require 'w3m "w3m")
107   (error))
108
109 (eval-when-compile (require 'cl))
110
111 (defgroup mixi nil
112   "API library for accessing to mixi."
113   :group 'hypermedia)
114
115 (defcustom mixi-url "http://mixi.jp"
116   "*The URL of mixi."
117   :type 'string
118   :group 'mixi)
119
120 (defcustom mixi-coding-system 'euc-jp
121   "*Coding system for mixi."
122   :type 'coding-system
123   :group 'mixi)
124
125 (defcustom mixi-retrieve-function
126   (if (fboundp 'url-retrieve-synchronously)
127       'mixi-w3-retrieve 'mixi-w3m-retrieve)
128   "*The function for retrieving."
129   :type '(choice (const :tag "Using w3" mixi-w3-retrieve)
130                  (const :tag "Using w3m" mixi-w3m-retrieve)
131                  (const :tag "Using curl" mixi-curl-retrieve)
132                  (function :format "Other function: %v\n" :size 0))
133   :group 'mixi)
134
135 (defcustom mixi-curl-program "curl"
136   "*The program name of `curl'."
137   :type 'file
138   :group 'mixi)
139
140 (defcustom mixi-curl-cookie-file (expand-file-name "~/.mixi-cookies.txt")
141   "*The location of cookie file created by `curl'."
142   :type 'file
143   :group 'mixi)
144
145 (defcustom mixi-default-email nil
146   "*Default E-mail address that is used to login automatically."
147   :type '(choice (string :tag "E-mail address")
148                  (const :tag "Asked when it is necessary" nil))
149   :group 'mixi)
150
151 (defcustom mixi-default-password nil
152   "*Default password that is used to login automatically."
153   :type '(choice (string :tag "Password")
154                  (const :tag "Asked when it is necessary" nil))
155   :group 'mixi)
156
157 (defcustom mixi-accept-adult-contents t
158   "*If non-nil, accept to access to the adult contents."
159   :type 'boolean
160   :group 'mixi)
161
162 (defcustom mixi-continuously-access-interval 4.0
163   "*Time interval between each mixi access.
164 Increase this value when unexpected error frequently occurs."
165   :type 'number
166   :group 'mixi)
167
168 (defcustom mixi-cache-expires 3600
169   "*Seconds for expiration of a cached object."
170   :type '(choice (integer :tag "Expired seconds")
171                  (const :tag "Don't expire" nil))
172   :group 'mixi)
173
174 ;; FIXME: Not implemented.
175 (defcustom mixi-cache-use-file t
176   "*If non-nil, caches are saved to files."
177   :type 'boolean
178   :group 'mixi)
179
180 (defcustom mixi-cache-directory (expand-file-name "~/.mixi")
181   "*Where to look for cache files."
182   :type 'directory
183   :group 'mixi)
184
185 (defvar mixi-me nil)
186
187 ;; Utilities.
188 (defmacro mixi-message (string)
189   `(concat "[mixi] " ,string))
190
191 (defconst mixi-message-adult-contents
192   "¤³¤Î¥Ú¡¼¥¸¤«¤éÀè¤Ï¥¢¥À¥ë¥È¡ÊÀ®¿Í¸þ¤±¡Ë¥³¥ó¥Æ¥ó¥Ä¤¬´Þ¤Þ¤ì¤Æ¤¤¤Þ¤¹¡£<br>
193 ±ÜÍ÷¤ËƱ°Õ¤µ¤ì¤¿Êý¤Î¤ß¡¢Àè¤Ø¤ª¿Ê¤ß¤¯¤À¤µ¤¤¡£")
194 (defconst mixi-message-continuously-accessing
195   "°ÂÄꤷ¤Æ¥µ¥¤¥È¤Î±¿±Ä¤ò¤ª¤³¤Ê¤¦°Ù¡¢´Ö³Ö¤ò¶õ¤±¤Ê¤¤Ï¢Â³Åª¤Ê¥Ú¡¼¥¸¤ÎÁ«°Ü¡¦¹¹<br>
196 ¿·¤ÏÀ©¸Â¤µ¤»¤Æ¤¤¤¿¤À¤¤¤Æ¤ª¤ê¤Þ¤¹¡£¤´ÌÂÏǤò¤ª¤«¤±¤¤¤¿¤·¤Þ¤¹¤¬¡¢¤·¤Ð¤é¤¯¤ª<br>
197 ÂÔ¤Á¤¤¤¿¤À¤¤¤Æ¤«¤éÁàºî¤ò¤ª¤³¤Ê¤Ã¤Æ¤¯¤À¤µ¤¤¡£")
198 (defconst mixi-warning-continuously-accessing
199   "´Ö³Ö¤ò¶õ¤±¤Ê¤¤Ï¢Â³Åª¤Ê¥Ú¡¼¥¸¤ÎÁ«°Ü¡¦¹¹¿·¤òÉÑÈˤˤª¤³¤Ê¤ï¤ì¤Æ¤¤¤ë¤³¤È¤¬¸«<br>
200 ¼õ¤±¤é¤ì¤Þ¤·¤¿¤Î¤Ç¡¢°ì»þŪ¤ËÁàºî¤òÄä»ß¤µ¤»¤Æ¤¤¤¿¤À¤­¤Þ¤¹¡£¿½¤·Ìõ¤´¤¶¤¤¤Þ<br>
201 ¤»¤ó¤¬¡¢¤·¤Ð¤é¤¯¤Î´Ö¤ªÂÔ¤Á¤¯¤À¤µ¤¤¡£")
202
203 (defun mixi-retrieve-1 (buffer url &optional post-data)
204   (when (string-match mixi-message-adult-contents buffer)
205     (if mixi-accept-adult-contents
206         (setq buffer (funcall mixi-retrieve-function url "submit=agree"))
207       (setq buffer (funcall mixi-retrieve-function (concat url "?")))))
208   (when (string-match mixi-warning-continuously-accessing buffer)
209     (error (mixi-message "Continuously accessing")))
210   (if (not (string-match mixi-message-continuously-accessing buffer))
211       buffer
212     (message (mixi-message "Waiting for continuously accessing..."))
213     (sit-for mixi-continuously-access-interval)
214     (funcall mixi-retrieve-function url post-data)))
215
216 (defun mixi-w3-retrieve (url &optional post-data)
217   "Retrieve the URL and return gotten strings."
218   (if post-data
219       (progn
220         (setq url-request-method "POST")
221         (setq url-request-data post-data))
222     (setq url-request-method "GET")
223     (setq url-request-data nil))
224   (let* ((url (url-expand-file-name url mixi-url))
225          (buffer (url-retrieve-synchronously url))
226          ret)
227     (unless (bufferp buffer)
228       (error (mixi-message "Cannot retrieve")))
229     (with-current-buffer buffer
230       (goto-char (point-min))
231       (if (re-search-forward "HTTP/[0-9.]+ 302 Moved" nil t)
232           (if (re-search-forward
233                (concat "Location: " mixi-url "\\(.+\\)") nil t)
234               (setq ret (mixi-w3-retrieve (match-string 1) post-data))
235             (setq ret (mixi-w3-retrieve "/home.pl" post-data)))
236         (unless (re-search-forward "HTTP/[0-9.]+ 200 OK" nil t)
237           (error (mixi-message "Cannot retrieve")))
238         (search-forward "\n\n")
239         (setq ret (mm-decode-coding-string
240                    (buffer-substring-no-properties (point) (point-max))
241                    mixi-coding-system))
242         (kill-buffer buffer)
243         (setq ret (mixi-retrieve-1 ret url post-data))))
244     ret))
245
246 (defun mixi-w3m-retrieve (url &optional post-data)
247   "Retrieve the URL and return gotten strings."
248   (let ((url (w3m-expand-url url mixi-url)))
249     (with-temp-buffer
250       (if (not (string= (w3m-retrieve url nil nil post-data) "text/html"))
251           (error (mixi-message "Cannot retrieve"))
252         (w3m-decode-buffer url)
253         (let ((ret (buffer-substring-no-properties (point-min) (point-max))))
254           (mixi-retrieve-1 ret url post-data))))))
255
256 (defun mixi-curl-retrieve (url &optional post-data)
257   "Retrieve the URL and return gotten strings."
258   (with-temp-buffer
259     (let ((orig-mode (default-file-modes))
260           process ret)
261       (unwind-protect
262           (progn
263             (set-default-file-modes 448)
264             (setq process
265                   (apply #'start-process "curl" (current-buffer)
266                          mixi-curl-program
267                          (append (if post-data '("-d" "@-"))
268                                  (list "-i" "-L" "-s"
269                                        "-b" mixi-curl-cookie-file
270                                        "-c" mixi-curl-cookie-file
271                                        (concat mixi-url url)))))
272             (set-process-sentinel process #'ignore))
273         (set-default-file-modes orig-mode))
274       (when post-data
275         (process-send-string process (concat post-data "\n"))
276         (process-send-eof process))
277       (while (eq (process-status process) 'run)
278         (accept-process-output process 1))
279       (goto-char (point-min))
280       (while (looking-at "HTTP/[0-9]+\\.[0-9]+ [13][0-9][0-9]")
281         (delete-region (point) (re-search-forward "\r?\n\r?\n")))
282       (unless (looking-at "HTTP/[0-9]+\\.[0-9]+ 200")
283         (error (mixi-message "Cannot retrieve")))
284       (delete-region (point) (re-search-forward "\r?\n\r?\n"))
285       (setq ret (decode-coding-string (buffer-substring (point) (point-max))
286                                       mixi-coding-system))
287       (mixi-retrieve-1 ret url post-data))))
288
289 (defconst mixi-my-id-regexp
290   "<a href=\"add_diary\\.pl\\?id=\\([0-9]+\\)")
291
292 (defun mixi-login (&optional email password)
293   "Login to mixi."
294   (when (and (eq mixi-retrieve-function 'mixi-w3m-retrieve)
295              (not w3m-use-cookies))
296     (error
297      (mixi-message
298       "Require to accept cookies.  Please set `w3m-use-cookies' to t.")))
299   (let ((email (or email mixi-default-email
300                    (read-from-minibuffer (mixi-message "Login Email: "))))
301         (password (or password mixi-default-password
302                       (read-passwd (mixi-message "Login Password: ")))))
303     (let ((buffer (funcall mixi-retrieve-function "/login.pl"
304                            (concat "email=" email
305                                    "&password=" password
306                                    "&next_url=/home.pl"
307                                    "&sticky=on"))))
308       (unless (string-match "url=/check\\.pl\\?n=" buffer)
309         (error (mixi-message "Cannot login")))
310       (setq buffer (funcall mixi-retrieve-function "/check.pl?n=home.pl"))
311       (if (string-match mixi-my-id-regexp buffer)
312           (setq mixi-me (mixi-make-friend (match-string 1 buffer)))
313         (error (mixi-message "Cannot login"))))))
314
315 (defun mixi-logout ()
316   (funcall mixi-retrieve-function "/logout.pl"))
317
318 (defmacro with-mixi-retrieve (url &rest body)
319   `(let (buffer)
320      (when ,url
321        (setq buffer (funcall mixi-retrieve-function ,url))
322        (when (string-match "login.pl" buffer)
323          (mixi-login)
324          (setq buffer (funcall mixi-retrieve-function ,url))))
325      ,@body))
326 (put 'with-mixi-retrieve 'lisp-indent-function 'defun)
327
328 (defun mixi-get-matched-items (url max-pages regexp)
329   "Get matched items to REGEXP in URL."
330   (let ((page 1)
331         ids)
332     (catch 'end
333       (while (or (null max-pages) (<= page max-pages))
334         (with-mixi-retrieve (format url page)
335           (let ((pos 0))
336             (while (string-match regexp buffer pos)
337               (let ((num 1)
338                     list)
339                 (while (match-string num buffer)
340                   (setq list (cons (match-string num buffer) list))
341                   (incf num))
342                 (setq ids (cons (reverse list) ids))
343                 (setq pos (match-end (1- num)))))
344             (when (eq pos 0)
345               (throw 'end ids))))
346         (incf page)))
347     ;; FIXME: Sort? Now order by newest.
348     (reverse ids)))
349
350 ;; stolen (and modified) from shimbun.el
351 (defun mixi-remove-markup (string)
352   "Remove markups from STRING."
353   (with-temp-buffer
354     (insert string)
355     (save-excursion
356       (goto-char (point-min))
357       (while (search-forward "<!--" nil t)
358         (delete-region (match-beginning 0)
359                        (or (search-forward "-->" nil t)
360                            (point-max))))
361       (goto-char (point-min))
362       (while (re-search-forward "<[^>]+>" nil t)
363         (replace-match "" t t))
364       (goto-char (point-min))
365       (while (re-search-forward "\r" nil t)
366         (replace-match "\n" t t)))
367     (buffer-string)))
368
369 ;; Cache.
370
371 (defun mixi-cache-expired-p (object)
372   "Whether a cache of OBJECT is expired."
373   ;; FIXME: Use method instead of `(aref (cdr object) 0)'.
374   (let ((timestamp (aref (cdr object) 0)))
375     (unless (or (null mixi-cache-expires)
376                  (null timestamp))
377       (time-less-p (time-add timestamp
378                              (seconds-to-time mixi-cache-expires))
379                    (current-time)))))
380
381 (defun mixi-make-cache (key value table)
382   "Make a cache object and return it."
383   (let ((cache (gethash key table)))
384     (if (and cache (not (mixi-cache-expired-p cache)))
385         cache
386       (puthash key value table))))
387
388 ;; Object.
389 (defconst mixi-object-prefix "mixi-")
390
391 (defmacro mixi-object-class (object)
392   `(car-safe ,object))
393
394 (defmacro mixi-object-p (object)
395   `(eq (string-match (concat "^" mixi-object-prefix)
396                      (symbol-name (mixi-object-class ,object))) 0))
397
398 (defun mixi-object-name (object)
399   "Return the name of OBJECT."
400   (unless (mixi-object-p object)
401     (signal 'wrong-type-argument (list 'mixi-object-p object)))
402   (let ((class (mixi-object-class object)))
403     (substring (symbol-name class) (length mixi-object-prefix))))
404
405 (defun mixi-object-id (object)
406   "Return the id of OBJECT."
407   (unless (mixi-object-p object)
408     (signal 'wrong-type-argument (list 'mixi-object-p object)))
409   (let ((func (intern (concat mixi-object-prefix
410                               (mixi-object-name object) "-id"))))
411     (funcall func object)))
412
413 ;; Friend object.
414 (defvar mixi-friend-cache (make-hash-table :test 'equal))
415 (defun mixi-make-friend (id &optional nick)
416   "Return a friend object."
417   (mixi-make-cache id (cons 'mixi-friend (vector nil id nick nil nil nil nil
418                                                  nil nil nil nil nil nil nil))
419                    mixi-friend-cache))
420
421 (defun mixi-make-me ()
422   (unless mixi-me
423     (with-mixi-retrieve "/home.pl"
424       (if (string-match mixi-my-id-regexp buffer)
425           (setq mixi-me (mixi-make-friend (match-string 1 buffer)))
426         (signal 'error (list 'who-am-i)))))
427   mixi-me)
428
429 (defmacro mixi-friend-p (friend)
430   `(eq (mixi-object-class ,friend) 'mixi-friend))
431
432 (defmacro mixi-friend-page (friend)
433   `(concat "/show_friend.pl?id=" (mixi-friend-id ,friend)))
434
435 (defconst mixi-friend-nick-regexp
436   "<img alt=\"\\*\" src=\"http://img\\.mixi\\.jp/img/dot0\\.gif\" width=\"1\" height=\"5\"><br>\n\\(.*\\)¤µ¤ó([0-9]+)")
437 (defconst mixi-friend-name-sex-regexp
438   "<td BGCOLOR=#F2DDB7 WIDTH=80 NOWRAP><font COLOR=#996600>̾\\(&nbsp;\\| \\)Á°</font></td>\n+<td WIDTH=345>\\([^<]+\\) (\\([Ã˽÷]\\)À­)</td>")
439 (defconst mixi-friend-address-regexp
440   "<td BGCOLOR=#F2DDB7><font COLOR=#996600>¸½½»½ê</font></td>\n<td>\\(.+\\)\\(\n.+\n\\)?</td></tr>")
441 (defconst mixi-friend-age-regexp
442   "<td BGCOLOR=#F2DDB7><font COLOR=#996600>ǯ\\(&nbsp;\\| \\)Îð</font></td>\n<td>\\([0-9]+\\)ºÐ\\(\n.+\n\\)?</td></tr>")
443 (defconst mixi-friend-birthday-regexp
444   "<td BGCOLOR=#F2DDB7><font COLOR=#996600>ÃÂÀ¸Æü</font></td>\n<td>\\([0-9]+\\)·î\\([0-9]+\\)Æü\\(\n.+\n\\)?</td></tr>")
445 (defconst mixi-friend-blood-type-regexp
446   "<td BGCOLOR=#F2DDB7><font COLOR=#996600>·ì±Õ·¿</font></td>\n<td>\\([ABO]B?\\)·¿\\(\n\n\\)?</td></tr>")
447 (defconst mixi-friend-birthplace-regexp
448   "<td BGCOLOR=#F2DDB7><font COLOR=#996600>½Ð¿ÈÃÏ</font>\n?</td>\n<td>\\(.+\\)\\(\n.+\n\\)?</td></tr>")
449 (defconst mixi-friend-hobby-regexp
450   "<td BGCOLOR=#F2DDB7><font COLOR=#996600>¼ñ\\(&nbsp;\\| \\)Ì£</font></td>\n<td>\\(.+\\)</td></tr>")
451 (defconst mixi-friend-job-regexp
452   "<td BGCOLOR=#F2DDB7><font COLOR=#996600>¿¦\\(&nbsp;\\| \\)¶È</font></td>\n<td>\\(.+\\)\\(\n.+\n\\)?</td></tr>")
453 (defconst mixi-friend-organization-regexp
454   "<td BGCOLOR=#F2DDB7><font COLOR=#996600>½ê\\(&nbsp;\\| \\)°</font></td>\n<td[^>]*>\\(.+\\)\\(\n.+\n\\)?</td></tr>")
455 (defconst mixi-friend-profile-regexp
456   "<td BGCOLOR=#F2DDB7><font COLOR=#996600>¼«¸Ê¾Ò²ð</font></td>\n<td CLASS=h120>\\(.+\\)</td></tr>")
457
458 (defun mixi-friend-realize (friend)
459   "Realize a FRIEND."
460   ;; FIXME: Check a expiration of cache?
461   (unless (mixi-friend-realize-p friend)
462     (let (buf)
463       (with-mixi-retrieve (mixi-friend-page friend)
464         (setq buf buffer))
465       (if (string-match mixi-friend-nick-regexp buf)
466           (mixi-friend-set-nick friend (match-string 1 buf))
467         (signal 'error (list 'cannot-find-nick friend)))
468       ;; For getting my profile.
469       (unless (string-match mixi-friend-name-sex-regexp buf)
470         (with-mixi-retrieve "/show_profile.pl"
471           (setq buf buffer)))
472       (if (string-match mixi-friend-name-sex-regexp buf)
473           (progn
474             (mixi-friend-set-name friend (match-string 2 buf))
475             (mixi-friend-set-sex friend
476                                  (if (string= (match-string 3 buf) "ÃË")
477                                      'male 'female)))
478         (signal 'error (list 'cannot-find-name-or-sex friend)))
479       (when (string-match mixi-friend-address-regexp buf)
480         (mixi-friend-set-address friend (match-string 1 buf)))
481       (when (string-match mixi-friend-age-regexp buf)
482         (mixi-friend-set-age
483          friend (string-to-number (match-string 2 buf))))
484       (when (string-match mixi-friend-birthday-regexp buf)
485         (mixi-friend-set-birthday
486          friend (list (string-to-number (match-string 1 buf))
487                       (string-to-number (match-string 2 buf)))))
488       (when (string-match mixi-friend-blood-type-regexp buf)
489         (mixi-friend-set-blood-type friend (intern (match-string 1 buf))))
490       (when (string-match mixi-friend-birthplace-regexp buf)
491         (mixi-friend-set-birthplace friend (match-string 1 buf)))
492       (when (string-match mixi-friend-hobby-regexp buf)
493         (mixi-friend-set-hobby
494          friend (split-string (match-string 2 buf) ", ")))
495       (when (string-match mixi-friend-job-regexp buf)
496         (mixi-friend-set-job friend (match-string 2 buf)))
497       (when (string-match mixi-friend-organization-regexp buf)
498         (mixi-friend-set-organization friend (match-string 2 buf)))
499       (when (string-match mixi-friend-profile-regexp buf)
500         (mixi-friend-set-profile
501          friend (mixi-remove-markup (match-string 1 buf)))))
502     (mixi-friend-touch friend)))
503
504 (defun mixi-friend-realize-p (friend)
505   "Return the timestamp of FRIEND."
506   (unless (mixi-friend-p friend)
507     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
508   (aref (cdr friend) 0))
509
510 (defun mixi-friend-id (friend)
511   "Return the id of FRIEND."
512   (unless (mixi-friend-p friend)
513     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
514   (aref (cdr friend) 1))
515
516 (defun mixi-friend-nick (friend)
517   "Return the nick of FRIEND."
518   (unless (mixi-friend-p friend)
519     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
520   (unless (aref (cdr friend) 2)
521     (mixi-friend-realize friend))
522   (aref (cdr friend) 2))
523
524 (defun mixi-friend-name (friend)
525   "Return the name of FRIEND."
526   (unless (mixi-friend-p friend)
527     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
528   (mixi-friend-realize friend)
529   (aref (cdr friend) 3))
530
531 (defun mixi-friend-sex (friend)
532   "Return the sex of FRIEND."
533   (unless (mixi-friend-p friend)
534     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
535   (mixi-friend-realize friend)
536   (aref (cdr friend) 4))
537
538 (defun mixi-friend-address (friend)
539   "Return the address of FRIEND."
540   (unless (mixi-friend-p friend)
541     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
542   (mixi-friend-realize friend)
543   (aref (cdr friend) 5))
544
545 (defun mixi-friend-age (friend)
546   "Return the age of FRIEND."
547   (unless (mixi-friend-p friend)
548     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
549   (mixi-friend-realize friend)
550   (aref (cdr friend) 6))
551
552 (defun mixi-friend-birthday (friend)
553   "Return the birthday of FRIEND."
554   (unless (mixi-friend-p friend)
555     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
556   (mixi-friend-realize friend)
557   (aref (cdr friend) 7))
558
559 (defun mixi-friend-blood-type (friend)
560   "Return the blood type of FRIEND."
561   (unless (mixi-friend-p friend)
562     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
563   (mixi-friend-realize friend)
564   (aref (cdr friend) 8))
565
566 (defun mixi-friend-birthplace (friend)
567   "Return the birthplace of FRIEND."
568   (unless (mixi-friend-p friend)
569     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
570   (mixi-friend-realize friend)
571   (aref (cdr friend) 9))
572
573 (defun mixi-friend-hobby (friend)
574   "Return the hobby of FRIEND."
575   (unless (mixi-friend-p friend)
576     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
577   (mixi-friend-realize friend)
578   (aref (cdr friend) 10))
579
580 (defun mixi-friend-job (friend)
581   "Return the job of FRIEND."
582   (unless (mixi-friend-p friend)
583     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
584   (mixi-friend-realize friend)
585   (aref (cdr friend) 11))
586
587 (defun mixi-friend-organization (friend)
588   "Return the organization of FRIEND."
589   (unless (mixi-friend-p friend)
590     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
591   (mixi-friend-realize friend)
592   (aref (cdr friend) 12))
593
594 (defun mixi-friend-profile (friend)
595   "Return the pforile of FRIEND."
596   (unless (mixi-friend-p friend)
597     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
598   (mixi-friend-realize friend)
599   (aref (cdr friend) 13))
600
601 (defun mixi-friend-touch (friend)
602   "Set the timestamp of FRIEND."
603   (unless (mixi-friend-p friend)
604     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
605   (aset (cdr friend) 0 (current-time)))
606
607 (defun mixi-friend-set-nick (friend nick)
608   "Set the nick of FRIEND."
609   (unless (mixi-friend-p friend)
610     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
611   (aset (cdr friend) 2 nick))
612
613 (defun mixi-friend-set-name (friend name)
614   "Set the name of FRIEND."
615   (unless (mixi-friend-p friend)
616     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
617   (aset (cdr friend) 3 name))
618
619 (defun mixi-friend-set-sex (friend sex)
620   "Set the sex of FRIEND."
621   (unless (mixi-friend-p friend)
622     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
623   (aset (cdr friend) 4 sex))
624
625 (defun mixi-friend-set-address (friend address)
626   "Set the address of FRIEND."
627   (unless (mixi-friend-p friend)
628     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
629   (aset (cdr friend) 5 address))
630
631 (defun mixi-friend-set-age (friend age)
632   "Set the age of FRIEND."
633   (unless (mixi-friend-p friend)
634     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
635   (aset (cdr friend) 6 age))
636
637 (defun mixi-friend-set-birthday (friend birthday)
638   "Set the birthday of FRIEND."
639   (unless (mixi-friend-p friend)
640     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
641   (aset (cdr friend) 7 birthday))
642
643 (defun mixi-friend-set-blood-type (friend blood-type)
644   "Set the blood type of FRIEND."
645   (unless (mixi-friend-p friend)
646     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
647   (aset (cdr friend) 8 blood-type))
648
649 (defun mixi-friend-set-birthplace (friend birthplace)
650   "Set the birthplace of FRIEND."
651   (unless (mixi-friend-p friend)
652     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
653   (aset (cdr friend) 9 birthplace))
654
655 (defun mixi-friend-set-hobby (friend hobby)
656   "Set the hobby of FRIEND."
657   (unless (mixi-friend-p friend)
658     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
659   (aset (cdr friend) 10 hobby))
660
661 (defun mixi-friend-set-job (friend job)
662   "Set the job of FRIEND."
663   (unless (mixi-friend-p friend)
664     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
665   (aset (cdr friend) 11 job))
666
667 (defun mixi-friend-set-organization (friend organization)
668   "Set the organization of FRIEND."
669   (unless (mixi-friend-p friend)
670     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
671   (aset (cdr friend) 12 organization))
672
673 (defun mixi-friend-set-profile (friend profile)
674   "Set the profile of FRIEND."
675   (unless (mixi-friend-p friend)
676     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
677   (aset (cdr friend) 13 profile))
678
679 (defmacro mixi-friend-list-page (&optional friend)
680   `(concat "/list_friend.pl?page=%d"
681            (when ,friend (concat "&id=" (mixi-friend-id ,friend)))))
682
683 (defconst mixi-friend-list-id-regexp
684   "<a href=show_friend\\.pl\\?id=\\([0-9]+\\)")
685 (defconst mixi-friend-list-nick-regexp
686   "<td valign=middle>\\(.+\\)¤µ¤ó([0-9]+)<br />")
687
688 (defvar mixi-friend-max-pages 10)
689 (defun mixi-get-friends (&optional friend)
690   "Get friends of FRIEND."
691   (unless (or (null friend) (mixi-friend-p friend))
692     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
693   (let ((ids (mixi-get-matched-items (mixi-friend-list-page friend)
694                                      mixi-friend-max-pages
695                                      mixi-friend-list-id-regexp))
696         (nicks (mixi-get-matched-items (mixi-friend-list-page friend)
697                                        mixi-friend-max-pages
698                                        mixi-friend-list-nick-regexp)))
699     (let ((index 0)
700           ret)
701       (while (< index (length ids))
702         (setq ret (cons (mixi-make-friend (nth 0 (nth index ids))
703                                           (nth 0 (nth index nicks))) ret))
704         (incf index))
705       (reverse ret))))
706
707 ;; Favorite.
708 (defmacro mixi-favorite-list-page ()
709   `(concat "/list_bookmark.pl?page=%d"))
710
711 (defconst mixi-favorite-list-id-regexp
712   "<td ALIGN=center BGCOLOR=#FDF9F2 width=330><a href=\"show_friend\\.pl\\?id=\\([0-9]+\\)\">")
713 (defconst mixi-favorite-list-nick-regexp
714   "<td BGCOLOR=#FDF9F2><font COLOR=#996600>̾&nbsp;&nbsp;Á°</font></td>
715 <td COLSPAN=2 BGCOLOR=#FFFFFF>\\(.+\\)</td></tr>")
716
717 (defvar mixi-favorite-max-pages nil)
718 (defun mixi-get-favorites ()
719   "Get favorites."
720   (let ((ids (mixi-get-matched-items (mixi-favorite-list-page)
721                                      mixi-favorite-max-pages
722                                      mixi-favorite-list-id-regexp))
723         (nicks (mixi-get-matched-items (mixi-favorite-list-page)
724                                        mixi-favorite-max-pages
725                                        mixi-favorite-list-nick-regexp)))
726     (let ((index 0)
727           ret)
728       (while (< index (length ids))
729         (setq ret (cons (mixi-make-friend (nth 0 (nth index ids))
730                                           (nth 0 (nth index nicks))) ret))
731         (incf index))
732       (reverse ret))))
733
734 ;; Log object.
735 (defun mixi-make-log (friend time)
736   "Return a log object."
737   (cons 'mixi-log (vector friend time)))
738
739 (defmacro mixi-log-p (log)
740   `(eq (mixi-object-class ,log) 'mixi-log))
741
742 (defun mixi-log-friend (log)
743   "Return the friend of LOG."
744   (unless (mixi-log-p log)
745     (signal 'wrong-type-argument (list 'mixi-log-p log)))
746   (aref (cdr log) 0))
747
748 (defun mixi-log-time (log)
749   "Return the time of LOG."
750   (unless (mixi-log-p log)
751     (signal 'wrong-type-argument (list 'mixi-log-p log)))
752   (aref (cdr log) 1))
753
754 (defmacro mixi-log-list-page ()
755   `(concat "/show_log.pl"))
756
757 (defconst mixi-log-list-regexp
758   "\\([0-9]+\\)ǯ\\([0-9]+\\)·î\\([0-9]+\\)Æü \\([0-9]+\\):\\([0-9]+\\) <a href=\"show_friend\\.pl\\?id=\\([0-9]+\\)\">\\(.+\\)</a><br>")
759
760 (defvar mixi-log-max-pages 1)
761 (defun mixi-get-logs ()
762   "Get logs."
763   (let ((items (mixi-get-matched-items (mixi-log-list-page)
764                                        mixi-log-max-pages
765                                        mixi-log-list-regexp)))
766     (mapcar (lambda (item)
767               (mixi-make-log (mixi-make-friend (nth 5 item) (nth 6 item))
768                              (encode-time 0
769                                           (string-to-number (nth 4 item))
770                                           (string-to-number (nth 3 item))
771                                           (string-to-number (nth 2 item))
772                                           (string-to-number (nth 1 item))
773                                           (string-to-number (nth 0 item)))))
774             items)))
775
776 ;; Diary object.
777 (defvar mixi-diary-cache (make-hash-table :test 'equal))
778 (defun mixi-make-diary (owner id)
779   "Return a diary object."
780   (let ((owner (or owner (mixi-make-me))))
781     (mixi-make-cache (list (mixi-friend-id owner) id)
782                      (cons 'mixi-diary (vector nil owner id nil nil nil))
783                      mixi-diary-cache)))
784
785 (defmacro mixi-diary-p (diary)
786   `(eq (mixi-object-class ,diary) 'mixi-diary))
787
788 (defmacro mixi-diary-page (diary)
789   `(concat "/view_diary.pl?id=" (mixi-diary-id ,diary)
790            "&owner_id=" (mixi-friend-id (mixi-diary-owner ,diary))))
791
792 ;; FIXME: Remove `¤µ¤ó'.
793 (defconst mixi-diary-owner-nick-regexp
794   "<td WIDTH=490 background=http://img\\.mixi\\.jp/img/bg_w\\.gif><b><font COLOR=#605048>\\(.+\\)\\(¤µ¤ó\\)?¤ÎÆüµ­</font></b></td>")
795 (defconst mixi-diary-time-regexp
796   "<td ALIGN=center ROWSPAN=2 NOWRAP WIDTH=95 bgcolor=#FFD8B0>\\([0-9]+\\)ǯ\\([0-9]+\\)·î\\([0-9]+\\)Æü<br>\\([0-9]+\\):\\([0-9]+\\)</td>")
797 (defconst mixi-diary-title-regexp
798   "<td BGCOLOR=#FFF4E0 WIDTH=430>&nbsp;\\([^<]+\\)</td></tr>")
799 (defconst mixi-diary-content-regexp
800   "<td CLASS=h12>\\(.+\\)</td></tr>")
801
802 (defun mixi-diary-realize (diary)
803   "Realize a DIARY."
804   ;; FIXME: Check a expiration of cache?
805   (unless (mixi-diary-realize-p diary)
806     (with-mixi-retrieve (mixi-diary-page diary)
807       (if (string-match mixi-diary-owner-nick-regexp buffer)
808           (mixi-friend-set-nick (mixi-diary-owner diary)
809                                 (match-string 1 buffer))
810         (signal 'error (list 'cannot-find-owner-nick diary)))
811       (if (string-match mixi-diary-time-regexp buffer)
812           (mixi-diary-set-time
813            diary (encode-time 0 (string-to-number (match-string 5 buffer))
814                               (string-to-number (match-string 4 buffer))
815                               (string-to-number (match-string 3 buffer))
816                               (string-to-number (match-string 2 buffer))
817                               (string-to-number (match-string 1 buffer))))
818         (signal 'error (list 'cannot-find-time diary)))
819       (if (string-match mixi-diary-title-regexp buffer)
820           (mixi-diary-set-title diary (match-string 1 buffer))
821         (signal 'error (list 'cannot-find-title diary)))
822       (if (string-match mixi-diary-content-regexp buffer)
823           (mixi-diary-set-content diary (mixi-remove-markup
824                                          (match-string 1 buffer)))
825         (signal 'error (list 'cannot-find-content diary))))
826     (mixi-diary-touch diary)))
827
828 (defun mixi-diary-realize-p (diary)
829   "Return the timestamp of DIARY."
830   (unless (mixi-diary-p diary)
831     (signal 'wrong-type-argument (list 'mixi-diary-p diary)))
832   (aref (cdr diary) 0))
833
834 (defun mixi-diary-owner (diary)
835   "Return the owner of DIARY."
836   (unless (mixi-diary-p diary)
837     (signal 'wrong-type-argument (list 'mixi-diary-p diary)))
838   (aref (cdr diary) 1))
839
840 (defun mixi-diary-id (diary)
841   "Return the id of DIARY."
842   (unless (mixi-diary-p diary)
843     (signal 'wrong-type-argument (list 'mixi-diary-p diary)))
844   (aref (cdr diary) 2))
845
846 (defun mixi-diary-time (diary)
847   "Return the time of DIARY."
848   (unless (mixi-diary-p diary)
849     (signal 'wrong-type-argument (list 'mixi-diary-p diary)))
850   (mixi-diary-realize diary)
851   (aref (cdr diary) 3))
852
853 (defun mixi-diary-title (diary)
854   "Return the title of DIARY."
855   (unless (mixi-diary-p diary)
856     (signal 'wrong-type-argument (list 'mixi-diary-p diary)))
857   (mixi-diary-realize diary)
858   (aref (cdr diary) 4))
859
860 (defun mixi-diary-content (diary)
861   "Return the content of DIARY."
862   (unless (mixi-diary-p diary)
863     (signal 'wrong-type-argument (list 'mixi-diary-p diary)))
864   (mixi-diary-realize diary)
865   (aref (cdr diary) 5))
866
867 (defun mixi-diary-touch (diary)
868   "Set the timestamp of DIARY."
869   (unless (mixi-diary-p diary)
870     (signal 'wrong-type-argument (list 'mixi-diary-p diary)))
871   (aset (cdr diary) 0 (current-time)))
872
873 (defun mixi-diary-set-time (diary time)
874   "Set the time of DIARY."
875   (unless (mixi-diary-p diary)
876     (signal 'wrong-type-argument (list 'mixi-diary-p diary)))
877   (aset (cdr diary) 3 time))
878
879 (defun mixi-diary-set-title (diary title)
880   "Set the title of DIARY."
881   (unless (mixi-diary-p diary)
882     (signal 'wrong-type-argument (list 'mixi-diary-p diary)))
883   (aset (cdr diary) 4 title))
884
885 (defun mixi-diary-set-content (diary content)
886   "Set the content of DIARY."
887   (unless (mixi-diary-p diary)
888     (signal 'wrong-type-argument (list 'mixi-diary-p diary)))
889   (aset (cdr diary) 5 content))
890
891 (defmacro mixi-diary-list-page (&optional friend)
892   `(concat "/list_diary.pl?page=%d"
893            (when ,friend (concat "&id=" (mixi-friend-id ,friend)))))
894
895 (defconst mixi-diary-list-regexp
896   "<a href=\"view_diary\\.pl\\?id=\\([0-9]+\\)&owner_id=[0-9]+\">")
897
898 (defvar mixi-diary-max-pages nil)
899 (defun mixi-get-diaries (&optional friend)
900   "Get diaries of FRIEND."
901   (unless (or (null friend) (mixi-friend-p friend))
902     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
903   (let ((items (mixi-get-matched-items (mixi-diary-list-page friend)
904                                        mixi-diary-max-pages
905                                        mixi-diary-list-regexp)))
906     (mapcar (lambda (item)
907               (mixi-make-diary friend (nth 0 item)))
908             items)))
909
910 (defmacro mixi-new-diary-list-page ()
911   `(concat "/new_friend_diary.pl?page=%d"))
912
913 (defconst mixi-new-diary-list-regexp
914   "<a class=\"new_link\" href=view_diary\\.pl\\?id=\\([0-9]+\\)&owner_id=\\([0-9]+\\)>")
915
916 (defvar mixi-new-diary-max-pages nil)
917 (defun mixi-get-new-diaries ()
918   "Get new diaries."
919   (let ((items (mixi-get-matched-items (mixi-new-diary-list-page)
920                                        mixi-new-diary-max-pages
921                                        mixi-new-diary-list-regexp)))
922     (mapcar (lambda (item)
923               (mixi-make-diary (mixi-make-friend (nth 1 item)) (nth 0 item)))
924             items)))
925
926 ;; Community object.
927 (defvar mixi-community-cache (make-hash-table :test 'equal))
928 (defun mixi-make-community (id &optional name)
929   "Return a community object."
930   (mixi-make-cache id (cons 'mixi-community (vector nil id name nil nil nil
931                                                     nil nil nil nil))
932                    mixi-community-cache))
933
934 (defmacro mixi-community-p (community)
935   `(eq (mixi-object-class ,community) 'mixi-community))
936
937 (defmacro mixi-community-page (community)
938   `(concat "/view_community.pl?id=" (mixi-community-id ,community)))
939
940 (defconst mixi-community-nodata-regexp
941   "^¥Ç¡¼¥¿¤¬¤¢¤ê¤Þ¤»¤ó")
942 (defconst mixi-community-name-regexp
943   "<td WIDTH=345>\\(.*\\)</td></tr>")
944 (defconst mixi-community-birthday-regexp
945   "<td BGCOLOR=#F2DDB7><font COLOR=#996600>³«ÀßÆü</font></td>\n<td>\\([0-9]+\\)ǯ\\([0-9]+\\)·î\\([0-9]+\\)Æü</td>")
946 ;; FIXME: Care when the owner has seceded.
947 (defconst mixi-community-owner-regexp
948   "<td BGCOLOR=#F2DDB7><font COLOR=#996600>´ÉÍý¿Í</font></td>\n<td>\n\n<a href=\"\\(home\\.pl\\|show_friend\\.pl\\?id=\\([0-9]+\\)\\)\">\n\\(.+\\)</a>")
949 (defconst mixi-community-category-regexp
950   "<td BGCOLOR=#F2DDB7><font COLOR=#996600>¥«¥Æ¥´¥ê</font></td>\n<td>\\([^<]+\\)</td>")
951 (defconst mixi-community-members-regexp
952   "<td BGCOLOR=#F2DDB7><font COLOR=#996600>¥á¥ó¥Ð¡¼¿ô</font></td>\n<td>\\([0-9]+\\)¿Í</td></tr>")
953 (defconst mixi-community-open-level-regexp
954   "<td BGCOLOR=#F2DDB7><font COLOR=#996600>»²²Ã¾ò·ï¤È<br>¸ø³«¥ì¥Ù¥ë</font></td>
955 <td>\\(.+\\)</td></tr>")
956 (defconst mixi-community-authority-regexp
957   "<td BGCOLOR=#F2DDB7><font COLOR=#996600>¥È¥Ô¥Ã¥¯ºîÀ®¤Î¸¢¸Â</font></td>\n<td>\\(.+\\)</td></tr>")
958 (defconst mixi-community-description-regexp
959   "<td CLASS=h120>\\(.+\\)</td>")
960
961 (defun mixi-community-realize (community)
962   "Realize a COMMUNITY."
963   ;; FIXME: Check a expiration of cache?
964   (unless (mixi-community-realize-p community)
965     (with-mixi-retrieve (mixi-community-page community)
966       (if (string-match mixi-community-nodata-regexp buffer)
967           ;; FIXME: Set all members?
968           (mixi-community-set-name community "¥Ç¡¼¥¿¤¬¤¢¤ê¤Þ¤»¤ó")
969         (if (string-match mixi-community-name-regexp buffer)
970             (mixi-community-set-name community (match-string 1 buffer))
971           (signal 'error (list 'cannot-find-name community)))
972         (if (string-match mixi-community-birthday-regexp buffer)
973             (mixi-community-set-birthday
974              community
975              (encode-time 0 0 0 (string-to-number (match-string 3 buffer))
976                           (string-to-number (match-string 2 buffer))
977                           (string-to-number (match-string 1 buffer))))
978           (signal 'error (list 'cannot-find-birthday community)))
979         (if (string-match mixi-community-owner-regexp buffer)
980             (if (string= (match-string 1 buffer) "home.pl")
981                 (mixi-community-set-owner community (mixi-make-me))
982               (mixi-community-set-owner
983                community (mixi-make-friend (match-string 2 buffer)
984                                            (match-string 3 buffer))))
985           (signal 'error (list 'cannot-find-owner community)))
986         (if (string-match mixi-community-category-regexp buffer)
987             (mixi-community-set-category community (match-string 1 buffer))
988           (signal 'error (list 'cannot-find-category community)))
989         (if (string-match mixi-community-members-regexp buffer)
990             (mixi-community-set-members
991              community (string-to-number (match-string 1 buffer)))
992           (signal 'error (list 'cannot-find-members community)))
993         (if (string-match mixi-community-open-level-regexp buffer)
994             (mixi-community-set-open-level community (match-string 1 buffer))
995           (signal 'error (list 'cannot-find-open-level community)))
996         (if (string-match mixi-community-authority-regexp buffer)
997             (mixi-community-set-authority community (match-string 1 buffer))
998           (signal 'error (list 'cannot-find-authority community)))
999         (if (string-match mixi-community-description-regexp buffer)
1000             (mixi-community-set-description
1001              community (mixi-remove-markup (match-string 1 buffer)))
1002           (signal 'error (list 'cannot-find-description community)))))
1003     (mixi-community-touch community)))
1004
1005 (defun mixi-community-realize-p (community)
1006   "Return the timestamp of COMMUNITY."
1007   (unless (mixi-community-p community)
1008     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1009   (aref (cdr community) 0))
1010
1011 (defun mixi-community-id (community)
1012   "Return the id of COMMUNITY."
1013   (unless (mixi-community-p community)
1014     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1015   (aref (cdr community) 1))
1016
1017 (defun mixi-community-name (community)
1018   "Return the name of COMMUNITY."
1019   (unless (mixi-community-p community)
1020     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1021   (unless (aref (cdr community) 2)
1022     (mixi-community-realize community))
1023   (aref (cdr community) 2))
1024
1025 (defun mixi-community-birthday (community)
1026   "Return the birthday of COMMUNITY."
1027   (unless (mixi-community-p community)
1028     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1029   (mixi-community-realize community)
1030   (aref (cdr community) 3))
1031
1032 (defun mixi-community-owner (community)
1033   "Return the owner of COMMUNITY."
1034   (unless (mixi-community-p community)
1035     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1036   (mixi-community-realize community)
1037   (aref (cdr community) 4))
1038
1039 (defun mixi-community-category (community)
1040   "Return the category of COMMUNITY."
1041   (unless (mixi-community-p community)
1042     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1043   (mixi-community-realize community)
1044   (aref (cdr community) 5))
1045
1046 (defun mixi-community-members (community)
1047   "Return the members of COMMUNITY."
1048   (unless (mixi-community-p community)
1049     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1050   (mixi-community-realize community)
1051   (aref (cdr community) 6))
1052
1053 (defun mixi-community-open-level (community)
1054   "Return the open-level of COMMUNITY."
1055   (unless (mixi-community-p community)
1056     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1057   (mixi-community-realize community)
1058   (aref (cdr community) 7))
1059
1060 (defun mixi-community-authority (community)
1061   "Return the authority of COMMUNITY."
1062   (unless (mixi-community-p community)
1063     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1064   (mixi-community-realize community)
1065   (aref (cdr community) 8))
1066
1067 (defun mixi-community-description (community)
1068   "Return the description of COMMUNITY."
1069   (unless (mixi-community-p community)
1070     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1071   (mixi-community-realize community)
1072   (aref (cdr community) 9))
1073
1074 (defun mixi-community-touch (community)
1075   "Set the timestamp of COMMUNITY."
1076   (unless (mixi-community-p community)
1077     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1078   (aset (cdr community) 0 (current-time)))
1079
1080 (defun mixi-community-set-name (community name)
1081   "Set the name of COMMUNITY."
1082   (unless (mixi-community-p community)
1083     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1084   (aset (cdr community) 2 name))
1085
1086 (defun mixi-community-set-birthday (community birthday)
1087   "Set the birthday of COMMUNITY."
1088   (unless (mixi-community-p community)
1089     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1090   (aset (cdr community) 3 birthday))
1091
1092 (defun mixi-community-set-owner (community owner)
1093   "Set the owner of COMMUNITY."
1094   (unless (mixi-community-p community)
1095     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1096   (unless (mixi-friend-p owner)
1097     (signal 'wrong-type-argument (list 'mixi-friend-p owner)))
1098   (aset (cdr community) 4 owner))
1099
1100 (defun mixi-community-set-category (community category)
1101   "Set the category of COMMUNITY."
1102   (unless (mixi-community-p community)
1103     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1104   (aset (cdr community) 5 category))
1105
1106 (defun mixi-community-set-members (community members)
1107   "Set the name of COMMUNITY."
1108   (unless (mixi-community-p community)
1109     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1110   (aset (cdr community) 6 members))
1111
1112 (defun mixi-community-set-open-level (community open-level)
1113   "Set the name of COMMUNITY."
1114   (unless (mixi-community-p community)
1115     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1116   (aset (cdr community) 7 open-level))
1117
1118 (defun mixi-community-set-authority (community authority)
1119   "Set the name of COMMUNITY."
1120   (unless (mixi-community-p community)
1121     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1122   (aset (cdr community) 8 authority))
1123
1124 (defun mixi-community-set-description (community description)
1125   "Set the name of COMMUNITY."
1126   (unless (mixi-community-p community)
1127     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1128   (aset (cdr community) 9 description))
1129
1130 (defmacro mixi-community-list-page (&optional friend)
1131   `(concat "/list_community.pl?page=%d"
1132            (when ,friend (concat "&id=" (mixi-friend-id ,friend)))))
1133
1134 (defconst mixi-community-list-id-regexp
1135   "<a href=view_community\\.pl\\?id=\\([0-9]+\\)")
1136 (defconst mixi-community-list-name-regexp
1137   "<td valign=middle>\\(.+\\)([0-9]+)</td>")
1138
1139 (defvar mixi-community-max-pages nil)
1140 (defun mixi-get-communities (&optional friend)
1141   "Get communities of FRIEND."
1142   (unless (or (null friend) (mixi-friend-p friend))
1143     (signal 'wrong-type-argument (list 'mixi-friend-p friend)))
1144   (let ((ids (mixi-get-matched-items (mixi-community-list-page friend)
1145                                      mixi-community-max-pages
1146                                      mixi-community-list-id-regexp))
1147         (names (mixi-get-matched-items (mixi-community-list-page friend)
1148                                      mixi-community-max-pages
1149                                      mixi-community-list-name-regexp)))
1150     (let ((index 0)
1151           ret)
1152       (while (< index (length ids))
1153         (setq ret (cons (mixi-make-community (nth 0 (nth index ids))
1154                                              (nth 0 (nth index names))) ret))
1155         (incf index))
1156       (reverse ret))))
1157
1158 ;; Topic object.
1159 (defvar mixi-topic-cache (make-hash-table :test 'equal))
1160 (defun mixi-make-topic (community id)
1161   "Return a topic object."
1162   (mixi-make-cache (list (mixi-community-id community) id)
1163                    (cons 'mixi-topic (vector nil community id nil nil nil nil))
1164                    mixi-topic-cache))
1165
1166 (defmacro mixi-topic-p (topic)
1167   `(eq (mixi-object-class ,topic) 'mixi-topic))
1168
1169 (defmacro mixi-topic-page (topic)
1170   `(concat "/view_bbs.pl?id=" (mixi-topic-id ,topic)
1171            "&comm_id=" (mixi-community-id (mixi-topic-community ,topic))))
1172
1173 (defconst mixi-topic-time-regexp
1174   "<td rowspan=\"3\" width=\"110\" bgcolor=\"#ffd8b0\" align=\"center\" valign=\"top\" nowrap>\\([0-9]+\\)ǯ\\([0-9]+\\)·î\\([0-9]+\\)Æü<br>\\([0-9]+\\):\\([0-9]+\\)</td>")
1175 (defconst mixi-topic-title-regexp
1176   "<td bgcolor=\"#fff4e0\">&nbsp;\\([^<]+\\)</td>")
1177 ;; FIXME: Remove `¤µ¤ó'.
1178 (defconst mixi-topic-owner-regexp
1179   "<td bgcolor=\"#fdf9f2\">&nbsp;<font color=\"#dfb479\"></font>&nbsp;<a href=\"show_friend\\.pl\\?id=\\([0-9]+\\)\">\\(.+\\)\\(¤µ¤ó\\)?</a>")
1180 (defconst mixi-topic-content-regexp
1181   "<td class=\"h120\"><table><tr>\\(.+\\)?</tr></table>\\(.+\\)</td>")
1182
1183 (defun mixi-topic-realize (topic)
1184   "Realize a TOPIC."
1185   ;; FIXME: Check a expiration of cache?
1186   (unless (mixi-topic-realize-p topic)
1187     (with-mixi-retrieve (mixi-topic-page topic)
1188       (if (string-match mixi-topic-time-regexp buffer)
1189           (mixi-topic-set-time
1190            topic (encode-time 0 (string-to-number (match-string 5 buffer))
1191                               (string-to-number (match-string 4 buffer))
1192                               (string-to-number (match-string 3 buffer))
1193                               (string-to-number (match-string 2 buffer))
1194                               (string-to-number (match-string 1 buffer))))
1195         (signal 'error (list 'cannot-find-time topic)))
1196       (if (string-match mixi-topic-title-regexp buffer)
1197           (mixi-topic-set-title topic (match-string 1 buffer))
1198         (signal 'error (list 'cannot-find-title topic)))
1199       (if (string-match mixi-topic-owner-regexp buffer)
1200           (mixi-topic-set-owner topic
1201                                 (mixi-make-friend (match-string 1 buffer)
1202                                                   (match-string 2 buffer)))
1203         (signal 'error (list 'cannot-find-owner topic)))
1204       (if (string-match mixi-topic-content-regexp buffer)
1205           (mixi-topic-set-content topic (mixi-remove-markup
1206                                          (match-string 2 buffer)))
1207         (signal 'error (list 'cannot-find-content topic))))
1208     (mixi-topic-touch topic)))
1209
1210 (defun mixi-topic-realize-p (topic)
1211   "Return the timestamp of TOPIC."
1212   (unless (mixi-topic-p topic)
1213     (signal 'wrong-type-argument (list 'mixi-topic-p topic)))
1214   (aref (cdr topic) 0))
1215
1216 (defun mixi-topic-community (topic)
1217   "Return the community of TOPIC."
1218   (unless (mixi-topic-p topic)
1219     (signal 'wrong-type-argument (list 'mixi-topic-p topic)))
1220   (aref (cdr topic) 1))
1221
1222 (defun mixi-topic-id (topic)
1223   "Return the id of TOPIC."
1224   (unless (mixi-topic-p topic)
1225     (signal 'wrong-type-argument (list 'mixi-topic-p topic)))
1226   (aref (cdr topic) 2))
1227
1228 (defun mixi-topic-time (topic)
1229   "Return the time of TOPIC."
1230   (unless (mixi-topic-p topic)
1231     (signal 'wrong-type-argument (list 'mixi-topic-p topic)))
1232   (mixi-topic-realize topic)
1233   (aref (cdr topic) 3))
1234
1235 (defun mixi-topic-title (topic)
1236   "Return the title of TOPIC."
1237   (unless (mixi-topic-p topic)
1238     (signal 'wrong-type-argument (list 'mixi-topic-p topic)))
1239   (mixi-topic-realize topic)
1240   (aref (cdr topic) 4))
1241
1242 (defun mixi-topic-owner (topic)
1243   "Return the owner of TOPIC."
1244   (unless (mixi-topic-p topic)
1245     (signal 'wrong-type-argument (list 'mixi-topic-p topic)))
1246   (mixi-topic-realize topic)
1247   (aref (cdr topic) 5))
1248
1249 (defun mixi-topic-content (topic)
1250   "Return the content of TOPIC."
1251   (unless (mixi-topic-p topic)
1252     (signal 'wrong-type-argument (list 'mixi-topic-p topic)))
1253   (mixi-topic-realize topic)
1254   (aref (cdr topic) 6))
1255
1256 (defun mixi-topic-touch (topic)
1257   "Set the timestamp of TOPIC."
1258   (unless (mixi-topic-p topic)
1259     (signal 'wrong-type-argument (list 'mixi-topic-p topic)))
1260   (aset (cdr topic) 0 (current-time)))
1261
1262 (defun mixi-topic-set-time (topic time)
1263   "Set the time of TOPIC."
1264   (unless (mixi-topic-p topic)
1265     (signal 'wrong-type-argument (list 'mixi-topic-p topic)))
1266   (aset (cdr topic) 3 time))
1267
1268 (defun mixi-topic-set-title (topic title)
1269   "Set the title of TOPIC."
1270   (unless (mixi-topic-p topic)
1271     (signal 'wrong-type-argument (list 'mixi-topic-p topic)))
1272   (aset (cdr topic) 4 title))
1273
1274 (defun mixi-topic-set-owner (topic owner)
1275   "Set the owner of TOPIC."
1276   (unless (mixi-topic-p topic)
1277     (signal 'wrong-type-argument (list 'mixi-topic-p topic)))
1278   (unless (mixi-friend-p owner)
1279     (signal 'wrong-type-argument (list 'mixi-friend-p owner)))
1280   (aset (cdr topic) 5 owner))
1281
1282 (defun mixi-topic-set-content (topic content)
1283   "Set the content of TOPIC."
1284   (unless (mixi-topic-p topic)
1285     (signal 'wrong-type-argument (list 'mixi-topic-p topic)))
1286   (aset (cdr topic) 6 content))
1287
1288 (defmacro mixi-topic-list-page (community)
1289   `(concat "/list_bbs.pl?page=%d"
1290            "&id=" (mixi-community-id ,community)))
1291
1292 (defconst mixi-topic-list-regexp
1293   "<a href=view_bbs\\.pl\\?id=\\([0-9]+\\)")
1294
1295 (defvar mixi-topic-max-pages nil)
1296 (defun mixi-get-topics (community)
1297   "Get topics of COMMUNITY."
1298   (unless (mixi-community-p community)
1299     (signal 'wrong-type-argument (list 'mixi-community-p community)))
1300   (let ((items (mixi-get-matched-items (mixi-topic-list-page community)
1301                                        mixi-topic-max-pages
1302                                        mixi-topic-list-regexp)))
1303     (mapcar (lambda (item)
1304               (mixi-make-topic community (nth 0 item)))
1305             items)))
1306
1307 (defmacro mixi-new-topic-list-page ()
1308   `(concat "/new_bbs.pl?page=%d"))
1309
1310 (defconst mixi-new-topic-list-regexp
1311   "<a href=\"view_bbs\\.pl\\?id=\\([0-9]+\\)&comment_count=[0-9]+&comm_id=\\([0-9]+\\)\" class=\"new_link\">")
1312
1313 (defvar mixi-new-topic-max-pages nil)
1314 (defun mixi-get-new-topics ()
1315   "Get new topics."
1316   (let ((items (mixi-get-matched-items (mixi-new-topic-list-page)
1317                                        mixi-new-topic-max-pages
1318                                        mixi-new-topic-list-regexp)))
1319     (mapcar (lambda (item)
1320               (mixi-make-topic (mixi-make-community (nth 1 item))
1321                                (nth 0 item)))
1322             items)))
1323
1324 ;; Comment object.
1325 (defun mixi-make-comment (parent owner time content)
1326   "Return a comment object."
1327   (cons 'mixi-comment (vector parent owner time content)))
1328
1329 (defmacro mixi-comment-p (comment)
1330   `(eq (mixi-object-class ,comment) 'mixi-comment))
1331
1332 (defun mixi-comment-parent (comment)
1333   "Return the parent of COMMENT."
1334   (unless (mixi-comment-p comment)
1335     (signal 'wrong-type-argument (list 'mixi-comment-p comment)))
1336   (aref (cdr comment) 0))
1337
1338 (defun mixi-comment-owner (comment)
1339   "Return the owner of COMMENT."
1340   (unless (mixi-comment-p comment)
1341     (signal 'wrong-type-argument (list 'mixi-comment-p comment)))
1342   (aref (cdr comment) 1))
1343
1344 (defun mixi-comment-time (comment)
1345   "Return the time of COMMENT."
1346   (unless (mixi-comment-p comment)
1347     (signal 'wrong-type-argument (list 'mixi-comment-p comment)))
1348   (aref (cdr comment) 2))
1349
1350 (defun mixi-comment-content (comment)
1351   "Return the content of COMMENT."
1352   (unless (mixi-comment-p comment)
1353     (signal 'wrong-type-argument (list 'mixi-comment-p comment)))
1354   (aref (cdr comment) 3))
1355
1356 (defun mixi-diary-comment-list-page (diary)
1357   (concat "/view_diary.pl?page=all"
1358           "&id=" (mixi-diary-id diary)
1359           "&owner_id=" (mixi-friend-id (mixi-diary-owner diary))))
1360
1361 ;; FIXME: Split regexp to time, owner(id and nick) and contents.
1362 (defconst mixi-diary-comment-list-regexp
1363 "<td rowspan=\"2\" align=\"center\" width=\"95\" bgcolor=\"#f2ddb7\" nowrap>
1364 \\([0-9]+\\)ǯ\\([0-9]+\\)·î\\([0-9]+\\)Æü<br>\\([0-9]+\\):\\([0-9]+\\)\\(<br>
1365 <input type=checkbox name=comment_id value=\".+\">
1366 \\|\\)
1367 </td>
1368 <td ALIGN=center BGCOLOR=#FDF9F2 WIDTH=430>
1369 <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" width=\"410\">
1370 <tr>
1371 <td>
1372 <a href=\"show_friend\\.pl\\?id=\\([0-9]+\\)\">\\(.+\\)</a>
1373
1374 </td>
1375 </tr>
1376 </table>
1377 </td>
1378 </tr>
1379 <!-- ËÜʸ : start -->
1380 <tr>
1381 <td bgcolor=\"#ffffff\">
1382 <table BORDER=0 CELLSPACING=0 CELLPADDING=[35] WIDTH=410>
1383 <tr>
1384 <td CLASS=h12>
1385 \\(.+\\)
1386 </td></tr></table>")
1387
1388 (defun mixi-topic-comment-list-page (topic)
1389   (concat "/view_bbs.pl?page=all"
1390           "&id=" (mixi-topic-id topic)
1391           "&comm_id=" (mixi-community-id (mixi-topic-community topic))))
1392
1393 ;; FIXME: Split regexp to time, owner(id and nick) and contents.
1394 (defconst mixi-topic-comment-list-regexp
1395   "<tr valign=\"top\">
1396 <td rowspan=\"2\" width=\"110\" bgcolor=\"#f2ddb7\" align=\"center\" nowrap>
1397 \\([0-9]+\\)ǯ\\([0-9]+\\)·î\\([0-9]+\\)Æü<br>
1398 \\([0-9]+\\):\\([0-9]+\\)<br>
1399 \\(</td>\\)
1400 <td bgcolor=\"#fdf9f2\">&nbsp;<font color=\"#f8a448\">
1401 <b>&nbsp;&nbsp;[0-9]+</b>:</font>&nbsp;
1402
1403 <a href=\"show_friend\\.pl\\?id=\\([0-9]+\\)\">\\(.+\\)</a>
1404
1405
1406
1407 </td>
1408 </tr>
1409 <tr>
1410 <td bgcolor=\"#ffffff\" align=\"center\">
1411 <table border=\"0\" cellspacing=\"0\" cellpadding=\"5\" width=\"500\">
1412 <tr>
1413 <td class=\"h120\">
1414
1415 \\(.+\\)
1416 </td>
1417 </tr>
1418 </table>
1419 </td>
1420 </tr>")
1421
1422 (defun mixi-get-comments (parent)
1423   "Get comments of PARENT."
1424   (unless (mixi-object-p parent)
1425     (signal 'wrong-type-argument (list 'mixi-object-p parent)))
1426   (let* ((name (mixi-object-name parent))
1427          (list-page (intern (concat mixi-object-prefix name
1428                                     "-comment-list-page")))
1429          (regexp (eval (intern (concat mixi-object-prefix name
1430                                        "-comment-list-regexp")))))
1431     (let ((items (mixi-get-matched-items (funcall list-page parent) 1 regexp)))
1432       (mapcar (lambda (item)
1433                 (mixi-make-comment parent (mixi-make-friend
1434                                            (nth 6 item) (nth 7 item))
1435                                    (encode-time
1436                                     0
1437                                     (string-to-number (nth 4 item))
1438                                     (string-to-number (nth 3 item))
1439                                     (string-to-number (nth 2 item))
1440                                     (string-to-number (nth 1 item))
1441                                     (string-to-number (nth 0 item)))
1442                                    (mixi-remove-markup (nth 8 item))))
1443               items))))
1444
1445 (defmacro mixi-new-comment-list-page ()
1446   `(concat "/new_comment.pl?page=%d"))
1447
1448 (defconst mixi-new-comment-list-regexp
1449   "<a href=\"view_diary\\.pl\\?id=\\([0-9]+\\)&owner_id=\\([0-9]+\\)&comment_count=[0-9]+\" class=\"new_link\">")
1450
1451 (defvar mixi-new-comment-max-pages nil)
1452 (defun mixi-get-new-comments ()
1453   "Get new comments."
1454   (let ((items (mixi-get-matched-items (mixi-new-comment-list-page)
1455                                        mixi-new-comment-max-pages
1456                                        mixi-new-comment-list-regexp)))
1457     (mapcar (lambda (item)
1458               (let ((diary (mixi-make-diary
1459                             (mixi-make-friend (nth 1 item))
1460                             (nth 0 item))))
1461                 (mixi-get-comments diary)))
1462             items)))
1463
1464 (provide 'mixi)
1465
1466 ;;; mixi.el ends here