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.

text.go 4.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. // Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
  2. // released under the MIT license
  3. package utils
  4. import (
  5. "bytes"
  6. "strings"
  7. "time"
  8. )
  9. func IsRestrictedCTCPMessage(message string) bool {
  10. // block all CTCP privmsgs to Tor clients except for ACTION
  11. // DCC can potentially be used for deanonymization, the others for fingerprinting
  12. return strings.HasPrefix(message, "\x01") && !strings.HasPrefix(message, "\x01ACTION")
  13. }
  14. // WordWrap wraps the given text into a series of lines that don't exceed lineWidth characters.
  15. func WordWrap(text string, lineWidth int) []string {
  16. var lines []string
  17. var cacheLine, cacheWord bytes.Buffer
  18. for _, char := range text {
  19. if char == '\r' {
  20. continue
  21. } else if char == '\n' {
  22. cacheLine.Write(cacheWord.Bytes())
  23. lines = append(lines, cacheLine.String())
  24. cacheWord.Reset()
  25. cacheLine.Reset()
  26. } else if (char == ' ' || char == '-') && cacheLine.Len()+cacheWord.Len()+1 < lineWidth {
  27. // natural word boundary
  28. cacheLine.Write(cacheWord.Bytes())
  29. cacheLine.WriteRune(char)
  30. cacheWord.Reset()
  31. } else if lineWidth <= cacheLine.Len()+cacheWord.Len()+1 {
  32. // time to wrap to next line
  33. if cacheLine.Len() < (lineWidth / 2) {
  34. // this word takes up more than half a line... just split in the middle of the word
  35. cacheLine.Write(cacheWord.Bytes())
  36. cacheLine.WriteRune(char)
  37. cacheWord.Reset()
  38. } else {
  39. cacheWord.WriteRune(char)
  40. }
  41. lines = append(lines, cacheLine.String())
  42. cacheLine.Reset()
  43. } else {
  44. // normal character
  45. cacheWord.WriteRune(char)
  46. }
  47. }
  48. if 0 < cacheWord.Len() {
  49. cacheLine.Write(cacheWord.Bytes())
  50. }
  51. if 0 < cacheLine.Len() {
  52. lines = append(lines, cacheLine.String())
  53. }
  54. return lines
  55. }
  56. type MessagePair struct {
  57. Message string
  58. Msgid string
  59. Concat bool // should be relayed with the multiline-concat tag
  60. }
  61. // SplitMessage represents a message that's been split for sending.
  62. // Three possibilities:
  63. // (a) Standard message that can be relayed on a single 512-byte line
  64. // (MessagePair contains the message, Wrapped == nil)
  65. // (b) oragono.io/maxline-2 message that was split on the server side
  66. // (MessagePair contains the unsplit message, Wrapped contains the split lines)
  67. // (c) multiline message that was split on the client side
  68. // (MessagePair is zero, Wrapped contains the split lines)
  69. type SplitMessage struct {
  70. MessagePair
  71. Wrapped []MessagePair // if this is nil, `Message` didn't need wrapping and can be sent to anyone
  72. Time time.Time
  73. }
  74. const defaultLineWidth = 400
  75. func MakeSplitMessage(original string, origIs512 bool) (result SplitMessage) {
  76. result.Message = original
  77. result.Msgid = GenerateSecretToken()
  78. result.Time = time.Now().UTC()
  79. if !origIs512 && defaultLineWidth < len(original) {
  80. wrapped := WordWrap(original, defaultLineWidth)
  81. result.Wrapped = make([]MessagePair, len(wrapped))
  82. for i, wrappedMessage := range wrapped {
  83. result.Wrapped[i] = MessagePair{
  84. Message: wrappedMessage,
  85. Msgid: GenerateSecretToken(),
  86. }
  87. }
  88. }
  89. return
  90. }
  91. func (sm *SplitMessage) Append(message string, concat bool) {
  92. if sm.Msgid == "" {
  93. sm.Msgid = GenerateSecretToken()
  94. }
  95. sm.Wrapped = append(sm.Wrapped, MessagePair{
  96. Message: message,
  97. Msgid: GenerateSecretToken(),
  98. Concat: concat,
  99. })
  100. }
  101. func (sm *SplitMessage) LenLines() int {
  102. if sm.Wrapped == nil {
  103. if (sm.MessagePair == MessagePair{}) {
  104. return 0
  105. } else {
  106. return 1
  107. }
  108. }
  109. return len(sm.Wrapped)
  110. }
  111. func (sm *SplitMessage) LenBytes() (result int) {
  112. if sm.Wrapped == nil {
  113. return len(sm.Message)
  114. }
  115. for i := 0; i < len(sm.Wrapped); i++ {
  116. result += len(sm.Wrapped[i].Message)
  117. }
  118. return
  119. }
  120. func (sm *SplitMessage) IsRestrictedCTCPMessage() bool {
  121. if IsRestrictedCTCPMessage(sm.Message) {
  122. return true
  123. }
  124. for i := 0; i < len(sm.Wrapped); i++ {
  125. if IsRestrictedCTCPMessage(sm.Wrapped[i].Message) {
  126. return true
  127. }
  128. }
  129. return false
  130. }
  131. func (sm *SplitMessage) IsMultiline() bool {
  132. return sm.Message == "" && len(sm.Wrapped) != 0
  133. }
  134. func (sm *SplitMessage) Is512() bool {
  135. return sm.Message != "" && sm.Wrapped == nil
  136. }
  137. // TokenLineBuilder is a helper for building IRC lines composed of delimited tokens,
  138. // with a maximum line length.
  139. type TokenLineBuilder struct {
  140. lineLen int
  141. delim string
  142. buf bytes.Buffer
  143. result []string
  144. }
  145. func (t *TokenLineBuilder) Initialize(lineLen int, delim string) {
  146. t.lineLen = lineLen
  147. t.delim = delim
  148. }
  149. // Add adds a token to the line, creating a new line if necessary.
  150. func (t *TokenLineBuilder) Add(token string) {
  151. tokenLen := len(token)
  152. if t.buf.Len() != 0 {
  153. tokenLen += len(t.delim)
  154. }
  155. if t.lineLen < t.buf.Len()+tokenLen {
  156. t.result = append(t.result, t.buf.String())
  157. t.buf.Reset()
  158. }
  159. if t.buf.Len() != 0 {
  160. t.buf.WriteString(t.delim)
  161. }
  162. t.buf.WriteString(token)
  163. }
  164. // Lines terminates the line-building and returns all the lines.
  165. func (t *TokenLineBuilder) Lines() (result []string) {
  166. result = t.result
  167. t.result = nil
  168. if t.buf.Len() != 0 {
  169. result = append(result, t.buf.String())
  170. t.buf.Reset()
  171. }
  172. return
  173. }