From f582d6b8afb009f7b9452d540731b860cd6c3028 Mon Sep 17 00:00:00 2001 From: AK Date: Sat, 8 Aug 2026 05:53:26 -0700 Subject: [PATCH] feat: optionally hard-wrap inline pastes --- config.yaml | 3 +++ docs/configuration.md | 7 ++++++- docs/plugins.md | 6 +++++- plugins/paste.go | 49 +++++++++++++++++++++++++++++++++++++++++++ plugins/paste_test.go | 8 +++++++ 5 files changed, 71 insertions(+), 2 deletions(-) diff --git a/config.yaml b/config.yaml index ade52e7..31c1b1d 100644 --- a/config.yaml +++ b/config.yaml @@ -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"} diff --git a/docs/configuration.md b/docs/configuration.md index a814c4e..8187c25 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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"} @@ -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. diff --git a/docs/plugins.md b/docs/plugins.md index 3bde1a6..2ba1c10 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -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 diff --git a/plugins/paste.go b/plugins/paste.go index 3990a9a..d077d3c 100644 --- a/plugins/paste.go +++ b/plugins/paste.go @@ -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" } @@ -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 } @@ -71,8 +80,10 @@ 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") @@ -80,6 +91,9 @@ func (p *Paste) Handle(b *bot.Bot, m bot.Message) bool { } 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 @@ -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() diff --git a/plugins/paste_test.go b/plugins/paste_test.go index f54648e..36f9c28 100644 --- a/plugins/paste_test.go +++ b/plugins/paste_test.go @@ -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) + } +}