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.

955 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. timeout, cancel := context.WithTimeout(context.Background(), time.Minute*10)
  402. defer cancel()
  403. s.changeService.Submit(timeout, models.ChangeModelPost, "edit", true, changekeys.Many(l, post), post)
  404. _, err = s.refreshLogCharacters(timeout, *l, nil, false)
  405. if err != nil {
  406. log.Printf("Failed to update characters in log %s: %s", l.ID, err)
  407. }
  408. }()
  409. return post, nil
  410. }
  411. func (s *LogService) MovePost(ctx context.Context, id string, position int) ([]*models.Post, error) {
  412. if position < 1 {
  413. return nil, repositories.ErrInvalidPosition
  414. }
  415. post, err := s.posts.Find(ctx, id)
  416. if err != nil {
  417. return nil, err
  418. }
  419. if err := s.authService.CheckPermission(ctx, "move", post); err != nil {
  420. return nil, err
  421. }
  422. posts, err := s.posts.Move(ctx, *post, position)
  423. if err != nil {
  424. return nil, err
  425. }
  426. go func() {
  427. if len(posts) == 0 {
  428. return
  429. }
  430. log, err := s.logs.Find(context.Background(), posts[0].LogID)
  431. if err != nil {
  432. return
  433. }
  434. s.changeService.Submit(ctx, models.ChangeModelPost, "move", true, changekeys.Many(log, posts), posts)
  435. }()
  436. return posts, nil
  437. }
  438. func (s *LogService) DeletePost(ctx context.Context, id string) (*models.Post, error) {
  439. post, err := s.posts.Find(ctx, id)
  440. if err != nil {
  441. return nil, err
  442. }
  443. if err := s.authService.CheckPermission(ctx, "remove", post); err != nil {
  444. return nil, err
  445. }
  446. err = s.posts.Delete(ctx, *post)
  447. if err != nil {
  448. return nil, err
  449. }
  450. go func() {
  451. l, err := s.logs.Find(context.Background(), post.LogID)
  452. if err != nil {
  453. return
  454. }
  455. _, err = s.refreshLogCharacters(ctx, *l, nil, false)
  456. if err != nil {
  457. log.Printf("Failed to update characters in log %s: %s", l.ID, err)
  458. }
  459. s.changeService.Submit(ctx, models.ChangeModelPost, "remove", true, changekeys.Many(l, post), post)
  460. }()
  461. return post, nil
  462. }
  463. func (s *LogService) Delete(ctx context.Context, id string) (*models.Log, error) {
  464. log, err := s.logs.Find(ctx, id)
  465. if err != nil {
  466. return nil, err
  467. }
  468. if err := s.authService.CheckPermission(ctx, "remove", log); err != nil {
  469. return nil, err
  470. }
  471. err = s.logs.Delete(ctx, *log)
  472. if err != nil {
  473. return nil, err
  474. }
  475. s.changeService.Submit(ctx, models.ChangeModelLog, "remove", true, changekeys.Listed(log), log)
  476. return log, nil
  477. }
  478. func (s *LogService) FixImportDateBug(ctx context.Context) error {
  479. start := time.Now()
  480. logs, err := s.logs.List(ctx, models.LogFilter{})
  481. if err != nil {
  482. return err
  483. }
  484. eg := errgroup.Group{}
  485. for i := range logs {
  486. l := logs[i]
  487. eg.Go(func() error {
  488. return s.fixImportDateBug(ctx, *l)
  489. })
  490. }
  491. err = eg.Wait()
  492. if err != nil {
  493. return err
  494. }
  495. log.Printf("Date import bug check finished: logs: %d, duration: %s", len(logs), time.Since(start))
  496. return nil
  497. }
  498. func (s *LogService) fixImportDateBug(ctx context.Context, l models.Log) error {
  499. // Find the log's posts.
  500. posts, err := s.ListPosts(ctx, &models.PostFilter{LogID: &l.ShortID})
  501. if err != nil {
  502. return err
  503. }
  504. if len(posts) < 8 {
  505. return nil
  506. }
  507. // Find first action post
  508. first := posts[0]
  509. fi := 0
  510. for first.Kind != "action" && first.Kind != "text" {
  511. fi++
  512. if fi >= len(posts) {
  513. return nil
  514. }
  515. first = posts[fi]
  516. }
  517. last := posts[len(posts)-1]
  518. if first == last {
  519. return nil
  520. }
  521. // Stop here if this log probably isn't affected
  522. if last.Time.Sub(first.Time) < time.Hour*72 {
  523. return nil
  524. }
  525. // Find the first post past midnight.
  526. midnight := first
  527. mi := fi
  528. for i, post := range posts[fi+1:] {
  529. if post.Time.Hour() < first.Time.Hour() {
  530. midnight = post
  531. mi = fi + 1 + i
  532. break
  533. }
  534. }
  535. if midnight == last {
  536. return nil
  537. }
  538. if len(posts[mi+1:]) == 1 {
  539. return nil
  540. }
  541. hits := 0
  542. prev := midnight
  543. for _, offender := range posts[mi+1:] {
  544. if offender.Time.Day() != prev.Time.Day() {
  545. hits += 1
  546. }
  547. prev = offender
  548. }
  549. if hits < ((len(posts[mi+1:]) * 3) / 4) {
  550. return nil
  551. }
  552. for _, offender := range posts[mi+1:] {
  553. ot := offender.Time.UTC()
  554. mt := midnight.Time.UTC()
  555. y, m, d := mt.Date()
  556. hr, mi, se, ns := ot.Hour(), ot.Minute(), ot.Second(), ot.Nanosecond()
  557. newTime := time.Date(y, m, d, hr, mi, se, ns, time.UTC)
  558. _, err := s.posts.Update(ctx, *offender, models.PostUpdate{Time: &newTime})
  559. if err != nil {
  560. return err
  561. }
  562. }
  563. log.Printf("Fixed import date bug in %d posts in log %s", len(posts[mi+1:]), l.ID)
  564. return nil
  565. }
  566. func (s *LogService) RefreshAllLogCharacters(ctx context.Context) error {
  567. start := time.Now()
  568. // Get all logs
  569. logs, err := s.logs.List(ctx, models.LogFilter{})
  570. if err != nil {
  571. return err
  572. }
  573. // Check all characters now instead of later.
  574. characters, err := s.characterService.List(ctx, models.CharacterFilter{})
  575. if err != nil {
  576. return err
  577. }
  578. characterMap := s.makeCharacterMap(characters)
  579. s.unknownNicksMutex.Lock()
  580. for key := range s.unknownNicks {
  581. delete(s.unknownNicks, key)
  582. }
  583. s.unknownNicksMutex.Unlock()
  584. tokens := make(chan struct{}, 33)
  585. for i := 0; i < 32; i++ {
  586. tokens <- struct{}{}
  587. }
  588. eg := errgroup.Group{}
  589. for i := range logs {
  590. l := logs[i]
  591. eg.Go(func() error {
  592. <-tokens
  593. defer func() { tokens <- struct{}{} }()
  594. _, err := s.refreshLogCharacters(ctx, *l, characterMap, true)
  595. return err
  596. })
  597. }
  598. err = eg.Wait()
  599. if err != nil {
  600. return err
  601. }
  602. s.unknownNicksMutex.Lock()
  603. unknownCount := len(s.unknownNicks)
  604. s.unknownNicksMutex.Unlock()
  605. log.Printf("Full log character refresh complete; nicks: %d, unknowns: %d, logs: %d, duration: %s", len(characterMap), unknownCount, len(logs), time.Since(start))
  606. return nil
  607. }
  608. func (s *LogService) RefreshLogCharacters(ctx context.Context, log models.Log) (*models.Log, error) {
  609. return s.refreshLogCharacters(ctx, log, nil, false)
  610. }
  611. func (s *LogService) refreshLogCharacters(ctx context.Context, log models.Log, characterMap map[string]*models.Character, useUnknownNicks bool) (*models.Log, error) {
  612. posts, err := s.ListPosts(ctx, &models.PostFilter{LogID: &log.ShortID})
  613. if err != nil {
  614. return nil, err
  615. }
  616. counts := make(map[string]int)
  617. added := make(map[string]bool)
  618. removed := make(map[string]bool)
  619. for _, post := range posts {
  620. if post.Kind == "text" || post.Kind == "action" {
  621. if strings.HasPrefix(post.Text, "(") || strings.Contains(post.Nick, "(") || strings.Contains(post.Nick, "[E]") || strings.HasSuffix(post.Nick, "|") {
  622. continue
  623. }
  624. // Clean up the nick (remove possessive suffix, comma, formatting stuff)
  625. if strings.HasSuffix(post.Nick, "'s") || strings.HasSuffix(post.Nick, "`s") {
  626. post.Nick = post.Nick[:len(post.Nick)-2]
  627. } else if strings.HasSuffix(post.Nick, "'") || strings.HasSuffix(post.Nick, "`") || strings.HasSuffix(post.Nick, ",") || strings.HasSuffix(post.Nick, "\x0f") {
  628. post.Nick = post.Nick[:len(post.Nick)-1]
  629. }
  630. added[post.Nick] = true
  631. counts[post.Nick]++
  632. }
  633. if post.Kind == "chars" {
  634. tokens := strings.Fields(post.Text)
  635. for _, token := range tokens {
  636. if strings.HasPrefix(token, "-") {
  637. removed[token[1:]] = true
  638. } else {
  639. added[strings.Replace(token, "+", "", 1)] = true
  640. }
  641. }
  642. }
  643. }
  644. nicks := make([]string, 0, len(added))
  645. for nick := range added {
  646. if added[nick] && !removed[nick] {
  647. nicks = append(nicks, nick)
  648. }
  649. }
  650. if characterMap == nil {
  651. characters, err := s.characterService.List(ctx, models.CharacterFilter{Nicks: nicks})
  652. if err != nil {
  653. return nil, err
  654. }
  655. characterMap = s.makeCharacterMap(characters)
  656. }
  657. log.CharacterIDs = log.CharacterIDs[:0]
  658. for key := range added {
  659. delete(added, key)
  660. }
  661. unknowned := make(map[string]bool)
  662. for _, nick := range nicks {
  663. character := characterMap[nick]
  664. if character == nil {
  665. if useUnknownNicks && !unknowned[nick] {
  666. unknowned[nick] = true
  667. s.unknownNicksMutex.Lock()
  668. s.unknownNicks[nick]++
  669. s.unknownNicksMutex.Unlock()
  670. }
  671. continue
  672. } else if added[character.ID] {
  673. continue
  674. }
  675. added[character.ID] = true
  676. log.CharacterIDs = append(log.CharacterIDs, character.ID)
  677. }
  678. return s.logs.Update(ctx, log, models.LogUpdate{CharacterIDs: log.CharacterIDs})
  679. }
  680. func (s *LogService) makeCharacterMap(characters []*models.Character) map[string]*models.Character {
  681. characterMap := make(map[string]*models.Character, len(characters)*3)
  682. for _, character := range characters {
  683. for _, nick := range character.Nicks {
  684. characterMap[nick] = character
  685. }
  686. }
  687. return characterMap
  688. }
  689. func (s *LogService) NextLogs(ctx context.Context, log *models.Log) ([]*models.LogSuggestion, error) {
  690. minDate := log.Date.Add(time.Millisecond)
  691. logs, err := s.logs.List(ctx, models.LogFilter{
  692. MinDate: &minDate,
  693. })
  694. if err != nil {
  695. return nil, err
  696. }
  697. sort.Slice(logs, func(i, j int) bool {
  698. return logs[i].Date.Before(logs[j].Date)
  699. })
  700. if len(logs) >= 1 && logs[0].ID == log.ID {
  701. logs = logs[1:]
  702. }
  703. return s.findSuggestions(ctx, log, logs)
  704. }
  705. func (s *LogService) PrevLogs(ctx context.Context, log *models.Log) ([]*models.LogSuggestion, error) {
  706. logs, err := s.logs.List(ctx, models.LogFilter{
  707. MaxDate: &log.Date,
  708. })
  709. if err != nil {
  710. return nil, err
  711. }
  712. if len(logs) >= 1 && logs[0].ID == log.ID {
  713. logs = logs[1:]
  714. }
  715. return s.findSuggestions(ctx, log, logs)
  716. }
  717. func (s *LogService) findSuggestions(ctx context.Context, log *models.Log, logs []*models.Log) ([]*models.LogSuggestion, error) {
  718. characters, err := s.characterService.List(ctx, models.CharacterFilter{
  719. IDs: log.CharacterIDs,
  720. })
  721. if err != nil {
  722. return nil, err
  723. }
  724. charIntersect := func(l1, l2 *models.Log) []*models.Character {
  725. results := make([]*models.Character, 0, len(l1.CharacterIDs))
  726. for _, c1 := range characters {
  727. for _, c2ID := range l2.CharacterIDs {
  728. if c1.ID == c2ID {
  729. results = append(results, c1)
  730. break
  731. }
  732. }
  733. }
  734. return results
  735. }
  736. groupKey := func(characters []*models.Character) string {
  737. if len(characters) == 0 {
  738. return ""
  739. } else if len(characters) == 1 {
  740. return characters[0].ID
  741. }
  742. builder := strings.Builder{}
  743. builder.WriteString(characters[0].ID)
  744. for _, character := range characters {
  745. builder.WriteRune(',')
  746. builder.WriteString(character.ID)
  747. }
  748. return builder.String()
  749. }
  750. suggestions := make([]*models.LogSuggestion, 0, 16)
  751. foundGroups := make(map[string]bool)
  752. foundChannel := false
  753. for _, log2 := range logs {
  754. hasEvent := log.EventName != "" && log2.EventName == log.EventName
  755. hasChannel := log.ChannelName == log2.ChannelName
  756. characters := charIntersect(log, log2)
  757. groupKey := groupKey(characters)
  758. suggestion := &models.LogSuggestion{
  759. Log: log2,
  760. Characters: characters,
  761. HasChannel: hasChannel,
  762. HasEvent: hasEvent,
  763. }
  764. if hasChannel && foundChannel {
  765. foundChannel = true
  766. foundGroups[groupKey] = true
  767. suggestions = append(suggestions, suggestion)
  768. } else if hasEvent {
  769. foundGroups[groupKey] = true
  770. suggestions = append(suggestions, suggestion)
  771. } else if len(suggestions) < 8 && !hasEvent && len(characters) > 1 && !foundGroups[groupKey] {
  772. foundGroups[groupKey] = true
  773. suggestions = append(suggestions, suggestion)
  774. }
  775. }
  776. return suggestions, nil
  777. }