タイムラインコマンドの自作 (xyttr Advent Calendar 14日目)

この記事はxyttr Advent Calendar 2011の記事です。

11日目の記事の最後でチラっと紹介しましたが、今回はxyttr::define-tl-commandマクロの紹介。

リファレンスに今日追加した部分より抜粋

- *macro* define-tl-command (name params &key interactive buffer-name api-func api-params auto-reload hook)

    タイムラインコマンドを定義します。
    
    * name -- コマンド関数名
    * params -- コマンドパラメータ
    * interactive -- パラメータの受け取り方を指定するinteractive-string (paramsがnilの場合は不要)
    * buffer-name -- タイムラインバッファ名
    * api-func -- ツイートデータを取得するためのtimeline系api関数
    * api-params -- api-funcに渡す基本パラメータ
    * auto-reload -- 自動リロード間隔(秒) 省略時は`xyttr:*auto-reload*`の値
    * hook -- タイムライン用バッファ生成後に実行するhook関数(シンボル)

    api-funcは非同期版の関数を指定します。
    api関数を自作する場合はキーワード引数onsuccessとonfailureで
    コールバックを受け取るように定義してください。

xyttrで使えるtimeline系apiについては、public-timeline以外は全て対応するコマンドが定義済みです。

という事で最初の例はpublic-timelineを表示するコマンドを。

(in-package :xyttr)

