Greasemonkey Manual:Installing Scripts: Difference between revisions

From GreaseSpot Wiki
Jump to navigationJump to search
No edit summary
No edit summary
 
(57 intermediate revisions by 32 users not shown)
Line 1: Line 1:
// ==UserScript==
{{Greasemonkey Manual TOC}}
// @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 ==
// 設定
//====================================================================


// bot が投稿するメッセージ
The purpose of [[Greasemonkey]] is to manage user scripts.
var MESSAGES = [
[[User script]]s allow the ''user'' to control the way they use the web, by customizing it with scripting.
    'びゃああああああああああああああああああああ',
The [https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/ 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 but you should look first at [[User_Script_Hosting|common hosts]].
var TIMER_INTERVAL = 1;


//====================================================================
Installation of a script is most often done by clicking a link on a web page.
// 以下も設定ですが、変更するとプログラムが壊れることがありますので
<!-- Not in 4.0
// あまり自信の無い方は変更しないようにしてください
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 &rarr; Open File...] dialog to open it.
//====================================================================
-->


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


// メッセージ判定(POST_MODE=='post' 時は強制的に none になります)
When navigating to a URL which ends with <code>.user.js</code>, [[Greasemonkey]] will trigger the installation dialog.
//  none        メッセージ判定を行わない
Note that [[Greasemonkey]] must be [[Troubleshooting_(Users)#Greasemonkey Enabled Status|enabled]] to do so.
//  disable_RT  RT QT を含まないメッセージのみリプライします
//  hello      挨拶モード(おはように対してリプライを返す)
var MESSAGE_COND = 'none';


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


// 投稿モード
[[Image:Install-dialog.png|left|thumb|150px|GM Installation Dialog]]
//  post      自動ポスト
//  reply    自動リプライ
//  timeline  タイムラインからリプライ
// ※(MESSAGE_COND == hello && POST_MODE != post) はエラーになります
var POST_MODE = 'reply';


// (MESSAGE_COND == hello) のとき挨拶か判定する関数
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.
function is_hello(text) {
It displays the name and description of the script as well as the [[include and exclude rules]] and special [[API]]s that apply.
    if (text.search(/^おは(?:や[ーう]|[よゆ]ー?)/) >= 0) {
<!-- Not in 4.0!
        return true;
''Note:'' Greasemonkey must be  [[Troubleshooting (Users)#Greasemonkey Enabled Status|enabled]] to install scripts.
    }
-->
    if (text.search(/^こんに?ち[はわ]/) >= 0) {
        return true;
    }
    if (text.search(/^(?:こん?ばん?[わみ]|こん?ば[やゆよ])/) >= 0) {
        return true;
    }
    return false;
}


// true なら起動時に MAX_REPLY_ID_KEY をタイムラインの最新の id で
;* The Install button
// 更新します(タイムライン取得に成功するまでつぶやきを開始しません)
This button will, of course, install the script in question.
var MAX_REPLY_ID_SYNC_REBOOT = true;
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].


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


// 最後に投稿した時刻を保存するキー
<!-- Not in 4.0
var LAST_POSTED_KEY = PREFIX + 'last-posted';
;* The View Script Source button
This button will allow viewing of the source code contained in the script.
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]].
var POST_SERIAL_KEY = PREFIX + 'post-serial';
In this window there is an information bar at the top similar to the Firefox extension installation security warning.
Clicking the install button here will also install the script.


// 次に投稿する MESSAGES のインデクスを保存するキー
-->
var LAST_INDEX_KEY = PREFIX + 'last-index';
Now with some scripts installed, we can open the [[Greasemonkey Manual:Monkey Menu|Monkey Menu]] to list and manage them.
 
// 最後にリプライした投稿の 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();

Latest revision as of 19:06, 3 November 2017


Greasemonkey Manual
Using Greasemonkey
Installing Scripts
Monkey Menu
Getting Help
User Script Authoring
Editing
Environment
API

About User Scripts

The purpose of Greasemonkey is to manage user scripts. User scripts allow the user to control the way they use the web, by customizing it with scripting. The Greasemonkey extension won't do any good without any scripts installed.

The first thing an eager user should do is find and install (or write!) a useful script.

Installation of a script is most often done by clicking a link on a web page.

  • Any file that ends in .user.js is a user script.

When navigating to a URL which ends with .user.js, Greasemonkey will trigger the installation dialog. Note that Greasemonkey must be enabled to do so.

The Installation Dialog

GM Installation Dialog

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. It displays the name and description of the script as well as the include and exclude rules and special APIs that apply.

  • The Install button

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 security vulnerability.

  • The Cancel button

This button will cancel the installation of a script.

Now with some scripts installed, we can open the Monkey Menu to list and manage them.