automation labels

This commit is contained in:
Cynthia Foxwell 2025-04-09 12:53:07 -06:00
parent 536f9d7fab
commit f76cfcc154
No known key found for this signature in database
8 changed files with 208 additions and 161 deletions

View File

@ -30,8 +30,6 @@ proc parseUser(js: JsonNode; id=""): User =
result.expandUserEntities(js) result.expandUserEntities(js)
proc parseGraphUser*(js: JsonNode): User = proc parseGraphUser*(js: JsonNode): User =
echo "node: ", $js
var user = js{"data", "user", "result"} var user = js{"data", "user", "result"}
if user.isNull: if user.isNull:
user = js{"user_results", "result"} user = js{"user_results", "result"}
@ -40,6 +38,16 @@ proc parseGraphUser*(js: JsonNode): User =
result = parseUser(user{"legacy"}, user{"rest_id"}.getStr) 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): if result.verifiedType == VerifiedType.none and user{"is_blue_verified"}.getBool(false):
result.verifiedType = blue result.verifiedType = blue

View File

@ -128,6 +128,14 @@ proc getTweetById*(id: string; after=""): Future[string] {.async.} =
params = {"variables": variables, "features": gqlFeatures} params = {"variables": variables, "features": gqlFeatures}
result = await fetchRaw(graphTweet ? params, Api.tweetDetail) 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) = proc createTwitterApiRouter*(cfg: Config) =
router api: router api:
get "/api/echo": get "/api/echo":
@ -135,8 +143,8 @@ proc createTwitterApiRouter*(cfg: Config) =
get "/api/user/@username": get "/api/user/@username":
let username = @"username" let username = @"username"
let response = await getUserProfileJson(username) let response = await getUser(username)
respJson response resp Http200, { "Content-Type": "application/json" }, response
#get "/api/user/@id/tweets": #get "/api/user/@id/tweets":
# let id = @"id" # let id = @"id"

View File

