add patches

This commit is contained in:
Michael Zhang 2020-10-14 15:58:01 -05:00
parent 301379574a
commit 32b87975d0
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
9 changed files with 271 additions and 125 deletions

View file

@ -4,8 +4,9 @@ subscribe-bot
Subscribes to OSU map updates and stores versions for later review. 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 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 you want to contribute or test the bot, instructions on how to run it are below.
run it:
Join the [Discord][2]
How to run How to run
---------- ----------
@ -37,3 +38,4 @@ License
[GPL3][1] [GPL3][1]
[1]: https://www.gnu.org/licenses/gpl-3.0.en.html [1]: https://www.gnu.org/licenses/gpl-3.0.en.html
[2]: https://discord.gg/eqjVG2H

1
go.mod
View file

@ -6,6 +6,7 @@ require (
github.com/BurntSushi/toml v0.3.1 github.com/BurntSushi/toml v0.3.1
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
github.com/bwmarrin/discordgo v0.22.0 github.com/bwmarrin/discordgo v0.22.0
github.com/dustin/go-humanize v1.0.0
github.com/foolin/goview v0.3.0 github.com/foolin/goview v0.3.0
github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e
github.com/gin-gonic/contrib v0.0.0-20201005132743-ca038bbf2944 github.com/gin-gonic/contrib v0.0.0-20201005132743-ca038bbf2944

2
go.sum
View file

@ -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.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/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/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/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 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=

67
web/auth.go Normal file
View 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
View 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())
}

View file

@ -1,51 +1,27 @@
<!DOCTYPE html> {{ define "content" }}
<html lang="en">
<head>
<title>subscribe-bot</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <p>Maps:</p>
<link rel="stylesheet" href="/static/main.css" /> <table class="table-auto">
</head> <thead>
<th>Links</th>
<th>Title</th>
<th>Mapper</th>
</thead>
<tbody>
{{ range .Beatmapsets }}
<tr>
<td>
<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/u/{{ .UserID }}" target="_blank">{{ .Creator }}</a>
</td>
</tr>
{{ end }}
</tbody>
</table>
<body> {{ end }}
<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">
<thead>
<th>Mapper</th>
<th>Title</th>
<th>Links</th>
</thead>
{{ range .Beatmapsets }}
<tbody>
<td>
<a href="https://osu.ppy.sh/u/{{ .UserID }}" target="_blank">{{ .Creator }}</a>
</td>
<td>{{ .Artist }} - {{ .Title }}</td>
<td>
<a href="https://osu.ppy.sh/s/{{ .ID }}" target="_blank">osu</a>
</td>
</tbody>
{{ end }}
</table>
</div>
</body>
</html>

View 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
View 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>

View file

@ -1,15 +1,12 @@
package web package web
import ( import (
"encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"os" "os"
"path" "path"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
@ -32,108 +29,77 @@ var (
cache = memoize.NewMemoizer(90*time.Second, 10*time.Minute) 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) { func RunWeb(config *config.Config, api *osuapi.Osuapi) {
hc := http.Client{ hc := &http.Client{
Timeout: 10 * time.Second, 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) gin.SetMode(gin.ReleaseMode)
} }
r := gin.Default() r := gin.Default()
r.Use(gin.Recovery()) r.Use(gin.Recovery())
r.Use(static.Serve("/static", static.LocalFile("web/static", false))) 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{ r.HTMLRender = ginview.New(goview.Config{
Root: "web/templates", Root: "web/templates",
DisableCache: config.Debug, Master: "master.html",
DisableCache: web.config.Debug,
}) })
r.GET("/logout", func(c *gin.Context) { r.GET("/logout", web.logout)
session := sessions.Default(c)
session.Delete("access_token")
session.Save()
c.Redirect(http.StatusTemporaryRedirect, "/") r.GET("/login", web.login)
})
r.GET("/login", func(c *gin.Context) { r.GET("/login/callback", web.loginCallback)
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", func(c *gin.Context) { r.GET("/map/:userId/:mapId/versions", web.mapVersions)
receivedCode := c.Query("code")
bodyQuery := url.Values{} r.GET("/map/:userId/:mapId/patch/:hash", web.mapPatch)
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("/", func(c *gin.Context) { r.GET("/", func(c *gin.Context) {
session := sessions.Default(c) beatmapSets := web.listRepos()
var accessToken string
loggedIn := false
accessTokenI := session.Get("access_token")
if accessTokenI != nil {
accessToken = accessTokenI.(string)
if len(accessToken) > 0 {
loggedIn = true
}
}
beatmapSets := getRepos(config, api)
// render with master
c.HTML(http.StatusOK, "index.html", gin.H{ c.HTML(http.StatusOK, "index.html", gin.H{
"LoggedIn": loggedIn, "LoggedIn": isLoggedIn(c),
"Beatmapsets": beatmapSets, "Beatmapsets": beatmapSets,
}) })
}) })
addr := fmt.Sprintf("%s:%d", config.Web.Host, config.Web.Port) addr := fmt.Sprintf("%s:%d", web.config.Web.Host, web.config.Web.Port)
r.Run(addr) r.Run(addr)
} }
func getRepos(config *config.Config, api *osuapi.Osuapi) []osuapi.Beatmapset { func isLoggedIn(c *gin.Context) bool {
session := sessions.Default(c)
var accessToken string
loggedIn := false
accessTokenI := session.Get("access_token")
if accessTokenI != nil {
accessToken = accessTokenI.(string)
if len(accessToken) > 0 {
loggedIn = true
}
}
return loggedIn
}
func (web *Web) listRepos() []osuapi.Beatmapset {
expensive := func() (interface{}, error) { expensive := func() (interface{}, error) {
repos := make([]int, 0) repos := make([]int, 0)
reposDir := config.Repos reposDir := web.config.Repos
users, _ := ioutil.ReadDir(reposDir) users, _ := ioutil.ReadDir(reposDir)
for _, user := range users { for _, user := range users {
@ -155,7 +121,7 @@ func getRepos(config *config.Config, api *osuapi.Osuapi) []osuapi.Beatmapset {
for i, repo := range repos { for i, repo := range repos {
wg.Add(1) wg.Add(1)
go func(i int, repo int) { go func(i int, repo int) {
bs, _ := api.GetBeatmapSet(repo) bs, _ := web.api.GetBeatmapSet(repo)
beatmapSets[i] = bs beatmapSets[i] = bs
wg.Done() wg.Done()
}(i, repo) }(i, repo)