openidec

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

msg.go (8270B)


      1 // Message bundles manipulations (encode/decode).
      2 // Decode message from user (point).
      3 // Some validation functions.
      4 
      5 package ii
      6 
      7 import (
      8 	"crypto/sha256"
      9 	"encoding/base64"
     10 	"errors"
     11 	"fmt"
     12 	"strings"
     13 	"time"
     14 )
     15 
     16 // II-tags, encoded in raw message as key1/value1/key2/value2.. string
     17 // When message is decoded into Msg,
     18 // key/value properties of tags associated with it.
     19 // When encoding Msg, all properties will translated to tags string
     20 // List - is the names of properties
     21 // Hash - is the map of properties (Name->Value)
     22 type Tags struct {
     23 	Hash map[string]string
     24 	List []string
     25 }
     26 
     27 // Decoded message.
     28 // Has all atrributes of message
     29 // including Tags.
     30 type Msg struct {
     31 	MsgId string
     32 	Tags  Tags
     33 	Echo  string
     34 	Date  int64
     35 	From  string
     36 	Addr  string
     37 	To    string
     38 	Subj  string
     39 	Text  string
     40 }
     41 
     42 // Make MsgId from raw text
     43 // MsgId is unique identificator of message
     44 // It is supposed that there is no collision of MsgId
     45 // It is base64(sha256(text)) transformation
     46 func MsgId(msg string) string {
     47 	h := sha256.Sum256([]byte(msg))
     48 	id := base64.StdEncoding.EncodeToString(h[:])
     49 	id = strings.Replace(id, "+", "A", -1)
     50 	id = strings.Replace(id, "/", "Z", -1)
     51 	return id[0:20]
     52 }
     53 
     54 // Check if string is valid MsgId
     55 func IsMsgId(id string) bool {
     56 	return len(id) == 20 && !strings.Contains(id, ".")
     57 }
     58 
     59 // Check if Echoarea is private area
     60 // This is openidec extension, echoareas
     61 // that has "." prefix are for private messaging.
     62 // Those areas can be fetched only with /u/point/auth/u/e/ scheme
     63 func IsPrivate(e string) bool {
     64 	return strings.HasPrefix(e, ".")
     65 }
     66 
     67 // Check if string is valid echoarea name
     68 func IsEcho(e string) bool {
     69 	l := len(e)
     70 	return l >= 3 && l <= 120 && strings.Contains(e, ".") && !strings.Contains(e, ":")
     71 }
     72 
     73 // Check if string is valid subject
     74 // In fact, it is just return true stub :)
     75 func IsSubject(s string) bool {
     76 	return true // len(strings.TrimSpace(s)) > 0
     77 }
     78 
     79 // Check if subject is empty string
     80 // Used when validate msg from points
     81 func IsEmptySubject(s string) bool {
     82 	return len(strings.TrimSpace(s)) > 0
     83 }
     84 
     85 // Decode message from point sent with /u/point scheme.
     86 // Try to use URL save and STD base64.
     87 // Returns pointrt to decoded Msg or nil (and error)
     88 // Note: This function adds "ii/ok" to Tags and
     89 // set Date field with UTC Unix time.
     90 func DecodeMsgline(msg string, enc bool) (*Msg, error) {
     91 	var m Msg
     92 	var data []byte
     93 	var err error
     94 	if len(msg) > 65536 {
     95 		return nil, errors.New("Message too long")
     96 	}
     97 	if enc {
     98 		if data, err = base64.StdEncoding.DecodeString(msg); err != nil {
     99 			if data, err = base64.URLEncoding.DecodeString(msg); err != nil {
    100 				return nil, err
    101 			}
    102 		}
    103 	} else {
    104 		data = []byte(msg)
    105 	}
    106 	text := strings.Split(string(data), "\n")
    107 	if len(text) < 5 {
    108 		return nil, errors.New("Wrong message format")
    109 	}
    110 	if text[3] != "" {
    111 		return nil, errors.New("No body delimiter in message")
    112 	}
    113 	m.Echo = strings.TrimSpace(text[0])
    114 	if !IsEcho(m.Echo) {
    115 		return nil, errors.New("Wrong echoarea format")
    116 	}
    117 	m.To = strings.TrimSpace(text[1])
    118 	if len(m.To) == 0 {
    119 		m.To = "All"
    120 	}
    121 	m.Subj = strings.TrimSpace(text[2])
    122 	if !IsEmptySubject(m.Subj) {
    123 		return nil, errors.New("Wrong subject")
    124 	}
    125 	m.Date = time.Now().Unix()
    126 	start := 4
    127 	repto := text[4]
    128 	m.Tags, _ = MakeTags("ii/ok")
    129 	if strings.HasPrefix(repto, "@repto:") {
    130 		start += 1
    131 		repto = strings.Trim(strings.Split(repto, ":")[1], " ")
    132 		if err := m.Tags.Add("repto/" + repto); err != nil {
    133 			return nil, err
    134 		}
    135 		Trace.Printf("Add repto tag: %s", repto)
    136 	}
    137 	for i := start; i < len(text); i++ {
    138 		m.Text += text[i] + "\n"
    139 	}
    140 	m.Text = strings.TrimSuffix(m.Text, "\n")
    141 	Trace.Printf("Final message: %s\n", m.String())
    142 	return &m, nil
    143 }
    144 
    145 // Decode bundle line in msgid:message format or just message
    146 // Returns pointer to decoded Msg or nil, error if fail.
    147 // Can parse URL safe and STD BASE64.
    148 // This function does NOT add ii/ok tag and does NOT change Date
    149 func DecodeBundle(msg string) (*Msg, error) {
    150 	var m Msg
    151 	if strings.Contains(msg, ":") {
    152 		spl := strings.Split(msg, ":")
    153 		if len(spl) != 2 {
    154 			return nil, errors.New("Wrong bundle format")
    155 		}
    156 		msg = spl[1]
    157 		m.MsgId = spl[0]
    158 		if !IsMsgId(m.MsgId) {
    159 			return nil, errors.New("Wrong MsgId format")
    160 		}
    161 	}
    162 	msg = strings.Replace(msg, "-", "+", -1) /* if it is URL base64 */
    163 	msg = strings.Replace(msg, "_", "/", -1) /* make it base64 */
    164 	data, err := base64.StdEncoding.DecodeString(msg)
    165 	if err != nil {
    166 		return nil, err
    167 	}
    168 	if m.MsgId == "" {
    169 		m.MsgId = MsgId(string(data))
    170 	}
    171 	text := strings.Split(string(data), "\n")
    172 	if len(text) <= 8 {
    173 		return nil, errors.New("Wrong message format")
    174 	}
    175 	m.Tags, err = MakeTags(text[0])
    176 	if err != nil {
    177 		return nil, err
    178 	}
    179 	m.Echo = text[1]
    180 	if !IsEcho(m.Echo) {
    181 		return nil, errors.New("Wrong echoarea format")
    182 	}
    183 	_, err = fmt.Sscanf(text[2], "%d", &m.Date)
    184 	if err != nil {
    185 		return nil, err
    186 	}
    187 	m.From = text[3]
    188 	m.Addr = text[4]
    189 	m.To = text[5]
    190 	m.Subj = text[6]
    191 	if !IsSubject(m.Subj) {
    192 		return nil, errors.New("Wrong subject")
    193 	}
    194 	for i := 8; i < len(text); i++ {
    195 		m.Text += text[i] + "\n"
    196 	}
    197 	m.Text = strings.TrimSuffix(m.Text, "\n")
    198 	return &m, nil
    199 }
    200 
    201 // Creates Tags from string in key1/value1/key2/value2/... format
    202 // Can return error (with unfilled Tags) if format is wrong.
    203 func MakeTags(str string) (Tags, error) {
    204 	var t Tags
    205 	str = strings.Trim(str, " ")
    206 	if str == "" { // empty
    207 		return t, nil
    208 	}
    209 	tags := strings.Split(str, "/")
    210 	if len(tags)%2 != 0 {
    211 		return t, errors.New("Wrong tags: " + str)
    212 	}
    213 	t.Hash = make(map[string]string)
    214 	for i := 0; i < len(tags); i += 2 {
    215 		t.Hash[tags[i]] = tags[i+1]
    216 		t.List = append(t.List, tags[i])
    217 	}
    218 	return t, nil
    219 }
    220 
    221 // Create Tags from string in key1/value1/key2/value2/... format
    222 // ignoring errors. This is useful for creating new "ii/ok" tag.
    223 func NewTags(str string) Tags {
    224 	t, _ := MakeTags(str)
    225 	return t
    226 }
    227 
    228 // Returns Tags propertie with name n.
    229 // Returns "", false if such propertie does not exists in Tags.
    230 func (t *Tags) Get(n string) (string, bool) {
    231 	if t == nil || t.Hash == nil {
    232 		return "", false
    233 	}
    234 	v, ok := t.Hash[n]
    235 	if ok {
    236 		return v, true
    237 	}
    238 	return "", false
    239 }
    240 
    241 // Add tags in key/value/... format to existing Tags.
    242 func (t *Tags) Add(str string) error {
    243 	tags := strings.Split(str, "/")
    244 	if len(tags)%2 != 0 {
    245 		return errors.New("Wrong tags")
    246 	}
    247 	if t.Hash == nil {
    248 		t.Hash = make(map[string]string)
    249 	}
    250 	for i := 0; i < len(tags); i += 2 {
    251 		_, ok := t.Hash[tags[i]]
    252 		t.Hash[tags[i]] = tags[i+1]
    253 		if !ok { /* new tag */
    254 			t.List = append(t.List, tags[i])
    255 		}
    256 	}
    257 	return nil
    258 }
    259 
    260 // Remove tag with name tag from Tags.
    261 func (t *Tags) Del(tag string) bool {
    262 	if t.Hash == nil {
    263 		return false
    264 	}
    265 	_, ok := t.Hash[tag]
    266 	if !ok {
    267 		return false
    268 	}
    269 	delete(t.Hash, tag)
    270 	for k, v := range t.List {
    271 		if v == tag {
    272 			copy(t.List[k:], t.List[k+1:])
    273 			t.List[len(t.List)-1] = ""
    274 			t.List = t.List[:len(t.List)-1]
    275 			return true
    276 		}
    277 	}
    278 	return false
    279 }
    280 
    281 // Translate Tags to string in key1/value1/key2/value2/... format.
    282 func (t Tags) String() string {
    283 	var text string
    284 	if t.Hash == nil {
    285 		return ""
    286 	}
    287 	for _, n := range t.List {
    288 		if val, ok := t.Hash[n]; ok {
    289 			text += fmt.Sprintf("%s/%s/", n, val)
    290 		}
    291 	}
    292 	text = strings.TrimSuffix(text, "/")
    293 	return text
    294 }
    295 
    296 // Dump (returns string) decoded message for debug purposes.
    297 func (m *Msg) Dump() string {
    298 	if m == nil {
    299 		return ""
    300 	}
    301 	return fmt.Sprintf("id: %s\ntags: %s\nechoarea: %s\ndate: %s\nmsgfrom: %s\naddr: %s\nmsgto: %s\nsubj: %s\n\n%s",
    302 		m.MsgId, m.Tags.String(), m.Echo, time.Unix(m.Date, 0), m.From, m.Addr, m.To, m.Subj, m.Text)
    303 }
    304 
    305 // Get if tag property with name n is associated with Msg
    306 func (m *Msg) Tag(n string) (string, bool) {
    307 	return m.Tags.Get(n)
    308 }
    309 
    310 // Translate decoded Msg to raw text format ready to encoding.
    311 func (m *Msg) String() string {
    312 	tags := m.Tags.String()
    313 	text := strings.Join([]string{tags, m.Echo,
    314 		fmt.Sprint(m.Date),
    315 		m.From,
    316 		m.Addr,
    317 		m.To,
    318 		m.Subj,
    319 		"",
    320 		m.Text}, "\n")
    321 	return text
    322 }
    323 
    324 // Encode Msg into bundle format (msgid:base64text).
    325 func (m *Msg) Encode() string {
    326 	var text string
    327 	if m == nil || m.Echo == "" {
    328 		return ""
    329 	}
    330 	if m.Date == 0 {
    331 		now := time.Now()
    332 		m.Date = now.Unix()
    333 	}
    334 	text = m.String()
    335 	if m.MsgId == "" {
    336 		m.MsgId = MsgId(text)
    337 	}
    338 	return m.MsgId + ":" + base64.StdEncoding.EncodeToString([]byte(text))
    339 }