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.

172 lines
3.8 KiB

  1. package parsers
  2. import (
  3. "errors"
  4. "fmt"
  5. "git.aiterp.net/rpdata/api/models"
  6. "strconv"
  7. "strings"
  8. "time"
  9. )
  10. type ParseError struct {
  11. Line string
  12. Problem string
  13. }
  14. func (e *ParseError) Error() string {
  15. return fmt.Sprintf("Unrecognized post: %s (error: %s)", e.Line, e.Problem)
  16. }
  17. func IsParseError(err error) bool {
  18. _, ok := err.(*ParseError)
  19. return ok
  20. }
  21. // ErrEmptyLog is returned by ParseLog if there are no (valid) posts in the log.
  22. var ErrEmptyLog = errors.New("no valid posts found in log")
  23. // MircLog parses the log and returns the things that can be gleamed from them.
  24. func MircLog(data string, date time.Time, strict bool) (*ParsedLog, error) {
  25. lines := strings.Split(data, "\n")
  26. posts := make([]*models.Post, 0, len(lines))
  27. prev := models.Post{}
  28. for _, line := range lines {
  29. line = strings.Trim(line, "\r\t  ")
  30. if len(line) < 1 {
  31. continue
  32. }
  33. post, err := MircPost(line, date, prev)
  34. if err != nil {
  35. if strict {
  36. return nil, err
  37. }
  38. continue
  39. }
  40. posts = append(posts, &post)
  41. prev = post
  42. }
  43. if len(posts) == 0 {
  44. return nil, ErrEmptyLog
  45. }
  46. log := models.Log{
  47. Date: posts[0].Time,
  48. }
  49. return &ParsedLog{log, posts}, nil
  50. }
  51. // MircPost parses a post from a mirc-like line. If the previous post is included (it can be empty), it will be used
  52. // to determine whether midnight has passed.
  53. func MircPost(line string, date time.Time, prev models.Post) (models.Post, error) {
  54. // Do basic validation
  55. line = strings.Trim(line, "  \t\n\r")
  56. if len(line) == 0 || !strings.HasPrefix(line, "[") {
  57. return models.Post{}, &ParseError{
  58. Line: line,
  59. Problem: "no timestamp",
  60. }
  61. }
  62. // Parse timestamp
  63. tsEndIndex := strings.IndexByte(line, ']')
  64. if tsEndIndex == -1 || len(line) < tsEndIndex+5 {
  65. return models.Post{}, &ParseError{
  66. Line: line,
  67. Problem: "incomplete timestamp",
  68. }
  69. }
  70. tsStr := line[1:tsEndIndex]
  71. tsSplit := strings.Split(tsStr, ":")
  72. tsUnits := make([]int, len(tsSplit))
  73. if len(tsSplit) < 2 {
  74. return models.Post{}, &ParseError{
  75. Line: line,
  76. Problem: "invalid timestamp",
  77. }
  78. }
  79. for i := range tsSplit {
  80. n, err := strconv.Atoi(tsSplit[i])
  81. if err != nil {
  82. return models.Post{}, &ParseError{
  83. Line: line,
  84. Problem: "invalid number in timestamp",
  85. }
  86. }
  87. tsUnits[i] = n
  88. }
  89. if len(tsUnits) == 2 {
  90. tsUnits = append(tsUnits, 0)
  91. }
  92. // Determine timestamp from parsed data and previous post.
  93. if !prev.Time.IsZero() {
  94. date = prev.Time
  95. }
  96. ts := time.Date(date.Year(), date.Month(), date.Day(), tsUnits[0], tsUnits[1], tsUnits[2], 0, date.Location())
  97. if ts.Before(prev.Time) {
  98. ts = ts.Add(time.Hour * 24)
  99. }
  100. if line[tsEndIndex+2] == '*' {
  101. split := strings.SplitN(line[tsEndIndex+4:], " ", 2)
  102. if len(split) == 1 {
  103. return models.Post{}, &ParseError{
  104. Line: line,
  105. Problem: "post is empty",
  106. }
  107. }
  108. post := models.Post{
  109. ID: "UNASSIGNED",
  110. LogID: "UNASSIGNED",
  111. Time: ts,
  112. Kind: "action",
  113. Nick: strings.Trim(strings.TrimLeft(split[0], "+@!~\u001F"), "\u001F"),
  114. Text: split[1],
  115. Position: prev.Position + 1,
  116. }
  117. if post.Nick[0] == '=' {
  118. post.Kind = "scene"
  119. }
  120. return post, nil
  121. } else if line[tsEndIndex+2] == '<' {
  122. split := strings.SplitN(line[tsEndIndex+2:], " ", 2)
  123. if len(split) == 1 {
  124. return models.Post{}, &ParseError{
  125. Line: line,
  126. Problem: "post is empty",
  127. }
  128. }
  129. post := models.Post{
  130. ID: "UNASSIGNED",
  131. LogID: "UNASSIGNED",
  132. Time: ts,
  133. Kind: "text",
  134. Nick: strings.Trim(strings.TrimLeft(split[0][1:len(split[0])-1], "+@!~"), "\u001F"),
  135. Text: split[1],
  136. Position: prev.Position + 1,
  137. }
  138. if post.Nick[0] == '=' {
  139. post.Kind = "scene"
  140. }
  141. return post, nil
  142. } else {
  143. return models.Post{}, &ParseError{
  144. Line: line,
  145. Problem: "line is neither action nor text post",
  146. }
  147. }
  148. }