diff --git a/src/github.com/matrix-org/dendrite/common/eventcontent.go b/src/github.com/matrix-org/dendrite/common/eventcontent.go index e4b7e1820..7b2663ddc 100644 --- a/src/github.com/matrix-org/dendrite/common/eventcontent.go +++ b/src/github.com/matrix-org/dendrite/common/eventcontent.go @@ -90,3 +90,13 @@ func InitialPowerLevelsContent(roomCreator string) PowerLevelContent { Users: map[string]int{roomCreator: 100}, } } + +// AliasesContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-aliases +type AliasesContent struct { + Aliases []string `json:"aliases"` +} + +// CanonicalAliasContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-canonical-alias +type CanonicalAliasContent struct { + Alias string `json:"alias"` +} diff --git a/src/github.com/matrix-org/dendrite/publicroomsapi/README.md b/src/github.com/matrix-org/dendrite/publicroomsapi/README.md new file mode 100644 index 000000000..594fe29c5 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/publicroomsapi/README.md @@ -0,0 +1,5 @@ +# Public rooms API + +This server is responsible for serving requests hitting `/publicRooms` and `/directory/list/room/{roomID}` as per: + +https://matrix.org/docs/spec/client_server/r0.2.0.html#listing-rooms diff --git a/src/github.com/matrix-org/dendrite/publicroomsapi/storage/public_rooms_table.go b/src/github.com/matrix-org/dendrite/publicroomsapi/storage/public_rooms_table.go new file mode 100644 index 000000000..805ee39fd --- /dev/null +++ b/src/github.com/matrix-org/dendrite/publicroomsapi/storage/public_rooms_table.go @@ -0,0 +1,194 @@ +// Copyright 2017 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package storage + +import ( + "database/sql" + + "github.com/lib/pq" + "github.com/matrix-org/dendrite/publicroomsapi/types" +) + +const publicRoomsSchema = ` +-- Stores all of the rooms with data needed to create the server's room directory +CREATE TABLE IF NOT EXISTS publicroomsapi_public_rooms( + -- The room's ID + room_id TEXT NOT NULL PRIMARY KEY, + -- Number of joined members in the room + joined_members INTEGER NOT NULL DEFAULT 0, + -- Aliases of the room (empty array if none) + aliases TEXT[] NOT NULL DEFAULT DEFAULT '{}'::TEXT[], + -- Canonical alias of the room (empty string if none) + canonical_alias TEXT, + -- Name of the room (empty string if none) + name TEXT NOT NULL DEFAULT '', + -- Topic of the room (empty string if none) + topic TEXT NOT NULL DEFAULT '', + -- Is the room world readable? + world_readable BOOLEAN NOT NULL DEFAULT false, + -- Can guest join the room? + guest_can_join BOOLEAN NOT NULL DEFAULT false, + -- URL of the room avatar (empty string if none) + avatar_url TEXT NOT NULL DEFAULT '', + -- Visibility of the room: true means the room is publicly visible, false + -- means the room is private + visibility BOOLEAN NOT NULL DEFAULT false +); +` + +const countPublicRoomsSQL = "" + + "SELECT COUNT(*) FROM publicroomsapi_public_rooms" + + " WHERE visibility = true" + +const selectPublicRoomsSQL = "" + + "SELECT room_id, joined_members, aliases, canonical_alias, name, topic, world_readable, guest_can_join, avatar_url" + + " FROM publicroomsapi_public_rooms WHERE visibility = true" + + " ORDER BY joined_members DESC" + + " OFFSET $1" + +const selectPublicRoomswithLimitSQL = "" + + "SELECT room_id, joined_members, aliases, canonical_alias, name, topic, world_readable, guest_can_join, avatar_url" + + " FROM publicroomsapi_public_rooms WHERE visibility = true" + + " ORDER BY joined_members DESC" + + " OFFSET $1 LIMIT $2" + +const insertNewRoomSQL = "" + + "INSERT INTO publicroomsapi_public_rooms(room_id)" + + " VALUES ($1)" + +const incrementJoinedMembersInRoomSQL = "" + + "UPDATE publicroomsapi_public_rooms" + + " SET joined_members = joined_members + 1" + + " WHERE room_id = $1" + +const decrementJoinedMembersInRoomSQL = "" + + "UPDATE publicroomsapi_public_rooms" + + " SET joined_members = joined_members - 1" + + " WHERE room_id = $1" + +const updateRoomAttributeSQL = "" + + "UPDATE publicroomsapi_public_rooms" + + " SET $1 = $2" + + " WHERE room_id = $3" + +type publicRoomsStatements struct { + countPublicRoomsStmt *sql.Stmt + selectPublicRoomsStmt *sql.Stmt + selectPublicRoomswithLimitStmt *sql.Stmt + insertNewRoomStmt *sql.Stmt + incrementJoinedMembersInRoomStmt *sql.Stmt + decrementJoinedMembersInRoomStmt *sql.Stmt + updateRoomAttributeStmt *sql.Stmt +} + +func (s *publicRoomsStatements) prepare(db *sql.DB) (err error) { + _, err = db.Exec(publicRoomsSchema) + if err != nil { + return + } + if s.countPublicRoomsStmt, err = db.Prepare(countPublicRoomsSQL); err != nil { + return + } + if s.selectPublicRoomsStmt, err = db.Prepare(selectPublicRoomsSQL); err != nil { + return + } + if s.selectPublicRoomswithLimitStmt, err = db.Prepare(selectPublicRoomswithLimitSQL); err != nil { + return + } + if s.insertNewRoomStmt, err = db.Prepare(insertNewRoomSQL); err != nil { + return + } + if s.incrementJoinedMembersInRoomStmt, err = db.Prepare(incrementJoinedMembersInRoomSQL); err != nil { + return + } + if s.decrementJoinedMembersInRoomStmt, err = db.Prepare(decrementJoinedMembersInRoomSQL); err != nil { + return + } + if s.updateRoomAttributeStmt, err = db.Prepare(updateRoomAttributeSQL); err != nil { + return + } + return +} + +func (s *publicRoomsStatements) countPublicRooms() (nb int64, err error) { + err = s.countPublicRoomsStmt.QueryRow().Scan(&nb) + return +} + +func (s *publicRoomsStatements) selectPublicRooms(offset int64, limit int16) ([]types.PublicRoom, error) { + var rows *sql.Rows + var err error + + if limit == 0 { + rows, err = s.selectPublicRoomsStmt.Query(offset) + } else { + rows, err = s.selectPublicRoomswithLimitStmt.Query(offset, limit) + } + + if err != nil { + return []types.PublicRoom{}, nil + } + + rooms := []types.PublicRoom{} + for rows.Next() { + var r types.PublicRoom + var aliases pq.StringArray + + err = rows.Scan( + &r.RoomID, &r.NumJoinedMembers, &aliases, &r.CanonicalAlias, + &r.Name, &r.Topic, &r.WorldReadable, &r.GuestCanJoin, &r.AvatarURL, + ) + if err != nil { + return rooms, err + } + + r.Aliases = make([]string, len(aliases)) + for i := range aliases { + r.Aliases[i] = aliases[i] + } + + rooms = append(rooms, r) + } + + return rooms, nil +} + +func (s *publicRoomsStatements) insertNewRoom(roomID string) error { + _, err := s.insertNewRoomStmt.Exec(roomID) + return err +} + +func (s *publicRoomsStatements) incrementJoinedMembersInRoom(roomID string) error { + _, err := s.incrementJoinedMembersInRoomStmt.Exec(roomID) + return err +} + +func (s *publicRoomsStatements) decrementJoinedMembersInRoom(roomID string) error { + _, err := s.decrementJoinedMembersInRoomStmt.Exec(roomID) + return err +} + +func (s *publicRoomsStatements) updateRoomAttribute(attrName string, attrValue attributeValue, roomID string) error { + var value interface{} + if attrName == "aliases" { + // Aliases need a special conversion + value = pq.StringArray(attrValue.([]string)) + } else { + value = attrValue + } + + _, err := s.updateRoomAttributeStmt.Exec(attrName, value, roomID) + return err +} diff --git a/src/github.com/matrix-org/dendrite/publicroomsapi/storage/storage.go b/src/github.com/matrix-org/dendrite/publicroomsapi/storage/storage.go new file mode 100644 index 000000000..fcbdbfb00 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/publicroomsapi/storage/storage.go @@ -0,0 +1,104 @@ +// Copyright 2017 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package storage + +import ( + "database/sql" + "encoding/json" + + "github.com/matrix-org/dendrite/common" + "github.com/matrix-org/dendrite/publicroomsapi/types" + + "github.com/matrix-org/gomatrixserverlib" +) + +// PublicRoomsServerDatabase represents a public rooms server database +type PublicRoomsServerDatabase struct { + db *sql.DB + partitions common.PartitionOffsetStatements + statements publicRoomsStatements +} + +type attributeValue interface{} + +// NewPublicRoomsServerDatabase creates a new public rooms server database +func NewPublicRoomsServerDatabase(dataSourceName string) (*PublicRoomsServerDatabase, error) { + var db *sql.DB + var err error + if db, err = sql.Open("postgres", dataSourceName); err != nil { + return nil, err + } + partitions := common.PartitionOffsetStatements{} + if err = partitions.Prepare(db, "publicroomsapi"); err != nil { + return nil, err + } + statements := publicRoomsStatements{} + if err = statements.prepare(db); err != nil { + return nil, err + } + return &PublicRoomsServerDatabase{db, partitions, statements}, nil +} + +// CountPublicRooms returns the number of room set as publicly visible on the server. +// Returns an error if the retrieval failed. +func (d *PublicRoomsServerDatabase) CountPublicRooms() (int64, error) { + return d.statements.countPublicRooms() +} + +// GetPublicRooms returns an array containing the local rooms set as publicly visible, ordered by their number +// of joined members. This array can be limited by a given number of elements, and offset by a given value. +// If the limit is 0, doesn't limit the number of results. If the offset is 0 too, the array contains all +// the rooms set as publicly visible on the server. +// Returns an error if the retrieval failed. +func (d *PublicRoomsServerDatabase) GetPublicRooms(offset int64, limit int16) ([]types.PublicRoom, error) { + return d.statements.selectPublicRooms(offset, limit) +} + +// UpdateRoomFromEvent updates the database representation of a room from a Matrix event, by +// checking the event's type to know which attribute to change and using the event's content +// to define the new value of the attribute. +// If the event doesn't match with any property used to compute the public room directory, +// does nothing. +// If something went wrong during the process, returns an error. +func (d *PublicRoomsServerDatabase) UpdateRoomFromEvent(event gomatrixserverlib.Event) error { + var attrName string + var attrValue attributeValue + + roomID := event.RoomID() + switch event.Type() { + case "m.room.create": + return d.statements.insertNewRoom(roomID) + case "m.room.aliases": + var content common.AliasesContent + if err := json.Unmarshal(event.Content(), &content); err != nil { + return err + } + + attrName = "aliases" + attrValue = content.Aliases + break + case "m.room.canonical_alias": + var content common.CanonicalAliasContent + if err := json.Unmarshal(event.Content(), &content); err != nil { + return err + } + + attrName = "canonical_alias" + attrValue = content.Alias + break + } + + return d.statements.updateRoomAttribute(attrName, attrValue, roomID) +} diff --git a/src/github.com/matrix-org/dendrite/publicroomsapi/types/types.go b/src/github.com/matrix-org/dendrite/publicroomsapi/types/types.go new file mode 100644 index 000000000..c284bcca4 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/publicroomsapi/types/types.go @@ -0,0 +1,28 @@ +// Copyright 2017 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +// PublicRoom represents a local public room +type PublicRoom struct { + RoomID string `json:"room_id"` + Aliases []string `json:"aliases,omitempty"` + CanonicalAlias string `json:"canonical_alias,omitempty"` + Name string `json:"name,omitempty"` + Topic string `json:"topic,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` + NumJoinedMembers int64 `json:"num_joined_members"` + WorldReadable bool `json:"world_readable"` + GuestCanJoin bool `json:"guest_can_join"` +}