Skip to content

Commit d578d1a

Browse files
committed
Move a bunch of stuff from mautrix-whatsapp
Moved parts: * Appservice SQL state store * Bridge crypto helper * Database upgrade framework * Bridge startup flow Other changes: * Improved database upgrade framework * Now primarily using static SQL files compiled with go:embed * Moved appservice SQL state store to using membership enum on Postgres
1 parent 915aa9d commit d578d1a

24 files changed

+1925
-393
lines changed

appservice/appservice.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
"github.com/gorilla/mux"
2424
"github.com/gorilla/websocket"
2525
"golang.org/x/net/publicsuffix"
26-
"gopkg.in/yaml.v2"
26+
"gopkg.in/yaml.v3"
2727

2828
"maunium.net/go/maulogger/v2"
2929

appservice/registration.go

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2019 Tulir Asokan
1+
// Copyright (c) 2022 Tulir Asokan
22
//
33
// This Source Code Form is subject to the terms of the Mozilla Public
44
// License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -10,11 +10,11 @@ import (
1010
"io/ioutil"
1111
"regexp"
1212

13-
"gopkg.in/yaml.v2"
13+
"gopkg.in/yaml.v3"
1414
)
1515

1616
// Registration contains the data in a Matrix appservice registration.
17-
// See https://matrix.org/docs/spec/application_service/unstable.html#registration
17+
// See https://spec.matrix.org/v1.2/application-service-api/#registration
1818
type Registration struct {
1919
ID string `yaml:"id"`
2020
URL string `yaml:"url"`
@@ -23,8 +23,10 @@ type Registration struct {
2323
SenderLocalpart string `yaml:"sender_localpart"`
2424
RateLimited *bool `yaml:"rate_limited,omitempty"`
2525
Namespaces Namespaces `yaml:"namespaces"`
26-
EphemeralEvents bool `yaml:"de.sorunome.msc2409.push_ephemeral,omitempty"`
2726
Protocols []string `yaml:"protocols,omitempty"`
27+
28+
SoruEphemeralEvents bool `yaml:"de.sorunome.msc2409.push_ephemeral,omitempty"`
29+
EphemeralEvents bool `yaml:"push_ephemeral,omitempty"`
2830
}
2931

3032
// CreateRegistration creates a Registration with random appservice and homeserver tokens.
@@ -70,9 +72,9 @@ func (reg *Registration) YAML() (string, error) {
7072

7173
// Namespaces contains the three areas that appservices can reserve parts of.
7274
type Namespaces struct {
73-
UserIDs []Namespace `yaml:"users,omitempty"`
74-
RoomAliases []Namespace `yaml:"aliases,omitempty"`
75-
RoomIDs []Namespace `yaml:"rooms,omitempty"`
75+
UserIDs NamespaceList `yaml:"users,omitempty"`
76+
RoomAliases NamespaceList `yaml:"aliases,omitempty"`
77+
RoomIDs NamespaceList `yaml:"rooms,omitempty"`
7678
}
7779

7880
// Namespace is a reserved namespace in any area.
@@ -81,26 +83,16 @@ type Namespace struct {
8183
Exclusive bool `yaml:"exclusive"`
8284
}
8385

84-
// RegisterUserIDs creates an user ID namespace registration.
85-
func (nslist *Namespaces) RegisterUserIDs(regex *regexp.Regexp, exclusive bool) {
86-
nslist.UserIDs = append(nslist.UserIDs, Namespace{
87-
Regex: regex.String(),
88-
Exclusive: exclusive,
89-
})
90-
}
91-
92-
// RegisterRoomAliases creates an room alias namespace registration.
93-
func (nslist *Namespaces) RegisterRoomAliases(regex *regexp.Regexp, exclusive bool) {
94-
nslist.RoomAliases = append(nslist.RoomAliases, Namespace{
95-
Regex: regex.String(),
96-
Exclusive: exclusive,
97-
})
98-
}
86+
type NamespaceList []Namespace
9987

100-
// RegisterRoomIDs creates an room ID namespace registration.
101-
func (nslist *Namespaces) RegisterRoomIDs(regex *regexp.Regexp, exclusive bool) {
102-
nslist.RoomIDs = append(nslist.RoomIDs, Namespace{
88+
func (nsl *NamespaceList) Register(regex *regexp.Regexp, exclusive bool) {
89+
ns := Namespace{
10390
Regex: regex.String(),
10491
Exclusive: exclusive,
105-
})
92+
}
93+
if nsl == nil {
94+
*nsl = []Namespace{ns}
95+
} else {
96+
*nsl = append(*nsl, ns)
97+
}
10698
}
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
// Copyright (c) 2022 Tulir Asokan
2+
//
3+
// This Source Code Form is subject to the terms of the Mozilla Public
4+
// License, v. 2.0. If a copy of the MPL was not distributed with this
5+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
7+
package sqlstatestore
8+
9+
import (
10+
"database/sql"
11+
"embed"
12+
"encoding/json"
13+
"errors"
14+
"sync"
15+
16+
log "maunium.net/go/maulogger/v2"
17+
18+
"maunium.net/go/mautrix/appservice"
19+
"maunium.net/go/mautrix/event"
20+
"maunium.net/go/mautrix/id"
21+
"maunium.net/go/mautrix/util/dbutil"
22+
)
23+
24+
//go:embed *.sql
25+
var rawUpgrades embed.FS
26+
27+
var UpgradeTable dbutil.UpgradeTable
28+
29+
func init() {
30+
UpgradeTable.RegisterFS(rawUpgrades)
31+
}
32+
33+
const VersionTableName = "mx_version"
34+
35+
type SQLStateStore struct {
36+
*dbutil.Database
37+
*appservice.TypingStateStore
38+
39+
log log.Logger
40+
41+
Typing map[id.RoomID]map[id.UserID]int64
42+
typingLock sync.RWMutex
43+
}
44+
45+
var _ appservice.StateStore = (*SQLStateStore)(nil)
46+
47+
func NewSQLStateStore(db *dbutil.Database) *SQLStateStore {
48+
return &SQLStateStore{
49+
Database: db.Child("StateStore", VersionTableName, UpgradeTable),
50+
TypingStateStore: appservice.NewTypingStateStore(),
51+
}
52+
}
53+
54+
func (store *SQLStateStore) IsRegistered(userID id.UserID) bool {
55+
var isRegistered bool
56+
err := store.
57+
QueryRow("SELECT EXISTS(SELECT 1 FROM mx_registrations WHERE user_id=$1)", userID).
58+
Scan(&isRegistered)
59+
if err != nil {
60+
store.log.Warnfln("Failed to scan registration existence for %s: %v", userID, err)
61+
}
62+
return isRegistered
63+
}
64+
65+
func (store *SQLStateStore) MarkRegistered(userID id.UserID) {
66+
_, err := store.Exec("INSERT INTO mx_registrations (user_id) VALUES ($1) ON CONFLICT (user_id) DO NOTHING", userID)
67+
if err != nil {
68+
store.log.Warnfln("Failed to mark %s as registered: %v", userID, err)
69+
}
70+
}
71+
72+
func (store *SQLStateStore) GetRoomMembers(roomID id.RoomID) map[id.UserID]*event.MemberEventContent {
73+
members := make(map[id.UserID]*event.MemberEventContent)
74+
rows, err := store.Query("SELECT user_id, membership, displayname, avatar_url FROM mx_user_profile WHERE room_id=$1", roomID)
75+
if err != nil {
76+
return members
77+
}
78+
var userID id.UserID
79+
var member event.MemberEventContent
80+
for rows.Next() {
81+
err = rows.Scan(&userID, &member.Membership, &member.Displayname, &member.AvatarURL)
82+
if err != nil {
83+
store.log.Warnfln("Failed to scan member in %s: %v", roomID, err)
84+
} else {
85+
members[userID] = &member
86+
}
87+
}
88+
return members
89+
}
90+
91+
func (store *SQLStateStore) GetMembership(roomID id.RoomID, userID id.UserID) event.Membership {
92+
membership := event.MembershipLeave
93+
err := store.
94+
QueryRow("SELECT membership FROM mx_user_profile WHERE room_id=$1 AND user_id=$2", roomID, userID).
95+
Scan(&membership)
96+
if err != nil && err != sql.ErrNoRows {
97+
store.log.Warnfln("Failed to scan membership of %s in %s: %v", userID, roomID, err)
98+
}
99+
return membership
100+
}
101+
102+
func (store *SQLStateStore) GetMember(roomID id.RoomID, userID id.UserID) *event.MemberEventContent {
103+
member, ok := store.TryGetMember(roomID, userID)
104+
if !ok {
105+
member.Membership = event.MembershipLeave
106+
}
107+
return member
108+
}
109+
110+
func (store *SQLStateStore) TryGetMember(roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, bool) {
111+
var member event.MemberEventContent
112+
err := store.
113+
QueryRow("SELECT membership, displayname, avatar_url FROM mx_user_profile WHERE room_id=$1 AND user_id=$2", roomID, userID).
114+
Scan(&member.Membership, &member.Displayname, &member.AvatarURL)
115+
if err != nil && err != sql.ErrNoRows {
116+
store.log.Warnfln("Failed to scan member info of %s in %s: %v", userID, roomID, err)
117+
}
118+
return &member, err == nil
119+
}
120+
121+
func (store *SQLStateStore) FindSharedRooms(userID id.UserID) (rooms []id.RoomID) {
122+
rows, err := store.Query(`
123+
SELECT room_id FROM mx_user_profile
124+
LEFT JOIN portal ON portal.mxid=mx_user_profile.room_id
125+
WHERE user_id=$1 AND portal.encrypted=true
126+
`, userID)
127+
if err != nil {
128+
store.log.Warnfln("Failed to query shared rooms with %s: %v", userID, err)
129+
return
130+
}
131+
for rows.Next() {
132+
var roomID id.RoomID
133+
err = rows.Scan(&roomID)
134+
if err != nil {
135+
store.log.Warnfln("Failed to scan room ID: %v", err)
136+
} else {
137+
rooms = append(rooms, roomID)
138+
}
139+
}
140+
return
141+
}
142+
143+
func (store *SQLStateStore) IsInRoom(roomID id.RoomID, userID id.UserID) bool {
144+
return store.IsMembership(roomID, userID, "join")
145+
}
146+
147+
func (store *SQLStateStore) IsInvited(roomID id.RoomID, userID id.UserID) bool {
148+
return store.IsMembership(roomID, userID, "join", "invite")
149+
}
150+
151+
func (store *SQLStateStore) IsMembership(roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool {
152+
membership := store.GetMembership(roomID, userID)
153+
for _, allowedMembership := range allowedMemberships {
154+
if allowedMembership == membership {
155+
return true
156+
}
157+
}
158+
return false
159+
}
160+
161+
func (store *SQLStateStore) SetMembership(roomID id.RoomID, userID id.UserID, membership event.Membership) {
162+
_, err := store.Exec(`
163+
INSERT INTO mx_user_profile (room_id, user_id, membership) VALUES ($1, $2, $3)
164+
ON CONFLICT (room_id, user_id) DO UPDATE SET membership=excluded.membership
165+
`, roomID, userID, membership)
166+
if err != nil {
167+
store.log.Warnfln("Failed to set membership of %s in %s to %s: %v", userID, roomID, membership, err)
168+
}
169+
}
170+
171+
func (store *SQLStateStore) SetMember(roomID id.RoomID, userID id.UserID, member *event.MemberEventContent) {
172+
_, err := store.Exec(`
173+
INSERT INTO mx_user_profile (room_id, user_id, membership, displayname, avatar_url) VALUES ($1, $2, $3, $4, $5)
174+
ON CONFLICT (room_id, user_id) DO UPDATE SET membership=excluded.membership, displayname=excluded.displayname, avatar_url=excluded.avatar_url
175+
`, roomID, userID, member.Membership, member.Displayname, member.AvatarURL)
176+
if err != nil {
177+
store.log.Warnfln("Failed to set membership of %s in %s to %s: %v", userID, roomID, member, err)
178+
}
179+
}
180+
181+
func (store *SQLStateStore) SetPowerLevels(roomID id.RoomID, levels *event.PowerLevelsEventContent) {
182+
levelsBytes, err := json.Marshal(levels)
183+
if err != nil {
184+
store.log.Errorfln("Failed to marshal power levels of %s: %v", roomID, err)
185+
return
186+
}
187+
_, err = store.Exec(`
188+
INSERT INTO mx_room_state (room_id, power_levels) VALUES ($1, $2)
189+
ON CONFLICT (room_id) DO UPDATE SET power_levels=excluded.power_levels
190+
`, roomID, levelsBytes)
191+
if err != nil {
192+
store.log.Warnfln("Failed to store power levels of %s: %v", roomID, err)
193+
}
194+
}
195+
196+
func (store *SQLStateStore) GetPowerLevels(roomID id.RoomID) (levels *event.PowerLevelsEventContent) {
197+
var data []byte
198+
err := store.
199+
QueryRow("SELECT power_levels FROM mx_room_state WHERE room_id=$1", roomID).
200+
Scan(&data)
201+
if err != nil {
202+
if !errors.Is(err, sql.ErrNoRows) {
203+
store.log.Errorfln("Failed to scan power levels of %s: %v", roomID, err)
204+
}
205+
return
206+
}
207+
levels = &event.PowerLevelsEventContent{}
208+
err = json.Unmarshal(data, levels)
209+
if err != nil {
210+
store.log.Errorfln("Failed to parse power levels of %s: %v", roomID, err)
211+
return nil
212+
}
213+
return
214+
}
215+
216+
func (store *SQLStateStore) GetPowerLevel(roomID id.RoomID, userID id.UserID) int {
217+
if store.Dialect == dbutil.Postgres {
218+
var powerLevel int
219+
err := store.
220+
QueryRow(`
221+
SELECT COALESCE((power_levels->'users'->$2)::int, (power_levels->'users_default')::int, 0)
222+
FROM mx_room_state WHERE room_id=$1
223+
`, roomID, userID).
224+
Scan(&powerLevel)
225+
if err != nil && !errors.Is(err, sql.ErrNoRows) {
226+
store.log.Errorfln("Failed to scan power level of %s in %s: %v", userID, roomID, err)
227+
}
228+
return powerLevel
229+
}
230+
return store.GetPowerLevels(roomID).GetUserLevel(userID)
231+
}
232+
233+
func (store *SQLStateStore) GetPowerLevelRequirement(roomID id.RoomID, eventType event.Type) int {
234+
if store.Dialect == dbutil.Postgres {
235+
defaultType := "events_default"
236+
defaultValue := 0
237+
if eventType.IsState() {
238+
defaultType = "state_default"
239+
defaultValue = 50
240+
}
241+
var powerLevel int
242+
err := store.
243+
QueryRow(`
244+
SELECT COALESCE((power_levels->'events'->$2)::int, (power_levels->'$3')::int, $4)
245+
FROM mx_room_state WHERE room_id=$1
246+
`, roomID, eventType.Type, defaultType, defaultValue).
247+
Scan(&powerLevel)
248+
if err != nil {
249+
if !errors.Is(err, sql.ErrNoRows) {
250+
store.log.Errorfln("Failed to scan power level for %s in %s: %v", eventType, roomID, err)
251+
}
252+
return defaultValue
253+
}
254+
return powerLevel
255+
}
256+
return store.GetPowerLevels(roomID).GetEventLevel(eventType)
257+
}
258+
259+
func (store *SQLStateStore) HasPowerLevel(roomID id.RoomID, userID id.UserID, eventType event.Type) bool {
260+
if store.Dialect == dbutil.Postgres {
261+
defaultType := "events_default"
262+
defaultValue := 0
263+
if eventType.IsState() {
264+
defaultType = "state_default"
265+
defaultValue = 50
266+
}
267+
var hasPower bool
268+
err := store.
269+
QueryRow(`SELECT
270+
COALESCE((power_levels->'users'->$2)::int, (power_levels->'users_default')::int, 0)
271+
>=
272+
COALESCE((power_levels->'events'->$3)::int, (power_levels->'$4')::int, $5)
273+
FROM mx_room_state WHERE room_id=$1`, roomID, userID, eventType.Type, defaultType, defaultValue).
274+
Scan(&hasPower)
275+
if err != nil {
276+
if !errors.Is(err, sql.ErrNoRows) {
277+
store.log.Errorfln("Failed to scan power level for %s in %s: %v", eventType, roomID, err)
278+
}
279+
return defaultValue == 0
280+
}
281+
return hasPower
282+
}
283+
return store.GetPowerLevel(roomID, userID) >= store.GetPowerLevelRequirement(roomID, eventType)
284+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-- v1: Initial revision
2+
3+
CREATE TABLE mx_registrations (
4+
user_id TEXT PRIMARY KEY
5+
);
6+
7+
CREATE TABLE mx_user_profile (
8+
room_id TEXT,
9+
user_id TEXT,
10+
membership TEXT NOT NULL,
11+
displayname TEXT,
12+
avatar_url TEXT,
13+
PRIMARY KEY (room_id, user_id)
14+
);
15+
16+
CREATE TABLE mx_room_state (
17+
room_id TEXT PRIMARY KEY,
18+
power_levels jsonb
19+
);

0 commit comments

Comments
 (0)