diff --git a/STATUS.md b/STATUS.md index 143b393..71864c0 100644 --- a/STATUS.md +++ b/STATUS.md @@ -113,7 +113,7 @@ Implemented from [Client-Server API](https://matrix.org/docs/spec/client_server/ - [x] [10.5.1 GET /_matrix/client/r0/directory/list/room/{roomId}](https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-directory-list-room-roomid) - [x] [10.5.2 PUT /_matrix/client/r0/directory/list/room/{roomId}](https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-directory-list-room-roomid) -- [ ] [10.5.3 GET /_matrix/client/r0/publicRooms](https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-publicrooms) +- [x] [10.5.3 GET /_matrix/client/r0/publicRooms](https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-publicrooms) - [ ] [10.5.4 POST /_matrix/client/r0/publicRooms](https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-publicrooms) ## [11 User Data](https://matrix.org/docs/spec/client_server/latest#user-data) diff --git a/internal/backend.go b/internal/backend.go index 1f2ba49..5db6f7b 100644 --- a/internal/backend.go +++ b/internal/backend.go @@ -15,6 +15,7 @@ type Backend interface { GetUserByName(userName string) User GetRoomByID(id string) Room Sync(token string, request sync.SyncRequest) (response *sync.SyncReply, err *models.ApiError) + PublicRooms() []Room } type Room interface { @@ -26,6 +27,9 @@ type Room interface { Topic() string Events() []rooms.Event Visibility() createroom.VisibilityType + WorldReadable() bool + GuestCanJoin() bool + AvatarURL() string State() createroom.Preset } diff --git a/internal/backends/memory/backend.go b/internal/backends/memory/backend.go index 727800b..d67b5e7 100644 --- a/internal/backends/memory/backend.go +++ b/internal/backends/memory/backend.go @@ -1,8 +1,11 @@ package memory import ( + "sort" "sync" + "github.com/nxshock/signaller/internal/models/createroom" + "github.com/nxshock/signaller/internal" "github.com/nxshock/signaller/internal/models" mSync "github.com/nxshock/signaller/internal/models/sync" @@ -111,3 +114,20 @@ func (backend *Backend) GetUserByName(userName string) internal.User { return nil } + +func (backend *Backend) PublicRooms() []internal.Room { + backend.mutex.Lock() + defer backend.mutex.Unlock() + + var rooms []internal.Room + + for _, room := range backend.rooms { + if room.State() == createroom.PublicChat { + rooms = append(rooms, room) + } + } + + sort.Sort(BySize(rooms)) + + return rooms +} diff --git a/internal/backends/memory/rooms.go b/internal/backends/memory/rooms.go index 1b7f1e1..202a401 100644 --- a/internal/backends/memory/rooms.go +++ b/internal/backends/memory/rooms.go @@ -9,12 +9,15 @@ import ( ) type Room struct { - id string - visibility createroom.VisibilityType - aliasName string - name string - topic string - state createroom.Preset + id string + visibility createroom.VisibilityType + aliasName string + name string + topic string + state createroom.Preset + worldReadable bool + guestCanJoin bool + avatarURL string creator internal.User joined []internal.User @@ -93,3 +96,24 @@ func (room *Room) State() createroom.Preset { return room.state } + +func (room *Room) WorldReadable() bool { + room.mutex.RLock() + defer room.mutex.RUnlock() + + return room.worldReadable +} + +func (room *Room) GuestCanJoin() bool { + room.mutex.RLock() + defer room.mutex.RUnlock() + + return room.guestCanJoin +} + +func (room *Room) AvatarURL() string { + room.mutex.RLock() + defer room.mutex.RUnlock() + + return room.avatarURL +} diff --git a/internal/backends/memory/roomsort.go b/internal/backends/memory/roomsort.go new file mode 100644 index 0000000..a72097f --- /dev/null +++ b/internal/backends/memory/roomsort.go @@ -0,0 +1,11 @@ +package memory + +import ( + "github.com/nxshock/signaller/internal" +) + +type BySize []internal.Room + +func (a BySize) Len() int { return len(a) } +func (a BySize) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a BySize) Less(i, j int) bool { return len(a[i].Users()) > len(a[j].Users()) } diff --git a/internal/handlers.go b/internal/handlers.go index 44bfc10..4ba8c58 100644 --- a/internal/handlers.go +++ b/internal/handlers.go @@ -8,21 +8,19 @@ import ( "strconv" "strings" - "github.com/nxshock/signaller/internal/models/registeravailable" - - "github.com/nxshock/signaller/internal/models/devices" - "github.com/gorilla/mux" - "github.com/nxshock/signaller/internal/models/common" - "github.com/nxshock/signaller/internal/models" "github.com/nxshock/signaller/internal/models/capabilities" + "github.com/nxshock/signaller/internal/models/common" + "github.com/nxshock/signaller/internal/models/devices" "github.com/nxshock/signaller/internal/models/joinedrooms" "github.com/nxshock/signaller/internal/models/listroom" login "github.com/nxshock/signaller/internal/models/login" "github.com/nxshock/signaller/internal/models/password" + "github.com/nxshock/signaller/internal/models/publicrooms" register "github.com/nxshock/signaller/internal/models/register" + "github.com/nxshock/signaller/internal/models/registeravailable" mSync "github.com/nxshock/signaller/internal/models/sync" "github.com/nxshock/signaller/internal/models/versions" "github.com/nxshock/signaller/internal/models/whoami" @@ -412,6 +410,58 @@ func listRoomHandler(w http.ResponseWriter, r *http.Request) { } } +// https://matrix.org/docs/spec/client_server/latest#id335 +func publicRoomsHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + var request publicrooms.Request + err := getRequest(r, &request) + if err != nil { + errorResponse(w, models.M_BAD_JSON, http.StatusBadRequest, err.Error()) + return + } + + if request.Limit <= 0 { + request.Limit = 50 // TODO: move to const + } + + chunks := roomsToPublicRoomsChunks(currServer.Backend.PublicRooms()) + + var response publicrooms.Response + + // TODO: test and rewrite this code + if request.Since == "" { + if request.Limit >= len(chunks) { + response.Chunk = chunks + // TODO: should fill response.TotalRoomCountEstimate? + } else { + response.Chunk = chunks[:request.Limit] + response.NextBatch = strconv.Itoa(request.Limit + 1) + response.TotalRoomCountEstimate = len(chunks) + } + } else { + response.PrevBatch = "0" + + from, err := strconv.Atoi(request.Since) + if err != nil { + errorResponse(w, models.M_INVALID_PARAM, http.StatusBadRequest, "Wrong Since field specified") // TODO: check code + return + } + if len(chunks) >= from { + if request.Limit >= len(chunks) { + response.Chunk = chunks[from : from+request.Limit] + } else { + response.Chunk = chunks[from:] + } + } + } + sendJsonResponse(w, http.StatusOK, response) + case http.MethodPost: // TODO: implement + default: + errorResponse(w, models.M_UNKNOWN, http.StatusBadRequest, "wrong method: "+r.Method) + } +} + func sendJsonResponse(w http.ResponseWriter, httpStatus int, data interface{}) error { b, err := json.Marshal(data) if err != nil { diff --git a/internal/models/publicrooms/publicrooms.go b/internal/models/publicrooms/publicrooms.go new file mode 100644 index 0000000..47b936a --- /dev/null +++ b/internal/models/publicrooms/publicrooms.go @@ -0,0 +1,26 @@ +package publicrooms + +type Request struct { + Limit int `json:"limit"` // Limit the number of results returned. + Since string `json:"since"` // A pagination token from a previous request, allowing clients to get the next (or previous) batch of rooms. The direction of pagination is specified solely by which token is supplied, rather than via an explicit flag. + Server string `json:"server"` // The server to fetch the public room lists from. Defaults to the local server. +} + +type Response struct { + Chunk []PublicRoomsChunk `json:"chunk"` // A paginated chunk of public rooms. + NextBatch string `json:"next_batch,omitempty"` // A pagination token for the response. The absence of this token means there are no more results to fetch and the client should stop paginating. + PrevBatch string `json:"prev_batch,omitempty"` // A pagination token that allows fetching previous results. The absence of this token means there are no results before this batch, i.e. this is the first batch. + TotalRoomCountEstimate int `json:"total_room_count_estimate,omitempty"` // An estimate on the total number of public rooms, if the server has an estimate. +} + +type PublicRoomsChunk struct { + Aliases []string `json:"aliases,omitempty"` // Aliases of the room. May be empty. + CanonicalAlias string `json:"canonical_alias,omitempty"` // The canonical alias of the room, if any. + Name string `json:"name,omitempty"` // The name of the room, if any. + NumJoinedMembers int `json:"num_joined_members"` // Required. The number of members joined to the room. + RoomID string `json:"room_id"` // Required. The ID of the room. + Topic string `json:"topic,omitempty"` // The topic of the room, if any. + WorldReadable bool `json:"world_readable"` // Required. Whether the room may be viewed by guest users without joining. + GuestCanJoin bool `json:"guest_can_join"` // Required. Whether guest users may join the room and participate in it. If they can, they will be subject to ordinary power level rules like any other user. + AvatarURL string `json:"avatar_url,omitempty"` // The URL for the room's avatar, if one is set. +} diff --git a/internal/server.go b/internal/server.go index 28cdffd..5d8a849 100644 --- a/internal/server.go +++ b/internal/server.go @@ -39,6 +39,7 @@ func NewServer(port int) (*Server, error) { router.HandleFunc("/_matrix/client/r0/directory/list/room/{roomID}", listRoomHandler) router.HandleFunc("/_matrix/client/r0/rooms/{roomId}/leave", leaveRoomHandler) router.HandleFunc("/_matrix/client/r0/register/available", registerAvailableHandler) + router.HandleFunc("/_matrix/client/r0/publicRooms", publicRoomsHandler) router.HandleFunc("/", RootHandler) if port <= 0 || port > 65535 { diff --git a/internal/utils.go b/internal/utils.go new file mode 100644 index 0000000..3ccd128 --- /dev/null +++ b/internal/utils.go @@ -0,0 +1,24 @@ +package internal + +import "github.com/nxshock/signaller/internal/models/publicrooms" + +func roomsToPublicRoomsChunks(rooms []Room) []publicrooms.PublicRoomsChunk { + var chunks []publicrooms.PublicRoomsChunk + + for _, room := range rooms { + chunk := publicrooms.PublicRoomsChunk{ + // TODO: Aliases: + CanonicalAlias: room.AliasName(), + Name: room.Name(), + NumJoinedMembers: len(room.Users()), + RoomID: room.ID(), + Topic: room.Topic(), + WorldReadable: room.WorldReadable(), + GuestCanJoin: room.GuestCanJoin(), + AvatarURL: room.AvatarURL()} + + chunks = append(chunks, chunk) + } + + return chunks +}