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.

922 lines
22 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, sessionThreshold time.Duration, 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. parsed.Log.EventName = eventName
  144. parsed.Log.ChannelName = channelName
  145. log, err := s.logs.Insert(ctx, parsed.Log)
  146. if err != nil {
  147. return nil, err
  148. }
  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. results = append(results, log)
  160. }
  161. }
  162. case models.LogImporterIrcCloud:
  163. {
  164. parseResults, err := parsers.IRCCloudLogs(data, tz, sessionThreshold)
  165. if err != nil {
  166. return nil, err
  167. }
  168. for _, parsed := range parseResults {
  169. parsed.Log.EventName = eventName
  170. parsed.Log.ChannelName = channelName
  171. log, err := s.logs.Insert(ctx, parsed.Log)
  172. if err != nil {
  173. return nil, err
  174. }
  175. for _, post := range parsed.Posts {
  176. post.LogID = log.ShortID
  177. }
  178. posts, err := s.posts.InsertMany(ctx, parsed.Posts...)
  179. if err != nil {
  180. _ = s.logs.Delete(ctx, *log)
  181. return nil, err
  182. }
  183. s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
  184. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Listed(log, posts), log, posts)
  185. results = append(results, log)
  186. }
  187. }
  188. default:
  189. {
  190. return nil, errors.New("Invalid importer: " + importer.String())
  191. }
  192. }
  193. return results, nil
  194. }
  195. func (s *LogService) Update(ctx context.Context, id string, update models.LogUpdate) (*models.Log, error) {
  196. log, err := s.logs.Find(ctx, id)
  197. if err != nil {
  198. return nil, err
  199. }
  200. if err := s.authService.CheckPermission(ctx, "edit", log); err != nil {
  201. return nil, err
  202. }
  203. log, err = s.logs.Update(ctx, *log, update)
  204. if err != nil {
  205. return nil, err
  206. }
  207. s.changeService.Submit(ctx, models.ChangeModelLog, "edit", true, changekeys.Listed(log), log)
  208. return log, nil
  209. }
  210. func (s *LogService) SplitLog(ctx context.Context, logId string, startPostId string) (*models.Log, error) {
  211. // Find log
  212. l, err := s.logs.Find(ctx, logId)
  213. if err != nil {
  214. return nil, err
  215. }
  216. if err := s.authService.CheckPermission(ctx, "add", l); err != nil {
  217. return nil, err
  218. }
  219. // Find posts
  220. posts, err := s.ListPosts(ctx, &models.PostFilter{LogID: &l.ShortID})
  221. if err != nil {
  222. return nil, err
  223. }
  224. if len(posts) == 0 {
  225. return nil, errors.New("cannot split empty log")
  226. }
  227. // Cut the posts slice.
  228. firstPost := posts[0]
  229. cutPosts := posts[len(posts):]
  230. for i, post := range posts {
  231. if post.ID == startPostId {
  232. cutPosts = posts[i:]
  233. firstPost = post
  234. break
  235. }
  236. }
  237. if len(cutPosts) == 0 {
  238. return nil, errors.New("post not found")
  239. }
  240. if len(cutPosts) == len(posts) || firstPost.Time.Equal(l.Date) {
  241. return nil, errors.New("cannot move posts")
  242. }
  243. // Create a new log
  244. newLog := &models.Log{
  245. Date: firstPost.Time,
  246. ChannelName: l.ChannelName,
  247. EventName: l.EventName,
  248. }
  249. newLog, err = s.logs.Insert(ctx, *newLog)
  250. if err != nil {
  251. return nil, err
  252. }
  253. // Put the cut posts in the new log
  254. newPosts := make([]*models.Post, 0, len(cutPosts))
  255. for _, post := range cutPosts {
  256. postCopy := *post
  257. postCopy.ID = ""
  258. postCopy.LogID = newLog.ShortID
  259. newPost, err := s.posts.Insert(ctx, postCopy)
  260. if err != nil {
  261. _ = s.logs.Delete(ctx, *newLog)
  262. return nil, err
  263. }
  264. newPosts = append(newPosts, newPost)
  265. }
  266. // Remove the posts from the old log (this can't error because that'll make a mess)
  267. for _, post := range cutPosts {
  268. err := s.posts.Delete(ctx, *post)
  269. if err != nil {
  270. log.Printf("Failed to delete post %s: %s", post.ID, err)
  271. }
  272. }
  273. // Submit the changes
  274. s.changeService.Submit(ctx, models.ChangeModelPost, "remove", true, changekeys.Many(l, cutPosts), cutPosts)
  275. s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(newLog), newLog)
  276. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Many(newLog, newPosts), newPosts)
  277. // Refresh character lists.
  278. _, _ = s.refreshLogCharacters(ctx, *l, nil, false)
  279. newLog2, err := s.refreshLogCharacters(ctx, *newLog, nil, false)
  280. if err == nil {
  281. newLog = newLog2
  282. }
  283. return newLog, nil
  284. }
  285. func (s *LogService) MergeLogs(ctx context.Context, targetID string, sourceID string, removeAfter bool) (*models.Log, error) {
  286. // Check permissions
  287. if err := s.authService.CheckPermission(ctx, "edit", &models.Log{}); err != nil {
  288. return nil, err
  289. }
  290. if removeAfter {
  291. if err := s.authService.CheckPermission(ctx, "remove", &models.Log{}); err != nil {
  292. return nil, err
  293. }
  294. }
  295. // Merge log posts into log.
  296. source, err := s.logs.Find(ctx, sourceID)
  297. if err != nil {
  298. return nil, errors.New("could not find source log: " + err.Error())
  299. }
  300. target, err := s.logs.Find(ctx, targetID)
  301. if err != nil {
  302. return nil, errors.New("could not find target log: " + err.Error())
  303. }
  304. // Get the source posts.
  305. posts, err := s.posts.List(ctx, models.PostFilter{LogID: &source.ShortID})
  306. if err != nil {
  307. return nil, errors.New("could not fetch source posts: " + err.Error())
  308. }
  309. // Associate the posts with the target logs
  310. for _, post := range posts {
  311. post.ID = ""
  312. post.LogID = target.ShortID
  313. }
  314. // Insert them
  315. posts, err = s.posts.InsertMany(ctx, posts...)
  316. if err != nil {
  317. return nil, errors.New("could not insert posts into target: " + err.Error())
  318. }
  319. // Remove other log
  320. if removeAfter {
  321. err = s.logs.Delete(ctx, *source)
  322. if err != nil {
  323. return nil, errors.New("posts have been inserted, but could not remove source: " + err.Error())
  324. }
  325. s.changeService.Submit(ctx, models.ChangeModelLog, "remove", true, changekeys.Listed(source), source)
  326. }
  327. // Refresh characters
  328. target2, err := s.refreshLogCharacters(ctx, *target, nil, false)
  329. if err != nil {
  330. log.Printf("Failed to update characters in log %s: %s", target.ID, err)
  331. } else {
  332. target = target2
  333. }
  334. // Submit changes after the target
  335. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Many(target, posts), target, posts)
  336. return target, nil
  337. }
  338. func (s *LogService) AddPost(ctx context.Context, logId string, time time.Time, kind, nick, text string) (*models.Post, error) {
  339. if kind == "" || nick == "" || time.IsZero() {
  340. return nil, errors.New("kind, nick and time must be non-empty")
  341. }
  342. l, err := s.logs.Find(ctx, logId)
  343. if err != nil {
  344. return nil, err
  345. }
  346. post := &models.Post{
  347. LogID: l.ShortID,
  348. Kind: kind,
  349. Nick: nick,
  350. Text: text,
  351. Time: time,
  352. }
  353. if err := s.authService.CheckPermission(ctx, "add", post); err != nil {
  354. return nil, err
  355. }
  356. post, err = s.posts.Insert(ctx, *post)
  357. if err != nil {
  358. return nil, err
  359. }
  360. l2, err := s.refreshLogCharacters(ctx, *l, nil, false)
  361. if err != nil {
  362. log.Printf("Failed to update characters in log %s: %s", l.ID, err)
  363. } else {
  364. l = l2
  365. }
  366. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Many(l, post), post)
  367. return post, nil
  368. }
  369. func (s *LogService) EditPost(ctx context.Context, id string, update models.PostUpdate) (*models.Post, error) {
  370. if (update.Kind != nil && *update.Kind == "") || (update.Nick != nil && *update.Nick == "") || (update.Text != nil && *update.Text == "") {
  371. return nil, errors.New("kind, nick and time must be non-empty")
  372. }
  373. post, err := s.posts.Find(ctx, id)
  374. if err != nil {
  375. return nil, err
  376. }
  377. if err := s.authService.CheckPermission(ctx, "edit", post); err != nil {
  378. return nil, err
  379. }
  380. post, err = s.posts.Update(ctx, *post, update)
  381. if err != nil {
  382. return nil, err
  383. }
  384. go func() {
  385. l, err := s.logs.Find(context.Background(), post.LogID)
  386. if err != nil {
  387. return
  388. }
  389. _, err = s.refreshLogCharacters(ctx, *l, nil, false)
  390. if err != nil {
  391. log.Printf("Failed to update characters in log %s: %s", l.ID, err)
  392. }
  393. s.changeService.Submit(ctx, models.ChangeModelPost, "edit", true, changekeys.Many(l, post), post)
  394. }()
  395. return post, nil
  396. }
  397. func (s *LogService) MovePost(ctx context.Context, id string, position int) ([]*models.Post, error) {
  398. if position < 1 {
  399. return nil, repositories.ErrInvalidPosition
  400. }
  401. post, err := s.posts.Find(ctx, id)
  402. if err != nil {
  403. return nil, err
  404. }
  405. if err := s.authService.CheckPermission(ctx, "move", post); err != nil {
  406. return nil, err
  407. }
  408. posts, err := s.posts.Move(ctx, *post, position)
  409. if err != nil {
  410. return nil, err
  411. }
  412. go func() {
  413. if len(posts) == 0 {
  414. return
  415. }
  416. log, err := s.logs.Find(context.Background(), posts[0].LogID)
  417. if err != nil {
  418. return
  419. }
  420. s.changeService.Submit(ctx, models.ChangeModelPost, "move", true, changekeys.Many(log, posts), posts)
  421. }()
  422. return posts, nil
  423. }
  424. func (s *LogService) DeletePost(ctx context.Context, id string) (*models.Post, error) {
  425. post, err := s.posts.Find(ctx, id)
  426. if err != nil {
  427. return nil, err
  428. }
  429. if err := s.authService.CheckPermission(ctx, "remove", post); err != nil {
  430. return nil, err
  431. }
  432. err = s.posts.Delete(ctx, *post)
  433. if err != nil {
  434. return nil, err
  435. }
  436. go func() {
  437. l, err := s.logs.Find(context.Background(), post.LogID)
  438. if err != nil {
  439. return
  440. }
  441. _, err = s.refreshLogCharacters(ctx, *l, nil, false)
  442. if err != nil {
  443. log.Printf("Failed to update characters in log %s: %s", l.ID, err)
  444. }
  445. s.changeService.Submit(ctx, models.ChangeModelPost, "remove", true, changekeys.Many(l, post), post)
  446. }()
  447. return post, nil
  448. }
  449. func (s *LogService) Delete(ctx context.Context, id string) (*models.Log, error) {
  450. log, err := s.logs.Find(ctx, id)
  451. if err != nil {
  452. return nil, err
  453. }
  454. if err := s.authService.CheckPermission(ctx, "remove", log); err != nil {
  455. return nil, err
  456. }
  457. err = s.logs.Delete(ctx, *log)
  458. if err != nil {
  459. return nil, err
  460. }
  461. s.changeService.Submit(ctx, models.ChangeModelLog, "remove", true, changekeys.Listed(log), log)
  462. return log, nil
  463. }
  464. func (s *LogService) FixImportDateBug(ctx context.Context) error {
  465. start := time.Now()
  466. logs, err := s.logs.List(ctx, models.LogFilter{})
  467. if err != nil {
  468. return err
  469. }
  470. eg := errgroup.Group{}
  471. for i := range logs {
  472. l := logs[i]
  473. eg.Go(func() error {
  474. return s.fixImportDateBug(ctx, *l)
  475. })
  476. }
  477. err = eg.Wait()
  478. if err != nil {
  479. return err
  480. }
  481. log.Printf("Date import bug check finished: logs: %d, duration: %s", len(logs), time.Since(start))
  482. return nil
  483. }
  484. func (s *LogService) fixImportDateBug(ctx context.Context, l models.Log) error {
  485. // Find the log's posts.
  486. posts, err := s.ListPosts(ctx, &models.PostFilter{LogID: &l.ShortID})
  487. if err != nil {
  488. return err
  489. }
  490. if len(posts) < 8 {
  491. return nil
  492. }
  493. // Find first action post
  494. first := posts[0]
  495. fi := 0
  496. for first.Kind != "action" && first.Kind != "text" {
  497. fi++
  498. if fi >= len(posts) {
  499. return nil
  500. }
  501. first = posts[fi]
  502. }
  503. last := posts[len(posts)-1]
  504. if first == last {
  505. return nil
  506. }
  507. // Stop here if this log probably isn't affected
  508. if last.Time.Sub(first.Time) < time.Hour*72 {
  509. return nil
  510. }
  511. // Find the first post past midnight.
  512. midnight := first
  513. mi := fi
  514. for i, post := range posts[fi+1:] {
  515. if post.Time.Hour() < first.Time.Hour() {
  516. midnight = post
  517. mi = fi + 1 + i
  518. break
  519. }
  520. }
  521. if midnight == last {
  522. return nil
  523. }
  524. if len(posts[mi+1:]) == 1 {
  525. return nil
  526. }
  527. hits := 0
  528. prev := midnight
  529. for _, offender := range posts[mi+1:] {
  530. if offender.Time.Day() != prev.Time.Day() {
  531. hits += 1
  532. }
  533. prev = offender
  534. }
  535. if hits < ((len(posts[mi+1:]) * 3) / 4) {
  536. return nil
  537. }
  538. for _, offender := range posts[mi+1:] {
  539. ot := offender.Time.UTC()
  540. mt := midnight.Time.UTC()
  541. y, m, d := mt.Date()
  542. hr, mi, se, ns := ot.Hour(), ot.Minute(), ot.Second(), ot.Nanosecond()
  543. newTime := time.Date(y, m, d, hr, mi, se, ns, time.UTC)
  544. _, err := s.posts.Update(ctx, *offender, models.PostUpdate{Time: &newTime})
  545. if err != nil {
  546. return err
  547. }
  548. }
  549. log.Printf("Fixed import date bug in %d posts in log %s", len(posts[mi+1:]), l.ID)
  550. return nil
  551. }
  552. func (s *LogService) RefreshAllLogCharacters(ctx context.Context) error {
  553. start := time.Now()
  554. // Get all logs
  555. logs, err := s.logs.List(ctx, models.LogFilter{})
  556. if err != nil {
  557. return err
  558. }
  559. // Check all characters now instead of later.
  560. characters, err := s.characterService.List(ctx, models.CharacterFilter{})
  561. if err != nil {
  562. return err
  563. }
  564. characterMap := s.makeCharacterMap(characters)
  565. s.unknownNicksMutex.Lock()
  566. for key := range s.unknownNicks {
  567. delete(s.unknownNicks, key)
  568. }
  569. s.unknownNicksMutex.Unlock()
  570. eg := errgroup.Group{}
  571. for i := range logs {
  572. l := logs[i]
  573. eg.Go(func() error {
  574. _, err := s.refreshLogCharacters(ctx, *l, characterMap, true)
  575. return err
  576. })
  577. }
  578. err = eg.Wait()
  579. if err != nil {
  580. return err
  581. }
  582. s.unknownNicksMutex.Lock()
  583. unknownCount := len(s.unknownNicks)
  584. s.unknownNicksMutex.Unlock()
  585. log.Printf("Full log character refresh complete; nicks: %d, unknowns: %d, logs: %d, duration: %s", len(characterMap), unknownCount, len(logs), time.Since(start))
  586. return nil
  587. }
  588. func (s *LogService) RefreshLogCharacters(ctx context.Context, log models.Log) (*models.Log, error) {
  589. return s.refreshLogCharacters(ctx, log, nil, false)
  590. }
  591. func (s *LogService) refreshLogCharacters(ctx context.Context, log models.Log, characterMap map[string]*models.Character, useUnknownNicks bool) (*models.Log, error) {
  592. posts, err := s.ListPosts(ctx, &models.PostFilter{LogID: &log.ShortID})
  593. if err != nil {
  594. return &log, nil
  595. }
  596. counts := make(map[string]int)
  597. added := make(map[string]bool)
  598. removed := make(map[string]bool)
  599. for _, post := range posts {
  600. if post.Kind == "text" || post.Kind == "action" {
  601. if strings.HasPrefix(post.Text, "(") || strings.Contains(post.Nick, "(") || strings.Contains(post.Nick, "[E]") || strings.HasSuffix(post.Nick, "|") {
  602. continue
  603. }
  604. // Clean up the nick (remove possessive suffix, comma, formatting stuff)
  605. if strings.HasSuffix(post.Nick, "'s") || strings.HasSuffix(post.Nick, "`s") {
  606. post.Nick = post.Nick[:len(post.Nick)-2]
  607. } else if strings.HasSuffix(post.Nick, "'") || strings.HasSuffix(post.Nick, "`") || strings.HasSuffix(post.Nick, ",") || strings.HasSuffix(post.Nick, "\x0f") {
  608. post.Nick = post.Nick[:len(post.Nick)-1]
  609. }
  610. added[post.Nick] = true
  611. counts[post.Nick]++
  612. }
  613. if post.Kind == "chars" {
  614. tokens := strings.Fields(post.Text)
  615. for _, token := range tokens {
  616. if strings.HasPrefix(token, "-") {
  617. removed[token[1:]] = true
  618. } else {
  619. added[strings.Replace(token, "+", "", 1)] = true
  620. }
  621. }
  622. }
  623. }
  624. nicks := make([]string, 0, len(added))
  625. for nick := range added {
  626. if added[nick] && !removed[nick] {
  627. nicks = append(nicks, nick)
  628. }
  629. }
  630. if characterMap == nil {
  631. characters, err := s.characterService.List(ctx, models.CharacterFilter{Nicks: nicks})
  632. if err != nil {
  633. return nil, err
  634. }
  635. characterMap = s.makeCharacterMap(characters)
  636. }
  637. log.CharacterIDs = log.CharacterIDs[:0]
  638. for key := range added {
  639. delete(added, key)
  640. }
  641. unknowned := make(map[string]bool)
  642. for _, nick := range nicks {
  643. character := characterMap[nick]
  644. if character == nil {
  645. if useUnknownNicks && !unknowned[nick] {
  646. unknowned[nick] = true
  647. s.unknownNicksMutex.Lock()
  648. s.unknownNicks[nick]++
  649. s.unknownNicksMutex.Unlock()
  650. }
  651. continue
  652. } else if added[character.ID] {
  653. continue
  654. }
  655. added[character.ID] = true
  656. log.CharacterIDs = append(log.CharacterIDs, character.ID)
  657. }
  658. return s.logs.Update(ctx, log, models.LogUpdate{CharacterIDs: log.CharacterIDs})
  659. }
  660. func (s *LogService) makeCharacterMap(characters []*models.Character) map[string]*models.Character {
  661. characterMap := make(map[string]*models.Character, len(characters)*3)
  662. for _, character := range characters {
  663. for _, nick := range character.Nicks {
  664. characterMap[nick] = character
  665. }
  666. }
  667. return characterMap
  668. }
  669. func (s *LogService) NextLogs(ctx context.Context, log *models.Log) ([]*models.LogSuggestion, error) {
  670. minDate := log.Date.Add(time.Millisecond)
  671. logs, err := s.logs.List(ctx, models.LogFilter{
  672. MinDate: &minDate,
  673. })
  674. if err != nil {
  675. return nil, err
  676. }
  677. sort.Slice(logs, func(i, j int) bool {
  678. return logs[i].Date.Before(logs[j].Date)
  679. })
  680. return s.findSuggestions(ctx, log, logs)
  681. }
  682. func (s *LogService) PrevLogs(ctx context.Context, log *models.Log) ([]*models.LogSuggestion, error) {
  683. logs, err := s.logs.List(ctx, models.LogFilter{
  684. MaxDate: &log.Date,
  685. })
  686. if err != nil {
  687. return nil, err
  688. }
  689. return s.findSuggestions(ctx, log, logs)
  690. }
  691. func (s *LogService) findSuggestions(ctx context.Context, log *models.Log, logs []*models.Log) ([]*models.LogSuggestion, error) {
  692. characters, err := s.characterService.List(ctx, models.CharacterFilter{
  693. IDs: log.CharacterIDs,
  694. })
  695. if err != nil {
  696. return nil, err
  697. }
  698. charIntersect := func(l1, l2 *models.Log) []*models.Character {
  699. results := make([]*models.Character, 0, len(l1.CharacterIDs))
  700. for _, c1 := range characters {
  701. for _, c2ID := range l2.CharacterIDs {
  702. if c1.ID == c2ID {
  703. results = append(results, c1)
  704. break
  705. }
  706. }
  707. }
  708. return results
  709. }
  710. groupKey := func(characters []*models.Character) string {
  711. if len(characters) == 0 {
  712. return ""
  713. } else if len(characters) == 1 {
  714. return characters[0].ID
  715. }
  716. builder := strings.Builder{}
  717. builder.WriteString(characters[0].ID)
  718. for _, character := range characters {
  719. builder.WriteRune(',')
  720. builder.WriteString(character.ID)
  721. }
  722. return builder.String()
  723. }
  724. suggestions := make([]*models.LogSuggestion, 0, 16)
  725. foundGroups := make(map[string]bool)
  726. foundChannel := false
  727. for _, log2 := range logs {
  728. hasEvent := log.EventName != "" && log2.EventName == log.EventName
  729. hasChannel := log.ChannelName == log2.ChannelName
  730. characters := charIntersect(log, log2)
  731. groupKey := groupKey(characters)
  732. suggestion := &models.LogSuggestion{
  733. Log: log2,
  734. Characters: characters,
  735. HasChannel: hasChannel,
  736. HasEvent: hasEvent,
  737. }
  738. if hasChannel && foundChannel {
  739. foundChannel = true
  740. foundGroups[groupKey] = true
  741. suggestions = append(suggestions, suggestion)
  742. } else if hasEvent {
  743. foundGroups[groupKey] = true
  744. suggestions = append(suggestions, suggestion)
  745. } else if len(suggestions) < 8 && !hasEvent && len(characters) > 1 && !foundGroups[groupKey] {
  746. foundGroups[groupKey] = true
  747. suggestions = append(suggestions, suggestion)
  748. }
  749. }
  750. return suggestions, nil
  751. }