You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
113 lines
2.8 KiB
113 lines
2.8 KiB
// Package mapping holds the manual source->Slack-email overrides that the admin |
|
// page manages. It supplements automatic email matching for when the Gitea/Notion |
|
// email differs from the Slack email (or isn't present). |
|
package mapping |
|
|
|
import ( |
|
"encoding/json" |
|
"os" |
|
"path/filepath" |
|
"sort" |
|
"sync" |
|
) |
|
|
|
// Entry is one override row, as shown in the admin UI and stored on disk. |
|
type Entry struct { |
|
Source string `json:"source"` // gitea/notion email OR login/username |
|
SlackEmail string `json:"slack_email"` // email to look up in Slack |
|
} |
|
|
|
// Store is a thread-safe, file-backed map of Source -> SlackEmail. |
|
type Store struct { |
|
mu sync.RWMutex |
|
path string |
|
m map[string]string |
|
} |
|
|
|
// New loads the store from path (an empty store if the file is absent). |
|
func New(path string) (*Store, error) { |
|
s := &Store{path: path, m: make(map[string]string)} |
|
|
|
data, err := os.ReadFile(path) |
|
if os.IsNotExist(err) { |
|
return s, nil |
|
} |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
var entries []Entry |
|
if len(data) > 0 { |
|
if err := json.Unmarshal(data, &entries); err != nil { |
|
return nil, err |
|
} |
|
} |
|
for _, e := range entries { |
|
s.m[e.Source] = e.SlackEmail |
|
} |
|
return s, nil |
|
} |
|
|
|
// Resolve returns the Slack email to use for a recipient. It prefers an override |
|
// keyed by email, then by login, otherwise falls back to the original email. |
|
func (s *Store) Resolve(email, login string) string { |
|
s.mu.RLock() |
|
defer s.mu.RUnlock() |
|
if email != "" { |
|
if v, ok := s.m[email]; ok { |
|
return v |
|
} |
|
} |
|
if login != "" { |
|
if v, ok := s.m[login]; ok { |
|
return v |
|
} |
|
} |
|
return email |
|
} |
|
|
|
// List returns all entries sorted by source (for stable UI rendering). |
|
func (s *Store) List() []Entry { |
|
s.mu.RLock() |
|
defer s.mu.RUnlock() |
|
out := make([]Entry, 0, len(s.m)) |
|
for k, v := range s.m { |
|
out = append(out, Entry{Source: k, SlackEmail: v}) |
|
} |
|
sort.Slice(out, func(i, j int) bool { return out[i].Source < out[j].Source }) |
|
return out |
|
} |
|
|
|
// Add inserts/updates an override and persists. |
|
func (s *Store) Add(source, slackEmail string) error { |
|
s.mu.Lock() |
|
defer s.mu.Unlock() |
|
s.m[source] = slackEmail |
|
return s.save() |
|
} |
|
|
|
// Delete removes an override and persists. |
|
func (s *Store) Delete(source string) error { |
|
s.mu.Lock() |
|
defer s.mu.Unlock() |
|
delete(s.m, source) |
|
return s.save() |
|
} |
|
|
|
// save writes the store to disk. Caller must hold the write lock. |
|
func (s *Store) save() error { |
|
entries := make([]Entry, 0, len(s.m)) |
|
for k, v := range s.m { |
|
entries = append(entries, Entry{Source: k, SlackEmail: v}) |
|
} |
|
sort.Slice(entries, func(i, j int) bool { return entries[i].Source < entries[j].Source }) |
|
|
|
data, err := json.MarshalIndent(entries, "", " ") |
|
if err != nil { |
|
return err |
|
} |
|
if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { |
|
return err |
|
} |
|
return os.WriteFile(s.path, data, 0o644) |
|
}
|
|
|