Initial commit
This commit is contained in:
commit
bfaf0a59de
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal 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
93
.golangci.yaml
Normal 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
27
.pre-commit-config.yaml
Normal 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
11
go.mod
Normal 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
10
go.sum
Normal 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
482
main.go
Normal 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
87
template.go
Normal 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
183
xml.go
Normal 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]) == "�" {
|
||||||
|
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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user