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.

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