Difference between revisions of "Greasemonkey Manual:Installing Scripts"

From GreaseSpot Wiki
Jump to navigationJump to search
m (Reverted edits by 187.1.195.43 (Talk) to last revision by Arantius)
Line 1: Line 1:
{{Greasemonkey Manual TOC}}
// ==UserScript==
// @name          Twitter bot modoki reply random
// @namespace      http://sakuratan.biz/
// @description    Twitter bot もどき
// @include        http://twitter.com/
// @include        http://twitter.com/#*
// @include        https://twitter.com/
// @include        https://twitter.com/#*
// ==/UserScript==


== About User Scripts ==
//====================================================================
// 設定
//====================================================================


The purpose of [[Greasemonkey]] is to manage user scripts.
// bot が投稿するメッセージ
[[User script]]s allow the ''user'' to control the way they use the web, by customizing it with scripting.
var MESSAGES = [
The [https://addons.mozilla.org/firefox/addon/748 Greasemonkey extension] won't do any good without any scripts installed.
    'びゃああああああああああああああああああああ',
    'バタッ',
    '昨日が終らない(´;ω;`)',
    'キャッキャッ',
    'gkbr!',
    'ぱあああああああ(*´ν`*)あああああああっっっっ',
    '…………((((´;ω;`))))ブワッ',
    'おえ〜',
    '永久就職マダー(・∀・ )っ/凵⌒☆チンチン',
    '現実ドコー',
    'タイムリープ♡',
    'ひさっちゃんはぁはぁ',
    'えへっ(*´ν`*)',
    'いちごmogmog',
    '(`・ω・´)!',
];


The first thing an eager user should do is find and install ''(or write!)'' a useful script.
// フッター
// null か空文字列 ('') にすると表示されなくなります
var FOOTER = '[miro_bot]';


:* A word on finding [[user script]]s. They may be located anywhere on the internet or even offline. The Greasemonkey community typically uses the general purpose user script repository site created for it at [http://userscripts.org/ http://userscripts.org].
// 投稿する間隔 (単位は分)
var TIMER_INTERVAL = 1;


Installation of a script is most often done by clicking a link on a web page. One may also drag-and-drop a local file into the browser window, or optionally use the menu bar [http://support.mozilla.com/en-US/kb/Menu+Reference#Open_File_ File → Open File...] dialog to open it.
//====================================================================
// 以下も設定ですが、変更するとプログラムが壊れることがありますので
// あまり自信の無い方は変更しないようにしてください
//====================================================================


:* Any file that ends in '''<code>.user.js</code>''' is a valid Greasemonkey user script.
// デバッグフラグ
var DEBUG = true;


When the URL of a link is clicked or otherwise navigated to ends with <code>.user.js</code>, [[Greasemonkey]] will intercept the loading file by presenting the installation dialog.
// メッセージ判定(POST_MODE=='post' 時は強制的に none になります)
//  none        メッセージ判定を行わない
//  disable_RT  RT QT を含まないメッセージのみリプライします
//  hello      挨拶モード(おはように対してリプライを返す)
var MESSAGE_COND = 'none';


== The Installation Dialog ==
// メッセージモード
//  oneshot  ツイッターアクセスごとに先頭から順に一度投稿
//  loop    先頭から順に繰り返し投稿
//  random  ランダムに繰り返し投稿
var MESSAGE_MODE = 'random';


When navigating to a [[user script]], Greasemonkey will open its installation dialog instead of loading the script like a normal page.
// 投稿モード
A thumbnail of this dialog is shown to the left.
//  post      自動ポスト
It displays the name and description of the script, if available, as well as the [[include and exclude rules]] that apply.
//  reply    自動リプライ
//  timeline  タイムラインからリプライ
// ※(MESSAGE_COND == hello && POST_MODE != post) はエラーになります
var POST_MODE = 'reply';


[[Image:Install-dialog.png|left|thumb|150px|GM Installation Dialog]]
// (MESSAGE_COND == hello) のとき挨拶か判定する関数
// 適当なのでお好きに直してください
function is_hello(text) {
    if (text.search(/^おは(?:や[ーう]|[よゆ]ー?)/) >= 0) {
        return true;
    }
    if (text.search(/^こんに?ち[はわ]/) >= 0) {
        return true;
    }
    if (text.search(/^(?:こん?ばん?[わみ]|こん?ば[やゆよ])/) >= 0) {
        return true;
    }
    return false;
}


;* The Install button
// true なら起動時に MAX_REPLY_ID_KEY をタイムラインの最新の id で
This button will, of course, install the script in question.
// 更新します(タイムライン取得に成功するまでつぶやきを開始しません)
Like the Firefox extension installation dialog, this button is disabled for a few seconds to avoid the same potential [http://www.squarefree.com/2004/07/01/race-conditions-in-security-dialogs security vulnerability].
var MAX_REPLY_ID_SYNC_REBOOT = true;


;* The Cancel button
// プレフィクス
This button will cancel the installation of a script.
var PREFIX = ['twitter-bot-modoki', MESSAGE_COND, MESSAGE_MODE, POST_MODE, '']
            .join('-');


;* The View Script Source button
// 最後に投稿した時刻を保存するキー
This button will allow viewing of the source code contained in the script.
var LAST_POSTED_KEY = PREFIX + 'last-posted';
At this point, [[Greasemonkey]] has already downloaded the [[user script]] in question to display the name and other details.


When a user shows the script source, it displays the temporary file that Greasemonkey has already downloaded depicted in this [[:media:View-source.png|screenshot]]. This is intentionally engineered to avoid a potential security problem. An evil server could deliver one innocent script for viewing, and on the second load an evil script for installing.
// 排他制御用のシリアルを保存するキー
This approach ensures that the script is downloaded only once.  Upon examination of the visible script source, ''this copy'' is the script that will be installed.
var POST_SERIAL_KEY = PREFIX + 'post-serial';


Finally, in this window there is an information bar at the top similar to the Firefox extension installation security warning. This bar includes another Install button much like the the prior dialog. Clicking this button will also install the script. Using the [[Greasemonkey_Manual:Manage_Dialog|Manage Dialog]] is the next step.
// 次に投稿する MESSAGES のインデクスを保存するキー
var LAST_INDEX_KEY = PREFIX + 'last-index';
 
// 最後にリプライした投稿の id を保存するキー
var MAX_REPLY_ID_KEY = PREFIX + 'max-reply-id';
 
//====================================================================
// プログラムの挙動を変更する場合は以下を編集してください
// ※プログラミングの仕方が分からない場合は編集しないでください
//====================================================================
 
// 自分のスクリーンネーム
var MY_SCREEN_NAME;
 
// フック
var COND_HOOK;
var MESSAGE_HOOK;
var POST_HOOK;
 
// インターバルタイマ
var TIMER;
 
// タブ間の排他処理用のシリアル
var SERIAL;
 
// POSTエラー時は真
var RETRING;
 
//
// ツイッター
//
 
function get_auth_token() {
    var auth_token = document.getElementById('authenticity_token');
    if (auth_token) {
        return auth_token.value;
    } else {
        return null;
    }
}
 
function twit(data) {
    var auth_token = document.getElementById('authenticity_token');
    if (!auth_token) {
        return;
    }
 
    var postdata = 'authenticity_token=' +
                  encodeURIComponent(auth_token.value) +
                  '&status=' + encodeURIComponent(data.status) +
                  '&twttr=true';
    if (data.in_reply_to_status_id) {
        postdata += '&in_reply_to_status_id=' +
                    encodeURIComponent(data.in_reply_to_status_id);
        if (DEBUG) {
            GM_log('twit: status=' + data.status +
                  '&in_reply_to_status_id=' + data.in_reply_to_status_id);
        }
    } else {
        if (DEBUG) {
            GM_log('twit: status=' + data.status);
        }
    }
 
    var params = {
        type: 'POST',
        url: 'http://twitter.com/status/update',
        data: postdata
    };
    if (data.success) {
        params.success = data.success;
    }
    params.error = function() {
        if (data.error) {
            data.error();
        }
        window.setTimeout(function() {
            unsafeWindow.$.ajax(params);
        }, 300000);
        throw new Error('Ajax error, retring after 300 seconds...');
    };
 
    unsafeWindow.$.ajax(params);
}
 
//
// COND_FUNCS
// mixed COND_HOOK(mesg);
// mesg (リプライメッセージもしくは null) を受け取りメッセージを
// 返すなら false 以外を、返さないなら false を返す
// false 以外が返した場合、その値は MESSAGE_HOOK の cond 引数に渡される
//
var COND_FUNCS = {};
 
COND_FUNCS.none = function(mesg) {
    return true;
};
 
COND_FUNCS.disable_RT = function(mesg) {
    return (mesg.search(/\b(?:RT|QT)\b/) < 0);
};
 
COND_FUNCS.hello = function(mesg) {
    var can_reply = true;
    var flagment = unsafeWindow.$(mesg);
    var reply_me = flagment.filter('a[href="/' + MY_SCREEN_NAME + '"]');
    if (reply_me.length <= 0) {
        var links = flagment.filter('a[href^="/"]')
            .filter('a[href!="/' + MY_SCREEN_NAME + '"]');
        if (links.length > 0) {
            can_reply = false;
        }
    }
 
    if (can_reply) {
        // タグを除去して挨拶か調べる
        var text = mesg.replace(/<[^>]*>/g, '');
        if (is_hello(text)) {
            if (DEBUG) {
                GM_log('hello: repling=' + mesg);
            }
            return true;
        }
    }
 
    if (DEBUG) {
        GM_log('hello: skipped,reply=' + mesg);
    }
    return false;
};
 
//
// MESSAGE_FUNCS
// string or null MESSAGE_HOOK(mesg, cond);
// mesg (リプライメッセージもしくは null) と COND_HOOK からの戻り値
// cond を受け取りメッセージ文字列を返す
// 文字列の代わりに null を返すとメッセージは送信されない
//
var MESSAGE_FUNCS = {};
 
MESSAGE_FUNCS.oneshot = function(mesg, cond) {
    var i = GM_getValue(LAST_INDEX_KEY, 0);
    var next = i + 1;
 
    if (next >= MESSAGES.length) {
        GM_setValue(LAST_INDEX_KEY, MESSAGES.length);
        stop_timer();
        if (i >= MESSAGES.length) {
            if (DEBUG) {
                GM_log('oneshot: last message was posted');
            }
            return null;
        }
    }
 
    if (DEBUG) {
        GM_log('oneshot: index=' + i);
    }
    GM_setValue(LAST_INDEX_KEY, next);
    return MESSAGES[i];
};
 
MESSAGE_FUNCS.loop = function(mesg, cond) {
    var i = GM_getValue(LAST_INDEX_KEY, 0);
    if (i >= MESSAGES.length) {
        i = 0;
    }
    if (DEBUG) {
        GM_log('loop: index=' + i);
    }
 
    var next = i + 1;
    GM_setValue(LAST_INDEX_KEY, next);
    return MESSAGES[i];
};
 
MESSAGE_FUNCS.random = function(mesg, cond) {
    var i = Math.floor(Math.random() * MESSAGES.length);
    if (DEBUG) {
        GM_log('random: index=' + i);
    }
    return MESSAGES[i];
};
 
//
// POST_FUNCS
// void POST_HOOK();
// タイマから起動されメッセージを送信する
//
var POST_FUNCS = {};
 
function get_message(reply_mesg, cond) {
    var mesg = MESSAGE_HOOK(reply_mesg, cond);
    if (mesg == null) {
        return null;
    }
    if (FOOTER) {
        mesg += ' ';
        mesg += FOOTER;
    }
    return mesg;
}
 
function stop_timer() {
    if (TIMER) {
        if (DEBUG) {
            GM_log('stop_timer: timer=stop');
        }
        window.clearInterval(TIMER);
        TIMER = null;
    }
}
 
POST_FUNCS.post = function() {
    var rv = COND_HOOK(null);
    if (!rv) {
        return;
    }
 
    var mesg = get_message(null, rv);
    if (mesg == null) {
        stop_timer();
        return;
    }
 
    twit({
        status: mesg
    });
};
 
function update_max_reply_id(id) {
    var i = parseInt(GM_getValue(MAX_REPLY_ID_KEY, 0));
    if (parseInt(id) > i) {
        if (DEBUG) {
            GM_log('update_max_reply_id: ' + MAX_REPLY_ID_KEY + '=' + id);
        }
        // Twitter の id の範囲が不明なので文字列のまま保存
        GM_setValue(MAX_REPLY_ID_KEY, id);
    }
}
 
function dequeue_replies(replies) {
    var reply = replies.pop();
    if (!reply) {
        return;
    }
 
    if (DEBUG) {
        GM_log('dequeue_replies: screen_name=' + reply.screen_name +
              ',in_reply_to_status_id=' + reply.id);
    }
 
    twit({
        status: reply.status,
        in_reply_to_status_id: reply.id,
        success: function() {
            window.setTimeout(function() {
                update_max_reply_id(reply.id);
                dequeue_replies(replies);
            }, 0);
        }
    });
}
 
function parse_timeline(res) {
    var doc = unsafeWindow.$(res);
    return unsafeWindow.$('#timeline li.status', doc);
}
 
function parse_replies(res) {
    var max_reply_id = parseInt(GM_getValue(MAX_REPLY_ID_KEY, 0));
    var reply_screen_names = {};
    var replies = [];
    var statuses = parse_timeline(res);
    var last_id = null;
    for (var i = 0; i < statuses.length; ++i) {
        var status = unsafeWindow.$(statuses[i]);
        var id = status.attr('id').replace(/^status_/, '');
        if (parseInt(id) <= max_reply_id) {
            break;
        }
        last_id = id;
 
        // 自分へはリプライしない
        var action_reply = unsafeWindow.$('.actions,.actions-hover .reply', status);
        if (action_reply.length <= 0) {
            continue;
        }
 
        var screen_name = unsafeWindow.$('.screen-name', status).text();
        if (reply_screen_names[screen_name]) {
            if (DEBUG) {
                GM_log('parse_replies: already sent to ' + screen_name +
                      ' in this session');
            }
            continue;
        }
 
        var s = unsafeWindow.$('.entry-content', status).html();
        var cond = COND_HOOK(s);
        if (DEBUG) {
            GM_log('parse_replies: status=' + s + ',cond=' + cond);
        }
 
        if (cond) {
            var t = get_message(s, cond);
            if (t != null) {
                reply_screen_names[screen_name] = true;
                replies.push({
                    status: '@' + screen_name + ' ' + t,
                    screen_name: screen_name,
                    id: id
                });
            }
        }
    }
 
    if (DEBUG) {
        GM_log('parse_replies: max_reply_id=' + max_reply_id +
              ',replies=' + replies.length);
    }
 
    if (replies.length == 0) {
        if (last_id != null) {
            update_max_reply_id(last_id);
        }
    } else {
        // XXX dequeue_replies が完了してから
        // last_id で MAX_REPLY_ID_KEY を更新した方が良いかもしれない
        dequeue_replies(replies);
    }
}
 
function post_replies_success(res) {
    window.setTimeout(function() {
        parse_replies(res);
    }, 0);
}
 
POST_FUNCS.reply = function() {
    unsafeWindow.$.ajax({
        type: 'GET',
        url: 'http://twitter.com/replies?twttr=true',
        success: post_replies_success
    });
};
 
POST_FUNCS.timeline = function() {
    unsafeWindow.$.ajax({
        type: 'GET',
        url: 'http://twitter.com/home?twttr=true',
        success: post_replies_success
    });
};
 
//
// Main
//
 
function start(wait) {
    SERIAL = GM_getValue(POST_SERIAL_KEY, 0);
    if (DEBUG) {
        GM_log('start: SERIAL=' + SERIAL);
    }
    window.setTimeout(on_timer, wait);
}
 
function on_timer() {
    if (RETRING) {
        return;
    }
 
    if (!TIMER) {
        var intr = TIMER_INTERVAL * 60000;
        if (DEBUG) {
            GM_log('on_timer: setInterval=' + intr);
        }
        TIMER = window.setInterval(on_timer, intr);
    }
 
    // NOTE 複数タブからグリモンが実行されると
    // この辺がクリティカルセクションになる
    if (SERIAL != GM_getValue(POST_SERIAL_KEY, 0)) {
        // こっちのパスはそれほどクリティカルじゃない
        if (DEBUG) {
            GM_log('on_timer: another process running');
        }
        SERIAL = GM_getValue(POST_SERIAL_KEY, 0);
        return;
    }
    // クリティカルなのはこっち
    GM_setValue(POST_SERIAL_KEY, ++SERIAL);
    // ここまで
    if (DEBUG) {
        GM_log('on_timer: SERIAL=' + SERIAL);
    }
 
    var dt = new Date();
    GM_setValue(LAST_POSTED_KEY, dt.getTime().toString());
    POST_HOOK();
}
 
function get_wait() {
    var last_posted = GM_getValue(LAST_POSTED_KEY);
    if (!last_posted) {
        return 0;
    }
 
    var ms = parseFloat(last_posted) + TIMER_INTERVAL * 60000;
    var dt = new Date();
    var wait = ms - dt.getTime();
    if (wait < 0) {
        return 0;
    }
    return wait;
}
 
function max_reply_id_sync_success(res) {
    var wait = get_wait();
    var statuses = parse_timeline(res);
    if (statuses.length > 0) {
        var status = unsafeWindow.$(statuses[0]);
        var id = status.attr('id').replace(/^status_/, '');
        if (DEBUG) {
            GM_log('max_reply_id_sync_success: '+MAX_REPLY_ID_KEY+'='+id);
        }
        GM_setValue(MAX_REPLY_ID_KEY, id);
    }
 
    if (DEBUG) {
        GM_log('max_reply_id_sync_success: wait=' + wait);
    }
    start(wait);
}
 
function max_reply_id_sync() {
    unsafeWindow.$.ajax({
        type: 'GET',
        url: 'http://twitter.com/home?twttr=true',
        success: function(res) {
            window.setTimeout(function() {
                max_reply_id_sync_success(res);
            }, 0);
        },
        error: function() {
            if (DEBUG) {
                GM_log('max_reply_id_sync: Ajax failed, retring...');
            }
            window.setTimeout(function() {
                max_reply_id_sync();
            }, 60000);
        }
    });
}
 
function main() {
    if (DEBUG) {
        GM_log('main: ' + MESSAGE_MODE + ',' + MESSAGE_COND + ',' +
              POST_MODE);
    }
 
    if (!document.getElementById('authenticity_token')) {
        if (DEBUG) {
            GM_log('main: No authenticity_token');
        }
        return;
    }
 
    var metas = document.getElementsByTagName('meta');
    for (var i = 0; i < metas.length; ++i) {
        if (metas[i].name == 'session-user-screen_name') {
            MY_SCREEN_NAME = metas[i].content;
            break;
        }
    }
 
    if (!MY_SCREEN_NAME) {
        if (DEBUG) {
            GM_log('main: No session-user-screen_name');
        }
        return;
    }
 
    if (DEBUG) {
        GM_log('main: MY_SCREEN_NAME=' + MY_SCREEN_NAME);
    }
 
    RETRING = false;
 
    // 先にエラーチェック
    if (MESSAGE_COND == 'hello' && POST_MODE == 'post') {
        throw new Error('Invalid MESSAGE_COND ' + MESSAGE_COND +
                        ' and POST_MODE ' + POST_MODE);
    }
 
    // oneshot モード時は必要ならインデクスをリセット
    if (MESSAGE_MODE == 'oneshot' &&
        GM_getValue(LAST_INDEX_KEY, 0) >= MESSAGES.length) {
        if (DEBUG) {
            GM_log('main: ' + LAST_INDEX_KEY + '=0');
        }
        GM_setValue(LAST_INDEX_KEY, 0);
    }
 
    // 各フックを初期化
    MESSAGE_HOOK = MESSAGE_FUNCS[MESSAGE_MODE];
    if (!MESSAGE_HOOK) {
        throw new Error('Unknown MESSAGE_MODE ' + MESSAGE_MODE);
    }
    COND_HOOK = COND_FUNCS[MESSAGE_COND];
    if (!COND_HOOK) {
        throw new Error('Unknown MESSAGE_COND ' + MESSAGE_COND);
    }
    POST_HOOK = POST_FUNCS[POST_MODE];
    if (!POST_HOOK) {
        throw new Error('Unknown POST_MODE ' + POST_MODE);
    }
 
    // ボット開始
    if (MAX_REPLY_ID_SYNC_REBOOT && POST_MODE != 'post') {
        max_reply_id_sync(wait);
    } else {
        var wait = get_wait();
        if (DEBUG) {
            GM_log('main: wait='+wait);
        }
        start(wait);
    }
}
 
main();

Revision as of 13:13, 1 April 2010

// ==UserScript== // @name Twitter bot modoki reply random // @namespace http://sakuratan.biz/ // @description Twitter bot もどき // @include http://twitter.com/ // @include http://twitter.com/#* // @include https://twitter.com/ // @include https://twitter.com/#* // ==/UserScript==

//==================================================================== // 設定 //====================================================================

// bot が投稿するメッセージ var MESSAGES = [

   'びゃああああああああああああああああああああ',
   'バタッ',
   '昨日が終らない(´;ω;`)',
   'キャッキャッ',
   'gkbr!',
   'ぱあああああああ(*´ν`*)あああああああっっっっ',
   '…………((((´;ω;`))))ブワッ',
   'おえ〜',
   '永久就職マダー(・∀・ )っ/凵⌒☆チンチン',
   '現実ドコー',
   'タイムリープ♡',
   'ひさっちゃんはぁはぁ',
   'えへっ(*´ν`*)',
   'いちごmogmog',
   '(`・ω・´)!',

];

// フッター // null か空文字列 () にすると表示されなくなります var FOOTER = '[miro_bot]';

// 投稿する間隔 (単位は分) var TIMER_INTERVAL = 1;

//==================================================================== // 以下も設定ですが、変更するとプログラムが壊れることがありますので // あまり自信の無い方は変更しないようにしてください //====================================================================

// デバッグフラグ var DEBUG = true;

// メッセージ判定(POST_MODE=='post' 時は強制的に none になります) // none メッセージ判定を行わない // disable_RT RT QT を含まないメッセージのみリプライします // hello 挨拶モード(おはように対してリプライを返す) var MESSAGE_COND = 'none';

// メッセージモード // oneshot ツイッターアクセスごとに先頭から順に一度投稿 // loop 先頭から順に繰り返し投稿 // random ランダムに繰り返し投稿 var MESSAGE_MODE = 'random';

// 投稿モード // post 自動ポスト // reply 自動リプライ // timeline タイムラインからリプライ // ※(MESSAGE_COND == hello && POST_MODE != post) はエラーになります var POST_MODE = 'reply';

// (MESSAGE_COND == hello) のとき挨拶か判定する関数 // 適当なのでお好きに直してください function is_hello(text) {

   if (text.search(/^おは(?:や[ーう]|[よゆ]ー?)/) >= 0) {
       return true;
   }
   if (text.search(/^こんに?ち[はわ]/) >= 0) {
       return true;
   }
   if (text.search(/^(?:こん?ばん?[わみ]|こん?ば[やゆよ])/) >= 0) {
       return true;
   }
   return false;

}

// true なら起動時に MAX_REPLY_ID_KEY をタイムラインの最新の id で // 更新します(タイムライン取得に成功するまでつぶやきを開始しません) var MAX_REPLY_ID_SYNC_REBOOT = true;

// プレフィクス var PREFIX = ['twitter-bot-modoki', MESSAGE_COND, MESSAGE_MODE, POST_MODE, ]

            .join('-');

// 最後に投稿した時刻を保存するキー var LAST_POSTED_KEY = PREFIX + 'last-posted';

// 排他制御用のシリアルを保存するキー var POST_SERIAL_KEY = PREFIX + 'post-serial';

// 次に投稿する MESSAGES のインデクスを保存するキー var LAST_INDEX_KEY = PREFIX + 'last-index';

// 最後にリプライした投稿の id を保存するキー var MAX_REPLY_ID_KEY = PREFIX + 'max-reply-id';

//==================================================================== // プログラムの挙動を変更する場合は以下を編集してください // ※プログラミングの仕方が分からない場合は編集しないでください //====================================================================

// 自分のスクリーンネーム var MY_SCREEN_NAME;

// フック var COND_HOOK; var MESSAGE_HOOK; var POST_HOOK;

// インターバルタイマ var TIMER;

// タブ間の排他処理用のシリアル var SERIAL;

// POSTエラー時は真 var RETRING;

// // ツイッター //

function get_auth_token() {

   var auth_token = document.getElementById('authenticity_token');
   if (auth_token) {
       return auth_token.value;
   } else {
       return null;
   }

}

function twit(data) {

   var auth_token = document.getElementById('authenticity_token');
   if (!auth_token) {
       return;
   }
   var postdata = 'authenticity_token=' +
                  encodeURIComponent(auth_token.value) +
                  '&status=' + encodeURIComponent(data.status) +
                  '&twttr=true';
   if (data.in_reply_to_status_id) {
       postdata += '&in_reply_to_status_id=' +
                   encodeURIComponent(data.in_reply_to_status_id);
       if (DEBUG) {
           GM_log('twit: status=' + data.status +
                  '&in_reply_to_status_id=' + data.in_reply_to_status_id);
       }
   } else {
       if (DEBUG) {
           GM_log('twit: status=' + data.status);
       }
   }
   var params = {
       type: 'POST',
       url: 'http://twitter.com/status/update',
       data: postdata
   };
   if (data.success) {
       params.success = data.success;
   }
   params.error = function() {
       if (data.error) {
           data.error();
       }
       window.setTimeout(function() {
           unsafeWindow.$.ajax(params);
       }, 300000);
       throw new Error('Ajax error, retring after 300 seconds...');
   };
   unsafeWindow.$.ajax(params);

}

// // COND_FUNCS // mixed COND_HOOK(mesg); // mesg (リプライメッセージもしくは null) を受け取りメッセージを // 返すなら false 以外を、返さないなら false を返す // false 以外が返した場合、その値は MESSAGE_HOOK の cond 引数に渡される // var COND_FUNCS = {};

COND_FUNCS.none = function(mesg) {

   return true;

};

COND_FUNCS.disable_RT = function(mesg) {

   return (mesg.search(/\b(?:RT|QT)\b/) < 0);

};

COND_FUNCS.hello = function(mesg) {

   var can_reply = true;
   var flagment = unsafeWindow.$(mesg);
   var reply_me = flagment.filter('a[href="/' + MY_SCREEN_NAME + '"]');
   if (reply_me.length <= 0) {
       var links = flagment.filter('a[href^="/"]')
           .filter('a[href!="/' + MY_SCREEN_NAME + '"]');
       if (links.length > 0) {
           can_reply = false;
       }
   }
   if (can_reply) {
       // タグを除去して挨拶か調べる
       var text = mesg.replace(/<[^>]*>/g, );
       if (is_hello(text)) {
           if (DEBUG) {
               GM_log('hello: repling=' + mesg);
           }
           return true;
       }
   }
   if (DEBUG) {
       GM_log('hello: skipped,reply=' + mesg);
   }
   return false;

};

// // MESSAGE_FUNCS // string or null MESSAGE_HOOK(mesg, cond); // mesg (リプライメッセージもしくは null) と COND_HOOK からの戻り値 // cond を受け取りメッセージ文字列を返す // 文字列の代わりに null を返すとメッセージは送信されない // var MESSAGE_FUNCS = {};

MESSAGE_FUNCS.oneshot = function(mesg, cond) {

   var i = GM_getValue(LAST_INDEX_KEY, 0);
   var next = i + 1;
   if (next >= MESSAGES.length) {
       GM_setValue(LAST_INDEX_KEY, MESSAGES.length);
       stop_timer();
       if (i >= MESSAGES.length) {
           if (DEBUG) {
               GM_log('oneshot: last message was posted');
           }
           return null;
       }
   }
   if (DEBUG) {
       GM_log('oneshot: index=' + i);
   }
   GM_setValue(LAST_INDEX_KEY, next);
   return MESSAGES[i];

};

MESSAGE_FUNCS.loop = function(mesg, cond) {

   var i = GM_getValue(LAST_INDEX_KEY, 0);
   if (i >= MESSAGES.length) {
       i = 0;
   }
   if (DEBUG) {
       GM_log('loop: index=' + i);
   }
   var next = i + 1;
   GM_setValue(LAST_INDEX_KEY, next);
   return MESSAGES[i];

};

MESSAGE_FUNCS.random = function(mesg, cond) {

   var i = Math.floor(Math.random() * MESSAGES.length);
   if (DEBUG) {
       GM_log('random: index=' + i);
   }
   return MESSAGES[i];

};

// // POST_FUNCS // void POST_HOOK(); // タイマから起動されメッセージを送信する // var POST_FUNCS = {};

function get_message(reply_mesg, cond) {

   var mesg = MESSAGE_HOOK(reply_mesg, cond);
   if (mesg == null) {
       return null;
   }
   if (FOOTER) {
       mesg += ' ';
       mesg += FOOTER;
   }
   return mesg;

}

function stop_timer() {

   if (TIMER) {
       if (DEBUG) {
           GM_log('stop_timer: timer=stop');
       }
       window.clearInterval(TIMER);
       TIMER = null;
   }

}

POST_FUNCS.post = function() {

   var rv = COND_HOOK(null);
   if (!rv) {
       return;
   }
   var mesg = get_message(null, rv);
   if (mesg == null) {
       stop_timer();
       return;
   }
   twit({
       status: mesg
   });

};

function update_max_reply_id(id) {

   var i = parseInt(GM_getValue(MAX_REPLY_ID_KEY, 0));
   if (parseInt(id) > i) {
       if (DEBUG) {
           GM_log('update_max_reply_id: ' + MAX_REPLY_ID_KEY + '=' + id);
       }
       // Twitter の id の範囲が不明なので文字列のまま保存
       GM_setValue(MAX_REPLY_ID_KEY, id);
   }

}

function dequeue_replies(replies) {

   var reply = replies.pop();
   if (!reply) {
       return;
   }
   if (DEBUG) {
       GM_log('dequeue_replies: screen_name=' + reply.screen_name +
              ',in_reply_to_status_id=' + reply.id);
   }
   twit({
       status: reply.status,
       in_reply_to_status_id: reply.id,
       success: function() {
           window.setTimeout(function() {
               update_max_reply_id(reply.id);
               dequeue_replies(replies);
           }, 0);
       }
   });

}

function parse_timeline(res) {

   var doc = unsafeWindow.$(res);
   return unsafeWindow.$('#timeline li.status', doc);

}

function parse_replies(res) {

   var max_reply_id = parseInt(GM_getValue(MAX_REPLY_ID_KEY, 0));
   var reply_screen_names = {};
   var replies = [];
   var statuses = parse_timeline(res);
   var last_id = null;
   for (var i = 0; i < statuses.length; ++i) {
       var status = unsafeWindow.$(statuses[i]);
       var id = status.attr('id').replace(/^status_/, );
       if (parseInt(id) <= max_reply_id) {
           break;
       }
       last_id = id;
       // 自分へはリプライしない
       var action_reply = unsafeWindow.$('.actions,.actions-hover .reply', status);
       if (action_reply.length <= 0) {
           continue;
       }
       var screen_name = unsafeWindow.$('.screen-name', status).text();
       if (reply_screen_names[screen_name]) {
           if (DEBUG) {
               GM_log('parse_replies: already sent to ' + screen_name +
                      ' in this session');
           }
           continue;
       }
       var s = unsafeWindow.$('.entry-content', status).html();
       var cond = COND_HOOK(s);
       if (DEBUG) {
           GM_log('parse_replies: status=' + s + ',cond=' + cond);
       }
       if (cond) {
           var t = get_message(s, cond);
           if (t != null) {
               reply_screen_names[screen_name] = true;
               replies.push({
                   status: '@' + screen_name + ' ' + t,
                   screen_name: screen_name,
                   id: id
               });
           }
       }
   }
   if (DEBUG) {
       GM_log('parse_replies: max_reply_id=' + max_reply_id +
              ',replies=' + replies.length);
   }
   if (replies.length == 0) {
       if (last_id != null) {
           update_max_reply_id(last_id);
       }
   } else {
       // XXX dequeue_replies が完了してから
       // last_id で MAX_REPLY_ID_KEY を更新した方が良いかもしれない
       dequeue_replies(replies);
   }

}

function post_replies_success(res) {

   window.setTimeout(function() {
       parse_replies(res);
   }, 0);

}

POST_FUNCS.reply = function() {

   unsafeWindow.$.ajax({
       type: 'GET',
       url: 'http://twitter.com/replies?twttr=true',
       success: post_replies_success
   });

};

POST_FUNCS.timeline = function() {

   unsafeWindow.$.ajax({
       type: 'GET',
       url: 'http://twitter.com/home?twttr=true',
       success: post_replies_success
   });

};

// // Main //

function start(wait) {

   SERIAL = GM_getValue(POST_SERIAL_KEY, 0);
   if (DEBUG) {
       GM_log('start: SERIAL=' + SERIAL);
   }
   window.setTimeout(on_timer, wait);

}

function on_timer() {

   if (RETRING) {
       return;
   }
   if (!TIMER) {
       var intr = TIMER_INTERVAL * 60000;
       if (DEBUG) {
           GM_log('on_timer: setInterval=' + intr);
       }
       TIMER = window.setInterval(on_timer, intr);
   }
   // NOTE 複数タブからグリモンが実行されると
   // この辺がクリティカルセクションになる
   if (SERIAL != GM_getValue(POST_SERIAL_KEY, 0)) {
       // こっちのパスはそれほどクリティカルじゃない
       if (DEBUG) {
           GM_log('on_timer: another process running');
       }
       SERIAL = GM_getValue(POST_SERIAL_KEY, 0);
       return;
   }
   // クリティカルなのはこっち
   GM_setValue(POST_SERIAL_KEY, ++SERIAL);
   // ここまで
   if (DEBUG) {
       GM_log('on_timer: SERIAL=' + SERIAL);
   }
   var dt = new Date();
   GM_setValue(LAST_POSTED_KEY, dt.getTime().toString());
   POST_HOOK();

}

function get_wait() {

   var last_posted = GM_getValue(LAST_POSTED_KEY);
   if (!last_posted) {
       return 0;
   }
   var ms = parseFloat(last_posted) + TIMER_INTERVAL * 60000;
   var dt = new Date();
   var wait = ms - dt.getTime();
   if (wait < 0) {
       return 0;
   }
   return wait;

}

function max_reply_id_sync_success(res) {

   var wait = get_wait();
   var statuses = parse_timeline(res);
   if (statuses.length > 0) {
       var status = unsafeWindow.$(statuses[0]);
       var id = status.attr('id').replace(/^status_/, );
       if (DEBUG) {
           GM_log('max_reply_id_sync_success: '+MAX_REPLY_ID_KEY+'='+id);
       }
       GM_setValue(MAX_REPLY_ID_KEY, id);
   }
   if (DEBUG) {
       GM_log('max_reply_id_sync_success: wait=' + wait);
   }
   start(wait);

}

function max_reply_id_sync() {

   unsafeWindow.$.ajax({
       type: 'GET',
       url: 'http://twitter.com/home?twttr=true',
       success: function(res) {
           window.setTimeout(function() {
               max_reply_id_sync_success(res);
           }, 0);
       },
       error: function() {
           if (DEBUG) {
               GM_log('max_reply_id_sync: Ajax failed, retring...');
           }
           window.setTimeout(function() {
               max_reply_id_sync();
           }, 60000);
       }
   });

}

function main() {

   if (DEBUG) {
       GM_log('main: ' + MESSAGE_MODE + ',' + MESSAGE_COND + ',' +
              POST_MODE);
   }
   if (!document.getElementById('authenticity_token')) {
       if (DEBUG) {
           GM_log('main: No authenticity_token');
       }
       return;
   }
   var metas = document.getElementsByTagName('meta');
   for (var i = 0; i < metas.length; ++i) {
       if (metas[i].name == 'session-user-screen_name') {
           MY_SCREEN_NAME = metas[i].content;
           break;
       }
   }
   if (!MY_SCREEN_NAME) {
       if (DEBUG) {
           GM_log('main: No session-user-screen_name');
       }
       return;
   }
   if (DEBUG) {
       GM_log('main: MY_SCREEN_NAME=' + MY_SCREEN_NAME);
   }
   RETRING = false;
   // 先にエラーチェック
   if (MESSAGE_COND == 'hello' && POST_MODE == 'post') {
       throw new Error('Invalid MESSAGE_COND ' + MESSAGE_COND +
                       ' and POST_MODE ' + POST_MODE);
   }
   // oneshot モード時は必要ならインデクスをリセット
   if (MESSAGE_MODE == 'oneshot' &&
       GM_getValue(LAST_INDEX_KEY, 0) >= MESSAGES.length) {
       if (DEBUG) {
           GM_log('main: ' + LAST_INDEX_KEY + '=0');
       }
       GM_setValue(LAST_INDEX_KEY, 0);
   }
   // 各フックを初期化
   MESSAGE_HOOK = MESSAGE_FUNCS[MESSAGE_MODE];
   if (!MESSAGE_HOOK) {
       throw new Error('Unknown MESSAGE_MODE ' + MESSAGE_MODE);
   }
   COND_HOOK = COND_FUNCS[MESSAGE_COND];
   if (!COND_HOOK) {
       throw new Error('Unknown MESSAGE_COND ' + MESSAGE_COND);
   }
   POST_HOOK = POST_FUNCS[POST_MODE];
   if (!POST_HOOK) {
       throw new Error('Unknown POST_MODE ' + POST_MODE);
   }
   // ボット開始
   if (MAX_REPLY_ID_SYNC_REBOOT && POST_MODE != 'post') {
       max_reply_id_sync(wait);
   } else {
       var wait = get_wait();
       if (DEBUG) {
           GM_log('main: wait='+wait);
       }
       start(wait);
   }

}

main();