Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ plugins:
token: "" # kept empty; use BOT_PASTE_TOKEN instead
default_visibility: unlisted
max_input_length: 4096
hard_wrap: false
hard_wrap_width: 80
hard_wrap_urls: false
crypto: {enabled: true}
pkg: {enabled: true, timeout_seconds: 8, max_length: 300}
port: {enabled: true, data_file: "data/ports.txt"}
Expand Down
7 changes: 6 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ plugins:
token: "" # use BOT_PASTE_TOKEN; this field is intentionally ignored
default_visibility: unlisted
max_input_length: 4096
hard_wrap: false
hard_wrap_width: 80
hard_wrap_urls: false
crypto: {enabled: true}
pkg: {enabled: true, timeout_seconds: 8, max_length: 300}
port: {enabled: true, data_file: "data/ports.txt"}
Expand Down Expand Up @@ -177,7 +180,9 @@ IRC. Disable any of them with `enabled: false` if they are not wanted.
The paste plugin uses `BOT_PASTE_BASE_URL` and `BOT_PASTE_TOKEN`; the token is
not loaded from `config.yaml`. `default_visibility` accepts `public`,
`unlisted`, or `private`, and `max_input_length` bounds inline text and fetched
URL content. Package, audit, and Docker lookups use the configured
URL content. Set `hard_wrap: true` to insert real newlines into inline text;
`hard_wrap_width` controls the target line width, and `hard_wrap_urls` must be
enabled separately to transform fetched URL content. Package, audit, and Docker lookups use the configured
`timeout_seconds` and `max_length` values. The port plugin reads its local
catalog from `data/ports.txt` and has a fixed bounded response; it has no
`max_length` setting.
Expand Down
6 changes: 5 additions & 1 deletion docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ outbound request made from the bot host, follows HTTP redirects, and can reach
any address permitted by that host's network. Do not enable URL pasting for
untrusted users without restricting egress at the host or network firewall;
disable the plugin with `plugins.paste.enabled: false` if that boundary cannot
be enforced.
be enforced. To insert real line breaks into inline IRC text, enable
`hard_wrap` and choose a `hard_wrap_width` (80 by default). URL-fetched content
is left unchanged unless `hard_wrap_urls: true` is also configured; softwrap in
the Opengist editor remains a display preference and does not change file
content.

## Crypto and encoding

Expand Down
49 changes: 49 additions & 0 deletions plugins/paste.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ type Paste struct {
provider string
visibility string
maxInputLength int
hardWrap bool
hardWrapURLs bool
hardWrapWidth int
}

func (p *Paste) Name() string { return "paste" }
Expand All @@ -48,6 +51,12 @@ func (p *Paste) Init(c bot.PluginConfig, _ *storage.DB) error {
if p.maxInputLength < 1 || p.maxInputLength > 64*1024 {
p.maxInputLength = pasteDefaultMaxInput
}
p.hardWrap = c.Bool("hard_wrap", false)
p.hardWrapURLs = c.Bool("hard_wrap_urls", false)
p.hardWrapWidth = c.Int("hard_wrap_width", 80)
if p.hardWrapWidth < 20 || p.hardWrapWidth > 500 {
p.hardWrapWidth = 80
}
return nil
}

Expand All @@ -71,15 +80,20 @@ func (p *Paste) Handle(b *bot.Bot, m bot.Message) bool {
}

input := arg
isURL := false
truncated := false
if parsed, err := url.ParseRequestURI(arg); err == nil && (parsed.Scheme == "http" || parsed.Scheme == "https") && parsed.Host != "" {
isURL = true
fetched, wasTruncated, err := fetchPasteURL(context.Background(), arg, p.maxInputLength)
if err != nil {
b.Send(m.ReplyTarget(), "could not fetch URL content for paste")
return true
}
input, truncated = fetched, wasTruncated
}
if p.hardWrap && (!isURL || p.hardWrapURLs) {
input = hardWrapPasteText(input, p.hardWrapWidth)
}
if len([]rune(input)) > p.maxInputLength {
input = truncateRunes(input, p.maxInputLength)
truncated = true
Expand All @@ -106,6 +120,41 @@ type pasteHTTPError struct{ status int }

func (e pasteHTTPError) Error() string { return fmt.Sprintf("Opengist returned HTTP %d", e.status) }

func hardWrapPasteText(text string, width int) string {
if width <= 0 {
return text
}
paragraphs := strings.Split(text, "\n")
for i, paragraph := range paragraphs {
words := strings.Fields(paragraph)
if len(words) == 0 {
paragraphs[i] = ""
continue
}
var wrapped strings.Builder
lineLength := 0
for _, word := range words {
wordLength := len([]rune(word))
if lineLength == 0 {
wrapped.WriteString(word)
lineLength = wordLength
continue
}
if lineLength+1+wordLength <= width {
wrapped.WriteByte(' ')
wrapped.WriteString(word)
lineLength += 1 + wordLength
continue
}
wrapped.WriteByte('\n')
wrapped.WriteString(word)
lineLength = wordLength
}
paragraphs[i] = wrapped.String()
}
return strings.Join(paragraphs, "\n")
}

func fetchPasteURL(parent context.Context, rawURL string, maxLength int) (string, bool, error) {
ctx, cancel := context.WithTimeout(parent, 8*time.Second)
defer cancel()
Expand Down
8 changes: 8 additions & 0 deletions plugins/paste_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,11 @@ func TestPasteOutputIsOneSanitizedLine(t *testing.T) {
t.Fatalf("paste output contains line breaks: %q", got)
}
}

func TestHardWrapPasteTextWrapsWordsAndPreservesParagraphs(t *testing.T) {
got := hardWrapPasteText("one two three four\nnext paragraph", 10)
want := "one two\nthree four\nnext\nparagraph"
if got != want {
t.Fatalf("hardWrapPasteText() = %q, want %q", got, want)
}
}