| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693 |
- package main
- /*
- MeetRoom - Video conferencing backend endpoints
- author: tobychui / AI assisted
- HTTP + WebSocket wiring for the MeetRoom WebApp (web/MeetRoom). Room
- state lives in mod/meetroom; this file exposes it to logged-in users:
- POST /system/meetroom/create title=&password= --> {"roomid":"...","title":"..."}
- POST /system/meetroom/join roomid=&password= --> room info (pre-flight check)
- GET /system/meetroom/info?roomid=XXXXXXXXX --> {"exists":..,"protected":..}
- GET /system/meetroom/ws?roomid=&password= --> WebSocket signaling upgrade
- POST /system/meetroom/upload multipart (file) --> {"fileid":...}
- POST /system/meetroom/attachfile roomid=&password=&path= --> {"fileid":...}
- GET /system/meetroom/download?roomid=&password=&fileid=[&inline=1]
- GET /system/meetroom/end?roomid= --> host ends the meeting
- GET /system/meetroom/iceservers --> WebRTC ICE config
- Signaling protocol (JSON frames over the WebSocket):
- client -> server: {"type":"signal","to":peerid,"data":{...}} SDP/ICE relay
- {"type":"chat","text":"..."}
- {"type":"file","fileid":"..."} announce uploaded file
- {"type":"state","audio":b,"video":b,"screen":b,"hand":b}
- {"type":"attendance"} request the join/leave log
- {"type":"kick","to":peerid} host only
- {"type":"ping"} app-level heartbeat
- {"type":"end"} host only
- server -> client: {"type":"welcome",...}, {"type":"peer-join",...},
- {"type":"peer-leave",...,"kicked":b}, {"type":"signal","from":..},
- {"type":"chat",...}, {"type":"file",...},
- {"type":"state",...}, {"type":"attendance",...},
- {"type":"kicked"}, {"type":"pong"}, {"type":"room-closed"}
- Shared space integration: every room owns a mod/sharedspace space (its ID
- travels with the room). Chat and uploads mirror into the space with
- origin meetroom.OriginMeetRoom; items posted into the space by AGI
- scripts flow back through mrSpaceItemBroadcast as chat / file frames
- with peer ID 0 (a reserved ID no real participant ever gets).
- Liveness: the server sends a WebSocket protocol ping every
- mrPingInterval and enforces mrReadTimeout as a read deadline (refreshed
- by pongs and by any incoming frame), so a silently dead client is
- removed from the room after at most mrReadTimeout. The client keeps its
- own app-level ping/pong heartbeat and auto-reconnects with backoff when
- the socket drops (see web/MeetRoom/app.js).
- Media never touches the server: clients negotiate WebRTC peer-to-peer
- connections through this relay, using the same STUN/TURN configuration
- as Arozcast screen share (see cast.go acBuildICEConfig).
- */
- import (
- "encoding/json"
- "mime"
- "net/http"
- "net/url"
- "os"
- "path"
- "strings"
- "time"
- "github.com/gorilla/websocket"
- "imuslab.com/arozos/mod/meetroom"
- prout "imuslab.com/arozos/mod/prouter"
- "imuslab.com/arozos/mod/sharedspace"
- "imuslab.com/arozos/mod/utils"
- )
- const (
- mrMaxChatLength = 4000 // runes per chat message
- mrMaxSocketFrame = 512 << 10 // 512KB per signaling frame (SDP blobs included)
- mrSweepInterval = 30 * time.Second // idle room sweep cadence
- mrPingInterval = 20 * time.Second // server keepalive ping cadence on signaling sockets
- mrReadTimeout = 60 * time.Second // drop a participant whose socket goes silent this long
- mrWriteTimeout = 10 * time.Second // per-frame write deadline on signaling sockets
- )
- var (
- meetRoomManager *meetroom.Manager
- meetRoomUpgrader = websocket.Upgrader{
- ReadBufferSize: 4096,
- WriteBufferSize: 4096,
- CheckOrigin: func(r *http.Request) bool { return true },
- }
- )
- // mrPeerInfo is the participant descriptor shared with clients.
- type mrPeerInfo struct {
- PeerID int `json:"peerid"`
- Username string `json:"username"`
- IsHost bool `json:"isHost"`
- }
- // mrRoomInfo is the room descriptor shared with clients.
- type mrRoomInfo struct {
- ID string `json:"id"`
- DisplayID string `json:"displayid"`
- Title string `json:"title"`
- Host string `json:"host"`
- Protected bool `json:"protected"`
- CreatedAt int64 `json:"createdat"` // unix seconds; lets the client show the meeting duration
- }
- func mrDescribeRoom(room *meetroom.Room) mrRoomInfo {
- return mrRoomInfo{
- ID: room.ID,
- DisplayID: meetroom.FormatRoomID(room.ID),
- Title: room.Title,
- Host: room.Host,
- Protected: room.HasPassword(),
- CreatedAt: room.CreatedAt.Unix(),
- }
- }
- // mrMarshalOrDrop marshals v, returning nil when marshalling fails (the
- // frame is simply not sent - all inputs are server-constructed).
- func mrMarshalOrDrop(v interface{}) []byte {
- js, err := json.Marshal(v)
- if err != nil {
- return nil
- }
- return js
- }
- // mrEndMeeting broadcasts room-closed and tears the room down.
- func mrEndMeeting(roomID string) {
- room, ok := meetRoomManager.GetRoom(roomID)
- if !ok {
- return
- }
- room.Broadcast([]byte(`{"type":"room-closed"}`), -1)
- meetRoomManager.CloseRoom(roomID)
- }
- // mrAttendanceList renders a room's join/leave log for the client. leftat is
- // 0 while the participant is still connected.
- func mrAttendanceList(room *meetroom.Room) []map[string]interface{} {
- records := room.Attendance()
- list := make([]map[string]interface{}, 0, len(records))
- for _, record := range records {
- entry := map[string]interface{}{
- "username": record.Username,
- "peerid": record.PeerID, // lets the host target this attendee for a kick
- "joinedat": record.JoinedAt.Unix(),
- "present": record.Present(),
- "leftat": int64(0),
- }
- if !record.Present() {
- entry["leftat"] = record.LeftAt.Unix()
- }
- list = append(list, entry)
- }
- return list
- }
- // mrSpaceItemBroadcast pushes items posted into a room's shared space from
- // outside the room (e.g. by AGI scripts) into the meeting as live chat /
- // file frames. Peer ID 0 marks server-side senders: real participants are
- // numbered from 1.
- func mrSpaceItemBroadcast(room *meetroom.Room, item *sharedspace.Item) {
- if item.Type == sharedspace.ItemTypeText {
- room.Broadcast(mrMarshalOrDrop(map[string]interface{}{
- "type": "chat",
- "from": 0,
- "username": item.Uploader,
- "text": item.Text,
- "time": item.CreatedAt.Unix(),
- }), -1)
- return
- }
- room.Broadcast(mrMarshalOrDrop(map[string]interface{}{
- "type": "file",
- "from": 0,
- "username": item.Uploader,
- "fileid": item.ID,
- "name": item.Name,
- "size": item.Size,
- "time": item.CreatedAt.Unix(),
- }), -1)
- }
- // MeetRoomInit wires up the MeetRoom video conferencing endpoints.
- func MeetRoomInit() {
- meetRoomManager = meetroom.NewManager("")
- //Give every room a shared space (chat + files mirror into it, AGI
- //scripts post into it) and bridge external posts back into the meeting.
- if sharedSpaceManager != nil {
- meetRoomManager.BindSpaceManager(sharedSpaceManager)
- meetRoomManager.SetSpaceItemHandler(mrSpaceItemBroadcast)
- }
- //Sweep abandoned rooms so forgotten meetings do not accumulate
- go func() {
- ticker := time.NewTicker(mrSweepInterval)
- defer ticker.Stop()
- for range ticker.C {
- meetRoomManager.SweepIdleRooms(meetroom.DefaultEmptyIdle)
- }
- }()
- router := prout.NewModuleRouter(prout.RouterOption{
- ModuleName: "MeetRoom",
- AdminOnly: false,
- UserHandler: userHandler,
- DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
- errorHandlePermissionDenied(w, r)
- },
- })
- //Create a new meeting room; the creator becomes the host.
- router.HandleFunc("/system/meetroom/create", func(w http.ResponseWriter, r *http.Request) {
- userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
- if err != nil {
- utils.SendErrorResponse(w, "Not logged in")
- return
- }
- title, _ := utils.PostPara(r, "title")
- password, _ := utils.PostPara(r, "password")
- room := meetRoomManager.CreateRoom(userinfo.Username, title, password)
- js := mrMarshalOrDrop(map[string]interface{}{
- "roomid": room.ID,
- "displayid": meetroom.FormatRoomID(room.ID),
- "title": room.Title,
- })
- utils.SendJSONResponse(w, string(js))
- })
- //Pre-flight join check: validates room ID + password before the client
- //acquires media devices and opens the signaling socket.
- router.HandleFunc("/system/meetroom/join", func(w http.ResponseWriter, r *http.Request) {
- roomID, err := utils.PostPara(r, "roomid")
- if err != nil {
- utils.SendErrorResponse(w, "Missing room ID")
- return
- }
- password, _ := utils.PostPara(r, "password")
- room, err := meetRoomManager.ValidateJoin(meetroom.NormalizeRoomID(roomID), password)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
- js := mrMarshalOrDrop(map[string]interface{}{
- "room": mrDescribeRoom(room),
- "participants": room.ParticipantCount(),
- })
- utils.SendJSONResponse(w, string(js))
- })
- //Room existence probe for the lobby (no password required; reveals only
- //whether the room exists and needs a password).
- router.HandleFunc("/system/meetroom/info", func(w http.ResponseWriter, r *http.Request) {
- roomID, err := utils.GetPara(r, "roomid")
- if err != nil {
- utils.SendErrorResponse(w, "Missing room ID")
- return
- }
- room, ok := meetRoomManager.GetRoom(meetroom.NormalizeRoomID(roomID))
- if !ok {
- utils.SendJSONResponse(w, `{"exists":false}`)
- return
- }
- js := mrMarshalOrDrop(map[string]interface{}{
- "exists": true,
- "protected": room.HasPassword(),
- "title": room.Title,
- "participants": room.ParticipantCount(),
- })
- utils.SendJSONResponse(w, string(js))
- })
- //Host ends the meeting for everyone.
- router.HandleFunc("/system/meetroom/end", func(w http.ResponseWriter, r *http.Request) {
- userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
- if err != nil {
- utils.SendErrorResponse(w, "Not logged in")
- return
- }
- roomID, err := utils.GetPara(r, "roomid")
- if err != nil {
- utils.SendErrorResponse(w, "Missing room ID")
- return
- }
- room, ok := meetRoomManager.GetRoom(meetroom.NormalizeRoomID(roomID))
- if !ok {
- utils.SendErrorResponse(w, "Room not found")
- return
- }
- if room.Host != userinfo.Username {
- utils.SendErrorResponse(w, "Only the host can end the meeting")
- return
- }
- mrEndMeeting(room.ID)
- utils.SendOK(w)
- })
- //ICE servers for the WebRTC mesh - shares Arozcast's STUN/TURN setup so
- //the built-in TURN relay (System Settings > Screen Share Relay) also
- //carries MeetRoom calls across NAT.
- router.HandleFunc("/system/meetroom/iceservers", func(w http.ResponseWriter, r *http.Request) {
- identity := ""
- if userinfo, err := userHandler.GetUserInfoFromRequest(w, r); err == nil {
- identity = userinfo.Username
- }
- js := mrMarshalOrDrop(acBuildICEConfig(r, identity))
- if js == nil {
- utils.SendErrorResponse(w, "Failed to build ICE config")
- return
- }
- utils.SendJSONResponse(w, string(js))
- })
- //Attachment upload: multipart form with roomid, password and file.
- router.HandleFunc("/system/meetroom/upload", func(w http.ResponseWriter, r *http.Request) {
- userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
- if err != nil {
- utils.SendErrorResponse(w, "Not logged in")
- return
- }
- if err := r.ParseMultipartForm(32 << 20); err != nil {
- utils.SendErrorResponse(w, "Invalid upload")
- return
- }
- roomID := meetroom.NormalizeRoomID(r.FormValue("roomid"))
- password := r.FormValue("password")
- if _, err := meetRoomManager.ValidateJoin(roomID, password); err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
- file, header, err := r.FormFile("file")
- if err != nil {
- utils.SendErrorResponse(w, "Missing file")
- return
- }
- defer file.Close()
- attachment, err := meetRoomManager.SaveAttachment(roomID, header.Filename, userinfo.Username, file, meetroom.DefaultMaxUpload)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
- js := mrMarshalOrDrop(map[string]interface{}{
- "fileid": attachment.ID,
- "name": attachment.Name,
- "size": attachment.Size,
- })
- utils.SendJSONResponse(w, string(js))
- })
- //Attach a file the user already owns in their ArozOS storage, addressed by
- //virtual path (e.g. user:/Desktop/report.pdf), without a round-trip
- //download+reupload. The file is streamed straight from the user's file
- //system into the room's attachment store. Access is inherently scoped:
- //GetFileSystemHandlerFromVirtualPath only resolves storages the user can
- //reach, and the real path is translated for that user.
- router.HandleFunc("/system/meetroom/attachfile", func(w http.ResponseWriter, r *http.Request) {
- userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
- if err != nil {
- utils.SendErrorResponse(w, "Not logged in")
- return
- }
- roomID := meetroom.NormalizeRoomID(r.FormValue("roomid"))
- password := r.FormValue("password")
- if _, err := meetRoomManager.ValidateJoin(roomID, password); err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
- vpath, err := utils.PostPara(r, "path")
- if err != nil {
- utils.SendErrorResponse(w, "Missing file path")
- return
- }
- //Resolve the virtual path within the requesting user's file system
- fsh, err := userinfo.GetFileSystemHandlerFromVirtualPath(vpath)
- if err != nil {
- utils.SendErrorResponse(w, "File not accessible")
- return
- }
- fshAbs := fsh.FileSystemAbstraction
- rpath, err := fshAbs.VirtualPathToRealPath(vpath, userinfo.Username)
- if err != nil {
- utils.SendErrorResponse(w, "Invalid file path")
- return
- }
- fileStat, err := fshAbs.Stat(rpath)
- if err != nil {
- utils.SendErrorResponse(w, "File not found")
- return
- }
- if fileStat.IsDir() {
- utils.SendErrorResponse(w, "Cannot attach a folder")
- return
- }
- stream, err := fshAbs.ReadStream(rpath)
- if err != nil {
- utils.SendErrorResponse(w, "Could not open the file")
- return
- }
- defer stream.Close()
- attachment, err := meetRoomManager.SaveAttachment(roomID, path.Base(vpath), userinfo.Username, stream, meetroom.DefaultMaxUpload)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
- js := mrMarshalOrDrop(map[string]interface{}{
- "fileid": attachment.ID,
- "name": attachment.Name,
- "size": attachment.Size,
- })
- utils.SendJSONResponse(w, string(js))
- })
- //Attachment download for room members.
- router.HandleFunc("/system/meetroom/download", func(w http.ResponseWriter, r *http.Request) {
- roomID := meetroom.NormalizeRoomID(r.URL.Query().Get("roomid"))
- password := r.URL.Query().Get("password")
- fileID, err := utils.GetPara(r, "fileid")
- if err != nil {
- utils.SendErrorResponse(w, "Missing file ID")
- return
- }
- if _, err := meetRoomManager.ValidateJoin(roomID, password); err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
- attachment, ok := meetRoomManager.GetAttachment(roomID, fileID)
- if !ok {
- http.NotFound(w, r)
- return
- }
- f, err := os.Open(attachment.DiskPath)
- if err != nil {
- http.NotFound(w, r)
- return
- }
- defer f.Close()
- //Serve with the original name; the ASCII fallback strips anything
- //that could break the header, the RFC 5987 form keeps unicode names.
- fallback := strings.Map(func(c rune) rune {
- if c < 32 || c == '"' || c == '\\' || c > 126 {
- return '_'
- }
- return c
- }, attachment.Name)
- //inline=1 lets the chat render images directly in the browser. Only
- //raster images may be served inline: anything else (notably SVG,
- //which can carry scripts) keeps the attachment disposition.
- disposition := "attachment"
- if r.URL.Query().Get("inline") == "1" && sharedspace.IsImageName(attachment.Name) {
- disposition = "inline"
- }
- w.Header().Set("Content-Disposition", disposition+"; filename=\""+fallback+"\"; filename*=UTF-8''"+url.PathEscape(attachment.Name))
- w.Header().Set("X-Content-Type-Options", "nosniff")
- if ctype := mime.TypeByExtension(strings.ToLower(filepathExt(attachment.Name))); ctype != "" {
- w.Header().Set("Content-Type", ctype)
- } else {
- w.Header().Set("Content-Type", "application/octet-stream")
- }
- http.ServeContent(w, r, "", time.Now(), f)
- })
- //WebSocket signaling relay.
- router.HandleFunc("/system/meetroom/ws", func(w http.ResponseWriter, r *http.Request) {
- userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
- if err != nil {
- http.Error(w, "Not logged in", http.StatusUnauthorized)
- return
- }
- roomID := meetroom.NormalizeRoomID(r.URL.Query().Get("roomid"))
- password := r.URL.Query().Get("password")
- room, err := meetRoomManager.ValidateJoin(roomID, password)
- if err != nil {
- http.Error(w, err.Error(), http.StatusForbidden)
- return
- }
- conn, err := meetRoomUpgrader.Upgrade(w, r, nil)
- if err != nil {
- return
- }
- conn.SetReadLimit(mrMaxSocketFrame)
- //Liveness: a client that goes silent (no frames and no pong
- //replies) past mrReadTimeout is dropped so it does not linger as a
- //ghost participant after a network failure.
- conn.SetReadDeadline(time.Now().Add(mrReadTimeout))
- conn.SetPongHandler(func(string) error {
- conn.SetReadDeadline(time.Now().Add(mrReadTimeout))
- return nil
- })
- participant, err := room.AddParticipant(userinfo.Username)
- if err != nil {
- conn.Close()
- return
- }
- //Writer: drain the send channel until it is closed, interleaving
- //keepalive pings, then hang up.
- go func() {
- pinger := time.NewTicker(mrPingInterval)
- defer pinger.Stop()
- defer conn.Close()
- for {
- select {
- case msg, ok := <-participant.Send:
- if !ok {
- return
- }
- conn.SetWriteDeadline(time.Now().Add(mrWriteTimeout))
- if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
- return
- }
- case <-pinger.C:
- conn.SetWriteDeadline(time.Now().Add(mrWriteTimeout))
- if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
- return
- }
- }
- }
- }()
- //Welcome frame: own identity, room descriptor and current peers.
- peers := []mrPeerInfo{}
- for _, p := range room.Participants() {
- if p.PeerID == participant.PeerID {
- continue
- }
- peers = append(peers, mrPeerInfo{PeerID: p.PeerID, Username: p.Username, IsHost: p.IsHost})
- }
- room.SendTo(participant.PeerID, mrMarshalOrDrop(map[string]interface{}{
- "type": "welcome",
- "peerid": participant.PeerID,
- "username": participant.Username,
- "isHost": participant.IsHost,
- "room": mrDescribeRoom(room),
- "peers": peers,
- }))
- //Announce the newcomer to everyone else.
- room.Broadcast(mrMarshalOrDrop(map[string]interface{}{
- "type": "peer-join",
- "peer": mrPeerInfo{PeerID: participant.PeerID, Username: participant.Username, IsHost: participant.IsHost},
- }), participant.PeerID)
- defer func() {
- //The participant may already be gone if the room was closed.
- if _, stillHere := room.GetParticipant(participant.PeerID); stillHere {
- room.RemoveParticipant(participant.PeerID)
- room.Broadcast(mrMarshalOrDrop(map[string]interface{}{
- "type": "peer-leave",
- "peerid": participant.PeerID,
- "username": participant.Username,
- }), -1)
- }
- participant.CloseSend()
- }()
- for {
- _, raw, err := conn.ReadMessage()
- if err != nil {
- return
- }
- conn.SetReadDeadline(time.Now().Add(mrReadTimeout))
- var frame struct {
- Type string `json:"type"`
- To int `json:"to"`
- Data json.RawMessage `json:"data"`
- Text string `json:"text"`
- FileID string `json:"fileid"`
- Audio bool `json:"audio"`
- Video bool `json:"video"`
- Screen bool `json:"screen"`
- Hand bool `json:"hand"`
- }
- if json.Unmarshal(raw, &frame) != nil {
- continue
- }
- room.Touch()
- switch frame.Type {
- case "signal":
- //SDP / ICE relay to a single peer
- room.SendTo(frame.To, mrMarshalOrDrop(map[string]interface{}{
- "type": "signal",
- "from": participant.PeerID,
- "data": frame.Data,
- }))
- case "chat":
- text := frame.Text
- if strings.TrimSpace(text) == "" {
- continue
- }
- if runes := []rune(text); len(runes) > mrMaxChatLength {
- text = string(runes[:mrMaxChatLength])
- }
- room.Broadcast(mrMarshalOrDrop(map[string]interface{}{
- "type": "chat",
- "from": participant.PeerID,
- "username": participant.Username,
- "text": text,
- "time": time.Now().Unix(),
- }), -1)
- //Mirror into the room's shared space so AGI scripts can
- //read the conversation
- meetRoomManager.LogChat(room.ID, participant.Username, text)
- case "file":
- attachment, ok := meetRoomManager.GetAttachment(room.ID, frame.FileID)
- if !ok {
- continue
- }
- room.Broadcast(mrMarshalOrDrop(map[string]interface{}{
- "type": "file",
- "from": participant.PeerID,
- "username": participant.Username,
- "fileid": attachment.ID,
- "name": attachment.Name,
- "size": attachment.Size,
- "time": time.Now().Unix(),
- }), -1)
- case "state":
- //Mic / camera / screen share / raised-hand indicator update
- room.Broadcast(mrMarshalOrDrop(map[string]interface{}{
- "type": "state",
- "from": participant.PeerID,
- "audio": frame.Audio,
- "video": frame.Video,
- "screen": frame.Screen,
- "hand": frame.Hand,
- }), participant.PeerID)
- case "attendance":
- //Send the requester the room's join/leave log (the
- //participants side panel polls this on open and on
- //peer-join / peer-leave)
- room.SendTo(participant.PeerID, mrMarshalOrDrop(map[string]interface{}{
- "type": "attendance",
- "records": mrAttendanceList(room),
- }))
- case "ping":
- //App-level heartbeat: lets the client detect a half-dead
- //connection and trigger its auto-reconnect logic.
- room.SendTo(participant.PeerID, []byte(`{"type":"pong"}`))
- case "kick":
- //Host removes another participant. Order matters: the target
- //is told it was kicked (so its client stops auto-reconnecting)
- //while it is still subscribed, so the frame is queued and
- //drained before its socket drops; everyone is then told it
- //left; finally KickParticipant unregisters it and closes its
- //send channel, which ends its writer goroutine and hangs up.
- if !participant.IsHost || frame.To == participant.PeerID {
- continue
- }
- target, ok := room.GetParticipant(frame.To)
- if !ok || target.IsHost {
- continue
- }
- room.SendTo(target.PeerID, []byte(`{"type":"kicked"}`))
- //Everyone but the target hears the removal; the target gets the
- //dedicated "kicked" frame above instead of its own leave.
- room.Broadcast(mrMarshalOrDrop(map[string]interface{}{
- "type": "peer-leave",
- "peerid": target.PeerID,
- "username": target.Username,
- "kicked": true,
- }), target.PeerID)
- room.KickParticipant(target.PeerID)
- case "end":
- if participant.IsHost {
- mrEndMeeting(room.ID)
- return
- }
- }
- }
- })
- }
- // filepathExt returns the extension of a display file name (which never
- // contains a path separator by the time it reaches the server).
- func filepathExt(name string) string {
- if idx := strings.LastIndex(name, "."); idx >= 0 {
- return name[idx:]
- }
- return ""
- }
|