feat: support quoted tweets
This commit is contained in:
parent
607f063abc
commit
749758fca4
195
main.go
195
main.go
@ -94,6 +94,18 @@ func getTweet(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
processTweet(&tweet)
|
||||||
|
|
||||||
|
w.Header().Set("Content-type", "image/svg+xml")
|
||||||
|
_, err = w.Write(renderTemplate(tweet, false))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processTweet(tweet *anaconda.Tweet) {
|
||||||
gr := uniseg.NewGraphemes(tweet.FullText)
|
gr := uniseg.NewGraphemes(tweet.FullText)
|
||||||
count := 0
|
count := 0
|
||||||
displayText := ""
|
displayText := ""
|
||||||
@ -109,7 +121,7 @@ func getTweet(w http.ResponseWriter, r *http.Request) {
|
|||||||
tweet.FullText = strings.ReplaceAll(tweet.FullText, "@"+user.Screen_name, fmt.Sprintf("<a rel=\"noopener\" target=\"_blank\" href=\"https://twitter.com/%s/\">@%s</a>", user.Screen_name, user.Screen_name))
|
tweet.FullText = strings.ReplaceAll(tweet.FullText, "@"+user.Screen_name, fmt.Sprintf("<a rel=\"noopener\" target=\"_blank\" href=\"https://twitter.com/%s/\">@%s</a>", user.Screen_name, user.Screen_name))
|
||||||
}
|
}
|
||||||
for _, url := range tweet.Entities.Urls {
|
for _, url := range tweet.Entities.Urls {
|
||||||
tweet.FullText = strings.ReplaceAll(tweet.FullText, url.Url, fmt.Sprintf("<a rel=\"noopener\" target=\"_blank\" href=\"https://twitter.com/%s/\">%s</a>", url.Expanded_url, url.Display_url))
|
tweet.FullText = strings.ReplaceAll(tweet.FullText, url.Url, fmt.Sprintf("<a rel=\"noopener\" target=\"_blank\" href=\"%s\">%s</a>", url.Expanded_url, url.Display_url))
|
||||||
}
|
}
|
||||||
for _, hashtag := range tweet.Entities.Hashtags {
|
for _, hashtag := range tweet.Entities.Hashtags {
|
||||||
tweet.FullText = strings.ReplaceAll(tweet.FullText, "#"+hashtag.Text, fmt.Sprintf("<a rel=\"noopener\" target=\"_blank\" href=\"https://twitter.com/hashtag/%s\">#%s</a>", hashtag.Text, hashtag.Text))
|
tweet.FullText = strings.ReplaceAll(tweet.FullText, "#"+hashtag.Text, fmt.Sprintf("<a rel=\"noopener\" target=\"_blank\" href=\"https://twitter.com/hashtag/%s\">#%s</a>", hashtag.Text, hashtag.Text))
|
||||||
@ -117,6 +129,13 @@ func getTweet(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
tweet.FullText = strings.ReplaceAll(tweet.FullText, "\n", "<br />")
|
tweet.FullText = strings.ReplaceAll(tweet.FullText, "\n", "<br />")
|
||||||
|
|
||||||
|
if tweet.QuotedStatus != nil {
|
||||||
|
processTweet(tweet.QuotedStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTemplate(tweet anaconda.Tweet, isQuoted bool) []byte {
|
||||||
templateFuncs := template.FuncMap{
|
templateFuncs := template.FuncMap{
|
||||||
"base64": func(url string) string {
|
"base64": func(url string) string {
|
||||||
res, err := http.Get(url)
|
res, err := http.Get(url)
|
||||||
@ -139,79 +158,22 @@ func getTweet(w http.ResponseWriter, r *http.Request) {
|
|||||||
return template.HTML(in)
|
return template.HTML(in)
|
||||||
},
|
},
|
||||||
"calculateHeight": func(tweet anaconda.Tweet) string {
|
"calculateHeight": func(tweet anaconda.Tweet) string {
|
||||||
height := 64.0 /* Avatar */ + 20 /* footer */ + 46 /* text margin */ + 22 /* margin */
|
return fmt.Sprintf("%dpx", calculateHeight(tweet))
|
||||||
|
},
|
||||||
lineWidth := 0.0
|
"renderTweet": func(tweet anaconda.Tweet) template.HTML {
|
||||||
lineHeight := 28.0
|
return template.HTML(string(renderTemplate(tweet, true)))
|
||||||
tweetText := strings.ReplaceAll(tweet.FullText, "<br />", " \n")
|
},
|
||||||
tweetText = strip.StripTags(tweetText)
|
"tweetWidth": func() string {
|
||||||
words := regexp.MustCompile(`[ |-]`).Split(tweetText, -1)
|
if isQuoted {
|
||||||
for _, word := range words {
|
return "450px"
|
||||||
if len(emoji.FindAll(word)) > 0 {
|
|
||||||
lineHeight = 32.0
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(word, "\n") {
|
|
||||||
height += lineHeight
|
|
||||||
lineHeight = 28.0
|
|
||||||
lineWidth = 0
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
chars := strings.Split(word, "")
|
|
||||||
wordWidth := 0.0
|
|
||||||
for _, char := range chars {
|
|
||||||
wordWidth += getCharWidth(char)
|
|
||||||
}
|
|
||||||
|
|
||||||
if wordWidth > 435 {
|
|
||||||
height += (lineHeight * (math.Ceil(wordWidth/435) + 1))
|
|
||||||
lineHeight = 28.0
|
|
||||||
lineWidth = 0
|
|
||||||
} else if lineWidth+getCharWidth(" ")+wordWidth > 435 {
|
|
||||||
height += lineHeight
|
|
||||||
lineHeight = 28.0
|
|
||||||
lineWidth = wordWidth
|
|
||||||
} else {
|
|
||||||
lineWidth += wordWidth
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if lineWidth > 0 {
|
return "499px"
|
||||||
height += lineHeight
|
},
|
||||||
|
"className": func() string {
|
||||||
|
if isQuoted {
|
||||||
|
return "subtweet"
|
||||||
}
|
}
|
||||||
|
return "tweetsvg"
|
||||||
if tweet.InReplyToScreenName != "" {
|
|
||||||
height += 42
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, img := range tweet.ExtendedEntities.Media {
|
|
||||||
ratio := float64(img.Sizes.Small.W) / 468
|
|
||||||
tweet.ExtendedEntities.Media[i].Sizes.Small.W = 468
|
|
||||||
tweet.ExtendedEntities.Media[i].Sizes.Small.H = int((float64(img.Sizes.Small.H) / ratio) + 5.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tweet.ExtendedEntities.Media) > 1 {
|
|
||||||
for i, img := range tweet.ExtendedEntities.Media {
|
|
||||||
tweet.ExtendedEntities.Media[i].Sizes.Small.W = (img.Sizes.Small.W / 2) - 20
|
|
||||||
tweet.ExtendedEntities.Media[i].Sizes.Small.H = (img.Sizes.Small.H / 2) - 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(tweet.ExtendedEntities.Media) {
|
|
||||||
case 1:
|
|
||||||
height += float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H)
|
|
||||||
case 2:
|
|
||||||
height += math.Max(float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H), float64(tweet.ExtendedEntities.Media[1].Sizes.Small.H)) + 5
|
|
||||||
case 3:
|
|
||||||
height += math.Max(float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H), float64(tweet.ExtendedEntities.Media[1].Sizes.Small.H)) + 5
|
|
||||||
height += float64(tweet.ExtendedEntities.Media[2].Sizes.Small.H) + 35
|
|
||||||
case 4:
|
|
||||||
height += math.Max(float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H), float64(tweet.ExtendedEntities.Media[1].Sizes.Small.H)) + 10
|
|
||||||
height += math.Max(float64(tweet.ExtendedEntities.Media[2].Sizes.Small.H), float64(tweet.ExtendedEntities.Media[3].Sizes.Small.H)) + 10
|
|
||||||
height += 7
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%dpx", int64(height))
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,13 +182,90 @@ func getTweet(w http.ResponseWriter, r *http.Request) {
|
|||||||
Funcs(templateFuncs).
|
Funcs(templateFuncs).
|
||||||
ParseFS(content, "tweet.svg.tmpl"))
|
ParseFS(content, "tweet.svg.tmpl"))
|
||||||
|
|
||||||
w.Header().Set("Content-type", "image/svg+xml")
|
var buf bytes.Buffer
|
||||||
err = t.Execute(w, tweet)
|
t.Execute(&buf, tweet)
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
return buf.Bytes()
|
||||||
w.WriteHeader(500)
|
}
|
||||||
return
|
|
||||||
|
func calculateHeight(tweet anaconda.Tweet) int64 {
|
||||||
|
height := 64.0 /* Avatar */ + 20 /* footer */ + 46 /* text margin */ + 22 /* margin */
|
||||||
|
|
||||||
|
lineWidth := 0.0
|
||||||
|
lineHeight := 28.0
|
||||||
|
tweetText := strings.ReplaceAll(tweet.FullText, "<br />", " \n")
|
||||||
|
tweetText = strip.StripTags(tweetText)
|
||||||
|
words := regexp.MustCompile(`[ |-]`).Split(tweetText, -1)
|
||||||
|
for _, word := range words {
|
||||||
|
if len(emoji.FindAll(word)) > 0 {
|
||||||
|
lineHeight = 32.0
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(word, "\n") {
|
||||||
|
height += lineHeight
|
||||||
|
lineHeight = 28.0
|
||||||
|
lineWidth = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
chars := strings.Split(word, "")
|
||||||
|
wordWidth := 0.0
|
||||||
|
for _, char := range chars {
|
||||||
|
wordWidth += getCharWidth(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wordWidth > 435 {
|
||||||
|
height += (lineHeight * (math.Ceil(wordWidth/435) + 1))
|
||||||
|
lineHeight = 28.0
|
||||||
|
lineWidth = 0
|
||||||
|
} else if lineWidth+getCharWidth(" ")+wordWidth > 435 {
|
||||||
|
height += lineHeight
|
||||||
|
lineHeight = 28.0
|
||||||
|
lineWidth = wordWidth
|
||||||
|
} else {
|
||||||
|
lineWidth += wordWidth
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if lineWidth > 0 {
|
||||||
|
height += lineHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
if tweet.InReplyToScreenName != "" {
|
||||||
|
height += 42
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, img := range tweet.ExtendedEntities.Media {
|
||||||
|
ratio := float64(img.Sizes.Small.W) / 468
|
||||||
|
tweet.ExtendedEntities.Media[i].Sizes.Small.W = 468
|
||||||
|
tweet.ExtendedEntities.Media[i].Sizes.Small.H = int((float64(img.Sizes.Small.H) / ratio) + 5.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tweet.ExtendedEntities.Media) > 1 {
|
||||||
|
for i, img := range tweet.ExtendedEntities.Media {
|
||||||
|
tweet.ExtendedEntities.Media[i].Sizes.Small.W = (img.Sizes.Small.W / 2) - 20
|
||||||
|
tweet.ExtendedEntities.Media[i].Sizes.Small.H = (img.Sizes.Small.H / 2) - 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(tweet.ExtendedEntities.Media) {
|
||||||
|
case 1:
|
||||||
|
height += float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H)
|
||||||
|
case 2:
|
||||||
|
height += math.Max(float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H), float64(tweet.ExtendedEntities.Media[1].Sizes.Small.H)) + 5
|
||||||
|
case 3:
|
||||||
|
height += math.Max(float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H), float64(tweet.ExtendedEntities.Media[1].Sizes.Small.H)) + 5
|
||||||
|
height += float64(tweet.ExtendedEntities.Media[2].Sizes.Small.H) + 35
|
||||||
|
case 4:
|
||||||
|
height += math.Max(float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H), float64(tweet.ExtendedEntities.Media[1].Sizes.Small.H)) + 10
|
||||||
|
height += math.Max(float64(tweet.ExtendedEntities.Media[2].Sizes.Small.H), float64(tweet.ExtendedEntities.Media[3].Sizes.Small.H)) + 10
|
||||||
|
height += 7
|
||||||
|
}
|
||||||
|
|
||||||
|
if tweet.QuotedStatus != nil {
|
||||||
|
height += float64(calculateHeight(*tweet.QuotedStatus)) + 9
|
||||||
|
}
|
||||||
|
|
||||||
|
return int64(height)
|
||||||
}
|
}
|
||||||
|
|
||||||
func suspendedTweet(w http.ResponseWriter) {
|
func suspendedTweet(w http.ResponseWriter) {
|
||||||
|
@ -1,44 +1,49 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="499px" height="{{ calculateHeight . }}">
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{ tweetWidth }}" height="{{ calculateHeight . }}">
|
||||||
<foreignObject x="0" y="0" width="499px" height="100%" fill="#eade52">
|
<foreignObject x="0" y="0" width="{{ tweetWidth }}" height="100%" fill="#eade52">
|
||||||
<style>
|
<style>
|
||||||
.tweetsvg{clear:none;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;}
|
.{{ className }}{clear:none;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;}
|
||||||
.tweetsvg.text{font-size: 23px;}
|
.{{ className }}.text{font-size: 23px;}
|
||||||
a.tweetsvg{color: rgb(27, 149, 224); text-decoration:none;}
|
a.{{ className }}{color: rgb(27, 149, 224); text-decoration:none;}
|
||||||
.tweetsvg a { color: #1da1f2; }
|
.{{ className }} a { color: #1da1f2; }
|
||||||
blockquote.tweetsvg{margin:1px; background-color:#fefefe; border-radius:2%; border-style:solid; border-width:.1em; border-color:#ddd; padding:1em; font-family:sans; width:29rem}
|
blockquote.{{ className }}{margin:1px; background-color:#fefefe; border-radius:2%; border-style:solid; border-width:.1em; border-color:#ddd; padding:1em; font-family:sans; width:29rem}
|
||||||
.avatar-tweetsvg{float:left; width:4rem; height:4rem; border-radius:50%;margin-right:.5rem;;margin-bottom:.5rem;border-style: solid; border-width:.1em; border-color:#ddd;}
|
blockquote.subtweet{width:26rem; padding:0.8em;}
|
||||||
h1.tweetsvg{margin:0;font-size:15px;text-decoration:none;color:#000;}
|
.avatar-{{ className }}{float:left; width:4rem; height:4rem; border-radius:50%;margin-right:.5rem;;margin-bottom:.5rem;border-style: solid; border-width:.1em; border-color:#ddd;}
|
||||||
h2.tweetsvg{margin:0;font-size:15px;font-weight:normal;text-decoration:none;color:rgb(101, 119, 134);}
|
h1.{{ className }}{margin:0;font-size:15px;text-decoration:none;color:#000;}
|
||||||
p.tweetsvg{font-size:1rem; clear:both;}
|
h2.{{ className }}{margin:0;font-size:15px;font-weight:normal;text-decoration:none;color:rgb(101, 119, 134);}
|
||||||
hr.tweetsvg{color:#ddd;}
|
p.{{ className }}{font-size:1rem; clear:both;}
|
||||||
.media-tweetsvg{border-radius:2%; max-width:100%;border-radius: 2%; border-style: solid; border-width: .1em; border-color: #ddd;}
|
hr.{{ className }}{color:#ddd;}
|
||||||
time.tweetsvg{font-size:15px;margin:0;margin-left: 2px;padding-bottom:1rem;color:rgb(101, 119, 134);text-decoration:none;}
|
.media-{{ className }}{border-radius:2%; max-width:100%;border-radius: 2%; border-style: solid; border-width: .1em; border-color: #ddd;}
|
||||||
.tweetsvg.reply{font-size:15px;color:rgb(110, 118, 125);}
|
time.{{ className }}{font-size:15px;margin:0;margin-left: 2px;padding-bottom:1rem;color:rgb(101, 119, 134);text-decoration:none;}
|
||||||
.tweetsvg.footer{display:block;}
|
.{{ className }}.reply{font-size:15px;color:rgb(110, 118, 125);}
|
||||||
|
.{{ className }}.footer{display:block;}
|
||||||
</style>
|
</style>
|
||||||
<blockquote class="tweetsvg" xmlns="http://www.w3.org/1999/xhtml">
|
<blockquote class="{{ className }}" xmlns="http://www.w3.org/1999/xhtml">
|
||||||
<a rel="noopener" target="_blank" class="tweetsvg" href="https://twitter.com/{{ .User.ScreenName }}/"><img class="avatar-tweetsvg" alt="" src="data:image/jpeg;base64,{{ base64 .User.ProfileImageUrlHttps }}" /></a>
|
<a rel="noopener" target="_blank" class="{{ className }}" href="https://twitter.com/{{ .User.ScreenName }}/"><img class="avatar-{{ className }}" alt="" src="data:image/jpeg;base64,{{ base64 .User.ProfileImageUrlHttps }}" /></a>
|
||||||
|
|
||||||
<a rel="noopener" target="_blank" class="tweetsvg" href="https://twitter.com/{{ .User.ScreenName }}/"><h1 class="tweetsvg">{{ .User.Name }}</h1></a>
|
<a rel="noopener" target="_blank" class="{{ className }}" href="https://twitter.com/{{ .User.ScreenName }}/"><h1 class="{{ className }}">{{ .User.Name }}</h1></a>
|
||||||
|
|
||||||
<a rel="noopener" target="_blank" class="tweetsvg" href="https://twitter.com/{{ .User.ScreenName }}/"><h2 class="tweetsvg">@{{ .User.ScreenName }}</h2></a>
|
<a rel="noopener" target="_blank" class="{{ className }}" href="https://twitter.com/{{ .User.ScreenName }}/"><h2 class="{{ className }}">@{{ .User.ScreenName }}</h2></a>
|
||||||
|
|
||||||
{{ if .InReplyToScreenName }}
|
{{ if .InReplyToScreenName }}
|
||||||
<p class="tweetsvg reply">Replying to <a rel="noopener" target="_blank" href="https://twitter.com/{{ .InReplyToScreenName }}/">@{{ .InReplyToScreenName }}</a></p>
|
<p class="{{ className }} reply">Replying to <a rel="noopener" target="_blank" href="https://twitter.com/{{ .InReplyToScreenName }}/">@{{ .InReplyToScreenName }}</a></p>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
<p class="tweetsvg text">{{ html .FullText }}</p>
|
<p class="{{ className }} text">{{ html .FullText }}</p>
|
||||||
|
|
||||||
|
{{ if .QuotedStatus }}
|
||||||
|
{{ renderTweet .QuotedStatus }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
{{ if .ExtendedEntities }}
|
{{ if .ExtendedEntities }}
|
||||||
{{ range .ExtendedEntities.Media }}
|
{{ range .ExtendedEntities.Media }}
|
||||||
|
|
||||||
<a rel="noopener" target="_blank" href="{{ .Media_url_https }}"><img class="media-tweetsvg" width="{{ .Sizes.Small.W }}" src="data:image/jpeg;base64,{{ base64 .Media_url }}" alt="{{ .ExtAltText }}"/></a>
|
<a rel="noopener" target="_blank" href="{{ .Media_url_https }}"><img class="media-{{ className }}" width="{{ .Sizes.Small.W }}" src="data:image/jpeg;base64,{{ base64 .Media_url }}" alt="{{ .ExtAltText }}"/></a>
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
<a rel="noopener" target="_blank" class="tweetsvg footer" href="https://twitter.com/{{ .User.ScreenName }}/status/{{ .Id }}">
|
<a rel="noopener" target="_blank" class="{{ className }} footer" href="https://twitter.com/{{ .User.ScreenName }}/status/{{ .Id }}">
|
||||||
<time class="tweetsvg" datetime="{{ isoDate .CreatedAt }}">{{ humanDate .CreatedAt }}</time>
|
<time class="{{ className }}" datetime="{{ isoDate .CreatedAt }}">{{ humanDate .CreatedAt }}</time>
|
||||||
</a>
|
</a>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</foreignObject>
|
</foreignObject>
|
||||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.1 KiB |
Loading…
Reference in New Issue
Block a user