Files
ARTEX/db/chat_mentions.go
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00

114 lines
4.4 KiB
Go

package db
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
)
// ChatMention is a lightweight search result. Details are read again on send.
type ChatMention struct {
Kind string `json:"kind"`
ID int64 `json:"id"`
Label string `json:"label"`
Description string `json:"description"`
}
type ChatMentionPage struct {
Items []ChatMention `json:"items"`
NextCursor string `json:"next_cursor,omitempty"`
}
var ErrInvalidChatMentionCursor = errors.New("invalid pagination cursor, please search again")
type chatMentionCursor struct {
ID int64 `json:"id"`
Kind string `json:"kind"`
Exact bool `json:"exact"`
Scope string `json:"scope"`
Query string `json:"query"`
}
func ValidChatMentionKind(kind string) bool {
switch kind {
case "finding", "company", "asset", "endpoint", "ip", "app", "root_domain", "subdomain", "service":
return true
}
return false
}
// SearchChatMentions searches the shared catalog, just like the asset/finding
// pages. Values remain SQL parameters; %, _ and backslash are literal search text.
func (d *DB) SearchChatMentions(ctx context.Context, kind, query string) ([]ChatMention, error) {
page, err := d.SearchChatMentionsPage(ctx, kind, query, "")
return page.Items, err
}
// SearchChatMentionsPage uses the last result's stable sort key rather than an
// offset, so loading later pages does not repeatedly skip all earlier results.
func (d *DB) SearchChatMentionsPage(ctx context.Context, kind, query, cursor string) (ChatMentionPage, error) {
page := ChatMentionPage{Items: make([]ChatMention, 0)}
if kind != "" && !ValidChatMentionKind(kind) {
return page, fmt.Errorf("unsupported mention kind")
}
var after chatMentionCursor
if cursor != "" {
if len(cursor) > 2048 {
return page, ErrInvalidChatMentionCursor
}
blob, err := base64.RawURLEncoding.DecodeString(cursor)
if err != nil || json.Unmarshal(blob, &after) != nil || after.ID <= 0 ||
!ValidChatMentionKind(after.Kind) || after.Scope != kind || after.Query != query {
return page, ErrInvalidChatMentionCursor
}
}
pattern := "%" + strings.NewReplacer(`\`, `\\`, "%", `\%`, "_", `\_`).Replace(query) + "%"
rows, err := d.QueryContext(ctx, `
SELECT kind, id, left(label, 160), left(description, 240) FROM (
(SELECT 'finding' AS kind, id, COALESCE(NULLIF(name,''), vulnclass) AS label,
concat_ws(' · ', severity, status, left(summary, 160)) AS description
FROM findings WHERE ($1='' OR $1='finding') AND
($2='' OR id::text=$2 OR concat_ws(' ',name,vulnclass,summary) ILIKE $3)
AND ($4::bigint=0 OR (id::text=$2)<$6 OR ((id::text=$2)=$6 AND (id<$4 OR (id=$4 AND 'finding'>$5))))
ORDER BY (id::text=$2) DESC, id DESC LIMIT 21)
UNION ALL
(SELECT 'company', id, name, nkey FROM companies
WHERE ($1='' OR $1='company') AND ($2='' OR id::text=$2 OR name ILIKE $3 OR nkey ILIKE $3)
AND ($4::bigint=0 OR (id::text=$2)<$6 OR ((id::text=$2)=$6 AND (id<$4 OR (id=$4 AND 'company'>$5))))
ORDER BY (id::text=$2) DESC, id DESC LIMIT 21)
UNION ALL
(SELECT type, id,
CASE WHEN type='endpoint' THEN concat_ws(' ',NULLIF(method,''),url)
ELSE COALESCE(NULLIF(app_name,''),NULLIF(url,''),NULLIF(domain,''),NULLIF(ip,''),NULLIF(bundle_id,''),'Asset #'||id::text) END,
concat_ws(' · ',type,NULLIF(page_title,''),NULLIF(service_name,''),NULLIF(bundle_id,''),NULLIF(ip,''),port::text)
FROM assets WHERE ($1='' OR $1='asset' OR type=$1) AND
($2='' OR id::text=$2 OR concat_ws(' ',domain,root_domain,ip,url,app_name,bundle_id,page_title,service_name,method) ILIKE $3)
AND ($4::bigint=0 OR (id::text=$2)<$6 OR ((id::text=$2)=$6 AND (id<$4 OR (id=$4 AND type>$5))))
ORDER BY (id::text=$2) DESC, id DESC LIMIT 21)
) matches ORDER BY (id::text=$2) DESC, id DESC, kind LIMIT 21`, kind, query, pattern, after.ID, after.Kind, after.Exact)
if err != nil {
return page, err
}
defer rows.Close()
for rows.Next() {
var item ChatMention
if err := rows.Scan(&item.Kind, &item.ID, &item.Label, &item.Description); err != nil {
return page, err
}
page.Items = append(page.Items, item)
}
if err := rows.Err(); err != nil {
return page, err
}
if len(page.Items) > 20 {
page.Items = page.Items[:20]
last := page.Items[19]
blob, _ := json.Marshal(chatMentionCursor{last.ID, last.Kind, fmt.Sprint(last.ID) == query, kind, query})
page.NextCursor = base64.RawURLEncoding.EncodeToString(blob)
}
return page, nil
}