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