Ctrl K
ComunidadGuías…

internal/app/bots/service_bot_entities.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package bots

import (
	"sort"
	"strings"
	"unicode"
	"unicode/utf8"

	"telesrv/internal/domain"
)

type serviceBotEntitySpan struct {
	start int
	end   int
}

func serviceBotReplyEntities(text string, explicit []domain.MessageEntity) []domain.MessageEntity {
	if text == "" && len(explicit) == 0 {
		return nil
	}
	out := append([]domain.MessageEntity(nil), explicit...)
	occupied := make([]serviceBotEntitySpan, 0, len(out)+8)
	for _, entity := range out {
		if entity.Length <= 0 {
			continue
		}
		occupied = append(occupied, serviceBotEntitySpan{start: entity.Offset, end: entity.Offset + entity.Length})
	}
	appendEntity := func(entity domain.MessageEntity) {
		if entity.Length <= 0 || len(out) >= domain.MaxMessageEntityCount {
			return
		}
		span := serviceBotEntitySpan{start: entity.Offset, end: entity.Offset + entity.Length}
		if serviceBotSpanOverlaps(span, occupied) {
			return
		}
		out = append(out, entity)
		occupied = append(occupied, span)
	}
	for _, span := range serviceBotURLByteSpans(text) {
		offset, length := utf16Range(text, span.start, span.end)
		appendEntity(domain.MessageEntity{Type: domain.MessageEntityURL, Offset: offset, Length: length})
	}
	for _, span := range serviceBotCommandByteSpans(text) {
		offset, length := utf16Range(text, span.start, span.end)
		appendEntity(domain.MessageEntity{Type: domain.MessageEntityBotCommand, Offset: offset, Length: length})
	}
	sort.SliceStable(out, func(i, j int) bool {
		if out[i].Offset != out[j].Offset {
			return out[i].Offset < out[j].Offset
		}
		return out[i].Length < out[j].Length
	})
	return out
}

func serviceBotURLByteSpans(text string) []serviceBotEntitySpan {
	var spans []serviceBotEntitySpan
	for i := 0; i < len(text); {
		if !strings.HasPrefix(text[i:], "https://") && !strings.HasPrefix(text[i:], "http://") {
			_, size := utf8.DecodeRuneInString(text[i:])
			i += size
			continue
		}
		start := i
		i += len("http://")
		if strings.HasPrefix(text[start:], "https://") {
			i = start + len("https://")
		}
		for i < len(text) {
			r, size := utf8.DecodeRuneInString(text[i:])
			if unicode.IsSpace(r) || r == '<' || r == '>' {
				break
			}
			i += size
		}
		end := trimServiceBotURLTrailingPunctuation(text, start, i)
		if end > start {
			spans = append(spans, serviceBotEntitySpan{start: start, end: end})
		}
		if i == start {
			i++
		}
	}
	return spans
}

func trimServiceBotURLTrailingPunctuation(text string, start, end int) int {
	for end > start {
		r, size := utf8.DecodeLastRuneInString(text[start:end])
		if !strings.ContainsRune(".,;:!?)]}", r) {
			break
		}
		end -= size
	}
	return end
}

func serviceBotCommandByteSpans(text string) []serviceBotEntitySpan {
	var spans []serviceBotEntitySpan
	for i := 0; i < len(text); {
		r, size := utf8.DecodeRuneInString(text[i:])
		if r != '/' || !serviceBotCommandStart(text, i) {
			i += size
			continue
		}
		start := i
		i += size
		commandStartEnd := i
		for i < len(text) {
			r, size = utf8.DecodeRuneInString(text[i:])
			if !serviceBotCommandChar(r) {
				break
			}
			i += size
		}
		if i == commandStartEnd {
			continue
		}
		if i < len(text) {
			r, size = utf8.DecodeRuneInString(text[i:])
			if r == '@' {
				mentionEnd := i + size
				for mentionEnd < len(text) {
					r, size = utf8.DecodeRuneInString(text[mentionEnd:])
					if !serviceBotCommandChar(r) {
						break
					}
					mentionEnd += size
				}
				if mentionEnd > i+size {
					i = mentionEnd
				}
			}
		}
		spans = append(spans, serviceBotEntitySpan{start: start, end: i})
	}
	return spans
}

func serviceBotCommandStart(text string, byteIndex int) bool {
	if byteIndex == 0 {
		return true
	}
	prev, _ := utf8.DecodeLastRuneInString(text[:byteIndex])
	if prev == ':' || prev == '/' || prev == '@' {
		return false
	}
	return !serviceBotCommandChar(prev)
}

func serviceBotCommandChar(r rune) bool {
	return r == '_' || ('0' <= r && r <= '9') || ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z')
}

func utf16Range(text string, startByte, endByte int) (int, int) {
	offset := 0
	for _, r := range text[:startByte] {
		offset += utf16RuneLen(r)
	}
	length := 0
	for _, r := range text[startByte:endByte] {
		length += utf16RuneLen(r)
	}
	return offset, length
}

func utf16RuneLen(r rune) int {
	if r > 0xFFFF {
		return 2
	}
	return 1
}

func serviceBotSpanOverlaps(span serviceBotEntitySpan, occupied []serviceBotEntitySpan) bool {
	for _, other := range occupied {
		if span.start < other.end && other.start < span.end {
			return true
		}
	}
	return false
}