twitterchiver/viewer: support quoted tweets
This commit is contained in:
parent
7003f84fb7
commit
787e89c24d
2 changed files with 105 additions and 33 deletions
|
@ -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}}
|
||||||
|
·
|
||||||
|
<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">
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -282,6 +290,10 @@ LIMIT $2
|
||||||
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{}
|
||||||
|
if err := json.Unmarshal(o.Bytes, &oo); err != nil {
|
||||||
|
writeError(rw, http.StatusInternalServerError, "parsing JSON from database (object)", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ago := now.Sub(t.CreatedAt)
|
t.Object = oo
|
||||||
switch {
|
t.CreatedAtFriendly = agoFriendly(now, t.CreatedAt)
|
||||||
case t.CreatedAt.Year() != now.Year():
|
}
|
||||||
t.CreatedAtFriendly = t.CreatedAt.Format("Jan 2, 2006")
|
if ro.Status == pgtype.Present {
|
||||||
case t.CreatedAt.YearDay() != now.YearDay():
|
var oo interface{}
|
||||||
t.CreatedAtFriendly = t.CreatedAt.Format("Jan 2")
|
if err := json.Unmarshal(ro.Bytes, &oo); err != nil {
|
||||||
case ago.Hours() >= 1.0:
|
writeError(rw, http.StatusInternalServerError, "parsing JSON from database (retweeted object)", err)
|
||||||
t.CreatedAtFriendly = fmt.Sprintf("%dh", int(ago.Hours()))
|
return
|
||||||
case ago.Minutes() >= 1.0:
|
}
|
||||||
t.CreatedAtFriendly = fmt.Sprintf("%dm", int(ago.Minutes()))
|
t.RetweetedObject = oo
|
||||||
case ago.Seconds() >= 0.0:
|
}
|
||||||
t.CreatedAtFriendly = fmt.Sprintf("%ds", int(ago.Seconds()))
|
if qo.Status == pgtype.Present {
|
||||||
default:
|
var oo interface{}
|
||||||
t.CreatedAtFriendly = fmt.Sprintf("in %ds", -int(ago.Seconds()))
|
if err := json.Unmarshal(qo.Bytes, &oo); err != nil {
|
||||||
|
writeError(rw, http.StatusInternalServerError, "parsing JSON from database (quoted object)", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue