feat: extract files from message logs
This commit is contained in:
142
internal/isphere/files.go
Normal file
142
internal/isphere/files.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package isphere
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
FileID string
|
||||
MessageID string
|
||||
ConversationID string
|
||||
FileName string
|
||||
SizeBytes *int64
|
||||
MimeType string
|
||||
CreatedAt string
|
||||
DownloadRef string
|
||||
SavedPath string
|
||||
SHA256 string
|
||||
Source string
|
||||
RawRef string
|
||||
}
|
||||
|
||||
type ListFilesQuery struct {
|
||||
ConversationID string
|
||||
MessageID string
|
||||
FileID string
|
||||
NameContains string
|
||||
Limit int
|
||||
}
|
||||
|
||||
type ListFilesResult struct {
|
||||
Files []File
|
||||
}
|
||||
|
||||
var fileNamePattern = regexp.MustCompile(`(?i)[^\s<>:"|?*]+\.(?:docx?|xlsx?|pptx?|pdf|zip|rar|png|jpe?g|txt|csv)\b`)
|
||||
|
||||
func ListFilesFromMessages(messages []Message, query ListFilesQuery) ListFilesResult {
|
||||
files := make([]File, 0)
|
||||
for _, message := range messages {
|
||||
if !messageLooksFileBacked(message) {
|
||||
continue
|
||||
}
|
||||
fileName := extractMessageFileName(message.Text)
|
||||
if fileName == "" {
|
||||
continue
|
||||
}
|
||||
file := File{
|
||||
FileID: fileID(message.ID, fileName),
|
||||
MessageID: message.ID,
|
||||
ConversationID: message.ConversationID,
|
||||
FileName: fileName,
|
||||
MimeType: mimeTypeForFileName(fileName),
|
||||
CreatedAt: message.Timestamp,
|
||||
Source: "local_readonly",
|
||||
RawRef: "packetlog_file_transfer",
|
||||
}
|
||||
if fileMatchesQuery(file, query) {
|
||||
files = append(files, file)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
if files[i].FileID == files[j].FileID {
|
||||
return files[i].MessageID < files[j].MessageID
|
||||
}
|
||||
return files[i].FileID < files[j].FileID
|
||||
})
|
||||
|
||||
limit := query.Limit
|
||||
if limit <= 0 || limit > len(files) {
|
||||
limit = len(files)
|
||||
}
|
||||
return ListFilesResult{Files: files[:limit]}
|
||||
}
|
||||
|
||||
func messageLooksFileBacked(message Message) bool {
|
||||
return strings.Contains(strings.ToUpper(message.Subject), "FILE_TRANSFER") || extractMessageFileName(message.Text) != ""
|
||||
}
|
||||
|
||||
func extractMessageFileName(text string) string {
|
||||
match := fileNamePattern.FindString(strings.TrimSpace(text))
|
||||
return strings.Trim(match, " .,;,。;")
|
||||
}
|
||||
|
||||
func fileID(messageID string, fileName string) string {
|
||||
if messageID == "" {
|
||||
return fileName
|
||||
}
|
||||
return messageID + ":" + fileName
|
||||
}
|
||||
|
||||
func fileMatchesQuery(file File, query ListFilesQuery) bool {
|
||||
if query.ConversationID != "" && file.ConversationID != query.ConversationID {
|
||||
return false
|
||||
}
|
||||
if query.MessageID != "" && file.MessageID != query.MessageID {
|
||||
return false
|
||||
}
|
||||
if query.FileID != "" && file.FileID != query.FileID {
|
||||
return false
|
||||
}
|
||||
nameQuery := strings.ToLower(strings.TrimSpace(query.NameContains))
|
||||
if nameQuery != "" && !strings.Contains(strings.ToLower(file.FileName), nameQuery) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func mimeTypeForFileName(name string) string {
|
||||
switch strings.ToLower(filepath.Ext(name)) {
|
||||
case ".doc":
|
||||
return "application/msword"
|
||||
case ".docx":
|
||||
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
case ".xls":
|
||||
return "application/vnd.ms-excel"
|
||||
case ".xlsx":
|
||||
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
case ".ppt":
|
||||
return "application/vnd.ms-powerpoint"
|
||||
case ".pptx":
|
||||
return "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||
case ".pdf":
|
||||
return "application/pdf"
|
||||
case ".zip":
|
||||
return "application/zip"
|
||||
case ".rar":
|
||||
return "application/vnd.rar"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".jpg", ".jpeg":
|
||||
return "image/jpeg"
|
||||
case ".txt":
|
||||
return "text/plain"
|
||||
case ".csv":
|
||||
return "text/csv"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
31
internal/isphere/files_test.go
Normal file
31
internal/isphere/files_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package isphere
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListFilesFromMessagesExtractsFileTransferCandidates(t *testing.T) {
|
||||
messages := []Message{
|
||||
{ID: "msg-1", ConversationID: "alice@imopenfire1-lanzhou|bob@imopenfire1-lanzhou", Text: "redacted-report.docx", Timestamp: "1783423807000", Subject: "FILE_TRANSFER_CANCEL"},
|
||||
{ID: "msg-2", ConversationID: "alice@imopenfire1-lanzhou|bob@imopenfire1-lanzhou", Text: "plain chat", Timestamp: "1783423808000"},
|
||||
{ID: "msg-3", ConversationID: "room@conference.imopenfire1-lanzhou", Text: "redacted-screenshot.png", Timestamp: "1783423809000"},
|
||||
}
|
||||
|
||||
got := ListFilesFromMessages(messages, ListFilesQuery{NameContains: "report", Limit: 10})
|
||||
want := ListFilesResult{Files: []File{
|
||||
{
|
||||
FileID: "msg-1:redacted-report.docx",
|
||||
MessageID: "msg-1",
|
||||
ConversationID: "alice@imopenfire1-lanzhou|bob@imopenfire1-lanzhou",
|
||||
FileName: "redacted-report.docx",
|
||||
MimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
CreatedAt: "1783423807000",
|
||||
Source: "local_readonly",
|
||||
RawRef: "packetlog_file_transfer",
|
||||
},
|
||||
}}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("ListFilesFromMessages() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user