Initial commit

This commit is contained in:
Timmy Welch 2022-10-21 14:19:02 -07:00
commit bfaf0a59de
No known key found for this signature in database
8 changed files with 914 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@ -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

93
.golangci.yaml Normal file
View File

@ -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

27
.pre-commit-config.yaml Normal file
View File

@ -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

11
go.mod Normal file
View File

@ -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
)

10
go.sum Normal file
View File

@ -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=

482
main.go Normal file
View File

@ -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)
}
}

87
template.go Normal file
View File

@ -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 = `
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Conversation: {{.Title}}</title>
<style type="text/css">
body { font-family: "Helvetica Neue", sans-serif; font-size: 10pt; }
p { margin: 0; clear: both; }
.time { text-align: center; color: #8e8e93; font-variant: small-caps; font-weight: bold; font-size: 9pt; }
.name { text-align: left; color: #8e8e93; font-size: 9pt; padding-left: 1ex; padding-top: 1ex; margin-bottom: 2px; }
img { max-width: 100%; }
.message { text-align: left; color: black; border-radius: 8px; background-color: #e1e1e1; padding: 6px; display: inline-block; max-width: 75%; margin-bottom: 5px; float: left; }
.message.sent { text-align: right; background-color: #007aff; color: white; float: right;}
</style>
</head>
<body>
{{- range $index, $msg := .Messages}}
{{- if newDate $index}}
<p class="time">{{$msg.Date}}</p><br />
{{end}}
{{- if $msg.Sent}}
<p class="message sent" title="{{$msg.Date}}">
{{else}}
<p class="name">{{$msg.Sender.Name}}</p>
<p class="message" title="{{$msg.Date}}">
{{ end}}
{{- range $index, $attachment := $msg.Attachments}}
<img title="{{$attachment.Name}}" src="data:{{$attachment.Mime}};base64,{{$attachment.Data | base64 }}"></img><br/>
{{end}}
{{- $msg.Text}}
</p><br />
{{end}}
</body>
</html>
`
// 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
}

183
xml.go Normal file
View File

@ -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]) == "&#55357;" {
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
}