From bfaf0a59de73a013ddab25d174b2d62e6f085c65 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Fri, 21 Oct 2022 14:19:02 -0700 Subject: [PATCH] Initial commit --- .gitignore | 21 ++ .golangci.yaml | 93 ++++++++ .pre-commit-config.yaml | 27 +++ go.mod | 11 + go.sum | 10 + main.go | 482 ++++++++++++++++++++++++++++++++++++++++ template.go | 87 ++++++++ xml.go | 183 +++++++++++++++ 8 files changed, 914 insertions(+) create mode 100644 .gitignore create mode 100644 .golangci.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 template.go create mode 100644 xml.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b735ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..7cd8cd9 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,93 @@ +issues: + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + max-same-issues: 1 + # Fix found issues (if it's supported by the linter). + fix: true +linters: + # Enable specific linter + # https://golangci-lint.run/usage/linters/#enabled-by-default + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + - decorder + - depguard + - dogsled + - dupl + - dupword + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - execinquery + - exhaustive + - exportloopref + - forcetypeassert + - gci + - gochecknoglobals + - gochecknoinits + - goconst + - gocritic + - godot + - godox + - goerr113 + - gofmt + - gofumpt + - goheader + - goimports + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - gosimple + - govet + - grouper + - importas + - ineffassign + - interfacebloat + - ireturn + - loggercheck + - maintidx + - makezero + - misspell + - nakedret + - nestif + - nilerr + - nilnil + - nlreturn + - noctx + - nolintlint + - nosprintfhostport + - paralleltest + - prealloc + - predeclared + - promlinter + - reassign + - revive + - staticcheck + - stylecheck + - tagliatelle + - tenv + - testableexamples + - testpackage + - thelper + - tparallel + - typecheck + - unconvert + - unused + - usestdlibvars + - varnamelen + - whitespace + - wsl + +linters-settings: + varnamelen: + ignore-names: + - i + - d diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..85e7e87 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml +- repo: local + hooks: + - id: gofumpt + name: gofumpt + entry: gofumpt + args: [-l, -w, -extra] + language: golang + types: [go] + additional_dependencies: ['mvdan.cc/gofumpt@latest'] + - id: structslop + name: structslop + entry: structslop + args: [-apply] + language: golang + types: [go] + additional_dependencies: ['github.com/orijtech/structslop/cmd/structslop@latest'] +- repo: https://github.com/golangci/golangci-lint + rev: v1.50.0 + hooks: + - id: golangci-lint diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7134875 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module sms + +go 1.19 + +require golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 + +require ( + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a8d729a --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 h1:sBdrWpxhGDdTAYNqbgBLAR+ULAPPhfgncLr1X0lyWtg= +golang.org/x/exp v0.0.0-20221012211006-4de253d81b95/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= diff --git a/main.go b/main.go new file mode 100644 index 0000000..e329ddb --- /dev/null +++ b/main.go @@ -0,0 +1,482 @@ +package main + +import ( + "encoding/base64" + "encoding/xml" + "flag" + "fmt" + "os" + "strconv" + "strings" + "time" + + "golang.org/x/exp/slices" +) + +const ( + RECEIVED = 1 + SENT = 2 +) + +type SMSType int + +func (s SMSType) String() string { + switch s { + case RECEIVED: + return "From" + default: + return "To" + } +} + +// Smses was generated 2022-10-16 10:53:24 by timmy on turin.narnian.us. +type customTime struct{ time.Time } + +func (c *customTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var ( + value string + err error + date int64 + ) + + err = d.DecodeElement(&value, &start) + if err != nil { + return err + } + + date, err = strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + + *c = customTime{time.UnixMilli(date)} + + return nil +} + +func (c *customTime) UnmarshalXMLAttr(attr xml.Attr) error { + date, err := strconv.ParseInt(attr.Value, 10, 64) + if err != nil { + return err + } + + *c = customTime{time.UnixMilli(date)} + + return nil +} + +type address string + +func (a *address) UnmarshalXMLAttr(attr xml.Attr) error { + *a = address(cleanAddress(attr.Value)) + + return nil +} + +func cleanAddress(addr string) string { + r := strings.NewReplacer("(", "", ")", "", " ", "", "-", "", "+1", "") + + return strings.TrimLeft(r.Replace(addr), "1") +} + +type sms struct { + DateSent customTime `xml:"date_sent,attr"` + Date customTime `xml:"date,attr"` + Address address `xml:"address,attr"` + ContactName string `xml:"contact_name,attr"` + Subject string `xml:"subject,attr"` + Text string `xml:"body,attr"` + Toa string `xml:"toa,attr"` + ScToa string `xml:"sc_toa,attr"` + ServiceCenter string `xml:"service_center,attr"` + ReadableDate string `xml:"readable_date,attr"` + Type SMSType `xml:"type,attr"` + Status int `xml:"status,attr"` + Protocol int `xml:"protocol,attr"` + Locked bool `xml:"locked,attr"` + Read bool `xml:"read,attr"` +} + +func (s sms) String() string { + return fmt.Sprint(s.Date) +} + +type attachment struct { + Mime string + Name string + data []byte +} + +type mms struct { + Date customTime `xml:"date,attr"` + DateSent customTime `xml:"date_sent,attr"` + Contacts []Contact + Attachments []attachment + ImageResizeStatus string `xml:"image_resize_status,attr"` + Text string + Type SMSType `xml:"msg_box,attr"` + TextOnly bool `xml:"text_only,attr"` + Read bool `xml:"read,attr"` + Seen bool `xml:"seen,attr"` +} + +type Contact struct { + Name string + Number string + Sender bool +} + +type Message struct { + Date customTime + Type SMSType + DateSent customTime + Contacts []Contact + Attachments []attachment + Text string +} + +func (m Message) Sent() bool { + return m.Type == SENT +} + +func (m Message) Sender() Contact { + for _, v := range m.Contacts { + if v.Sender { + return v + } + } + + if len(m.Contacts) > 0 { + return m.Contacts[0] + } + + return Contact{} +} + +func (m Message) Addresses() []string { + str := make([]string, 0, len(m.Contacts)) + for _, v := range m.Contacts { + str = append(str, v.Number) + } + + return str +} + +func (m Message) Names() []string { + str := make([]string, 0, len(m.Contacts)) + for _, v := range m.Contacts { + str = append(str, v.Name) + } + + return str +} + +func (m *mms) unmarshalXML(d *xml.Decoder, start *xml.StartElement) error { + var ( + err error + data []byte + ) + + switch start.Name.Local { + case "parts": + tmp := struct { + A []struct { + Mime string `xml:"ct,attr"` + Text string `xml:"text,attr"` + Name string `xml:"Name,attr"` + Data string `xml:"data,attr"` + } `xml:"part"` + }{} + + err = d.DecodeElement(&tmp, start) + if err != nil { + return err + } + + for _, att := range tmp.A { + switch att.Mime { + case "application/smil": + continue + case "text/plain": + m.Text += att.Text + default: + data, err = base64.StdEncoding.DecodeString(att.Data) + if err != nil { + return err + } + + m.Attachments = append(m.Attachments, attachment{ + Mime: att.Mime, + Name: att.Name, + data: data, + }) + } + } + + case "addrs": + tmp := struct { + A []struct { + Address address `xml:"address,attr"` + Type string `xml:"type"` + } `xml:"addr"` + }{} + + err = d.DecodeElement(&tmp, start) + if err != nil { + return err + } + + for _, v := range tmp.A { + m.Contacts = append(m.Contacts, Contact{Name: "", Number: string(v.Address), Sender: v.Type == "137"}) + } + } + + return nil +} + +func (m *mms) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var ( + err error + contacts []string + addresses []string + ) + + for _, attr := range start.Attr { + switch attr.Name.Local { + case "text_only": + m.TextOnly, err = strconv.ParseBool(attr.Value) + if err != nil { + return err + } + case "date": + m.Date = customTime{} + + err = (&m.Date).UnmarshalXMLAttr(attr) + if err != nil { + return err + } + case "read": + m.Read, err = strconv.ParseBool(attr.Value) + if err != nil { + return err + } + case "image_resize_status": + m.ImageResizeStatus = attr.Value + case "msg_box": + var num int + + num, err = strconv.Atoi(attr.Value) + if err != nil { + return err + } + + m.Type = SMSType(num) + case "date_sent": + err = m.DateSent.UnmarshalXMLAttr(attr) + if err != nil { + return err + } + case "seen": + m.Seen, err = strconv.ParseBool(attr.Value) + if err != nil { + return err + } + case "contact_name": + contacts = strings.Split(attr.Value, ", ") + case "address": + addresses = strings.Split(attr.Value, "~") + } + } + + Contacts := make(map[string]string, len(addresses)) + + for i, v := range addresses { + if i < len(contacts) { + Contacts[v] = contacts[i] + } + } + + cont := true + for cont { + token, _ := d.Token() + switch element := token.(type) { + case xml.StartElement: + err = m.unmarshalXML(d, &element) + if err != nil { + return err + } + case xml.EndElement: + if element.Name.Local == "mms" { + cont = false + } + } + } + + for _, v := range m.Contacts { + if v.Name == "" { + v.Name = Contacts[v.Number] + } + } + + // pretty.Println(m,"\n\n") + // os.Exit(1) + return nil +} + +type Smses struct { + Count int `xml:"count,attr"` + SMS []sms `xml:"sms"` + MMS []mms `xml:"mms"` + MSGs []Message `xml:"-"` +} + +func (s *Smses) Merge() { + s.MSGs = make([]Message, 0, len(s.SMS)+len(s.MMS)) + for _, msg := range s.SMS { + s.MSGs = append(s.MSGs, Message{ + Date: msg.Date, + Type: msg.Type, + DateSent: msg.DateSent, + Contacts: []Contact{{Name: msg.ContactName, Number: string(msg.Address), Sender: msg.Type == SENT}}, + Attachments: []attachment{}, + Text: msg.Text, + }) + } + + for _, msg := range s.MMS { + s.MSGs = append(s.MSGs, Message{ + Date: msg.Date, + Type: msg.Type, + DateSent: msg.DateSent, + Contacts: msg.Contacts, + Attachments: msg.Attachments, + Text: msg.Text, + }) + } +} + +func (s *Smses) Map() map[string][]Message { + conversations := make(map[string][]Message, 128) + for _, m := range s.MSGs { + conversations[strings.Join(m.Addresses(), ", ")] = append(conversations[strings.Join(m.Addresses(), ", ")], m) + } + + for _, m := range conversations { + slices.SortFunc(m, func(a, b Message) bool { + return a.Date.Before(b.Date.Time) + }) + } + + return conversations +} + +func (s *Smses) deDupeMessages() { + slices.SortStableFunc(s.MSGs, func(a, b Message) bool { + return a.Date.Before(b.Date.Time) + }) + slices.SortStableFunc(s.MSGs, func(a, b Message) bool { + return a.Type < b.Type + }) + slices.SortStableFunc(s.MSGs, func(a, b Message) bool { + return strings.Join(a.Addresses(), "-") < strings.Join(b.Addresses(), "-") + }) + slices.SortStableFunc(s.MSGs, func(a, b Message) bool { + return a.Text < b.Text + }) + + s.MSGs = slices.CompactFunc(s.MSGs, func(a, b Message) bool { + return a.Date == b.Date && a.Text == b.Text && slices.EqualFunc(a.Contacts, b.Contacts, func(s1, s2 Contact) bool { + return s1.Number == s2.Number + }) + }) + + fmt.Printf("MSGs Count: %d\n", len(s.MSGs)) + + deDup := make(map[string][]Message, 30) + for _, m := range s.MSGs { + deDup[fmt.Sprintf("%v%v%v", m.Addresses(), m.Type, m.Text)] = append(deDup[fmt.Sprintf("%v%v%v", m.Addresses(), m.Type, m.Text)], m) + } + + fmt.Printf("Count: %d\n", len(deDup)) + + for i, conversation := range deDup { + if len(conversation) != 1 { + slices.SortFunc(conversation, func(a, b Message) bool { + return a.Date.Before(b.Date.Time) + }) + + msgs := []Message{conversation[0]} + + for index, msg := range conversation { + if index+1 == len(conversation) { + break + } + + if msg.Date.Add(time.Hour*2) != conversation[index+1].Date.Time { + msgs = append(msgs, conversation[index+1]) + } + } + + slices.SortFunc(msgs, func(a, b Message) bool { + return a.Date.Before(b.Date.Time) + }) + + deDup[i] = slices.CompactFunc(msgs, func(a, b Message) bool { + return a.Date == b.Date && a.Text == b.Text && slices.EqualFunc(a.Contacts, b.Contacts, func(s1, s2 Contact) bool { + return s1.Number == s2.Number + }) + }) + } + } +} + +func main() { + msg := new(Smses) + + number := flag.String("number","","the number to pull messages from") + + flag.Parse() + if number == nil || *number == "" { + return + } + + if flag.NArg() < 1 { + fmt.Println("fail") + os.Exit(1) + } + + file, err := os.Open(flag.Arg(0)) + if err != nil { + fmt.Printf("You fail: %v\n", err) + os.Exit(2) + } + + xmlFile := xml.NewDecoder(&xmlDecoder{file, nil, nil}) + + err = xmlFile.Decode(msg) + if err != nil { + fmt.Printf("Fuck: %v\n", err) + file.Close() + os.Exit(3) + } + + file.Close() + + msg.Merge() + + fmt.Printf("MSGs Count: %d\n", len(msg.MSGs)) + + msg.deDupeMessages() + + msgMap := msg.Map() + + msgs := msgMap[*number] + + fmt.Println(msgs) + + err = tpl(msgs) + if err != nil { + fmt.Printf("Fuck: %v\n", err) + os.Exit(4) + } +} diff --git a/template.go b/template.go new file mode 100644 index 0000000..6cfe429 --- /dev/null +++ b/template.go @@ -0,0 +1,87 @@ +package main + +import ( + "encoding/base64" + "html/template" + "os" + "time" +) + +func tpl(msgs []Message) error { + // First we create a FuncMap with which to register the function. + funcMap := template.FuncMap{ + // The name "title" is what the function will be called in the template text. + "newDate": func(i int) bool { + if i == 0 { + return true + } + + return msgs[i].Date.Sub(msgs[i-1].Date.Time) > time.Minute*30 + }, + "base64": func(data []byte) string { + return base64.StdEncoding.EncodeToString(data) + }, + } + + const templateText = ` + + + + + Conversation: {{.Title}} + + + +{{- range $index, $msg := .Messages}} + {{- if newDate $index}} +

