GraphQL API and utilities for the rpdata project
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.

786 lines
19 KiB

  1. package services
  2. import (
  3. "context"
  4. "errors"
  5. "git.aiterp.net/rpdata/api/internal/auth"
  6. "git.aiterp.net/rpdata/api/models"
  7. "git.aiterp.net/rpdata/api/models/changekeys"
  8. "git.aiterp.net/rpdata/api/models/channels"
  9. "git.aiterp.net/rpdata/api/repositories"
  10. "git.aiterp.net/rpdata/api/services/parsers"
  11. "golang.org/x/sync/errgroup"
  12. "log"
  13. "sort"
  14. "strings"
  15. "sync"
  16. "time"
  17. )
  18. type LogService struct {
  19. logs repositories.LogRepository
  20. posts repositories.PostRepository
  21. changeService *ChangeService
  22. channelService *ChannelService
  23. characterService *CharacterService
  24. unknownNicks map[string]int
  25. unknownNicksMutex sync.Mutex
  26. }
  27. func (s *LogService) Find(ctx context.Context, id string) (*models.Log, error) {
  28. return s.logs.Find(ctx, id)
  29. }
  30. func (s *LogService) FindPosts(ctx context.Context, id string) (*models.Post, error) {
  31. return s.posts.Find(ctx, id)
  32. }
  33. func (s *LogService) List(ctx context.Context, filter *models.LogFilter) ([]*models.Log, error) {
  34. if filter == nil {
  35. filter = &models.LogFilter{}
  36. }
  37. return s.logs.List(ctx, *filter)
  38. }
  39. func (s *LogService) ListPosts(ctx context.Context, filter *models.PostFilter) ([]*models.Post, error) {
  40. // Some sanity checks to avoid querying an insame amount of posts.
  41. if filter == nil {
  42. filter = &models.PostFilter{Limit: 100}
  43. } else {
  44. if (filter.Limit <= 0 || filter.Limit > 512) && (filter.LogID == nil && len(filter.IDs) == 0) {
  45. return nil, errors.New("a limit of 0 (no limit) or >512 without a logId or a set of IDs is not allowed")
  46. }
  47. if len(filter.IDs) > 100 {
  48. return nil, errors.New("you may not query for more than 100 ids, split your query")
  49. }
  50. }
  51. return s.posts.List(ctx, *filter)
  52. }
  53. func (s *LogService) ListUnknownNicks() []*models.UnknownNick {
  54. s.unknownNicksMutex.Lock()
  55. nicks := make([]*models.UnknownNick, 0, len(s.unknownNicks))
  56. for nick, score := range s.unknownNicks {
  57. nicks = append(nicks, &models.UnknownNick{
  58. Nick: nick,
  59. Score: score,
  60. })
  61. }
  62. s.unknownNicksMutex.Unlock()
  63. sort.Slice(nicks, func(i, j int) bool {
  64. return nicks[i].Score > nicks[j].Score
  65. })
  66. return nicks
  67. }
  68. func (s *LogService) Create(ctx context.Context, title, description, channelName, eventName string, open bool) (*models.Log, error) {
  69. if channelName == "" {
  70. return nil, errors.New("channel name cannot be empty")
  71. }
  72. log := &models.Log{
  73. Title: title,
  74. Description: description,
  75. ChannelName: channelName,
  76. EventName: eventName,
  77. Date: time.Now(),
  78. Open: open,
  79. }
  80. if err := auth.CheckPermission(ctx, "add", log); err != nil {
  81. return nil, err
  82. }
  83. _, err := s.channelService.Ensure(ctx, channelName)
  84. if err != nil {
  85. return nil, err
  86. }
  87. log, err = s.logs.Insert(ctx, *log)
  88. if err != nil {
  89. return nil, err
  90. }
  91. s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
  92. return log, nil
  93. }
  94. // Import creates new logs from common formats.
  95. func (s *LogService) Import(ctx context.Context, importer models.LogImporter, date time.Time, tz *time.Location, channelName string, data string) ([]*models.Log, error) {
  96. if err := auth.CheckPermission(ctx, "add", &models.Log{}); err != nil {
  97. return nil, err
  98. }
  99. results := make([]*models.Log, 0, 8)
  100. _, err := s.channelService.Ensure(ctx, channelName)
  101. if err != nil {
  102. return nil, err
  103. }
  104. eventName := ""
  105. if channel, err := channels.FindName(channelName); err == nil {
  106. eventName = channel.EventName
  107. }
  108. date = date.In(tz)
  109. switch importer {
  110. case models.LogImporterMircLike:
  111. {
  112. if date.IsZero() {
  113. return nil, errors.New("date is not optional for mirc-like logs")
  114. }
  115. parsed, err := parsers.MircLog(data, date, true)
  116. if err != nil {
  117. return nil, err
  118. }
  119. parsed.Log.EventName = eventName
  120. parsed.Log.ChannelName = channelName
  121. log, err := s.logs.Insert(ctx, parsed.Log)
  122. if err != nil {
  123. return nil, err
  124. }
  125. for _, post := range parsed.Posts {
  126. post.LogID = log.ShortID
  127. }
  128. posts, err := s.posts.InsertMany(ctx, parsed.Posts...)
  129. if err != nil {
  130. _ = s.logs.Delete(ctx, *log)
  131. return nil, err
  132. }
  133. s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
  134. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Listed(log, posts), log, posts)
  135. results = append(results, log)
  136. }
  137. case models.LogImporterForumLog:
  138. {
  139. parseResults, err := parsers.ForumLog(data, tz)
  140. if err != nil {
  141. return nil, err
  142. }
  143. for _, parsed := range parseResults {
  144. log, err := s.logs.Insert(ctx, parsed.Log)
  145. if err != nil {
  146. return nil, err
  147. }
  148. parsed.Log.EventName = eventName
  149. parsed.Log.ChannelName = channelName
  150. for _, post := range parsed.Posts {
  151. post.LogID = log.ShortID
  152. }
  153. posts, err := s.posts.InsertMany(ctx, parsed.Posts...)
  154. if err != nil {
  155. _ = s.logs.Delete(ctx, *log)
  156. return nil, err
  157. }
  158. s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
  159. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Listed(log, posts), log, posts)
  160. }
  161. }
  162. default:
  163. {
  164. return nil, errors.New("Invalid importer: " + importer.String())
  165. }
  166. }
  167. return results, nil
  168. }
  169. func (s *LogService) Update(ctx context.Context, id string, update models.LogUpdate) (*models.Log, error) {
  170. log, err := s.logs.Find(ctx, id)
  171. if err != nil {
  172. return nil, err
  173. }
  174. if err := auth.CheckPermission(ctx, "edit", log); err != nil {
  175. return nil, err
  176. }
  177. log, err = s.logs.Update(ctx, *log, update)
  178. if err != nil {
  179. return nil, err
  180. }
  181. s.changeService.Submit(ctx, models.ChangeModelLog, "edit", true, changekeys.Listed(log), log)
  182. return log, nil
  183. }
  184. func (s *LogService) SplitLog(ctx context.Context, logId string, startPostId string) (*models.Log, error) {
  185. // Find log
  186. l, err := s.logs.Find(ctx, logId)
  187. if err != nil {
  188. return nil, err
  189. }
  190. if err := auth.CheckPermission(ctx, "add", l); err != nil {
  191. return nil, err
  192. }
  193. // Find posts
  194. posts, err := s.ListPosts(ctx, &models.PostFilter{LogID: &l.ShortID})
  195. if err != nil {
  196. return nil, err
  197. }
  198. if len(posts) == 0 {
  199. return nil, errors.New("cannot split empty log")
  200. }
  201. // Cut the posts slice.
  202. firstPost := posts[0]
  203. cutPosts := posts[len(posts):]
  204. for i, post := range posts {
  205. if post.ID == startPostId {
  206. cutPosts = posts[i:]
  207. firstPost = post
  208. break
  209. }
  210. }
  211. if len(cutPosts) == 0 {
  212. return nil, errors.New("post not found")
  213. }
  214. if len(cutPosts) == len(posts) || firstPost.Time.Equal(l.Date) {
  215. return nil, errors.New("cannot move posts")
  216. }
  217. // Create a new log
  218. newLog := &models.Log{
  219. Date: firstPost.Time,
  220. ChannelName: l.ChannelName,
  221. EventName: l.EventName,
  222. }
  223. newLog, err = s.logs.Insert(ctx, *newLog)
  224. if err != nil {
  225. return nil, err
  226. }
  227. // Put the cut posts in the new log
  228. newPosts := make([]*models.Post, 0, len(cutPosts))
  229. for _, post := range cutPosts {
  230. postCopy := *post
  231. postCopy.ID = ""
  232. postCopy.LogID = newLog.ShortID
  233. newPost, err := s.posts.Insert(ctx, postCopy)
  234. if err != nil {
  235. _ = s.logs.Delete(ctx, *newLog)
  236. return nil, err
  237. }
  238. newPosts = append(newPosts, newPost)
  239. }
  240. // Remove the posts from the old log (this can't error because that'll make a mess)
  241. for _, post := range cutPosts {
  242. err := s.posts.Delete(ctx, *post)
  243. if err != nil {
  244. log.Printf("Failed to delete post %s: %s", post.ID, err)
  245. }
  246. }
  247. // Submit the changes
  248. s.changeService.Submit(ctx, models.ChangeModelPost, "remove", true, changekeys.Many(l, cutPosts), cutPosts)
  249. s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(newLog), newLog)
  250. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Many(newLog, newPosts), newPosts)
  251. // Refresh character lists.
  252. _, _ = s.refreshLogCharacters(ctx, *l, nil, false)
  253. newLog2, err := s.refreshLogCharacters(ctx, *newLog, nil, false)
  254. if err == nil {
  255. newLog = newLog2
  256. }
  257. return newLog, nil
  258. }
  259. func (s *LogService) MergeLogs(ctx context.Context, targetID string, sourceID string, removeAfter bool) (*models.Log, error) {
  260. // Check permissions
  261. if err := auth.CheckPermission(ctx, "edit", &models.Log{}); err != nil {
  262. return nil, err
  263. }
  264. if removeAfter {
  265. if err := auth.CheckPermission(ctx, "remove", &models.Log{}); err != nil {
  266. return nil, err
  267. }
  268. }
  269. // Merge log posts into log.
  270. source, err := s.logs.Find(ctx, sourceID)
  271. if err != nil {
  272. return nil, errors.New("could not find source log: " + err.Error())
  273. }
  274. target, err := s.logs.Find(ctx, targetID)
  275. if err != nil {
  276. return nil, errors.New("could not find target log: " + err.Error())
  277. }
  278. // Get the source posts.
  279. posts, err := s.posts.List(ctx, models.PostFilter{LogID: &source.ShortID})
  280. if err != nil {
  281. return nil, errors.New("could not fetch source posts: " + err.Error())
  282. }
  283. // Associate the posts with the target logs
  284. for _, post := range posts {
  285. post.ID = ""
  286. post.LogID = target.ShortID
  287. }
  288. // Insert them
  289. posts, err = s.posts.InsertMany(ctx, posts...)
  290. if err != nil {
  291. return nil, errors.New("could not insert posts into target: " + err.Error())
  292. }
  293. // Remove other log
  294. if removeAfter {
  295. err = s.logs.Delete(ctx, *source)
  296. if err != nil {
  297. return nil, errors.New("posts have been inserted, but could not remove source: " + err.Error())
  298. }
  299. s.changeService.Submit(ctx, models.ChangeModelLog, "remove", true, changekeys.Listed(source), source)
  300. }
  301. // Refresh characters
  302. target2, err := s.refreshLogCharacters(ctx, *target, nil, false)
  303. if err != nil {
  304. log.Printf("Failed to update characters in log %s: %s", target.ID, err)
  305. } else {
  306. target = target2
  307. }
  308. // Submit changes after the target
  309. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Many(target, posts), target, posts)
  310. return target, nil
  311. }
  312. func (s *LogService) AddPost(ctx context.Context, logId string, time time.Time, kind, nick, text string) (*models.Post, error) {
  313. if kind == "" || nick == "" || time.IsZero() {
  314. return nil, errors.New("kind, nick and time must be non-empty")
  315. }
  316. l, err := s.logs.Find(ctx, logId)
  317. if err != nil {
  318. return nil, err
  319. }
  320. post := &models.Post{
  321. LogID: l.ShortID,
  322. Kind: kind,
  323. Nick: nick,
  324. Text: text,
  325. Time: time,
  326. }
  327. if err := auth.CheckPermission(ctx, "add", post); err != nil {
  328. return nil, err
  329. }
  330. post, err = s.posts.Insert(ctx, *post)
  331. if err != nil {
  332. return nil, err
  333. }
  334. l2, err := s.refreshLogCharacters(ctx, *l, nil, false)
  335. if err != nil {
  336. log.Printf("Failed to update characters in log %s: %s", l.ID, err)
  337. } else {
  338. l = l2
  339. }
  340. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Many(l, post), post)
  341. return post, nil
  342. }
  343. func (s *LogService) EditPost(ctx context.Context, id string, update models.PostUpdate) (*models.Post, error) {
  344. if (update.Kind != nil && *update.Kind == "") || (update.Nick != nil && *update.Nick == "") || (update.Text != nil && *update.Text == "") {
  345. return nil, errors.New("kind, nick and time must be non-empty")
  346. }
  347. post, err := s.posts.Find(ctx, id)
  348. if err != nil {
  349. return nil, err
  350. }
  351. if err := auth.CheckPermission(ctx, "edit", post); err != nil {
  352. return nil, err
  353. }
  354. post, err = s.posts.Update(ctx, *post, update)
  355. if err != nil {
  356. return nil, err
  357. }
  358. go func() {
  359. l, err := s.logs.Find(context.Background(), post.LogID)
  360. if err != nil {
  361. return
  362. }
  363. _, err = s.refreshLogCharacters(ctx, *l, nil, false)
  364. if err != nil {
  365. log.Printf("Failed to update characters in log %s: %s", l.ID, err)
  366. }
  367. s.changeService.Submit(ctx, models.ChangeModelPost, "edit", true, changekeys.Many(l, post), post)
  368. }()
  369. return post, nil
  370. }
  371. func (s *LogService) MovePost(ctx context.Context, id string, position int) ([]*models.Post, error) {
  372. if position < 1 {
  373. return nil, repositories.ErrInvalidPosition
  374. }
  375. post, err := s.posts.Find(ctx, id)
  376. if err != nil {
  377. return nil, err
  378. }
  379. if err := auth.CheckPermission(ctx, "move", post); err != nil {
  380. return nil, err
  381. }
  382. posts, err := s.posts.Move(ctx, *post, position)
  383. if err != nil {
  384. return nil, err
  385. }
  386. go func() {
  387. if len(posts) == 0 {
  388. return
  389. }
  390. log, err := s.logs.Find(context.Background(), posts[0].LogID)
  391. if err != nil {
  392. return
  393. }
  394. s.changeService.Submit(ctx, models.ChangeModelPost, "move", true, changekeys.Many(log, posts), posts)
  395. }()
  396. return posts, nil
  397. }
  398. func (s *LogService) DeletePost(ctx context.Context, id string) (*models.Post, error) {
  399. post, err := s.posts.Find(ctx, id)
  400. if err != nil {
  401. return nil, err
  402. }
  403. if err := auth.CheckPermission(ctx, "remove", post); err != nil {
  404. return nil, err
  405. }
  406. err = s.posts.Delete(ctx, *post)
  407. if err != nil {
  408. return nil, err
  409. }
  410. go func() {
  411. l, err := s.logs.Find(context.Background(), post.LogID)
  412. if err != nil {
  413. return
  414. }
  415. _, err = s.refreshLogCharacters(ctx, *l, nil, false)
  416. if err != nil {
  417. log.Printf("Failed to update characters in log %s: %s", l.ID, err)
  418. }
  419. s.changeService.Submit(ctx, models.ChangeModelPost, "remove", true, changekeys.Many(l, post), post)
  420. }()
  421. return post, nil
  422. }
  423. func (s *LogService) Delete(ctx context.Context, id string) (*models.Log, error) {
  424. log, err := s.logs.Find(ctx, id)
  425. if err != nil {
  426. return nil, err
  427. }
  428. if err := auth.CheckPermission(ctx, "remove", log); err != nil {
  429. return nil, err
  430. }
  431. err = s.logs.Delete(ctx, *log)
  432. if err != nil {
  433. return nil, err
  434. }
  435. s.changeService.Submit(ctx, models.ChangeModelLog, "remove", true, changekeys.Listed(log), log)
  436. return log, nil
  437. }
  438. func (s *LogService) FixImportDateBug(ctx context.Context) error {
  439. start := time.Now()
  440. logs, err := s.logs.List(ctx, models.LogFilter{})
  441. if err != nil {
  442. return err
  443. }
  444. eg := errgroup.Group{}
  445. for i := range logs {
  446. l := logs[i]
  447. eg.Go(func() error {
  448. return s.fixImportDateBug(ctx, *l)
  449. })
  450. }
  451. err = eg.Wait()
  452. if err != nil {
  453. return err
  454. }
  455. log.Printf("Date import bug check finished: logs: %d, duration: %s", len(logs), time.Since(start))
  456. return nil
  457. }
  458. func (s *LogService) fixImportDateBug(ctx context.Context, l models.Log) error {
  459. // Find the log's posts.
  460. posts, err := s.ListPosts(ctx, &models.PostFilter{LogID: &l.ShortID})
  461. if err != nil {
  462. return err
  463. }
  464. if len(posts) < 8 {
  465. return nil
  466. }
  467. // Find first action post
  468. first := posts[0]
  469. fi := 0
  470. for first.Kind != "action" && first.Kind != "text" {
  471. fi++
  472. if fi >= len(posts) {
  473. return nil
  474. }
  475. first = posts[fi]
  476. }
  477. last := posts[len(posts)-1]
  478. if first == last {
  479. return nil
  480. }
  481. // Stop here if this log probably isn't affected
  482. if last.Time.Sub(first.Time) < time.Hour*72 {
  483. return nil
  484. }
  485. // Find the first post past midnight.
  486. midnight := first
  487. mi := fi
  488. for i, post := range posts[fi+1:] {
  489. if post.Time.Hour() < first.Time.Hour() {
  490. midnight = post
  491. mi = fi + 1 + i
  492. break
  493. }
  494. }
  495. if midnight == last {
  496. return nil
  497. }
  498. if len(posts[mi+1:]) == 1 {
  499. return nil
  500. }
  501. hits := 0
  502. prev := midnight
  503. for _, offender := range posts[mi+1:] {
  504. if offender.Time.Day() != prev.Time.Day() {
  505. hits += 1
  506. }
  507. prev = offender
  508. }
  509. if hits < ((len(posts[mi+1:]) * 3) / 4) {
  510. return nil
  511. }
  512. for _, offender := range posts[mi+1:] {
  513. ot := offender.Time.UTC()
  514. mt := midnight.Time.UTC()
  515. y, m, d := mt.Date()
  516. hr, mi, se, ns := ot.Hour(), ot.Minute(), ot.Second(), ot.Nanosecond()
  517. newTime := time.Date(y, m, d, hr, mi, se, ns, time.UTC)
  518. _, err := s.posts.Update(ctx, *offender, models.PostUpdate{Time: &newTime})
  519. if err != nil {
  520. return err
  521. }
  522. }
  523. log.Printf("Fixed import date bug in %d posts in log %s", len(posts[mi+1:]), l.ID)
  524. return nil
  525. }
  526. func (s *LogService) RefreshAllLogCharacters(ctx context.Context) error {
  527. start := time.Now()
  528. // Get all logs
  529. logs, err := s.logs.List(ctx, models.LogFilter{})
  530. if err != nil {
  531. return err
  532. }
  533. // Check all characters now instead of later.
  534. characters, err := s.characterService.List(ctx, models.CharacterFilter{})
  535. if err != nil {
  536. return err
  537. }
  538. characterMap := s.makeCharacterMap(characters)
  539. s.unknownNicksMutex.Lock()
  540. for key := range s.unknownNicks {
  541. delete(s.unknownNicks, key)
  542. }
  543. s.unknownNicksMutex.Unlock()
  544. eg := errgroup.Group{}
  545. for i := range logs {
  546. l := logs[i]
  547. eg.Go(func() error {
  548. _, err := s.refreshLogCharacters(ctx, *l, characterMap, true)
  549. return err
  550. })
  551. }
  552. err = eg.Wait()
  553. if err != nil {
  554. return err
  555. }
  556. s.unknownNicksMutex.Lock()
  557. unknownCount := len(s.unknownNicks)
  558. s.unknownNicksMutex.Unlock()
  559. log.Printf("Full log character refresh complete; nicks: %d, unknowns: %d, logs: %d, duration: %s", len(characterMap), unknownCount, len(logs), time.Since(start))
  560. return nil
  561. }
  562. func (s *LogService) RefreshLogCharacters(ctx context.Context, log models.Log) (*models.Log, error) {
  563. return s.refreshLogCharacters(ctx, log, nil, false)
  564. }
  565. func (s *LogService) refreshLogCharacters(ctx context.Context, log models.Log, characterMap map[string]*models.Character, useUnknownNicks bool) (*models.Log, error) {
  566. posts, err := s.ListPosts(ctx, &models.PostFilter{LogID: &log.ShortID})
  567. if err != nil {
  568. return &log, nil
  569. }
  570. counts := make(map[string]int)
  571. added := make(map[string]bool)
  572. removed := make(map[string]bool)
  573. for _, post := range posts {
  574. if post.Kind == "text" || post.Kind == "action" {
  575. if strings.HasPrefix(post.Text, "(") || strings.Contains(post.Nick, "(") || strings.Contains(post.Nick, "[E]") || strings.HasSuffix(post.Nick, "|") {
  576. continue
  577. }
  578. // Clean up the nick (remove possessive suffix, comma, formatting stuff)
  579. if strings.HasSuffix(post.Nick, "'s") || strings.HasSuffix(post.Nick, "`s") {
  580. post.Nick = post.Nick[:len(post.Nick)-2]
  581. } else if strings.HasSuffix(post.Nick, "'") || strings.HasSuffix(post.Nick, "`") || strings.HasSuffix(post.Nick, ",") || strings.HasSuffix(post.Nick, "\x0f") {
  582. post.Nick = post.Nick[:len(post.Nick)-1]
  583. }
  584. added[post.Nick] = true
  585. counts[post.Nick]++
  586. }
  587. if post.Kind == "chars" {
  588. tokens := strings.Fields(post.Text)
  589. for _, token := range tokens {
  590. if strings.HasPrefix(token, "-") {
  591. removed[token[1:]] = true
  592. } else {
  593. added[strings.Replace(token, "+", "", 1)] = true
  594. }
  595. }
  596. }
  597. }
  598. nicks := make([]string, 0, len(added))
  599. for nick := range added {
  600. if added[nick] && !removed[nick] {
  601. nicks = append(nicks, nick)
  602. }
  603. }
  604. if characterMap == nil {
  605. characters, err := s.characterService.List(ctx, models.CharacterFilter{Nicks: nicks})
  606. if err != nil {
  607. return nil, err
  608. }
  609. characterMap = s.makeCharacterMap(characters)
  610. }
  611. log.CharacterIDs = log.CharacterIDs[:0]
  612. for key := range added {
  613. delete(added, key)
  614. }
  615. unknowned := make(map[string]bool)
  616. for _, nick := range nicks {
  617. character := characterMap[nick]
  618. if character == nil {
  619. if useUnknownNicks && !unknowned[nick] {
  620. unknowned[nick] = true
  621. s.unknownNicksMutex.Lock()
  622. s.unknownNicks[nick]++
  623. s.unknownNicksMutex.Unlock()
  624. }
  625. continue
  626. } else if added[character.ID] {
  627. continue
  628. }
  629. added[character.ID] = true
  630. log.CharacterIDs = append(log.CharacterIDs, character.ID)
  631. }
  632. return s.logs.Update(ctx, log, models.LogUpdate{CharacterIDs: log.CharacterIDs})
  633. }
  634. func (s *LogService) makeCharacterMap(characters []*models.Character) map[string]*models.Character {
  635. characterMap := make(map[string]*models.Character, len(characters)*3)
  636. for _, character := range characters {
  637. for _, nick := range character.Nicks {
  638. characterMap[nick] = character
  639. }
  640. }
  641. return characterMap
  642. }