@ -82,7 +82,8 @@
.profile-joindate, .profile-joindate,
.profile-location, .profile-location,
.profile-website { .profile-website,
.profile-automated {
color: var(--fg_faded); color: var(--fg_faded);
margin: 1px 0; margin: 1px 0;
width: 100%; width: 100%;

View File

@ -1,242 +1,254 @@
@import '_variables'; @import "_variables";
@import '_mixins'; @import "_mixins";
@import 'thread'; @import "thread";
@import 'media'; @import "media";
@import 'video'; @import "video";
@import 'embed'; @import "embed";
@import 'card'; @import "card";
@import 'poll'; @import "poll";
@import 'quote'; @import "quote";
@import 'community_note'; @import "community_note";
.tweet-body { .tweet-body {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
margin-left: 58px; margin-left: 58px;
pointer-events: none; pointer-events: none;
z-index: 1; z-index: 1;
} }
.tweet-content { .tweet-content {
font-family: $font_3; font-family: $font_3;
line-height: 1.3em; line-height: 1.3em;
pointer-events: all; pointer-events: all;
display: inline; display: inline;
} }
.tweet-bidi { .tweet-bidi {
display: block !important; display: block !important;
} }
.tweet-header { .tweet-header {
padding: 0; padding: 0;
vertical-align: bottom; vertical-align: bottom;
flex-basis: 100%; flex-basis: 100%;
margin-bottom: .2em; margin-bottom: 0.2em;
a { a {
display: inline-block; display: inline-block;
word-break: break-all; word-break: break-all;
max-width: 100%; max-width: 100%;
pointer-events: all; pointer-events: all;
} }
} }
.tweet-name-row { .tweet-name-row {
padding: 0; padding: 0;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.fullname-and-username { .fullname-and-username {
display: flex; display: flex;
min-width: 0; min-width: 0;
} }
.fullname { .fullname {
@include ellipsis; @include ellipsis;
flex-shrink: 2; flex-shrink: 2;
max-width: 80%; max-width: 80%;
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 700;
color: var(--fg_color); color: var(--fg_color);
} }
.username { .username {
@include ellipsis; @include ellipsis;
min-width: 1.6em; min-width: 1.6em;
margin-left: .4em; margin-left: 0.4em;
word-wrap: normal; word-wrap: normal;
}
.user-automated {
@include ellipsis;
min-width: 1px;
margin-left: 0.4em;
color: var(--fg_faded);
} }
.tweet-date { .tweet-date {
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
margin-left: 4px; margin-left: 4px;
} }
.tweet-date a, .username, .show-more a { .tweet-date a,
color: var(--fg_dark); .username,
.show-more a {
color: var(--fg_dark);
} }
.tweet-published { .tweet-published {
margin: 0; margin: 0;
margin-top: 5px; margin-top: 5px;
color: var(--grey); color: var(--grey);
pointer-events: all; pointer-events: all;
} }
.tweet-avatar { .tweet-avatar {
display: contents !important; display: contents !important;
img { img {
float: left; float: left;
margin-top: 3px; margin-top: 3px;
margin-left: -58px; margin-left: -58px;
width: 48px; width: 48px;
height: 48px; height: 48px;
} }
} }
.avatar { .avatar {
&.round { &.round {
border-radius: 50%; border-radius: 50%;
-webkit-user-select: none; -webkit-user-select: none;
} }
&.mini { &.mini {
position: unset; position: unset;
margin-right: 5px; margin-right: 5px;
margin-top: -1px; margin-top: -1px;
width: 20px; width: 20px;
height: 20px; height: 20px;
} }
} }
.tweet-embed { .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; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; max-height: calc(100vh - 0.75em * 2);
height: 100%; }
background-color: var(--bg_panel);
.tweet-content { .card-image img {
font-size: 18px; height: auto;
} }
.tweet-body { .avatar {
display: flex; position: absolute;
flex-direction: column; }
max-height: calc(100vh - 0.75em * 2);
}
.card-image img {
height: auto;
}
.avatar {
position: absolute;
}
} }
.attribution { .attribution {
display: flex; display: flex;
pointer-events: all; pointer-events: all;
margin: 5px 0; margin: 5px 0;
strong { strong {
color: var(--fg_color); color: var(--fg_color);
} }
} }
.media-tag-block { .media-tag-block {
padding-top: 5px; padding-top: 5px;
pointer-events: all; pointer-events: all;
color: var(--fg_faded);
.icon-container {
padding-right: 2px;
}
.media-tag,
.icon-container {
color: var(--fg_faded); color: var(--fg_faded);
}
.icon-container {
padding-right: 2px;
}
.media-tag, .icon-container {
color: var(--fg_faded);
}
} }
.timeline-container .media-tag-block { .timeline-container .media-tag-block {
font-size: 13px; font-size: 13px;
} }
.tweet-geo { .tweet-geo {
color: var(--fg_faded); color: var(--fg_faded);
} }
.replying-to { .replying-to {
color: var(--fg_faded); color: var(--fg_faded);
margin: -2px 0 4px; margin: -2px 0 4px;
a { a {
pointer-events: all; pointer-events: all;
} }
} }
.retweet-header, .pinned, .tweet-stats { .retweet-header,
align-content: center; .pinned,
color: var(--grey); .tweet-stats {
display: flex; align-content: center;
flex-shrink: 0; color: var(--grey);
flex-wrap: wrap; display: flex;
font-size: 14px; flex-shrink: 0;
font-weight: 600; flex-wrap: wrap;
line-height: 22px; font-size: 14px;
font-weight: 600;
line-height: 22px;
span { span {
@include ellipsis; @include ellipsis;
} }
} }
.retweet-header { .retweet-header {
margin-top: -5px !important; margin-top: -5px !important;
} }
.tweet-stats { .tweet-stats {
margin-bottom: -3px; margin-bottom: -3px;
-webkit-user-select: none; -webkit-user-select: none;
} }
.tweet-stat { .tweet-stat {
padding-top: 5px; padding-top: 5px;
min-width: 1em; min-width: 1em;
margin-right: 0.8em; margin-right: 0.8em;
pointer-events: all; pointer-events: all;
} }
.show-thread { .show-thread {
display: block; display: block;
pointer-events: all; pointer-events: all;
padding-top: 2px; padding-top: 2px;
} }
.unavailable-box { .unavailable-box {
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 12px; padding: 12px;
border: solid 1px var(--dark_grey); border: solid 1px var(--dark_grey);
box-sizing: border-box; box-sizing: border-box;
border-radius: 10px; border-radius: 10px;
background-color: var(--bg_color); background-color: var(--bg_color);
z-index: 2; z-index: 2;
} }
.tweet-link { .tweet-link {
height: 100%; height: 100%;
width: 100%; width: 100%;
left: 0; left: 0;
top: 0; top: 0;
position: absolute; position: absolute;
-webkit-user-select: none; -webkit-user-select: none;
&:hover { &:hover {
background-color: var(--bg_hover); background-color: var(--bg_hover);
} }
} }

View File

@ -99,6 +99,8 @@ type
protected*: bool protected*: bool
suspended*: bool suspended*: bool
joinDate*: DateTime joinDate*: DateTime
bot*: bool
botOwner*: string
VideoType* = enum VideoType* = enum
m3u8 = "application/x-mpegURL" m3u8 = "application/x-mpegURL"

View File

@ -221,13 +221,18 @@ proc getMastoAPIUser*(user: User, cfg: Config): JsonNode =
website["verified_at"] = newJNull() website["verified_at"] = newJNull()
fields.add(website) fields.add(website)
if user.botOwner.len > 0:
var botOwner = newJObject()
botOwner["name"] = %"Automated by"
botOwner["value"] = %(&"<a href=\"{getUrlPrefix(cfg)}/{user.botOwner}\" translate=\"no\">{user.botOwner}</a>")
var userJson = newJObject() var userJson = newJObject()
userJson["id"] = %user.id userJson["id"] = %user.id
userJson["username"] = %user.username userJson["username"] = %user.username
userJson["acct"] = %user.username userJson["acct"] = %user.username
userJson["display_name"] = %user.fullname userJson["display_name"] = %user.fullname
userJson["locked"] = %user.protected userJson["locked"] = %user.protected
userJson["bot"] = %false # TODO? userJson["bot"] = %user.bot
userJson["discoverable"] = %true userJson["discoverable"] = %true
userJson["indexable"] = %false userJson["indexable"] = %false
userJson["group"] = %false userJson["group"] = %false

View File

@ -35,6 +35,15 @@ proc renderUserCard*(user: User; prefs: Prefs; path: string): VNode =
buttonReferer "/unfollow/" & user.username, "Unfollow", path, "profile-card-follow-button" buttonReferer "/unfollow/" & user.username, "Unfollow", path, "profile-card-follow-button"
tdiv(class="profile-card-extra"): 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: if user.bio.len > 0:
tdiv(class="profile-bio"): tdiv(class="profile-bio"):
p(dir="auto"): p(dir="auto"):

View File

@ -34,6 +34,8 @@ proc renderHeader(tweet: Tweet; retweet: string; pinned: bool; prefs: Prefs): VN
tdiv(class="fullname-and-username"): tdiv(class="fullname-and-username"):
linkUser(tweet.user, class="fullname") linkUser(tweet.user, class="fullname")
linkUser(tweet.user, class="username") linkUser(tweet.user, class="username")
if tweet.user.bot:
tdiv(class="user-automated"): icon "cog", "Automated"
span(class="tweet-date"): span(class="tweet-date"):
a(href=getLink(tweet), title=tweet.getTime): a(href=getLink(tweet), title=tweet.getTime):