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.

136 lines
3.2 KiB

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