From f76cfcc1545e80ab5dfdb896bc76e88129c05c75 Mon Sep 17 00:00:00 2001 From: Cynthia Foxwell Date: Wed, 9 Apr 2025 12:53:07 -0600 Subject: [PATCH] automation labels --- src/parser.nim | 12 +- src/routes/twitter_api.nim | 12 +- src/sass/profile/card.scss | 3 +- src/sass/tweet/_base.scss | 322 +++++++++++++++++++------------------ src/types.nim | 2 + src/views/mastoapi.nim | 7 +- src/views/profile.nim | 9 ++ src/views/tweet.nim | 2 + 8 files changed, 208 insertions(+), 161 deletions(-) diff --git a/src/parser.nim b/src/parser.nim index f26f9b8..1759e8c 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -30,8 +30,6 @@ proc parseUser(js: JsonNode; id=""): User = result.expandUserEntities(js) proc parseGraphUser*(js: JsonNode): User = - echo "node: ", $js - var user = js{"data", "user", "result"} if user.isNull: user = js{"user_results", "result"} @@ -39,6 +37,16 @@ proc parseGraphUser*(js: JsonNode): User = user = js{"user_result", "result"} result = parseUser(user{"legacy"}, user{"rest_id"}.getStr) + + let label = user{"affiliates_highlighted_label", "label"} + if not label.isNull and label{"userLabelType"}.getStr == "AutomatedLabel": + result.bot = true + let entities = label{"longDescription", "entities"} + if not entities.isNull: + for ent in entities: + if ent{"ref", "type"}.getStr != "TimelineRichTextMention": continue + result.botOwner = ent{"ref", "screen_name"}.getStr + break if result.verifiedType == VerifiedType.none and user{"is_blue_verified"}.getBool(false): result.verifiedType = blue diff --git a/src/routes/twitter_api.nim b/src/routes/twitter_api.nim index 092ebd8..b58924b 100644 --- a/src/routes/twitter_api.nim +++ b/src/routes/twitter_api.nim @@ -128,6 +128,14 @@ proc getTweetById*(id: string; after=""): Future[string] {.async.} = params = {"variables": variables, "features": gqlFeatures} result = await fetchRaw(graphTweet ? params, Api.tweetDetail) +proc getUser*(username: string): Future[string] {.async.} = + if username.len == 0: return + let + variables = """{"screen_name":"$1"}""" % username + fieldToggles = """{"withAuxiliaryUserLabels":true}""" + params = {"variables": variables, "features": gqlFeatures, "fieldToggles": fieldToggles} + result = await fetchRaw(graphUser ? params, Api.userScreenName) + proc createTwitterApiRouter*(cfg: Config) = router api: get "/api/echo": @@ -135,8 +143,8 @@ proc createTwitterApiRouter*(cfg: Config) = get "/api/user/@username": let username = @"username" - let response = await getUserProfileJson(username) - respJson response + let response = await getUser(username) + resp Http200, { "Content-Type": "application/json" }, response #get "/api/user/@id/tweets": # let id = @"id" diff --git a/src/sass/profile/card.scss b/src/sass/profile/card.scss index 98790a3..f3e25f5 100644 --- a/src/sass/profile/card.scss +++ b/src/sass/profile/card.scss @@ -82,7 +82,8 @@ .profile-joindate, .profile-location, - .profile-website { + .profile-website, + .profile-automated { color: var(--fg_faded); margin: 1px 0; width: 100%; diff --git a/src/sass/tweet/_base.scss b/src/sass/tweet/_base.scss index 6621a3f..513d4f4 100644 --- a/src/sass/tweet/_base.scss +++ b/src/sass/tweet/_base.scss @@ -1,242 +1,254 @@ -@import '_variables'; -@import '_mixins'; -@import 'thread'; -@import 'media'; -@import 'video'; -@import 'embed'; -@import 'card'; -@import 'poll'; -@import 'quote'; -@import 'community_note'; +@import "_variables"; +@import "_mixins"; +@import "thread"; +@import "media"; +@import "video"; +@import "embed"; +@import "card"; +@import "poll"; +@import "quote"; +@import "community_note"; .tweet-body { - flex: 1; - min-width: 0; - margin-left: 58px; - pointer-events: none; - z-index: 1; + flex: 1; + min-width: 0; + margin-left: 58px; + pointer-events: none; + z-index: 1; } .tweet-content { - font-family: $font_3; - line-height: 1.3em; - pointer-events: all; - display: inline; + font-family: $font_3; + line-height: 1.3em; + pointer-events: all; + display: inline; } .tweet-bidi { - display: block !important; + display: block !important; } .tweet-header { - padding: 0; - vertical-align: bottom; - flex-basis: 100%; - margin-bottom: .2em; + padding: 0; + vertical-align: bottom; + flex-basis: 100%; + margin-bottom: 0.2em; - a { - display: inline-block; - word-break: break-all; - max-width: 100%; - pointer-events: all; - } + a { + display: inline-block; + word-break: break-all; + max-width: 100%; + pointer-events: all; + } } .tweet-name-row { - padding: 0; - display: flex; - justify-content: space-between; + padding: 0; + display: flex; + justify-content: space-between; } .fullname-and-username { - display: flex; - min-width: 0; + display: flex; + min-width: 0; } .fullname { - @include ellipsis; - flex-shrink: 2; - max-width: 80%; - font-size: 14px; - font-weight: 700; - color: var(--fg_color); + @include ellipsis; + flex-shrink: 2; + max-width: 80%; + font-size: 14px; + font-weight: 700; + color: var(--fg_color); } .username { - @include ellipsis; - min-width: 1.6em; - margin-left: .4em; - word-wrap: normal; + @include ellipsis; + min-width: 1.6em; + margin-left: 0.4em; + word-wrap: normal; +} + +.user-automated { + @include ellipsis; + min-width: 1px; + margin-left: 0.4em; + color: var(--fg_faded); } .tweet-date { - display: flex; - flex-shrink: 0; - margin-left: 4px; + display: flex; + flex-shrink: 0; + margin-left: 4px; } -.tweet-date a, .username, .show-more a { - color: var(--fg_dark); +.tweet-date a, +.username, +.show-more a { + color: var(--fg_dark); } .tweet-published { - margin: 0; - margin-top: 5px; - color: var(--grey); - pointer-events: all; + margin: 0; + margin-top: 5px; + color: var(--grey); + pointer-events: all; } .tweet-avatar { - display: contents !important; + display: contents !important; - img { - float: left; - margin-top: 3px; - margin-left: -58px; - width: 48px; - height: 48px; - } + img { + float: left; + margin-top: 3px; + margin-left: -58px; + width: 48px; + height: 48px; + } } .avatar { - &.round { - border-radius: 50%; - -webkit-user-select: none; - } - - &.mini { - position: unset; - margin-right: 5px; - margin-top: -1px; - width: 20px; - height: 20px; - } + &.round { + border-radius: 50%; + -webkit-user-select: none; + } + + &.mini { + position: unset; + margin-right: 5px; + margin-top: -1px; + width: 20px; + height: 20px; + } } .tweet-embed { + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + background-color: var(--bg_panel); + + .tweet-content { + font-size: 18px; + } + + .tweet-body { display: flex; flex-direction: column; - justify-content: center; - height: 100%; - background-color: var(--bg_panel); + max-height: calc(100vh - 0.75em * 2); + } - .tweet-content { - font-size: 18px; - } - - .tweet-body { - display: flex; - flex-direction: column; - max-height: calc(100vh - 0.75em * 2); - } + .card-image img { + height: auto; + } - .card-image img { - height: auto; - } - - .avatar { - position: absolute; - } + .avatar { + position: absolute; + } } .attribution { - display: flex; - pointer-events: all; - margin: 5px 0; + display: flex; + pointer-events: all; + margin: 5px 0; - strong { - color: var(--fg_color); - } + strong { + color: var(--fg_color); + } } .media-tag-block { - padding-top: 5px; - pointer-events: all; + padding-top: 5px; + pointer-events: all; + color: var(--fg_faded); + + .icon-container { + padding-right: 2px; + } + + .media-tag, + .icon-container { color: var(--fg_faded); - - .icon-container { - padding-right: 2px; - } - - .media-tag, .icon-container { - color: var(--fg_faded); - } + } } .timeline-container .media-tag-block { - font-size: 13px; + font-size: 13px; } .tweet-geo { - color: var(--fg_faded); + color: var(--fg_faded); } .replying-to { - color: var(--fg_faded); - margin: -2px 0 4px; + color: var(--fg_faded); + margin: -2px 0 4px; - a { - pointer-events: all; - } + a { + pointer-events: all; + } } -.retweet-header, .pinned, .tweet-stats { - align-content: center; - color: var(--grey); - display: flex; - flex-shrink: 0; - flex-wrap: wrap; - font-size: 14px; - font-weight: 600; - line-height: 22px; +.retweet-header, +.pinned, +.tweet-stats { + align-content: center; + color: var(--grey); + display: flex; + flex-shrink: 0; + flex-wrap: wrap; + font-size: 14px; + font-weight: 600; + line-height: 22px; - span { - @include ellipsis; - } + span { + @include ellipsis; + } } .retweet-header { - margin-top: -5px !important; + margin-top: -5px !important; } .tweet-stats { - margin-bottom: -3px; - -webkit-user-select: none; + margin-bottom: -3px; + -webkit-user-select: none; } .tweet-stat { - padding-top: 5px; - min-width: 1em; - margin-right: 0.8em; - pointer-events: all; + padding-top: 5px; + min-width: 1em; + margin-right: 0.8em; + pointer-events: all; } .show-thread { - display: block; - pointer-events: all; - padding-top: 2px; + display: block; + pointer-events: all; + padding-top: 2px; } .unavailable-box { - width: 100%; - height: 100%; - padding: 12px; - border: solid 1px var(--dark_grey); - box-sizing: border-box; - border-radius: 10px; - background-color: var(--bg_color); - z-index: 2; + width: 100%; + height: 100%; + padding: 12px; + border: solid 1px var(--dark_grey); + box-sizing: border-box; + border-radius: 10px; + background-color: var(--bg_color); + z-index: 2; } .tweet-link { - height: 100%; - width: 100%; - left: 0; - top: 0; - position: absolute; - -webkit-user-select: none; + height: 100%; + width: 100%; + left: 0; + top: 0; + position: absolute; + -webkit-user-select: none; - &:hover { - background-color: var(--bg_hover); - } + &:hover { + background-color: var(--bg_hover); + } } diff --git a/src/types.nim b/src/types.nim index 3f3b48d..496e300 100644 --- a/src/types.nim +++ b/src/types.nim @@ -99,6 +99,8 @@ type protected*: bool suspended*: bool joinDate*: DateTime + bot*: bool + botOwner*: string VideoType* = enum m3u8 = "application/x-mpegURL" diff --git a/src/views/mastoapi.nim b/src/views/mastoapi.nim index b37ad2f..7b2637f 100644 --- a/src/views/mastoapi.nim +++ b/src/views/mastoapi.nim @@ -221,13 +221,18 @@ proc getMastoAPIUser*(user: User, cfg: Config): JsonNode = website["verified_at"] = newJNull() fields.add(website) + if user.botOwner.len > 0: + var botOwner = newJObject() + botOwner["name"] = %"Automated by" + botOwner["value"] = %(&"{user.botOwner}") + var userJson = newJObject() userJson["id"] = %user.id userJson["username"] = %user.username userJson["acct"] = %user.username userJson["display_name"] = %user.fullname userJson["locked"] = %user.protected - userJson["bot"] = %false # TODO? + userJson["bot"] = %user.bot userJson["discoverable"] = %true userJson["indexable"] = %false userJson["group"] = %false diff --git a/src/views/profile.nim b/src/views/profile.nim index 7f15288..62b3c3b 100644 --- a/src/views/profile.nim +++ b/src/views/profile.nim @@ -35,6 +35,15 @@ proc renderUserCard*(user: User; prefs: Prefs; path: string): VNode = buttonReferer "/unfollow/" & user.username, "Unfollow", path, "profile-card-follow-button" tdiv(class="profile-card-extra"): + if user.bot: + tdiv(class="profile-automated"): + span: + if user.botOwner.len > 0: + icon "cog", "Automated by " + a(href=(&"/{user.botOwner}")): text &"@{user.botOwner}" + else: + icon "cog", "Automated" + if user.bio.len > 0: tdiv(class="profile-bio"): p(dir="auto"): diff --git a/src/views/tweet.nim b/src/views/tweet.nim index 11bb4c0..9072bc0 100644 --- a/src/views/tweet.nim +++ b/src/views/tweet.nim @@ -34,6 +34,8 @@ proc renderHeader(tweet: Tweet; retweet: string; pinned: bool; prefs: Prefs): VN tdiv(class="fullname-and-username"): linkUser(tweet.user, class="fullname") linkUser(tweet.user, class="username") + if tweet.user.bot: + tdiv(class="user-automated"): icon "cog", "Automated" span(class="tweet-date"): a(href=getLink(tweet), title=tweet.getTime):