justlog/api/server.go

474 lines
11 KiB
Go
Raw Normal View History

2020-12-02 16:45:57 -03:00
// Package classification justlog API
//
// https://github.com/gempir/justlog
//
2020-12-01 18:52:42 -03:00
// Schemes: https
// BasePath: /
//
// Consumes:
// - application/json
// - application/xml
//
// Produces:
// - application/json
// - text/plain
//
// Security:
// - api_key:
//
// SecurityDefinitions:
// api_key:
// type: apiKey
// name: X-Api-Key
// in: header
//
// swagger:meta
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
2020-09-25 20:45:18 -03:00
"github.com/gempir/justlog/bot"
"github.com/gempir/justlog/config"
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
2020-09-25 20:45:18 -03:00
bot *bot.Bot
cfg *config.Config
2018-12-02 10:53:01 -03:00
fileLogger *filelog.Logger
2018-12-02 15:23:54 -03:00
helixClient *helix.Client
2018-12-02 10:53:01 -03:00
channels []string
assetHandler http.Handler
2017-03-08 17:38:01 -03:00
}
// NewServer create api Server
2020-09-25 20:45:18 -03:00
func NewServer(cfg *config.Config, bot *bot.Bot, fileLogger *filelog.Logger, helixClient *helix.Client, channels []string) Server {
2017-03-08 17:38:01 -03:00
return Server{
2020-09-25 20:45:18 -03:00
listenAddress: cfg.ListenAddress,
bot: bot,
logPath: cfg.LogsDirectory,
cfg: cfg,
2018-12-02 10:53:01 -03:00
fileLogger: fileLogger,
2018-12-02 15:23:54 -03:00
helixClient: helixClient,
channels: channels,
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"`
}
2020-12-02 16:45:57 -03:00
//
// swagger:response AllChannelsJSON
2020-02-25 17:44:25 -03:00
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"`
2020-11-07 06:38:21 -03:00
ID string `json:"id"`
2020-02-25 17:44:25 -03:00
Type twitch.MessageType `json:"type"`
Raw string `json:"raw"`
2020-11-07 10:25:40 -03:00
Tags map[string]string `json:"tags"`
2020-02-25 17:44:25 -03:00
}
// 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
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-09-25 20:45:18 -03:00
if strings.HasPrefix(url, "/admin/channelConfigs/") {
success := s.authenticateAdmin(w, r)
if success {
2020-10-08 16:52:58 -03:00
s.writeChannelConfigs(w, r)
}
return
}
if strings.HasPrefix(url, "/admin/channels") {
success := s.authenticateAdmin(w, r)
if success {
s.writeChannels(w, r)
2020-09-25 20:45:18 -03:00
}
return
}
2020-11-08 12:43:30 -03:00
routedLogs := s.routeLogs(w, r)
if !routedLogs {
s.assetHandler.ServeHTTP(w, r)
return
}
2020-02-25 08:36:10 -03:00
}
2020-05-31 08:58:32 -04:00
func (s *Server) fillUserids(w http.ResponseWriter, r *http.Request) url.Values {
query := r.URL.Query()
2020-05-31 10:39:43 -04:00
if query.Get("userid") == "" && query.Get("user") != "" {
users, err := s.helixClient.GetUsersByUsernames([]string{query.Get("user")})
2020-05-31 08:58:32 -04:00
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil
}
2020-05-31 10:39:43 -04:00
query.Set("userid", users[query.Get("user")].ID)
2020-05-31 08:58:32 -04:00
}
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-11-08 12:43:30 -03:00
func (s *Server) routeLogs(w http.ResponseWriter, r *http.Request) bool {
2020-02-25 08:36:10 -03:00
2020-03-01 10:53:53 -03:00
request, err := s.newLogRequestFromURL(r)
if err != nil {
2020-11-08 12:43:30 -03:00
return false
2020-02-25 08:36:10 -03:00
}
2020-03-01 10:53:53 -03:00
if request.redirectPath != "" {
2020-11-08 16:21:00 -03:00
http.Redirect(w, r, request.redirectPath, http.StatusFound)
return true
2020-02-25 08:36:10 -03:00
}
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)
2020-11-08 12:43:30 -03:00
return true
2020-02-25 08:36:10 -03:00
}
// Disable content type sniffing for log output
w.Header().Set("X-Content-Type-Options", "nosniff")
2020-02-25 08:36:10 -03:00
if request.responseType == responseTypeJSON {
writeJSON(logs, http.StatusOK, w, r)
2020-11-08 12:43:30 -03:00
return true
2020-02-25 08:36:10 -03:00
}
if request.responseType == responseTypeRaw {
writeRaw(logs, http.StatusOK, w, r)
2020-11-08 12:43:30 -03:00
return true
2020-02-25 08:36:10 -03:00
}
if request.responseType == responseTypeText {
writeText(logs, http.StatusOK, w, r)
2020-11-08 12:43:30 -03:00
return true
2020-02-25 08:36:10 -03:00
}
2020-11-08 12:43:30 -03:00
return false
2020-02-25 08:36:10 -03:00
}
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
2020-08-30 06:07:56 -04:00
func contains(s []string, e string) bool {
for _, a := range s {
2020-08-30 06:07:56 -04:00
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-12-02 16:45:57 -03:00
// swagger:route GET /channels justlog channels
//
// List currently logged channels
//
// Produces:
// - application/json
// - text/plain
//
// Schemes: https
//
// Responses:
// 200: AllChannelsJSON
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 createLogResult() chatLog {
return chatLog{Messages: []chatMessage{}}
}
2018-12-02 15:23:54 -03:00
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 createChatMessage(parsedMessage twitch.Message) chatMessage {
switch message := parsedMessage.(type) {
case *twitch.PrivateMessage:
return chatMessage{
Timestamp: timestamp{message.Time},
Username: message.User.Name,
DisplayName: message.User.DisplayName,
Text: message.Message,
Type: message.Type,
Channel: message.Channel,
Raw: message.Raw,
ID: message.ID,
Tags: message.Tags,
}
case *twitch.ClearChatMessage:
return chatMessage{
Timestamp: timestamp{message.Time},
Username: message.TargetUsername,
DisplayName: message.TargetUsername,
Text: buildClearChatMessageText(*message),
Type: message.Type,
Channel: message.Channel,
Raw: message.Raw,
Tags: message.Tags,
}
case *twitch.UserNoticeMessage:
return chatMessage{
Timestamp: timestamp{message.Time},
Username: message.User.Name,
DisplayName: message.User.DisplayName,
Text: message.SystemMsg + " " + message.Message,
Type: message.Type,
Channel: message.Channel,
Raw: message.Raw,
ID: message.ID,
Tags: message.Tags,
}
}
return chatMessage{}
}
2018-12-02 15:23:54 -03:00
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)
}