remove everything except the frontend
This commit is contained in:
parent
020333b358
commit
61157c4cd5
|
@ -1,44 +0,0 @@
|
|||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.13
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
|
||||
- name: Build
|
||||
run: go build -v .
|
||||
|
||||
- name: Test
|
||||
run: go test -v .
|
||||
|
||||
release:
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
|
||||
- name: Set outputs
|
||||
id: vars
|
||||
run: echo "::set-output name=sha_short::$(echo ${{ github.sha }} | cut -c1-4)"
|
|
@ -1,77 +0,0 @@
|
|||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
# Publish `main` as Docker `latest` image.
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Publish `v1.2.3` tags as releases.
|
||||
tags:
|
||||
- v*
|
||||
|
||||
# Run tests for any PRs.
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
# TODO: Change variable to your image's name.
|
||||
IMAGE_NAME: justlog
|
||||
|
||||
jobs:
|
||||
# Run tests.
|
||||
# See also https://docs.docker.com/docker-hub/builds/automated-testing/
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
if [ -f docker-compose.test.yml ]; then
|
||||
docker-compose --file docker-compose.test.yml build
|
||||
docker-compose --file docker-compose.test.yml run sut
|
||||
else
|
||||
docker build . --file Dockerfile
|
||||
fi
|
||||
|
||||
# Push image to GitHub Packages.
|
||||
# See also https://docs.docker.com/docker-hub/builds/
|
||||
push:
|
||||
# Ensure test job passes before pushing image.
|
||||
needs: test
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Build image
|
||||
run: docker build . --file Dockerfile --tag $IMAGE_NAME
|
||||
|
||||
- name: Log into GitHub Container Registry
|
||||
# TODO: Create a PAT with `read:packages` and `write:packages` scopes and save it as an Actions secret `CR_PAT`
|
||||
run: echo "${{ secrets.CR_PAT }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Push image to GitHub Container Registry
|
||||
run: |
|
||||
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
|
||||
|
||||
# Change all uppercase to lowercase
|
||||
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
||||
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
|
||||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "main" ] && VERSION=latest
|
||||
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
|
||||
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
||||
docker push $IMAGE_ID:$VERSION
|
|
@ -1,9 +1,25 @@
|
|||
justlog
|
||||
vendor/
|
||||
.idea/
|
||||
.vscode/
|
||||
coverage-all.out
|
||||
coverage.out
|
||||
.env
|
||||
logs/
|
||||
config.json
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
dist/*
|
||||
!dist/.gitkeep
|
||||
|
||||
public/swagger.json
|
||||
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
|
24
Dockerfile
24
Dockerfile
|
@ -1,24 +0,0 @@
|
|||
FROM quay.io/goswagger/swagger:latest as build-docs
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN make docs
|
||||
|
||||
FROM node:18-alpine as build-web
|
||||
WORKDIR /web
|
||||
COPY web .
|
||||
COPY --from=build-docs /app/web/public/swagger.json /web/public
|
||||
RUN yarn install --ignore-optional
|
||||
RUN yarn build
|
||||
|
||||
FROM golang:alpine as build-app
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
COPY --from=build-web /web/dist /app/web/dist
|
||||
RUN go build -o app .
|
||||
|
||||
FROM alpine:latest
|
||||
RUN apk --no-cache add ca-certificates
|
||||
COPY --from=build-app /app/app .
|
||||
USER 1000:1000
|
||||
CMD ["./app", "--config=/etc/justlog.json"]
|
||||
EXPOSE 8025
|
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018 gempir
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
26
Makefile
26
Makefile
|
@ -1,26 +0,0 @@
|
|||
full: docs web build
|
||||
|
||||
build:
|
||||
go build
|
||||
|
||||
run: build
|
||||
./justlog
|
||||
|
||||
run_web:
|
||||
cd web && yarn start
|
||||
|
||||
web: init_web
|
||||
cd web && yarn build
|
||||
|
||||
init_web:
|
||||
cd web && yarn install --ignore-optional
|
||||
|
||||
container:
|
||||
docker build -t gempir/justlog .
|
||||
|
||||
run_container:
|
||||
docker run -p 8025:8025 --restart=unless-stopped -v $(PWD)/config.json:/etc/justlog.json -v $(PWD)/logs:/logs gempir/justlog:latest
|
||||
|
||||
docs:
|
||||
swagger generate spec -m -o ./web/public/swagger.json -w api
|
||||
|
56
README.MD
56
README.MD
|
@ -1,56 +0,0 @@
|
|||
# justlog [![Build Status](https://github.com/gempir/justlog/workflows/ci/badge.svg)](https://github.com/gempir/justlog/actions?query=workflow%3Aci)
|
||||
|
||||
### What is this?
|
||||
Justlog is an twitch irc bot. It focuses on logging and providing an api for the logs.
|
||||
|
||||
### Optout
|
||||
|
||||
Click the X icon on the web ui to find a explanation how to opt out.
|
||||
|
||||
### API
|
||||
|
||||
API documentation can be viewed via the justlog frontend by clicking the "docs" symbol:
|
||||
![image](https://user-images.githubusercontent.com/1629196/159481078-0de98f01-2816-49bd-8e17-ba7cf66cb064.png)
|
||||
|
||||
### Docker
|
||||
|
||||
```
|
||||
mkdir logs
|
||||
docker run -p 8025:8025 --restart=unless-stopped -v $PWD/config.json:/etc/justlog.json -v $PWD/logs:/logs ghcr.io/gempir/justlog
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
Only admins can use these commands
|
||||
|
||||
- `!justlog status` will respond with uptime
|
||||
- `!justlog join gempir pajlada` will join the channels and append them to the config
|
||||
- `!justlog part gempir pajlada` will part the channels and remove them from the config
|
||||
- `!justlog optout gempir gempbot` will opt out users of message logging or querying previous logs of that user, same applies to users own channel
|
||||
- `!justlog optin gempir gempbot` will revert the opt out
|
||||
|
||||
### Config
|
||||
|
||||
```
|
||||
{
|
||||
"admins": ["gempir"], // will only respond to commands executed by these users
|
||||
"logsDirectory": "./logs", // the directory to log into
|
||||
"adminAPIKey": "noshot", // your secret api key to access the admin api, can be any string, used in api request to admin endpoints
|
||||
"username": "gempbot", // bot username (can be justinfan123123 if you don't want to use an account)
|
||||
"oauth": "oauthtokenforchat", // bot token can be anything if justinfan123123
|
||||
"botVerified": true, // increase ratelimits if you have a verified bot, so the bot can join faster, false by default
|
||||
"clientID": "mytwitchclientid", // your client ID, needed for fetching userids or usernames etc
|
||||
"clientSecret": "mysecret", // your twitch client secret
|
||||
"logLevel": "info", // the log level, keep this to info probably, all options are: trace, debug, info, warn, error, fatal, and panic, logs output is stdout
|
||||
"channels": ["77829817", "11148817"], // the channels (userids) you want to log
|
||||
"archive": true // probably keep to true, will disable gzipping of old logs if false, useful if you setup compression on your own
|
||||
}
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
Development requires [yarn](https://classic.yarnpkg.com/) and [go-swagger](https://goswagger.io/)
|
||||
|
||||
Run `go build && ./justlog` and `yarn start` in the web folder.
|
||||
|
||||
Or run `make container` and `make run_container`
|
121
api/admin.go
121
api/admin.go
|
@ -1,121 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (s *Server) authenticateAdmin(w http.ResponseWriter, r *http.Request) bool {
|
||||
apiKey := r.Header.Get("X-Api-Key")
|
||||
|
||||
if apiKey == "" || apiKey != s.cfg.AdminAPIKey {
|
||||
http.Error(w, "No I don't think so.", http.StatusForbidden)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type channelsDeleteRequest struct {
|
||||
// list of userIds
|
||||
Channels []string `json:"channels"`
|
||||
}
|
||||
|
||||
type channelConfigsJoinRequest struct {
|
||||
// list of userIds
|
||||
Channels []string `json:"channels"`
|
||||
}
|
||||
|
||||
// swagger:route POST /admin/channels admin addChannels
|
||||
//
|
||||
// Will add the channels to log, only works with userIds
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Security:
|
||||
// - api_key:
|
||||
//
|
||||
// Schemes: https
|
||||
//
|
||||
// Responses:
|
||||
// 200:
|
||||
// 400:
|
||||
// 405:
|
||||
// 500:
|
||||
|
||||
// swagger:route DELETE /admin/channels admin deleteChannels
|
||||
//
|
||||
// Will remove the channels to log, only works with userIds
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Security:
|
||||
// - api_key:
|
||||
//
|
||||
// Schemes: https
|
||||
//
|
||||
// Responses:
|
||||
// 200:
|
||||
// 400:
|
||||
// 405:
|
||||
// 500:
|
||||
func (s *Server) writeChannels(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost && r.Method != http.MethodDelete {
|
||||
http.Error(w, "We'll see, we'll see. The winner gets tea.", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodDelete {
|
||||
var request channelsDeleteRequest
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&request)
|
||||
if err != nil {
|
||||
http.Error(w, "ANYWAYS: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.cfg.RemoveChannels(request.Channels...)
|
||||
data, err := s.helixClient.GetUsersByUserIds(request.Channels)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get channel names to leave, config might be already updated", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
for _, userData := range data {
|
||||
s.bot.Part(userData.Login)
|
||||
}
|
||||
|
||||
writeJSON(fmt.Sprintf("Doubters? Removed channels %v", request.Channels), http.StatusOK, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
var request channelConfigsJoinRequest
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&request)
|
||||
if err != nil {
|
||||
http.Error(w, "ANYWAYS: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.cfg.AddChannels(request.Channels...)
|
||||
data, err := s.helixClient.GetUsersByUserIds(request.Channels)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get channel names to join, config might be already updated", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
for _, userData := range data {
|
||||
s.bot.Join(userData.Login)
|
||||
}
|
||||
|
||||
writeJSON(fmt.Sprintf("Doubters? Joined channels or already in: %v", request.Channels), http.StatusOK, w, r)
|
||||
}
|
153
api/channel.go
153
api/channel.go
|
@ -1,153 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
)
|
||||
|
||||
// RandomQuoteJSON response when request a random message
|
||||
type RandomChannelQuoteJSON struct {
|
||||
Channel string `json:"channel"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Message string `json:"message"`
|
||||
Timestamp timestamp `json:"timestamp"`
|
||||
}
|
||||
|
||||
// swagger:route GET /channel/{channel}/random logs randomChannelLog
|
||||
//
|
||||
// Get a random line from the entire channel log's history
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Responses:
|
||||
// 200: chatLog
|
||||
|
||||
// swagger:route GET /channelid/{channelid}/random logs randomChannelLog
|
||||
//
|
||||
// Get a random line from the entire channel log's history
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Responses:
|
||||
// 200: chatLog
|
||||
func (s *Server) getChannelRandomQuote(request logRequest) (*chatLog, error) {
|
||||
rawMessage, err := s.fileLogger.ReadRandomMessageForChannel(request.channelid)
|
||||
if err != nil {
|
||||
return &chatLog{}, err
|
||||
}
|
||||
parsedMessage := twitch.ParseMessage(rawMessage)
|
||||
|
||||
chatMsg := createChatMessage(parsedMessage)
|
||||
|
||||
return &chatLog{Messages: []chatMessage{chatMsg}}, nil
|
||||
}
|
||||
|
||||
// swagger:route GET /channel/{channel} logs channelLogs
|
||||
//
|
||||
// Get entire channel logs of current day
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Responses:
|
||||
// 200: chatLog
|
||||
|
||||
// swagger:route GET /channel/{channel}/{year}/{month}/{day} logs channelLogsYearMonthDay
|
||||
//
|
||||
// Get entire channel logs of given day
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Responses:
|
||||
// 200: chatLog
|
||||
func (s *Server) getChannelLogs(request logRequest) (*chatLog, error) {
|
||||
yearStr := request.time.year
|
||||
monthStr := request.time.month
|
||||
dayStr := request.time.day
|
||||
|
||||
year, err := strconv.Atoi(yearStr)
|
||||
if err != nil {
|
||||
return &chatLog{}, errors.New("invalid year")
|
||||
}
|
||||
month, err := strconv.Atoi(monthStr)
|
||||
if err != nil {
|
||||
return &chatLog{}, errors.New("invalid month")
|
||||
}
|
||||
day, err := strconv.Atoi(dayStr)
|
||||
if err != nil {
|
||||
return &chatLog{}, errors.New("invalid day")
|
||||
}
|
||||
|
||||
logMessages, err := s.fileLogger.ReadLogForChannel(request.channelid, year, month, day)
|
||||
if err != nil {
|
||||
return &chatLog{}, err
|
||||
}
|
||||
|
||||
if request.reverse {
|
||||
reverseSlice(logMessages)
|
||||
}
|
||||
|
||||
logResult := createLogResult()
|
||||
|
||||
for _, rawMessage := range logMessages {
|
||||
logResult.Messages = append(logResult.Messages, createChatMessage(twitch.ParseMessage(rawMessage)))
|
||||
}
|
||||
|
||||
return &logResult, nil
|
||||
}
|
||||
|
||||
func (s *Server) getChannelLogsRange(request logRequest) (*chatLog, error) {
|
||||
fromTime, toTime, err := parseFromTo(request.time.from, request.time.to, channelHourLimit)
|
||||
if err != nil {
|
||||
return &chatLog{}, err
|
||||
}
|
||||
|
||||
var logMessages []string
|
||||
|
||||
logMessages, _ = s.fileLogger.ReadLogForChannel(request.channelid, fromTime.Year(), int(fromTime.Month()), fromTime.Day())
|
||||
|
||||
if fromTime.Month() != toTime.Month() {
|
||||
additionalMessages, _ := s.fileLogger.ReadLogForChannel(request.channelid, toTime.Year(), int(toTime.Month()), toTime.Day())
|
||||
|
||||
logMessages = append(logMessages, additionalMessages...)
|
||||
}
|
||||
|
||||
if request.reverse {
|
||||
reverseSlice(logMessages)
|
||||
}
|
||||
|
||||
logResult := createLogResult()
|
||||
|
||||
for _, rawMessage := range logMessages {
|
||||
parsedMessage := twitch.ParseMessage(rawMessage)
|
||||
|
||||
switch message := parsedMessage.(type) {
|
||||
case *twitch.PrivateMessage:
|
||||
if message.Time.Unix() < fromTime.Unix() || message.Time.Unix() > toTime.Unix() {
|
||||
continue
|
||||
}
|
||||
case *twitch.ClearChatMessage:
|
||||
if message.Time.Unix() < fromTime.Unix() || message.Time.Unix() > toTime.Unix() {
|
||||
continue
|
||||
}
|
||||
case *twitch.UserNoticeMessage:
|
||||
if message.Time.Unix() < fromTime.Unix() || message.Time.Unix() > toTime.Unix() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
logResult.Messages = append(logResult.Messages, createChatMessage(parsedMessage))
|
||||
}
|
||||
|
||||
return &logResult, nil
|
||||
}
|
222
api/docs.go
222
api/docs.go
|
@ -1,222 +0,0 @@
|
|||
// Package classification justlog API
|
||||
//
|
||||
// https://github.com/gempir/justlog
|
||||
//
|
||||
// Schemes: https
|
||||
// BasePath: /
|
||||
//
|
||||
// Consumes:
|
||||
// - application/json
|
||||
// - application/xml
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// SecurityDefinitions:
|
||||
// api_key:
|
||||
// type: apiKey
|
||||
// name: X-Api-Key
|
||||
// in: header
|
||||
//
|
||||
// swagger:meta
|
||||
package api
|
||||
|
||||
type LogParams struct {
|
||||
// in: query
|
||||
Json string `json:"json"`
|
||||
// in: query
|
||||
Reverse string `json:"reverse"`
|
||||
// in: query
|
||||
From int32 `json:"from"`
|
||||
// in: query
|
||||
To int32 `json:"to"`
|
||||
}
|
||||
|
||||
//swagger:parameters channelUserLogsRandom
|
||||
type ChannelUserLogsRandomParams struct {
|
||||
// in: path
|
||||
Channel string `json:"channel"`
|
||||
// in: path
|
||||
Username string `json:"username"`
|
||||
LogParams
|
||||
}
|
||||
|
||||
//swagger:parameters channelUserLogs
|
||||
type ChannelUserLogsParams struct {
|
||||
// in: path
|
||||
Channel string `json:"channel"`
|
||||
// in: path
|
||||
Username string `json:"username"`
|
||||
LogParams
|
||||
}
|
||||
|
||||
//swagger:parameters channelUserLogsYearMonth
|
||||
type ChannelUserLogsYearMonthParams struct {
|
||||
// in: path
|
||||
Channel string `json:"channel"`
|
||||
// in: path
|
||||
Username string `json:"username"`
|
||||
// in: path
|
||||
Year string `json:"year"`
|
||||
// in: path
|
||||
Month string `json:"month"`
|
||||
LogParams
|
||||
}
|
||||
|
||||
//swagger:parameters channelLogs
|
||||
type ChannelLogsParams struct {
|
||||
// in: path
|
||||
Channel string `json:"channel"`
|
||||
LogParams
|
||||
}
|
||||
|
||||
//swagger:parameters channelLogsYearMonthDay
|
||||
type ChannelLogsYearMonthDayParams struct {
|
||||
// in: path
|
||||
Channel string `json:"channel"`
|
||||
// in: path
|
||||
Year string `json:"year"`
|
||||
// in: path
|
||||
Month string `json:"month"`
|
||||
// in: path
|
||||
Day string `json:"day"`
|
||||
LogParams
|
||||
}
|
||||
|
||||
//swagger:parameters channelIdUserIdLogsRandom
|
||||
type ChannelIdUserIdLogsRandomParams struct {
|
||||
// in: path
|
||||
ChannelId string `json:"channelid"`
|
||||
// in: path
|
||||
UserId string `json:"userid"`
|
||||
LogParams
|
||||
}
|
||||
|
||||
//swagger:parameters channelIdUserIdLogs
|
||||
type ChannelIdUserIdLogsParams struct {
|
||||
// in: path
|
||||
ChannelId string `json:"channelid"`
|
||||
// in: path
|
||||
UserId string `json:"userid"`
|
||||
LogParams
|
||||
}
|
||||
|
||||
//swagger:parameters channelIdUserIdLogsYearMonth
|
||||
type ChannelIdUserIdLogsYearMonthParams struct {
|
||||
// in: path
|
||||
ChannelId string `json:"channelid"`
|
||||
// in: path
|
||||
UserId string `json:"userid"`
|
||||
// in: path
|
||||
Year string `json:"year"`
|
||||
// in: path
|
||||
Month string `json:"month"`
|
||||
LogParams
|
||||
}
|
||||
|
||||
//swagger:parameters channelIdLogs
|
||||
type ChannelIdLogsParams struct {
|
||||
// in: path
|
||||
Channel string `json:"channelid"`
|
||||
LogParams
|
||||
}
|
||||
|
||||
//swagger:parameters channelIdLogsYearMonthDay
|
||||
type ChannelIdLogsYearMonthDayParams struct {
|
||||
// in: path
|
||||
Channel string `json:"channel"`
|
||||
// in: path
|
||||
Year string `json:"year"`
|
||||
// in: path
|
||||
Month string `json:"month"`
|
||||
// in: path
|
||||
Day string `json:"day"`
|
||||
LogParams
|
||||
}
|
||||
|
||||
//swagger:parameters channelIdUserLogsRandom
|
||||
type ChannelIdUserLogsRandomParams struct {
|
||||
// in: path
|
||||
ChannelId string `json:"channelid"`
|
||||
// in: path
|
||||
Username string `json:"username"`
|
||||
LogParams
|
||||
}
|
||||
|
||||
//swagger:parameters channelIdUserLogs
|
||||
type ChannelIdUserLogsParams struct {
|
||||
// in: path
|
||||
ChannelId string `json:"channelid"`
|
||||
// in: path
|
||||
Username string `json:"username"`
|
||||
LogParams
|
||||
}
|
||||
|
||||
//swagger:parameters channelIdUserLogsYearMonth
|
||||
type ChannelIdUserLogsYearMonthParams struct {
|
||||
// in: path
|
||||
ChannelId string `json:"channelid"`
|
||||
// in: path
|
||||
Username string `json:"username"`
|
||||
// in: path
|
||||
Year string `json:"year"`
|
||||
// in: path
|
||||
Month string `json:"month"`
|
||||
LogParams
|
||||
}
|
||||
|
||||
//swagger:parameters channelUserIdLogsRandom
|
||||
type ChannelUserIdLogsRandomParams struct {
|
||||
// in: path
|
||||
Channel string `json:"channel"`
|
||||
// in: path
|
||||
UserId string `json:"userid"`
|
||||
LogParams
|
||||
}
|
||||
|
||||
//swagger:parameters channelUserIdLogs
|
||||
type ChannelUserIdLogsParams struct {
|
||||
// in: path
|
||||
Channel string `json:"channel"`
|
||||
// in: path
|
||||
UserId string `json:"userid"`
|
||||
LogParams
|
||||
}
|
||||
|
||||
//swagger:parameters channelUserIdLogsYearMonth
|
||||
type ChannelUserIdLogsYearMonthParams struct {
|
||||
// in: path
|
||||
Channel string `json:"channel"`
|
||||
// in: path
|
||||
Userid string `json:"userid"`
|
||||
// in: path
|
||||
Year string `json:"year"`
|
||||
// in: path
|
||||
Month string `json:"month"`
|
||||
LogParams
|
||||
}
|
||||
|
||||
// swagger:parameters addChannels
|
||||
type AddChannelsParameters struct {
|
||||
// in:body
|
||||
Body channelConfigsJoinRequest
|
||||
}
|
||||
|
||||
// swagger:parameters deleteChannels
|
||||
type DeleteChannelsParameters struct {
|
||||
// in:body
|
||||
Body channelsDeleteRequest
|
||||
}
|
||||
|
||||
//swagger:parameters list
|
||||
type ListLogsParams struct {
|
||||
// in: query
|
||||
Channel string `json:"channel"`
|
||||
// in: query
|
||||
Username string `json:"username"`
|
||||
// in: query
|
||||
ChannelId string `json:"channelid"`
|
||||
// in: query
|
||||
Userid string `json:"userid"`
|
||||
}
|
|
@ -1,190 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type logRequest struct {
|
||||
channel string
|
||||
user string
|
||||
channelid string
|
||||
userid string
|
||||
time logTime
|
||||
reverse bool
|
||||
responseType string
|
||||
redirectPath string
|
||||
isUserRequest bool
|
||||
isChannelRequest bool
|
||||
}
|
||||
|
||||
// userRandomMessageRequest /channel/pajlada/user/gempir/random
|
||||
|
||||
type logTime struct {
|
||||
from string
|
||||
to string
|
||||
year string
|
||||
month string
|
||||
day string
|
||||
random bool
|
||||
}
|
||||
|
||||
var (
|
||||
pathRegex = regexp.MustCompile(`\/(channel|channelid)\/(\w+)(?:\/(user|userid)\/(\w+))?(?:(?:\/(\d{4})\/(\d{1,2})(?:\/(\d{1,2}))?)|(?:\/(random)))?`)
|
||||
)
|
||||
|
||||
func (s *Server) newLogRequestFromURL(r *http.Request) (logRequest, error) {
|
||||
path := r.URL.EscapedPath()
|
||||
|
||||
if path != strings.ToLower(path) {
|
||||
return logRequest{redirectPath: fmt.Sprintf("%s?%s", strings.ToLower(path), r.URL.Query().Encode())}, nil
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(path, "/channel") && !strings.HasPrefix(path, "/channelid") {
|
||||
return logRequest{}, errors.New("route not found")
|
||||
}
|
||||
|
||||
url := strings.TrimRight(path, "/")
|
||||
|
||||
matches := pathRegex.FindAllStringSubmatch(url, -1)
|
||||
if len(matches) == 0 || len(matches[0]) < 5 {
|
||||
return logRequest{}, errors.New("route not found")
|
||||
}
|
||||
|
||||
request := logRequest{
|
||||
time: logTime{},
|
||||
}
|
||||
|
||||
params := []string{}
|
||||
for _, match := range matches[0] {
|
||||
if match != "" {
|
||||
params = append(params, match)
|
||||
}
|
||||
}
|
||||
|
||||
request.isUserRequest = len(params) > 4 && (params[3] == "user" || params[3] == "userid")
|
||||
request.isChannelRequest = len(params) < 4 || (len(params) >= 4 && params[3] != "user" && params[3] != "userid")
|
||||
|
||||
if params[1] == "channel" {
|
||||
request.channel = params[2]
|
||||
}
|
||||
if params[1] == "channelid" {
|
||||
request.channelid = params[2]
|
||||
}
|
||||
if request.isUserRequest && params[3] == "user" {
|
||||
request.user = params[4]
|
||||
}
|
||||
if request.isUserRequest && params[3] == "userid" {
|
||||
request.userid = params[4]
|
||||
}
|
||||
|
||||
var err error
|
||||
request, err = s.fillIds(request)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return logRequest{}, nil
|
||||
}
|
||||
|
||||
if request.isUserRequest && len(params) == 7 {
|
||||
request.time.year = params[5]
|
||||
request.time.month = params[6]
|
||||
} else if request.isUserRequest && len(params) == 8 {
|
||||
return logRequest{}, errors.New("route not found")
|
||||
} else if request.isChannelRequest && len(params) == 6 {
|
||||
request.time.year = params[3]
|
||||
request.time.month = params[4]
|
||||
request.time.day = params[5]
|
||||
} else if request.isUserRequest && len(params) == 6 && params[5] == "random" {
|
||||
request.time.random = true
|
||||
} else if request.isChannelRequest && len(params) == 4 && params[3] == "random" {
|
||||
request.time.random = true
|
||||
} else {
|
||||
if request.isChannelRequest {
|
||||
request.time.year = fmt.Sprintf("%d", time.Now().Year())
|
||||
request.time.month = fmt.Sprintf("%d", time.Now().Month())
|
||||
} else {
|
||||
year, month, err := s.fileLogger.GetLastLogYearAndMonthForUser(request.channelid, request.userid)
|
||||
if err == nil {
|
||||
request.time.year = fmt.Sprintf("%d", year)
|
||||
request.time.month = fmt.Sprintf("%d", month)
|
||||
} else {
|
||||
request.time.year = fmt.Sprintf("%d", time.Now().Year())
|
||||
request.time.month = fmt.Sprintf("%d", time.Now().Month())
|
||||
}
|
||||
}
|
||||
|
||||
timePath := request.time.year + "/" + request.time.month
|
||||
|
||||
if request.isChannelRequest {
|
||||
request.time.day = fmt.Sprintf("%d", time.Now().Day())
|
||||
timePath += "/" + request.time.day
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
|
||||
encodedQuery := ""
|
||||
if query.Encode() != "" {
|
||||
encodedQuery = "?" + query.Encode()
|
||||
}
|
||||
|
||||
return logRequest{redirectPath: fmt.Sprintf("%s/%s%s", params[0], timePath, encodedQuery)}, nil
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("from") != "" || r.URL.Query().Get("to") != "" {
|
||||
request.time.from = r.URL.Query().Get("from")
|
||||
if request.time.from == "" {
|
||||
request.time.from = strconv.FormatInt(time.Now().Unix(), 10)
|
||||
}
|
||||
request.time.to = r.URL.Query().Get("to")
|
||||
if request.time.to == "" {
|
||||
request.time.to = strconv.FormatInt(time.Now().Unix(), 10)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := r.URL.Query()["reverse"]; ok {
|
||||
request.reverse = true
|
||||
} else {
|
||||
request.reverse = false
|
||||
}
|
||||
|
||||
if _, ok := r.URL.Query()["json"]; ok || r.URL.Query().Get("type") == "json" || r.Header.Get("Content-Type") == "application/json" {
|
||||
request.responseType = responseTypeJSON
|
||||
} else if _, ok := r.URL.Query()["raw"]; ok || r.URL.Query().Get("type") == "raw" {
|
||||
request.responseType = responseTypeRaw
|
||||
} else {
|
||||
request.responseType = responseTypeText
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (s *Server) fillIds(request logRequest) (logRequest, error) {
|
||||
usernames := []string{}
|
||||
if request.channelid == "" && request.channel != "" {
|
||||
usernames = append(usernames, request.channel)
|
||||
}
|
||||
if request.userid == "" && request.user != "" {
|
||||
usernames = append(usernames, request.user)
|
||||
}
|
||||
|
||||
ids, err := s.helixClient.GetUsersByUsernames(usernames)
|
||||
if err != nil {
|
||||
return request, err
|
||||
}
|
||||
|
||||
if request.channelid == "" {
|
||||
request.channelid = ids[strings.ToLower(request.channel)].ID
|
||||
}
|
||||
if request.userid == "" {
|
||||
request.userid = ids[strings.ToLower(request.user)].ID
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// swagger:route POST /optout justlog
|
||||
//
|
||||
// Generates optout code to use in chat
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Schemes: https
|
||||
//
|
||||
// Responses:
|
||||
// 200: string
|
||||
func (s *Server) writeOptOutCode(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
code := randomString(6)
|
||||
|
||||
s.bot.OptoutCodes.Store(code, true)
|
||||
go func() {
|
||||
time.Sleep(time.Second * 60)
|
||||
s.bot.OptoutCodes.Delete(code)
|
||||
}()
|
||||
|
||||
writeJSON(code, http.StatusOK, w, r)
|
||||
}
|
||||
|
||||
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890")
|
||||
|
||||
func randomString(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
498
api/server.go
498
api/server.go
|
@ -1,498 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gempir/justlog/bot"
|
||||
|
||||
"github.com/gempir/justlog/config"
|
||||
|
||||
"github.com/gempir/justlog/helix"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/gempir/justlog/filelog"
|
||||
)
|
||||
|
||||
// Server api server
|
||||
type Server struct {
|
||||
listenAddress string
|
||||
logPath string
|
||||
bot *bot.Bot
|
||||
cfg *config.Config
|
||||
fileLogger filelog.Logger
|
||||
helixClient helix.TwitchApiClient
|
||||
assetsHandler http.Handler
|
||||
}
|
||||
|
||||
// NewServer create api Server
|
||||
func NewServer(cfg *config.Config, bot *bot.Bot, fileLogger filelog.Logger, helixClient helix.TwitchApiClient, assets fs.FS) Server {
|
||||
build, err := fs.Sub(assets, "web/dist")
|
||||
if err != nil {
|
||||
log.Fatal("failed to read public assets")
|
||||
}
|
||||
|
||||
return Server{
|
||||
listenAddress: cfg.ListenAddress,
|
||||
bot: bot,
|
||||
logPath: cfg.LogsDirectory,
|
||||
cfg: cfg,
|
||||
fileLogger: fileLogger,
|
||||
helixClient: helixClient,
|
||||
assetsHandler: http.FileServer(http.FS(build)),
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
responseTypeJSON = "json"
|
||||
responseTypeText = "text"
|
||||
responseTypeRaw = "raw"
|
||||
)
|
||||
|
||||
var (
|
||||
userHourLimit = 744.0
|
||||
channelHourLimit = 24.0
|
||||
)
|
||||
|
||||
type channel struct {
|
||||
UserID string `json:"userID"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// swagger:model
|
||||
type AllChannelsJSON struct {
|
||||
Channels []channel `json:"channels"`
|
||||
}
|
||||
|
||||
// swagger:model
|
||||
type chatLog struct {
|
||||
Messages []chatMessage `json:"messages"`
|
||||
}
|
||||
|
||||
// swagger:model
|
||||
type logList struct {
|
||||
AvailableLogs []filelog.UserLogFile `json:"availableLogs"`
|
||||
}
|
||||
|
||||
// swagger:model
|
||||
type channelLogList struct {
|
||||
AvailableLogs []filelog.ChannelLogFile `json:"availableLogs"`
|
||||
}
|
||||
|
||||
type chatMessage struct {
|
||||
Text string `json:"text"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Channel string `json:"channel"`
|
||||
Timestamp timestamp `json:"timestamp"`
|
||||
ID string `json:"id"`
|
||||
Type twitch.MessageType `json:"type"`
|
||||
Raw string `json:"raw"`
|
||||
Tags map[string]string `json:"tags"`
|
||||
}
|
||||
|
||||
// ErrorResponse a simple error response
|
||||
type ErrorResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type timestamp struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
// Init start the server
|
||||
func (s *Server) Init() {
|
||||
http.Handle("/", corsHandler(http.HandlerFunc(s.route)))
|
||||
|
||||
log.Infof("Listening on %s", s.listenAddress)
|
||||
log.Fatal(http.ListenAndServe(s.listenAddress, nil))
|
||||
}
|
||||
|
||||
func (s *Server) route(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.URL.EscapedPath()
|
||||
|
||||
query, err := s.fillUserids(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if url == "/list" {
|
||||
if s.cfg.IsOptedOut(query.Get("userid")) || s.cfg.IsOptedOut(query.Get("channelid")) {
|
||||
http.Error(w, "User or channel has opted out", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
s.writeAvailableLogs(w, r, query)
|
||||
return
|
||||
}
|
||||
|
||||
if url == "/channels" {
|
||||
s.writeAllChannels(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if url == "/optout" && r.Method == http.MethodPost {
|
||||
s.writeOptOutCode(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(url, "/admin/channels") {
|
||||
success := s.authenticateAdmin(w, r)
|
||||
if success {
|
||||
s.writeChannels(w, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
routedLogs := s.routeLogs(w, r)
|
||||
|
||||
if !routedLogs {
|
||||
s.assetsHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) fillUserids(w http.ResponseWriter, r *http.Request) (url.Values, error) {
|
||||
query := r.URL.Query()
|
||||
|
||||
if query.Get("userid") == "" && query.Get("user") != "" {
|
||||
username := strings.ToLower(query.Get("user"))
|
||||
|
||||
users, err := s.helixClient.GetUsersByUsernames([]string{username})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return nil, err
|
||||
}
|
||||
if len(users) == 0 {
|
||||
err := fmt.Errorf("could not find users")
|
||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query.Set("userid", users[username].ID)
|
||||
}
|
||||
|
||||
if query.Get("channelid") == "" && query.Get("channel") != "" {
|
||||
channelName := strings.ToLower(query.Get("channel"))
|
||||
|
||||
users, err := s.helixClient.GetUsersByUsernames([]string{channelName})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return nil, err
|
||||
}
|
||||
if len(users) == 0 {
|
||||
err := fmt.Errorf("could not find users")
|
||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query.Set("channelid", users[channelName].ID)
|
||||
}
|
||||
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func (s *Server) routeLogs(w http.ResponseWriter, r *http.Request) bool {
|
||||
|
||||
request, err := s.newLogRequestFromURL(r)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if request.redirectPath != "" {
|
||||
http.Redirect(w, r, request.redirectPath, http.StatusFound)
|
||||
return true
|
||||
}
|
||||
|
||||
if s.cfg.IsOptedOut(request.channelid) || s.cfg.IsOptedOut(request.userid) {
|
||||
http.Error(w, "User or channel has opted out", http.StatusForbidden)
|
||||
return true
|
||||
}
|
||||
|
||||
var logs *chatLog
|
||||
if request.time.random {
|
||||
if request.isUserRequest {
|
||||
logs, err = s.getRandomQuote(request)
|
||||
} else {
|
||||
logs, err = s.getChannelRandomQuote(request)
|
||||
}
|
||||
} else if request.time.from != "" && request.time.to != "" {
|
||||
if request.isUserRequest {
|
||||
logs, err = s.getUserLogsRange(request)
|
||||
} else {
|
||||
logs, err = s.getChannelLogsRange(request)
|
||||
}
|
||||
|
||||
} else {
|
||||
if request.isUserRequest {
|
||||
logs, err = s.getUserLogs(request)
|
||||
} else {
|
||||
logs, err = s.getChannelLogs(request)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Error(w, "could not load logs", http.StatusNotFound)
|
||||
return true
|
||||
}
|
||||
|
||||
// Disable content type sniffing for log output
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
currentYear := fmt.Sprintf("%d", int(time.Now().Year()))
|
||||
currentMonth := fmt.Sprintf("%d", int(time.Now().Month()))
|
||||
|
||||
if (request.time.year != "" && request.time.month != "") && (request.time.year < currentYear || (request.time.year == currentYear && request.time.month < currentMonth)) {
|
||||
writeCacheControl(w, r, time.Hour*8760)
|
||||
} else {
|
||||
writeCacheControlNoCache(w, r)
|
||||
}
|
||||
|
||||
if request.responseType == responseTypeJSON {
|
||||
writeJSON(logs, http.StatusOK, w, r)
|
||||
return true
|
||||
}
|
||||
|
||||
if request.responseType == responseTypeRaw {
|
||||
writeRaw(logs, http.StatusOK, w, r)
|
||||
return true
|
||||
}
|
||||
|
||||
if request.responseType == responseTypeText {
|
||||
writeText(logs, http.StatusOK, w, r)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func contains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func reverseSlice(input []string) []string {
|
||||
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
|
||||
}
|
||||
|
||||
// swagger:route GET /channels justlog channels
|
||||
//
|
||||
// List currently logged channels
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Schemes: https
|
||||
//
|
||||
// Responses:
|
||||
// 200: AllChannelsJSON
|
||||
func (s *Server) writeAllChannels(w http.ResponseWriter, r *http.Request) {
|
||||
response := new(AllChannelsJSON)
|
||||
response.Channels = []channel{}
|
||||
users, err := s.helixClient.GetUsersByUserIds(s.cfg.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})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(code)
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func writeCacheControl(w http.ResponseWriter, r *http.Request, cacheDuration time.Duration) {
|
||||
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%.0f", cacheDuration.Seconds()))
|
||||
}
|
||||
|
||||
func writeCacheControlNoCache(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
}
|
||||
|
||||
func writeRaw(cLog *chatLog, code int, w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
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) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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{}}
|
||||
}
|
||||
|
||||
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{}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
265
api/user.go
265
api/user.go
|
@ -1,265 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
)
|
||||
|
||||
// RandomQuoteJSON response when request a random message
|
||||
type RandomQuoteJSON struct {
|
||||
Channel string `json:"channel"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Message string `json:"message"`
|
||||
Timestamp timestamp `json:"timestamp"`
|
||||
}
|
||||
|
||||
// swagger:route GET /channel/{channel}/user/{username}/random logs channelUserLogsRandom
|
||||
//
|
||||
// Get a random line from a user in a given channel
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Responses:
|
||||
// 200: chatLog
|
||||
|
||||
// swagger:route GET /channelid/{channelid}/userid/{userid}/random logs channelIdUserIdLogsRandom
|
||||
//
|
||||
// Get a random line from a user in a given channel
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Responses:
|
||||
// 200: chatLog
|
||||
|
||||
// swagger:route GET /channelid/{channelid}/user/{user}/random logs channelIdUserLogsRandom
|
||||
//
|
||||
// Get a random line from a user in a given channel
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Responses:
|
||||
// 200: chatLog
|
||||
|
||||
// swagger:route GET /channel/{channel}/userid/{userid}/random logs channelUserIdLogsRandom
|
||||
//
|
||||
// Get a random line from a user in a given channel
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Responses:
|
||||
// 200: chatLog
|
||||
func (s *Server) getRandomQuote(request logRequest) (*chatLog, error) {
|
||||
rawMessage, err := s.fileLogger.ReadRandomMessageForUser(request.channelid, request.userid)
|
||||
if err != nil {
|
||||
return &chatLog{}, err
|
||||
}
|
||||
parsedMessage := twitch.ParseMessage(rawMessage)
|
||||
|
||||
chatMsg := createChatMessage(parsedMessage)
|
||||
|
||||
return &chatLog{Messages: []chatMessage{chatMsg}}, nil
|
||||
}
|
||||
|
||||
// swagger:route GET /list logs list
|
||||
//
|
||||
// Lists available logs of a user or channel, channel response also includes the day. OpenAPI 2 does not support multiple responses with the same http code right now.
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Schemes: https
|
||||
//
|
||||
// Responses:
|
||||
// 200: logList
|
||||
func (s *Server) writeAvailableLogs(w http.ResponseWriter, r *http.Request, q url.Values) {
|
||||
channelid := q.Get("channelid")
|
||||
userid := q.Get("userid")
|
||||
|
||||
if userid == "" {
|
||||
logs, err := s.fileLogger.GetAvailableLogsForChannel(channelid)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to get available channel logs: "+err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
writeCacheControl(w, r, time.Hour)
|
||||
writeJSON(&channelLogList{logs}, http.StatusOK, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
logs, err := s.fileLogger.GetAvailableLogsForUser(channelid, userid)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to get available user logs: "+err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
writeCacheControl(w, r, time.Hour)
|
||||
writeJSON(&logList{logs}, http.StatusOK, w, r)
|
||||
}
|
||||
|
||||
// swagger:route GET /channel/{channel}/user/{username} logs channelUserLogs
|
||||
//
|
||||
// Get user logs in channel of current month
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Responses:
|
||||
// 200: chatLog
|
||||
|
||||
// swagger:route GET /channelid/{channelid}/userid/{userid} logs channelIdUserIdLogs
|
||||
//
|
||||
// Get user logs in channel of current month
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Responses:
|
||||
// 200: chatLog
|
||||
|
||||
// swagger:route GET /channelid/{channelid}/user/{username} logs channelIdUserLogs
|
||||
//
|
||||
// Get user logs in channel of current month
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Responses:
|
||||
// 200: chatLog
|
||||
|
||||
// swagger:route GET /channel/{channel}/userid/{userid} logs channelUserIdLogs
|
||||
//
|
||||
// Get user logs in channel of current month
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Responses:
|
||||
// 200: chatLog
|
||||
|
||||
// swagger:route GET /channel/{channel}/user/{username}/{year}/{month} logs channelUserLogsYearMonth
|
||||
//
|
||||
// Get user logs in channel of given year month
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Responses:
|
||||
// 200: chatLog
|
||||
|
||||
// swagger:route GET /channelid/{channelid}/userid/{userid}/{year}/{month} logs channelIdUserIdLogsYearMonth
|
||||
//
|
||||
// Get user logs in channel of given year month
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Responses:
|
||||
// 200: chatLog
|
||||
|
||||
// swagger:route GET /channelid/{channelid}/user/{username}/{year}/{month} logs channelIdUserLogsYearMonth
|
||||
//
|
||||
// Get user logs in channel of given year month
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Responses:
|
||||
// 200: chatLog
|
||||
|
||||
// swagger:route GET /channel/{channel}/userid/{userid}/{year}/{month} logs channelUserIdLogsYearMonth
|
||||
//
|
||||
// Get user logs in channel of given year month
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - text/plain
|
||||
//
|
||||
// Responses:
|
||||
// 200: chatLog
|
||||
func (s *Server) getUserLogs(request logRequest) (*chatLog, error) {
|
||||
logMessages, err := s.fileLogger.ReadLogForUser(request.channelid, request.userid, request.time.year, request.time.month)
|
||||
if err != nil {
|
||||
return &chatLog{}, err
|
||||
}
|
||||
|
||||
if request.reverse {
|
||||
reverseSlice(logMessages)
|
||||
}
|
||||
|
||||
logResult := createLogResult()
|
||||
|
||||
for _, rawMessage := range logMessages {
|
||||
logResult.Messages = append(logResult.Messages, createChatMessage(twitch.ParseMessage(rawMessage)))
|
||||
}
|
||||
|
||||
return &logResult, nil
|
||||
}
|
||||
|
||||
func (s *Server) getUserLogsRange(request logRequest) (*chatLog, error) {
|
||||
|
||||
fromTime, toTime, err := parseFromTo(request.time.from, request.time.to, userHourLimit)
|
||||
if err != nil {
|
||||
return &chatLog{}, err
|
||||
}
|
||||
|
||||
var logMessages []string
|
||||
|
||||
logMessages, _ = s.fileLogger.ReadLogForUser(request.channelid, request.userid, fmt.Sprintf("%d", fromTime.Year()), fmt.Sprintf("%d", int(fromTime.Month())))
|
||||
|
||||
if fromTime.Month() != toTime.Month() {
|
||||
additionalMessages, _ := s.fileLogger.ReadLogForUser(request.channelid, request.userid, fmt.Sprint(toTime.Year()), fmt.Sprint(toTime.Month()))
|
||||
|
||||
logMessages = append(logMessages, additionalMessages...)
|
||||
}
|
||||
|
||||
if request.reverse {
|
||||
reverseSlice(logMessages)
|
||||
}
|
||||
|
||||
logResult := createLogResult()
|
||||
|
||||
for _, rawMessage := range logMessages {
|
||||
parsedMessage := twitch.ParseMessage(rawMessage)
|
||||
|
||||
switch message := parsedMessage.(type) {
|
||||
case *twitch.PrivateMessage:
|
||||
if message.Time.Unix() < fromTime.Unix() || message.Time.Unix() > toTime.Unix() {
|
||||
continue
|
||||
}
|
||||
case *twitch.ClearChatMessage:
|
||||
if message.Time.Unix() < fromTime.Unix() || message.Time.Unix() > toTime.Unix() {
|
||||
continue
|
||||
}
|
||||
case *twitch.UserNoticeMessage:
|
||||
if message.Time.Unix() < fromTime.Unix() || message.Time.Unix() > toTime.Unix() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
logResult.Messages = append(logResult.Messages, createChatMessage(parsedMessage))
|
||||
}
|
||||
|
||||
return &logResult, nil
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package archiver
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewArchiver(logPath string) *Archiver {
|
||||
return &Archiver{
|
||||
logPath: logPath,
|
||||
workQueue: make(chan string),
|
||||
}
|
||||
}
|
||||
|
||||
type Archiver struct {
|
||||
logPath string
|
||||
workQueue chan string
|
||||
}
|
||||
|
||||
func (a *Archiver) Boot() {
|
||||
go a.startScanner()
|
||||
a.startConsumer()
|
||||
}
|
||||
|
||||
func (a *Archiver) startConsumer() {
|
||||
for task := range a.workQueue {
|
||||
a.gzipFile(task)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Archiver) startScanner() {
|
||||
for {
|
||||
a.scanLogPath()
|
||||
time.Sleep(time.Second * 60)
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package archiver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (a *Archiver) gzipFile(filePath string) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
log.Errorf("File not found: %s Error: %s", filePath, err.Error())
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
log.Infof("Archiving: %s", filePath)
|
||||
|
||||
reader := bufio.NewReader(file)
|
||||
content, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
log.Errorf("Failure reading file: %s Error: %s", filePath, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
gzipFile, err := os.Create(filePath + ".gz")
|
||||
if err != nil {
|
||||
log.Errorf("Failure creating file: %s.gz Error: %s", filePath, err.Error())
|
||||
return
|
||||
}
|
||||
defer gzipFile.Close()
|
||||
|
||||
w := gzip.NewWriter(gzipFile)
|
||||
_, err = w.Write(content)
|
||||
if err != nil {
|
||||
log.Errorf("Failure writing content in file: %s.gz Error: %s", filePath, err.Error())
|
||||
}
|
||||
w.Close()
|
||||
|
||||
err = os.Remove(filePath)
|
||||
if err != nil {
|
||||
log.Errorf("Failure deleting file: %s Error: %s", filePath, err.Error())
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
package archiver
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (a *Archiver) scanLogPath() {
|
||||
channelFiles, err := ioutil.ReadDir(a.logPath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
for _, channelId := range channelFiles {
|
||||
if channelId.IsDir() {
|
||||
yearFiles, err := ioutil.ReadDir(a.logPath + "/" + channelId.Name())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
yearFiles = a.filterFiles(yearFiles)
|
||||
|
||||
for _, year := range yearFiles {
|
||||
if year.IsDir() {
|
||||
monthFiles, err := ioutil.ReadDir(a.logPath + "/" + channelId.Name() + "/" + year.Name())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
monthFiles = a.filterFiles(monthFiles)
|
||||
|
||||
for _, month := range monthFiles {
|
||||
if month.IsDir() {
|
||||
dayFiles, err := ioutil.ReadDir(a.logPath + "/" + channelId.Name() + "/" + year.Name() + "/" + month.Name())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
dayFiles = a.filterFiles(dayFiles)
|
||||
|
||||
for _, dayOrUserId := range dayFiles {
|
||||
if dayOrUserId.IsDir() {
|
||||
channelLogFiles, err := ioutil.ReadDir(a.logPath + "/" + channelId.Name() + "/" + year.Name() + "/" + month.Name() + "/" + dayOrUserId.Name())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
channelLogFiles = a.filterFiles(channelLogFiles)
|
||||
|
||||
for _, channelLogFile := range channelLogFiles {
|
||||
if strings.HasSuffix(channelLogFile.Name(), ".txt") {
|
||||
dayInt, err := strconv.Atoi(dayOrUserId.Name())
|
||||
if err != nil {
|
||||
log.Errorf("Failure converting day to int in scanner %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if dayInt == int(time.Now().Day()) {
|
||||
continue
|
||||
}
|
||||
|
||||
a.workQueue <- a.logPath + "/" + channelId.Name() + "/" + year.Name() + "/" + month.Name() + "/" + dayOrUserId.Name() + "/" + channelLogFile.Name()
|
||||
}
|
||||
}
|
||||
|
||||
} else if strings.HasSuffix(dayOrUserId.Name(), ".txt") {
|
||||
monthInt, err := strconv.Atoi(month.Name())
|
||||
if err != nil {
|
||||
log.Errorf("Failure converting month to int in scanner %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if monthInt == int(time.Now().Month()) {
|
||||
continue
|
||||
}
|
||||
|
||||
a.workQueue <- a.logPath + "/" + channelId.Name() + "/" + year.Name() + "/" + month.Name() + "/" + dayOrUserId.Name()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Archiver) filterFiles(files []os.FileInfo) []os.FileInfo {
|
||||
var result []os.FileInfo
|
||||
|
||||
for _, file := range files {
|
||||
if !strings.HasPrefix(file.Name(), ".") {
|
||||
result = append(result, file)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
164
bot/commands.go
164
bot/commands.go
|
@ -1,164 +0,0 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
twitch "github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/gempir/justlog/humanize"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
commandPrefix = "!justlog"
|
||||
errNoUsernames = ", at least 1 username has to be provided. multiple usernames have to be separated with a space"
|
||||
errRequestingUserIDs = ", something went wrong requesting the userids"
|
||||
)
|
||||
|
||||
func (b *Bot) handlePrivateMessageCommands(message twitch.PrivateMessage) {
|
||||
if !strings.HasPrefix(strings.ToLower(message.Message), commandPrefix) {
|
||||
return
|
||||
}
|
||||
|
||||
args := strings.Fields(message.Message[len(commandPrefix):])
|
||||
if len(args) < 1 {
|
||||
return
|
||||
}
|
||||
commandName := args[0]
|
||||
args = args[1:]
|
||||
|
||||
switch commandName {
|
||||
case "status":
|
||||
if !contains(b.cfg.Admins, message.User.Name) {
|
||||
return
|
||||
}
|
||||
uptime := humanize.TimeSince(b.startTime)
|
||||
b.Say(message.Channel, fmt.Sprintf("%s, uptime: %s", message.User.DisplayName, uptime))
|
||||
|
||||
case "join":
|
||||
if !contains(b.cfg.Admins, message.User.Name) {
|
||||
return
|
||||
}
|
||||
b.handleJoin(message, args)
|
||||
|
||||
case "part":
|
||||
if !contains(b.cfg.Admins, message.User.Name) {
|
||||
return
|
||||
}
|
||||
b.handlePart(message, args)
|
||||
|
||||
case "optout":
|
||||
b.handleOptOut(message, args)
|
||||
case "optin":
|
||||
if !contains(b.cfg.Admins, message.User.Name) {
|
||||
return
|
||||
}
|
||||
b.handleOptIn(message, args)
|
||||
}
|
||||
}
|
||||
|
||||
// Commands
|
||||
|
||||
func (b *Bot) handleJoin(message twitch.PrivateMessage, args []string) {
|
||||
if len(args) < 1 {
|
||||
b.Say(message.Channel, message.User.DisplayName+errNoUsernames)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := b.helixClient.GetUsersByUsernames(args)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
b.Say(message.Channel, message.User.DisplayName+errRequestingUserIDs)
|
||||
}
|
||||
|
||||
ids := []string{}
|
||||
for _, user := range users {
|
||||
ids = append(ids, user.ID)
|
||||
b.Join(user.Login)
|
||||
}
|
||||
b.cfg.AddChannels(ids...)
|
||||
b.Say(message.Channel, fmt.Sprintf("%s, added channels: %v", message.User.DisplayName, ids))
|
||||
}
|
||||
|
||||
func (b *Bot) handlePart(message twitch.PrivateMessage, args []string) {
|
||||
if len(args) < 1 {
|
||||
b.Say(message.Channel, message.User.DisplayName+errNoUsernames)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := b.helixClient.GetUsersByUsernames(args)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
b.Say(message.Channel, message.User.DisplayName+errRequestingUserIDs)
|
||||
}
|
||||
|
||||
ids := []string{}
|
||||
for _, user := range users {
|
||||
ids = append(ids, user.ID)
|
||||
b.Part(user.Login)
|
||||
}
|
||||
b.cfg.RemoveChannels(ids...)
|
||||
b.Say(message.Channel, fmt.Sprintf("%s, removed channels: %v", message.User.DisplayName, ids))
|
||||
}
|
||||
|
||||
func (b *Bot) handleOptOut(message twitch.PrivateMessage, args []string) {
|
||||
if len(args) < 1 {
|
||||
b.Say(message.Channel, message.User.DisplayName+errNoUsernames)
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := b.OptoutCodes.LoadAndDelete(args[0]); ok {
|
||||
b.cfg.OptOutUsers(message.User.ID)
|
||||
b.Say(message.Channel, fmt.Sprintf("%s, opted you out", message.User.DisplayName))
|
||||
return
|
||||
}
|
||||
|
||||
if !contains(b.cfg.Admins, message.User.Name) {
|
||||
return
|
||||
}
|
||||
|
||||
users, err := b.helixClient.GetUsersByUsernames(args)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
b.Say(message.Channel, message.User.DisplayName+errRequestingUserIDs)
|
||||
}
|
||||
|
||||
ids := []string{}
|
||||
for _, user := range users {
|
||||
ids = append(ids, user.ID)
|
||||
}
|
||||
b.cfg.OptOutUsers(ids...)
|
||||
b.Say(message.Channel, fmt.Sprintf("%s, opted out channels: %v", message.User.DisplayName, ids))
|
||||
}
|
||||
|
||||
func (b *Bot) handleOptIn(message twitch.PrivateMessage, args []string) {
|
||||
if len(args) < 1 {
|
||||
b.Say(message.Channel, message.User.DisplayName+errNoUsernames)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := b.helixClient.GetUsersByUsernames(args)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
b.Say(message.Channel, message.User.DisplayName+errRequestingUserIDs)
|
||||
}
|
||||
|
||||
ids := []string{}
|
||||
for _, user := range users {
|
||||
ids = append(ids, user.ID)
|
||||
}
|
||||
|
||||
b.cfg.RemoveOptOut(ids...)
|
||||
b.Say(message.Channel, fmt.Sprintf("%s, opted in channels: %v", message.User.DisplayName, ids))
|
||||
}
|
||||
|
||||
// Utilities
|
||||
|
||||
func contains(arr []string, str string) bool {
|
||||
for _, x := range arr {
|
||||
if x == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
255
bot/main.go
255
bot/main.go
|
@ -1,255 +0,0 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gempir/justlog/config"
|
||||
"github.com/gempir/justlog/filelog"
|
||||
expiremap "github.com/nursik/go-expire-map"
|
||||
|
||||
twitch "github.com/gempir/go-twitch-irc/v3"
|
||||
"github.com/gempir/justlog/helix"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Bot basic logging bot
|
||||
type Bot struct {
|
||||
startTime time.Time
|
||||
cfg *config.Config
|
||||
helixClient helix.TwitchApiClient
|
||||
logger filelog.Logger
|
||||
worker []*worker
|
||||
channels map[string]helix.UserData
|
||||
clearchats sync.Map
|
||||
OptoutCodes sync.Map
|
||||
msgMap *expiremap.ExpireMap
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
client *twitch.Client
|
||||
joinedChannels map[string]bool
|
||||
}
|
||||
|
||||
func newWorker(client *twitch.Client) *worker {
|
||||
return &worker{
|
||||
client: client,
|
||||
joinedChannels: map[string]bool{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewBot create new bot instance
|
||||
func NewBot(cfg *config.Config, helixClient helix.TwitchApiClient, fileLogger filelog.Logger) *Bot {
|
||||
channels, err := helixClient.GetUsersByUserIds(cfg.Channels)
|
||||
if err != nil {
|
||||
log.Fatalf("[bot] failed to load configured channels %s", err.Error())
|
||||
}
|
||||
|
||||
return &Bot{
|
||||
cfg: cfg,
|
||||
helixClient: helixClient,
|
||||
logger: fileLogger,
|
||||
channels: channels,
|
||||
worker: []*worker{},
|
||||
OptoutCodes: sync.Map{},
|
||||
msgMap: expiremap.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) Say(channel, text string) {
|
||||
randomIndex := rand.Intn(len(b.worker))
|
||||
b.worker[randomIndex].client.Say(channel, text)
|
||||
}
|
||||
|
||||
// Connect startup the logger and bot
|
||||
func (b *Bot) Connect() {
|
||||
b.startTime = time.Now()
|
||||
client := b.newClient()
|
||||
go b.startJoinLoop()
|
||||
|
||||
if strings.HasPrefix(b.cfg.Username, "justinfan") {
|
||||
log.Info("[bot] joining as anonymous user")
|
||||
} else {
|
||||
log.Info("[bot] joining as user " + b.cfg.Username)
|
||||
}
|
||||
|
||||
defer b.msgMap.Close()
|
||||
log.Fatal(client.Connect())
|
||||
}
|
||||
|
||||
// constantly join channels to rejoin some channels that got unbanned over time
|
||||
func (b *Bot) startJoinLoop() {
|
||||
for {
|
||||
for _, channel := range b.channels {
|
||||
b.Join(channel.Login)
|
||||
}
|
||||
|
||||
time.Sleep(time.Hour * 1)
|
||||
log.Info("[bot] running hourly join loop")
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) Part(channelNames ...string) {
|
||||
for _, channelName := range channelNames {
|
||||
log.Info("[bot] leaving " + channelName)
|
||||
|
||||
for _, worker := range b.worker {
|
||||
worker.client.Depart(channelName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) Join(channelNames ...string) {
|
||||
for _, channel := range channelNames {
|
||||
channel = strings.ToLower(channel)
|
||||
|
||||
joined := false
|
||||
|
||||
for _, worker := range b.worker {
|
||||
if _, ok := worker.joinedChannels[channel]; ok {
|
||||
// already joined but join again in case it was a temporary ban
|
||||
worker.client.Join(channel)
|
||||
joined = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, worker := range b.worker {
|
||||
if len(worker.joinedChannels) < 50 {
|
||||
log.Info("[bot] joining " + channel)
|
||||
worker.client.Join(channel)
|
||||
worker.joinedChannels[channel] = true
|
||||
joined = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !joined {
|
||||
client := b.newClient()
|
||||
go client.Connect()
|
||||
b.Join(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) newClient() *twitch.Client {
|
||||
client := twitch.NewClient(b.cfg.Username, "oauth:"+b.cfg.OAuth)
|
||||
if b.cfg.BotVerified {
|
||||
client.SetJoinRateLimiter(twitch.CreateVerifiedRateLimiter())
|
||||
}
|
||||
|
||||
b.worker = append(b.worker, newWorker(client))
|
||||
log.Infof("[bot] creating new twitch connection, new total: %d", len(b.worker))
|
||||
|
||||
client.OnPrivateMessage(b.handlePrivateMessage)
|
||||
client.OnUserNoticeMessage(b.handleUserNotice)
|
||||
client.OnClearChatMessage(b.handleClearChat)
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (b *Bot) handlePrivateMessage(message twitch.PrivateMessage) {
|
||||
if _, ok := b.msgMap.Get(message.ID); ok {
|
||||
return
|
||||
}
|
||||
b.msgMap.Set(message.ID, true, time.Second*3)
|
||||
|
||||
b.handlePrivateMessageCommands(message)
|
||||
|
||||
if b.cfg.IsOptedOut(message.User.ID) || b.cfg.IsOptedOut(message.RoomID) {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := b.logger.LogPrivateMessageForUser(message.User, message)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
err := b.logger.LogPrivateMessageForChannel(message)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (b *Bot) handleUserNotice(message twitch.UserNoticeMessage) {
|
||||
if _, ok := b.msgMap.Get(message.ID); ok {
|
||||
return
|
||||
}
|
||||
b.msgMap.Set(message.ID, true, time.Second*3)
|
||||
|
||||
if b.cfg.IsOptedOut(message.User.ID) || b.cfg.IsOptedOut(message.RoomID) {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := b.logger.LogUserNoticeMessageForUser(message.User.ID, message)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
if _, ok := message.Tags["msg-param-recipient-id"]; ok {
|
||||
go func() {
|
||||
err := b.logger.LogUserNoticeMessageForUser(message.Tags["msg-param-recipient-id"], message)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := b.logger.LogUserNoticeMessageForChannel(message)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (b *Bot) handleClearChat(message twitch.ClearChatMessage) {
|
||||
if b.cfg.IsOptedOut(message.TargetUserID) || b.cfg.IsOptedOut(message.RoomID) {
|
||||
return
|
||||
}
|
||||
|
||||
if message.BanDuration == 0 {
|
||||
count, ok := b.clearchats.Load(message.RoomID)
|
||||
if !ok {
|
||||
count = 0
|
||||
}
|
||||
newCount := count.(int) + 1
|
||||
b.clearchats.Store(message.RoomID, newCount)
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Second * 1)
|
||||
count, ok := b.clearchats.Load(message.RoomID)
|
||||
if ok {
|
||||
b.clearchats.Store(message.RoomID, count.(int)-1)
|
||||
}
|
||||
}()
|
||||
|
||||
if newCount > 50 {
|
||||
if newCount == 51 {
|
||||
log.Infof("Stopped recording CLEARCHAT permabans in: %s", message.Channel)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := b.logger.LogClearchatMessageForUser(message.TargetUserID, message)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
err := b.logger.LogClearchatMessageForChannel(message)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"admins": ["gempir"],
|
||||
"logsDirectory": "./logs",
|
||||
"adminAPIKey": "noshot",
|
||||
"username": "gempbot",
|
||||
"oauth": "oauthtokenforchat",
|
||||
"botVerified": true,
|
||||
"clientID": "mytwitchclientid",
|
||||
"clientSecret": "mysecret",
|
||||
"logLevel": "info",
|
||||
"channels": ["77829817", "11148817"],
|
||||
"archive": true
|
||||
}
|
176
config/main.go
176
config/main.go
|
@ -1,176 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Config application configuration
|
||||
type Config struct {
|
||||
configFile string
|
||||
configFilePermissions os.FileMode
|
||||
BotVerified bool `json:"botVerified"`
|
||||
LogsDirectory string `json:"logsDirectory"`
|
||||
Archive bool `json:"archive"`
|
||||
AdminAPIKey string `json:"adminAPIKey"`
|
||||
Username string `json:"username"`
|
||||
OAuth string `json:"oauth"`
|
||||
ListenAddress string `json:"listenAddress"`
|
||||
Admins []string `json:"admins"`
|
||||
Channels []string `json:"channels"`
|
||||
ClientID string `json:"clientID"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
LogLevel string `json:"logLevel"`
|
||||
OptOut map[string]bool `json:"optOut"`
|
||||
}
|
||||
|
||||
// NewConfig create configuration from file
|
||||
func NewConfig(filePath string) *Config {
|
||||
cfg := loadConfiguration(filePath)
|
||||
|
||||
log.Info("Loaded config from " + filePath)
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// AddChannels adds channels to the config
|
||||
func (cfg *Config) AddChannels(channelIDs ...string) {
|
||||
for _, id := range channelIDs {
|
||||
cfg.Channels = appendIfMissing(cfg.Channels, id)
|
||||
}
|
||||
|
||||
cfg.persistConfig()
|
||||
}
|
||||
|
||||
// OptOutUsers will opt out a user
|
||||
func (cfg *Config) OptOutUsers(userIDs ...string) {
|
||||
for _, id := range userIDs {
|
||||
cfg.OptOut[id] = true
|
||||
}
|
||||
|
||||
cfg.persistConfig()
|
||||
}
|
||||
|
||||
// IsOptedOut check if a user is opted out
|
||||
func (cfg *Config) IsOptedOut(userID string) bool {
|
||||
_, ok := cfg.OptOut[userID]
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// AddChannels remove user from opt out
|
||||
func (cfg *Config) RemoveOptOut(userIDs ...string) {
|
||||
for _, id := range userIDs {
|
||||
delete(cfg.OptOut, id)
|
||||
}
|
||||
|
||||
cfg.persistConfig()
|
||||
}
|
||||
|
||||
// RemoveChannels removes channels from the config
|
||||
func (cfg *Config) RemoveChannels(channelIDs ...string) {
|
||||
channels := cfg.Channels
|
||||
|
||||
for i, channel := range channels {
|
||||
for _, removeChannel := range channelIDs {
|
||||
if channel == removeChannel {
|
||||
channels[i] = channels[len(channels)-1]
|
||||
channels[len(channels)-1] = ""
|
||||
channels = channels[:len(channels)-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg.Channels = channels
|
||||
cfg.persistConfig()
|
||||
}
|
||||
|
||||
func appendIfMissing(slice []string, i string) []string {
|
||||
for _, ele := range slice {
|
||||
if ele == i {
|
||||
return slice
|
||||
}
|
||||
}
|
||||
return append(slice, i)
|
||||
}
|
||||
|
||||
func (cfg *Config) persistConfig() {
|
||||
fileContents, err := json.MarshalIndent(*cfg, "", " ")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(cfg.configFile, fileContents, cfg.configFilePermissions)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfiguration(filePath string) *Config {
|
||||
// setup defaults
|
||||
cfg := Config{
|
||||
configFile: filePath,
|
||||
LogsDirectory: "./logs",
|
||||
ListenAddress: ":8025",
|
||||
Username: "justinfan777777",
|
||||
OAuth: "oauth:777777777",
|
||||
Channels: []string{},
|
||||
Admins: []string{"gempir"},
|
||||
LogLevel: "info",
|
||||
Archive: true,
|
||||
OptOut: map[string]bool{},
|
||||
}
|
||||
|
||||
info, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cfg.configFilePermissions = info.Mode()
|
||||
|
||||
configFile, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer configFile.Close()
|
||||
|
||||
jsonParser := json.NewDecoder(configFile)
|
||||
err = jsonParser.Decode(&cfg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// normalize
|
||||
cfg.LogsDirectory = strings.TrimSuffix(cfg.LogsDirectory, "/")
|
||||
cfg.OAuth = strings.TrimPrefix(cfg.OAuth, "oauth:")
|
||||
cfg.LogLevel = strings.ToLower(cfg.LogLevel)
|
||||
cfg.setupLogger()
|
||||
|
||||
// ensure required
|
||||
if cfg.ClientID == "" {
|
||||
log.Fatal("No clientID specified")
|
||||
}
|
||||
|
||||
return &cfg
|
||||
}
|
||||
|
||||
func (cfg *Config) setupLogger() {
|
||||
switch cfg.LogLevel {
|
||||
case "fatal":
|
||||
log.SetLevel(log.FatalLevel)
|
||||
case "panic":
|
||||
log.SetLevel(log.PanicLevel)
|
||||
case "error":
|
||||
log.SetLevel(log.ErrorLevel)
|
||||
case "warn":
|
||||
log.SetLevel(log.WarnLevel)
|
||||
case "info":
|
||||
log.SetLevel(log.InfoLevel)
|
||||
case "debug":
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
}
|
|
@ -1,191 +0,0 @@
|
|||
package filelog
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (l *FileLogger) LogPrivateMessageForChannel(message twitch.PrivateMessage) error {
|
||||
year := message.Time.Year()
|
||||
month := int(message.Time.Month())
|
||||
day := message.Time.Day()
|
||||
err := os.MkdirAll(fmt.Sprintf(l.logPath+"/%s/%d/%d/%d", message.RoomID, year, month, day), 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filename := fmt.Sprintf(l.logPath+"/%s/%d/%d/%d/channel.txt", message.RoomID, year, month, day)
|
||||
|
||||
file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err = file.WriteString(message.Raw + "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *FileLogger) LogClearchatMessageForChannel(message twitch.ClearChatMessage) error {
|
||||
year := message.Time.Year()
|
||||
month := int(message.Time.Month())
|
||||
day := message.Time.Day()
|
||||
err := os.MkdirAll(fmt.Sprintf(l.logPath+"/%s/%d/%d/%d", message.RoomID, year, month, day), 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filename := fmt.Sprintf(l.logPath+"/%s/%d/%d/%d/channel.txt", message.RoomID, year, month, day)
|
||||
|
||||
file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err = file.WriteString(message.Raw + "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *FileLogger) LogUserNoticeMessageForChannel(message twitch.UserNoticeMessage) error {
|
||||
year := message.Time.Year()
|
||||
month := int(message.Time.Month())
|
||||
day := message.Time.Day()
|
||||
err := os.MkdirAll(fmt.Sprintf(l.logPath+"/%s/%d/%d/%d", message.RoomID, year, month, day), 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filename := fmt.Sprintf(l.logPath+"/%s/%d/%d/%d/channel.txt", message.RoomID, year, month, day)
|
||||
|
||||
file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err = file.WriteString(message.Raw + "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *FileLogger) ReadLogForChannel(channelID string, year int, month int, day int) ([]string, error) {
|
||||
filename := fmt.Sprintf(l.logPath+"/%s/%d/%d/%d/channel.txt", channelID, year, month, day)
|
||||
|
||||
if _, err := os.Stat(filename); err != nil {
|
||||
filename = filename + ".gz"
|
||||
}
|
||||
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return []string{}, errors.New("file not found: " + filename)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var reader io.Reader
|
||||
|
||||
if strings.HasSuffix(filename, ".gz") {
|
||||
gz, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return []string{}, errors.New("file gzip not readable")
|
||||
}
|
||||
reader = gz
|
||||
} else {
|
||||
reader = f
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return []string{}, errors.New("file not readable")
|
||||
}
|
||||
|
||||
content := []string{}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
content = append(content, line)
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func (l *FileLogger) ReadRandomMessageForChannel(channelID string) (string, error) {
|
||||
var dayFileList []string
|
||||
var lines []string
|
||||
|
||||
if channelID == "" {
|
||||
return "", errors.New("missing channelID")
|
||||
}
|
||||
|
||||
years, _ := ioutil.ReadDir(l.logPath + "/" + channelID)
|
||||
|
||||
for _, yearDir := range years {
|
||||
year := yearDir.Name()
|
||||
months, _ := ioutil.ReadDir(l.logPath + "/" + channelID + "/" + year + "/")
|
||||
for _, monthDir := range months {
|
||||
month := monthDir.Name()
|
||||
|
||||
possibleDays := []int{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}
|
||||
|
||||
for _, day := range possibleDays {
|
||||
dayDirPath := l.logPath + "/" + channelID + "/" + year + "/" + month + "/" + fmt.Sprint(day)
|
||||
logFiles, err := ioutil.ReadDir(dayDirPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, logFile := range logFiles {
|
||||
logFilePath := dayDirPath + "/" + logFile.Name()
|
||||
dayFileList = append(dayFileList, logFilePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(dayFileList) < 1 {
|
||||
return "", errors.New("no log found")
|
||||
}
|
||||
|
||||
randomDayIndex := rand.Intn(len(dayFileList))
|
||||
randomDayPath := dayFileList[randomDayIndex]
|
||||
|
||||
f, _ := os.Open(randomDayPath)
|
||||
scanner := bufio.NewScanner(f)
|
||||
|
||||
if strings.HasSuffix(randomDayPath, ".gz") {
|
||||
gz, _ := gzip.NewReader(f)
|
||||
scanner = bufio.NewScanner(gz)
|
||||
}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
lines = append(lines, line)
|
||||
}
|
||||
|
||||
f.Close()
|
||||
|
||||
if len(lines) < 1 {
|
||||
log.Infof("path %s", randomDayPath)
|
||||
return "", errors.New("no lines found")
|
||||
}
|
||||
|
||||
randomLineNumber := rand.Intn(len(lines))
|
||||
return lines[randomLineNumber], nil
|
||||
}
|
|
@ -1,377 +0,0 @@
|
|||
package filelog
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gempir/go-twitch-irc/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
LogPrivateMessageForUser(user twitch.User, message twitch.PrivateMessage) error
|
||||
LogClearchatMessageForUser(userID string, message twitch.ClearChatMessage) error
|
||||
LogUserNoticeMessageForUser(userID string, message twitch.UserNoticeMessage) error
|
||||
GetLastLogYearAndMonthForUser(channelID, userID string) (int, int, error)
|
||||
GetAvailableLogsForUser(channelID, userID string) ([]UserLogFile, error)
|
||||
ReadLogForUser(channelID, userID string, year string, month string) ([]string, error)
|
||||
ReadRandomMessageForUser(channelID, userID string) (string, error)
|
||||
|
||||
LogPrivateMessageForChannel(message twitch.PrivateMessage) error
|
||||
LogClearchatMessageForChannel(message twitch.ClearChatMessage) error
|
||||
LogUserNoticeMessageForChannel(message twitch.UserNoticeMessage) error
|
||||
ReadLogForChannel(channelID string, year int, month int, day int) ([]string, error)
|
||||
ReadRandomMessageForChannel(channelID string) (string, error)
|
||||
GetAvailableLogsForChannel(channelID string) ([]ChannelLogFile, error)
|
||||
}
|
||||
|
||||
type FileLogger struct {
|
||||
logPath string
|
||||
}
|
||||
|
||||
func NewFileLogger(logPath string) FileLogger {
|
||||
return FileLogger{
|
||||
logPath: logPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *FileLogger) LogPrivateMessageForUser(user twitch.User, message twitch.PrivateMessage) error {
|
||||
year := message.Time.Year()
|
||||
month := int(message.Time.Month())
|
||||
|
||||
err := os.MkdirAll(fmt.Sprintf(l.logPath+"/%s/%d/%d/", message.RoomID, year, month), 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filename := fmt.Sprintf(l.logPath+"/%s/%d/%d/%s.txt", message.RoomID, year, month, user.ID)
|
||||
|
||||
file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err = file.WriteString(message.Raw + "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *FileLogger) LogClearchatMessageForUser(userID string, message twitch.ClearChatMessage) error {
|
||||
year := message.Time.Year()
|
||||
month := int(message.Time.Month())
|
||||
|
||||
err := os.MkdirAll(fmt.Sprintf(l.logPath+"/%s/%d/%d/", message.RoomID, year, month), 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filename := fmt.Sprintf(l.logPath+"/%s/%d/%d/%s.txt", message.RoomID, year, month, userID)
|
||||
|
||||
file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err = file.WriteString(message.Raw + "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *FileLogger) LogUserNoticeMessageForUser(userID string, message twitch.UserNoticeMessage) error {
|
||||
year := message.Time.Year()
|
||||
month := int(message.Time.Month())
|
||||
|
||||
err := os.MkdirAll(fmt.Sprintf(l.logPath+"/%s/%d/%d/", message.RoomID, year, month), 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filename := fmt.Sprintf(l.logPath+"/%s/%d/%d/%s.txt", message.RoomID, year, month, userID)
|
||||
|
||||
file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err = file.WriteString(message.Raw + "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserLogFile struct {
|
||||
path string
|
||||
Year string `json:"year"`
|
||||
Month string `json:"month"`
|
||||
}
|
||||
|
||||
func (l *FileLogger) GetLastLogYearAndMonthForUser(channelID, userID string) (int, int, error) {
|
||||
if channelID == "" || userID == "" {
|
||||
return 0, 0, fmt.Errorf("Invalid channelID: %s or userID: %s", channelID, userID)
|
||||
}
|
||||
|
||||
logFiles := []UserLogFile{}
|
||||
|
||||
years, _ := ioutil.ReadDir(l.logPath + "/" + channelID)
|
||||
|
||||
for _, yearDir := range years {
|
||||
year := yearDir.Name()
|
||||
months, _ := ioutil.ReadDir(l.logPath + "/" + channelID + "/" + year + "/")
|
||||
for _, monthDir := range months {
|
||||
month := monthDir.Name()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/%s.txt", l.logPath, channelID, year, month, userID)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
|
||||
logFile := UserLogFile{path, year, month}
|
||||
logFiles = append(logFiles, logFile)
|
||||
} else if _, err := os.Stat(path + ".gz"); err == nil {
|
||||
logFile := UserLogFile{path + ".gz", year, month}
|
||||
logFiles = append(logFiles, logFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(logFiles, func(i, j int) bool {
|
||||
yearA, _ := strconv.Atoi(logFiles[i].Year)
|
||||
yearB, _ := strconv.Atoi(logFiles[j].Year)
|
||||
monthA, _ := strconv.Atoi(logFiles[i].Month)
|
||||
monthB, _ := strconv.Atoi(logFiles[j].Month)
|
||||
|
||||
if yearA == yearB {
|
||||
return monthA > monthB
|
||||
}
|
||||
|
||||
return yearA > yearB
|
||||
})
|
||||
|
||||
if len(logFiles) > 0 {
|
||||
year, _ := strconv.Atoi(logFiles[0].Year)
|
||||
month, _ := strconv.Atoi(logFiles[0].Month)
|
||||
|
||||
return year, month, nil
|
||||
}
|
||||
|
||||
return 0, 0, errors.New("No logs file")
|
||||
}
|
||||
|
||||
func (l *FileLogger) GetAvailableLogsForUser(channelID, userID string) ([]UserLogFile, error) {
|
||||
if channelID == "" || userID == "" {
|
||||
return []UserLogFile{}, fmt.Errorf("Invalid channelID: %s or userID: %s", channelID, userID)
|
||||
}
|
||||
|
||||
logFiles := []UserLogFile{}
|
||||
|
||||
years, _ := ioutil.ReadDir(l.logPath + "/" + channelID)
|
||||
|
||||
for _, yearDir := range years {
|
||||
year := yearDir.Name()
|
||||
months, _ := ioutil.ReadDir(l.logPath + "/" + channelID + "/" + year + "/")
|
||||
for _, monthDir := range months {
|
||||
month := monthDir.Name()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/%s.txt", l.logPath, channelID, year, month, userID)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
|
||||
logFile := UserLogFile{path, year, month}
|
||||
logFiles = append(logFiles, logFile)
|
||||
} else if _, err := os.Stat(path + ".gz"); err == nil {
|
||||
logFile := UserLogFile{path + ".gz", year, month}
|
||||
logFiles = append(logFiles, logFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(logFiles, func(i, j int) bool {
|
||||
yearA, _ := strconv.Atoi(logFiles[i].Year)
|
||||
yearB, _ := strconv.Atoi(logFiles[j].Year)
|
||||
monthA, _ := strconv.Atoi(logFiles[i].Month)
|
||||
monthB, _ := strconv.Atoi(logFiles[j].Month)
|
||||
|
||||
if yearA == yearB {
|
||||
return monthA > monthB
|
||||
}
|
||||
|
||||
return yearA > yearB
|
||||
})
|
||||
|
||||
if len(logFiles) > 0 {
|
||||
return logFiles, nil
|
||||
}
|
||||
|
||||
return logFiles, errors.New("No logs file")
|
||||
}
|
||||
|
||||
type ChannelLogFile struct {
|
||||
path string
|
||||
Year string `json:"year"`
|
||||
Month string `json:"month"`
|
||||
Day string `json:"day"`
|
||||
}
|
||||
|
||||
func (l *FileLogger) GetAvailableLogsForChannel(channelID string) ([]ChannelLogFile, error) {
|
||||
if channelID == "" {
|
||||
return []ChannelLogFile{}, fmt.Errorf("Invalid channelID: %s", channelID)
|
||||
}
|
||||
|
||||
logFiles := []ChannelLogFile{}
|
||||
|
||||
years, _ := ioutil.ReadDir(l.logPath + "/" + channelID)
|
||||
|
||||
for _, yearDir := range years {
|
||||
year := yearDir.Name()
|
||||
months, _ := ioutil.ReadDir(l.logPath + "/" + channelID + "/" + year + "/")
|
||||
for _, monthDir := range months {
|
||||
month := monthDir.Name()
|
||||
|
||||
days, _ := ioutil.ReadDir(l.logPath + "/" + channelID + "/" + year + "/" + month + "/")
|
||||
for _, dayDir := range days {
|
||||
day := dayDir.Name()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/%s/channel.txt", l.logPath, channelID, year, month, day)
|
||||
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
logFile := ChannelLogFile{path, year, month, day}
|
||||
logFiles = append(logFiles, logFile)
|
||||
} else if _, err := os.Stat(path + ".gz"); err == nil {
|
||||
logFile := ChannelLogFile{path + ".gz", year, month, day}
|
||||
logFiles = append(logFiles, logFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(logFiles, func(i, j int) bool {
|
||||
yearA, _ := strconv.Atoi(logFiles[i].Year)
|
||||
yearB, _ := strconv.Atoi(logFiles[j].Year)
|
||||
monthA, _ := strconv.Atoi(logFiles[i].Month)
|
||||
monthB, _ := strconv.Atoi(logFiles[j].Month)
|
||||
dayA, _ := strconv.Atoi(logFiles[j].Day)
|
||||
dayB, _ := strconv.Atoi(logFiles[j].Day)
|
||||
|
||||
if yearA == yearB {
|
||||
if monthA == monthB {
|
||||
return dayA > dayB
|
||||
}
|
||||
|
||||
return monthA > monthB
|
||||
}
|
||||
|
||||
if monthA == monthB {
|
||||
return dayA > dayB
|
||||
}
|
||||
|
||||
return yearA > yearB
|
||||
})
|
||||
|
||||
if len(logFiles) > 0 {
|
||||
return logFiles, nil
|
||||
}
|
||||
|
||||
return logFiles, errors.New("No logs file")
|
||||
}
|
||||
|
||||
// ReadLogForUser fetch logs
|
||||
func (l *FileLogger) ReadLogForUser(channelID, userID string, year string, month string) ([]string, error) {
|
||||
if channelID == "" || userID == "" {
|
||||
return []string{}, fmt.Errorf("Invalid channelID: %s or userID: %s", channelID, userID)
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf(l.logPath+"/%s/%s/%s/%s.txt", channelID, year, month, userID)
|
||||
|
||||
if _, err := os.Stat(filename); err != nil {
|
||||
filename = filename + ".gz"
|
||||
}
|
||||
|
||||
log.Debug("Opening " + filename)
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return []string{}, errors.New("file not found: " + filename)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var reader io.Reader
|
||||
|
||||
if strings.HasSuffix(filename, ".gz") {
|
||||
gz, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return []string{}, errors.New("file gzip not readable")
|
||||
}
|
||||
reader = gz
|
||||
} else {
|
||||
reader = f
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return []string{}, errors.New("file not readable")
|
||||
}
|
||||
|
||||
content := []string{}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
content = append(content, line)
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func (l *FileLogger) ReadRandomMessageForUser(channelID, userID string) (string, error) {
|
||||
var userLogs []string
|
||||
var lines []string
|
||||
|
||||
if channelID == "" || userID == "" {
|
||||
return "", errors.New("missing channelID or userID")
|
||||
}
|
||||
|
||||
years, _ := ioutil.ReadDir(l.logPath + "/" + channelID)
|
||||
for _, yearDir := range years {
|
||||
year := yearDir.Name()
|
||||
months, _ := ioutil.ReadDir(l.logPath + "/" + channelID + "/" + year + "/")
|
||||
for _, monthDir := range months {
|
||||
month := monthDir.Name()
|
||||
path := fmt.Sprintf("%s/%s/%s/%s/%s.txt", l.logPath, channelID, year, month, userID)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
userLogs = append(userLogs, path)
|
||||
} else if _, err := os.Stat(path + ".gz"); err == nil {
|
||||
userLogs = append(userLogs, path+".gz")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(userLogs) < 1 {
|
||||
return "", errors.New("no log found")
|
||||
}
|
||||
|
||||
for _, logFile := range userLogs {
|
||||
f, _ := os.Open(logFile)
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
|
||||
if strings.HasSuffix(logFile, ".gz") {
|
||||
gz, _ := gzip.NewReader(f)
|
||||
scanner = bufio.NewScanner(gz)
|
||||
}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
lines = append(lines, line)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
ranNum := rand.Intn(len(lines))
|
||||
|
||||
return lines[ranNum], nil
|
||||
}
|
11
go.mod
11
go.mod
|
@ -1,11 +0,0 @@
|
|||
module github.com/gempir/justlog
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/gempir/go-twitch-irc/v3 v3.0.0
|
||||
github.com/nicklaw5/helix v1.25.0
|
||||
github.com/nursik/go-expire-map v1.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/stretchr/testify v1.7.0 // indirect
|
||||
)
|
26
go.sum
26
go.sum
|
@ -1,26 +0,0 @@
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gempir/go-twitch-irc/v3 v3.0.0 h1:e34R+9BdKy+qrO/wN+FCt+BUtyn38gCnJuKWscIKbl4=
|
||||
github.com/gempir/go-twitch-irc/v3 v3.0.0/go.mod h1:/W9KZIiyizVecp4PEb7kc4AlIyXKiCmvlXrzlpPUytU=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/nicklaw5/helix v1.25.0 h1:Mrz537izZVsGdM3I46uGAAlslj61frgkhS/9xQqyT/M=
|
||||
github.com/nicklaw5/helix v1.25.0/go.mod h1:yvXZFapT6afIoxnAvlWiJiUMsYnoHl7tNs+t0bloAMw=
|
||||
github.com/nursik/go-expire-map v1.1.0 h1:C+OJ81JtHDSPJXfuu0g3e8RRjHLd5of8dVzyfDOB9KY=
|
||||
github.com/nursik/go-expire-map v1.1.0/go.mod h1:wdQsai5n32Uw1IuVXXZoopePGCFh5vb0Dka/TRcboHs=
|
||||
github.com/nursik/go-ordered-set v0.0.0-20190626022851-0e8872c36517 h1:jau4pavdQo5lHeVTjZEGrm4+zvVGZj8SFQt4awsLLXE=
|
||||
github.com/nursik/go-ordered-set v0.0.0-20190626022851-0e8872c36517/go.mod h1:qFI7Mmmx8i+Qz8a52FarvpgPQzylvD3w77JAwvnFtKg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
208
helix/user.go
208
helix/user.go
|
@ -1,208 +0,0 @@
|
|||
package helix
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
helixClient "github.com/nicklaw5/helix"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Client wrapper for helix
|
||||
type Client struct {
|
||||
clientID string
|
||||
clientSecret string
|
||||
appAccessToken string
|
||||
client *helixClient.Client
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
var (
|
||||
userCacheByID sync.Map
|
||||
userCacheByUsername sync.Map
|
||||
)
|
||||
|
||||
type TwitchApiClient interface {
|
||||
GetUsersByUserIds([]string) (map[string]UserData, error)
|
||||
GetUsersByUsernames([]string) (map[string]UserData, error)
|
||||
}
|
||||
|
||||
// NewClient Create helix client
|
||||
func NewClient(clientID string, clientSecret string) Client {
|
||||
client, err := helixClient.NewClient(&helixClient.Options{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
resp, err := client.RequestAppAccessToken([]string{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Infof("Requested access token, response: %d, expires in: %d", resp.StatusCode, resp.Data.ExpiresIn)
|
||||
client.SetAppAccessToken(resp.Data.AccessToken)
|
||||
|
||||
return Client{
|
||||
clientID: clientID,
|
||||
clientSecret: clientSecret,
|
||||
appAccessToken: resp.Data.AccessToken,
|
||||
client: client,
|
||||
httpClient: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
// UserData exported data from twitch
|
||||
type UserData struct {
|
||||
ID string `json:"id"`
|
||||
Login string `json:"login"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Type string `json:"type"`
|
||||
BroadcasterType string `json:"broadcaster_type"`
|
||||
Description string `json:"description"`
|
||||
ProfileImageURL string `json:"profile_image_url"`
|
||||
OfflineImageURL string `json:"offline_image_url"`
|
||||
ViewCount int `json:"view_count"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// StartRefreshTokenRoutine refresh our token
|
||||
func (c *Client) StartRefreshTokenRoutine() {
|
||||
ticker := time.NewTicker(24 * time.Hour)
|
||||
|
||||
for range ticker.C {
|
||||
resp, err := c.client.RequestAppAccessToken([]string{})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
log.Infof("Requested access token from routine, response: %d, expires in: %d", resp.StatusCode, resp.Data.ExpiresIn)
|
||||
|
||||
c.client.SetAppAccessToken(resp.Data.AccessToken)
|
||||
}
|
||||
}
|
||||
|
||||
func chunkBy(items []string, chunkSize int) (chunks [][]string) {
|
||||
for chunkSize < len(items) {
|
||||
items, chunks = items[chunkSize:], append(chunks, items[0:chunkSize:chunkSize])
|
||||
}
|
||||
|
||||
return append(chunks, items)
|
||||
}
|
||||
|
||||
// GetUsersByUserIds receive userData for given ids
|
||||
func (c *Client) GetUsersByUserIds(userIDs []string) (map[string]UserData, error) {
|
||||
var filteredUserIDs []string
|
||||
|
||||
for _, id := range userIDs {
|
||||
if _, ok := userCacheByID.Load(id); !ok {
|
||||
filteredUserIDs = append(filteredUserIDs, id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(filteredUserIDs) > 0 {
|
||||
chunks := chunkBy(filteredUserIDs, 100)
|
||||
|
||||
for _, chunk := range chunks {
|
||||
resp, err := c.client.GetUsers(&helixClient.UsersParams{
|
||||
IDs: chunk,
|
||||
})
|
||||
if err != nil {
|
||||
return map[string]UserData{}, err
|
||||
}
|
||||
|
||||
log.Infof("%d GetUsersByUserIds %v", resp.StatusCode, chunk)
|
||||
|
||||
for _, user := range resp.Data.Users {
|
||||
data := &UserData{
|
||||
ID: user.ID,
|
||||
Login: user.Login,
|
||||
DisplayName: user.Login,
|
||||
Type: user.Type,
|
||||
BroadcasterType: user.BroadcasterType,
|
||||
Description: user.Description,
|
||||
ProfileImageURL: user.ProfileImageURL,
|
||||
OfflineImageURL: user.OfflineImageURL,
|
||||
ViewCount: user.ViewCount,
|
||||
Email: user.Email,
|
||||
}
|
||||
userCacheByID.Store(user.ID, data)
|
||||
userCacheByUsername.Store(user.Login, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(map[string]UserData)
|
||||
|
||||
for _, id := range userIDs {
|
||||
value, ok := userCacheByID.Load(id)
|
||||
if !ok {
|
||||
log.Debugf("Could not find userId, channel might be banned: %s", id)
|
||||
continue
|
||||
}
|
||||
result[id] = *(value.(*UserData))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetUsersByUsernames fetches userdata from helix
|
||||
func (c *Client) GetUsersByUsernames(usernames []string) (map[string]UserData, error) {
|
||||
var filteredUsernames []string
|
||||
|
||||
for _, username := range usernames {
|
||||
username = strings.ToLower(username)
|
||||
if _, ok := userCacheByUsername.Load(username); !ok {
|
||||
filteredUsernames = append(filteredUsernames, username)
|
||||
}
|
||||
}
|
||||
|
||||
if len(filteredUsernames) > 0 {
|
||||
chunks := chunkBy(filteredUsernames, 100)
|
||||
|
||||
for _, chunk := range chunks {
|
||||
resp, err := c.client.GetUsers(&helixClient.UsersParams{
|
||||
Logins: chunk,
|
||||
})
|
||||
if err != nil {
|
||||
return map[string]UserData{}, err
|
||||
}
|
||||
|
||||
log.Infof("%d GetUsersByUsernames %v", resp.StatusCode, chunk)
|
||||
|
||||
for _, user := range resp.Data.Users {
|
||||
data := &UserData{
|
||||
ID: user.ID,
|
||||
Login: user.Login,
|
||||
DisplayName: user.Login,
|
||||
Type: user.Type,
|
||||
BroadcasterType: user.BroadcasterType,
|
||||
Description: user.Description,
|
||||
ProfileImageURL: user.ProfileImageURL,
|
||||
OfflineImageURL: user.OfflineImageURL,
|
||||
ViewCount: user.ViewCount,
|
||||
Email: user.Email,
|
||||
}
|
||||
userCacheByID.Store(user.ID, data)
|
||||
userCacheByUsername.Store(user.Login, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(map[string]UserData)
|
||||
|
||||
for _, username := range usernames {
|
||||
username = strings.ToLower(username)
|
||||
value, ok := userCacheByUsername.Load(username)
|
||||
if !ok {
|
||||
log.Debugf("Could not find username, channel might be banned: %s", username)
|
||||
continue
|
||||
}
|
||||
result[username] = *(value.(*UserData))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
107
humanize/time.go
107
humanize/time.go
|
@ -1,107 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TimeSince(a time.Time) string {
|
||||
return formatDiff(diff(a, time.Now()))
|
||||
}
|
||||
|
||||
func formatDiff(years, months, days, hours, mins, secs int) string {
|
||||
since := ""
|
||||
if years > 0 {
|
||||
switch years {
|
||||
case 1:
|
||||
since += fmt.Sprintf("%d year ", years)
|
||||
default:
|
||||
since += fmt.Sprintf("%d years ", years)
|
||||
}
|
||||
}
|
||||
if months > 0 {
|
||||
switch months {
|
||||
case 1:
|
||||
since += fmt.Sprintf("%d month ", months)
|
||||
default:
|
||||
since += fmt.Sprintf("%d months ", months)
|
||||
}
|
||||
}
|
||||
if days > 0 {
|
||||
switch days {
|
||||
case 1:
|
||||
since += fmt.Sprintf("%d day ", days)
|
||||
default:
|
||||
since += fmt.Sprintf("%d days ", days)
|
||||
}
|
||||
}
|
||||
if hours > 0 {
|
||||
switch hours {
|
||||
case 1:
|
||||
since += fmt.Sprintf("%d hour ", hours)
|
||||
default:
|
||||
since += fmt.Sprintf("%d hours ", hours)
|
||||
}
|
||||
}
|
||||
if mins > 0 && days == 0 && months == 0 && years == 0 {
|
||||
switch mins {
|
||||
case 1:
|
||||
since += fmt.Sprintf("%d min ", mins)
|
||||
default:
|
||||
since += fmt.Sprintf("%d mins ", mins)
|
||||
}
|
||||
}
|
||||
if secs > 0 && days == 0 && months == 0 && years == 0 && hours == 0 {
|
||||
switch secs {
|
||||
case 1:
|
||||
since += fmt.Sprintf("%d sec ", secs)
|
||||
default:
|
||||
since += fmt.Sprintf("%d secs ", secs)
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(since)
|
||||
}
|
||||
|
||||
func diff(a, b time.Time) (year, month, day, hour, min, sec int) {
|
||||
if a.After(b) {
|
||||
a, b = b, a
|
||||
}
|
||||
y1, M1, d1 := a.Date()
|
||||
y2, M2, d2 := b.Date()
|
||||
|
||||
h1, m1, s1 := a.Clock()
|
||||
h2, m2, s2 := b.Clock()
|
||||
|
||||
year = int(y2 - y1)
|
||||
month = int(M2 - M1)
|
||||
day = int(d2 - d1)
|
||||
hour = int(h2 - h1)
|
||||
min = int(m2 - m1)
|
||||
sec = int(s2 - s1)
|
||||
|
||||
// Normalize negative values
|
||||
if sec < 0 {
|
||||
sec += 60
|
||||
min--
|
||||
}
|
||||
if min < 0 {
|
||||
min += 60
|
||||
hour--
|
||||
}
|
||||
if hour < 0 {
|
||||
hour += 24
|
||||
day--
|
||||
}
|
||||
if day < 0 {
|
||||
// days in month:
|
||||
t := time.Date(y1, M1, 32, 0, 0, 0, 0, time.UTC)
|
||||
day += 32 - t.Day()
|
||||
month--
|
||||
}
|
||||
if month < 0 {
|
||||
month += 12
|
||||
year--
|
||||
}
|
||||
return
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var timeSinceTests = []struct {
|
||||
in time.Time
|
||||
out string
|
||||
}{
|
||||
{time.Now().AddDate(0, 0, -1), "1 day"},
|
||||
{time.Now().AddDate(0, -1, -1), "1 month 1 day"},
|
||||
{time.Now().Add(time.Minute * -10), "10 mins"},
|
||||
{time.Now().Add(time.Minute * -10 + time.Second * -30), "10 mins 30 secs"},
|
||||
}
|
||||
|
||||
func TestTimeSince(t *testing.T) {
|
||||
for _, testCase := range timeSinceTests {
|
||||
if since := TimeSince(testCase.in); since != testCase.out {
|
||||
t.Errorf("Incorrect time since string. Expected %s, Actual: %s", testCase.out, since)
|
||||
}
|
||||
}
|
||||
}
|
42
main.go
42
main.go
|
@ -1,42 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"flag"
|
||||
|
||||
"github.com/gempir/justlog/api"
|
||||
"github.com/gempir/justlog/archiver"
|
||||
"github.com/gempir/justlog/bot"
|
||||
"github.com/gempir/justlog/config"
|
||||
"github.com/gempir/justlog/filelog"
|
||||
"github.com/gempir/justlog/helix"
|
||||
)
|
||||
|
||||
// content holds our static web server content.
|
||||
//
|
||||
//go:embed web/dist/*
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
|
||||
configFile := flag.String("config", "config.json", "json config file")
|
||||
flag.Parse()
|
||||
|
||||
cfg := config.NewConfig(*configFile)
|
||||
|
||||
fileLogger := filelog.NewFileLogger(cfg.LogsDirectory)
|
||||
helixClient := helix.NewClient(cfg.ClientID, cfg.ClientSecret)
|
||||
go helixClient.StartRefreshTokenRoutine()
|
||||
|
||||
if cfg.Archive {
|
||||
archiver := archiver.NewArchiver(cfg.LogsDirectory)
|
||||
go archiver.Boot()
|
||||
}
|
||||
|
||||
bot := bot.NewBot(cfg, &helixClient, &fileLogger)
|
||||
|
||||
apiServer := api.NewServer(cfg, bot, &fileLogger, &helixClient, assets)
|
||||
go apiServer.Init()
|
||||
|
||||
bot.Connect()
|
||||
}
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
@ -1,25 +0,0 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
dist/*
|
||||
!dist/.gitkeep
|
||||
|
||||
public/swagger.json
|
||||
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
Loading…
Reference in New Issue