{{$msg.Date}}


+ {{end}} + {{- if $msg.Sent}} +

+ {{else}} +

{{$msg.Sender.Name}}

+

+ {{ end}} + {{- range $index, $attachment := $msg.Attachments}} +
+ {{end}} + {{- $msg.Text}} +


+{{end}} + + +` + + // Create a template, add the function map, and parse the text. + tmpl, err := template.New("smsMessage").Funcs(funcMap).Parse(templateText) + if err != nil { + return err + } + + file, err := os.Create("untitled.html") + if err != nil { + return err + } + defer file.Close() + + // Run the template to verify the output. + err = tmpl.Execute(file, struct { + Title string + Messages []Message + }{Title: "testing", Messages: msgs}) + if err != nil { + return err + } + + return nil +} diff --git a/xml.go b/xml.go new file mode 100644 index 0000000..1f7e37e --- /dev/null +++ b/xml.go @@ -0,0 +1,183 @@ +package main + +import ( + "bytes" + "encoding/xml" + "io" + "os" + "strconv" + "strings" + "unicode/utf16" + "unicode/utf8" +) + +type lex struct { + start int + input []byte // the string being scanned + pos int // current position in the input + width int // width of last rune read from input +} + +func (l *lex) next() rune { + if l.pos >= len(l.input) { + l.width = 0 + + return -1 + } + + r, _ := utf8.DecodeRune(l.input[l.pos:]) + l.pos++ + + return r +} + +func (l *lex) peek() rune { + if l.pos >= len(l.input) { + return -1 + } + + r, _ := utf8.DecodeRune(l.input[l.pos:]) + + return r +} + +func (l *lex) backup() { + l.pos-- +} + +type xmlDecoder struct { + reader io.Reader + previous []byte + temp []byte +} + +func (l *lex) acceptRun(valid string) { + for strings.ContainsRune(valid, l.next()) { + } + l.backup() +} + +func (l *lex) getResult() []byte { + result := l.input[l.start:l.pos] + l.start = l.pos + + return result +} + +func (l *lex) getEntities() ([][]byte, []byte, []byte) { + var ( + rest []byte + result = make([][]byte, 0) + ) + + for l.peek() == '&' { + l.next() + + if l.next() != '#' { + l.backup() + l.backup() + + break + } + + l.acceptRun("1234567890") + + if r := l.next(); r == ';' { + result = append(result, l.getResult()) + } else { + l.pos = l.start + rest = make([]byte, len(l.input)-l.pos) + copy(rest, l.input[l.pos:]) + + break + } + } + + if len(result) > 0 && string(result[len(result)-1]) == "�" { + rest = result[len(result)-1] + result = result[:len(result)-1] + } + + return result, l.input[:l.pos], rest +} + +func (x *xmlDecoder) Read(data []byte) (n int, err error) { + start := 0 + + if x.previous != nil { + start = len(x.previous) + copy(data, x.previous) + x.previous = nil + } + + n, err = x.reader.Read(data[start:]) + if err != nil { + return n, err + } + + resultLen := n + i := 0 + data = data[:n] + workingData := data + + for index := bytes.Index(workingData, []byte("&#")); index >= 0; i++ { + var ( + entities [][]byte + xmlEntity []byte + l = &lex{ + input: workingData[index:], + } + ) + + entities, xmlEntity, x.previous = l.getEntities() + if x.previous != nil { + resultLen -= len(x.previous) + + break + } + + result := &strings.Builder{} + entitiesUINT16 := []uint16{} + + for i, e := range entities { + if len(e) > 2 { + e = e[2 : len(e)-1] + entities[i] = entities[i][0:0] + + v, err := strconv.Atoi(string(e)) + if err != nil { + os.Exit(91) + } + + entitiesUINT16 = append(entitiesUINT16, uint16(v)) + } + } + + runes := utf16.Decode(entitiesUINT16) + + err = xml.EscapeText(result, []byte(string(runes))) + if err != nil { + os.Exit(92) + } + + resultBytes := []byte(result.String()) + if len(xmlEntity) == len(resultBytes) { + copy(xmlEntity, resultBytes) + } else { + copy(workingData[index:], resultBytes) + copy(workingData[index+len(resultBytes):], workingData[index+len(xmlEntity):]) + resultLen += len(resultBytes) - len(xmlEntity) + workingData = workingData[:len(workingData)+(len(resultBytes)-len(xmlEntity))] + } + + workingData = workingData[index+len(resultBytes):] + index = bytes.Index(workingData, []byte("&#")) + } + + data = data[:resultLen] + x.temp = make([]byte, resultLen) + copy(x.temp, data) + x.previous = nil + + return resultLen, nil +}