/
server.go
135 lines (114 loc) · 3.4 KB
/
server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package jobs
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/xarantolus/spacex-hop-bot/config"
"github.com/xarantolus/spacex-hop-bot/consumer"
"github.com/xarantolus/spacex-hop-bot/match"
)
type httpServer struct {
twitter consumer.TwitterClient
processor *consumer.Processor
tweetChan chan<- match.TweetWrapper
}
func httpErrWrapper(f func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := f(w, r)
if err != nil {
log.Printf("[Error] in %s %s: %s", r.Method, r.URL.Path, err.Error())
if strings.Contains(r.URL.Path, "/api/") {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"error_message": err.Error(),
})
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
}
func parseTweetID(ustr string) (tweetID int64, err error) {
u, err := url.ParseRequestURI(ustr)
if err != nil {
return
}
if !strings.HasSuffix(u.Host, "twitter.com") {
return 0, fmt.Errorf("URL host must be *.twitter.com, but was %s", u.Host)
}
pathSplit := strings.Split(u.Path, "/")
if len(pathSplit) < 4 {
return 0, fmt.Errorf("URL does not point to a tweet")
}
parsedID, err := strconv.ParseInt(pathSplit[3], 10, 64)
if err != nil {
return 0, fmt.Errorf("URL does not contain a tweet ID: %w", err)
}
return parsedID, nil
}
func (h *httpServer) submitTweet(w http.ResponseWriter, r *http.Request) (err error) {
type incomingJSON struct {
TweetURL string `json:"url"`
}
body := new(incomingJSON)
err = json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(body)
if err != nil {
return
}
parsedID, err := parseTweetID(body.TweetURL)
if err != nil {
return
}
status, err := h.twitter.LoadStatus(parsedID)
// If we cannot load the tweet, it could be that we're blocked.
if err != nil || (status != nil && status.Retweeted) {
log.Printf("Unretweeting tweet with id %d\n", parsedID)
err = h.twitter.UnRetweet(parsedID)
if err != nil {
return
}
log.Printf("Unretweeted tweet with id %d\n", parsedID)
} else if status != nil {
// Put it into the matcher
select {
case h.tweetChan <- match.TweetWrapper{
TweetSource: match.TweetSourceUnknown,
Tweet: *status,
EnableLogging: true,
}:
break
case <-time.After(1 * time.Second):
return fmt.Errorf("could not send tweet on tweetChan: timeout")
}
} else {
return fmt.Errorf("could not load tweet with id %d", parsedID)
}
return
}
func (s *httpServer) stats(w http.ResponseWriter, r *http.Request) (err error) {
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(s.processor.Stats())
}
func RunWebServer(c config.Config, t consumer.TwitterClient, p *consumer.Processor, tweetChan chan<- match.TweetWrapper) {
defer panic("web server stopped running, but it should never do that")
server := &httpServer{
twitter: t,
tweetChan: tweetChan,
processor: p,
}
http.HandleFunc("/api/v1/tweet/submit", httpErrWrapper(server.submitTweet))
http.HandleFunc("/api/v1/stats", httpErrWrapper(server.stats))
port := strconv.Itoa(int(c.Server.Port))
log.Printf("[HTTP] Server listening on port %s", port)
err := http.ListenAndServe(":"+port, nil)
if err != nil {
panic("running web server: " + err.Error())
}
}