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.

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