mirror of
https://mau.dev/mautrix/go.git
synced 2026-03-14 14:25:53 +01:00
bridgev2: add support for starting DMs
This commit is contained in:
parent
e182928df7
commit
eefa219183
9 changed files with 211 additions and 21 deletions
|
|
@ -29,6 +29,7 @@ var (
|
|||
|
||||
HelpSectionGeneral = HelpSection{"General", 0}
|
||||
HelpSectionAuth = HelpSection{"Authentication", 10}
|
||||
HelpSectionChats = HelpSection{"Starting and managing chats", 20}
|
||||
HelpSectionAdmin = HelpSection{"Administration", 50}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -300,16 +300,6 @@ var CommandLogout = &FullHandler{
|
|||
},
|
||||
}
|
||||
|
||||
func (user *User) GetFormattedUserLogins() string {
|
||||
user.Bridge.cacheLock.Lock()
|
||||
logins := make([]string, len(user.logins))
|
||||
for key, val := range user.logins {
|
||||
logins = append(logins, fmt.Sprintf("* `%s` (%s)", key, val.Metadata.RemoteName))
|
||||
}
|
||||
user.Bridge.cacheLock.Unlock()
|
||||
return strings.Join(logins, "\n")
|
||||
}
|
||||
|
||||
func fnLogout(ce *CommandEvent) {
|
||||
if len(ce.Args) == 0 {
|
||||
ce.Reply("Usage: `$cmdprefix logout <login ID>`\n\nYour logins:\n\n%s", ce.User.GetFormattedUserLogins())
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ func NewProcessor(bridge *Bridge) *CommandProcessor {
|
|||
CommandHelp, CommandCancel,
|
||||
CommandRegisterPush,
|
||||
CommandLogin, CommandLogout, CommandSetPreferredLogin,
|
||||
CommandResolveIdentifier, CommandStartChat,
|
||||
)
|
||||
return proc
|
||||
}
|
||||
|
|
|
|||
128
bridgev2/cmdstartchat.go
Normal file
128
bridgev2/cmdstartchat.go
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
// Copyright (c) 2024 Tulir Asokan
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package bridgev2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
var CommandResolveIdentifier = &FullHandler{
|
||||
Func: fnResolveIdentifier,
|
||||
Name: "resolve-identifier",
|
||||
Help: HelpMeta{
|
||||
Section: HelpSectionChats,
|
||||
Description: "Check if a given identifier is on the remote network",
|
||||
Args: "[_login ID_] <_identifier_>",
|
||||
},
|
||||
RequiresLogin: true,
|
||||
}
|
||||
|
||||
var CommandStartChat = &FullHandler{
|
||||
Func: fnResolveIdentifier,
|
||||
Name: "start-chat",
|
||||
Help: HelpMeta{
|
||||
Section: HelpSectionChats,
|
||||
Description: "Start a direct chat with the given user",
|
||||
Args: "[_login ID_] <_identifier_>",
|
||||
},
|
||||
RequiresLogin: true,
|
||||
}
|
||||
|
||||
func getClientForStartingChat[T IdentifierResolvingNetworkAPI](ce *CommandEvent, thing string) (*UserLogin, T, []string) {
|
||||
remainingArgs := ce.Args[1:]
|
||||
login := ce.Bridge.GetCachedUserLoginByID(networkid.UserLoginID(ce.Args[0]))
|
||||
if login == nil || login.UserMXID != ce.User.MXID {
|
||||
remainingArgs = ce.Args
|
||||
login = ce.User.GetDefaultLogin()
|
||||
}
|
||||
api, ok := login.Client.(T)
|
||||
if !ok {
|
||||
ce.Reply("This bridge does not support %s", thing)
|
||||
}
|
||||
return login, api, remainingArgs
|
||||
}
|
||||
|
||||
func fnResolveIdentifier(ce *CommandEvent) {
|
||||
login, api, identifierParts := getClientForStartingChat[IdentifierResolvingNetworkAPI](ce, "resolving identifiers")
|
||||
if api == nil {
|
||||
return
|
||||
}
|
||||
createChat := ce.Command == "start-chat"
|
||||
identifier := strings.Join(identifierParts, " ")
|
||||
resp, err := api.ResolveIdentifier(ce.Ctx, identifier, createChat)
|
||||
if err != nil {
|
||||
ce.Reply("Failed to resolve identifier: %v", err)
|
||||
return
|
||||
} else if resp == nil {
|
||||
ce.ReplyAdvanced(fmt.Sprintf("Identifier <code>%s</code> not found", html.EscapeString(identifier)), false, true)
|
||||
return
|
||||
}
|
||||
var targetName string
|
||||
var targetMXID id.UserID
|
||||
if resp.Ghost != nil {
|
||||
if resp.UserInfo != nil {
|
||||
resp.Ghost.UpdateInfo(ce.Ctx, resp.UserInfo)
|
||||
}
|
||||
targetName = resp.Ghost.Name
|
||||
targetMXID = resp.Ghost.MXID
|
||||
if !createChat {
|
||||
ce.Reply("Found `%s` / [%s](%s)", resp.Ghost.ID, resp.Ghost.Name, resp.Ghost.MXID.URI().MatrixToURL())
|
||||
}
|
||||
} else if resp.UserInfo != nil && resp.UserInfo.Name != nil {
|
||||
targetName = *resp.UserInfo.Name
|
||||
}
|
||||
var formattedName string
|
||||
if targetMXID != "" {
|
||||
formattedName = fmt.Sprintf("`%s` / [%s](%s)", resp.UserID, targetName, targetMXID.URI().MatrixToURL())
|
||||
} else if targetName != "" {
|
||||
formattedName = fmt.Sprintf("`%s` / %s", resp.UserID, targetName)
|
||||
} else {
|
||||
formattedName = fmt.Sprintf("`%s`", resp.UserID)
|
||||
}
|
||||
if createChat {
|
||||
if resp.Chat == nil {
|
||||
ce.Reply("Interface error: network connector did not return chat for create chat request")
|
||||
return
|
||||
}
|
||||
portal := resp.Chat.Portal
|
||||
if portal == nil {
|
||||
portal, err = ce.Bridge.GetPortalByID(ce.Ctx, resp.Chat.PortalID)
|
||||
if err != nil {
|
||||
ce.Reply("Failed to get portal: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if portal.MXID != "" {
|
||||
name := portal.Name
|
||||
if name == "" {
|
||||
name = portal.MXID.String()
|
||||
}
|
||||
portal.UpdateInfo(ce.Ctx, resp.Chat.PortalInfo, login, nil, time.Time{})
|
||||
ce.Reply("You already have a direct chat with %s at [%s](%s)", formattedName, name, portal.MXID.URI().MatrixToURL())
|
||||
} else {
|
||||
err = portal.CreateMatrixRoom(ce.Ctx, login, resp.Chat.PortalInfo)
|
||||
if err != nil {
|
||||
ce.Reply("Failed to create room: %v", err)
|
||||
return
|
||||
}
|
||||
name := portal.Name
|
||||
if name == "" {
|
||||
name = portal.MXID.String()
|
||||
}
|
||||
ce.Reply("Created chat with %s: [%s](%s)", formattedName, name, portal.MXID.URI().MatrixToURL())
|
||||
}
|
||||
} else {
|
||||
ce.Reply("Found %s", formattedName)
|
||||
}
|
||||
}
|
||||
|
|
@ -146,6 +146,18 @@ func isOnlyNumbers(input string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func CleanPhoneNumber(phone string) (string, error) {
|
||||
phone = numberCleaner.Replace(phone)
|
||||
if len(phone) < 2 {
|
||||
return "", fmt.Errorf("phone number must start with + and contain numbers")
|
||||
} else if phone[0] != '+' {
|
||||
return "", fmt.Errorf("phone number must start with +")
|
||||
} else if !isOnlyNumbers(phone[1:]) {
|
||||
return "", fmt.Errorf("phone number must only contain numbers")
|
||||
}
|
||||
return phone, nil
|
||||
}
|
||||
|
||||
func (f *LoginInputDataField) FillDefaultValidate() {
|
||||
noopValidate := func(input string) (string, error) { return input, nil }
|
||||
if f.Validate != nil {
|
||||
|
|
@ -153,17 +165,7 @@ func (f *LoginInputDataField) FillDefaultValidate() {
|
|||
}
|
||||
switch f.Type {
|
||||
case LoginInputFieldTypePhoneNumber:
|
||||
f.Validate = func(phone string) (string, error) {
|
||||
phone = numberCleaner.Replace(phone)
|
||||
if len(phone) < 2 {
|
||||
return "", fmt.Errorf("phone number must start with + and contain numbers")
|
||||
} else if phone[0] != '+' {
|
||||
return "", fmt.Errorf("phone number must start with +")
|
||||
} else if !isOnlyNumbers(phone[1:]) {
|
||||
return "", fmt.Errorf("phone number must only contain numbers")
|
||||
}
|
||||
return phone, nil
|
||||
}
|
||||
f.Validate = CleanPhoneNumber
|
||||
case LoginInputFieldTypeEmail:
|
||||
f.Validate = func(email string) (string, error) {
|
||||
if !strings.ContainsRune(email, '@') {
|
||||
|
|
|
|||
|
|
@ -180,6 +180,37 @@ type NetworkAPI interface {
|
|||
HandleMatrixTyping(ctx context.Context, msg *MatrixTyping) error
|
||||
}
|
||||
|
||||
type ResolveIdentifierResponse struct {
|
||||
Ghost *Ghost
|
||||
|
||||
UserID networkid.UserID
|
||||
UserInfo *UserInfo
|
||||
|
||||
Chat *CreateChatResponse
|
||||
}
|
||||
|
||||
type CreateChatResponse struct {
|
||||
Portal *Portal
|
||||
|
||||
PortalID networkid.PortalKey
|
||||
PortalInfo *PortalInfo
|
||||
}
|
||||
|
||||
type IdentifierResolvingNetworkAPI interface {
|
||||
NetworkAPI
|
||||
ResolveIdentifier(ctx context.Context, identifier string, createChat bool) (*ResolveIdentifierResponse, error)
|
||||
}
|
||||
|
||||
type UserSearchingNetworkAPI interface {
|
||||
IdentifierResolvingNetworkAPI
|
||||
SearchUsers(ctx context.Context, query string) ([]*ResolveIdentifierResponse, error)
|
||||
}
|
||||
|
||||
type GroupCreatingNetworkAPI interface {
|
||||
IdentifierResolvingNetworkAPI
|
||||
CreateGroup(ctx context.Context, name string, users ...networkid.UserID) (*CreateChatResponse, error)
|
||||
}
|
||||
|
||||
type PushType int
|
||||
|
||||
func (pt PushType) String() string {
|
||||
|
|
|
|||
|
|
@ -1357,6 +1357,9 @@ func (portal *Portal) SyncParticipants(ctx context.Context, members []networkid.
|
|||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get user logins in portal: %w", err)
|
||||
}
|
||||
if !slices.Contains(loginsInPortal, source) {
|
||||
loginsInPortal = append(loginsInPortal, source)
|
||||
}
|
||||
expectedUserIDs := make([]id.UserID, 0, len(members))
|
||||
expectedExtraUsers := make([]id.UserID, 0)
|
||||
expectedIntents := make([]MatrixAPI, len(members))
|
||||
|
|
@ -1459,6 +1462,13 @@ func (portal *Portal) UpdateInfo(ctx context.Context, info *PortalInfo, source *
|
|||
}
|
||||
// TODO detect changes to functional members list?
|
||||
}
|
||||
if source != nil {
|
||||
// TODO is this a good place for this call? there's another one in QueueRemoteEvent
|
||||
err := portal.Bridge.DB.UserPortal.EnsureExists(ctx, source.UserLogin, portal.PortalKey)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to ensure user portal row exists")
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
portal.UpdateBridgeInfo(ctx)
|
||||
err := portal.Save(ctx)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ const (
|
|||
)
|
||||
|
||||
func (br *Bridge) ReIDPortal(ctx context.Context, source, target networkid.PortalKey) (ReIDResult, *Portal, error) {
|
||||
if source == target {
|
||||
return ReIDResultError, nil, fmt.Errorf("illegal re-ID call: source and target are the same")
|
||||
}
|
||||
log := zerolog.Ctx(ctx)
|
||||
log.Debug().Msg("Re-ID'ing portal")
|
||||
defer func() {
|
||||
|
|
|
|||
|
|
@ -9,10 +9,13 @@ package bridgev2
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
|
|
@ -149,6 +152,27 @@ func (user *User) DoublePuppet(ctx context.Context) MatrixAPI {
|
|||
return intent
|
||||
}
|
||||
|
||||
func (user *User) GetFormattedUserLogins() string {
|
||||
user.Bridge.cacheLock.Lock()
|
||||
logins := make([]string, len(user.logins))
|
||||
for key, val := range user.logins {
|
||||
logins = append(logins, fmt.Sprintf("* `%s` (%s)", key, val.Metadata.RemoteName))
|
||||
}
|
||||
user.Bridge.cacheLock.Unlock()
|
||||
return strings.Join(logins, "\n")
|
||||
}
|
||||
|
||||
func (user *User) GetDefaultLogin() *UserLogin {
|
||||
user.Bridge.cacheLock.Lock()
|
||||
defer user.Bridge.cacheLock.Unlock()
|
||||
if len(user.logins) == 0 {
|
||||
return nil
|
||||
}
|
||||
loginKeys := maps.Keys(user.logins)
|
||||
slices.Sort(loginKeys)
|
||||
return user.logins[loginKeys[0]]
|
||||
}
|
||||
|
||||
func (user *User) Save(ctx context.Context) error {
|
||||
return user.Bridge.DB.User.Update(ctx, user.User)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue