replace zxq.co/ripple/hanayo

This commit is contained in:
Alicia
2019-02-23 13:29:15 +00:00
commit c3d206c173
5871 changed files with 1353715 additions and 0 deletions

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

View File

@@ -0,0 +1,101 @@
package websockets
import (
"crypto/md5"
"crypto/sha256"
"encoding/json"
"fmt"
"database/sql"
"zxq.co/ripple/rippleapi/common"
)
type websocketUser struct {
ID int `json:"id"`
Username string `json:"username"`
UserPrivileges uint64 `json:"user_privileges"`
TokenPrivileges uint64 `json:"token_privileges"`
ApplicationID *string `json:"application_id"`
}
type identifyMessage struct {
Token string `json:"token"`
IsBearer bool `json:"is_bearer"`
}
// Identify sets the identity of the user.
func Identify(c *conn, message incomingMessage) {
var idMsg identifyMessage
err := json.Unmarshal(message.Data, &idMsg)
if err != nil {
c.WriteJSON(TypeInvalidMessage, err.Error())
return
}
var wsu websocketUser
if idMsg.IsBearer {
err = getBearerToken(idMsg.Token, &wsu)
} else {
err = db.Get(&wsu, `
SELECT
t.user as id, t.privileges as token_privileges,
u.username, u.privileges as user_privileges
FROM tokens t
INNER JOIN users u ON t.user = u.id
WHERE t.token = ?`, fmt.Sprintf("%x", md5.Sum([]byte(idMsg.Token))))
}
switch err {
case nil:
break
case sql.ErrNoRows:
c.WriteJSON(TypeNotFound, nil)
return
default:
common.WSErr(err)
c.WriteJSON(TypeUnexpectedError, nil)
return
}
wsu.TokenPrivileges = uint64(
common.Privileges(wsu.TokenPrivileges).CanOnly(
common.UserPrivileges(wsu.UserPrivileges),
),
)
c.Mtx.Lock()
c.User = &wsu
c.Mtx.Unlock()
c.WriteJSON(TypeIdentified, wsu)
}
func getBearerToken(token string, wsu *websocketUser) error {
var x struct {
Client string
Scope string
Extra int
}
err := db.Get(&x, "SELECT client, scope, extra FROM osin_access WHERE access_token = ? LIMIT 1", fmt.Sprintf("%x", sha256.Sum256([]byte(token))))
if err != nil {
return err
}
var userInfo struct {
Username string
Privileges uint64
}
err = db.Get(&userInfo, "SELECT username, privileges FROM users WHERE id = ? LIMIT 1", x.Extra)
if err != nil {
return err
}
wsu.ApplicationID = &x.Client
wsu.ID = x.Extra
wsu.Username = userInfo.Username
wsu.UserPrivileges = userInfo.Privileges
wsu.TokenPrivileges = uint64(common.OAuthPrivileges(x.Scope))
return nil
}

View File

@@ -0,0 +1,125 @@
package websockets
import (
"encoding/json"
"sync"
"sync/atomic"
"time"
"github.com/leavengood/websocket"
)
var stepNumber uint64
func handler(rawConn *websocket.Conn) {
defer catchPanic()
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),
false,
nil,
}
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
}
if f != nil {
f(c, i)
}
}
}
type conn struct {
Conn *websocket.Conn
Mtx sync.Mutex
ID uint64
RestrictedVisible bool
User *websocketUser
}
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,
TypeSubscribeMultiMatches: SubscribeMultiMatches,
TypeSetRestrictedVisibility: SetRestrictedVisibility,
TypeIdentify: Identify,
TypePing: pingHandler,
}
// Server Message Types
const (
TypeConnected = "connected"
TypeInvalidMessage = "invalid_message_type"
TypeUnexpectedError = "unexpected_error"
TypeNotFound = "not_found"
TypeSubscribedToScores = "subscribed_to_scores"
TypeNewScore = "new_score"
TypeSubscribedToMultiMatches = "subscribed_mp_complete_match"
TypeNewMatch = "new_completed_match"
TypeIdentified = "identified"
TypeRestrictedVisibilitySet = "restricted_visibility_set"
TypePong = "pong"
)
// Client Message Types
const (
TypeSubscribeScores = "subscribe_scores"
TypeSubscribeMultiMatches = "subscribe_mp_complete_match"
TypeIdentify = "identify"
TypeSetRestrictedVisibility = "set_restricted_visibility"
TypePing = "ping"
)
func pingHandler(c *conn, message incomingMessage) {
c.WriteJSON(TypePong, nil)
}
// 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"`
}

View File

