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.

743 lines
18 KiB

  1. package services
  2. import (
  3. "context"
  4. "errors"
  5. "git.aiterp.net/rpdata/api/internal/auth"
  6. "git.aiterp.net/rpdata/api/models"
  7. "git.aiterp.net/rpdata/api/models/changekeys"
  8. "git.aiterp.net/rpdata/api/models/channels"
  9. "git.aiterp.net/rpdata/api/repositories"
  10. "git.aiterp.net/rpdata/api/services/parsers"
  11. "golang.org/x/sync/errgroup"
  12. "log"
  13. "strings"
  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. }
  23. func (s *LogService) Find(ctx context.Context, id string) (*models.Log, error) {
  24. return s.logs.Find(ctx, id)
  25. }
  26. func (s *LogService) FindPosts(ctx context.Context, id string) (*models.Post, error) {
  27. return s.posts.Find(ctx, id)
  28. }
  29. func (s *LogService) List(ctx context.Context, filter *models.LogFilter) ([]*models.Log, error) {
  30. if filter == nil {
  31. filter = &models.LogFilter{}
  32. }
  33. return s.logs.List(ctx, *filter)
  34. }
  35. func (s *LogService) ListPosts(ctx context.Context, filter *models.PostFilter) ([]*models.Post, error) {
  36. // Some sanity checks to avoid querying an insame amount of posts.
  37. if filter == nil {
  38. filter = &models.PostFilter{Limit: 100}
  39. } else {
  40. if (filter.Limit <= 0 || filter.Limit > 512) && (filter.LogID == nil && len(filter.IDs) == 0) {
  41. return nil, errors.New("a limit of 0 (no limit) or >512 without a logId or a set of IDs is not allowed")
  42. }
  43. if len(filter.IDs) > 100 {
  44. return nil, errors.New("you may not query for more than 100 ids, split your query")
  45. }
  46. }
  47. return s.posts.List(ctx, *filter)
  48. }
  49. func (s *LogService) Create(ctx context.Context, title, description, channelName, eventName string, open bool) (*models.Log, error) {
  50. if channelName == "" {
  51. return nil, errors.New("channel name cannot be empty")
  52. }
  53. log := &models.Log{
  54. Title: title,
  55. Description: description,
  56. ChannelName: channelName,
  57. EventName: eventName,
  58. Date: time.Now(),
  59. Open: open,
  60. }
  61. if err := auth.CheckPermission(ctx, "add", log); err != nil {
  62. return nil, err
  63. }
  64. _, err := s.channelService.Ensure(ctx, channelName)
  65. if err != nil {
  66. return nil, err
  67. }
  68. log, err = s.logs.Insert(ctx, *log)
  69. if err != nil {
  70. return nil, err
  71. }
  72. s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
  73. return log, nil
  74. }
  75. // Import creates new logs from common formats.
  76. func (s *LogService) Import(ctx context.Context, importer models.LogImporter, date time.Time, tz *time.Location, channelName string, data string) ([]*models.Log, error) {
  77. if err := auth.CheckPermission(ctx, "add", &models.Log{}); err != nil {
  78. return nil, err
  79. }
  80. results := make([]*models.Log, 0, 8)
  81. _, err := s.channelService.Ensure(ctx, channelName)
  82. if err != nil {
  83. return nil, err
  84. }
  85. eventName := ""
  86. if channel, err := channels.FindName(channelName); err == nil {
  87. eventName = channel.EventName
  88. }
  89. date = date.In(tz)
  90. switch importer {
  91. case models.LogImporterMircLike:
  92. {
  93. if date.IsZero() {
  94. return nil, errors.New("date is not optional for mirc-like logs")
  95. }
  96. parsed, err := parsers.MircLog(data, date, true)
  97. if err != nil {
  98. return nil, err
  99. }
  100. parsed.Log.EventName = eventName
  101. parsed.Log.ChannelName = channelName
  102. log, err := s.logs.Insert(ctx, parsed.Log)
  103. if err != nil {
  104. return nil, err
  105. }
  106. for _, post := range parsed.Posts {
  107. post.LogID = log.ShortID
  108. }
  109. posts, err := s.posts.InsertMany(ctx, parsed.Posts...)
  110. if err != nil {
  111. _ = s.logs.Delete(ctx, *log)
  112. return nil, err
  113. }
  114. s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
  115. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Listed(log, posts), log, posts)
  116. results = append(results, log)
  117. }
  118. case models.LogImporterForumLog:
  119. {
  120. parseResults, err := parsers.ForumLog(data, tz)
  121. if err != nil {
  122. return nil, err
  123. }
  124. for _, parsed := range parseResults {
  125. log, err := s.logs.Insert(ctx, parsed.Log)
  126. if err != nil {
  127. return nil, err
  128. }
  129. parsed.Log.EventName = eventName
  130. parsed.Log.ChannelName = channelName
  131. for _, post := range parsed.Posts {
  132. post.LogID = log.ShortID
  133. }
  134. posts, err := s.posts.InsertMany(ctx, parsed.Posts...)
  135. if err != nil {
  136. _ = s.logs.Delete(ctx, *log)
  137. return nil, err
  138. }
  139. s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
  140. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Listed(log, posts), log, posts)
  141. }
  142. }
  143. default:
  144. {
  145. return nil, errors.New("Invalid importer: " + importer.String())
  146. }
  147. }
  148. return results, nil
  149. }
  150. func (s *LogService) Update(ctx context.Context, id string, update models.LogUpdate) (*models.Log, error) {
  151. log, err := s.logs.Find(ctx, id)
  152. if err != nil {
  153. return nil, err
  154. }
  155. if err := auth.CheckPermission(ctx, "edit", log); err != nil {
  156. return nil, err
  157. }
  158. log, err = s.logs.Update(ctx, *log, update)
  159. if err != nil {
  160. return nil, err
  161. }
  162. s.changeService.Submit(ctx, models.ChangeModelLog, "edit", true, changekeys.Listed(log), log)
  163. return log, nil
  164. }
  165. func (s *LogService) SplitLog(ctx context.Context, logId string, startPostId string) (*models.Log, error) {
  166. // Find log
  167. l, err := s.logs.Find(ctx, logId)
  168. if err != nil {
  169. return nil, err
  170. }
  171. if err := auth.CheckPermission(ctx, "add", l); err != nil {
  172. return nil, err
  173. }
  174. // Find posts
  175. posts, err := s.ListPosts(ctx, &models.PostFilter{LogID: &l.ShortID})
  176. if err != nil {
  177. return nil, err
  178. }
  179. if len(posts) == 0 {
  180. return nil, errors.New("cannot split empty log")
  181. }
  182. // Cut the posts slice.
  183. firstPost := posts[0]
  184. cutPosts := posts[len(posts):]
  185. for i, post := range posts {
  186. if post.ID == startPostId {
  187. cutPosts = posts[i:]
  188. firstPost = post
  189. break
  190. }
  191. }
  192. if len(cutPosts) == 0 {
  193. return nil, errors.New("post not found")
  194. }
  195. if len(cutPosts) == len(posts) || firstPost.Time.Equal(l.Date) {
  196. return nil, errors.New("cannot move posts")
  197. }
  198. // Create a new log
  199. newLog := &models.Log{
  200. Date: firstPost.Time,
  201. ChannelName: l.ChannelName,
  202. EventName: l.EventName,
  203. }
  204. newLog, err = s.logs.Insert(ctx, *newLog)
  205. if err != nil {
  206. return nil, err
  207. }
  208. // Put the cut posts in the new log
  209. newPosts := make([]*models.Post, 0, len(cutPosts))
  210. for _, post := range cutPosts {
  211. postCopy := *post
  212. postCopy.ID = ""
  213. postCopy.LogID = newLog.ShortID
  214. newPost, err := s.posts.Insert(ctx, postCopy)
  215. if err != nil {
  216. _ = s.logs.Delete(ctx, *newLog)
  217. return nil, err
  218. }
  219. newPosts = append(newPosts, newPost)
  220. }
  221. // Remove the posts from the old log (this can't error because that'll make a mess)
  222. for _, post := range cutPosts {
  223. err := s.posts.Delete(ctx, *post)
  224. if err != nil {
  225. log.Printf("Failed to delete post %s: %s", post.ID, err)
  226. }
  227. }
  228. // Submit the changes
  229. s.changeService.Submit(ctx, models.ChangeModelPost, "remove", true, changekeys.Many(l, cutPosts), cutPosts)
  230. s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(newLog), newLog)
  231. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Many(newLog, newPosts), newPosts)
  232. // Refresh character lists.
  233. _, _ = s.refreshLogCharacters(ctx, *l, nil)
  234. newLog2, err := s.refreshLogCharacters(ctx, *newLog, nil)
  235. if err == nil {
  236. newLog = newLog2
  237. }
  238. return newLog, nil
  239. }
  240. func (s *LogService) MergeLogs(ctx context.Context, targetID string, sourceID string, removeAfter bool) (*models.Log, error) {
  241. // Check permissions
  242. if err := auth.CheckPermission(ctx, "edit", &models.Log{}); err != nil {
  243. return nil, err
  244. }
  245. if removeAfter {
  246. if err := auth.CheckPermission(ctx, "remove", &models.Log{}); err != nil {
  247. return nil, err
  248. }
  249. }
  250. // Merge log posts into log.
  251. source, err := s.logs.Find(ctx, sourceID)
  252. if err != nil {
  253. return nil, errors.New("could not find source log: " + err.Error())
  254. }
  255. target, err := s.logs.Find(ctx, targetID)
  256. if err != nil {
  257. return nil, errors.New("could not find target log: " + err.Error())
  258. }
  259. // Get the source posts.
  260. posts, err := s.posts.List(ctx, models.PostFilter{LogID: &source.ShortID})
  261. if err != nil {
  262. return nil, errors.New("could not fetch source posts: " + err.Error())
  263. }
  264. // Associate the posts with the target logs
  265. for _, post := range posts {
  266. post.ID = ""
  267. post.LogID = target.ShortID
  268. }
  269. // Insert them
  270. posts, err = s.posts.InsertMany(ctx, posts...)
  271. if err != nil {
  272. return nil, errors.New("could not insert posts into target: " + err.Error())
  273. }
  274. // Remove other log
  275. if removeAfter {
  276. err = s.logs.Delete(ctx, *source)
  277. if err != nil {
  278. return nil, errors.New("posts have been inserted, but could not remove source: " + err.Error())
  279. }
  280. s.changeService.Submit(ctx, models.ChangeModelLog, "remove", true, changekeys.Listed(source), source)
  281. }
  282. // Refresh characters
  283. target2, err := s.refreshLogCharacters(ctx, *target, nil)
  284. if err != nil {
  285. log.Printf("Failed to update characters in log %s: %s", target.ID, err)
  286. } else {
  287. target = target2
  288. }
  289. // Submit changes after the target
  290. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Many(target, posts), target, posts)
  291. return target, nil
  292. }
  293. func (s *LogService) AddPost(ctx context.Context, logId string, time time.Time, kind, nick, text string) (*models.Post, error) {
  294. if kind == "" || nick == "" || time.IsZero() {
  295. return nil, errors.New("kind, nick and time must be non-empty")
  296. }
  297. l, err := s.logs.Find(ctx, logId)
  298. if err != nil {
  299. return nil, err
  300. }
  301. post := &models.Post{
  302. LogID: l.ShortID,
  303. Kind: kind,
  304. Nick: nick,
  305. Text: text,
  306. Time: time,
  307. }
  308. if err := auth.CheckPermission(ctx, "add", post); err != nil {
  309. return nil, err
  310. }
  311. post, err = s.posts.Insert(ctx, *post)
  312. if err != nil {
  313. return nil, err
  314. }
  315. l2, err := s.refreshLogCharacters(ctx, *l, nil)
  316. if err != nil {
  317. log.Printf("Failed to update characters in log %s: %s", l.ID, err)
  318. } else {
  319. l = l2
  320. }
  321. s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Many(l, post), post)
  322. return post, nil
  323. }
  324. func (s *LogService) EditPost(ctx context.Context, id string, update models.PostUpdate) (*models.Post, error) {
  325. if (update.Kind != nil && *update.Kind == "") || (update.Nick != nil && *update.Nick == "") || (update.Text != nil && *update.Text == "") {
  326. return nil, errors.New("kind, nick and time must be non-empty")
  327. }
  328. post, err := s.posts.Find(ctx, id)
  329. if err != nil {
  330. return nil, err
  331. }
  332. if err := auth.CheckPermission(ctx, "edit", post); err != nil {
  333. return nil, err
  334. }
  335. post, err = s.posts.Update(ctx, *post, update)
  336. if err != nil {
  337. return nil, err
  338. }
  339. go func() {
  340. l, err := s.logs.Find(context.Background(), post.LogID)
  341. if err != nil {
  342. return
  343. }
  344. _, err = s.refreshLogCharacters(ctx, *l, nil)
  345. if err != nil {
  346. log.Printf("Failed to update characters in log %s: %s", l.ID, err)
  347. }
  348. s.changeService.Submit(ctx, models.ChangeModelPost, "edit", true, changekeys.Many(l, post), post)
  349. }()
  350. return post, nil
  351. }
  352. func (s *LogService) MovePost(ctx context.Context, id string, position int) ([]*models.Post, error) {
  353. if position < 1 {
  354. return nil, repositories.ErrInvalidPosition
  355. }
  356. post, err := s.posts.Find(ctx, id)
  357. if err != nil {
  358. return nil, err
  359. }
  360. if err := auth.CheckPermission(ctx, "move", post); err != nil {
  361. return nil, err
  362. }
  363. posts, err := s.posts.Move(ctx, *post, position)
  364. if err != nil {
  365. return nil, err
  366. }
  367. go func() {
  368. if len(posts) == 0 {
  369. return
  370. }
  371. log, err := s.logs.Find(context.Background(), posts[0].LogID)
  372. if err != nil {
  373. return
  374. }
  375. s.changeService.Submit(ctx, models.ChangeModelPost, "move", true, changekeys.Many(log, posts), posts)
  376. }()
  377. return posts, nil
  378. }
  379. func (s *LogService) DeletePost(ctx context.Context, id string) (*models.Post, error) {
  380. post, err := s.posts.Find(ctx, id)
  381. if err != nil {
  382. return nil, err
  383. }
  384. if err := auth.CheckPermission(ctx, "remove", post); err != nil {
  385. return nil, err
  386. }
  387. err = s.posts.Delete(ctx, *post)
  388. if err != nil {
  389. return nil, err
  390. }
  391. go func() {
  392. l, err := s.logs.Find(context.Background(), post.LogID)
  393. if err != nil {
  394. return
  395. }
  396. _, err = s.refreshLogCharacters(ctx, *l, nil)
  397. if err != nil {
  398. log.Printf("Failed to update characters in log %s: %s", l.ID, err)
  399. }
  400. s.changeService.Submit(ctx, models.ChangeModelPost, "remove", true, changekeys.Many(l, post), post)
  401. }()
  402. return post, nil
  403. }
  404. func (s *LogService) Delete(ctx context.Context, id string) (*models.Log, error) {
  405. log, err := s.logs.Find(ctx, id)
  406. if err != nil {
  407. return nil, err
  408. }
  409. if err := auth.CheckPermission(ctx, "remove", log); err != nil {
  410. return nil, err
  411. }
  412. err = s.logs.Delete(ctx, *log)
  413. if err != nil {
  414. return nil, err
  415. }
  416. s.changeService.Submit(ctx, models.ChangeModelLog, "remove", true, changekeys.Listed(log), log)
  417. return log, nil
  418. }
  419. func (s *LogService) FixImportDateBug(ctx context.Context) error {
  420. start := time.Now()
  421. logs, err := s.logs.List(ctx, models.LogFilter{})
  422. if err != nil {
  423. return err
  424. }
  425. eg := errgroup.Group{}
  426. for i := range logs {
  427. l := logs[i]
  428. eg.Go(func() error {
  429. return s.fixImportDateBug(ctx, *l)
  430. })
  431. }
  432. err = eg.Wait()
  433. if err != nil {
  434. return err
  435. }
  436. log.Printf("Date import bug check finished: logs: %d, duration: %s", len(logs), time.Since(start))
  437. return nil
  438. }
  439. func (s *LogService) fixImportDateBug(ctx context.Context, l models.Log) error {
  440. // Find the log's posts.
  441. posts, err := s.ListPosts(ctx, &models.PostFilter{LogID: &l.ShortID})
  442. if err != nil {
  443. return err
  444. }
  445. if len(posts) < 8 {
  446. return nil
  447. }
  448. // Find first action post
  449. first := posts[0]
  450. fi := 0
  451. for first.Kind != "action" && first.Kind != "text" {
  452. fi++
  453. if fi >= len(posts) {
  454. return nil
  455. }
  456. first = posts[fi]
  457. }
  458. last := posts[len(posts)-1]
  459. if first == last {
  460. return nil
  461. }
  462. // Stop here if this log probably isn't affected
  463. if last.Time.Sub(first.Time) < time.Hour*72 {
  464. return nil
  465. }
  466. // Find the first post past midnight.
  467. midnight := first
  468. mi := fi
  469. for i, post := range posts[fi+1:] {
  470. if post.Time.Hour() < first.Time.Hour() {
  471. midnight = post
  472. mi = fi + 1 + i
  473. break
  474. }
  475. }
  476. if midnight == last {
  477. return nil
  478. }
  479. if len(posts[mi+1:]) == 1 {
  480. return nil
  481. }
  482. hits := 0
  483. prev := midnight
  484. for _, offender := range posts[mi+1:] {
  485. if offender.Time.Day() != prev.Time.Day() {
  486. hits += 1
  487. }
  488. prev = offender
  489. }
  490. if hits < ((len(posts[mi+1:]) * 3) / 4) {
  491. return nil
  492. }
  493. for _, offender := range posts[mi+1:] {
  494. ot := offender.Time.UTC()
  495. mt := midnight.Time.UTC()
  496. y, m, d := mt.Date()
  497. hr, mi, se, ns := ot.Hour(), ot.Minute(), ot.Second(), ot.Nanosecond()
  498. newTime := time.Date(y, m, d, hr, mi, se, ns, time.UTC)
  499. _, err := s.posts.Update(ctx, *offender, models.PostUpdate{Time: &newTime})
  500. if err != nil {
  501. return err
  502. }
  503. }
  504. log.Printf("Fixed import date bug in %d posts in log %s", len(posts[mi+1:]), l.ID)
  505. return nil
  506. }
  507. func (s *LogService) RefreshAllLogCharacters(ctx context.Context) error {
  508. start := time.Now()
  509. // Get all logs
  510. logs, err := s.logs.List(ctx, models.LogFilter{})
  511. if err != nil {
  512. return err
  513. }
  514. // Check all characters now instead of later.
  515. characters, err := s.characterService.List(ctx, models.CharacterFilter{})
  516. if err != nil {
  517. return err
  518. }
  519. characterMap := s.makeCharacterMap(characters)
  520. eg := errgroup.Group{}
  521. for i := range logs {
  522. l := logs[i]
  523. eg.Go(func() error {
  524. _, err := s.refreshLogCharacters(ctx, *l, characterMap)
  525. return err
  526. })
  527. }
  528. err = eg.Wait()
  529. if err != nil {
  530. return err
  531. }
  532. log.Printf("Full log character refresh complete; nicks: %d, logs: %d, duration: %s", len(characterMap), len(logs), time.Since(start))
  533. return nil
  534. }
  535. func (s *LogService) RefreshLogCharacters(ctx context.Context, log models.Log) (*models.Log, error) {
  536. return s.refreshLogCharacters(ctx, log, nil)
  537. }
  538. func (s *LogService) refreshLogCharacters(ctx context.Context, log models.Log, characterMap map[string]*models.Character) (*models.Log, error) {
  539. posts, err := s.ListPosts(ctx, &models.PostFilter{LogID: &log.ShortID})
  540. if err != nil {
  541. return &log, nil
  542. }
  543. counts := make(map[string]int)
  544. added := make(map[string]bool)
  545. removed := make(map[string]bool)
  546. for _, post := range posts {
  547. if post.Kind == "text" || post.Kind == "action" {
  548. if strings.HasPrefix(post.Text, "(") || strings.Contains(post.Nick, "(") || strings.Contains(post.Nick, "[E]") || strings.HasSuffix(post.Nick, "|") {
  549. continue
  550. }
  551. // Clean up the nick (remove possessive suffix, comma, formatting stuff)
  552. if strings.HasSuffix(post.Nick, "'s") || strings.HasSuffix(post.Nick, "`s") {
  553. post.Nick = post.Nick[:len(post.Nick)-2]
  554. } else if strings.HasSuffix(post.Nick, "'") || strings.HasSuffix(post.Nick, "`") || strings.HasSuffix(post.Nick, ",") || strings.HasSuffix(post.Nick, "\x0f") {
  555. post.Nick = post.Nick[:len(post.Nick)-1]
  556. }
  557. added[post.Nick] = true
  558. counts[post.Nick]++
  559. }
  560. if post.Kind == "chars" {
  561. tokens := strings.Fields(post.Text)
  562. for _, token := range tokens {
  563. if strings.HasPrefix(token, "-") {
  564. removed[token[1:]] = true
  565. } else {
  566. added[strings.Replace(token, "+", "", 1)] = true
  567. }
  568. }
  569. }
  570. }
  571. nicks := make([]string, 0, len(added))
  572. for nick := range added {
  573. if added[nick] && !removed[nick] {
  574. nicks = append(nicks, nick)
  575. }
  576. }
  577. if characterMap == nil {
  578. characters, err := s.characterService.List(ctx, models.CharacterFilter{Nicks: nicks})
  579. if err != nil {
  580. return nil, err
  581. }
  582. characterMap = s.makeCharacterMap(characters)
  583. }
  584. log.CharacterIDs = log.CharacterIDs[:0]
  585. for key := range added {
  586. delete(added, key)
  587. }
  588. for _, nick := range nicks {
  589. character := characterMap[nick]
  590. if character == nil || added[character.ID] {
  591. continue
  592. }
  593. added[character.ID] = true
  594. log.CharacterIDs = append(log.CharacterIDs, character.ID)
  595. }
  596. return s.logs.Update(ctx, log, models.LogUpdate{CharacterIDs: log.CharacterIDs})
  597. }
  598. func (s *LogService) makeCharacterMap(characters []*models.Character) map[string]*models.Character {
  599. characterMap := make(map[string]*models.Character, len(characters)*3)
  600. for _, character := range characters {
  601. for _, nick := range character.Nicks {
  602. characterMap[nick] = character
  603. }
  604. }
  605. return characterMap
  606. }