liberator.modules.TWAnekoSB = ANekoSB = (function () { /********************************************************************************* * Config *********************************************************************************/ let Config = liberator.globalVariables.twittperator_sidebar_config || { // for Keyword タグ keyword: /neko|vimp|cat|猫/i, // ツイート内に含まれると、表示上抹殺される (reply とか除く vanish: /うぎぃいいい/i, // 自分のスクリーンネーム screenName: 'anekos', // 自分のその他の名前 myNames: /anekos|悪魔猫将軍/i, // ログファイル てけとーなフォーマットで保存されます //logFile: io.File('~/.chirpstream'), //myLogFile: io.File('~/.mychirpstream'), // 各イベント時に音がなる sound: { meow: makeAudio('file:///home/anekos/sound/my/meow.wav'), fanfare: makeAudio('file://C:/sound-data/fanfare.wav', 0.5), retweet: makeAudio('file:///home/anekos/sound/my/meow.wav', 0.8), favorite: makeAudio('file:///home/anekos/sound/my/meow.wav', 0.6), reply: makeAudio('file:///home/anekos/sound/my/meow.wav', 1.0), debug: makeAudio('file:///home/anekos/sound/my/meow.wav', 1.0), filter: makeAudio('file:///home/anekos/sound/my/meow.wav', 1.0), }, // 文字のサイズ fontSize: 15, // リストの最大保持数 listMax: 100, // リストの表示順(昇順/降順) listAscendingOrder: true, // ツイートされる度に最新ツイート位置までスクロールする listAutoScroll: true, // 日本語だけ for filter stream jpOnly: true, // 地震ツイートの本文に場所をくっつける earthquake: true, // サイドバーを閉じても機能を停止しない dontStop: true, // サイドバーが閉じていても、こっそり開始しておく silentStart: false, // 配列かオブジェクトを返すと、変更できる。 // 文字列 "reject" を返すと、そもそもツイートが表示されなくなる。 modifier: function (msg, tab, streamName) { return [msg, tab, streamName]; } }; // 日本語判定 JP = new RegExp("[\\u4e00-\\u9fa0\\u30A1-\\u30F6\\u30FC\\u3042-\\u3093\\u3001\\u3002\\uFF01\\uFF1F]"); /********************************************************************************* * Main *********************************************************************************/ // util {{{ function className (n) ('tw-anekos-sb-plugin-' + n); function px (n) parseInt(n, 10) + 'px'; // }}} function formatText (str) { // {{{ str = str.trim(); let reg = /https?:\/\/[^\s]+|[#@]\w+/g; let m, i = 0, buf = "", x = xml``; while((m=reg.exec(str))){ buf = str.substring(i, m.index); if (buf) x.appendChild(buf); let klass = "twlist-link", href = ""; switch (m[0].charAt(0)){ case "@": klass += " twlist-user"; href = "http://twitter.com/" + m[0].substr(1); break; case "#": klass += " twlist-hash"; href = "http://twitter.com/search?q=%23" + m[0].substr(1); break; default: klass += " twlist-url"; href = m[0]; } x.appendChild(xml`${m[0]}`); i=reg.lastIndex; } buf = str.substr(i); if (buf) x.appendChild(buf); return x; } // }}} function escapeBreakers (text) // {{{ text.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]+/g, function(c) uneval(c)); // }}} function getSidebarWindow () document.getElementById('sidebar')._contentWindow; let appendTweet = (function () { // {{{ function messageToXML (t) { let tweetXml; let sbWidth = getSidebarWindow().document.width; let richlistitemClasses = [className('tweet-panel'), className('tweet-' + t.type)]; let nameClass = className('item-name') + ' ' + (t.protected ? className('tweet-protected') : ''); tweetXml = xml` 70 ? 2 : 0)), "width: " + px(sbWidth - 100) + ' !important' ].join(';')} xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> ${escapeBreakers(t.text)} `; return tweetXml; } function xmlToDom(tweetXml, xmlns) { var doc = (new DOMParser).parseFromString( '' + tweetXml.toString() + "", "application/xml"); var imported = document.importNode(doc.documentElement, true); var range = document.createRange(); range.selectNodeContents(imported); var fragment = range.extractContents(); range.detach(); return fragment.childNodes.length > 1 ? fragment : fragment.firstChild; } let latest = {}; let latestNode = null; function append (t, tab, streamName) { tab = tab || 'home'; let modified = Config.modifier && Config.modifier(t, tab, streamName); if (modified) { if (modified instanceof Array) { [t, tab, streamName] = modified; } else if (modified === 'reject') { return; } else { t = modified; } } let now = JSON.stringify({name: t.name, text: t.text, tab: tab}); if (latest === now) { if (latestNode) latestNode.setAttribute( 'class', latestNode.getAttributeNode('class') + className('tweet-' + t.type) ); return; } latest = now; let cntr = getSidebarWindow().document.getElementById('tw-anekos-sb-' + tab + '-list'); let dom = xmlToDom(messageToXML(t)); let repDom = dom.cloneNode(true); let visibleIndex = cntr.getIndexOfFirstVisibleRow(); let len = cntr.itemCount; if (Config.listAscendingOrder) { cntr.appendChild(repDom); } else { cntr.insertBefore(repDom, cntr.firstChild); visibleIndex += 1; } latestNode = repDom; if (len > Config.listMax) { if (Config.listAscendingOrder) { cntr.removeChild(cntr.firstChild); visibleIndex -= 1; } else { cntr.removeChild(cntr.lastChild); } len -= 1; } if (Config.listAutoScroll) { if (Config.listAscendingOrder) { cntr.scrollToIndex(len - 1); } else { cntr.scrollToIndex(0); } } else { if (Config.listAscendingOrder) { if (len - visibleIndex < 10) { // 10 = 絶妙な値!これでいいのか! cntr.scrollToIndex(len - 1); } else { cntr.scrollToIndex(visibleIndex); } } else { if (visibleIndex < 3) { // 3 = 絶妙な値!これでいいのか! cntr.scrollToIndex(0); } else { cntr.scrollToIndex(visibleIndex); } } } } return append; })(); // }}} function objectToString (obj, head) { // {{{ if (!head) head = ''; let nextHead = head + ' '; let result = ''; for (let [n, v] in Iterator(obj)) { let vstr = (v && typeof v === 'object') ? objectToString(v, nextHead) : (v || '').toString(); result += head + n + ':\n' + vstr.split(/\n/).map(function(s) nextHead + s).join('\n') + '\n'; } return result.trim(); } // }}} function onMsg (real, msg, raw, streamName) { // {{{ if (real) { Tweets.unshift(msg); if (Tweets.length > Config.listMax) Tweets.splice(Config.listMax); } if (sidebarClosed) return; let screenName = Config.screenName; let my = (msg.retweeted_status && msg.retweeted_status.user.screen_name === screenName) || (msg.target_object && msg.event && ( (msg.event === 'favorite' && msg.target_object.user.screen_name == screenName) || (msg.event === 'list_member_added' && msg.target.screen_name == screenName) )) || (msg.user && msg.text && Config.myNames.test(msg.text)) || (msg.user && msg.text && msg.in_reply_to_screen_name == screenName) || (msg.direct_message); let protected = msg.user && msg.user.protected; // Fav test try { //liberator.log(JSON.stringify(msg, null, 2)); if (msg.event && msg.event === 'favorite' && msg.source && msg.source.screen_name === screenName) { let t = { name: '>' + msg.target_object.user.screen_name + '<', img: msg.target_object.user.profile_image_url, text: msg.target_object.text, type: 'favorite', protected: protected }; appendTweet(t, 'home', streamName); } } catch (e) { liberator.log(e); } // Ignore not JP if (!my && streamName === 'filter' && msg.text && Config.jpOnly && !JP.test(msg.text)) { return; } if (msg.text && msg.user && msg.user && msg.user.screen_name === screenName) my = false; let t, dummy; if (msg.direct_message) { t = { id: msg.id, name: msg.direct_message.sender.screen_name, img: msg.direct_message.sender.profile_image_url, text: msg.direct_message.text, sub: 'DM', type: 'DM' }; } else if (msg.retweeted_status) { t = { id: msg.id, name: my ? msg.user.screen_name : msg.retweeted_status.user.screen_name, img: my ? msg.user.profile_image_url : msg.retweeted_status.user.profile_image_url, text: msg.retweeted_status.text, sub: '\u21BB ' + msg.user.screen_name, type: 'retweet' }; dummy = true; } else if (my && msg.target && msg.event) { if (msg.event === 'favorite' && msg.target_object && !msg.target_object.retweeted_status) { t = { name: msg.source.screen_name, img: msg.source.profile_image_url, text: msg.target_object.text, type: 'favorite', sub: 'fav' }; dummy = true; } else if (msg.event === 'list_member_added' && msg.target) { // 結構漏れがある? t = { name: msg.source.screen_name, img: msg.source.profile_image_url, text: '\u3042\u306A\u305F\u3092\u30EA\u30B9\u30C8\u300C' + msg.target_object.name + '\u300D\u306B\u8FFD\u52A0\u3057\u307E\u3057\u305F\u3002\n' + 'http://twitter.com' + msg.target_object.uri, type: 'list-member-added', sub: 'listed' }; dummy = true; } } else if (msg.event === 'follow' && msg.target && msg.source) { t = { name: msg.source.screen_name, img: msg.source.profile_image_url, text: 'follow ' + msg.target.screen_name, type: 'follow' }; my = msg.target.screen_name === screenName; dummy = true; } else if (msg.user && msg.text && msg.in_reply_to_screen_name == screenName) { t = { id: msg.id, name: msg.user.screen_name, img: msg.user.profile_image_url, text: msg.text, type: 'reply' }; } else if (msg.user && msg.text) { t = { id: msg.id, name: msg.user.screen_name, img: msg.user.profile_image_url, text: msg.text, type: 'normal' }; } if (t) { t.protected = protected; if (Config.earthquake && /\u5730\u9707/.test(t.text) && msg.text.length < 20 && msg.user && msg.user.location) { t.text += ' [\u5730\u57DF: ' + msg.user.location + ']'; } if (msg.created_at) { t.time = new Date(msg.created_at).toLocaleTimeString().replace(/:\d+$/,'');; } if (real && dummy) { if (typeof dummy != 'object') { dummy = { user: { screen_name: t.name || '', profile_image_url: t.img }, text: '[' + t.type + '] ' + t.text + ' - http://twitter.com/' + t.name }; } plugins.twittperator.Twittperator.onMessage(dummy); } if (my || !Config.vanish.test([t.name, t.text, t.sub].join(' '))) { if (my) { if (real) { let sound = Config.sound[t.type] || Config.sound.fanfare; sound.play(); } t.type += '-my'; } else { if (t.type === 'normal' && Config.keyword.test(t.text)) t.type = 'keyword'; } if (streamName === 'filter') { if (!msg.retweeted_status) { t.type = 'filter'; appendTweet(t, 'home', streamName); appendTweet(t, 'filter', streamName); (function () { let s = Config.sound.filter; return s && s.play(); })(); } } else if (/^(keyword)$/.test(t.type)) { appendTweet(t, 'home', streamName); appendTweet(t, t.type, streamName); } else if (my) { appendTweet(t, 'home', streamName); appendTweet(t, 'my', streamName); } else { appendTweet(t, 'home', streamName); } } } if (real) { let s = '----------------------------------------\n' + objectToString(msg).replace(/\x0D\x0A|\x0D|\x0A/g, '\n'); if (Config.logFile) Config.logFile.write(s, '>>'); if (my && Config.myLogFile) Config.myLogFile.write(s, '>>'); } } // }}} function makeOnMsg (real, streamName) // {{{ function (msg, raw) onMsg(real, msg, raw, streamName); // }}} function addCommands () { // {{{ commands.addUserCommand( ['tws[idebar'], 'nosidebar commands', function (args) { }, { subCommands: [ new Command( ['v[anish]'], 'Vanish matched tweets', function (args) { Config.vanish = new RegExp(args.literalArg, 'i'); }, { literal: 0, completer: function (context, args) { context.completions = [ [util.escapeRegex(Config.vanish.source), ''] ]; } } ), new Command( ['k[eyword]'], 'Show matched tweets in keyword tab', function (args) { Config.keyword = new RegExp(args.literalArg, 'i'); }, { literal: 0, completer: function (context, args) { context.completions = [ [util.escapeRegex(Config.keyword.source), ''] ]; } } ), new Command( ['j[ponly]'], 'Show only Japanese Tweet', function (args) { Config.jpOnly = /yes/i.test(args.literalArg); }, { literal: 0, completer: function (context, args) { context.completions = [ ['yes', 'yes'], ['no', 'no'] ]; } } ), new Command( ['t[ab]'], 'select tab', function (args) { let tabbox = getSidebarWindow().document.getElementById('tw-anekos-sb-tabbox'); let index = parseInt(args.literalArg, 10); tabbox.selectedIndex = index; }, { literal: 0, completer: function (context, args) { let tabs = getSidebarWindow().document.getElementById('tw-anekos-sb-tabbox').querySelectorAll('tab'); context.completions = [ [i + ': ' + tab.getAttribute('label'), tab.getAttribute('label')] for ([i, tab] in Iterator(Array.slice(tabs))) ]; } } ) ] }, true ); } // }}} /********************************************************************************* * Install *********************************************************************************/ let Store = storage.newMap("twittperator-anekos-sb", {store: true}); let started = false; let readyToStart = false; let sidebarClosed = true; let Tweets = __context__.Tweets; if (!Tweets) Tweets = __context__.Tweets = Store.get("history", []); let added = {}; function start (isOpen, silent) { // {{{ function restore () { Array.slice(Tweets).reverse().forEach(makeOnMsg(false)); } if (silent && (started || readyToStart)) return; if (isOpen && sidebarClosed) { sidebarClosed = false; if (started) { restore(); return; } } if (readyToStart) return; if (started) stop(); readyToStart = true; started = true; setTimeout( function () { readyToStart = false; restore(); plugins.twittperator.ChirpUserStream.addListener(added.chirp = makeOnMsg(true, 'chirp')); plugins.twittperator.TrackingStream.addListener(added.filter = makeOnMsg(true, 'filter')); }, 1000 ); } // }}} function stop (isClose) { // {{{ if (!started) return liberator.echoerr('TWAnekoSB has not been started!'); Store.set("history", Tweets); Store.save(); if (isClose && Config.dontStop) { sidebarClosed = true; return; } plugins.twittperator.ChirpUserStream.removeListener(added.chirp); plugins.twittperator.TrackingStream.removeListener(added.filter); } // }}} function makeAudio (path, volume) { // {{{ let audio = new Audio(path); // XXX 効いてない if (volume) audio.volume = volume; return audio; } // }}} __context__.onUnload = function() { stop(); }; addCommands(); if (Config.silentStart) start(false, true); return {start: start, stop: stop}; })();