2021-02-18 18:32:36 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2021-03-21 09:13:33 +00:00
|
|
|
"embed"
|
2021-02-18 18:32:36 +00:00
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
"html/template"
|
|
|
|
"log"
|
2021-02-22 19:29:31 +00:00
|
|
|
"math"
|
2021-02-18 18:32:36 +00:00
|
|
|
"net/http"
|
|
|
|
"os"
|
2021-02-20 16:18:26 +00:00
|
|
|
"regexp"
|
2021-02-18 18:32:36 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/ChimeraCoder/anaconda"
|
2021-02-22 19:29:31 +00:00
|
|
|
strip "github.com/grokify/html-strip-tags-go"
|
2021-02-18 18:55:15 +00:00
|
|
|
"github.com/joho/godotenv"
|
2021-02-18 18:32:36 +00:00
|
|
|
)
|
|
|
|
|
2021-03-21 09:13:33 +00:00
|
|
|
//go:embed index.html tweet.svg.tmpl
|
|
|
|
|
|
|
|
var content embed.FS
|
|
|
|
|
2021-02-18 18:32:36 +00:00
|
|
|
var (
|
|
|
|
api *anaconda.TwitterApi
|
|
|
|
|
|
|
|
tweetDateLayout = "Mon Jan 2 15:04:05 -0700 2006"
|
|
|
|
|
2021-02-18 18:55:15 +00:00
|
|
|
port string
|
|
|
|
accessToken string
|
|
|
|
accessTokenSecret string
|
|
|
|
consumerKey string
|
|
|
|
consumerSecret string
|
2021-02-18 18:32:36 +00:00
|
|
|
)
|
|
|
|
|
2021-02-18 18:55:15 +00:00
|
|
|
func init() {
|
|
|
|
godotenv.Load(os.Getenv("DOTENV_DIR") + ".env")
|
|
|
|
|
|
|
|
port = os.Getenv("PORT")
|
|
|
|
accessToken = os.Getenv("ACCESS_TOKEN")
|
|
|
|
accessTokenSecret = os.Getenv("ACCESS_TOKEN_SECRET")
|
|
|
|
consumerKey = os.Getenv("CONSUMER_KEY")
|
|
|
|
consumerSecret = os.Getenv("CONSUMER_SECRET")
|
|
|
|
}
|
|
|
|
|
2021-02-18 18:32:36 +00:00
|
|
|
func main() {
|
|
|
|
if accessToken == "" || accessTokenSecret == "" || consumerKey == "" || consumerSecret == "" {
|
|
|
|
panic("Missing Twitter credentials")
|
|
|
|
}
|
|
|
|
|
|
|
|
api = anaconda.NewTwitterApiWithCredentials(accessToken, accessTokenSecret, consumerKey, consumerSecret)
|
|
|
|
|
|
|
|
if port == "" {
|
|
|
|
port = "8080"
|
|
|
|
}
|
|
|
|
|
2021-02-22 19:29:31 +00:00
|
|
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if len(r.URL.Path) > 1 {
|
|
|
|
getTweet(w, r)
|
|
|
|
} else {
|
2021-03-21 09:13:33 +00:00
|
|
|
body, _ := content.ReadFile("index.html")
|
2021-02-22 19:29:31 +00:00
|
|
|
w.Write(body)
|
|
|
|
}
|
|
|
|
})
|
2021-02-18 18:32:36 +00:00
|
|
|
fmt.Println("Server started at port " + port)
|
|
|
|
log.Fatal(http.ListenAndServe(":"+port, nil))
|
|
|
|
}
|
|
|
|
|
|
|
|
func getTweet(w http.ResponseWriter, r *http.Request) {
|
|
|
|
id := strings.ReplaceAll(r.URL.Path, "/", "")
|
|
|
|
i, err := strconv.ParseInt(id, 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(400)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
tweet, err := api.GetTweet(i, nil)
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(404)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-02-20 16:18:26 +00:00
|
|
|
re := regexp.MustCompile(`[\x{1F300}-\x{1F6FF}]`)
|
|
|
|
emojis := re.FindAllString(tweet.FullText, -1)
|
|
|
|
|
|
|
|
emojiCount := 0
|
|
|
|
for _, emoji := range emojis {
|
|
|
|
emojiCount += len([]byte(emoji)) - 1
|
|
|
|
}
|
|
|
|
tweet.FullText = tweet.FullText[tweet.DisplayTextRange[0] : tweet.DisplayTextRange[1]+emojiCount]
|
|
|
|
|
|
|
|
for _, user := range tweet.Entities.User_mentions {
|
2021-02-20 16:26:08 +00:00
|
|
|
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))
|
2021-02-20 16:18:26 +00:00
|
|
|
}
|
|
|
|
for _, url := range tweet.Entities.Urls {
|
2021-02-20 16:26:08 +00:00
|
|
|
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))
|
2021-02-20 16:18:26 +00:00
|
|
|
}
|
|
|
|
for _, hashtag := range tweet.Entities.Hashtags {
|
2021-02-20 16:26:08 +00:00
|
|
|
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))
|
2021-02-20 16:18:26 +00:00
|
|
|
}
|
|
|
|
|
2021-02-22 19:29:31 +00:00
|
|
|
tweet.FullText = strings.ReplaceAll(tweet.FullText, "\n", "<br />")
|
|
|
|
|
2021-02-18 18:32:36 +00:00
|
|
|
templateFuncs := template.FuncMap{
|
|
|
|
"base64": func(url string) string {
|
|
|
|
res, err := http.Get(url)
|
|
|
|
if err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
buf.ReadFrom(res.Body)
|
|
|
|
return base64.StdEncoding.EncodeToString(buf.Bytes())
|
|
|
|
},
|
|
|
|
"isoDate": func(date string) string {
|
|
|
|
t, _ := time.Parse(tweetDateLayout, date)
|
|
|
|
return t.Format(time.RFC3339)
|
|
|
|
},
|
|
|
|
"humanDate": func(date string) string {
|
|
|
|
t, _ := time.Parse(tweetDateLayout, date)
|
|
|
|
return t.Format("3:04 PM · Jan 2, 2006")
|
|
|
|
},
|
2021-02-20 16:18:26 +00:00
|
|
|
"html": func(in string) template.HTML {
|
|
|
|
return template.HTML(in)
|
|
|
|
},
|
2021-02-22 19:29:31 +00:00
|
|
|
"calculateHeight": func(tweet anaconda.Tweet) string {
|
|
|
|
height := 205.0
|
|
|
|
|
|
|
|
lines := math.Floor(float64(len(strip.StripTags(tweet.FullText))) / 40)
|
|
|
|
height += lines * 20
|
|
|
|
|
|
|
|
if tweet.InReplyToScreenName != "" {
|
|
|
|
height += 45
|
|
|
|
}
|
|
|
|
|
|
|
|
height += float64(strings.Count(tweet.FullText, "<br />") * 20)
|
|
|
|
|
|
|
|
for _, pic := range tweet.ExtendedEntities.Media {
|
|
|
|
ratio := float64(pic.Sizes.Small.W) / 464
|
|
|
|
height += float64(pic.Sizes.Small.H) / ratio
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Sprintf("%dpx", int64(height))
|
|
|
|
},
|
2021-02-18 18:32:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
t := template.Must(
|
|
|
|
template.New("tweet.svg.tmpl").
|
|
|
|
Funcs(templateFuncs).
|
2021-03-21 09:13:33 +00:00
|
|
|
ParseFS(content, "tweet.svg.tmpl"))
|
2021-02-18 18:32:36 +00:00
|
|
|
|
|
|
|
w.Header().Set("Content-type", "image/svg+xml")
|
|
|
|
err = t.Execute(w, tweet)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println(err)
|
|
|
|
w.WriteHeader(500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|