@@ -0,0 +1,57 @@
package websockets
import (
"encoding/json"
"fmt"
"sync"
)
// SubscribeMultiMatches subscribes to receiving information from completed
// games in multiplayer matches.
func SubscribeMultiMatches(c *conn, message incomingMessage) {
multiSubscriptionsMtx.Lock()
var found bool
for _, el := range multiSubscriptions {
if el.ID == c.ID {
found = true
break
}
}
// if it was not found, we need to add it
if !found {
multiSubscriptions = append(multiSubscriptions, c)
}
multiSubscriptionsMtx.Unlock()
c.WriteJSON(TypeSubscribedToMultiMatches, nil)
}
var multiSubscriptions []*conn
var multiSubscriptionsMtx = new(sync.RWMutex)
func matchRetriever() {
ps, err := red.Subscribe("api:mp_complete_match")
if err != nil {
fmt.Println(err)
}
for {
msg, err := ps.ReceiveMessage()
if err != nil {
fmt.Println(err.Error())
return
}
go handleNewMultiGame(msg.Payload)
}
}
func handleNewMultiGame(payload string) {
defer catchPanic()
multiSubscriptionsMtx.RLock()
cp := make([]*conn, len(multiSubscriptions))
copy(cp, multiSubscriptions)
multiSubscriptionsMtx.RUnlock()
for _, el := range cp {
el.WriteJSON(TypeNewMatch, json.RawMessage(payload))
}
}

View File

@@ -0,0 +1,31 @@
package websockets
import (
"encoding/json"
"zxq.co/ripple/rippleapi/common"
)
// SetRestrictedVisibility sets whether the information of restricted users
// can be seen.
func SetRestrictedVisibility(c *conn, message incomingMessage) {
var visibility bool
err := json.Unmarshal(message.Data, &visibility)
if err != nil {
c.WriteJSON(TypeInvalidMessage, err.Error())
return
}
var userIsManager bool
if c.User != nil && (c.User.UserPrivileges&uint64(common.AdminPrivilegeManageUsers) > 0) {
userIsManager = true
}
c.Mtx.Lock()
visibility = visibility && userIsManager
c.RestrictedVisible = visibility
c.Mtx.Unlock()
c.WriteJSON(TypeRestrictedVisibilitySet, visibility)
}

View File

@@ -0,0 +1,173 @@
package websockets
import (
"encoding/json"
"fmt"
"strings"
"sync"
"gopkg.in/thehowl/go-osuapi.v1"
"zxq.co/ripple/rippleapi/app/v1"
"zxq.co/ripple/rippleapi/common"
"zxq.co/x/getrank"
)
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(TypeSubscribedToScores, ssu)
}
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 scoreUser struct {
UserID int `json:"id"`
Username string `json:"username"`
Privileges uint64 `json:"privileges"`
}
type score struct {
v1.Score
scoreUser
}
type scoreJSON struct {
v1.Score
UserID int `json:"user_id"`
User scoreUser `json:"user"`
}
func handleNewScore(id string) {
defer catchPanic()
var s score
err := db.Get(&s, `
SELECT
s.id, s.beatmap_md5, s.score, s.max_combo, s.full_combo, s.mods,
s.300_count, s.100_count, s.50_count, s.gekis_count, s.katus_count, s.misses_count,
s.time, s.play_mode, s.accuracy, s.pp, s.completed, s.userid AS user_id,
u.username, u.privileges
FROM scores s
INNER JOIN users u ON s.userid = u.id
WHERE s.id = ?`, id)
if err != nil {
fmt.Println(err)
return
}
s.Rank = strings.ToUpper(getrank.GetRank(
osuapi.Mode(s.PlayMode),
osuapi.Mods(s.Mods),
s.Accuracy,
s.Count300,
s.Count100,
s.Count50,
s.CountMiss,
))
sj := scoreJSON{
Score: s.Score,
UserID: s.UserID,
User: s.scoreUser,
}
scoreSubscriptionsMtx.RLock()
cp := make([]scoreSubscription, len(scoreSubscriptions))
copy(cp, scoreSubscriptions)
scoreSubscriptionsMtx.RUnlock()
for _, el := range cp {
if len(el.Users) > 0 && !scoreUserValid(el.Users, sj) {
continue
}
if sj.User.Privileges&3 != 3 && !el.Conn.RestrictedVisible {
continue
}
el.Conn.WriteJSON(TypeNewScore, sj)
}
}
func scoreUserValid(users []subscribeScoresUser, s scoreJSON) 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
}
func catchPanic() {
r := recover()
if r != nil {
switch r := r.(type) {
case error:
common.WSErr(r)
default:
fmt.Println("PANIC", r)
}
}
}

View File

@@ -0,0 +1,42 @@
// 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()
go matchRetriever()
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()
multiSubscriptionsMtx.Lock()
for idx, el := range multiSubscriptions {
if el.ID == connID {
multiSubscriptions[idx] = multiSubscriptions[len(multiSubscriptions)-1]
multiSubscriptions = multiSubscriptions[:len(multiSubscriptions)-1]
break
}
}
multiSubscriptionsMtx.Unlock()
}