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.sub || t.time)}
${escapeBreakers(t.text)}
${escapeBreakers(t.sub || '')}
${escapeBreakers(t.time || '')}
`;
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};
})();