|
|
Line 1: |
Line 1: |
| // ==UserScript==
| | meu no,e e patrick |
| // @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();
| |