rewrite mention in go (#3309)

This commit is contained in:
frank 2023-04-07 16:47:38 +08:00 committed by GitHub
parent 1754c23763
commit fd6073ab2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1701 additions and 2 deletions

View File

@ -1 +1 @@
0.143.0
0.143.1

2
go.mod
View File

@ -65,7 +65,7 @@ require (
go.uber.org/zap v1.24.0
golang.org/x/crypto v0.4.0
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
google.golang.org/protobuf v1.28.1
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v9 v9.31.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0

View File

@ -110,6 +110,7 @@ type Messenger struct {
pushNotificationServer *pushnotificationserver.Server
communitiesManager *communities.Manager
accountsManager *account.GethManager
mentionsManager *MentionManager
logger *zap.Logger
outputCSV bool
@ -505,6 +506,7 @@ func NewMessenger(
logger: logger,
savedAddressesManager: savedAddressesManager,
}
messenger.mentionsManager = NewMentionManager(messenger)
if c.outputMessagesCSV {
messenger.outputCSV = c.outputMessagesCSV
@ -6159,3 +6161,7 @@ func (m *Messenger) ImageServerURL() string {
func (m *Messenger) myHexIdentity() string {
return common.PubkeyToHex(&m.identity.PublicKey)
}
func (m *Messenger) GetMentionsManager() *MentionManager {
return m.mentionsManager
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,473 @@
package protocol
import (
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestRePosRegex(t *testing.T) {
testCases := []struct {
input string
expected bool
}{
{"@", true},
{"~", true},
{"\\", true},
{"*", true},
{"_", true},
{"\n", true},
{"`", true},
{"a", false},
{"#", false},
}
for _, tc := range testCases {
actual := specialCharsRegex.MatchString(tc.input)
if actual != tc.expected {
t.Errorf("Unexpected match result for input '%s': expected=%v, actual=%v", tc.input, tc.expected, actual)
}
}
}
func TestRePos(t *testing.T) {
// Test case 1: Empty string
s1 := ""
var want1 []specialCharLocation
if got1 := rePos(s1); !reflect.DeepEqual(got1, want1) {
t.Errorf("rePos(%q) = %v, want %v", s1, got1, want1)
}
// Test case 2: Single match
s2 := "test@string"
want2 := []specialCharLocation{{4, "@"}}
if got2 := rePos(s2); !reflect.DeepEqual(got2, want2) {
t.Errorf("rePos(%q) = %v, want %v", s2, got2, want2)
}
// Test case 3: Multiple matches
s3 := "this is a test string@with multiple@matches"
want3 := []specialCharLocation{{21, "@"}, {35, "@"}}
if got3 := rePos(s3); !reflect.DeepEqual(got3, want3) {
t.Errorf("rePos(%q) = %v, want %v", s3, got3, want3)
}
// Test case 4: No matches
s4 := "this is a test string with no matches"
var want4 []specialCharLocation
if got4 := rePos(s4); !reflect.DeepEqual(got4, want4) {
t.Errorf("rePos(%q) = %v, want %v", s4, got4, want4)
}
// Test case 5: Matches at the beginning and end
s5 := "@this is a test string@"
want5 := []specialCharLocation{{0, "@"}, {22, "@"}}
if got5 := rePos(s5); !reflect.DeepEqual(got5, want5) {
t.Errorf("rePos(%q) = %v, want %v", s5, got5, want5)
}
// Test case 6: special characters
s6 := "Привет @testm1 "
want6 := []specialCharLocation{{7, "@"}}
if got6 := rePos(s6); !reflect.DeepEqual(got6, want6) {
t.Errorf("rePos(%q) = %v, want %v", s6, got6, want6)
}
}
func TestReplaceMentions(t *testing.T) {
users := map[string]*MentionableUser{
"0xpk1": {
primaryName: "User Number One",
Contact: &Contact{
ID: "0xpk1",
},
},
"0xpk2": {
primaryName: "user2",
secondaryName: "User Number Two",
Contact: &Contact{
ID: "0xpk2",
},
},
"0xpk3": {
primaryName: "user3",
secondaryName: "User Number Three",
Contact: &Contact{
ID: "0xpk3",
},
},
}
tests := []struct {
name string
text string
expected string
}{
{"empty string", "", ""},
{"no text", "", ""},
{"incomlepte mention 1", "@", "@"},
{"incomplete mention 2", "@r", "@r"},
{"no mentions", "foo bar @buzz kek @foo", "foo bar @buzz kek @foo"},
{"starts with mention", "@User Number One", "@0xpk1"},
{"starts with mention, comma after mention", "@User Number One,", "@0xpk1,"},
{"starts with mention but no space after", "@User Number Onefoo", "@User Number Onefoo"},
{"starts with mention, some text after mention", "@User Number One foo", "@0xpk1 foo"},
{"starts with some text, then mention", "text @User Number One", "text @0xpk1"},
{"starts with some text, then mention, then more text", "text @User Number One foo", "text @0xpk1 foo"},
{"no space before mention", "text@User Number One", "text@0xpk1"},
{"two different mentions", "@User Number One @User Number two", "@0xpk1 @0xpk2"},
{"two different mentions, separated with comma", "@User Number One,@User Number two", "@0xpk1,@0xpk2"},
{"two different mentions inside text", "foo@User Number One bar @User Number two baz", "foo@0xpk1 bar @0xpk2 baz"},
{"ens mention", "@user2", "@0xpk2"},
{"multiple mentions", strings.Repeat("@User Number One @User Number two ", 1000), strings.Repeat("@0xpk1 @0xpk2 ", 1000)},
{"single * case 1", "*@user2*", "*@user2*"},
{"single * case 2", "*@user2 *", "*@0xpk2 *"},
{"single * case 3", "a*@user2*", "a*@user2*"},
{"single * case 4", "*@user2 foo*foo", "*@0xpk2 foo*foo"},
{"single * case 5", "a *@user2*", "a *@user2*"},
{"single * case 6", "*@user2 foo*", "*@user2 foo*"},
{"single * case 7", "@user2 *@user2 foo* @user2", "@0xpk2 *@user2 foo* @0xpk2"},
{"single * case 8", "*@user2 foo**@user2 foo*", "*@user2 foo**@user2 foo*"},
{"single * case 9", "*@user2 foo***@user2 foo* @user2", "*@user2 foo***@user2 foo* @0xpk2"},
{"double * case 1", "**@user2**", "**@user2**"},
{"double * case 2", "**@user2 **", "**@0xpk2 **"},
{"double * case 3", "a**@user2**", "a**@user2**"},
{"double * case 4", "**@user2 foo**foo", "**@user2 foo**foo"},
{"double * case 5", "a **@user2**", "a **@user2**"},
{"double * case 6", "**@user2 foo**", "**@user2 foo**"},
{"double * case 7", "@user2 **@user2 foo** @user2", "@0xpk2 **@user2 foo** @0xpk2"},
{"double * case 8", "**@user2 foo****@user2 foo**", "**@user2 foo****@user2 foo**"},
{"double * case 9", "**@user2 foo*****@user2 foo** @user2", "**@user2 foo*****@user2 foo** @0xpk2"},
{"tripple * case 1", "***@user2 foo***@user2 foo*", "***@user2 foo***@0xpk2 foo*"},
{"tripple ~ case 1", "~~~@user2 foo~~~@user2 foo~", "~~~@user2 foo~~~@user2 foo~"},
{"quote case 1", ">@user2", ">@user2"},
{"quote case 2", "\n>@user2", "\n>@user2"},
{"quote case 3", "\n> @user2 \n \n @user2", "\n> @user2 \n \n @0xpk2"},
{"quote case 4", ">@user2\n\n>@user2", ">@user2\n\n>@user2"},
{"quote case 5", "***hey\n\n>@user2\n\n@user2 foo***", "***hey\n\n>@user2\n\n@0xpk2 foo***"},
{"code case 1", "` @user2 `", "` @user2 `"},
{"code case 2", "` @user2 `", "` @user2 `"},
{"code case 3", "``` @user2 ```", "``` @user2 ```"},
{"code case 4", "` ` @user2 ``", "` ` @0xpk2 ``"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ReplaceMentions(tt.text, users)
if got != tt.expected {
t.Errorf("testing %q, ReplaceMentions(%q) got %q, expected %q", tt.name, tt.text, got, tt.expected)
}
})
}
}
func TestGetAtSignIdxs(t *testing.T) {
tests := []struct {
name string
text string
start int
want []int
}{
{
name: "no @ sign",
text: "hello world",
start: 0,
want: []int{},
},
{
name: "single @ sign",
text: "hello @world",
start: 0,
want: []int{6},
},
{
name: "multiple @ signs",
text: "@hello @world @again",
start: 0,
want: []int{0, 7, 14},
},
{
name: "start after first @ sign",
text: "hello @world",
start: 6,
want: []int{12},
},
{
name: "start after second @ sign",
text: "hello @world @again",
start: 8,
want: []int{14, 21},
},
{
name: "start after last @ sign",
text: "hello @world @again",
start: 15,
want: []int{21, 28},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := getAtSignIdxs(tt.text, tt.start)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("getAtSignIdxs() = %v, want %v", got, tt.want)
}
})
}
}
func TestCalcAtIdxs(t *testing.T) {
newText := "@abc"
state := MentionState{
AtIdxs: []*AtIndexEntry{
{From: 0, To: 3, Checked: false},
},
NewText: &newText,
PreviousText: "",
Start: 0,
}
want := []*AtIndexEntry{
{From: 0, To: 0, Checked: false},
{From: 4, To: 7, Checked: false},
}
got := calcAtIdxs(&state)
if !reflect.DeepEqual(got, want) {
t.Errorf("calcAtIdxs() = %v, want %v", got, want)
}
}
func TestToInfo(t *testing.T) {
newText := " "
t.Run("toInfo base case", func(t *testing.T) {
expected := &MentionState{
AtSignIdx: 2,
AtIdxs: []*AtIndexEntry{
{
Checked: true,
Mentioned: true,
From: 2,
To: 17,
},
},
MentionEnd: 19,
PreviousText: "",
NewText: &newText,
Start: 18,
End: 18,
}
inputSegments := []InputSegment{
{Type: Text, Value: "H."},
{Type: Mention, Value: "@helpinghand.eth"},
{Type: Text, Value: " "},
}
actual := toInfo(inputSegments)
if !reflect.DeepEqual(expected.AtIdxs, actual.AtIdxs) {
t.Errorf("Expected AtIdxs: %#v, but got: %#v", expected.AtIdxs, actual.AtIdxs)
}
expected.AtIdxs = nil
actual.AtIdxs = nil
if !reflect.DeepEqual(expected, actual) {
t.Errorf("Expected %#v, but got %#v", expected, actual)
}
})
}
func TestToInputField(t *testing.T) {
mentionText1 := "parse-text"
mentionTextResult1 := []InputSegment{
{Type: Text, Value: "parse-text"},
}
mentionText2 := "hey @0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073 he"
mentionTextResult2 := []InputSegment{
{Type: Text, Value: "hey "},
{Type: Mention, Value: "0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"},
{Type: Text, Value: " he"},
}
mentionText3 := "@0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073 he"
mentionTextResult3 := []InputSegment{
{Type: Mention, Value: "0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"},
{Type: Text, Value: " he"},
}
mentionText4 := "hey @0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"
mentionTextResult4 := []InputSegment{
{Type: Text, Value: "hey "},
{Type: Mention, Value: "0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"},
}
mentionText5 := "invalid @0x04fBce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"
mentionTextResult5 := []InputSegment{
{Type: Text, Value: "invalid @0x04fBce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073"},
}
testCases := []struct {
name string
input string
expected []InputSegment
}{
{"only text", mentionText1, mentionTextResult1},
{"in the middle", mentionText2, mentionTextResult2},
{"at the beginning", mentionText3, mentionTextResult3},
{"at the end", mentionText4, mentionTextResult4},
{"invalid", mentionText5, mentionTextResult5},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := toInputField(tc.input)
if !reflect.DeepEqual(result, tc.expected) {
t.Errorf("Expected: %v, got: %v", tc.expected, result)
}
})
}
}
func TestLastIndexOf(t *testing.T) {
atSignIdx := LastIndexOf("@", charAtSign, 0)
require.Equal(t, 0, atSignIdx)
atSignIdx = LastIndexOf("@@", charAtSign, 1)
require.Equal(t, 1, atSignIdx)
//at-sign-idx 0 text @t searched-text t start 2 end 2 new-text
atSignIdx = LastIndexOf("@t", charAtSign, 2)
require.Equal(t, 0, atSignIdx)
}
type MockMentionableUserGetter struct {
mentionableUserMap map[string]*MentionableUser
}
func (m *MockMentionableUserGetter) getMentionableUsers(chatID string) (map[string]*MentionableUser, error) {
return m.mentionableUserMap, nil
}
func (m *MockMentionableUserGetter) getMentionableUser(chatID string, pk string) (*MentionableUser, error) {
return m.mentionableUserMap[pk], nil
}
func TestMentionSuggestion(t *testing.T) {
mentionableUserMap, chatID, mentionManager := setupMentionSuggestionTest()
testCases := []struct {
newText string
prevText string
start int
end int
inputText string
expectedSize int
}{
{"@", "", 0, 0, "@", len(mentionableUserMap)},
{"u", "", 1, 1, "@u", len(mentionableUserMap)},
{"2", "", 2, 2, "@u2", 1},
{"3", "", 3, 3, "@u23", 0},
{"", "3", 3, 4, "@u2", 1},
}
for _, tc := range testCases {
input := MentionState{PreviousText: tc.prevText, NewText: &tc.newText, Start: tc.start, End: tc.end}
_, err := mentionManager.OnTextInput(chatID, &input)
require.NoError(t, err)
ctx, err := mentionManager.CalculateSuggestions(chatID, tc.inputText)
require.NoError(t, err)
require.Equal(t, tc.expectedSize, len(ctx.MentionSuggestions))
t.Logf("Input: %+v, MentionState:%+v, InputSegments:%+v\n", input, ctx.MentionState, ctx.InputSegments)
}
}
func TestMentionSuggestionWithSpecialCharacters(t *testing.T) {
mentionableUserMap, chatID, mentionManager := setupMentionSuggestionTest()
testCases := []struct {
newText string
prevText string
start int
end int
inputText string
expectedSize int
calculateSuggestion bool
}{
{"'", "", 0, 0, "'", 0, false},
{"", "", 0, 1, "", 0, true},
{"@", "", 1, 1, "@", len(mentionableUserMap), true},
}
for _, tc := range testCases {
input := MentionState{PreviousText: tc.prevText, NewText: &tc.newText, Start: tc.start, End: tc.end}
ctx, err := mentionManager.OnTextInput(chatID, &input)
require.NoError(t, err)
if tc.calculateSuggestion {
ctx, err = mentionManager.CalculateSuggestions(chatID, tc.inputText)
require.NoError(t, err)
require.Equal(t, tc.expectedSize, len(ctx.MentionSuggestions))
}
t.Logf("Input: %+v, MentionState:%+v, InputSegments:%+v\n", input, ctx.MentionState, ctx.InputSegments)
}
}
func setupMentionSuggestionTest() (map[string]*MentionableUser, string, *MentionManager) {
mentionableUserMap := getMentionableUserMap()
for _, u := range mentionableUserMap {
addSearchablePhrases(u)
}
mockMentionableUserGetter := &MockMentionableUserGetter{
mentionableUserMap: mentionableUserMap,
}
chatID := "0xchatID"
allChats := new(chatMap)
allChats.Store(chatID, &Chat{})
mentionManager := &MentionManager{
mentionableUserGetter: mockMentionableUserGetter,
mentionContexts: make(map[string]*ChatMentionContext),
Messenger: &Messenger{
allChats: allChats,
},
}
return mentionableUserMap, chatID, mentionManager
}
func getMentionableUserMap() map[string]*MentionableUser {
return map[string]*MentionableUser{
"0xpk1": {
primaryName: "User Number One",
Contact: &Contact{
ID: "0xpk1",
},
},
"0xpk2": {
primaryName: "u2",
secondaryName: "User Number Two",
Contact: &Contact{
ID: "0xpk2",
},
},
"0xpk3": {
primaryName: "u3",
secondaryName: "User Number Three",
Contact: &Contact{
ID: "0xpk3",
},
},
}
}

View File

@ -1289,6 +1289,38 @@ func (api *PublicAPI) Messenger() *protocol.Messenger {
return api.service.messenger
}
func (api *PublicAPI) ChatMentionCheckMentions(chatID, text string) (string, error) {
return api.service.messenger.GetMentionsManager().CheckMentions(chatID, text)
}
func (api *PublicAPI) ChatMentionOnTextInput(chatID string, state *protocol.MentionState) (*protocol.ChatMentionContext, error) {
return api.service.messenger.GetMentionsManager().OnTextInput(chatID, state)
}
func (api *PublicAPI) ChatMentionRecheckAtIdxs(chatID string, text string, publicKey string) (*protocol.ChatMentionContext, error) {
return api.service.messenger.GetMentionsManager().RecheckAtIdxs(chatID, text, publicKey)
}
func (api *PublicAPI) ChatMentionCalculateSuggestions(chatID, text string) (*protocol.ChatMentionContext, error) {
return api.service.messenger.GetMentionsManager().CalculateSuggestions(chatID, text)
}
func (api *PublicAPI) ChatMentionNewInputTextWithMention(chatID, text, primaryName string) *protocol.ChatMentionContext {
return api.service.messenger.GetMentionsManager().NewInputTextWithMention(chatID, text, primaryName)
}
func (api *PublicAPI) ChatMentionClearMentions(chatID string) {
api.service.messenger.GetMentionsManager().ClearMentions(chatID)
}
func (api *PublicAPI) ChatMentionHandleSelectionChange(chatID, text string, start int, end int) *protocol.ChatMentionContext {
return api.service.messenger.GetMentionsManager().HandleSelectionChange(chatID, text, start, end)
}
func (api *PublicAPI) ChatMentionToInputField(chatID, text string) (*protocol.ChatMentionContext, error) {
return api.service.messenger.GetMentionsManager().ToInputField(chatID, text)
}
// -----
// HELPER
// -----