github.com/la5nta/wl2k-go@v0.11.8/fbb/proposal.go (about) 1 // Copyright 2015 Martin Hebnes Pedersen (LA5NTA). All rights reserved. 2 // Use of this source code is governed by the MIT-license that can be 3 // found in the LICENSE file. 4 5 package fbb 6 7 import ( 8 "bytes" 9 "compress/gzip" 10 "errors" 11 "fmt" 12 "io" 13 "strconv" 14 "strings" 15 16 "github.com/la5nta/wl2k-go/lzhuf" 17 ) 18 19 type PropCode byte 20 21 const ( 22 BasicProposal PropCode = 'B' // Basic ASCII proposal (or compressed binary in v0/1) 23 AsciiProposal = 'A' // Compressed v0/1 ASCII proposal 24 Wl2kProposal = 'C' // Compressed v2 proposal (winlink extension) 25 GzipProposal = 'D' // Gzip compressed v2 proposal 26 ) 27 28 type ProposalAnswer byte 29 30 const ( 31 Accept ProposalAnswer = '+' 32 Reject = '-' 33 Defer = '=' 34 35 // Offset not supported yet 36 ) 37 38 // Proposal is the type representing a inbound or outbound proposal. 39 type Proposal struct { 40 code PropCode 41 msgType string 42 mid string 43 answer ProposalAnswer 44 title string 45 offset int 46 sent bool 47 size int 48 compressedData []byte 49 compressedSize int 50 } 51 52 // Constructor for a new Proposal given a Winlink Message. 53 // 54 // Reads the Winlink Message given and constructs a new proposal 55 // based on what's read and prepares for outbound delivery, returning 56 // a Proposal with the given data. 57 // 58 func NewProposal(MID, title string, code PropCode, data []byte) *Proposal { 59 prop := &Proposal{ 60 mid: MID, 61 code: code, 62 msgType: "EM", 63 title: title, 64 size: len(data), 65 } 66 67 if prop.title == `` { 68 prop.title = `No title` 69 } 70 71 var ( 72 z io.WriteCloser 73 buf bytes.Buffer 74 ) 75 switch prop.code { 76 case GzipProposal: 77 z, _ = gzip.NewWriterLevel(&buf, gzip.BestCompression) 78 default: 79 z = lzhuf.NewB2Writer(&buf) 80 } 81 82 z.Write(data) 83 if err := z.Close(); err != nil { 84 panic(err) 85 } 86 87 prop.compressedData = buf.Bytes() 88 prop.compressedSize = len(prop.compressedData) 89 90 return prop 91 } 92 93 // Method for checking if the Proposal is completely 94 // downloaded/loaded and ready to be read/sent. 95 // 96 // Typically used to check if the whole message was 97 // successfully downloaded from the CMS. 98 // 99 func (p *Proposal) DataIsComplete() bool { 100 return len(p.compressedData) == p.compressedSize 101 } 102 103 // Returns the uniqe Message ID 104 func (p *Proposal) MID() string { 105 return p.mid 106 } 107 108 // Returns the title of this proposal 109 func (p *Proposal) Title() string { 110 return p.title 111 } 112 113 func (p *Proposal) Message() (*Message, error) { 114 buf := bytes.NewBuffer(p.Data()) 115 m := new(Message) 116 err := m.ReadFrom(buf) 117 return m, err 118 } 119 120 // Data returns the decompressed raw message 121 func (p *Proposal) Data() []byte { 122 var r io.ReadCloser 123 var err error 124 125 switch p.code { 126 case GzipProposal: 127 r, err = gzip.NewReader(bytes.NewBuffer(p.compressedData)) 128 default: 129 r, err = lzhuf.NewB2Reader(bytes.NewBuffer(p.compressedData)) 130 } 131 132 if err != nil { 133 panic(err) //TODO: Should return error 134 } 135 136 var buf bytes.Buffer 137 if _, err := io.Copy(&buf, r); err != nil { 138 panic(err) //TODO 139 } 140 141 return buf.Bytes() 142 } 143 144 func parseProposal(line string, prop *Proposal) (err error) { 145 if len(line) < 1 { 146 return 147 } else if line[0] != 'F' { 148 return 149 } 150 151 prop.code = PropCode(line[1]) 152 153 switch prop.code { 154 case BasicProposal, AsciiProposal: // TODO: implement 155 case Wl2kProposal, GzipProposal: 156 err = parseB2Proposal(line, prop) 157 default: 158 err = fmt.Errorf("Unsupported proposal code '%c'", prop.code) 159 } 160 return 161 } 162 163 func parseB2Proposal(line string, prop *Proposal) (err error) { 164 if len(line) < 4 { 165 return errors.New("Unexpected end of proposal line") 166 } 167 168 if !(line[1] == Wl2kProposal || line[1] == GzipProposal) { 169 return errors.New("Not a type C or D proposal") 170 } 171 172 // FC EM TJKYEIMMHSRB 527 123 0 173 parts := strings.Split(line[3:], " ") 174 if len(parts) < 5 { 175 return errors.New(`Malformed proposal: ` + line[2:]) 176 } 177 178 for i, part := range parts { 179 switch i { 180 case 0: 181 if len(part) < 1 || len(part) > 2 { 182 return errors.New(`Malformed proposal 0`) 183 } else if part != "EM" && part != "CM" { 184 return fmt.Errorf(`Expected message type CM or EM, but found %s`, part) 185 } 186 prop.msgType = part 187 case 1: 188 prop.mid = part 189 case 2: 190 prop.size, _ = strconv.Atoi(part) 191 case 3: 192 prop.compressedSize, _ = strconv.Atoi(part) 193 case 4: 194 default: 195 return errors.New(fmt.Sprintf(`Too many parts in proposal: %+v`, parts)) 196 } 197 } 198 return 199 } 200 201 // precedence returns the priority level of the message. Lower precedence value is more important 202 // and should be handled sooner. 203 // 204 // See https://www.winlink.org/content/how_use_message_precedence_precedence. 205 func (p *Proposal) precedence() int { 206 const ( 207 Flash = iota 208 Immediate 209 Priority 210 Routine 211 ) 212 switch { 213 case strings.Contains(p.title, "//WL2K Z/"): 214 return Flash 215 case strings.Contains(p.title, "//WL2K O/"): 216 return Immediate 217 case strings.Contains(p.title, "//WL2K P/"): 218 return Priority 219 default: 220 return Routine 221 } 222 }