twitterchiver/viewer: support quoted tweets

This commit is contained in:
Luke Granger-Brown 2020-10-22 21:38:35 +00:00
parent 7003f84fb7
commit 787e89c24d
2 changed files with 105 additions and 33 deletions

View file

@ -14,12 +14,12 @@ html, body {
display: block; display: block;
padding-top: 8px; padding-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
border-bottom: 1px solid rgb(136, 153, 166); border-bottom: 1px solid rgb(56, 68, 77);
width: clamp(calc(45ch+64px), 100%, calc(75ch+64px)); width: clamp(calc(45ch+64px), 100%, calc(75ch+64px));
} }
.tweet-list-item:first-of-type { .tweet-list-item:first-of-type {
margin-top: 8px; margin-top: 8px;
border-top: 1px solid rgb(136, 153, 166); border-top: 1px solid rgb(56, 68, 77);
} }
.tweet { .tweet {
margin-left: 64px; margin-left: 64px;
@ -53,10 +53,10 @@ a:hover {
.byline-link:hover .byline-name { .byline-link:hover .byline-name {
text-decoration: underline; text-decoration: underline;
} }
.byline-name { .byline-name, .quoted-byline-name {
color: white; color: white;
} }
.byline, .retweeted-byline, .retweeted-icon { .byline, .retweeted-byline, .retweeted-icon, .quoted-byline {
color: rgb(136, 153, 166); color: rgb(136, 153, 166);
} }
.retweeted-byline { .retweeted-byline {
@ -95,6 +95,24 @@ a:hover {
.media-video { .media-video {
width: 100%; width: 100%;
} }
.quoted-tweet {
border-radius: 15px;
border-color: rgb(56, 68, 77);
border-width: 1px;
border-style: solid;
margin-top: 10px;
padding: 8px;
}
.quoted-byline {
margin-bottom: 6px;
}
.quoted-tweet-author-img {
border-radius: 100%;
height: 20px;
vertical-align: bottom;
margin-right: 2px;
}
</style> </style>
<h1>Twitterchiver: {{.TwitterUsername}}</h1> <h1>Twitterchiver: {{.TwitterUsername}}</h1>
{{if .Query}} {{if .Query}}
@ -127,9 +145,23 @@ a:hover {
<div class="content"><!-- <div class="content"><!--
-->{{call $.FormatTweetText $status.full_text $status}}<!-- -->{{call $.FormatTweetText $status.full_text $status}}<!--
--></div> --></div>
{{if .Object.extended_entities}} {{if .QuotedObject}}
<div class="quoted-tweet">
<div class="quoted-byline">
<img class="quoted-tweet-author-img" src="{{.QuotedObject.user.profile_image_url_https}}">
<strong class="quoted-byline-name">{{.QuotedObject.user.name}}</strong>
<span class="quoted-byline-username">@{{.QuotedObject.user.screen_name}}
&middot;
<time datetime="{{.QuotedCreatedAt.Format "2006-01-02T15:04:05-0700"}}">{{.QuotedCreatedAtFriendly}}</time>
</div>
<div class="quoted-content"><!--
-->{{if .QuotedObject.full_text}}{{.QuotedObject.full_text}}{{else}}{{.QuotedObject.text}}{{end}}<!--
--></div>
</div>
{{end}}
{{if $status.extended_entities}}
<div class="media"> <div class="media">
{{range $entity := .Object.extended_entities.media}} {{range $entity := $status.extended_entities.media}}
<div class="media-item"> <div class="media-item">
{{if eq $entity.type "video"}} {{if eq $entity.type "video"}}
<video controls poster="{{rewriteURL $entity.media_url_https}}" class="media-video"> <video controls poster="{{rewriteURL $entity.media_url_https}}" class="media-video">

View file

@ -256,17 +256,25 @@ SELECT
t.id, t.id,
t.text, t.text,
t.object, t.object,
CAST(COALESCE(t.object->'retweeted_status'->>'created_at', t.object->>'created_at') AS timestamp with time zone) created_at rt.object retweet_object,
qt.object quote_object,
CAST(COALESCE(t.object->'retweeted_status'->>'created_at', t.object->>'created_at') AS timestamp with time zone) created_at,
CAST(qt.object->>'created_at' AS timestamp with time zone) quoted_created_at
FROM FROM
user_accounts_tweets uat user_accounts_tweets uat
INNER JOIN INNER JOIN
user_accounts ua ON ua.userid=uat.userid user_accounts ua ON ua.userid=uat.userid
INNER JOIN tweets t ON t.id=uat.tweetid INNER JOIN
tweets t ON t.id=uat.tweetid
LEFT JOIN
tweets rt ON rt.id=CAST(t.object->'retweeted_status'->>'id' AS BIGINT)
LEFT JOIN
tweets qt ON qt.id=CAST(t.object->>'quoted_status_id' AS BIGINT)
WHERE 1=1 WHERE 1=1
AND ua.username=$1 AND ua.username=$1
AND uat.on_timeline AND uat.on_timeline
AND ($3::bigint=0 OR t.id <= $3::bigint) AND ($3::bigint=0 OR t.id <= $3::bigint)
AND ($4='' OR ($4<>'' AND (to_tsvector('english', text) @@ to_tsquery('english', $4) OR to_tsvector('english', object->'retweeted_status'->>'full_text') @@ to_tsquery('english', $4) OR object->'user'->>'screen_name'=$4))) AND ($4='' OR ($4<>'' AND (to_tsvector('english', t.text) @@ to_tsquery('english', $4) OR to_tsvector('english', t.object->'retweeted_status'->>'full_text') @@ to_tsquery('english', $4) OR t.object->'user'->>'screen_name'=$4)))
ORDER BY t.id DESC ORDER BY t.id DESC
LIMIT $2 LIMIT $2
`, twitterUser, pageSize+1, startFrom, query) `, twitterUser, pageSize+1, startFrom, query)
@ -277,11 +285,15 @@ LIMIT $2
defer rows.Close() defer rows.Close()
type tweet struct { type tweet struct {
ID int64 ID int64
Text string Text string
CreatedAt time.Time CreatedAt time.Time
CreatedAtFriendly string CreatedAtFriendly string
Object interface{} Object interface{}
RetweetedObject interface{}
QuotedObject interface{}
QuotedCreatedAt time.Time
QuotedCreatedAtFriendly string
} }
type twitterData struct { type twitterData struct {
TwitterUsername string TwitterUsername string
@ -447,29 +459,40 @@ LIMIT $2
now := time.Now() now := time.Now()
for rows.Next() { for rows.Next() {
var t tweet var t tweet
var o pgtype.JSONB var o, ro, qo pgtype.JSONB
if err := rows.Scan(&t.ID, &t.Text, &o, &t.CreatedAt); err != nil { var qca pgtype.Timestamptz
if err := rows.Scan(&t.ID, &t.Text, &o, &ro, &qo, &t.CreatedAt, &qca); err != nil {
writeError(rw, http.StatusInternalServerError, "reading from database", err) writeError(rw, http.StatusInternalServerError, "reading from database", err)
return return
} }
if err := json.Unmarshal(o.Bytes, &t.Object); err != nil { if o.Status == pgtype.Present {
writeError(rw, http.StatusInternalServerError, "parsing JSON from database", err) var oo interface{}
return if err := json.Unmarshal(o.Bytes, &oo); err != nil {
writeError(rw, http.StatusInternalServerError, "parsing JSON from database (object)", err)
return
}
t.Object = oo
t.CreatedAtFriendly = agoFriendly(now, t.CreatedAt)
} }
ago := now.Sub(t.CreatedAt) if ro.Status == pgtype.Present {
switch { var oo interface{}
case t.CreatedAt.Year() != now.Year(): if err := json.Unmarshal(ro.Bytes, &oo); err != nil {
t.CreatedAtFriendly = t.CreatedAt.Format("Jan 2, 2006") writeError(rw, http.StatusInternalServerError, "parsing JSON from database (retweeted object)", err)
case t.CreatedAt.YearDay() != now.YearDay(): return
t.CreatedAtFriendly = t.CreatedAt.Format("Jan 2") }
case ago.Hours() >= 1.0: t.RetweetedObject = oo
t.CreatedAtFriendly = fmt.Sprintf("%dh", int(ago.Hours())) }
case ago.Minutes() >= 1.0: if qo.Status == pgtype.Present {
t.CreatedAtFriendly = fmt.Sprintf("%dm", int(ago.Minutes())) var oo interface{}
case ago.Seconds() >= 0.0: if err := json.Unmarshal(qo.Bytes, &oo); err != nil {
t.CreatedAtFriendly = fmt.Sprintf("%ds", int(ago.Seconds())) writeError(rw, http.StatusInternalServerError, "parsing JSON from database (quoted object)", err)
default: return
t.CreatedAtFriendly = fmt.Sprintf("in %ds", -int(ago.Seconds())) }
t.QuotedObject = oo
}
if qca.Status == pgtype.Present {
t.QuotedCreatedAt = qca.Time
t.QuotedCreatedAtFriendly = agoFriendly(now, qca.Time)
} }
td.Tweets = append(td.Tweets, t) td.Tweets = append(td.Tweets, t)
} }
@ -488,3 +511,20 @@ LIMIT $2
log.Printf("now listening on :8080") log.Printf("now listening on :8080")
log.Print(http.ListenAndServe(":8080", r)) log.Print(http.ListenAndServe(":8080", r))
} }
func agoFriendly(now, at time.Time) string {
ago := now.Sub(at)
switch {
case at.Year() != now.Year():
return at.Format("Jan 2, 2006")
case at.YearDay() != now.YearDay():
return at.Format("Jan 2")
case ago.Hours() >= 1.0:
return fmt.Sprintf("%dh", int(ago.Hours()))
case ago.Minutes() >= 1.0:
return fmt.Sprintf("%dm", int(ago.Minutes()))
case ago.Seconds() >= 0.0:
return fmt.Sprintf("%ds", int(ago.Seconds()))
default:
return fmt.Sprintf("in %ds", -int(ago.Seconds()))
}
}