initial commit
This commit is contained in:
commit
24c9f21743
10 changed files with 761 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/subscribe-bot
|
||||
/db
|
||||
/config.toml
|
117
bot.go
Normal file
117
bot.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
type Bot struct {
|
||||
*discordgo.Session
|
||||
mentionRe *regexp.Regexp
|
||||
db *Db
|
||||
requests chan int
|
||||
}
|
||||
|
||||
func NewBot(token string, db *Db, requests chan int) (bot *Bot, err error) {
|
||||
s, err := discordgo.New("Bot " + token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = s.Open()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Println("connected to discord")
|
||||
|
||||
re, err := regexp.Compile("\\s*<@\\!?" + s.State.User.ID + ">\\s*")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
bot = &Bot{s, re, db, requests}
|
||||
s.AddHandler(bot.errWrap(bot.newMessageHandler))
|
||||
return
|
||||
}
|
||||
|
||||
func (bot *Bot) errWrap(fn interface{}) interface{} {
|
||||
val := reflect.ValueOf(fn)
|
||||
origType := reflect.TypeOf(fn)
|
||||
origTypeIn := make([]reflect.Type, origType.NumIn())
|
||||
for i := 0; i < origType.NumIn(); i++ {
|
||||
origTypeIn[i] = origType.In(i)
|
||||
}
|
||||
newType := reflect.FuncOf(origTypeIn, []reflect.Type{}, false)
|
||||
newFunc := reflect.MakeFunc(newType, func(args []reflect.Value) (result []reflect.Value) {
|
||||
res := val.Call(args)
|
||||
if len(res) > 0 && !res[0].IsNil() {
|
||||
err := res[0].Interface().(error)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("error: %s", err)
|
||||
channel, _ := bot.UserChannelCreate("100443064228646912")
|
||||
id, _ := bot.ChannelMessageSend(channel.ID, msg)
|
||||
log.Println(id, msg)
|
||||
}
|
||||
}
|
||||
return []reflect.Value{}
|
||||
})
|
||||
return newFunc.Interface()
|
||||
}
|
||||
|
||||
func (bot *Bot) newMessageHandler(s *discordgo.Session, m *discordgo.MessageCreate) (err error) {
|
||||
mentionsMe := false
|
||||
for _, user := range m.Mentions {
|
||||
if user.ID == s.State.User.ID {
|
||||
mentionsMe = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !mentionsMe {
|
||||
return
|
||||
}
|
||||
|
||||
msg := bot.mentionRe.ReplaceAllString(m.Content, " ")
|
||||
msg = strings.Trim(msg, " ")
|
||||
|
||||
parts := strings.Split(msg, " ")
|
||||
switch strings.ToLower(parts[0]) {
|
||||
case "track":
|
||||
if len(parts) < 2 {
|
||||
err = errors.New("fucked up")
|
||||
return
|
||||
}
|
||||
|
||||
var mapperId int
|
||||
mapperId, err = strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = bot.db.ChannelTrackMapper(m.ChannelID, mapperId, 3)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(refreshInterval)
|
||||
bot.requests <- mapperId
|
||||
}()
|
||||
|
||||
bot.MessageReactionAdd(m.ChannelID, m.ID, "\xf0\x9f\x91\x8d")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (bot *Bot) Close() {
|
||||
bot.Session.Close()
|
||||
}
|
37
config.go
Normal file
37
config.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
BotToken string `toml:"bot_token"`
|
||||
ClientId int `toml:"client_id"`
|
||||
ClientSecret string `toml:"client_secret"`
|
||||
}
|
||||
|
||||
func ReadConfig(path string) (config Config, err error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("couldn't open file %s: %w", path, err)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("couldn't read data from %s: %w", path, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = toml.Unmarshal(data, &config)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("couldn't parse config data from %s: %w", path, err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
240
db.go
Normal file
240
db.go
Normal file
|
@ -0,0 +1,240 @@
|
|||
package main
|
||||
|
||||
// Database is laid out like this:
|
||||
// mapper/<mapper_id>/trackers/<channel_id> -> priority
|
||||
// mapper/<mapper_id>/latestEvent
|
||||
// channel/<channel_id>/tracks/<mapper_id> -> priority
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var (
|
||||
LATEST_EVENT = []byte("latestEvent")
|
||||
)
|
||||
|
||||
type Db struct {
|
||||
*bolt.DB
|
||||
api *Osuapi
|
||||
}
|
||||
|
||||
func OpenDb(path string, api *Osuapi) (db *Db, err error) {
|
||||
inner, err := bolt.Open(path, 0666, nil)
|
||||
db = &Db{inner, api}
|
||||
return
|
||||
}
|
||||
|
||||
// Loop over channels that are tracking this specific mapper
|
||||
func (db *Db) IterTrackingChannels(mapperId int, fn func(channelId string) error) (err error) {
|
||||
err = db.DB.View(func(tx *bolt.Tx) error {
|
||||
mapper := getMapper(tx, mapperId)
|
||||
if mapper == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
trackers := mapper.Bucket([]byte("trackers"))
|
||||
if trackers == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := trackers.Cursor()
|
||||
for k, _ := c.First(); k != nil; k, _ = c.Next() {
|
||||
channelId := string(k)
|
||||
err := fn(channelId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Loop over tracked mappers
|
||||
func (db *Db) IterTrackedMappers(fn func(userId int) error) (err error) {
|
||||
err = db.DB.View(func(tx *bolt.Tx) error {
|
||||
mappers := tx.Bucket([]byte("mapper"))
|
||||
if mappers == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := mappers.Cursor()
|
||||
for k, _ := c.First(); k != nil; k, _ = c.Next() {
|
||||
userId, err := strconv.Atoi(string(k))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = fn(userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Get a list of channels that are tracking this mapper
|
||||
func (db *Db) GetMapperTrackers(userId int) (trackersList []string) {
|
||||
trackersList = make([]string, 0)
|
||||
db.DB.View(func(tx *bolt.Tx) error {
|
||||
mapper, err := getMapperMut(tx, userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trackers := mapper.Bucket([]byte("trackers"))
|
||||
if trackers == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := trackers.Cursor()
|
||||
for k, _ := c.First(); k != nil; k, _ = c.Next() {
|
||||
channelId := string(k)
|
||||
trackersList = append(trackersList, channelId)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Update the latest event of a mapper to the given one
|
||||
func (db *Db) UpdateMapperLatestEvent(userId int, eventId int) (err error) {
|
||||
err = db.DB.Update(func(tx *bolt.Tx) error {
|
||||
mapper, err := getMapperMut(tx, userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = mapper.Put(LATEST_EVENT, []byte(strconv.Itoa(eventId)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Get the latest event ID of this mapper, if they have one
|
||||
func (db *Db) MapperLastEvent(userId int) (has bool, id int) {
|
||||
has = false
|
||||
id = -1
|
||||
db.DB.View(func(tx *bolt.Tx) error {
|
||||
mapper := getMapper(tx, userId)
|
||||
if mapper == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
lastEventId := mapper.Get(LATEST_EVENT)
|
||||
if lastEventId == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
id, err = strconv.Atoi(string(lastEventId))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
has = true
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Start tracking a new mapper (if they're not already tracked)
|
||||
func (db *Db) ChannelTrackMapper(channelId string, mapperId int, priority int) (err error) {
|
||||
events, err := db.api.GetUserEvents(mapperId, 1, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = db.Batch(func(tx *bolt.Tx) error {
|
||||
{
|
||||
mapper, err := getMapperMut(tx, mapperId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(events) > 0 {
|
||||
latestEventId := strconv.Itoa(events[0].ID)
|
||||
mapper.Put(LATEST_EVENT, []byte(latestEventId))
|
||||
}
|
||||
|
||||
trackers, err := mapper.CreateBucketIfNotExists([]byte("trackers"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = trackers.Put([]byte(channelId), []byte(strconv.Itoa(priority)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
{
|
||||
channels, err := tx.CreateBucketIfNotExists([]byte("channels"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channel, err := channels.CreateBucketIfNotExists([]byte(channelId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tracks, err := channel.CreateBucketIfNotExists([]byte("tracks"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tracks.Put([]byte(strconv.Itoa(mapperId)), []byte(strconv.Itoa(priority)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (db *Db) Close() {
|
||||
db.DB.Close()
|
||||
}
|
||||
|
||||
func getMapper(tx *bolt.Tx, userId int) (mapper *bolt.Bucket) {
|
||||
mappers := tx.Bucket([]byte("mapper"))
|
||||
if mappers == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
mapper = mappers.Bucket([]byte(strconv.Itoa(userId)))
|
||||
if mapper == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getMapperMut(tx *bolt.Tx, userId int) (mapper *bolt.Bucket, err error) {
|
||||
mappers, err := tx.CreateBucketIfNotExists([]byte("mapper"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mapper, err = mappers.CreateBucketIfNotExists([]byte(strconv.Itoa(userId)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
10
go.mod
Normal file
10
go.mod
Normal file
|
@ -0,0 +1,10 @@
|
|||
module subscribe-bot
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/bwmarrin/discordgo v0.22.0
|
||||
go.etcd.io/bbolt v1.3.5
|
||||
golang.org/x/sync v0.0.0-20201008141435-b3e1573b7520
|
||||
)
|
13
go.sum
Normal file
13
go.sum
Normal file
|
@ -0,0 +1,13 @@
|
|||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/bwmarrin/discordgo v0.22.0 h1:uBxY1HmlVCsW1IuaPjpCGT6A2DBwRn0nvOguQIxDdFM=
|
||||
github.com/bwmarrin/discordgo v0.22.0/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/sync v0.0.0-20201008141435-b3e1573b7520 h1:Bx6FllMpG4NWDOfhMBz1VR2QYNp/SAOHPIAsaVmxfPo=
|
||||
golang.org/x/sync v0.0.0-20201008141435-b3e1573b7520/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
63
main.go
Normal file
63
main.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var exit_chan = make(chan int)
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "config.toml", "Path to the config file (defaults to config.toml)")
|
||||
flag.Parse()
|
||||
|
||||
config, err := ReadConfig(*configPath)
|
||||
|
||||
requests := make(chan int)
|
||||
|
||||
api := NewOsuapi(&config)
|
||||
|
||||
db, err := OpenDb("db", api)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
bot, err := NewBot(config.BotToken, db, requests)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
go RunScraper(bot, db, api, requests)
|
||||
|
||||
signal_chan := make(chan os.Signal, 1)
|
||||
signal.Notify(signal_chan,
|
||||
syscall.SIGHUP,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
syscall.SIGQUIT)
|
||||
go func() {
|
||||
for {
|
||||
s := <-signal_chan
|
||||
switch s {
|
||||
case syscall.SIGHUP:
|
||||
fallthrough
|
||||
case syscall.SIGINT:
|
||||
fallthrough
|
||||
case syscall.SIGTERM:
|
||||
fallthrough
|
||||
case syscall.SIGQUIT:
|
||||
exit_chan <- 0
|
||||
default:
|
||||
exit_chan <- 1
|
||||
}
|
||||
}
|
||||
}()
|
||||
code := <-exit_chan
|
||||
|
||||
db.Close()
|
||||
bot.Close()
|
||||
os.Exit(code)
|
||||
}
|
32
models.go
Normal file
32
models.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package main
|
||||
|
||||
type Event struct {
|
||||
CreatedAt string `json:"created_at"`
|
||||
ID int `json:"id"`
|
||||
Type string `json:"type"`
|
||||
|
||||
// type: achievement
|
||||
Achievement Achievement `json:"achievement,omitempty"`
|
||||
|
||||
// type: beatmapsetApprove
|
||||
// type: beatmapsetDelete
|
||||
// type: beatmapsetRevive
|
||||
// type: beatmapsetUpdate
|
||||
// type: beatmapsetUpload
|
||||
Beatmapset Beatmapset `json:"beatmapset,omitempty"`
|
||||
|
||||
User User `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
type Achievement struct{}
|
||||
|
||||
type Beatmapset struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
URL string `json:"url"`
|
||||
PreviousUsername string `json:"previousUsername,omitempty"`
|
||||
}
|
126
osuapi.go
Normal file
126
osuapi.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/semaphore"
|
||||
)
|
||||
|
||||
const BASE_URL = "https://osu.ppy.sh/api/v2"
|
||||
|
||||
type Osuapi struct {
|
||||
lock *semaphore.Weighted
|
||||
token string
|
||||
expires time.Time
|
||||
clientId int
|
||||
clientSecret string
|
||||
}
|
||||
|
||||
func NewOsuapi(config *Config) *Osuapi {
|
||||
// want to cap at around 1000 requests a minute, OSU cap is 1200
|
||||
lock := semaphore.NewWeighted(1000)
|
||||
return &Osuapi{lock, "", time.Now(), config.ClientId, config.ClientSecret}
|
||||
}
|
||||
|
||||
func (api *Osuapi) Token() (token string, err error) {
|
||||
if time.Now().Before(api.expires) {
|
||||
token = api.token
|
||||
return
|
||||
}
|
||||
|
||||
data := fmt.Sprintf(
|
||||
"client_id=%d&client_secret=%s&grant_type=client_credentials&scope=public",
|
||||
api.clientId,
|
||||
api.clientSecret,
|
||||
)
|
||||
|
||||
resp, err := http.Post("https://osu.ppy.sh/oauth/token", "application/x-www-form-urlencoded", strings.NewReader(data))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var osuToken OsuToken
|
||||
respBody, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(respBody, &osuToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("got new access token", osuToken.AccessToken[:12]+"...")
|
||||
api.token = osuToken.AccessToken
|
||||
api.expires = time.Now().Add(time.Duration(osuToken.ExpiresIn) * time.Second)
|
||||
token = api.token
|
||||
return
|
||||
}
|
||||
|
||||
func (api *Osuapi) Request(action string, url string, result interface{}) (err error) {
|
||||
err = api.lock.Acquire(context.TODO(), 1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
apiUrl := BASE_URL + url
|
||||
req, err := http.NewRequest(action, apiUrl, nil)
|
||||
|
||||
token, err := api.Token()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Bearer "+token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// release the lock after 1 minute
|
||||
go func() {
|
||||
time.Sleep(time.Minute)
|
||||
api.lock.Release(1)
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
func (api *Osuapi) GetUserEvents(userId int, limit int, offset int) (events []Event, err error) {
|
||||
url := fmt.Sprintf(
|
||||
"/users/%d/recent_activity?limit=%d&offset=%d",
|
||||
userId,
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
err = api.Request("GET", url, &events)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type OsuToken struct {
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
120
scrape.go
Normal file
120
scrape.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
refreshInterval = 60 * time.Second
|
||||
)
|
||||
|
||||
func RunScraper(bot *Bot, db *Db, api *Osuapi, requests chan int) {
|
||||
// start timers
|
||||
go startTimers(db, requests)
|
||||
|
||||
for userId := range requests {
|
||||
log.Println("scraping", userId)
|
||||
newMaps, err := getNewMaps(db, api, userId)
|
||||
if err != nil {
|
||||
log.Println("err getting new maps:", err)
|
||||
exit_chan <- 1
|
||||
}
|
||||
|
||||
db.IterTrackingChannels(userId, func(channelId string) error {
|
||||
for _, beatmap := range newMaps {
|
||||
bot.ChannelMessageSend(channelId, fmt.Sprintf("new beatmap event [%s](%s)", beatmap.Title, beatmap.URL))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// wait a minute and put them back into the queue
|
||||
go func() {
|
||||
time.Sleep(refreshInterval)
|
||||
requests <- userId
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func getNewMaps(db *Db, api *Osuapi, userId int) (newMaps []Beatmapset, err error) {
|
||||
// see if there's a last event
|
||||
hasLastEvent, lastEventId := db.MapperLastEvent(userId)
|
||||
newMaps = make([]Beatmapset, 0)
|
||||
var (
|
||||
events []Event
|
||||
newLatestEvent = 0
|
||||
updateLatestEvent = false
|
||||
)
|
||||
if hasLastEvent {
|
||||
log.Printf("last event id for %d is %d\n", userId, lastEventId)
|
||||
offset := 0
|
||||
|
||||
loop:
|
||||
for {
|
||||
log.Println("loading user events from", offset)
|
||||
events, err = api.GetUserEvents(userId, 50, offset)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("couldn't load events for user %d, offset %d: %w", userId, offset, err)
|
||||
return
|
||||
}
|
||||
if len(events) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
if event.ID == lastEventId {
|
||||
break loop
|
||||
}
|
||||
|
||||
if event.ID > newLatestEvent {
|
||||
updateLatestEvent = true
|
||||
newLatestEvent = event.ID
|
||||
}
|
||||
|
||||
if event.Type == "beatmapsetUpload" ||
|
||||
event.Type == "beatmapsetRevive" ||
|
||||
event.Type == "beatmapsetUpdate" {
|
||||
newMaps = append(newMaps, event.Beatmapset)
|
||||
}
|
||||
}
|
||||
|
||||
offset += len(events)
|
||||
}
|
||||
} else {
|
||||
log.Printf("no last event id found for %d\n", userId)
|
||||
events, err = api.GetUserEvents(userId, 50, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
if event.ID > newLatestEvent {
|
||||
updateLatestEvent = true
|
||||
newLatestEvent = event.ID
|
||||
}
|
||||
|
||||
if event.Type == "beatmapsetUpload" ||
|
||||
event.Type == "beatmapsetRevive" ||
|
||||
event.Type == "beatmapsetUpdate" {
|
||||
newMaps = append(newMaps, event.Beatmapset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updateLatestEvent {
|
||||
err = db.UpdateMapperLatestEvent(userId, newLatestEvent)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func startTimers(db *Db, requests chan int) {
|
||||
db.IterTrackedMappers(func(userId int) error {
|
||||
requests <- userId
|
||||
return nil
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue