justlog/api/server.go

364 lines
8.9 KiB
Go
Raw Normal View History

2017-03-08 17:38:01 -03:00
package api
import (
2018-12-02 15:23:54 -03:00
"encoding/json"
"errors"
"fmt"
2017-03-08 17:38:01 -03:00
"net/http"
2020-05-31 08:58:32 -04:00
"net/url"
2018-12-02 15:23:54 -03:00
"strconv"
"strings"
"time"
2017-09-12 14:47:30 -03:00
2018-12-02 15:23:54 -03:00
"github.com/gempir/justlog/helix"
log "github.com/sirupsen/logrus"
2018-12-02 15:23:54 -03:00
2019-09-05 14:20:11 -04:00
"github.com/gempir/go-twitch-irc/v2"
2018-12-02 10:53:01 -03:00
"github.com/gempir/justlog/filelog"
2017-03-08 17:38:01 -03:00
)
// Server api server
2017-03-08 17:38:01 -03:00
type Server struct {
2018-12-02 10:53:01 -03:00
listenAddress string
logPath string
fileLogger *filelog.Logger
2018-12-02 15:23:54 -03:00
helixClient *helix.Client
2018-12-02 10:53:01 -03:00
channels []string
assets []string
assetHandler http.Handler
2017-03-08 17:38:01 -03:00
}
// NewServer create api Server
func NewServer(logPath string, listenAddress string, fileLogger *filelog.Logger, helixClient *helix.Client, channels []string) Server {
2017-03-08 17:38:01 -03:00
return Server{
2018-12-02 10:53:01 -03:00
listenAddress: listenAddress,
logPath: logPath,
fileLogger: fileLogger,
2018-12-02 15:23:54 -03:00
helixClient: helixClient,
channels: channels,
2020-03-01 10:53:53 -03:00
assets: []string{"/", "/bundle.js", "/favicon.ico"},
assetHandler: http.FileServer(assets),
}
}
// AddChannel adds a channel to the collection to output on the channels endpoint
func (s *Server) AddChannel(channel string) {
s.channels = append(s.channels, channel)
}
2020-02-25 08:36:10 -03:00
const (
responseTypeJSON = "json"
responseTypeText = "text"
responseTypeRaw = "raw"
)
2020-02-25 17:44:25 -03:00
var (
userHourLimit = 744.0
channelHourLimit = 24.0
)
type channel struct {
UserID string `json:"userID"`
Name string `json:"name"`
}
// AllChannelsJSON inlcudes all channels
type AllChannelsJSON struct {
Channels []channel `json:"channels"`
}
type chatLog struct {
Messages []chatMessage `json:"messages"`
}
2020-05-31 08:58:32 -04:00
type logList struct {
AvailableLogs []filelog.UserLogFile `json:"availableLogs"`
}
2020-02-25 17:44:25 -03:00
type chatMessage struct {
Text string `json:"text"`
Username string `json:"username"`
DisplayName string `json:"displayName"`
Channel string `json:"channel"`
Timestamp timestamp `json:"timestamp"`
Type twitch.MessageType `json:"type"`
Raw string `json:"raw"`
}
// ErrorResponse a simple error response
type ErrorResponse struct {
Message string `json:"message"`
2020-02-25 08:36:10 -03:00
}
2020-02-25 17:44:25 -03:00
type timestamp struct {
time.Time
}
// Init start the server
2017-03-08 17:38:01 -03:00
func (s *Server) Init() {
http.Handle("/", corsHandler(http.HandlerFunc(s.route)))
2018-03-02 16:48:27 -03:00
2020-05-31 08:58:32 -04:00
log.Infof("Listening on %s", s.listenAddress)
log.Fatal(http.ListenAndServe(s.listenAddress, nil))
}
2018-12-03 18:58:45 -03:00
func (s *Server) route(w http.ResponseWriter, r *http.Request) {
url := r.URL.EscapedPath()
2018-12-03 17:59:48 -03:00
if contains(s.assets, url) {
s.assetHandler.ServeHTTP(w, r)
return
}
2018-12-03 17:24:35 -03:00
2020-05-31 08:58:32 -04:00
query := s.fillUserids(w, r)
if url == "/list" {
s.writeAvailableLogs(w, r, query)
return
}
if url == "/channels" {
2020-05-31 08:58:32 -04:00
s.writeAllChannels(w, r)
return
}
2018-12-04 17:07:59 -03:00
2020-02-25 08:36:10 -03:00
s.routeLogs(w, r)
}
2020-05-31 08:58:32 -04:00
func (s *Server) fillUserids(w http.ResponseWriter, r *http.Request) url.Values {
query := r.URL.Query()
if query.Get("userid") == "" && query.Get("username") != "" {
users, err := s.helixClient.GetUsersByUsernames([]string{query.Get("username")})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil
}
query.Set("userid", users[query.Get("username")].ID)
}
if query.Get("channelid") == "" && query.Get("channel") != "" {
users, err := s.helixClient.GetUsersByUsernames([]string{query.Get("channel")})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil
}
query.Set("channelid", users[query.Get("channel")].ID)
}
return query
}
2020-02-25 08:36:10 -03:00
func (s *Server) routeLogs(w http.ResponseWriter, r *http.Request) {
2020-03-01 10:53:53 -03:00
request, err := s.newLogRequestFromURL(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
2020-02-25 08:36:10 -03:00
}
2020-03-01 10:53:53 -03:00
if request.redirectPath != "" {
http.Redirect(w, r, request.redirectPath, http.StatusFound)
2020-02-25 08:36:10 -03:00
return
}
var logs *chatLog
2020-03-01 10:53:53 -03:00
if request.time.random {
logs, err = s.getRandomQuote(request)
} else if request.time.from != "" && request.time.to != "" {
if request.isUserRequest {
logs, err = s.getUserLogsRange(request)
} else {
logs, err = s.getChannelLogsRange(request)
}
} else {
2020-03-01 10:53:53 -03:00
if request.isUserRequest {
logs, err = s.getUserLogs(request)
} else {
logs, err = s.getChannelLogs(request)
}
}
2020-02-25 08:36:10 -03:00
if err != nil {
log.Error(err)
http.Error(w, "could not load logs", http.StatusInternalServerError)
return
}
if request.responseType == responseTypeJSON {
writeJSON(logs, http.StatusOK, w, r)
2020-02-25 08:36:10 -03:00
return
}
if request.responseType == responseTypeRaw {
writeRaw(logs, http.StatusOK, w, r)
2020-02-25 08:36:10 -03:00
return
}
if request.responseType == responseTypeText {
writeText(logs, http.StatusOK, w, r)
2020-02-25 08:36:10 -03:00
return
}
http.Error(w, "unkown response type", http.StatusBadRequest)
}
func corsHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
} else {
w.Header().Set("Access-Control-Allow-Origin", "*")
h.ServeHTTP(w, r)
}
})
2017-03-11 17:51:08 -03:00
}
2018-12-02 15:23:54 -03:00
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
2020-02-25 08:36:10 -03:00
func reverseSlice(input []string) []string {
2018-12-04 16:53:03 -03:00
for i, j := 0, len(input)-1; i < j; i, j = i+1, j-1 {
input[i], input[j] = input[j], input[i]
}
return input
}
2020-05-31 08:58:32 -04:00
func (s *Server) writeAllChannels(w http.ResponseWriter, r *http.Request) {
2018-12-03 18:58:45 -03:00
response := new(AllChannelsJSON)
response.Channels = []channel{}
users, err := s.helixClient.GetUsersByUserIds(s.channels)
if err != nil {
log.Error(err)
http.Error(w, "Failure fetching data from twitch", http.StatusInternalServerError)
return
}
for _, user := range users {
response.Channels = append(response.Channels, channel{UserID: user.ID, Name: user.Login})
}
2018-12-03 18:58:45 -03:00
writeJSON(response, http.StatusOK, w, r)
}
func writeJSON(data interface{}, code int, w http.ResponseWriter, r *http.Request) {
js, err := json.Marshal(data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
2020-04-11 13:22:39 -04:00
w.Header().Set("Content-Type", "application/json; charset=utf-8")
2020-02-25 08:36:10 -03:00
w.WriteHeader(code)
w.Write(js)
2018-12-03 18:58:45 -03:00
}
2020-02-25 08:36:10 -03:00
func writeRaw(cLog *chatLog, code int, w http.ResponseWriter, r *http.Request) {
2020-04-11 13:22:39 -04:00
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
2020-02-25 08:36:10 -03:00
w.WriteHeader(code)
for _, cMessage := range cLog.Messages {
w.Write([]byte(cMessage.Raw + "\n"))
}
}
func writeText(cLog *chatLog, code int, w http.ResponseWriter, r *http.Request) {
2020-04-11 13:22:39 -04:00
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
2020-02-25 08:36:10 -03:00
w.WriteHeader(code)
for _, cMessage := range cLog.Messages {
switch cMessage.Type {
case twitch.PRIVMSG:
w.Write([]byte(fmt.Sprintf("[%s] #%s %s: %s\n", cMessage.Timestamp.Format("2006-01-2 15:04:05"), cMessage.Channel, cMessage.Username, cMessage.Text)))
case twitch.CLEARCHAT:
w.Write([]byte(fmt.Sprintf("[%s] #%s %s\n", cMessage.Timestamp.Format("2006-01-2 15:04:05"), cMessage.Channel, cMessage.Text)))
case twitch.USERNOTICE:
w.Write([]byte(fmt.Sprintf("[%s] #%s %s\n", cMessage.Timestamp.Format("2006-01-2 15:04:05"), cMessage.Channel, cMessage.Text)))
}
}
}
2018-12-02 15:23:54 -03:00
func (t timestamp) MarshalJSON() ([]byte, error) {
return []byte("\"" + t.UTC().Format(time.RFC3339) + "\""), nil
}
func (t *timestamp) UnmarshalJSON(data []byte) error {
goTime, err := time.Parse(time.RFC3339, strings.TrimSuffix(strings.TrimPrefix(string(data[:]), "\""), "\""))
if err != nil {
return err
}
*t = timestamp{
goTime,
}
return nil
}
func parseFromTo(from, to string, limit float64) (time.Time, time.Time, error) {
var fromTime time.Time
var toTime time.Time
if from == "" && to == "" {
fromTime = time.Now().AddDate(0, -1, 0)
toTime = time.Now()
} else if from == "" && to != "" {
var err error
toTime, err = parseTimestamp(to)
if err != nil {
return fromTime, toTime, fmt.Errorf("Can't parse to timestamp: %s", err)
}
fromTime = toTime.AddDate(0, -1, 0)
} else if from != "" && to == "" {
var err error
fromTime, err = parseTimestamp(from)
if err != nil {
return fromTime, toTime, fmt.Errorf("Can't parse from timestamp: %s", err)
}
toTime = fromTime.AddDate(0, 1, 0)
} else {
var err error
fromTime, err = parseTimestamp(from)
if err != nil {
return fromTime, toTime, fmt.Errorf("Can't parse from timestamp: %s", err)
}
toTime, err = parseTimestamp(to)
if err != nil {
return fromTime, toTime, fmt.Errorf("Can't parse to timestamp: %s", err)
}
if toTime.Sub(fromTime).Hours() > limit {
return fromTime, toTime, errors.New("Timespan too big")
}
}
return fromTime, toTime, nil
}
func parseTimestamp(timestamp string) (time.Time, error) {
i, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return time.Now(), err
}
return time.Unix(i, 0), nil
}
2020-02-25 17:44:25 -03:00
func buildClearChatMessageText(message twitch.ClearChatMessage) string {
if message.BanDuration == 0 {
return fmt.Sprintf("%s has been banned", message.TargetUsername)
}
return fmt.Sprintf("%s has been timed out for %d seconds", message.TargetUsername, message.BanDuration)
}