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.

126 lines
2.6 KiB

  1. package main
  2. import (
  3. "bufio"
  4. "errors"
  5. "io"
  6. "strconv"
  7. "strings"
  8. "time"
  9. )
  10. // ErrNotPost is returned by parsePost if the line is empty or not a post.
  11. var ErrNotPost = errors.New("not a post")
  12. // ErrTooShort is returned by parsePost if the line too short to be a post.
  13. var ErrTooShort = errors.New("post too short")
  14. // ErrInvalidTimestamp is returned by parsePost if the line is empty or not a post.
  15. var ErrInvalidTimestamp = errors.New("invalid timestamp")
  16. // A Post is a part of a log.
  17. type Post struct {
  18. Time time.Time
  19. Kind string
  20. Nick string
  21. Text string
  22. }
  23. func parsePosts(reader io.Reader, date time.Time) ([]Post, error) {
  24. prev := Post{}
  25. posts := make([]Post, 0, 8)
  26. bufReader := bufio.NewReader(reader)
  27. for {
  28. line, err := bufReader.ReadString('\n')
  29. if err != nil && err != io.EOF {
  30. return nil, err
  31. }
  32. if len(line) > 8 {
  33. post, err := parsePost(strings.Trim(line, "  \n\r"), date, prev)
  34. if err != nil {
  35. return nil, err
  36. }
  37. posts = append(posts, post)
  38. prev = post
  39. }
  40. if err == io.EOF {
  41. break
  42. }
  43. }
  44. return posts, nil
  45. }
  46. func parsePost(line string, date time.Time, prev Post) (Post, error) {
  47. // Do basic validation
  48. line = strings.Trim(line, "  \t\n\r")
  49. if len(line) == 0 || !strings.HasPrefix(line, "[") {
  50. return Post{}, ErrNotPost
  51. }
  52. // Parse timestamp
  53. tsEndIndex := strings.IndexByte(line, ']')
  54. if tsEndIndex == -1 || len(line) < tsEndIndex+5 {
  55. return Post{}, ErrNotPost
  56. }
  57. tsStr := line[1:tsEndIndex]
  58. tsSplit := strings.Split(tsStr, ":")
  59. tsUnits := make([]int, len(tsSplit))
  60. if len(tsSplit) < 2 {
  61. return Post{}, ErrNotPost
  62. }
  63. for i := range tsSplit {
  64. n, err := strconv.Atoi(tsSplit[i])
  65. if err != nil {
  66. return Post{}, ErrNotPost
  67. }
  68. tsUnits[i] = n
  69. }
  70. if len(tsUnits) == 2 {
  71. tsUnits = append(tsUnits, 0)
  72. }
  73. ts := time.Date(date.Year(), date.Month(), date.Day(), tsUnits[0], tsUnits[1], tsUnits[2], 0, date.Location())
  74. if !prev.Time.IsZero() && prev.Time.After(ts) {
  75. ts = ts.AddDate(0, 0, 1)
  76. }
  77. if line[tsEndIndex+2] == '*' {
  78. split := strings.SplitN(line[tsEndIndex+4:], " ", 2)
  79. post := Post{
  80. Time: ts,
  81. Kind: "action",
  82. Nick: strings.TrimLeft(split[0], "+@!~"),
  83. Text: split[1],
  84. }
  85. if post.Nick[0] == '=' {
  86. post.Kind = "scene"
  87. }
  88. return post, nil
  89. } else if line[tsEndIndex+2] == '<' {
  90. split := strings.SplitN(line[tsEndIndex+2:], " ", 2)
  91. post := Post{
  92. Time: ts,
  93. Kind: "text",
  94. Nick: strings.TrimLeft(split[0][1:len(split[0])-1], "+@!~"),
  95. Text: split[1],
  96. }
  97. if post.Nick[0] == '=' {
  98. post.Kind = "scene"
  99. }
  100. return post, nil
  101. } else {
  102. return Post{}, ErrNotPost
  103. }
  104. }