Add sending server notices on startup
This commit is contained in:
parent
219a15c4c3
commit
6622fda08c
|
@ -1,14 +1,20 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
userdb "github.com/matrix-org/dendrite/userapi/storage"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
@ -104,6 +110,86 @@ func consent(writer http.ResponseWriter, req *http.Request, userAPI userapi.User
|
||||||
return &util.JSONResponse{Code: http.StatusOK}
|
return &util.JSONResponse{Code: http.StatusOK}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sendServerNoticeForConsent(userAPI userapi.UserInternalAPI, rsAPI api.RoomserverInternalAPI,
|
||||||
|
cfgNotices *config.ServerNotices,
|
||||||
|
cfgClient *config.ClientAPI,
|
||||||
|
senderDevice *userapi.Device,
|
||||||
|
accountsDB userdb.Database,
|
||||||
|
asAPI appserviceAPI.AppServiceQueryAPI,
|
||||||
|
) {
|
||||||
|
logrus.Infof("Sending server notice to users who have not yet accepted the policy")
|
||||||
|
res := &userapi.QueryOutdatedPolicyUsersResponse{}
|
||||||
|
if err := userAPI.GetOutdatedPolicy(context.Background(), &userapi.QueryOutdatedPolicyUsersRequest{
|
||||||
|
PolicyVersion: cfgClient.Matrix.UserConsentOptions.Version,
|
||||||
|
}, res); err != nil {
|
||||||
|
logrus.WithError(err).Error("unable to fetch users with outdated consent policy")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
consentOpts := cfgClient.Matrix.UserConsentOptions
|
||||||
|
data := make(map[string]string)
|
||||||
|
var err error
|
||||||
|
sentMessages := 0
|
||||||
|
for _, userID := range res.OutdatedUsers {
|
||||||
|
if userID == cfgClient.Matrix.ServerNotices.LocalPart {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userID = fmt.Sprintf("@%s:%s", userID, cfgClient.Matrix.ServerName)
|
||||||
|
data["ConsentURL"], err = buildConsentURI(cfgClient, userID)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).WithField("userID", userID).Error("unable to construct consentURI")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logrus.Debugf("sending message to %s", userID)
|
||||||
|
msgBody := &bytes.Buffer{}
|
||||||
|
|
||||||
|
if err = consentOpts.TextTemplates.ExecuteTemplate(msgBody, "serverNoticeTemplate", data); err != nil {
|
||||||
|
logrus.WithError(err).WithField("userID", userID).Error("unable to execute serverNoticeTemplate")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
req := sendServerNoticeRequest{
|
||||||
|
UserID: userID,
|
||||||
|
Content: struct {
|
||||||
|
MsgType string `json:"msgtype,omitempty"`
|
||||||
|
Body string `json:"body,omitempty"`
|
||||||
|
}{
|
||||||
|
MsgType: consentOpts.ServerNoticeContent.MsgType,
|
||||||
|
Body: msgBody.String(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = sendServerNotice(context.Background(), req, rsAPI, cfgNotices, cfgClient, senderDevice, accountsDB, asAPI, userAPI, nil, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).WithField("userID", userID).Error("failed to send server notice for consent to user")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sentMessages++
|
||||||
|
res := &userapi.UpdatePolicyVersionResponse{}
|
||||||
|
if err = userAPI.PerformUpdatePolicyVersion(context.Background(), &userapi.UpdatePolicyVersionRequest{
|
||||||
|
PolicyVersion: consentOpts.Version,
|
||||||
|
LocalPart: userID,
|
||||||
|
ServerNoticeUpdate: true,
|
||||||
|
}, res); err != nil {
|
||||||
|
logrus.WithError(err).WithField("userID", userID).Error("failed to update policy version")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logrus.Infof("Send messages to %d users", sentMessages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildConsentURI(cfgClient *config.ClientAPI, userID string) (string, error) {
|
||||||
|
consentOpts := cfgClient.Matrix.UserConsentOptions
|
||||||
|
|
||||||
|
mac := hmac.New(sha256.New, []byte(consentOpts.FormSecret))
|
||||||
|
_, err := mac.Write([]byte(userID))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
userMAC := mac.Sum(nil)
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/_matrix/consent?u=%s&h=%s&v=%s", consentOpts.BaseURL, userID, userMAC, consentOpts.Version), nil
|
||||||
|
}
|
||||||
|
|
||||||
func validHMAC(username, userHMAC, secret string) (bool, error) {
|
func validHMAC(username, userHMAC, secret string) (bool, error) {
|
||||||
mac := hmac.New(sha256.New, []byte(secret))
|
mac := hmac.New(sha256.New, []byte(secret))
|
||||||
_, err := mac.Write([]byte(username))
|
_, err := mac.Write([]byte(username))
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
@ -93,7 +94,7 @@ func PutTag(
|
||||||
}
|
}
|
||||||
tagContent.Tags[tag] = properties
|
tagContent.Tags[tag] = properties
|
||||||
|
|
||||||
if err = saveTagData(req, userID, roomID, userAPI, tagContent); err != nil {
|
if err = saveTagData(req.Context(), userID, roomID, userAPI, tagContent); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("saveTagData failed")
|
util.GetLogger(req.Context()).WithError(err).Error("saveTagData failed")
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
@ -145,7 +146,7 @@ func DeleteTag(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = saveTagData(req, userID, roomID, userAPI, tagContent); err != nil {
|
if err = saveTagData(req.Context(), userID, roomID, userAPI, tagContent); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("saveTagData failed")
|
util.GetLogger(req.Context()).WithError(err).Error("saveTagData failed")
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
@ -191,7 +192,7 @@ func obtainSavedTags(
|
||||||
|
|
||||||
// saveTagData saves the provided tag data into the database
|
// saveTagData saves the provided tag data into the database
|
||||||
func saveTagData(
|
func saveTagData(
|
||||||
req *http.Request,
|
context context.Context,
|
||||||
userID string,
|
userID string,
|
||||||
roomID string,
|
roomID string,
|
||||||
userAPI api.UserInternalAPI,
|
userAPI api.UserInternalAPI,
|
||||||
|
@ -208,5 +209,5 @@ func saveTagData(
|
||||||
AccountData: json.RawMessage(newTagData),
|
AccountData: json.RawMessage(newTagData),
|
||||||
}
|
}
|
||||||
dataRes := api.InputAccountDataResponse{}
|
dataRes := api.InputAccountDataResponse{}
|
||||||
return userAPI.InputAccountData(req.Context(), &dataReq, &dataRes)
|
return userAPI.InputAccountData(context, &dataReq, &dataRes)
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,9 +119,13 @@ func Setup(
|
||||||
}
|
}
|
||||||
|
|
||||||
// server notifications
|
// server notifications
|
||||||
|
var (
|
||||||
|
serverNotificationSender *userapi.Device
|
||||||
|
err error
|
||||||
|
)
|
||||||
if cfg.Matrix.ServerNotices.Enabled {
|
if cfg.Matrix.ServerNotices.Enabled {
|
||||||
logrus.Info("Enabling server notices at /_synapse/admin/v1/send_server_notice")
|
logrus.Info("Enabling server notices at /_synapse/admin/v1/send_server_notice")
|
||||||
serverNotificationSender, err := getSenderDevice(context.Background(), userAPI, accountDB, cfg)
|
serverNotificationSender, err = getSenderDevice(context.Background(), userAPI, accountDB, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("unable to get account for sending sending server notices")
|
logrus.WithError(err).Fatal("unable to get account for sending sending server notices")
|
||||||
}
|
}
|
||||||
|
@ -172,6 +176,12 @@ func Setup(
|
||||||
|
|
||||||
// unspecced consent tracking
|
// unspecced consent tracking
|
||||||
if cfg.Matrix.UserConsentOptions.Enabled {
|
if cfg.Matrix.UserConsentOptions.Enabled {
|
||||||
|
if !cfg.Matrix.ServerNotices.Enabled {
|
||||||
|
logrus.Warnf("Consent tracking is enabled, but server notes are not. No server notice will be sent to users")
|
||||||
|
} else {
|
||||||
|
// start a new go routine to send messages about consent
|
||||||
|
go sendServerNoticeForConsent(userAPI, rsAPI, &cfg.Matrix.ServerNotices, cfg, serverNotificationSender, accountDB, asAPI)
|
||||||
|
}
|
||||||
consentAPIMux.Handle("/consent",
|
consentAPIMux.Handle("/consent",
|
||||||
httputil.MakeHTMLAPI("consent", func(writer http.ResponseWriter, request *http.Request) *util.JSONResponse {
|
httputil.MakeHTMLAPI("consent", func(writer http.ResponseWriter, request *http.Request) *util.JSONResponse {
|
||||||
return consent(writer, request, userAPI, cfg)
|
return consent(writer, request, userAPI, cfg)
|
||||||
|
|
|
@ -85,41 +85,38 @@ func SendServerNotice(
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
res, _ := sendServerNotice(ctx, r, rsAPI, cfgNotices, cfgClient, senderDevice, accountsDB, asAPI, userAPI, txnID, device, txnCache)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendServerNotice(
|
||||||
|
ctx context.Context,
|
||||||
|
serverNoticeRequest sendServerNoticeRequest,
|
||||||
|
rsAPI api.RoomserverInternalAPI,
|
||||||
|
cfgNotices *config.ServerNotices,
|
||||||
|
cfgClient *config.ClientAPI,
|
||||||
|
senderDevice *userapi.Device,
|
||||||
|
accountsDB userdb.Database,
|
||||||
|
asAPI appserviceAPI.AppServiceQueryAPI,
|
||||||
|
userAPI userapi.UserInternalAPI,
|
||||||
|
txnID *string,
|
||||||
|
device *userapi.Device,
|
||||||
|
txnCache *transactions.Cache,
|
||||||
|
) (util.JSONResponse, error) {
|
||||||
|
|
||||||
// check that all required fields are set
|
// check that all required fields are set
|
||||||
if !r.valid() {
|
if !serverNoticeRequest.valid() {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("Invalid request"),
|
JSON: jsonerror.BadJSON("Invalid request"),
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// get rooms for specified user
|
// get rooms for specified user
|
||||||
allUserRooms := []string{}
|
allUserRooms, err := getAllUserRooms(ctx, rsAPI, serverNoticeRequest.UserID)
|
||||||
userRooms := api.QueryRoomsForUserResponse{}
|
if err != nil {
|
||||||
if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{
|
return util.ErrorResponse(err), nil
|
||||||
UserID: r.UserID,
|
|
||||||
WantMembership: "join",
|
|
||||||
}, &userRooms); err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
}
|
||||||
allUserRooms = append(allUserRooms, userRooms.RoomIDs...)
|
|
||||||
// get invites for specified user
|
|
||||||
if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{
|
|
||||||
UserID: r.UserID,
|
|
||||||
WantMembership: "invite",
|
|
||||||
}, &userRooms); err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
allUserRooms = append(allUserRooms, userRooms.RoomIDs...)
|
|
||||||
// get left rooms for specified user
|
|
||||||
if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{
|
|
||||||
UserID: r.UserID,
|
|
||||||
WantMembership: "leave",
|
|
||||||
}, &userRooms); err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
allUserRooms = append(allUserRooms, userRooms.RoomIDs...)
|
|
||||||
|
|
||||||
// get rooms of the sender
|
// get rooms of the sender
|
||||||
senderUserID := fmt.Sprintf("@%s:%s", cfgNotices.LocalPart, cfgClient.Matrix.ServerName)
|
senderUserID := fmt.Sprintf("@%s:%s", cfgNotices.LocalPart, cfgClient.Matrix.ServerName)
|
||||||
|
@ -128,7 +125,7 @@ func SendServerNotice(
|
||||||
UserID: senderUserID,
|
UserID: senderUserID,
|
||||||
WantMembership: "join",
|
WantMembership: "join",
|
||||||
}, &senderRooms); err != nil {
|
}, &senderRooms); err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if we have rooms in common
|
// check if we have rooms in common
|
||||||
|
@ -142,7 +139,7 @@ func SendServerNotice(
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(commonRooms) > 1 {
|
if len(commonRooms) > 1 {
|
||||||
return util.ErrorResponse(fmt.Errorf("expected to find one room, but got %d", len(commonRooms)))
|
return util.ErrorResponse(fmt.Errorf("expected to find one room, but got %d", len(commonRooms))), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -153,19 +150,19 @@ func SendServerNotice(
|
||||||
// create a new room for the user
|
// create a new room for the user
|
||||||
if len(commonRooms) == 0 {
|
if len(commonRooms) == 0 {
|
||||||
powerLevelContent := eventutil.InitialPowerLevelsContent(senderUserID)
|
powerLevelContent := eventutil.InitialPowerLevelsContent(senderUserID)
|
||||||
powerLevelContent.Users[r.UserID] = -10 // taken from Synapse
|
powerLevelContent.Users[serverNoticeRequest.UserID] = -10 // taken from Synapse
|
||||||
pl, err := json.Marshal(powerLevelContent)
|
pl, err := json.Marshal(powerLevelContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err), nil
|
||||||
}
|
}
|
||||||
createContent := map[string]interface{}{}
|
createContent := map[string]interface{}{}
|
||||||
createContent["m.federate"] = false
|
createContent["m.federate"] = false
|
||||||
cc, err := json.Marshal(createContent)
|
cc, err := json.Marshal(createContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err), nil
|
||||||
}
|
}
|
||||||
crReq := createRoomRequest{
|
crReq := createRoomRequest{
|
||||||
Invite: []string{r.UserID},
|
Invite: []string{serverNoticeRequest.UserID},
|
||||||
Name: cfgNotices.RoomName,
|
Name: cfgNotices.RoomName,
|
||||||
Visibility: "private",
|
Visibility: "private",
|
||||||
Preset: presetPrivateChat,
|
Preset: presetPrivateChat,
|
||||||
|
@ -187,36 +184,35 @@ func SendServerNotice(
|
||||||
Order: 1.0,
|
Order: 1.0,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
if err = saveTagData(req, r.UserID, roomID, userAPI, serverAlertTag); err != nil {
|
if err = saveTagData(ctx, serverNoticeRequest.UserID, roomID, userAPI, serverAlertTag); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("saveTagData failed")
|
util.GetLogger(ctx).WithError(err).Error("saveTagData failed")
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// if we didn't get a createRoomResponse, we probably received an error, so return that.
|
// if we didn't get a createRoomResponse, we probably received an error, so return that.
|
||||||
return roomRes
|
return roomRes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// we've found a room in common, check the membership
|
|
||||||
roomID = commonRooms[0]
|
roomID = commonRooms[0]
|
||||||
// re-invite the user
|
// re-invite the user
|
||||||
res, err := sendInvite(ctx, accountsDB, senderDevice, roomID, r.UserID, "Server notice room", cfgClient, rsAPI, asAPI, time.Now())
|
res, err := sendInvite(ctx, accountsDB, senderDevice, roomID, serverNoticeRequest.UserID, "Server notice room", cfgClient, rsAPI, asAPI, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res
|
return res, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startedGeneratingEvent := time.Now()
|
startedGeneratingEvent := time.Now()
|
||||||
|
|
||||||
request := map[string]interface{}{
|
request := map[string]interface{}{
|
||||||
"body": r.Content.Body,
|
"body": serverNoticeRequest.Content.Body,
|
||||||
"msgtype": r.Content.MsgType,
|
"msgtype": serverNoticeRequest.Content.MsgType,
|
||||||
}
|
}
|
||||||
e, resErr := generateSendEvent(ctx, request, senderDevice, roomID, "m.room.message", nil, cfgClient, rsAPI, time.Now())
|
e, resErr := generateSendEvent(ctx, request, senderDevice, roomID, "m.room.message", nil, cfgClient, rsAPI, time.Now())
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
logrus.Errorf("failed to send message: %+v", resErr)
|
logrus.Errorf("failed to send message: %+v", resErr)
|
||||||
return *resErr
|
return *resErr, nil
|
||||||
}
|
}
|
||||||
timeToGenerateEvent := time.Since(startedGeneratingEvent)
|
timeToGenerateEvent := time.Since(startedGeneratingEvent)
|
||||||
|
|
||||||
|
@ -243,7 +239,7 @@ func SendServerNotice(
|
||||||
false,
|
false,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("SendEvents failed")
|
util.GetLogger(ctx).WithError(err).Error("SendEvents failed")
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError(), nil
|
||||||
}
|
}
|
||||||
util.GetLogger(ctx).WithFields(logrus.Fields{
|
util.GetLogger(ctx).WithFields(logrus.Fields{
|
||||||
"event_id": e.EventID(),
|
"event_id": e.EventID(),
|
||||||
|
@ -266,7 +262,36 @@ func SendServerNotice(
|
||||||
sendEventDuration.With(prometheus.Labels{"action": "build"}).Observe(float64(timeToGenerateEvent.Milliseconds()))
|
sendEventDuration.With(prometheus.Labels{"action": "build"}).Observe(float64(timeToGenerateEvent.Milliseconds()))
|
||||||
sendEventDuration.With(prometheus.Labels{"action": "submit"}).Observe(float64(timeToSubmitEvent.Milliseconds()))
|
sendEventDuration.With(prometheus.Labels{"action": "submit"}).Observe(float64(timeToSubmitEvent.Milliseconds()))
|
||||||
|
|
||||||
return res
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllUserRooms(ctx context.Context, rsAPI api.RoomserverInternalAPI, userID string) ([]string, error) {
|
||||||
|
allUserRooms := []string{}
|
||||||
|
userRooms := api.QueryRoomsForUserResponse{}
|
||||||
|
if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{
|
||||||
|
UserID: userID,
|
||||||
|
WantMembership: "join",
|
||||||
|
}, &userRooms); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
allUserRooms = append(allUserRooms, userRooms.RoomIDs...)
|
||||||
|
// get invites for specified user
|
||||||
|
if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{
|
||||||
|
UserID: userID,
|
||||||
|
WantMembership: "invite",
|
||||||
|
}, &userRooms); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
allUserRooms = append(allUserRooms, userRooms.RoomIDs...)
|
||||||
|
// get left rooms for specified user
|
||||||
|
if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{
|
||||||
|
UserID: userID,
|
||||||
|
WantMembership: "leave",
|
||||||
|
}, &userRooms); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
allUserRooms = append(allUserRooms, userRooms.RoomIDs...)
|
||||||
|
return allUserRooms, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r sendServerNoticeRequest) valid() (ok bool) {
|
func (r sendServerNoticeRequest) valid() (ok bool) {
|
||||||
|
|
Loading…
Reference in a new issue