(define-tl-command xyttr-public ()
  :buffer-name "*tw: public*"
  :api-func #'api-public-timeline-async)

M-x xyttr-public でパブリック・タイムラインが表示されます。

ちなみに説明を見ると分かるんですが、public-timeline APIでは指定ID以降のツイートの取得や指定ID以前のツイートの取得といった機能はサポートされていません。
リロード時や次ページ取得時に取得されるツイートの時系列がおかしい場合があるかもしれないのでご注意を。

次に、api関数を自作してオリジナルタイムラインコマンドを作ってみます。

(in-package :xyttr)

;; ツイートデータ生成関数
(defun make-tweet (status-id name text)
  `(("id" . ,status-id)
    ("user" ("screen_name" . ,name))
    ("text" . ,text)
    ("created_at" . ,(format-date-string "%a %b %d %H:%M:%S %Z %Y"))))

;; fizzbuzz API
(defun api-fizzbuzz (&key since_id max_id onsuccess onfailure)
  (if max_id
      (funcall onsuccess nil)
    (flet ((fb (n) (case (gcd n 15)
		     (15 "FizzBuzz") (5 "Buzz") (3 "Fizz")
		     (t (format nil "~D" n)))))
      (funcall onsuccess
	       (nreverse
		(loop repeat 10
		  for i from (1+ (or since_id 0))
		  collect (make-tweet i "FizzBuzz" (fb i)))))))
  ; xhr::xhr-cancel-ticket以外の非nil値を返すとこけるのでnil
  nil)

(define-tl-command xyttr-fizzbuzz ()
  :buffer-name "*fizzbuzz*"
  :api-func #'api-fizzbuzz)

リロードする度に10件ずつfizzbuzzが表示されます。
http://gyazo.com/64b81302dbbe26cd4d472209f7950cf3.png


define-tl-commandマクロのapi-funcに指定する関数は、以下のキーワード引数を受け取れるように定義して下さい。

  • since_id -- リロード時に渡される、取得済みの一番新しいツイートのID
  • max_id -- 次ページ取得時に渡される、取得済みの一番古いツイートのID
  • oncomplete -- リクエスト成功時に実行する、ツイートのリストを引数に取るコールバック関数
  • onfailure -- リクエスト失敗時に実行する、レスポンス(文字列)、HTTPステータスコード、HTTPレスポンスヘッダ(alist)を引数に取るコールバック関数

この4つのパラメータに加えてapi-paramsに指定した基本パラメータがapi-funcに指定した関数に渡されます。
ちなみにコールバック関数の呼び出しはFizzBuzzの例の通り同期的になっていてもOKです。

この例は全く役に立たない物ですが、readitlaterなどからデータを取って来るAPI関数を用意してコマンドを作ったりすると面白いかもしれません。



以上、define-tl-commandマクロの紹介でした。

Twitter REST API関数の追加 (xyttr Advent Calendar 13日目)

この記事はxyttr Advent Calendar 2011の記事です。

xyttrパッケージには30ほどのTwitter REST API関数が用意されていますが、未対応のAPIがまだ沢山あります。(参照 Documentation | Twitter Developers)

使いたいAPIがxyttrでサポートされてない! という時は、マクロ xyttr::define-api を使うと簡単にAPI関数を追加できます。

リファレンスより抜粋

- *macro* define-api (name params &key auth method apiurl path key)

    Twitter REST API関数を定義します。
    同期版 api-{name} と 非同期版 api-{name}-async の2つの関数が生成され、
    xyttrパッケージよりexportされます。
    
    * name -- 関数名
    * params -- リクエストパラメータ
    * auth -- OAuth認証ヘッダの必要の有無 (省略時は t)
    * method -- HTTPメソッド (省略時は get)
    * apiurl -- リクエスト先ホスト名 (省略時は "api.twitter.com")
    * path -- リソースURLのパス部分 (省略不可)
    * key -- 関数を指定すると、リクエスト結果(jsonリスト)をその関数に通してから返します。

使用例として、現行のxyttr ver1.1.1には用意されてないトレンド関連のAPIを定義してみます。

trends/available API

トレンド情報の取得可能な地域を返すAPIです。
クエリパラメータlat(緯度)とlong(経度)を指定すると、指定座標から近い順でソートした結果を返します。

(in-package :xyttr)

(define-api trends-available (lat long)
  :path "/1/trends/available.json")

このコードで関数api-trends-availableとapi-trends-available-asyncが生成されます。
api-trends-available関数の引数は (&key lat long) となり、-asyncと付いてる方(非同期版)の引数はコールバック用の引数が追加されて (&key lat long onsuccess onfailure oncomplete handler) となります。

  • API関数使用例
(car (xyttr:api-trends-available :lat 35 :long 135))
; =>
; (("countryCode" . "JP")
;  ("woeid" . 1118370)
;  ("url" . "http://where.yahooapis.com/v1/place/1118370")
;  ("name" . "東京")
;  ("placeType" ("name" . "Town") ("code" . 7))
;  ("country" . "Japan")
;  ("parentid" . 23424856))

trends/:woeid API

指定したwoeid (Yahoo! Where On Earth ID) に対応する地域でのトレンドを返すAPIです。

(in-package :xyttr)

(define-api trends (woeid exclude)
  :path (format nil "/1/trends/~A.json" woeid))

このAPIはパス中にwoeidを埋め込む必要があるので、:pathパラメータはformat関数を使ったフォームになっています。

  • API関数使用例
(in-package :xyttr)

;; woeid 一覧
(defvar *woeids*
  (mapcar #'(lambda (l)
              (w/json (woeid name countryCode) l
                (cons (concat countryCode ":" name) woeid)))
          (api-trends-available :lat 35 :long 135)))

;; トレンド表示コマンド
(defun show-trends (&optional (p 0))
  (interactive "p")
  (let* ((loc (if (= p 0) "JP:日本"
                (completing-read "Location: " (mapcar #'car *woeids*) :must-match t)))
         (woeid (cdr (assoc loc *woeids* :test #'string=))))
    (w/json (trends) (car (api-trends :woeid woeid))
      (msgbox "~{~A~%~}" (mapcar #'(lambda (tr) (json-value tr name)) trends)))))

(define-key *xyttr-timeline-keymap* #\T 'show-trends)

タイムラインバッファでTキーを押すと
http://gyazo.com/5353927732f28bb1d8acf7bf7ccfeb02.png
こんな感じで日本のトレンドが表示されます。
0以外の前置引数を渡す (M-- やM-1 〜 M-9を押してからTを押す) とminibufferで指定した地域のトレンドを表示できます。(complete+の併用を推奨)
http://gyazo.com/080a1de16cc8f2baca65d601a66347a0.png



以上、define-apiマクロの解説でした。

kokomadeyonda (xyttr Advent Calendar 12日目)

この記事はxyttr Advent Calendar 2011の記事です。

月曜日なので(?)小ネタ。
カーソル位置より下のツイートを削除します。

(in-package :xyttr)

(defun kokomadeyonda ()
  (interactive)
  (w/buffer-modifying ()
    (forward-entry)
    (w/entry ((:id _id))
      (whenlet start (entry-point)
        (delete-region start (point-max))
        (recenter)
        (setf #0=(timeline-alldata buffer-timeline)
              (delete-if #'(lambda (e) (w/json (id) e (<= id _id))) #0#))))
    (w/entry (id)
      (setf (timeline-first-id buffer-timeline) id))))

(define-key *xyttr-timeline-keymap* '(#\C-k #\C-k) 'kokomadeyonda)

栞ならマーク(Ctrl+Space)とかでも良いですが、何度も遡って読み返す事もなければ綺麗サッパリ消しちゃいましょう。



以下、補足としてコード上の初出要素の解説。

◆ マクロ w/buffer-modifying ((&optional buf) &body body)

bufで指定したread-onlyなバッファ(省略時はselected-buffer)を、一時的に書き込み可能な状態にしてbodyを評価します。
もうちょっと良い名前が欲しいんだけど思いつかない…

◆ マクロ w/entryのリネーム束縛構文

(w/entry (id text)
  ...)

という書き方は2日目に説明しましたが、以下のように書くと束縛するシンボルの名前を変更できます。

(w/entry ((:field-name sym) ...)
  (do-something sym))

:field-name がjsonのキー名で、symが値を束縛するシンボルです。
user.screen_name や user.profile_image_url等の長い名前の値を使い回す時は、この構文を使って短い名前で束縛しておくと良いです。

(w/entry ((:user.screen_name name)
          (:user.profile_image_url imgurl))
  (format nil "<img src=\"~A\"> ~A~%" imgurl name))

◆ xyttr::buffer-timeline

バッファーローカル変数xyttr::buffer-timelineには、タイムラインの各種パラメータ(使用API, APIパラメータ, リロード間隔等)や取得済みツイートデータ等がxyttr::timeline構造体として保存されています。

;; timeline構造体定義
(defstruct timeline
  user        ; ユーザー名 (= screen_name)
  tokens      ; 未使用 (アクセストークンを保管する予定)
  mode        ; タイムラインモード名 (:home-timeline, :user-timeline, etc)
  apifunc     ; 使用API (#'api-home-timeline-async, #'api-user-timeline-async, etc)
  params      ; リロード/次ページ取得時にapifuncへ渡す基本パラメータ
  auto-reload ; 自動リロードの間隔 (秒数 or nil)
  (unread 0)  ; 未読件数
  request     ; リロード/次ページ取得リクエストのキャンセルオブジェクト
  alldata     ; 表示中の全ツイートデータのリスト
  last-id     ; 最新ツイートのid (リロード時に使用)
  first-id    ; 最古ツイートのid (次ページ取得時に使用)
  (page 0)    ; 未使用
  )

全ツイートデータへシーケンシャルにアクセスしたい場合はこの変数からデータを取得すると良いです。

;; 表示中のツイート数を表示
(msgbox "~D" (length (timeline-alldata buffer-timeline)))

alldata以外のスロットの内容の書き換えは、間違うとリロード処理が狂ったりするので注意してください。

NinjaSlayer (xyttr Advent Calendar 11日目)

この記事はxyttr Advent Calendar 2011の記事です。

世界全土を電子ネットワークが覆いつくし、サイバネティック技術が普遍化した未来。宇宙殖民など稚気じみた夢。人々は灰色のメガロシティに棲み、夜な夜なサイバースペースへ逃避する。政府よりも力を持つメガコーポ群が、国家を背後から操作する。ここはネオサイタマ。鎖国体制を敷く日本の中心地だ。
−「メリークリスマス・ネオサイタマ」より

今回は、全米を震撼させているサイバーパンクニンジャ活劇小説「ニンジャスレイヤー」の公式日本語翻訳チームアカウント@NJSLYRと、実況用ハッシュタグ#njslyrを快適に鑑賞するためのコマンドを紹介します。
ニンジャスレイヤーをご存知ない方は日本語版公式ファンサイトの紹介をご覧ください。

2画面モード

ワイドディスプレイを使用しているなら、xyzzyのwindowを縦に分割して翻訳公式アカウントと実況タグを表示すると見やすいです。
http://gyazo.com/629369d6758f25099bb218a66f3f0c19.png

;;; ~/.xyttr/config.l とかに書く
(in-package :xyttr)

(defun njslyr ()
  (interactive)
  (let ((*frame-name* "njslyr"))
    (select-pseudo-frame
     (or (find-pseudo-frame *frame-name*)
         (new-pseudo-frame *frame-name*)))
    (when (= (count-windows) 1)
      (split-window-vertically))
    (user::xyttr-user "NJSLYR")
    (other-window)
    (user::xyttr-search "#NJSLYR -RT")))

;; 任意のタイムラインバッファからNキーで呼び出せるようにする
(define-key *xyttr-timeline-keymap* #\N 'njslyr)

公式側のウィンドウでカーソルをバッファ先頭に置いてから実況用タグ側をアクティブにすると公式側は自動リロード時にスクロールするようになります。

define-keyを使用しない場合、M-x xyttr::njslyr で起動するか、global-set-keyで好きなキーに'xyttr::njslyrを割り当てます。

なおこのコマンドは密かにNetInstallerで配布されているファイルのconfig.lサンプルファイルに入れてあります。
https://github.com/youz/xyttr/blob/master/.xyttr/config.l#L15

1画面モード

今年始めくらいまでは単純に"NJSLYR -RT"と検索するだけで公式と実況を一本化して表示できていたみたいですが、NinjaHeadsが増えてきたせいか検索の仕様変更のせいか、抜けが多くなって使いにくくなってます。
そこでuser-timelineとsearchを一本化するapi関数を作り、このapiを使ってタイムライン表示コマンドを作成します。

(defun api-njslyr-async (&key since_id max_id count include_rts
                              onsuccess onfailure)
  (let ((tweets1 nil)
        (done nil))
    (labels
        ((cb (tweets)
           (if done
               (funcall onsuccess (sort (append tweets1 tweets) #'>
                                        :key #'(lambda (e) (json-value e id))))
             (setq tweets1 tweets done t))))
      (values
       (api-user-timeline-async
        :screen_name "NJSLYR"
        :since_id since_id :max_id max_id :count count :include_rts include_rts
        :onsuccess #'cb :onfailure onfailure)
       (api-search-async
        :q "#NJSLYR -RT" :lang "ja" :rpp count
        :onsuccess #'cb :onfailure onfailure)))))

;; コマンド定義 (起動は M-x xyttr-njslyr)
(define-tl-command xyttr-njslyr ()
  :buffer-name "*NJSLYR*"
  :api-func #'api-njslyr-async
  :api-params (:count 50 :include_rts t))

http://gyazo.com/883cd56cad85bfc54a405cb440161a2b.png

define-tl-commandマクロはまた後日説明しようと思いますが、api-home-timelineなどのタイムライン系api関数と互換のある関数からタイムライン表示コマンドを生成できます。
本当はdefine-tl-commandの"@NJSLYR"に色付けができると良いのですが現在define-tl-commandのhookパラメータの処理にバグがあるみたいでうまく行きません… 早めに直そう。

過去ログ鑑賞

xyttrから過去のエピソードに簡単にアクセスする方法は今の所良いアイディアがありません。
@alohakunさんの素晴らしいまとめ記事がありますのでこちらを利用しましょう。
ニンジャスレイヤー「はじめての皆さんへ」 (まとめ・リンク集)



以上ニンジャスレイヤーコマンドの紹介でした。

タイムラインのフィルタリング (xyttr Advent Calendar 10日目)

この記事はxyttr Advent Calendar 2011の記事です。

まだreadmeにもreferenceにも書いてないのですが、リストxyttr:*timeline-filter*に関数を追加する事でタイムラインのフィルタリングを行えます。
xyttr:*timeline-filter*に格納されている関数がツイートデータに順次適用され、最終的に返されたデータがタイムラインバッファに表示されます。
途中でフィルタ関数がnilを返した場合、ツイートデータは非表示になります。

;; フィルタリングの動作
(defun filter1 (tweet)
  (reduce #'(lambda (tweet f) (if tweet (funcall f tweet) nil))
          xyttr:*timeline-filter*
          :initial-value tweet))

xyttr:*timeline-filter*がnilの場合、ツイートデータはそのまま表示されます。

NGユーザー・NGワードフィルタの例

(in-package :xyttr)

(defvar *ng-users* nil) ; NGユーザー名(screen_name)のリスト
(defvar *ng-words* nil) ; NGワードのリスト

(defun ng-filter (tweet)
  (w/json (user.screen_name text) tweet
    (unless (or (find user.screen_name *ng-users* :test 'string-equal)
                (some #'(lambda (w) (string-match w text)) *ng-words*))
      ; NGユーザー・NGワードに引っかからなかったらtweetをそのまま返す
      tweet)))

;; フィルターリストに追加
(push #'ng-filter *timeline-filter*)

;; NGワードでRTを除外
(push "^RT " *ng-words*)

ツイートデータ改変の例

全員名無しさんにします。 (reply等の動作が狂うので注意)

(defun anonymous (tweet)
  (setf (json-value tweet user.screen_name) "7743"
        (json-value tweet user.name) "名無しさん")
  tweet)

(push #'anonymous *timeline-filter*)

データの改変はjson-valueマクロを使用して

(setf (json-value tweet key) new-value)

の形で書くのが簡単です。



以上、フィルタリング機能の解説でした。

Twitter投稿用API関数を単体で使う (xyttr Advent Calendar 9日目)

この記事はxyttr Advent Calendar 2011の記事です。

xyttrパッケージよりexportされているTwitter API関数を使用すると、自作xyzzy lispプログラムとTwitterの連携が簡単に実現できます。 (xyttr referenceTwitter REST APIの辺り参照)

投稿用API関数xyttr:api-update-asyncを使用した例を3つほどでっち上げたので参考にしてみてください。

gitのcommitメッセージをツイート

;;; .xyzzyとかに書く
(require "xyttr")
(xyttr::xyttr-init) ; タイムラインバッファを開く前にAPI関数を使用する場合に必要

(defun tweet-commit-msg ()
  (let ((fn (get-buffer-file-name)))
    (when (and fn (string-match "/\\.git/COMMIT_EDITMSG$" fn))
      (let ((msg (save-excursion
		   (beginning-of-buffer)
		   (next-line)
		   (buffer-substring 0 (1- (point))))))
	(xyttr:api-update-async :status (concat "committed: " msg))))))

(add-hook 'ed::*after-save-buffer-hook* 'tweet-commit)

msysgitでEDITORにxyzzyを指定して作業してる人用。
このままだと何の作業してるのか分からないので公開できる範囲で情報を足すと良いでしょう。

メジャーモード起動時にツイート

(defun tweet-mode ()
  (xyttr:api-update-async :status (concat  mode-name "はじめました。")))

(add-hook 'ed::*hoge-mode-hook* 'tweet-mode)

意識高そうな言語のmode-hook変数に加えておくと良いと思います。

.xyzzy開いたらツイート

(defun tweet-dot-xyzzy ()
  (when (string= (file-namestring (get-buffer-file-name)) ".xyzzy")
    (xyttr:api-update-async :status "また.xyzzyいじってる")))

(add-hook 'ed::*find-file-hooks* 'tweet-dot-xyzzy)

ほどほどにしましょう



以上、あまり役に立たない利用例でした。 何か面白い連携方法があったら教えてください。

xyzzyでスムーズスクロール (xyttr Advent Calendar 8日目)

この記事はxyttr Advent Calendar 2011の記事です。

もはやxyttrの機能とは何も関係ないですが、スマフォやタブレットPC用のアプリにある慣性スクロールをxyzzyでそれっぽく再現するコマンドをこさえてみました。
放っておくと数千行になるタイムラインバッファ上でカカッと移動したい時に便利かもしれません。

慣性スクロールっぽい何か for #xyzzy

;;; -*- mode:lisp; package:smooth -*-

;;; 慣性スクロールっぽい何か for xyzzy

;;; 設定
;; (require "smooth")
;; (global-set-key #\C-\, 'smooth:scroll-forward)
;; (global-set-key #\C-. 'smooth:scroll-backward)

(provide "smooth")

(defpackage :smooth
  (:use :lisp :editor))

(in-package :smooth)
(export '(*counter-max* *counter-dec* *max-speed*
	  scroll-forward scroll-backward))

(defparameter *counter-max* 50)
(defparameter *counter-dec* 2)
(defparameter *max-speed* 2)

(defvar *counter* 0)

(defun start-scrolling (win dir)
  (let ((com *this-command*)
	(buf (window-buffer win))
	(row (get-window-line))
	(dec *counter-dec*))
    (labels ((scroll ()
	       (when (and (eq *last-command* com)
			  (eq (selected-window) win)
			  (not (deleted-buffer-p buf))
			  (eq buf (selected-buffer)))
		 (let ((d (min (ceiling *counter* 10) *max-speed*)))
		   (unless (next-line (* dir d))
		     (setq *counter* 0)))
		 (recenter row)
		 (refresh-screen)
		 (if (> (decf *counter* dec) 2)
		     (start-timer (/ 1.0 *counter*) #'scroll t)
		   (setq *counter* 0)))))
      (stop-timer #'scroll)
      (setq *counter* *counter-max*
	    *last-command* com)
      (scroll))))

(defun scroll-forward ()
  (interactive)
  (start-scrolling (selected-window) 1))

(defun scroll-backward ()
  (interactive)
  (start-scrolling (selected-window) -1))

速度、減速の早さ等は3つのパラメータ変数で調整できます。

(require "smooth")
(setq smooth:*counter-max* 60  ; 総移動量に影響
      smooth:*counter-dec* 2   ; 減速係数
      smooth:*max-speed* 3)    ; 初速

(in-package :xyttr)
(define-key *xyttr-timeline-keymap* #\, 'smooth:scroll-forward)
(define-key *xyttr-timeline-keymap* #\. 'smooth:scroll-backward)

ちょっと分かりにくいですが上記の設定でスクロールする様子を録画してみました。
*counter-max*は50〜100, *counter-dec*と*max-speed*は1〜5あたりで調整するのが良いと思われます。

最初はネタのつもりで書き始めたのですが、実際使ってみると
カーソルキー移動(or C-n, C-p)に比べて

  • 押しっぱなしにしなくて良い
  • kbdaccを使わなくても高速にスクロールできる

Page-Up, Page-Downと比べて

  • 移動量が分かりやすい
  • 無駄に楽しい

等の利点がありました。
もうちょっと分かりやすいパラメータ指定の方法を考えてみてから、NetInstaller用パッケージを用意したいと思います。