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.

161 lines
3.4 KiB

  1. package parsers
  2. import (
  3. "errors"
  4. "fmt"
  5. "git.aiterp.net/rpdata/api/models"
  6. "strings"
  7. "time"
  8. )
  9. var ErrSkip = errors.New("parsers: skip this post")
  10. func IRCCloudLogs(data string, location *time.Location, threshold time.Duration) ([]ParsedLog, error) {
  11. lines := strings.Split(data, "\n")
  12. pos := 0
  13. results := make([]ParsedLog, 0, 8)
  14. for pos < len(lines) {
  15. log, n, err := IRCCloudLog(lines[pos:], location, threshold)
  16. if err != nil {
  17. if err == ErrEmptyLog {
  18. pos += n
  19. continue
  20. }
  21. return nil, err
  22. }
  23. pos += n
  24. results = append(results, *log)
  25. }
  26. return results, nil
  27. }
  28. // IRCCloudLog parses the log and returns the things that can be gleamed from them.
  29. func IRCCloudLog(lines []string, location *time.Location, threshold time.Duration) (*ParsedLog, int, error) {
  30. posts := make([]*models.Post, 0, len(lines))
  31. prev := (*models.Post)(nil)
  32. amount := 0
  33. for _, line := range lines {
  34. line = strings.Trim(line, "\r\t  ")
  35. if len(line) < 1 {
  36. amount += 1
  37. continue
  38. }
  39. post, err := IRCCloudPost(line, location)
  40. if err == ErrSkip {
  41. amount += 1
  42. continue
  43. } else if err != nil {
  44. return nil, -1, err
  45. }
  46. if prev != nil {
  47. if post.Time.Sub(prev.Time) >= threshold {
  48. break
  49. }
  50. post.Position = prev.Position + 1
  51. } else {
  52. post.Position = 1
  53. }
  54. posts = append(posts, &post)
  55. prev = &post
  56. amount += 1
  57. }
  58. if len(posts) == 0 {
  59. return nil, amount, ErrEmptyLog
  60. }
  61. log := models.Log{
  62. Date: posts[0].Time,
  63. }
  64. return &ParsedLog{log, posts}, amount, nil
  65. }
  66. // IRCCloudPost parses a post from a mirc-like line. If the previous post is included (it can be empty), it will be used
  67. // to determine whether midnight has passed.
  68. func IRCCloudPost(line string, tz *time.Location) (models.Post, error) {
  69. // Do basic validation
  70. line = strings.Trim(line, "  \t\n\r")
  71. if len(line) == 0 || !strings.HasPrefix(line, "[") {
  72. return models.Post{}, &ParseError{
  73. Line: line,
  74. Problem: "no timestamp",
  75. }
  76. }
  77. // Parse timestamp
  78. tsEndIndex := strings.IndexByte(line, ']')
  79. if tsEndIndex == -1 || len(line) < tsEndIndex+5 {
  80. return models.Post{}, &ParseError{
  81. Line: line,
  82. Problem: "incomplete timestamp",
  83. }
  84. }
  85. tsStr := line[1:tsEndIndex]
  86. ts, err := time.ParseInLocation("2006-01-02 15:04:05", tsStr, tz)
  87. if err != nil {
  88. return models.Post{}, &ParseError{
  89. Line: line,
  90. Problem: fmt.Sprintf("Could not parse date: %s", err.Error()),
  91. }
  92. }
  93. if strings.HasPrefix(line[tsEndIndex+2:], "—") {
  94. split := strings.SplitN(line[tsEndIndex+6:], " ", 2)
  95. if len(split) == 1 {
  96. return models.Post{}, &ParseError{
  97. Line: line,
  98. Problem: "post is empty",
  99. }
  100. }
  101. post := models.Post{
  102. ID: "UNASSIGNED",
  103. LogID: "UNASSIGNED",
  104. Time: ts,
  105. Kind: "action",
  106. Nick: strings.Trim(strings.TrimLeft(split[0], "+@!~"), "\u001F"),
  107. Text: split[1],
  108. }
  109. if strings.HasPrefix(post.Nick, "=") {
  110. post.Kind = "scene"
  111. }
  112. return post, nil
  113. } else if line[tsEndIndex+2] == '<' {
  114. split := strings.SplitN(line[tsEndIndex+2:], " ", 2)
  115. if len(split) == 1 {
  116. return models.Post{}, &ParseError{
  117. Line: line,
  118. Problem: "post is empty",
  119. }
  120. }
  121. post := models.Post{
  122. ID: "UNASSIGNED",
  123. LogID: "UNASSIGNED",
  124. Time: ts,
  125. Kind: "text",
  126. Nick: strings.Trim(strings.TrimLeft(split[0][1:len(split[0])-1], "+@!~"), "\u001F"),
  127. Text: split[1],
  128. }
  129. if strings.HasPrefix(post.Nick, "=") {
  130. post.Kind = "scene"
  131. }
  132. return post, nil
  133. } else {
  134. return models.Post{}, ErrSkip
  135. }
  136. }