Skip to content
Snippets Groups Projects
Commit cacbf92c authored by Jono Wenger's avatar Jono Wenger
Browse files

Fix indexDb worker for changes in release

parent a2c658ab
Branches
Tags
2 merge requests!67fix for latest client release,!52XX-4382 / Move indexedDb databases to web workers
......@@ -25,38 +25,24 @@ require (
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
github.com/elliotchance/orderedmap v1.4.0 // indirect
github.com/forPelevin/gomoji v1.1.8 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/gobwas/ws v1.1.0 // indirect
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/improbable-eng/grpc-web v0.15.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.11.7 // indirect
github.com/klauspost/cpuid/v2 v2.1.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/oasisprotocol/curve25519-voi v0.0.0-20221003100820-41fad3beba17 // indirect
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.2 // indirect
github.com/pkg/profile v1.6.0 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/rs/cors v1.8.2 // indirect
github.com/sethvargo/go-diceware v0.3.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/soheilhy/cmux v0.1.5 // indirect
github.com/spf13/afero v1.9.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.5.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.12.0 // indirect
github.com/subosito/gotenv v1.4.0 // indirect
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
github.com/ttacon/libphonenumber v1.2.1 // indirect
github.com/tyler-smith/go-bip39 v1.1.0 // indirect
......@@ -77,9 +63,6 @@ require (
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
google.golang.org/grpc v1.49.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
nhooyr.io/websocket v1.8.7 // indirect
src.agwa.name/tlshacks v0.0.0-20220518131152-d2c6f4e2b780 // indirect
)
This diff is collapsed.
......@@ -10,6 +10,7 @@
package main
import (
"crypto/ed25519"
"encoding/json"
"github.com/pkg/errors"
jww "github.com/spf13/jwalterweatherman"
......@@ -70,17 +71,18 @@ func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) {
}
m.model, err = NewWASMEventModel(msg.Path, encryption,
m.messageReceivedCallback, m.storeDatabaseName, m.storeEncryptionStatus)
m.messageReceivedCallback, m.deletedMessageCallback, m.mutedUserCallback,
m.storeDatabaseName, m.storeEncryptionStatus)
if err != nil {
return []byte(err.Error()), nil
}
return []byte{}, nil
}
// messageReceivedCallback sends calls to the MessageReceivedCallback in the
// main thread.
// messageReceivedCallback sends calls to the channels.MessageReceivedCallback
// in the main thread.
//
// storeEncryptionStatus adhere to the MessageReceivedCallback type.
// storeEncryptionStatus adhere to the channels.MessageReceivedCallback type.
func (m *manager) messageReceivedCallback(
uuid uint64, channelID *id.ID, update bool) {
// Package parameters for sending
......@@ -91,8 +93,7 @@ func (m *manager) messageReceivedCallback(
}
data, err := json.Marshal(msg)
if err != nil {
jww.ERROR.Printf(
"Could not JSON marshal MessageReceivedCallbackMessage: %+v", err)
jww.ERROR.Printf("Could not JSON marshal %T: %+v", msg, err)
return
}
......@@ -100,6 +101,36 @@ func (m *manager) messageReceivedCallback(
m.mh.SendMessage(wChannels.MessageReceivedCallbackTag, data)
}
// deletedMessageCallback sends calls to the channels.DeletedMessageCallback in
// the main thread.
//
// storeEncryptionStatus adhere to the channels.MessageReceivedCallback type.
func (m *manager) deletedMessageCallback(messageID message.ID) {
m.mh.SendMessage(wChannels.DeletedMessageCallbackTag, messageID.Marshal())
}
// mutedUserCallback sends calls to the channels.MutedUserCallback in the main
// thread.
//
// storeEncryptionStatus adhere to the channels.MessageReceivedCallback type.
func (m *manager) mutedUserCallback(
channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) {
// Package parameters for sending
msg := &wChannels.MuteUserMessage{
ChannelID: channelID,
PubKey: pubKey,
Unmute: unmute,
}
data, err := json.Marshal(msg)
if err != nil {
jww.ERROR.Printf("Could not JSON marshal %T: %+v", msg, err)
return
}
// Send it to the main thread
m.mh.SendMessage(wChannels.MutedUserCallbackTag, data)
}
// storeDatabaseName sends the database name to the main thread and waits for
// the response. This function mocks the behavior of storage.StoreIndexedDb.
//
......@@ -397,3 +428,18 @@ func (m *manager) deleteMessageCB(data []byte) ([]byte, error) {
return nil, nil
}
// muteUserCB is the callback for wasmModel.MuteUser. Always returns nil;
// meaning, no response is supplied (or expected).
func (m *manager) muteUserCB(data []byte) ([]byte, error) {
var msg wChannels.MuteUserMessage
err := json.Unmarshal(data, &msg)
if err != nil {
return nil, errors.Errorf(
"failed to JSON unmarshal %T from main thread: %+v", msg, err)
}
m.model.MuteUser(msg.ChannelID, msg.PubKey, msg.Unmute)
return nil, nil
}
......@@ -28,6 +28,7 @@ import (
cryptoChannel "gitlab.com/elixxir/crypto/channel"
"gitlab.com/elixxir/crypto/message"
"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
"gitlab.com/elixxir/xxdk-wasm/utils"
"gitlab.com/xx_network/primitives/id"
)
......@@ -38,9 +39,9 @@ import (
type wasmModel struct {
db *idb.Database
cipher cryptoChannel.Cipher
receivedMessageCB MessageReceivedCallback
deletedMessageCB DeletedMessageCallback
mutedUserCB MutedUserCallback
receivedMessageCB wChannels.MessageReceivedCallback
deletedMessageCB wChannels.DeletedMessageCallback
mutedUserCB wChannels.MutedUserCallback
updateMux sync.Mutex
}
......@@ -81,8 +82,7 @@ func (w *wasmModel) LeaveChannel(channelID *id.ID) {
parentErr := errors.New("failed to LeaveChannel")
// Delete the channel from storage
err := indexedDb.Delete(w.db, channelsStoreName,
js.ValueOf(channelID.String()))
err := impl.Delete(w.db, channelsStoreName, js.ValueOf(channelID.String()))
if err != nil {
jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
"Unable to delete Channel: %+v", err))
......
......@@ -10,16 +10,16 @@
package main
import (
"crypto/ed25519"
"syscall/js"
"github.com/hack-pad/go-indexeddb/idb"
"github.com/pkg/errors"
jww "github.com/spf13/jwalterweatherman"
"gitlab.com/elixxir/client/v4/channels"
cryptoChannel "gitlab.com/elixxir/crypto/channel"
"gitlab.com/elixxir/crypto/message"
"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
"gitlab.com/xx_network/primitives/id"
"syscall/js"
wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
)
const (
......@@ -32,19 +32,6 @@ const (
currentVersion uint = 1
)
// MessageReceivedCallback is called any time a message is received or updated.
//
// update is true if the row is old and was edited.
type MessageReceivedCallback func(uuid uint64, channelID *id.ID, update bool)
// DeletedMessageCallback is called any time a message is deleted.
type DeletedMessageCallback func(messageID message.ID)
// MutedUserCallback is called any time a user is muted or unmuted. unmute is
// true if the user has been unmuted and false if they have been muted.
type MutedUserCallback func(
channelID *id.ID, pubKey ed25519.PublicKey, unmute bool)
// storeDatabaseNameFn matches storage.StoreIndexedDb so that the data can be
// sent between the worker and main thread.
type storeDatabaseNameFn func(databaseName string) error
......@@ -57,9 +44,10 @@ type storeEncryptionStatusFn func(
// NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel.
// The name should be a base64 encoding of the users public key.
func NewWASMEventModel(path string, encryption cryptoChannel.Cipher,
messageReceivedCB MessageReceivedCallback,
deletedMessageCB DeletedMessageCallback,
mutedUserCB MutedUserCallback, storeDatabaseName storeDatabaseNameFn,
messageReceivedCB wChannels.MessageReceivedCallback,
deletedMessageCB wChannels.DeletedMessageCallback,
mutedUserCB wChannels.MutedUserCallback,
storeDatabaseName storeDatabaseNameFn,
storeEncryptionStatus storeEncryptionStatusFn) (channels.EventModel, error) {
databaseName := path + databaseSuffix
return newWASMModel(databaseName, encryption, messageReceivedCB,
......@@ -68,8 +56,9 @@ func NewWASMEventModel(path string, encryption cryptoChannel.Cipher,
// newWASMModel creates the given [idb.Database] and returns a wasmModel.
func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
messageReceivedCB MessageReceivedCallback,
deletedMessageCB DeletedMessageCallback, mutedUserCB MutedUserCallback,
messageReceivedCB wChannels.MessageReceivedCallback,
deletedMessageCB wChannels.DeletedMessageCallback,
mutedUserCB wChannels.MutedUserCallback,
storeDatabaseName storeDatabaseNameFn,
storeEncryptionStatus storeEncryptionStatusFn) (*wasmModel, error) {
// Attempt to open database object
......
......@@ -36,7 +36,8 @@ type wasmModel struct {
func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) {
data, err := json.Marshal(channel)
if err != nil {
jww.ERROR.Printf("Could not JSON marshal broadcast.Channel: %+v", err)
jww.ERROR.Printf(
"[CH] Could not JSON marshal broadcast.Channel: %+v", err)
return
}
......@@ -76,7 +77,7 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID,
data, err := json.Marshal(msg)
if err != nil {
jww.ERROR.Printf(
"Could not JSON marshal payload for ReceiveMessage: %+v", err)
"[CH] Could not JSON marshal payload for ReceiveMessage: %+v", err)
return 0
}
......@@ -85,8 +86,8 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID,
var uuid uint64
err = json.Unmarshal(data, &uuid)
if err != nil {
jww.ERROR.Printf(
"Could not JSON unmarshal response to ReceiveMessage: %+v", err)
jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+
"ReceiveMessage: %+v", err)
uuidChan <- 0
}
uuidChan <- uuid
......@@ -96,8 +97,8 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID,
case uuid := <-uuidChan:
return uuid
case <-time.After(worker.ResponseTimeout):
jww.ERROR.Printf("Timed out after %s waiting for response from the "+
"worker about ReceiveMessage", worker.ResponseTimeout)
jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+
"the worker about ReceiveMessage", worker.ResponseTimeout)
}
return 0
......@@ -144,7 +145,7 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID,
data, err := json.Marshal(msg)
if err != nil {
jww.ERROR.Printf(
"Could not JSON marshal payload for ReceiveReply: %+v", err)
"[CH] Could not JSON marshal payload for ReceiveReply: %+v", err)
return 0
}
......@@ -153,8 +154,8 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID,
var uuid uint64
err = json.Unmarshal(data, &uuid)
if err != nil {
jww.ERROR.Printf(
"Could not JSON unmarshal response to ReceiveReply: %+v", err)
jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+
"ReceiveReply: %+v", err)
uuidChan <- 0
}
uuidChan <- uuid
......@@ -164,8 +165,8 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID,
case uuid := <-uuidChan:
return uuid
case <-time.After(worker.ResponseTimeout):
jww.ERROR.Printf("Timed out after %s waiting for response from the "+
"worker about ReceiveReply", worker.ResponseTimeout)
jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+
"the worker about ReceiveReply", worker.ResponseTimeout)
}
return 0
......@@ -206,7 +207,7 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID,
data, err := json.Marshal(msg)
if err != nil {
jww.ERROR.Printf(
"Could not JSON marshal payload for ReceiveReaction: %+v", err)
"[CH] Could not JSON marshal payload for ReceiveReaction: %+v", err)
return 0
}
......@@ -215,8 +216,8 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID,
var uuid uint64
err = json.Unmarshal(data, &uuid)
if err != nil {
jww.ERROR.Printf(
"Could not JSON unmarshal response to ReceiveReaction: %+v", err)
jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+
"ReceiveReaction: %+v", err)
uuidChan <- 0
}
uuidChan <- uuid
......@@ -226,8 +227,8 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID,
case uuid := <-uuidChan:
return uuid
case <-time.After(worker.ResponseTimeout):
jww.ERROR.Printf("Timed out after %s waiting for response from the "+
"worker about ReceiveReply", worker.ResponseTimeout)
jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+
"the worker about ReceiveReply", worker.ResponseTimeout)
}
return 0
......@@ -294,7 +295,7 @@ func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID,
data, err := json.Marshal(msg)
if err != nil {
jww.ERROR.Printf(
"Could not JSON marshal payload for UpdateFromUUID: %+v", err)
"[CH] Could not JSON marshal payload for UpdateFromUUID: %+v", err)
return
}
......@@ -338,8 +339,8 @@ func (w *wasmModel) UpdateFromMessageID(messageID message.ID,
data, err := json.Marshal(msg)
if err != nil {
jww.ERROR.Printf(
"Could not JSON marshal payload for UpdateFromMessageID: %+v", err)
jww.ERROR.Printf("[CH] Could not JSON marshal payload for "+
"UpdateFromMessageID: %+v", err)
return 0
}
......@@ -349,7 +350,7 @@ func (w *wasmModel) UpdateFromMessageID(messageID message.ID,
var uuid uint64
err = json.Unmarshal(data, &uuid)
if err != nil {
jww.ERROR.Printf("Could not JSON unmarshal response to "+
jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+
"UpdateFromMessageID: %+v", err)
uuidChan <- 0
}
......@@ -360,8 +361,8 @@ func (w *wasmModel) UpdateFromMessageID(messageID message.ID,
case uuid := <-uuidChan:
return uuid
case <-time.After(worker.ResponseTimeout):
jww.ERROR.Printf("Timed out after %s waiting for response from the "+
"worker about UpdateFromMessageID", worker.ResponseTimeout)
jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+
"the worker about UpdateFromMessageID", worker.ResponseTimeout)
}
return 0
......@@ -383,8 +384,8 @@ func (w *wasmModel) GetMessage(
var msg GetMessageMessage
err := json.Unmarshal(data, &msg)
if err != nil {
jww.ERROR.Printf(
"Could not JSON unmarshal response to GetMessage: %+v", err)
jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+
"GetMessage: %+v", err)
}
msgChan <- msg
})
......@@ -422,3 +423,29 @@ func (w *wasmModel) DeleteMessage(messageID message.ID) error {
"the worker about DeleteMessage", worker.ResponseTimeout)
}
}
// MuteUserMessage is JSON marshalled and sent to the worker for
// [wasmModel.MuteUser].
type MuteUserMessage struct {
ChannelID *id.ID `json:"channelID"`
PubKey ed25519.PublicKey `json:"pubKey"`
Unmute bool `json:"unmute"`
}
// MuteUser is called whenever a user is muted or unmuted.
func (w *wasmModel) MuteUser(
channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) {
msg := MuteUserMessage{
ChannelID: channelID,
PubKey: pubKey,
Unmute: unmute,
}
data, err := json.Marshal(msg)
if err != nil {
jww.ERROR.Printf("[CH] Could not marshal MuteUserMessage: %+v", err)
return
}
w.wm.SendMessage(MuteUserTag, data, nil)
}
......@@ -10,6 +10,7 @@
package channels
import (
"crypto/ed25519"
"encoding/json"
"github.com/pkg/errors"
"time"
......@@ -18,6 +19,7 @@ import (
"gitlab.com/elixxir/client/v4/channels"
cryptoChannel "gitlab.com/elixxir/crypto/channel"
"gitlab.com/elixxir/crypto/message"
"gitlab.com/elixxir/xxdk-wasm/storage"
"gitlab.com/elixxir/xxdk-wasm/worker"
"gitlab.com/xx_network/primitives/id"
......@@ -28,13 +30,24 @@ import (
// update is true if the row is old and was edited.
type MessageReceivedCallback func(uuid uint64, channelID *id.ID, update bool)
// DeletedMessageCallback is called any time a message is deleted.
type DeletedMessageCallback func(messageID message.ID)
// MutedUserCallback is called any time a user is muted or unmuted. unmute is
// true if the user has been unmuted and false if they have been muted.
type MutedUserCallback func(
channelID *id.ID, pubKey ed25519.PublicKey, unmute bool)
// NewWASMEventModelBuilder returns an EventModelBuilder which allows
// the channel manager to define the path but the callback is the same
// across the board.
func NewWASMEventModelBuilder(wasmJsPath string, encryption cryptoChannel.Cipher,
cb MessageReceivedCallback) channels.EventModelBuilder {
func NewWASMEventModelBuilder(wasmJsPath string,
encryption cryptoChannel.Cipher, messageReceivedCB MessageReceivedCallback,
deletedMessageCB DeletedMessageCallback,
mutedUserCB MutedUserCallback) channels.EventModelBuilder {
fn := func(path string) (channels.EventModel, error) {
return NewWASMEventModel(path, wasmJsPath, encryption, cb)
return NewWASMEventModel(path, wasmJsPath, encryption,
messageReceivedCB, deletedMessageCB, mutedUserCB)
}
return fn
}
......@@ -49,7 +62,9 @@ type NewWASMEventModelMessage struct {
// NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel.
// The name should be a base64 encoding of the users public key.
func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher,
cb MessageReceivedCallback) (channels.EventModel, error) {
messageReceivedCB MessageReceivedCallback,
deletedMessageCB DeletedMessageCallback, mutedUserCB MutedUserCallback) (
channels.EventModel, error) {
wm, err := worker.NewManager(wasmJsPath, "channelsIndexedDb")
if err != nil {
......@@ -57,8 +72,16 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher,
}
// Register handler to manage messages for the MessageReceivedCallback
wm.RegisterCallback(
MessageReceivedCallbackTag, messageReceivedCallbackHandler(cb))
wm.RegisterCallback(MessageReceivedCallbackTag,
messageReceivedCallbackHandler(messageReceivedCB))
// Register handler to manage messages for the DeletedMessageCallback
wm.RegisterCallback(DeletedMessageCallbackTag,
deletedMessageCallbackHandler(deletedMessageCB))
// Register handler to manage messages for the MutedUserCallback
wm.RegisterCallback(MutedUserCallbackTag,
mutedUserCallbackHandler(mutedUserCB))
// Register handler to manage checking encryption status from local storage
wm.RegisterCallback(EncryptionStatusTag, checkDbEncryptionStatusHandler(wm))
......@@ -113,14 +136,45 @@ func messageReceivedCallbackHandler(cb MessageReceivedCallback) func(data []byte
var msg MessageReceivedCallbackMessage
err := json.Unmarshal(data, &msg)
if err != nil {
jww.ERROR.Printf("Failed to JSON unmarshal "+
"MessageReceivedCallback message from worker: %+v", err)
jww.ERROR.Printf(
"Failed to JSON unmarshal %T from worker: %+v", msg, err)
return
}
cb(msg.UUID, msg.ChannelID, msg.Update)
}
}
// deletedMessageCallbackHandler returns a handler to manage messages for the
// DeletedMessageCallback.
func deletedMessageCallbackHandler(cb DeletedMessageCallback) func(data []byte) {
return func(data []byte) {
messageID, err := message.UnmarshalID(data)
if err != nil {
jww.ERROR.Printf(
"Failed to JSON unmarshal message ID from worker: %+v", err)
}
cb(messageID)
}
}
// mutedUserCallbackHandler returns a handler to manage messages for the
// MutedUserCallback.
func mutedUserCallbackHandler(cb MutedUserCallback) func(data []byte) {
return func(data []byte) {
var msg MuteUserMessage
err := json.Unmarshal(data, &msg)
if err != nil {
jww.ERROR.Printf(
"Failed to JSON unmarshal %T from worker: %+v", msg, err)
return
}
cb(msg.ChannelID, msg.PubKey, msg.Unmute)
}
}
// EncryptionStatusMessage is JSON marshalled and received from the worker when
// the database checks if it is encrypted.
type EncryptionStatusMessage struct {
......
......@@ -16,6 +16,8 @@ import "gitlab.com/elixxir/xxdk-wasm/worker"
const (
NewWASMEventModelTag worker.Tag = "NewWASMEventModel"
MessageReceivedCallbackTag worker.Tag = "MessageReceivedCallback"
DeletedMessageCallbackTag worker.Tag = "DeletedMessageCallback"
MutedUserCallbackTag worker.Tag = "MutedUserCallback"
EncryptionStatusTag worker.Tag = "EncryptionStatus"
StoreDatabaseNameTag worker.Tag = "StoreDatabaseName"
......@@ -28,4 +30,5 @@ const (
UpdateFromMessageIDTag worker.Tag = "UpdateFromMessageID"
GetMessageTag worker.Tag = "GetMessage"
DeleteMessageTag worker.Tag = "DeleteMessage"
MuteUserTag worker.Tag = "MuteUser"
)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment