Add initial websocket implementation
This commit is contained in:
@@ -28,6 +28,9 @@ func (r router) Peppy(path string, a func(c *fasthttp.RequestCtx, db *sqlx.DB))
|
||||
func (r router) GET(path string, handle fasthttp.RequestHandler) {
|
||||
r.r.GET(path, wrap(handle))
|
||||
}
|
||||
func (r router) PlainGET(path string, handle fasthttp.RequestHandler) {
|
||||
r.r.GET(path, handle)
|
||||
}
|
||||
|
||||
const (
|
||||
// \x1b is escape code for ESC
|
||||
|
22
app/start.go
22
app/start.go
@@ -7,11 +7,11 @@ import (
|
||||
fhr "github.com/buaazp/fasthttprouter"
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/serenize/snaker"
|
||||
"gopkg.in/redis.v5"
|
||||
"zxq.co/ripple/rippleapi/app/internals"
|
||||
"zxq.co/ripple/rippleapi/app/peppy"
|
||||
"zxq.co/ripple/rippleapi/app/v1"
|
||||
"zxq.co/ripple/rippleapi/app/websockets"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
@@ -22,23 +22,11 @@ var (
|
||||
red *redis.Client
|
||||
)
|
||||
|
||||
var commonClusterfucks = map[string]string{
|
||||
"RegisteredOn": "register_datetime",
|
||||
"UsernameAKA": "username_aka",
|
||||
}
|
||||
|
||||
// Start begins taking HTTP connections.
|
||||
func Start(conf common.Conf, dbO *sqlx.DB) *fhr.Router {
|
||||
db = dbO
|
||||
cf = conf
|
||||
|
||||
db.MapperFunc(func(s string) string {
|
||||
if x, ok := commonClusterfucks[s]; ok {
|
||||
return x
|
||||
}
|
||||
return snaker.CamelToSnake(s)
|
||||
})
|
||||
|
||||
rawRouter := fhr.New()
|
||||
r := router{rawRouter}
|
||||
// TODO: add back gzip
|
||||
@@ -78,6 +66,9 @@ func Start(conf common.Conf, dbO *sqlx.DB) *fhr.Router {
|
||||
// token updater
|
||||
go tokenUpdater(db)
|
||||
|
||||
// start websocket
|
||||
websockets.Start(red, db)
|
||||
|
||||
// peppyapi
|
||||
{
|
||||
r.Peppy("/api/get_user", peppy.GetUser)
|
||||
@@ -152,6 +143,11 @@ func Start(conf common.Conf, dbO *sqlx.DB) *fhr.Router {
|
||||
common.PrivilegeManageUser, common.PrivilegeAPIMeta)
|
||||
}
|
||||
|
||||
// Websocket API
|
||||
{
|
||||
r.PlainGET("/api/v1/ws", websockets.WebsocketV1Entry)
|
||||
}
|
||||
|
||||
// in the new osu-web, the old endpoints are also in /v1 it seems. So /shrug
|
||||
{
|
||||
r.Peppy("/api/v1/get_user", peppy.GetUser)
|
||||
|
@@ -8,7 +8,8 @@ import (
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type score struct {
|
||||
// Score is a score done on Ripple.
|
||||
type Score struct {
|
||||
ID int `json:"id"`
|
||||
BeatmapMD5 string `json:"beatmap_md5"`
|
||||
Score int64 `json:"score"`
|
||||
@@ -31,7 +32,7 @@ type score struct {
|
||||
// beatmapScore is to differentiate from userScore, as beatmapScore contains
|
||||
// also an user, while userScore contains the beatmap.
|
||||
type beatmapScore struct {
|
||||
score
|
||||
Score
|
||||
User userData `json:"user"`
|
||||
}
|
||||
|
||||
|
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
type userScore struct {
|
||||
score
|
||||
Score
|
||||
Beatmap beatmap `json:"beatmap"`
|
||||
}
|
||||
|
||||
|
18
app/websockets/entry.go
Normal file
18
app/websockets/entry.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"github.com/leavengood/websocket"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
var upgrader = websocket.FastHTTPUpgrader{
|
||||
Handler: handler,
|
||||
CheckOrigin: func(ctx *fasthttp.RequestCtx) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// WebsocketV1Entry upgrades a connection to a websocket.
|
||||
func WebsocketV1Entry(ctx *fasthttp.RequestCtx) {
|
||||
upgrader.UpgradeHandler(ctx)
|
||||
}
|
99
app/websockets/main_handler.go
Normal file
99
app/websockets/main_handler.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/leavengood/websocket"
|
||||
)
|
||||
|
||||
var stepNumber uint64
|
||||
|
||||
func handler(rawConn *websocket.Conn) {
|
||||
defer rawConn.Close()
|
||||
|
||||
step := atomic.AddUint64(&stepNumber, 1)
|
||||
|
||||
// 5 is a security margin in case
|
||||
if step == (1<<10 - 5) {
|
||||
atomic.StoreUint64(&stepNumber, 0)
|
||||
}
|
||||
|
||||
c := &conn{
|
||||
rawConn,
|
||||
sync.Mutex{},
|
||||
step | uint64(time.Now().UnixNano()<<10),
|
||||
}
|
||||
|
||||
c.WriteJSON(TypeConnected, nil)
|
||||
|
||||
defer cleanup(c.ID)
|
||||
|
||||
for {
|
||||
var i incomingMessage
|
||||
err := c.Conn.ReadJSON(&i)
|
||||
if _, ok := err.(*websocket.CloseError); ok {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.WriteJSON(TypeInvalidMessage, err.Error())
|
||||
continue
|
||||
}
|
||||
f, ok := messageHandler[i.Type]
|
||||
if !ok {
|
||||
c.WriteJSON(TypeInvalidMessage, "invalid message type")
|
||||
continue
|
||||
}
|
||||
f(c, i)
|
||||
}
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
Conn *websocket.Conn
|
||||
Mtx sync.Mutex
|
||||
ID uint64
|
||||
}
|
||||
|
||||
func (c *conn) WriteJSON(t string, data interface{}) error {
|
||||
c.Mtx.Lock()
|
||||
err := c.Conn.WriteJSON(newMessage(t, data))
|
||||
c.Mtx.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
var messageHandler = map[string]func(c *conn, message incomingMessage){
|
||||
TypeSubscribeScores: SubscribeScores,
|
||||
}
|
||||
|
||||
// Server Message Types
|
||||
const (
|
||||
TypeConnected = "connected"
|
||||
TypeInvalidMessage = "invalid_message_type"
|
||||
TypeSubscribed = "subscribed"
|
||||
TypeNewScore = "new_score"
|
||||
)
|
||||
|
||||
// Client Message Types
|
||||
const (
|
||||
TypeSubscribeScores = "subscribe_scores"
|
||||
)
|
||||
|
||||
// Message is the wrapped information for a message sent to the client.
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func newMessage(t string, data interface{}) Message {
|
||||
return Message{
|
||||
Type: t,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
type incomingMessage struct {
|
||||
Type string `json:"type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
121
app/websockets/scores.go
Normal file
121
app/websockets/scores.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"zxq.co/ripple/rippleapi/app/v1"
|
||||
)
|
||||
|
||||
type subscribeScoresUser struct {
|
||||
User int `json:"user"`
|
||||
Modes []int `json:"modes"`
|
||||
}
|
||||
|
||||
// SubscribeScores subscribes a connection to score updates.
|
||||
func SubscribeScores(c *conn, message incomingMessage) {
|
||||
var ssu []subscribeScoresUser
|
||||
err := json.Unmarshal(message.Data, &ssu)
|
||||
if err != nil {
|
||||
c.WriteJSON(TypeInvalidMessage, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
scoreSubscriptionsMtx.Lock()
|
||||
|
||||
var found bool
|
||||
for idx, el := range scoreSubscriptions {
|
||||
// already exists, change the users
|
||||
if el.Conn.ID == c.ID {
|
||||
found = true
|
||||
scoreSubscriptions[idx].Users = ssu
|
||||
}
|
||||
}
|
||||
|
||||
// if it was not found, we need to add it
|
||||
if !found {
|
||||
scoreSubscriptions = append(scoreSubscriptions, scoreSubscription{c, ssu})
|
||||
}
|
||||
|
||||
scoreSubscriptionsMtx.Unlock()
|
||||
|
||||
c.WriteJSON(TypeSubscribed, message)
|
||||
}
|
||||
|
||||
type scoreSubscription struct {
|
||||
Conn *conn
|
||||
Users []subscribeScoresUser
|
||||
}
|
||||
|
||||
var scoreSubscriptions []scoreSubscription
|
||||
var scoreSubscriptionsMtx = new(sync.RWMutex)
|
||||
|
||||
func scoreRetriever() {
|
||||
ps, err := red.Subscribe("api:score_submission")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
for {
|
||||
msg, err := ps.ReceiveMessage()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
go handleNewScore(msg.Payload)
|
||||
}
|
||||
}
|
||||
|
||||
type score struct {
|
||||
v1.Score
|
||||
UserID int `json:"user_id"`
|
||||
}
|
||||
|
||||
func handleNewScore(id string) {
|
||||
var s score
|
||||
err := db.Get(&s, `
|
||||
SELECT
|
||||
id, beatmap_md5, score, max_combo, full_combo, mods,
|
||||
300_count, 100_count, 50_count, gekis_count, katus_count, misses_count,
|
||||
time, play_mode, accuracy, pp, completed, userid AS user_id
|
||||
FROM scores WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
scoreSubscriptionsMtx.RLock()
|
||||
cp := make([]scoreSubscription, len(scoreSubscriptions))
|
||||
copy(cp, scoreSubscriptions)
|
||||
scoreSubscriptionsMtx.RUnlock()
|
||||
|
||||
for _, el := range cp {
|
||||
if len(el.Users) > 0 && !scoreUserValid(el.Users, s) {
|
||||
continue
|
||||
}
|
||||
|
||||
el.Conn.WriteJSON(TypeNewScore, s)
|
||||
}
|
||||
}
|
||||
|
||||
func scoreUserValid(users []subscribeScoresUser, s score) bool {
|
||||
for _, u := range users {
|
||||
if u.User == s.UserID {
|
||||
if len(u.Modes) > 0 {
|
||||
if !inModes(u.Modes, s.PlayMode) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func inModes(modes []int, i int) bool {
|
||||
for _, m := range modes {
|
||||
if m == i {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
32
app/websockets/websockets.go
Normal file
32
app/websockets/websockets.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Package websockets implements functionality related to the API websockets.
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gopkg.in/redis.v5"
|
||||
)
|
||||
|
||||
var (
|
||||
red *redis.Client
|
||||
db *sqlx.DB
|
||||
)
|
||||
|
||||
// Start begins websocket functionality
|
||||
func Start(r *redis.Client, _db *sqlx.DB) error {
|
||||
red = r
|
||||
db = _db
|
||||
go scoreRetriever()
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanup(connID uint64) {
|
||||
scoreSubscriptionsMtx.Lock()
|
||||
for idx, el := range scoreSubscriptions {
|
||||
if el.Conn.ID == connID {
|
||||
scoreSubscriptions[idx] = scoreSubscriptions[len(scoreSubscriptions)-1]
|
||||
scoreSubscriptions = scoreSubscriptions[:len(scoreSubscriptions)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
scoreSubscriptionsMtx.Unlock()
|
||||
}
|
Reference in New Issue
Block a user