From 33bf2c2397785fceed1192b191c5f9b86b56330d Mon Sep 17 00:00:00 2001 From: Zed Date: Sat, 21 Mar 2026 06:52:51 +0100 Subject: [PATCH] Support tweet content disclosures (AI and ads) Fixes #1374 --- public/css/fontello.css | 17 +++++++----- public/fonts/fontello.eot | Bin 9820 -> 10072 bytes public/fonts/fontello.svg | 2 ++ public/fonts/fontello.ttf | Bin 9652 -> 9904 bytes public/fonts/fontello.woff | Bin 6152 -> 6320 bytes public/fonts/fontello.woff2 | Bin 5068 -> 5176 bytes src/consts.nim | 2 +- src/parser.nim | 48 ++++++++++++++++++++++++++++++--- src/parserutils.nim | 52 ++++++++++++++++++++++++++++++++++++ src/sass/tweet/_base.scss | 17 ++++++++++-- src/types.nim | 2 ++ src/views/tweet.nim | 20 +++++++++++--- 12 files changed, 143 insertions(+), 17 deletions(-) diff --git a/public/css/fontello.css b/public/css/fontello.css index 53a66a1..eb41de7 100644 --- a/public/css/fontello.css +++ b/public/css/fontello.css @@ -1,12 +1,12 @@ @font-face { font-family: "fontello"; - src: url("/fonts/fontello.eot?42791196"); + src: url("/fonts/fontello.eot?49059696"); src: - url("/fonts/fontello.eot?42791196#iefix") format("embedded-opentype"), - url("/fonts/fontello.woff2?42791196") format("woff2"), - url("/fonts/fontello.woff?42791196") format("woff"), - url("/fonts/fontello.ttf?42791196") format("truetype"), - url("/fonts/fontello.svg?42791196#fontello") format("svg"); + url("/fonts/fontello.eot?49059696#iefix") format("embedded-opentype"), + url("/fonts/fontello.woff2?49059696") format("woff2"), + url("/fonts/fontello.woff?49059696") format("woff"), + url("/fonts/fontello.ttf?49059696") format("truetype"), + url("/fonts/fontello.svg?49059696#fontello") format("svg"); font-weight: normal; font-style: normal; } @@ -126,6 +126,11 @@ } /* '' */ +.icon-attention:before { + content: "\e812"; +} + +/* '' */ .icon-circle:before { content: "\f111"; } diff --git a/public/fonts/fontello.eot b/public/fonts/fontello.eot index 2e05e0b254729c644c52a1291e36fa9d9c553a37..ed1dba857060b1a79481fb41b396899d62adf5c7 100644 GIT binary patch delta 688 zcmXw1T}TvB6h8OfnLFd`wyrz7GmC7Z7OR`(BnN4q+Av zz4&Bk6JigA4-vf-#)3xJLnv01Zw3(sVMI$rBoQcRc6OzEIp@3I_g&6C_s)Fme1D2} z*8$A>CwWBZnV(*q?3f!G96)VGKhlSSbc5YTotr_wrFa-p4fC*rwRS;3Z>rH_GG1L{1fdVbKtlP9k3;|B-8Eq6b+{3MP zx^;_5<;BuKyv2+|v$oiWrh*Hg7@z{ISL_D35$ksYJOIDTb_1|lena?V8)ZA&J5N{- zvw6F-ki*b}NV=BtQhpM4A9b2s;am5d#P#X9xrj?5nBtx||XR zJF!b#Yarn&;&zbQ5IsWFmS%qgspXQ#t?7rtkvg@xt|&;{qq?=OHWH4IN9J-aw_JFY z<|7L)6lqXau+E^Q5QU5KRaxYUQkIYvW;2(*FPWvZeSAPx6d4n-za-}*CW=h@`+&>% z8{}7=LF!wap6|I(-u?&5S)uq!U%!05`5!HxdYI@|mwMWoQCs$O^AU+l1Euew=hsvl kL-VH=q{M4g>M2)+VVd!@*`G-_M*DNoWIUE1)89G%0YEC7(EtDd delta 434 zcmccNcgKe5hMOlk%&0#V$r{4Iz_@{dfoVZNA=QhaFz z>A6XIKdTrR#Cd>Rp7g}x0-(461B2KXAkC4UQ<>({ti{H_z!CtIXUIrROwl{R8_&QX z1mrWAWdH@(o0t~^`5i#MN=9x;g_r$*pxHuefc%J@{AABWwJ4<)1_ogkpnzU(VnqR? zCEq+C-vr24$V<#kJv&!c4#)>OlId(gesRfxI&E{H15|+W=L(8a3+lF6Z)0Fkm6*JM z@wDa?21cMb14ADJGnl0Vq`83Jo5H}tkOUOxV&Gt4Vqkh9IC%<_BC8Gq5Ki93qzhEc zHu)pdze&ujlixG5aRPk{2BMSyG2c@9D9FG7!VJPdS_6U^L>L%8b3!nK=w=SqyWE?1 z3TiXSYz78wJipCX27cxjATyrsesUj1PZBX_6q{@z5-_<(#E@5jZ4Ns~2IOL~&8I}T UnSqkm#C0aKNaSvAmbl6X0Q_2KZ~y=R diff --git a/public/fonts/fontello.svg b/public/fonts/fontello.svg index ccc0436..19db3bd 100644 --- a/public/fonts/fontello.svg +++ b/public/fonts/fontello.svg @@ -42,6 +42,8 @@ + + diff --git a/public/fonts/fontello.ttf b/public/fonts/fontello.ttf index b4dcf100b9c57f6507424657c3d0dc77de2ffd11..be34c4eb29f2d82cecf4fa1e586298f6c227dc50 100644 GIT binary patch delta 659 zcmXw0Ur19?9RAKZ_n&uj>fPPl2pekQpPWmU8mLG?L{Wl)P)1ON`)S>mTMy5u0sO{FB$F%tCypo=^DI^S;anH$tptS&4A?&iVy821K14sfnPOS z0h6$KD-ZyFS8N3=pg16X^h}G5^^J#ONO<5yEnRS6-~Q$>mnAFU6N2-j3RNh{kW!Gr zh0IeBjEUnAg4tk#9b<^>+EKqPWK^pPyv(aARx68zndxHtU*x&rWM>1 z>uU0MG;Ip167ZYWU=NFRvHQ+ap|DhXl~bmkKG)STO{a6lRGsOHtX5G&$Y!z3h_)b KQT$}R_x%I*ZIW{U delta 408 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 delta 3556 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 aG&glycby1GH0vS_BtQ%RfzqYeLjDI{;+7Zy diff --git a/public/fonts/fontello.woff2 b/public/fonts/fontello.woff2 index 7efdfbdbc78248f820089a97017c306e49c23955..cfe8dfb8c45eda69650856a8a6f5f37cd2a39e6e 100644 GIT binary patch literal 5176 zcmV-86vyj#Pew8T0RR9102DX?4*&oF04A^i02AK;0RR9100000000000000000000 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 0 and parsedMedia.len == mediaEntities.len: result.media = parsedMedia @@ -409,7 +416,7 @@ proc parseGraphTweet(js: JsonNode): Tweet = else: discard - if not js.hasKey("legacy"): + if "legacy" notin js and "rest_id" notin js: return Tweet() var jsCard = select(js{"card"}, js{"tweet_card"}, js{"legacy", "tweet_card"}) @@ -432,8 +439,41 @@ proc parseGraphTweet(js: JsonNode): Tweet = with restId, js{"reply_to_results", "rest_id"}: replyId = restId.getId - result = parseTweet(js{"legacy"}, jsCard, replyId) - result.id = js{"rest_id"}.getId + if "details" in js: + result = Tweet( + id: js{"rest_id"}.getId, + available: true, + text: js{"details", "full_text"}.getStr, + time: js{"details", "created_at_ms"}.getTimeFromMs, + replyId: js{"reply_to_results", "rest_id"}.getId, + isAd: js{"content_disclosure", "advertising_disclosure", "is_paid_promotion"}.getBool, + isAI: js{"content_disclosure", "ai_generated_disclosure", "has_ai_generated_media"}.getBool, + stats: TweetStats( + replies: js{"counts", "reply_count"}.getInt, + retweets: js{"counts", "retweet_count"}.getInt, + likes: js{"counts", "favorite_count"}.getInt, + ) + ) + + if jsCard.kind != JNull: + let name = jsCard{"name"}.getStr + if "poll" in name: + if "image" in name: + result.media.addMedia(Photo( + url: jsCard{"binding_values", "image_large"}.getImageVal + )) + + result.poll = some parsePoll(jsCard) + elif name == "amplify": + result.media.addMedia(parsePromoVideo(jsCard{"binding_values"})) + else: + result.card = some parseCard(jsCard, js{"url_entities"}) + + result.expandTweetEntitiesV2(js) + else: + result = parseTweet(js{"legacy"}, jsCard, replyId) + result.id = js{"rest_id"}.getId + result.user = parseGraphUser(js{"core"}) if result.reply.len == 0: diff --git a/src/parserutils.nim b/src/parserutils.nim index ea37468..518724e 100644 --- a/src/parserutils.nim +++ b/src/parserutils.nim @@ -320,6 +320,58 @@ proc expandTweetEntities*(tweet: Tweet; js: JsonNode) = tweet.expandTextEntities(entities, tweet.text, textSlice, replyTo, hasQuote or hasJobCard) +proc expandTextEntitiesV2(tweet: Tweet; js: JsonNode; text: string; textSlice: Slice[int]; + hasRedundantLink=false) = + let hasCard = tweet.card.isSome + + var replacements = newSeq[ReplaceSlice]() + + with urls, js{"url_entities"}: + for u in urls: + let urlStr = u["url"].getStr + if urlStr.len == 0 or urlStr notin text: + continue + + replacements.extractUrls(u, textSlice.b, hideTwitter = hasRedundantLink) + + if hasCard and u{"url"}.getStr == get(tweet.card).url: + get(tweet.card).url = u.getExpandedUrl + + with hashtags, js{"details", "hashtag_entities"}: + for hashtag in hashtags: + replacements.extractHashtags(hashtag) + + with cashtags, js{"details", "cashtag_entities"}: + for cashtag in cashtags: + replacements.extractHashtags(cashtag) + + with mentions, js{"mention_entities"}: + for mention in mentions: + let + name = mention{"screen_name"}.getStr + slice = mention.extractSlice + idx = tweet.reply.find(name) + + if slice.a >= textSlice.a: + replacements.add ReplaceSlice(kind: rkMention, slice: slice, + url: "/" & name, display: mention["name"].getStr) + elif idx == -1 and tweet.replyId != 0: + tweet.reply.add name + + replacements.deduplicate + replacements.sort(cmp) + + tweet.text = text.toRunes.replacedWith(replacements, textSlice).strip(leading=false) + +proc expandTweetEntitiesV2*(tweet: Tweet; js: JsonNode) = + let + textRange = js{"details", "display_text_range"} + textSlice = textRange{0}.getInt .. textRange{1}.getInt + hasQuote = "quoted_tweet_results" in js + hasJobCard = tweet.card.isSome and get(tweet.card).kind == jobDetails + + tweet.expandTextEntitiesV2(js, tweet.text, textSlice, hasQuote or hasJobCard) + proc expandNoteTweetEntities*(tweet: Tweet; js: JsonNode) = let entities = ? js{"entity_set"} diff --git a/src/sass/tweet/_base.scss b/src/sass/tweet/_base.scss index 8019522..5bf5a2c 100644 --- a/src/sass/tweet/_base.scss +++ b/src/sass/tweet/_base.scss @@ -80,8 +80,8 @@ } .tweet-published { - margin-top: 10px; - margin-bottom: 3px; + margin-top: 6px; + margin-bottom: 0px; color: var(--grey); pointer-events: all; } @@ -292,3 +292,16 @@ padding: 10px 10px; padding-top: 6px; } + +.disclosures { + display: flex; + flex-direction: column; + color: var(--grey); + font-size: 14px; + margin-top: 4px; + margin-bottom: -2px; + + .icon-attention { + margin-right: -3px; + } +} diff --git a/src/types.nim b/src/types.nim index 4f52886..c2aca50 100644 --- a/src/types.nim +++ b/src/types.nim @@ -240,6 +240,8 @@ type media*: MediaEntities history*: seq[int64] note*: string + isAd*: bool + isAI*: bool Tweets* = seq[Tweet] diff --git a/src/views/tweet.nim b/src/views/tweet.nim index b1f805c..aae56e9 100644 --- a/src/views/tweet.nim +++ b/src/views/tweet.nim @@ -257,10 +257,6 @@ proc renderLatestPost(username: string; id: int64): VNode = a(href=getLink(id, username)): text "See the latest post" -proc renderQuoteMedia(quote: Tweet; prefs: Prefs; path: string): VNode = - buildHtml(tdiv(class="quote-media-container")): - renderMedia(quote.media, prefs, path) - proc renderCommunityNote(note: string; prefs: Prefs): VNode = buildHtml(tdiv(class="community-note")): tdiv(class="community-note-header"): @@ -269,6 +265,10 @@ proc renderCommunityNote(note: string; prefs: Prefs): VNode = tdiv(class="community-note-text", dir="auto"): verbatim replaceUrls(note, prefs) +proc renderQuoteMedia(quote: Tweet; prefs: Prefs; path: string): VNode = + buildHtml(tdiv(class="quote-media-container")): + renderMedia(quote.media, prefs, path) + proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode = if not quote.available: return buildHtml(tdiv(class="quote unavailable")): @@ -315,6 +315,15 @@ proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode = tdiv(class="quote-latest"): text "There's a new version of this post" +proc renderDisclosures*(tweet: Tweet): VNode = + buildHtml(tdiv(class="disclosures")): + if tweet.isAI: + span(data-disclosure="ai"): + icon "attention", "Made with AI" + if tweet.isAd: + span(data-disclosure="ad"): + icon "attention", "Paid partnership (ad)" + proc renderLocation*(tweet: Tweet): string = let (place, url) = tweet.getLocation() if place.len == 0: return @@ -391,6 +400,9 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0; if tweet.note.len > 0 and not prefs.hideCommunityNotes: renderCommunityNote(tweet.note, prefs) + if tweet.isAI or tweet.isAd: + renderDisclosures(tweet) + let hasEdits = tweet.history.len > 1 isLatest = hasEdits and tweet.id == max(tweet.history)