add patches
This commit is contained in:
parent
301379574a
commit
32b87975d0
9 changed files with 271 additions and 125 deletions
|
@ -4,8 +4,9 @@ subscribe-bot
|
|||
Subscribes to OSU map updates and stores versions for later review.
|
||||
|
||||
Please don't run a separate bot, the official one is `subscribe-bot#8789`. If
|
||||
you want to contribute or test the bot, then here are instructions on how to
|
||||
run it:
|
||||
you want to contribute or test the bot, instructions on how to run it are below.
|
||||
|
||||
Join the [Discord][2]
|
||||
|
||||
How to run
|
||||
----------
|
||||
|
@ -37,3 +38,4 @@ License
|
|||
[GPL3][1]
|
||||
|
||||
[1]: https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||
[2]: https://discord.gg/eqjVG2H
|
||||
|
|
1
go.mod
1
go.mod
|
@ -6,6 +6,7 @@ require (
|
|||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
|
||||
github.com/bwmarrin/discordgo v0.22.0
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/foolin/goview v0.3.0
|
||||
github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e
|
||||
github.com/gin-gonic/contrib v0.0.0-20201005132743-ca038bbf2944
|
||||
|
|
2
go.sum
2
go.sum
|
@ -15,6 +15,8 @@ github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CL
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
||||
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
|
|
67
web/auth.go
Normal file
67
web/auth.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (web *Web) logout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Delete("access_token")
|
||||
session.Save()
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/")
|
||||
}
|
||||
|
||||
func (web *Web) login(c *gin.Context) {
|
||||
url := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "osu.ppy.sh",
|
||||
Path: "/oauth/authorize",
|
||||
}
|
||||
q := url.Query()
|
||||
q.Set("client_id", web.config.Oauth.ClientId)
|
||||
q.Set("redirect_uri", web.config.Web.ServedAt+"/login/callback")
|
||||
q.Set("response_type", "code")
|
||||
q.Set("scope", "identify public")
|
||||
q.Set("state", "urmom")
|
||||
url.RawQuery = q.Encode()
|
||||
fmt.Println("redirecting to", url.String())
|
||||
c.Redirect(http.StatusTemporaryRedirect, url.String())
|
||||
}
|
||||
|
||||
func (web *Web) loginCallback(c *gin.Context) {
|
||||
receivedCode := c.Query("code")
|
||||
|
||||
bodyQuery := url.Values{}
|
||||
bodyQuery.Set("client_id", web.config.Oauth.ClientId)
|
||||
bodyQuery.Set("client_secret", web.config.Oauth.ClientSecret)
|
||||
bodyQuery.Set("code", receivedCode)
|
||||
bodyQuery.Set("grant_type", "authorization_code")
|
||||
bodyQuery.Set("redirect_uri", web.config.Web.ServedAt+"/login/callback")
|
||||
body := strings.NewReader(bodyQuery.Encode())
|
||||
resp, _ := web.hc.Post("https://osu.ppy.sh/oauth/token", "application/x-www-form-urlencoded", body)
|
||||
respBody, _ := ioutil.ReadAll(resp.Body)
|
||||
type OsuToken struct {
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
var token OsuToken
|
||||
_ = json.Unmarshal(respBody, &token)
|
||||
fmt.Println("TOKEN", token)
|
||||
|
||||
session := sessions.Default(c)
|
||||
session.Set("access_token", token.AccessToken)
|
||||
session.Save()
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/")
|
||||
}
|
73
web/repo.go
Normal file
73
web/repo.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
func (web *Web) mapVersions(c *gin.Context) {
|
||||
userId := c.Param("userId")
|
||||
mapId := c.Param("mapId")
|
||||
|
||||
repoDir := path.Join(web.config.Repos, userId, mapId)
|
||||
repo, _ := git.PlainOpen(repoDir)
|
||||
|
||||
type Revision struct {
|
||||
Date time.Time
|
||||
HumanDate string
|
||||
Summary string
|
||||
Hash string
|
||||
HasParent bool
|
||||
}
|
||||
|
||||
versions := make([]Revision, 0)
|
||||
logIter, _ := repo.Log(&git.LogOptions{})
|
||||
for {
|
||||
commit, err := logIter.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
stats, _ := commit.Stats()
|
||||
_, err = commit.Parent(0)
|
||||
hasParent := !errors.Is(err, object.ErrParentNotFound)
|
||||
|
||||
versions = append(versions, Revision{
|
||||
Date: commit.Author.When,
|
||||
HumanDate: humanize.Time(commit.Author.When),
|
||||
Summary: stats.String(),
|
||||
Hash: commit.Hash.String(),
|
||||
HasParent: hasParent,
|
||||
})
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "map-version.html", gin.H{
|
||||
"LoggedIn": isLoggedIn(c),
|
||||
"Versions": versions,
|
||||
})
|
||||
}
|
||||
|
||||
func (web *Web) mapPatch(c *gin.Context) {
|
||||
userId := c.Param("userId")
|
||||
mapId := c.Param("mapId")
|
||||
hash := c.Param("hash")
|
||||
|
||||
repoDir := path.Join(web.config.Repos, userId, mapId)
|
||||
repo, _ := git.PlainOpen(repoDir)
|
||||
|
||||
hashObj := plumbing.NewHash(hash)
|
||||
commit, _ := repo.CommitObject(hashObj)
|
||||
parent, _ := commit.Parent(0)
|
||||
patch, _ := commit.Patch(parent)
|
||||
|
||||
c.String(http.StatusOK, "text/plain", patch.String())
|
||||
}
|
|
@ -1,51 +1,27 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>subscribe-bot</title>
|
||||
{{ define "content" }}
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<p>Maps:</p>
|
||||
|
||||
<link rel="stylesheet" href="/static/main.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
<a href="/">subscribe-bot</a>
|
||||
</h1>
|
||||
|
||||
<small>logging in with your osu account does nothing</small>
|
||||
|
||||
<div class="nav-bar">
|
||||
<a href="/">home</a>
|
||||
|
||||
{{ if .LoggedIn }}
|
||||
<a href="/logout">logout</a>
|
||||
{{ else }}
|
||||
<a href="/login">login</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<p>Maps:</p>
|
||||
|
||||
<table class="table-auto">
|
||||
<table class="table-auto">
|
||||
<thead>
|
||||
<th>Mapper</th>
|
||||
<th>Title</th>
|
||||
<th>Links</th>
|
||||
<th>Title</th>
|
||||
<th>Mapper</th>
|
||||
</thead>
|
||||
{{ range .Beatmapsets }}
|
||||
<tbody>
|
||||
{{ range .Beatmapsets }}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://osu.ppy.sh/u/{{ .UserID }}" target="_blank">{{ .Creator }}</a>
|
||||
<a href="https://osu.ppy.sh/s/{{ .ID }}" target="_blank">osu</a>
|
||||
<a href="/map/{{ .UserID }}/{{ .ID }}/versions">versions</a>
|
||||
</td>
|
||||
<td>{{ .Artist }} - {{ .Title }}</td>
|
||||
<td>
|
||||
<a href="https://osu.ppy.sh/s/{{ .ID }}" target="_blank">osu</a>
|
||||
<a href="https://osu.ppy.sh/u/{{ .UserID }}" target="_blank">{{ .Creator }}</a>
|
||||
</td>
|
||||
</tbody>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{ end }}
|
||||
|
|
27
web/templates/map-version.html
Normal file
27
web/templates/map-version.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
{{ define "content" }}
|
||||
|
||||
<small>up to the latest 20 revisions, pagination coming later</small>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th>Date</th>
|
||||
<th>Links</th>
|
||||
<th>Summary</th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{ range .Versions }}
|
||||
<tr>
|
||||
<td><span title="{{ .Date }}">{{ .HumanDate }}</span></td>
|
||||
<td>
|
||||
{{ if .HasParent }}
|
||||
<a href="patch/{{ .Hash }}">patch</a>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td><pre>{{ .Summary }}</pre></td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{ end }}
|
32
web/templates/master.html
Normal file
32
web/templates/master.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>subscribe-bot</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="stylesheet" href="/static/main.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
<a href="/">subscribe-bot</a>
|
||||
</h1>
|
||||
|
||||
<small>logging in with your osu account does nothing</small>
|
||||
|
||||
<div class="nav-bar">
|
||||
<a href="/">home</a>
|
||||
|
||||
{{ if .LoggedIn }}
|
||||
<a href="/logout">logout</a>
|
||||
{{ else }}
|
||||
<a href="/login">login</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{ template "content" . }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
108
web/web.go
108
web/web.go
|
@ -1,15 +1,12 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -32,80 +29,60 @@ var (
|
|||
cache = memoize.NewMemoizer(90*time.Second, 10*time.Minute)
|
||||
)
|
||||
|
||||
type Web struct {
|
||||
config *config.Config
|
||||
api *osuapi.Osuapi
|
||||
hc *http.Client
|
||||
}
|
||||
|
||||
func RunWeb(config *config.Config, api *osuapi.Osuapi) {
|
||||
hc := http.Client{
|
||||
hc := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
if !config.Debug {
|
||||
web := Web{config, api, hc}
|
||||
web.Run()
|
||||
}
|
||||
|
||||
func (web *Web) Run() {
|
||||
if !web.config.Debug {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
r := gin.Default()
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(static.Serve("/static", static.LocalFile("web/static", false)))
|
||||
r.Use(sessions.Sessions("mysession", sessions.NewCookieStore([]byte(config.Web.SessionSecret))))
|
||||
r.Use(sessions.Sessions("mysession", sessions.NewCookieStore([]byte(web.config.Web.SessionSecret))))
|
||||
|
||||
r.HTMLRender = ginview.New(goview.Config{
|
||||
Root: "web/templates",
|
||||
DisableCache: config.Debug,
|
||||
Master: "master.html",
|
||||
DisableCache: web.config.Debug,
|
||||
})
|
||||
|
||||
r.GET("/logout", func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Delete("access_token")
|
||||
session.Save()
|
||||
r.GET("/logout", web.logout)
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/")
|
||||
})
|
||||
r.GET("/login", web.login)
|
||||
|
||||
r.GET("/login", func(c *gin.Context) {
|
||||
url := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "osu.ppy.sh",
|
||||
Path: "/oauth/authorize",
|
||||
}
|
||||
q := url.Query()
|
||||
q.Set("client_id", config.Oauth.ClientId)
|
||||
q.Set("redirect_uri", config.Web.ServedAt+"/login/callback")
|
||||
q.Set("response_type", "code")
|
||||
q.Set("scope", "identify public")
|
||||
q.Set("state", "urmom")
|
||||
url.RawQuery = q.Encode()
|
||||
fmt.Println("redirecting to", url.String())
|
||||
c.Redirect(http.StatusTemporaryRedirect, url.String())
|
||||
})
|
||||
r.GET("/login/callback", web.loginCallback)
|
||||
|
||||
r.GET("/login/callback", func(c *gin.Context) {
|
||||
receivedCode := c.Query("code")
|
||||
r.GET("/map/:userId/:mapId/versions", web.mapVersions)
|
||||
|
||||
bodyQuery := url.Values{}
|
||||
bodyQuery.Set("client_id", config.Oauth.ClientId)
|
||||
bodyQuery.Set("client_secret", config.Oauth.ClientSecret)
|
||||
bodyQuery.Set("code", receivedCode)
|
||||
bodyQuery.Set("grant_type", "authorization_code")
|
||||
bodyQuery.Set("redirect_uri", config.Web.ServedAt+"/login/callback")
|
||||
body := strings.NewReader(bodyQuery.Encode())
|
||||
resp, _ := hc.Post("https://osu.ppy.sh/oauth/token", "application/x-www-form-urlencoded", body)
|
||||
respBody, _ := ioutil.ReadAll(resp.Body)
|
||||
type OsuToken struct {
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
var token OsuToken
|
||||
_ = json.Unmarshal(respBody, &token)
|
||||
fmt.Println("TOKEN", token)
|
||||
|
||||
session := sessions.Default(c)
|
||||
session.Set("access_token", token.AccessToken)
|
||||
session.Save()
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/")
|
||||
})
|
||||
r.GET("/map/:userId/:mapId/patch/:hash", web.mapPatch)
|
||||
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
beatmapSets := web.listRepos()
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"LoggedIn": isLoggedIn(c),
|
||||
"Beatmapsets": beatmapSets,
|
||||
})
|
||||
})
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", web.config.Web.Host, web.config.Web.Port)
|
||||
r.Run(addr)
|
||||
}
|
||||
|
||||
func isLoggedIn(c *gin.Context) bool {
|
||||
session := sessions.Default(c)
|
||||
var accessToken string
|
||||
loggedIn := false
|
||||
|
@ -116,24 +93,13 @@ func RunWeb(config *config.Config, api *osuapi.Osuapi) {
|
|||
loggedIn = true
|
||||
}
|
||||
}
|
||||
|
||||
beatmapSets := getRepos(config, api)
|
||||
|
||||
// render with master
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"LoggedIn": loggedIn,
|
||||
"Beatmapsets": beatmapSets,
|
||||
})
|
||||
})
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", config.Web.Host, config.Web.Port)
|
||||
r.Run(addr)
|
||||
return loggedIn
|
||||
}
|
||||
|
||||
func getRepos(config *config.Config, api *osuapi.Osuapi) []osuapi.Beatmapset {
|
||||
func (web *Web) listRepos() []osuapi.Beatmapset {
|
||||
expensive := func() (interface{}, error) {
|
||||
repos := make([]int, 0)
|
||||
reposDir := config.Repos
|
||||
reposDir := web.config.Repos
|
||||
users, _ := ioutil.ReadDir(reposDir)
|
||||
|
||||
for _, user := range users {
|
||||
|
@ -155,7 +121,7 @@ func getRepos(config *config.Config, api *osuapi.Osuapi) []osuapi.Beatmapset {
|
|||
for i, repo := range repos {
|
||||
wg.Add(1)
|
||||
go func(i int, repo int) {
|
||||
bs, _ := api.GetBeatmapSet(repo)
|
||||
bs, _ := web.api.GetBeatmapSet(repo)
|
||||
beatmapSets[i] = bs
|
||||
wg.Done()
|
||||
}(i, repo)
|
||||
|
|
Loading…
Reference in a new issue