Files
monibuca/plugin/room/index.go
T
2024-12-12 20:17:56 +08:00

203 lines
5.0 KiB
Go

package plugin_room
import (
"context"
_ "embed"
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
"sync"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
"github.com/google/uuid"
. "m7s.live/v5"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/task"
"m7s.live/v5/pkg/util"
)
// User 结构体代表一个房间用户
type User struct {
task.Task
Subscriber *Subscriber
ID string
StreamPath string
Token string `json:"-" yaml:"-"`
Room *Room `json:"-" yaml:"-"`
net.Conn `json:"-" yaml:"-"`
writeLock sync.Mutex
}
func (u *User) GetKey() string {
return u.ID
}
func (u *User) Start() (err error) {
u.Subscriber, err = u.Room.Plugin.Subscribe(u, u.StreamPath)
if err != nil {
return err
}
u.Subscriber.DataChannel = make(chan pkg.IDataFrame, 10)
return nil
}
func (u *User) Go() error {
for data := range u.Subscriber.DataChannel {
u.writeLock.Lock()
wsutil.WriteServerText(u.Conn, data.([]byte))
u.writeLock.Unlock()
}
return nil
}
func (u *User) Dispose() {
close(u.Subscriber.DataChannel)
}
func (u *User) Send(event string, data any) {
if u.Conn != nil {
u.writeLock.Lock()
defer u.writeLock.Unlock()
j, err := json.Marshal(map[string]any{"event": event, "data": data})
if err == nil {
wsutil.WriteServerText(u.Conn, j)
}
}
}
type Room struct {
*Publisher
ID string
Users task.Manager[string, *User]
}
//go:embed default.yaml
var defaultYaml DefaultYaml
type RoomPlugin struct {
Plugin
AppName string `default:"room" desc:"用于订阅房间消息的应用名(streamPath第一段)"`
Size int `default:"20" desc:"房间大小"`
Private map[string]string `desc:"私密房间" key:"房间号" value:"密码"`
Verify struct {
URL string `desc:"验证用户身份的URL"`
Method string `desc:"验证用户身份的HTTP方法"`
Header map[string]string `desc:"验证用户身份的HTTP头" key:"名称" value:"值"`
} `desc:"验证用户身份"`
Ping string `default:"ping" desc:"用于客户端与服务器保持心跳时客户端发送的特殊字符串"`
Pong string `default:"pong" desc:"用于客户端与服务器保持心跳时服务器响应的特殊字符串"`
lock sync.RWMutex
rooms util.Collection[string, *Room]
}
var _ = InstallPlugin[RoomPlugin](defaultYaml)
func (rc *RoomPlugin) OnPublish(p *Publisher) {
args := p.Args
token := args.Get("token")
ss := strings.Split(token, ":")
if len(ss) != 3 {
return
}
roomId := ss[0]
userId := ss[1]
if roomId != "" && rc.rooms.Has(roomId) {
room, _ := rc.rooms.Get(roomId)
if user, ok := room.Users.Get(userId); ok {
if user.Token == token {
user.StreamPath = p.StreamPath
data, _ := json.Marshal(map[string]any{
"event": "publish",
"data": p.StreamPath,
"userId": user.ID,
})
room.WriteData(data)
}
}
}
}
func (rc *RoomPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ss := strings.Split(r.URL.Path, "/")[1:]
var roomId, userId, token string
if len(ss) == 2 {
roomId = ss[0]
userId = ss[1]
} else {
http.Error(w, "invalid url", http.StatusBadRequest)
return
}
if rc.Verify.URL != "" {
req, _ := http.NewRequest(rc.Verify.Method, rc.Verify.URL, nil)
req.Header = r.Header
res, _ := http.DefaultClient.Do(req)
if res.StatusCode != 200 {
http.Error(w, "verify failed", http.StatusForbidden)
}
}
if rc.Private != nil {
rc.lock.RLock()
pass, ok := rc.Private[roomId]
rc.lock.Unlock()
if ok {
if pass != r.URL.Query().Get("password") {
http.Error(w, "password wrong", http.StatusForbidden)
return
}
}
}
var room *Room
var ok bool
if room, ok = rc.rooms.Get(roomId); !ok {
room = &Room{ID: roomId}
if _, err := rc.Publish(context.Background(), rc.AppName+"/"+roomId); err == nil {
rc.rooms.Add(room)
} else {
http.Error(w, "room already exist", http.StatusBadRequest)
return
}
}
if room.Users.Has(userId) {
http.Error(w, "user exist", http.StatusBadRequest)
return
}
conn, _, _, err := ws.UpgradeHTTP(r, w)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer conn.Close()
token = fmt.Sprintf("%s:%s:%s", roomId, userId, uuid.NewString())
user := &User{Room: room, Conn: conn, Token: token, ID: userId}
data, _ := json.Marshal(map[string]any{"event": "userjoin", "data": user})
room.WriteData(data)
room.Users.Add(user)
user.Send("joined", map[string]any{"token": token, "userList": room.Users.Items})
defer func() {
user.Stop(err)
if room.Users.Length == 0 {
room.Stop(err)
rc.rooms.RemoveByKey(roomId)
}
}()
var msg []byte
var op ws.OpCode
for {
msg, op, err = wsutil.ReadClientData(conn)
if string(msg) == rc.Ping {
wsutil.WriteServerText(conn, []byte(rc.Pong))
} else {
data, _ := json.Marshal(map[string]any{"event": "msg", "data": string(msg), "userId": userId})
room.WriteData(data)
}
if op == ws.OpClose || err != nil {
data, _ := json.Marshal(map[string]any{"event": "userleave", "userId": userId, "data": user})
room.WriteData(data)
return
}
}
}