in visibilitylib/src/main/scala/com/twitter/visibility/interfaces/conversations/TombstoneVisibilityLibrary.scala [299:595]
def apply(request: TombstoneVisibilityRequest): Stitch[TombstoneVisibilityResponse] = {
val moderationFeatures = new ModerationFeatures(
moderationSource = request.moderatedTweetIds.contains,
statsReceiver = statsReceiver
)
val userSource = UserSource.fromFunction({
case (userId, _) =>
request.authorMap
.get(userId)
.map(Stitch.value).getOrElse(Stitch.NotFound)
})
val authorFeatures = new AuthorFeatures(userSource, statsReceiver)
val viewerFeatures = new ViewerFeatures(userSource, statsReceiver)
val languageTag = request.viewerContext.requestCountryCode.getOrElse(En)
val firstRound: Seq[(GetTweetFieldsResult, Option[TombstoneType])] = request.tweets.map {
case (gtfr, safetyLevel) =>
val actionStats = statsReceiver
.scope("action")
.scope(safetyLevel.map(_.toString().toLowerCase()).getOrElse("unknown_safety_level"))
toAction(gtfr.tweetResult, actionStats) match {
case Some(action) =>
(gtfr, Some(TombstoneType(gtfr.tweetId, gtfr.tweetId, action)))
case None =>
val quotedTweetId: Option[Long] = gtfr.tweetResult match {
case TweetFieldsResultState.Found(TweetFieldsResultFound(tweet, _, _)) =>
tweet.quotedTweet.map(_.tweetId)
case _ => None
}
(quotedTweetId, gtfr.quotedTweetResult) match {
case (Some(quotedTweetId), Some(tfrs)) =>
val qtActionStats = actionStats.scope("quoted")
toAction(tfrs, qtActionStats) match {
case None =>
(gtfr, None)
case Some(action) =>
(gtfr, Some(TombstoneType(gtfr.tweetId, quotedTweetId, action)))
}
case _ =>
(gtfr, None)
}
}
}
val (firstRoundActions, secondRoundInput) = firstRound.partition {
case (_, Some(tombstoneType)) =>
!tombstoneType.isInnerTombstone
case (_, None) => false
}
def invokeVisibilityLibrary(tweetId: Long, author: User): Stitch[Action] = {
visibilityLibrary
.runRuleEngine(
ContentId.TweetId(tweetId),
visibilityLibrary.featureMapBuilder(
Seq(
viewerFeatures.forViewerContext(request.viewerContext),
moderationFeatures.forTweetId(tweetId),
authorFeatures.forAuthor(author),
relationshipFeatures
.forAuthor(author, request.viewerContext.userId),
_.withConstantFeature(TweetId, tweetId),
_.withConstantFeature(FocalTweetId, request.focalTweetId)
)
),
request.viewerContext,
Tombstoning
).map(_.verdict)
}
val secondRoundActions: Stitch[Seq[(GetTweetFieldsResult, Option[TombstoneType])]] =
Stitch.traverse(secondRoundInput) {
case (gtfr: GetTweetFieldsResult, firstRoundTombstone: Option[TombstoneType]) =>
val secondRoundTombstone: Stitch[Option[TombstoneType]] = gtfr.tweetResult match {
case TweetFieldsResultState.Found(TweetFieldsResultFound(tweet, _, _)) =>
val tweetId = tweet.id
tweet.coreData
.flatMap { coreData => request.authorMap.get(coreData.userId) } match {
case Some(author) =>
invokeVisibilityLibrary(tweetId, author).flatMap {
case Allow =>
val quotedTweetId = tweet.quotedTweet.map(_.tweetId)
val quotedTweetAuthor = tweet.quotedTweet.flatMap { qt =>
request.authorMap.get(qt.userId)
}
(quotedTweetId, quotedTweetAuthor) match {
case (Some(quotedTweetId), Some(quotedTweetAuthor)) =>
invokeVisibilityLibrary(quotedTweetId, quotedTweetAuthor).flatMap {
case Allow =>
Stitch.None
case reason =>
Stitch.value(Some(TombstoneType(tweetId, quotedTweetId, reason)))
}
case _ =>
Stitch.None
}
case reason =>
Stitch.value(Some(TombstoneType(tweetId, tweetId, reason)))
}
case None =>
Stitch.None
}
case _ =>
Stitch.None
}
secondRoundTombstone.map { opt => opt.orElse(firstRoundTombstone) }.map { opt =>
(gtfr, opt)
}
}
secondRoundActions.map { secondRound =>
val tombstones: Seq[(Long, VfTombstone)] = (firstRoundActions ++ secondRound).flatMap {
case (gtfr, tombstoneTypeOpt) => {
val nonCompliantLimitedEngagementsOpt = gtfr.tweetResult match {
case TweetFieldsResultState.Found(TweetFieldsResultFound(_, _, Some(filteredReason)))
if FilteredReasonHelper.isLimitedEngagementsNonCompliant(filteredReason) =>
Some(LimitedEngagements(LimitedEngagementReason.NonCompliant))
case _ => None
}
(tombstoneTypeOpt, nonCompliantLimitedEngagementsOpt) match {
case (Some(tombstoneType), nonCompliantOpt) =>
val tombstoneId = tombstoneType.tombstoneId
val action = tombstoneType.action
val textOpt: Option[RichText] = action match {
case InterstitialLimitedEngagements(_, _, Some(localizedMessage), _) =>
Some(LocalizedMessageToRichText(localizedMessage))
case ipi: InterstitialLimitedEngagements =>
Some(
SafetyResultToUrtParser.fromSafetyResultToRichText(
SafetyResult(
Some(PublicInterest.ReasonToSafetyResultReason(ipi.reason)),
ipi.toActionThrift()
),
languageTag
)
)
case EmergencyDynamicInterstitial(_, _, Some(localizedMessage), _) =>
Some(LocalizedMessageToRichText(localizedMessage))
case edi: EmergencyDynamicInterstitial =>
Some(
SafetyResultToUrtParser.fromSafetyResultToRichText(
SafetyResult(
None,
edi.toActionThrift()
),
languageTag
)
)
case Tombstone(epitaph, _) =>
if (request.useRichText)
Some(EpitaphToRichText(epitaph, languageTag))
else
Some(EpitaphToRichText(Epitaph.UnavailableWithoutLink, languageTag))
case LocalizedTombstone(_, message) =>
if (request.useRichText)
Some(LocalizedMessageToRichText(LocalizedMessageConverter.toThrift(message)))
else
Some(EpitaphToRichText(Epitaph.UnavailableWithoutLink, languageTag))
case Interstitial(_, Some(localizedMessage), _) =>
Some(LocalizedMessageToRichText.apply(localizedMessage))
case interstitial: Interstitial =>
ReasonToUrtParser.fromReasonToRichText(interstitial.reason, languageTag)
case _ =>
None
}
val isRoot: Boolean = gtfr.tweetId == request.conversationId
val isOuter: Boolean = tombstoneId == request.conversationId
val revealTextOpt: Option[RichText] = action match {
case _: InterstitialLimitedEngagements | _: EmergencyDynamicInterstitial
if isRoot && isOuter =>
None
case _: Interstitial | _: InterstitialLimitedEngagements |
_: EmergencyDynamicInterstitial =>
Some(ReasonToUrtParser.getRichRevealText(languageTag))
case _ =>
None
}
val includeTweet = action match {
case _: Interstitial | _: InterstitialLimitedEngagements |
_: EmergencyDynamicInterstitial =>
true
case _ => false
}
val truncateForAction: Boolean =
shouldTruncateDescendantsWhenFocal(action)
val truncateForNonCompliant: Boolean =
nonCompliantOpt
.map(shouldTruncateDescendantsWhenFocal).getOrElse(false)
val truncateDescendants: Boolean =
truncateForAction || truncateForNonCompliant
val tombstone = textOpt match {
case Some(_) if request.useRichText =>
VfTombstone(
includeTweet = includeTweet,
action = action,
tombstoneInfo = Some(
TombstoneInfo(
cta = None,
revealText = None,
richText = textOpt,
richRevealText = revealTextOpt
)
),
tombstoneDisplayType = tombstoneType.tombstoneDisplayType,
truncateDescendantsWhenFocal = truncateDescendants
)
case Some(_) =>
VfTombstone(
includeTweet = includeTweet,
action = action,
tombstoneInfo = Some(
TombstoneInfo(
text = textOpt
.map(richText => richText.text).getOrElse(
""
cta = None,
revealText = revealTextOpt.map(_.text),
richText = None,
richRevealText = None
)
),
tombstoneDisplayType = tombstoneType.tombstoneDisplayType,
truncateDescendantsWhenFocal = truncateDescendants
)
case None =>
VfTombstone(
includeTweet = false,
action = action,
tombstoneInfo = Some(
TombstoneInfo(
cta = None,
revealText = None,
richText = Some(EpitaphToRichText(Epitaph.Unavailable, languageTag)),
richRevealText = None
)
),
tombstoneDisplayType = tombstoneType.tombstoneDisplayType,
truncateDescendantsWhenFocal = truncateDescendants
)
}
Some((gtfr.tweetId, tombstone))
case (None, Some(limitedEngagements))
if shouldTruncateDescendantsWhenFocal(limitedEngagements) =>
val tombstone = VfTombstone(
tombstoneId = gtfr.tweetId,
includeTweet = true,
action = limitedEngagements,
tombstoneInfo = None,
tombstoneDisplayType = TombstoneDisplayType.NonCompliant,
truncateDescendantsWhenFocal = true
)
Some((gtfr.tweetId, tombstone))
case _ =>
None
}
}
}
TombstoneVisibilityResponse(
tweetVerdicts = tombstones.toMap
)
}
}