Skip to content

Commit 34e350d

Browse files
committed
feat: improve contact labels
1 parent 89db075 commit 34e350d

23 files changed

+1154
-335
lines changed

internal/app/characterservice/contacts.go

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"fmt"
77
"log/slog"
8+
"maps"
89
"net/http"
910
"slices"
1011

@@ -27,62 +28,123 @@ func (s *CharacterService) updateContactsESI(ctx context.Context, arg app.Charac
2728
return false, fmt.Errorf("wrong section for update %s: %w", arg.Section, app.ErrInvalid)
2829
}
2930
return s.updateSectionIfChanged(
30-
ctx, arg, true,
31+
ctx, arg, false,
3132
func(ctx context.Context, characterID int64) (any, error) {
3233
ctx = xgoesi.NewContextWithOperationID(ctx, "GetCharactersCharacterIdContacts")
33-
rows, err := xgoesi.FetchPages(
34+
contacts, err := xgoesi.FetchPages(
3435
func(page int32) ([]esi.CharactersCharacterIdContactsGetInner, *http.Response, error) {
3536
return s.esiClient.ContactsAPI.GetCharactersCharacterIdContacts(ctx, characterID).Page(page).Execute()
3637
},
3738
)
3839
if err != nil {
3940
return nil, err
4041
}
41-
slices.SortFunc(rows, func(a, b esi.CharactersCharacterIdContactsGetInner) int {
42+
slices.SortFunc(contacts, func(a, b esi.CharactersCharacterIdContactsGetInner) int {
4243
return cmp.Compare(a.ContactId, b.ContactId)
4344
})
44-
45-
slog.Debug("Received contacts from ESI", "count", len(rows), "characterID", characterID)
46-
return rows, nil
45+
slog.Debug("Received contacts from ESI", "count", len(contacts), "characterID", characterID)
46+
return contacts, nil
4747
},
4848
func(ctx context.Context, characterID int64, data any) (bool, error) {
49-
rows := data.([]esi.CharactersCharacterIdContactsGetInner)
50-
incomingIDs := set.Collect(xiter.MapSlice(rows, func(x esi.CharactersCharacterIdContactsGetInner) int64 {
49+
contacts := data.([]esi.CharactersCharacterIdContactsGetInner)
50+
51+
incomingIDs := set.Collect(xiter.MapSlice(contacts, func(x esi.CharactersCharacterIdContactsGetInner) int64 {
5152
return x.ContactId
5253
}))
53-
5454
_, err := s.eus.AddMissingEntities(ctx, incomingIDs)
5555
if err != nil {
5656
return false, err
5757
}
58-
59-
for _, r := range rows {
58+
for _, r := range contacts {
6059
err = s.st.UpdateOrCreateCharacterContact(ctx, storage.UpdateOrCreateCharacterContactParams{
6160
CharacterID: characterID,
6261
ContactID: r.ContactId,
6362
IsBlocked: optional.FromPtr(r.IsBlocked),
6463
IsWatched: optional.FromPtr(r.IsWatched),
6564
Standing: r.Standing,
65+
LabelIDs: r.LabelIds,
6666
})
6767
if err != nil {
6868
return false, err
6969
}
7070
}
71-
slog.Info("Updated loyalty points entries", "characterID", characterID, "count", incomingIDs.Size())
71+
slog.Info("Updated contacts", "characterID", characterID, "count", incomingIDs.Size())
7272

7373
// Delete obsolete entries
7474
currentIDs, err := s.st.ListCharacterContactIDs(ctx, characterID)
7575
if err != nil {
7676
return false, err
7777
}
78-
obsoleteIDs := set.Difference(incomingIDs, currentIDs)
78+
obsoleteIDs := set.Difference(currentIDs, incomingIDs)
7979
if obsoleteIDs.Size() > 0 {
8080
err := s.st.DeleteCharacterContacts(ctx, characterID, obsoleteIDs)
8181
if err != nil {
8282
return false, err
8383
}
84-
slog.Info("Deleted obsolete loyalty points entries", "characterID", characterID, "count", obsoleteIDs.Size())
84+
slog.Info("Deleted obsolete contacts", "characterID", characterID, "count", obsoleteIDs.Size())
8585
}
8686
return true, nil
8787
})
8888
}
89+
90+
func (s *CharacterService) updateContactLabelsESI(ctx context.Context, arg app.CharacterSectionUpdateParams) (bool, error) {
91+
if arg.Section != app.SectionCharacterContactLabels {
92+
return false, fmt.Errorf("wrong section for update %s: %w", arg.Section, app.ErrInvalid)
93+
}
94+
return s.updateSectionIfChanged(
95+
ctx, arg, true,
96+
func(ctx context.Context, characterID int64) (any, error) {
97+
ctx = xgoesi.NewContextWithOperationID(ctx, "GetCharactersCharacterIdContactsLabels")
98+
labels, _, err := s.esiClient.ContactsAPI.GetCharactersCharacterIdContactsLabels(ctx, characterID).Execute()
99+
if err != nil {
100+
return nil, err
101+
}
102+
slog.Debug("Received labels from ESI", "count", len(labels), "characterID", characterID)
103+
return labels, nil
104+
},
105+
func(ctx context.Context, characterID int64, x any) (bool, error) {
106+
incoming := x.([]esi.AlliancesAllianceIdContactsLabelsGetInner)
107+
incoming2 := maps.Collect(xiter.MapSlice2(incoming, func(x esi.AlliancesAllianceIdContactsLabelsGetInner) (int64, string) {
108+
return x.LabelId, x.LabelName
109+
}))
110+
111+
current, err := s.st.ListCharacterContactLabels(ctx, characterID)
112+
if err != nil {
113+
return false, err
114+
}
115+
current2 := maps.Collect(xiter.MapSlice2(current, func(x *app.CharacterContactLabel) (int64, string) {
116+
return x.LabelID, x.Name
117+
}))
118+
119+
var changed int
120+
for id2, name2 := range incoming2 {
121+
if name1, ok := current2[id2]; ok && name1 == name2 {
122+
continue
123+
}
124+
err := s.st.UpdateOrCreateCharacterContactLabel(ctx, storage.UpdateOrCreateCharacterContactLabelParams{
125+
CharacterID: characterID,
126+
LabelID: id2,
127+
Name: name2,
128+
})
129+
if err != nil {
130+
return false, err
131+
}
132+
changed++
133+
}
134+
slog.Info("Updated contact labels", "characterID", characterID, "count", changed)
135+
136+
// Delete obsolete labels
137+
currentIDs := set.Collect(maps.Keys(current2))
138+
incomingIDs := set.Collect(maps.Keys(incoming2))
139+
obsoleteIDs := set.Difference(currentIDs, incomingIDs)
140+
if obsoleteIDs.Size() > 0 {
141+
err := s.st.DeleteCharacterContactLabels(ctx, characterID, obsoleteIDs)
142+
if err != nil {
143+
return false, err
144+
}
145+
slog.Info("Deleted obsolete contact labels", "characterID", characterID, "count", obsoleteIDs.Size())
146+
changed += obsoleteIDs.Size()
147+
}
148+
return changed > 0, nil
149+
})
150+
}

internal/app/characterservice/contacts_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,17 @@ func TestUpdateCharacterContactsESI(t *testing.T) {
3333
CharacterID: c.ID,
3434
})
3535
contact := factory.CreateEveEntityCharacter()
36+
label := factory.CreateCharacterContactLabel(storage.UpdateOrCreateCharacterContactLabelParams{
37+
CharacterID: c.ID,
38+
})
3639
httpmock.RegisterResponder(
3740
"GET",
3841
fmt.Sprintf("https://esi.evetech.net/characters/%d/contacts", c.ID),
3942
httpmock.NewJsonResponderOrPanic(200, []map[string]any{{
4043
"contact_id": contact.ID,
4144
"contact_type": "character",
4245
"standing": -1.5,
46+
"label_ids": []int64{label.LabelID},
4347
}}),
4448
)
4549

@@ -54,6 +58,7 @@ func TestUpdateCharacterContactsESI(t *testing.T) {
5458
o, err := st.GetCharacterContact(ctx, c.ID, contact.ID)
5559
require.NoError(t, err)
5660
xassert.Equal(t, -1.5, o.Standing)
61+
xassert.Equal(t, set.Of(label.Name), o.Labels)
5762
})
5863
t.Run("should update existing entries", func(t *testing.T) {
5964
// given
@@ -129,3 +134,133 @@ func TestUpdateCharacterContactsESI(t *testing.T) {
129134
xassert.Equal(t, set.Of(contact.ID), ids)
130135
})
131136
}
137+
138+
func TestUpdateCharacterContactLabelsESI(t *testing.T) {
139+
db, st, factory := testutil.NewDBInMemory()
140+
defer db.Close()
141+
httpmock.Activate()
142+
defer httpmock.DeactivateAndReset()
143+
s := NewFake(st)
144+
ctx := context.Background()
145+
t.Run("should create new entries from scratch", func(t *testing.T) {
146+
// given
147+
testutil.MustTruncateTables(db)
148+
httpmock.Reset()
149+
c := factory.CreateCharacter()
150+
factory.CreateCharacterToken(storage.UpdateOrCreateCharacterTokenParams{
151+
CharacterID: c.ID,
152+
})
153+
httpmock.RegisterResponder(
154+
"GET",
155+
fmt.Sprintf("https://esi.evetech.net/characters/%d/contacts/labels", c.ID),
156+
httpmock.NewJsonResponderOrPanic(200, []map[string]any{{
157+
"label_id": 42,
158+
"label_name": "alpha",
159+
}}),
160+
)
161+
// when
162+
changed, err := s.updateContactLabelsESI(ctx, app.CharacterSectionUpdateParams{
163+
CharacterID: c.ID,
164+
Section: app.SectionCharacterContactLabels,
165+
})
166+
// then
167+
require.NoError(t, err)
168+
assert.True(t, changed)
169+
o, err := st.GetCharacterContactLabel(ctx, c.ID, 42)
170+
require.NoError(t, err)
171+
xassert.Equal(t, "alpha", o.Name)
172+
})
173+
t.Run("should update existing entries", func(t *testing.T) {
174+
// given
175+
testutil.MustTruncateTables(db)
176+
httpmock.Reset()
177+
c := factory.CreateCharacter()
178+
factory.CreateCharacterToken(storage.UpdateOrCreateCharacterTokenParams{
179+
CharacterID: c.ID,
180+
})
181+
factory.CreateCharacterContactLabel(storage.UpdateOrCreateCharacterContactLabelParams{
182+
CharacterID: c.ID,
183+
LabelID: 42,
184+
})
185+
httpmock.RegisterResponder(
186+
"GET",
187+
fmt.Sprintf("https://esi.evetech.net/characters/%d/contacts/labels", c.ID),
188+
httpmock.NewJsonResponderOrPanic(200, []map[string]any{{
189+
"label_id": 42,
190+
"label_name": "alpha",
191+
}}),
192+
)
193+
// when
194+
changed, err := s.updateContactLabelsESI(ctx, app.CharacterSectionUpdateParams{
195+
CharacterID: c.ID,
196+
Section: app.SectionCharacterContactLabels,
197+
})
198+
// then
199+
require.NoError(t, err)
200+
assert.True(t, changed)
201+
o, err := st.GetCharacterContactLabel(ctx, c.ID, 42)
202+
require.NoError(t, err)
203+
xassert.Equal(t, "alpha", o.Name)
204+
})
205+
t.Run("should report when not changed", func(t *testing.T) {
206+
// given
207+
testutil.MustTruncateTables(db)
208+
httpmock.Reset()
209+
c := factory.CreateCharacter()
210+
factory.CreateCharacterToken(storage.UpdateOrCreateCharacterTokenParams{
211+
CharacterID: c.ID,
212+
})
213+
factory.CreateCharacterContactLabel(storage.UpdateOrCreateCharacterContactLabelParams{
214+
CharacterID: c.ID,
215+
LabelID: 42,
216+
Name: "alpha",
217+
})
218+
httpmock.RegisterResponder(
219+
"GET",
220+
fmt.Sprintf("https://esi.evetech.net/characters/%d/contacts/labels", c.ID),
221+
httpmock.NewJsonResponderOrPanic(200, []map[string]any{{
222+
"label_id": 42,
223+
"label_name": "alpha",
224+
}}),
225+
)
226+
// when
227+
changed, err := s.updateContactLabelsESI(ctx, app.CharacterSectionUpdateParams{
228+
CharacterID: c.ID,
229+
Section: app.SectionCharacterContactLabels,
230+
})
231+
// then
232+
require.NoError(t, err)
233+
assert.False(t, changed)
234+
})
235+
t.Run("should delete obsolete entries", func(t *testing.T) {
236+
// given
237+
testutil.MustTruncateTables(db)
238+
httpmock.Reset()
239+
c := factory.CreateCharacter()
240+
factory.CreateCharacterToken(storage.UpdateOrCreateCharacterTokenParams{
241+
CharacterID: c.ID,
242+
})
243+
factory.CreateCharacterContactLabel(storage.UpdateOrCreateCharacterContactLabelParams{
244+
CharacterID: c.ID,
245+
})
246+
httpmock.RegisterResponder(
247+
"GET",
248+
fmt.Sprintf("https://esi.evetech.net/characters/%d/contacts/labels", c.ID),
249+
httpmock.NewJsonResponderOrPanic(200, []map[string]any{{
250+
"label_id": 42,
251+
"label_name": "alpha",
252+
}}),
253+
)
254+
// when
255+
changed, err := s.updateContactLabelsESI(ctx, app.CharacterSectionUpdateParams{
256+
CharacterID: c.ID,
257+
Section: app.SectionCharacterContactLabels,
258+
})
259+
// then
260+
require.NoError(t, err)
261+
assert.True(t, changed)
262+
ids, err := st.ListCharacterContactLabelIDs(ctx, c.ID)
263+
require.NoError(t, err)
264+
xassert.Equal(t, set.Of[int64](42), ids)
265+
})
266+
}

internal/app/characterservice/loyaltypoints.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func (s *CharacterService) updateLoyaltyPointEntriesESI(ctx context.Context, arg
103103
return false, err
104104
}
105105
incomingIDs := set.Collect(maps.Keys(incoming))
106-
obsoleteIDs := set.Difference(incomingIDs, currentIDs)
106+
obsoleteIDs := set.Difference(currentIDs, incomingIDs)
107107
if obsoleteIDs.Size() > 0 {
108108
err := s.st.DeleteCharacterLoyaltyPointEntries(ctx, characterID, obsoleteIDs)
109109
if err != nil {

internal/app/characterservice/section.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ func (s *CharacterService) UpdateSectionIfNeeded(ctx context.Context, arg app.Ch
5858
f = s.updateAttributesESI
5959
case app.SectionCharacterContacts:
6060
f = s.updateContactsESI
61+
case app.SectionCharacterContactLabels:
62+
f = s.updateContactLabelsESI
6163
case app.SectionCharacterContracts:
6264
f = s.updateContractsESI
6365
case app.SectionCharacterImplants:

internal/app/contact.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,9 @@ type CharacterContact struct {
5858
Labels set.Set[string]
5959
Standing float64
6060
}
61+
62+
type CharacterContactLabel struct {
63+
CharacterID int64
64+
LabelID int64
65+
Name string
66+
}

internal/app/section.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const (
4848
SectionCharacterAssets CharacterSection = "assets"
4949
SectionCharacterAttributes CharacterSection = "attributes"
5050
SectionCharacterContacts CharacterSection = "contacts"
51+
SectionCharacterContactLabels CharacterSection = "contact_labels"
5152
SectionCharacterContracts CharacterSection = "contracts"
5253
SectionCharacterImplants CharacterSection = "implants"
5354
SectionCharacterIndustryJobs CharacterSection = "industry_jobs"
@@ -74,6 +75,7 @@ var CharacterSections = []CharacterSection{
7475
SectionCharacterAssets,
7576
SectionCharacterAttributes,
7677
SectionCharacterContacts,
78+
SectionCharacterContactLabels,
7779
SectionCharacterContracts,
7880
SectionCharacterImplants,
7981
SectionCharacterIndustryJobs,
@@ -105,6 +107,7 @@ func (cs CharacterSection) Scopes() set.Set[string] {
105107
SectionCharacterAssets: {goesi.ScopeAssetsReadAssetsV1, goesi.ScopeUniverseReadStructuresV1},
106108
SectionCharacterAttributes: {goesi.ScopeSkillsReadSkillsV1},
107109
SectionCharacterContacts: {goesi.ScopeCharactersReadContactsV1},
110+
SectionCharacterContactLabels: {goesi.ScopeCharactersReadContactsV1},
108111
SectionCharacterContracts: {goesi.ScopeContractsReadCharacterContractsV1, goesi.ScopeUniverseReadStructuresV1},
109112
SectionCharacterImplants: {goesi.ScopeClonesReadImplantsV1},
110113
SectionCharacterIndustryJobs: {goesi.ScopeIndustryReadCharacterJobsV1, goesi.ScopeUniverseReadStructuresV1},
@@ -143,6 +146,7 @@ func (cs CharacterSection) Timeout() time.Duration {
143146
SectionCharacterAssets: 3600 * time.Second,
144147
SectionCharacterAttributes: 120 * time.Second,
145148
SectionCharacterContacts: 300 * time.Second,
149+
SectionCharacterContactLabels: 300 * time.Second,
146150
SectionCharacterContracts: 300 * time.Second,
147151
SectionCharacterImplants: 120 * time.Second,
148152
SectionCharacterIndustryJobs: 300 * time.Second,

0 commit comments

Comments
 (0)