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.

785 lines
19 KiB

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