forked from MirrorHub/mautrix-whatsapp
Update mautrix-go for new bridge status package
This commit is contained in:
parent
aa15fde5da
commit
52072d9650
8 changed files with 57 additions and 54 deletions
|
@ -20,23 +20,23 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"maunium.net/go/mautrix/bridge"
|
||||
"maunium.net/go/mautrix/bridge/status"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
const (
|
||||
WALoggedOut bridge.StateErrorCode = "wa-logged-out"
|
||||
WAMainDeviceGone bridge.StateErrorCode = "wa-main-device-gone"
|
||||
WAUnknownLogout bridge.StateErrorCode = "wa-unknown-logout"
|
||||
WANotConnected bridge.StateErrorCode = "wa-not-connected"
|
||||
WAConnecting bridge.StateErrorCode = "wa-connecting"
|
||||
WAKeepaliveTimeout bridge.StateErrorCode = "wa-keepalive-timeout"
|
||||
WAPhoneOffline bridge.StateErrorCode = "wa-phone-offline"
|
||||
WAConnectionFailed bridge.StateErrorCode = "wa-connection-failed"
|
||||
WALoggedOut status.BridgeStateErrorCode = "wa-logged-out"
|
||||
WAMainDeviceGone status.BridgeStateErrorCode = "wa-main-device-gone"
|
||||
WAUnknownLogout status.BridgeStateErrorCode = "wa-unknown-logout"
|
||||
WANotConnected status.BridgeStateErrorCode = "wa-not-connected"
|
||||
WAConnecting status.BridgeStateErrorCode = "wa-connecting"
|
||||
WAKeepaliveTimeout status.BridgeStateErrorCode = "wa-keepalive-timeout"
|
||||
WAPhoneOffline status.BridgeStateErrorCode = "wa-phone-offline"
|
||||
WAConnectionFailed status.BridgeStateErrorCode = "wa-connection-failed"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bridge.StateHumanErrors.Update(bridge.StateErrorMap{
|
||||
status.BridgeStateHumanErrors.Update(status.BridgeStateErrorMap{
|
||||
WALoggedOut: "You were logged out from another device. Relogin to continue using the bridge.",
|
||||
WAMainDeviceGone: "Your phone was logged out from WhatsApp. Relogin to continue using the bridge.",
|
||||
WAUnknownLogout: "You were logged out for an unknown reason. Relogin to continue using the bridge.",
|
||||
|
@ -68,24 +68,24 @@ func (prov *ProvisioningAPI) BridgeStatePing(w http.ResponseWriter, r *http.Requ
|
|||
}
|
||||
userID := r.URL.Query().Get("user_id")
|
||||
user := prov.bridge.GetUserByMXID(id.UserID(userID))
|
||||
var global bridge.State
|
||||
global.StateEvent = bridge.StateRunning
|
||||
var remote bridge.State
|
||||
var global status.BridgeState
|
||||
global.StateEvent = status.StateRunning
|
||||
var remote status.BridgeState
|
||||
if user.IsConnected() {
|
||||
if user.Client.IsLoggedIn() {
|
||||
remote.StateEvent = bridge.StateConnected
|
||||
remote.StateEvent = status.StateConnected
|
||||
} else if user.Session != nil {
|
||||
remote.StateEvent = bridge.StateConnecting
|
||||
remote.StateEvent = status.StateConnecting
|
||||
remote.Error = WAConnecting
|
||||
} // else: unconfigured
|
||||
} else if user.Session != nil {
|
||||
remote.StateEvent = bridge.StateBadCredentials
|
||||
remote.StateEvent = status.StateBadCredentials
|
||||
remote.Error = WANotConnected
|
||||
} // else: unconfigured
|
||||
global = global.Fill(nil)
|
||||
resp := bridge.GlobalState{
|
||||
resp := status.GlobalBridgeState{
|
||||
BridgeState: global,
|
||||
RemoteStates: map[string]bridge.State{},
|
||||
RemoteStates: map[string]status.BridgeState{},
|
||||
}
|
||||
if len(remote.StateEvent) > 0 {
|
||||
remote = remote.Fill(user)
|
||||
|
|
|
@ -38,6 +38,7 @@ import (
|
|||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/bridge"
|
||||
"maunium.net/go/mautrix/bridge/commands"
|
||||
"maunium.net/go/mautrix/bridge/status"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
|
@ -518,7 +519,7 @@ func fnLogout(ce *WrappedCommandEvent) {
|
|||
return
|
||||
}
|
||||
ce.User.Session = nil
|
||||
ce.User.removeFromJIDMap(bridge.State{StateEvent: bridge.StateLoggedOut})
|
||||
ce.User.removeFromJIDMap(status.BridgeState{StateEvent: status.StateLoggedOut})
|
||||
ce.User.DeleteConnection()
|
||||
ce.User.DeleteSession()
|
||||
ce.Reply("Logged out successfully.")
|
||||
|
@ -575,7 +576,7 @@ func fnDeleteSession(ce *WrappedCommandEvent) {
|
|||
ce.Reply("Nothing to purge: no session information stored and no active connection.")
|
||||
return
|
||||
}
|
||||
ce.User.removeFromJIDMap(bridge.State{StateEvent: bridge.StateLoggedOut})
|
||||
ce.User.removeFromJIDMap(status.BridgeState{StateEvent: status.StateLoggedOut})
|
||||
ce.User.DeleteConnection()
|
||||
ce.User.DeleteSession()
|
||||
ce.Reply("Session information purged")
|
||||
|
@ -600,7 +601,7 @@ func fnReconnect(ce *WrappedCommandEvent) {
|
|||
}
|
||||
} else {
|
||||
ce.User.DeleteConnection()
|
||||
ce.User.BridgeState.Send(bridge.State{StateEvent: bridge.StateTransientDisconnect, Error: WANotConnected})
|
||||
ce.User.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: WANotConnected})
|
||||
ce.User.Connect()
|
||||
ce.Reply("Restarted connection to WhatsApp")
|
||||
}
|
||||
|
@ -622,7 +623,7 @@ func fnDisconnect(ce *WrappedCommandEvent) {
|
|||
}
|
||||
ce.User.DeleteConnection()
|
||||
ce.Reply("Successfully disconnected. Use the `reconnect` command to reconnect.")
|
||||
ce.User.BridgeState.Send(bridge.State{StateEvent: bridge.StateBadCredentials, Error: WANotConnected})
|
||||
ce.User.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Error: WANotConnected})
|
||||
}
|
||||
|
||||
var cmdPing = &commands.FullHandler{
|
||||
|
|
2
go.mod
2
go.mod
|
@ -15,7 +15,7 @@ require (
|
|||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e
|
||||
google.golang.org/protobuf v1.28.0
|
||||
maunium.net/go/maulogger/v2 v2.3.2
|
||||
maunium.net/go/mautrix v0.11.1-0.20220814160431-6f13ea458647
|
||||
maunium.net/go/mautrix v0.11.1-0.20220815133425-ba1fce8fce24
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
4
go.sum
4
go.sum
|
@ -122,5 +122,5 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
|||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||
maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0=
|
||||
maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
|
||||
maunium.net/go/mautrix v0.11.1-0.20220814160431-6f13ea458647 h1:CbSd7DSU7wXVNEhcjikUDuRtrDwegKFQfO4ZEwIxqgg=
|
||||
maunium.net/go/mautrix v0.11.1-0.20220814160431-6f13ea458647/go.mod h1:hHvNi5iKVAiI2MAdAeXHtP4g9BvNEX2rsQpSF/x6Kx4=
|
||||
maunium.net/go/mautrix v0.11.1-0.20220815133425-ba1fce8fce24 h1:40cxOxHwTw+UYdDTV//tHw9bjNMmJ9uH6ye+C1SbOGA=
|
||||
maunium.net/go/mautrix v0.11.1-0.20220815133425-ba1fce8fce24/go.mod h1:hHvNi5iKVAiI2MAdAeXHtP4g9BvNEX2rsQpSF/x6Kx4=
|
||||
|
|
3
main.go
3
main.go
|
@ -36,6 +36,7 @@ import (
|
|||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/bridge"
|
||||
"maunium.net/go/mautrix/bridge/commands"
|
||||
"maunium.net/go/mautrix/bridge/status"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
"maunium.net/go/mautrix/util/configupgrade"
|
||||
|
@ -194,7 +195,7 @@ func (br *WABridge) StartUsers() {
|
|||
go user.Connect()
|
||||
}
|
||||
if !foundAnySessions {
|
||||
br.SendGlobalBridgeState(bridge.State{StateEvent: bridge.StateUnconfigured}.Fill(nil))
|
||||
br.SendGlobalBridgeState(status.BridgeState{StateEvent: status.StateUnconfigured}.Fill(nil))
|
||||
}
|
||||
br.Log.Debugln("Starting custom puppets")
|
||||
for _, loopuppet := range br.GetAllPuppetsWithCustomMXID() {
|
||||
|
|
|
@ -28,7 +28,7 @@ import (
|
|||
"go.mau.fi/whatsmeow"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/bridge"
|
||||
"maunium.net/go/mautrix/bridge/status"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
@ -197,9 +197,9 @@ func (portal *Portal) sendMessageMetrics(evt *event.Event, err error, part strin
|
|||
level = log.LevelDebug
|
||||
}
|
||||
portal.log.Logfln(level, "%s %s %s from %s: %v", part, msgType, evtDescription, evt.Sender, err)
|
||||
reason, status, isCertain, sendNotice, _ := errorToStatusReason(err)
|
||||
checkpointStatus := bridge.ReasonToCheckpointStatus(reason, status)
|
||||
portal.bridge.SendMessageCheckpoint(evt, bridge.MsgStepRemote, err, checkpointStatus, ms.getRetryNum())
|
||||
reason, statusCode, isCertain, sendNotice, _ := errorToStatusReason(err)
|
||||
checkpointStatus := status.ReasonToCheckpointStatus(reason, statusCode)
|
||||
portal.bridge.SendMessageCheckpoint(evt, status.MsgStepRemote, err, checkpointStatus, ms.getRetryNum())
|
||||
if sendNotice {
|
||||
ms.setNoticeID(portal.sendErrorMessage(evt, err, isCertain, ms.getNoticeID()))
|
||||
}
|
||||
|
@ -207,7 +207,7 @@ func (portal *Portal) sendMessageMetrics(evt *event.Event, err error, part strin
|
|||
} else {
|
||||
portal.log.Debugfln("Handled Matrix %s %s", msgType, evtDescription)
|
||||
portal.sendDeliveryReceipt(evt.ID)
|
||||
portal.bridge.SendMessageSuccessCheckpoint(evt, bridge.MsgStepRemote, ms.getRetryNum())
|
||||
portal.bridge.SendMessageSuccessCheckpoint(evt, status.MsgStepRemote, ms.getRetryNum())
|
||||
portal.sendStatusEvent(origEvtID, evt.ID, nil)
|
||||
if prevNotice := ms.popNoticeID(); prevNotice != "" {
|
||||
_, _ = portal.MainIntent().RedactEvent(portal.MXID, prevNotice, mautrix.ReqRedact{
|
||||
|
|
|
@ -39,7 +39,7 @@ import (
|
|||
|
||||
log "maunium.net/go/maulogger/v2"
|
||||
|
||||
"maunium.net/go/mautrix/bridge"
|
||||
"maunium.net/go/mautrix/bridge/status"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
|
@ -151,7 +151,7 @@ func (prov *ProvisioningAPI) DeleteSession(w http.ResponseWriter, r *http.Reques
|
|||
user.DeleteConnection()
|
||||
user.DeleteSession()
|
||||
jsonResponse(w, http.StatusOK, Response{true, "Session information purged"})
|
||||
user.removeFromJIDMap(bridge.State{StateEvent: bridge.StateLoggedOut})
|
||||
user.removeFromJIDMap(status.BridgeState{StateEvent: status.StateLoggedOut})
|
||||
}
|
||||
|
||||
func (prov *ProvisioningAPI) Disconnect(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -165,7 +165,7 @@ func (prov *ProvisioningAPI) Disconnect(w http.ResponseWriter, r *http.Request)
|
|||
}
|
||||
user.DeleteConnection()
|
||||
jsonResponse(w, http.StatusOK, Response{true, "Disconnected from WhatsApp"})
|
||||
user.BridgeState.Send(bridge.State{StateEvent: bridge.StateBadCredentials, Error: WANotConnected})
|
||||
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Error: WANotConnected})
|
||||
}
|
||||
|
||||
func (prov *ProvisioningAPI) Reconnect(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -182,7 +182,7 @@ func (prov *ProvisioningAPI) Reconnect(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
} else {
|
||||
user.DeleteConnection()
|
||||
user.BridgeState.Send(bridge.State{StateEvent: bridge.StateTransientDisconnect, Error: WANotConnected})
|
||||
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: WANotConnected})
|
||||
user.Connect()
|
||||
jsonResponse(w, http.StatusAccepted, Response{true, "Restarted connection to WhatsApp"})
|
||||
}
|
||||
|
@ -577,7 +577,7 @@ func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
user.bridge.Metrics.TrackConnectionState(user.JID, false)
|
||||
user.removeFromJIDMap(bridge.State{StateEvent: bridge.StateLoggedOut})
|
||||
user.removeFromJIDMap(status.BridgeState{StateEvent: status.StateLoggedOut})
|
||||
user.DeleteSession()
|
||||
jsonResponse(w, http.StatusOK, Response{true, "Logged out successfully."})
|
||||
}
|
||||
|
|
37
user.go
37
user.go
|
@ -38,6 +38,7 @@ import (
|
|||
"maunium.net/go/mautrix/appservice"
|
||||
"maunium.net/go/mautrix/bridge"
|
||||
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
||||
"maunium.net/go/mautrix/bridge/status"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/format"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
@ -161,7 +162,7 @@ func (user *User) addToJIDMap() {
|
|||
user.bridge.usersLock.Unlock()
|
||||
}
|
||||
|
||||
func (user *User) removeFromJIDMap(state bridge.State) {
|
||||
func (user *User) removeFromJIDMap(state status.BridgeState) {
|
||||
user.bridge.usersLock.Lock()
|
||||
jidUser, ok := user.bridge.usersByUsername[user.JID.User]
|
||||
if ok && user == jidUser {
|
||||
|
@ -533,13 +534,13 @@ func (user *User) Connect() bool {
|
|||
return false
|
||||
}
|
||||
user.log.Debugln("Connecting to WhatsApp")
|
||||
user.BridgeState.Send(bridge.State{StateEvent: bridge.StateConnecting, Error: WAConnecting})
|
||||
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting, Error: WAConnecting})
|
||||
user.createClient(user.Session)
|
||||
err := user.Client.Connect()
|
||||
if err != nil {
|
||||
user.log.Warnln("Error connecting to WhatsApp:", err)
|
||||
user.BridgeState.Send(bridge.State{
|
||||
StateEvent: bridge.StateUnknownError,
|
||||
user.BridgeState.Send(status.BridgeState{
|
||||
StateEvent: status.StateUnknownError,
|
||||
Error: WAConnectionFailed,
|
||||
Info: map[string]interface{}{
|
||||
"go_error": err.Error(),
|
||||
|
@ -708,7 +709,7 @@ func (user *User) phoneSeen(ts time.Time) {
|
|||
} else if !user.PhoneRecentlySeen(false) {
|
||||
if user.BridgeState.GetPrev().Error == WAPhoneOffline && user.IsConnected() {
|
||||
user.log.Debugfln("Saw phone after current bridge state said it has been offline, switching state back to connected")
|
||||
go user.BridgeState.Send(bridge.State{StateEvent: bridge.StateConnected})
|
||||
go user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
|
||||
} else {
|
||||
user.log.Debugfln("Saw phone after current bridge state said it has been offline, not sending new bridge state (prev: %s, connected: %t)", user.BridgeState.GetPrev().Error, user.IsConnected())
|
||||
}
|
||||
|
@ -762,19 +763,19 @@ func (user *User) HandleEvent(event interface{}) {
|
|||
}
|
||||
case *events.OfflineSyncPreview:
|
||||
user.log.Infofln("Server says it's going to send %d messages and %d receipts that were missed during downtime", v.Messages, v.Receipts)
|
||||
go user.BridgeState.Send(bridge.State{
|
||||
StateEvent: bridge.StateBackfilling,
|
||||
go user.BridgeState.Send(status.BridgeState{
|
||||
StateEvent: status.StateBackfilling,
|
||||
Message: fmt.Sprintf("backfilling %d messages and %d receipts", v.Messages, v.Receipts),
|
||||
})
|
||||
case *events.OfflineSyncCompleted:
|
||||
if !user.PhoneRecentlySeen(true) {
|
||||
user.log.Infofln("Offline sync completed, but phone last seen date is still %s - sending phone offline bridge status", user.PhoneLastSeen)
|
||||
go user.BridgeState.Send(bridge.State{StateEvent: bridge.StateTransientDisconnect, Error: WAPhoneOffline})
|
||||
go user.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: WAPhoneOffline})
|
||||
} else {
|
||||
if user.BridgeState.GetPrev().StateEvent == bridge.StateBackfilling {
|
||||
if user.BridgeState.GetPrev().StateEvent == status.StateBackfilling {
|
||||
user.log.Infoln("Offline sync completed")
|
||||
}
|
||||
go user.BridgeState.Send(bridge.State{StateEvent: bridge.StateConnected})
|
||||
go user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
|
||||
}
|
||||
case *events.AppStateSyncComplete:
|
||||
if len(user.Client.Store.PushName) > 0 && v.Name == appstate.WAPatchCriticalBlock {
|
||||
|
@ -812,23 +813,23 @@ func (user *User) HandleEvent(event interface{}) {
|
|||
} else {
|
||||
message = "Unknown stream error"
|
||||
}
|
||||
go user.BridgeState.Send(bridge.State{StateEvent: bridge.StateUnknownError, Message: message})
|
||||
go user.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Message: message})
|
||||
user.bridge.Metrics.TrackConnectionState(user.JID, false)
|
||||
case *events.ConnectFailure:
|
||||
go user.BridgeState.Send(bridge.State{StateEvent: bridge.StateUnknownError, Message: fmt.Sprintf("Unknown connection failure: %s", v.Reason)})
|
||||
go user.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Message: fmt.Sprintf("Unknown connection failure: %s", v.Reason)})
|
||||
user.bridge.Metrics.TrackConnectionState(user.JID, false)
|
||||
case *events.ClientOutdated:
|
||||
user.log.Errorfln("Got a client outdated connect failure. The bridge is likely out of date, please update immediately.")
|
||||
go user.BridgeState.Send(bridge.State{StateEvent: bridge.StateUnknownError, Message: "Connect failure: 405 client outdated"})
|
||||
go user.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Message: "Connect failure: 405 client outdated"})
|
||||
user.bridge.Metrics.TrackConnectionState(user.JID, false)
|
||||
case *events.TemporaryBan:
|
||||
go user.BridgeState.Send(bridge.State{StateEvent: bridge.StateBadCredentials, Message: v.String()})
|
||||
go user.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: v.String()})
|
||||
user.bridge.Metrics.TrackConnectionState(user.JID, false)
|
||||
case *events.Disconnected:
|
||||
// Don't send the normal transient disconnect state if we're already in a different transient disconnect state.
|
||||
// TODO remove this if/when the phone offline state is moved to a sub-state of CONNECTED
|
||||
if user.BridgeState.GetPrev().Error != WAPhoneOffline && user.PhoneRecentlySeen(false) {
|
||||
go user.BridgeState.Send(bridge.State{StateEvent: bridge.StateTransientDisconnect, Message: "Disconnected from WhatsApp. Trying to reconnect."})
|
||||
go user.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Message: "Disconnected from WhatsApp. Trying to reconnect."})
|
||||
}
|
||||
user.bridge.Metrics.TrackConnectionState(user.JID, false)
|
||||
case *events.Contact:
|
||||
|
@ -913,10 +914,10 @@ func (user *User) HandleEvent(event interface{}) {
|
|||
case *events.AppState:
|
||||
// Ignore
|
||||
case *events.KeepAliveTimeout:
|
||||
go user.BridgeState.Send(bridge.State{StateEvent: bridge.StateTransientDisconnect, Error: WAKeepaliveTimeout})
|
||||
go user.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: WAKeepaliveTimeout})
|
||||
case *events.KeepAliveRestored:
|
||||
user.log.Infof("Keepalive restored after timeouts, sending connected event")
|
||||
go user.BridgeState.Send(bridge.State{StateEvent: bridge.StateConnected})
|
||||
go user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
|
||||
case *events.MarkChatAsRead:
|
||||
if user.bridge.Config.Bridge.SyncManualMarkedUnread {
|
||||
user.markUnread(user.GetPortalByJID(v.JID), !v.Action.GetRead())
|
||||
|
@ -1109,7 +1110,7 @@ func (user *User) handleLoggedOut(onConnect bool, reason events.ConnectFailureRe
|
|||
} else if reason == events.ConnectFailureMainDeviceGone {
|
||||
errorCode = WAMainDeviceGone
|
||||
}
|
||||
user.removeFromJIDMap(bridge.State{StateEvent: bridge.StateBadCredentials, Error: errorCode})
|
||||
user.removeFromJIDMap(status.BridgeState{StateEvent: status.StateBadCredentials, Error: errorCode})
|
||||
user.DeleteConnection()
|
||||
user.Session = nil
|
||||
user.JID = types.EmptyJID
|
||||
|
|
Loading…
Reference in a new issue