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.

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