Bug 955642 - Make DMs work w/ the OTR extension and use twitter-text to parse entities. r=aleth,florian
This commit is contained in:
Родитель
3b75d13bf2
Коммит
f632056220
|
@ -47,6 +47,7 @@ var kAllowedMozClasses =
|
|||
aClassName => aClassName == "moz-txt-underscore" ||
|
||||
aClassName == "moz-txt-tag" ||
|
||||
aClassName == "ib-person";
|
||||
var kAllowedAnchorClasses = aClassName => aClassName == "ib-person";
|
||||
|
||||
/* Tags whose content should be fully removed, and reported in the Error Console. */
|
||||
var kForbiddenTags = {
|
||||
|
@ -61,7 +62,8 @@ var kStrictMode = {
|
|||
tags: {
|
||||
'a': {
|
||||
'title': true,
|
||||
'href': kAllowedURLs
|
||||
'href': kAllowedURLs,
|
||||
'class': kAllowedAnchorClasses
|
||||
},
|
||||
'br': true,
|
||||
'p': true
|
||||
|
@ -80,7 +82,8 @@ var kStandardMode = {
|
|||
'div': true,
|
||||
'a': {
|
||||
'title': true,
|
||||
'href': kAllowedURLs
|
||||
'href': kAllowedURLs,
|
||||
'class': kAllowedAnchorClasses
|
||||
},
|
||||
'em': true,
|
||||
'strong': true,
|
||||
|
@ -117,7 +120,8 @@ var kPermissiveMode = {
|
|||
'div': true,
|
||||
'a': {
|
||||
'title': true,
|
||||
'href': kAllowedURLs
|
||||
'href': kAllowedURLs,
|
||||
'class': kAllowedAnchorClasses
|
||||
},
|
||||
'font': {
|
||||
'face': true,
|
||||
|
|
|
@ -153,17 +153,26 @@ var GenericTwitterConversation = {
|
|||
flags.time = aDate;
|
||||
this.writeMessage("twitter.com", aMessage, flags);
|
||||
},
|
||||
onSentCallback: function(aData) {
|
||||
onSentCallback: function(aMsg, aData) {
|
||||
// The conversation may have been unitialized in the time it takes for
|
||||
// the async callback to fire. Use `_observers` as a proxy for uninit'd.
|
||||
if (!this._observers)
|
||||
return;
|
||||
|
||||
let tweet = JSON.parse(aData);
|
||||
// The OTR extension requires that the protocol not modify the message
|
||||
// (see the notes at `imIOutgoingMessage`). That's the contract we made.
|
||||
// Unfortunately, Twitter trims tweets and substitutes links.
|
||||
tweet.text = aMsg;
|
||||
this.displayMessages([tweet]);
|
||||
},
|
||||
parseTweet: function(aTweet) {
|
||||
let text = aTweet.text;
|
||||
prepareForDisplaying: function(aMsg) {
|
||||
if (!this._tweets.has(aMsg.id))
|
||||
return;
|
||||
let tweet = this._tweets.get(aMsg.id)._tweet;
|
||||
this._tweets.delete(aMsg.id);
|
||||
|
||||
let text = aMsg.displayMessage;
|
||||
let entities = {};
|
||||
|
||||
// Handle retweets: retweeted_status contains the object for the original
|
||||
|
@ -173,8 +182,8 @@ var GenericTwitterConversation = {
|
|||
// the FULL text from the original tweet and update the entities to match.
|
||||
// Note: the truncated flag is not always set correctly by twitter, so we
|
||||
// always make use of the original tweet.
|
||||
if ("retweeted_status" in aTweet) {
|
||||
let retweet = aTweet["retweeted_status"];
|
||||
if ("retweeted_status" in tweet) {
|
||||
let retweet = tweet["retweeted_status"];
|
||||
let retweetText, retweetEntities = {};
|
||||
|
||||
if ("extended_tweet" in retweet) {
|
||||
|
@ -194,22 +203,24 @@ var GenericTwitterConversation = {
|
|||
// We're going to take portions of the retweeted status and replace parts
|
||||
// of the original tweet, the retweeted status prepends the original
|
||||
// status with "RT @<username>: ", we need to keep the prefix.
|
||||
// Note: this doesn't play nice with extensions that may have altered
|
||||
// `text` to this point, but at least OTR doesn't act on `isChat`.
|
||||
let offset = text.indexOf(": ") + 2;
|
||||
text = text.slice(0, offset) + retweetText;
|
||||
|
||||
// Keep any entities that refer to the prefix (we can refer directly to
|
||||
// aTweet for these since they are not edited).
|
||||
if ("entities" in aTweet) {
|
||||
for (let type in aTweet.entities) {
|
||||
// the tweet for these since they are not edited).
|
||||
if ("entities" in tweet) {
|
||||
for (let type in tweet.entities) {
|
||||
let filteredEntities =
|
||||
aTweet.entities[type].filter(e => e.indices[0] < offset);
|
||||
tweet.entities[type].filter(e => e.indices[0] < offset);
|
||||
if (filteredEntities.length)
|
||||
entities[type] = filteredEntities;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the entities from the retweet (a copy of these must be made since
|
||||
// they will be edited and we do not wish to change aTweet).
|
||||
// they will be edited and we do not wish to change the tweet).
|
||||
for (let type in retweetEntities) {
|
||||
if (!(type in entities))
|
||||
entities[type] = [];
|
||||
|
@ -224,83 +235,36 @@ var GenericTwitterConversation = {
|
|||
})
|
||||
);
|
||||
}
|
||||
} else if ("extended_tweet" in aTweet) {
|
||||
} else if ("extended_tweet" in tweet) {
|
||||
// Bare bones extended tweet handling.
|
||||
let extended = aTweet.extended_tweet;
|
||||
let extended = tweet.extended_tweet;
|
||||
text = extended.full_text;
|
||||
if ("entities" in extended)
|
||||
entities = extended.entities;
|
||||
} else {
|
||||
// For non-retweets, we just want to use the entities that are given.
|
||||
if ("entities" in aTweet)
|
||||
entities = aTweet.entities;
|
||||
if ("entities" in tweet)
|
||||
entities = tweet.entities;
|
||||
}
|
||||
|
||||
this._account.LOG("Tweet: " + text);
|
||||
|
||||
if (Object.keys(entities).length) {
|
||||
/* entArray is an array of entities ready to be replaced in the tweet,
|
||||
* each entity contains:
|
||||
* - start: the start index of the entity inside the tweet,
|
||||
* - end: the end index of the entity inside the tweet,
|
||||
* - str: the string that should be replaced inside the tweet,
|
||||
* - href: the url (href attribute) of the created link tag,
|
||||
* - [optional] text: the text to display for the link,
|
||||
* The original string (str) will be used if this is not set.
|
||||
* - [optional] title: the title attribute for the link.
|
||||
*/
|
||||
let entArray = [];
|
||||
if ("hashtags" in entities && Array.isArray(entities.hashtags)) {
|
||||
entArray = entArray.concat(entities.hashtags.map(h => ({
|
||||
start: h.indices[0],
|
||||
end: h.indices[1],
|
||||
str: "#" + h.text,
|
||||
href: "https://twitter.com/#!/search?q=%23" + h.text})));
|
||||
}
|
||||
if ("urls" in entities && Array.isArray(entities.urls)) {
|
||||
entArray = entArray.concat(entities.urls.map(u => ({
|
||||
start: u.indices[0],
|
||||
end: u.indices[1],
|
||||
str: u.url,
|
||||
text: u.display_url || u.url,
|
||||
href: u.expanded_url || u.url})));
|
||||
}
|
||||
if ("user_mentions" in entities &&
|
||||
Array.isArray(entities.user_mentions)) {
|
||||
entArray = entArray.concat(entities.user_mentions.map(um => ({
|
||||
start: um.indices[0],
|
||||
end: um.indices[1],
|
||||
str: "@" + um.screen_name,
|
||||
text: '@<span class="ib-person">' + um.screen_name + "</span>",
|
||||
title: um.name,
|
||||
href: "https://twitter.com/" + um.screen_name})));
|
||||
}
|
||||
entArray.sort((a, b) => a.start - b.start);
|
||||
let offset = 0;
|
||||
for (let entity of entArray) {
|
||||
let str = text.substring(offset + entity.start, offset + entity.end);
|
||||
if (str[0] == "\uFF20") // @ - unicode character similar to @
|
||||
str = "@" + str.substring(1);
|
||||
if (str[0] == "\uFF03") // # - unicode character similar to #
|
||||
str = "#" + str.substring(1);
|
||||
if (str.toLowerCase() != entity.str.toLowerCase())
|
||||
continue;
|
||||
aMsg.displayMessage = twttr.txt.autoLink(text, {
|
||||
usernameClass: "ib-person",
|
||||
usernameIncludeSymbol: true,
|
||||
// Pass in the url entities so the t.co links are replaced.
|
||||
urlEntities: tweet.entities.urls.map(function(u) {
|
||||
let o = Object.assign(u);
|
||||
// But remove the indices so they apply in the face of modifications.
|
||||
delete o.indices;
|
||||
return o;
|
||||
})
|
||||
});
|
||||
|
||||
let html = "<a href=\"" + entity.href + "\"";
|
||||
if ("title" in entity)
|
||||
html += " title=\"" + entity.title + "\"";
|
||||
html += ">" + ("text" in entity ? entity.text : entity.str) + "</a>";
|
||||
text = text.slice(0, offset + entity.start) + html +
|
||||
text.slice(offset + entity.end);
|
||||
offset += html.length - (entity.end - entity.start);
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
GenericConversationPrototype.prepareForDisplaying.apply(this, arguments);
|
||||
},
|
||||
displayTweet: function(aTweet, aUser) {
|
||||
let name = aUser.screen_name;
|
||||
let text = this.parseTweet(aTweet);
|
||||
|
||||
let flags = name == this.nick ? {outgoing: true} : {incoming: true};
|
||||
flags.time = Math.round(new Date(aTweet.created_at) / 1000);
|
||||
|
@ -312,7 +276,9 @@ var GenericTwitterConversation = {
|
|||
aTweet.entities.user_mentions.some(mention => mention.screen_name == this.nick))
|
||||
flags.containsNick = true;
|
||||
|
||||
(new Tweet(aTweet, name, text, flags)).conversation = this;
|
||||
let tweet = new Tweet(aTweet, name, aTweet.text, flags);
|
||||
this._tweets.set(tweet.id, tweet);
|
||||
tweet.conversation = this;
|
||||
},
|
||||
_parseError: function(aData) {
|
||||
let error = "";
|
||||
|
@ -326,7 +292,8 @@ var GenericTwitterConversation = {
|
|||
error = "(" + error + ")";
|
||||
} catch(e) {}
|
||||
return error;
|
||||
}
|
||||
},
|
||||
getNormalizedChatBuddyName: (aNick) => aNick.replace(/^@/, "")
|
||||
};
|
||||
|
||||
function TimelineConversation(aAccount)
|
||||
|
@ -349,6 +316,9 @@ function TimelineConversation(aAccount)
|
|||
if ("description" in userInfo)
|
||||
this.setTopic(userInfo.description, aAccount.name, true);
|
||||
}
|
||||
|
||||
// Store messages by message id.
|
||||
this._tweets = new Map();
|
||||
}
|
||||
TimelineConversation.prototype = {
|
||||
__proto__: GenericConvChatPrototype,
|
||||
|
@ -390,7 +360,8 @@ TimelineConversation.prototype = {
|
|||
this.systemMessage(_("error.tooLong"), true);
|
||||
throw Cr.NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
this._account.tweet(aMsg, this.inReplyToStatusId, this.onSentCallback,
|
||||
this._account.tweet(aMsg, this.inReplyToStatusId,
|
||||
this.onSentCallback.bind(this, aMsg),
|
||||
function(aException, aData) {
|
||||
let error = this._parseError(aData);
|
||||
this.systemMessage(_("error.general", error, aMsg), true);
|
||||
|
@ -463,11 +434,15 @@ Object.assign(TimelineConversation.prototype, GenericTwitterConversation);
|
|||
function DirectMessageConversation(aAccount, aName)
|
||||
{
|
||||
this._init(aAccount, aName);
|
||||
|
||||
// Store messages by message id.
|
||||
this._tweets = new Map();
|
||||
}
|
||||
DirectMessageConversation.prototype = {
|
||||
__proto__: GenericConvIMPrototype,
|
||||
sendMsg: function(aMsg) {
|
||||
this._account.directMessage(aMsg, this.name, this.onSentCallback,
|
||||
this._account.directMessage(aMsg, this.name,
|
||||
this.onSentCallback.bind(this, aMsg),
|
||||
function(aException, aData) {
|
||||
let error = this._parseError(aData);
|
||||
this.systemMessage(_("error.general", error, aMsg), true);
|
||||
|
|
Загрузка…
Ссылка в новой задаче