~psic4t/qcard

e079a4a9614716fe2c8136c5cb80da2278450272 — psic4t 7 months ago 90fbe4f
support contact creation
6 files changed, 172 insertions(+), 51 deletions(-)

M README.md
M defines.go
M helpers.go
M main.go
M parse.go
M qcard
M README.md => README.md +1 -1
@@ 1,6 1,6 @@
# qcard

qcard is a quick addressbook application for CardDAV servers written in Go. In
qcard is a CLI addressbook application for CardDAV servers written in Go. In
contrast to other tools it does not cache anything. It can fetch multiple
servers / addressbooks in parallel which makes it quite fast.


M defines.go => defines.go +10 -21
@@ 33,27 33,14 @@ var showColor bool = true
var qcardversion string = "0.1.1"

const (
	ConfigDir      = ".config/qcard"
	CacheDir       = ".cache/qcard"
	dateFormat     = "02.01.06"
	dayMonthFormat = "02.01"
	timeFormat     = "15:04"
	RFC822         = "02.01.06 15:04"
	// ics date format ( describes a whole day)
	IcsFormat   = "20060102T150405"
	IcsFormatTZ = "TZID=MST:20060102T150405"
	//IcsFormatTZ         = "20060102T150405Z -0700"
	IcsFormatWholeDay   = "20060102"
	IcsFormatWholeMonth = "200601"
	IcsFormatMonthDay   = "0102"
	IcsFormatTime       = "T150405Z"
	Weekday             = "Mon"
	IcsFormatYear       = "2006"
	ColWhite            = "\033[1;37m"
	ColDefault          = "\033[0m"
	ColGreen            = "\033[0;32m"
	ColYellow           = "\033[1;33m"
	ColBlue             = "\033[1;34m"
	ConfigDir  = ".config/qcard"
	CacheDir   = ".cache/qcard"
	IcsFormat  = "20060102T150405Z"
	ColWhite   = "\033[1;37m"
	ColDefault = "\033[0m"
	ColGreen   = "\033[0;32m"
	ColYellow  = "\033[1;33m"
	ColBlue    = "\033[1;34m"
)

type configStruct struct {


@@ 76,7 63,9 @@ type contactStruct struct {
	phoneHome    string
	phoneWork    string
	emailHome    string
	emailWork    string
	addressHome  string
	addressWork  string
	birthday     string
	note         string
}

M helpers.go => helpers.go +31 -3
@@ 103,6 103,19 @@ func checkError(e error) {
	}
}

func splitAfter(s string, re *regexp.Regexp) (r []string) {
	re.ReplaceAllStringFunc(s, func(x string) string {
		s = strings.Replace(s, x, "::"+x, -1)
		return s
	})
	for _, x := range strings.Split(s, "::") {
		if x != "" {
			r = append(r, x)
		}
	}
	return
}

func (e contactStruct) fancyOutput() {
	if showColor {
		fmt.Print(e.Color + colorBlock + ColDefault + ` `)


@@ 126,19 139,27 @@ func (e contactStruct) fancyOutput() {
		}
		if e.phoneHome != "" {
			fmt.Printf(`%2s`, ` `)
			fmt.Println(`H: ` + e.phoneHome)
			fmt.Println(`P: ` + e.phoneHome)
		}
		if e.emailHome != "" {
			fmt.Printf(`%2s`, ` `)
			fmt.Println(`E: ` + e.emailHome)
		}
		if e.emailWork != "" {
			fmt.Printf(`%2s`, ` `)
			fmt.Println(`e: ` + e.emailWork)
		}
		if e.addressHome != "" {
			fmt.Printf(`%2s`, ` `)
			fmt.Println(`A: ` + e.addressHome)
		}
		if e.addressWork != "" {
			fmt.Printf(`%2s`, ` `)
			fmt.Println(`a: ` + e.addressWork)
		}
		if e.phoneWork != "" {
			fmt.Printf(`%2s`, ` `)
			fmt.Println(`W: ` + e.phoneWork)
			fmt.Println(`p: ` + e.phoneWork)
		}
		if e.birthday != "" {
			fmt.Printf(`%2s`, ` `)


@@ 148,6 169,10 @@ func (e contactStruct) fancyOutput() {
			fmt.Printf(`%2s`, ` `)
			fmt.Println(`N: ` + e.name)
		}
		if e.note != "" {
			fmt.Printf(`%2s`, ` `)
			fmt.Println(`n: ` + e.note)
		}
	}

	if showFilename {


@@ 197,8 222,11 @@ func filterMatch(fullName string) bool {
}

func deleteContact(abNumber string, contactFilename string) (status string) {
	if contactFilename == "" {
		log.Fatal("No contact filename given")
	}

	abNo, _ := strconv.ParseInt(abNumber, 0, 64)
	//fmt.Println(config.Addressbooks[calNo].Url + eventFilename)

	req, _ := http.NewRequest("DELETE", config.Addressbooks[abNo].Url+contactFilename, nil)
	req.SetBasicAuth(config.Addressbooks[abNo].Username, config.Addressbooks[abNo].Password)

M main.go => main.go +115 -4
@@ 8,9 8,13 @@ import (
	"io/ioutil"
	"log"
	"net/http"
	//"os"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"
)

var config = getConf()


@@ 92,6 96,110 @@ func showAddresses(singleAB string) {
	}
}

func createContact(abNo string, contactData string) {
	//dataArr := strings.Split(appointmentData, " ")
	curTime := time.Now()
	d := regexp.MustCompile(`\s[a-z,A-Z]:`)
	//dataArr := t.SplitAfter(contactData, -1)
	dataArr := splitAfter(contactData, d) // own function, splitAfter is not supported by regex module
	//dataArr := re.FindAll(\s+A-Z:, contactData)

	var fullName string
	var name string
	var phoneCell string
	var phoneHome string
	var phoneWork string
	var emailHome string
	var emailWork string
	var addressHome string
	var addressWork string
	var note string
	var birthday string
	var organisation string
	var title string

	newUUID := genUUID()

	for i, e := range dataArr {
		if i == 0 {
			fullName = e
			lastInd := strings.LastIndex(e, " ")              // split name at last space
			name = e[lastInd+1:] + ";" + e[0:lastInd] + ";;;" // lastname, givenname1 givenname2
		} else {
			attr := strings.Split(e, ":")

			switch attr[0] {
			case " M":
				phoneCell = "\nTEL;TYPE=CELL:" + attr[1]
			case " P":
				phoneHome = "\nTEL;TYPE=HOME:" + attr[1]
			case " p":
				phoneWork = "\nTEL;TYPE=WORK:" + attr[1]
			case " E":
				emailHome = "\nEMAIL;TYPE=HOME:" + attr[1]
			case " e":
				emailWork = "\nEMAIL;TYPE=WORK:" + attr[1]
			case " A":
				if strings.Contains(attr[1], ";") == false {
					log.Fatal("Address must be splitted in Semicola")
				}
				addressHome = "\nADR;TYPE=HOME:" + attr[1]
			case " a":
				addressWork = "\nADR;TYPE=WORK:" + attr[1]
			case " O":
				organisation = "\nORG:" + attr[1]
			case " B":
				birthday = "\nBDAY:" + attr[1]
			case " n":
				note = "\nNOTE:" + attr[1]
			case " T":
				title = "\nTITLE:" + attr[1]
			}
		}
	}

	var contactSkel = `BEGIN:VCARD
VERSION:3.0
PRODID:-//qcard
UID:` + newUUID +
		emailHome +
		phoneCell +
		phoneHome +
		phoneWork +
		emailHome +
		emailWork +
		addressHome +
		addressWork +
		birthday +
		organisation +
		note +
		title + `
FN:` + fullName + `
N:` + name + `
REV:` + curTime.UTC().Format(IcsFormat) + `
END:VCARD`
	//fmt.Println(contactSkel)
	//os.Exit(3)

	newElem := newUUID + `.vcf`

	abNo1, _ := strconv.ParseInt(abNo, 0, 64)

	req, _ := http.NewRequest("PUT", config.Addressbooks[abNo1].Url+newElem, strings.NewReader(contactSkel))
	req.SetBasicAuth(config.Addressbooks[abNo1].Username, config.Addressbooks[abNo1].Password)
	req.Header.Add("Content-Type", "application/xml; charset=utf-8")

	cli := &http.Client{}
	resp, err := cli.Do(req)
	defer resp.Body.Close()

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(resp.Status)
}

func main() {
	toFile := false



@@ 103,9 211,10 @@ func main() {
	version := flag.Bool("v", false, "Show version")
	showAddressbooks := flag.Bool("l", false, "List configured addressbooks with their corresponding numbers (for \"-c\")")
	contactFile := flag.String("u", "", "Upload contact file. Provide filename and use with \"-c\"")
	contactDelete := flag.String("d", "", "Delete contact. Get filename with \"-f\" and use with \"-c\"")
	contactDump := flag.String("dump", "", "Dump raw contact data. Get filename with \"-f\" and use with \"-c\"")
	contactDelete := flag.String("delete", "", "Delete contact. Get filename with \"-f\" and use with \"-c\"")
	contactDump := flag.String("d", "", "Dump raw contact data. Get filename with \"-f\" and use with \"-c\"")
	contactEdit := flag.String("edit", "", "Edit + upload contact data. Get filename with \"-f\" and use with \"-c\"")
	contactNew := flag.String("n", "", "Add a new contact. Check README.md for syntax")
	flag.Parse()
	flagset := make(map[string]bool) // map for flag.Visit. get bools to determine set flags
	flag.Visit(func(f *flag.Flag) { flagset[f.Name] = true })


@@ 114,12 223,14 @@ func main() {
	}
	if flagset["l"] {
		getAbList()
	} else if flagset["d"] {
	} else if flagset["delete"] {
		deleteContact(*abNumber, *contactDelete)
	} else if flagset["dump"] {
	} else if flagset["d"] {
		dumpContact(*abNumber, *contactDump, toFile)
	} else if flagset["p"] {
		displayVCF()
	} else if flagset["n"] {
		createContact(*abNumber, *contactNew)
	} else if flagset["edit"] {
		editContact(*abNumber, *contactEdit)
	} else if flagset["u"] {

M parse.go => parse.go +15 -22
@@ 11,28 11,6 @@ func trimField(field, cutset string) string {
	return strings.TrimRight(cutsetRem, "\r\n")
}

func parseEventSummary(eventData *string) string {
	re, _ := regexp.Compile(`SUMMARY(?:;LANGUAGE=[a-zA-Z\-]+)?.*?\n`)
	result := re.FindString(*eventData)
	return trimField(result, `SUMMARY(?:;LANGUAGE=[a-zA-Z\-]+)?:`)
}

func parseEventDescription(eventData *string) string {
	re, _ := regexp.Compile(`DESCRIPTION:.*?\n(?:\s+.*?\n)*`)

	resultA := re.FindAllString(*eventData, -1)
	result := strings.Join(resultA, ", ")
	//result = strings.Replace(result, "\n", "", -1)
	result = strings.Replace(result, "\\N", "\n", -1)
	//better := strings.Replace(re.FindString(result), "\n ", "", -1)
	//better = strings.Replace(better, "\\n", " ", -1)
	//better = strings.Replace(better, "\\", "", -1)

	//return trimField(better, "DESCRIPTION:")
	//return trimField(result, "DESCRIPTION:")
	return trimField(strings.Replace(result, "\r\n ", "", -1), "DESCRIPTION:")
}

func parseContactFullName(contactData *string) string {
	re, _ := regexp.Compile(`\nFN:.*\n`)
	result := re.FindString(*contactData)


@@ 70,6 48,12 @@ func parseContactEmailHome(contactData *string) string {
	return trimField(result, "(?i)EMAIL;TYPE=HOME:")
}

func parseContactEmailWork(contactData *string) string {
	re, _ := regexp.Compile(`(?i)EMAIL;TYPE=WORK:.*?\n`)
	result := re.FindString(*contactData)
	return trimField(result, "(?i)EMAIL;TYPE=WORK:")
}

func parseContactAddressHome(contactData *string) string {
	re, _ := regexp.Compile(`(?i)ADR;TYPE=HOME:.*?\n`)
	result := re.FindString(*contactData)


@@ 77,6 61,13 @@ func parseContactAddressHome(contactData *string) string {
	return trimField(result, "(?i)ADR;TYPE=HOME:")
}

func parseContactAddressWork(contactData *string) string {
	re, _ := regexp.Compile(`(?i)ADR;TYPE=WORK:.*?\n`)
	result := re.FindString(*contactData)
	result = strings.Replace(result, ";;;", "", -1) // remove triple semicola
	return trimField(result, "(?i)ADR;TYPE=WORK:")
}

func parseContactBirthday(contactData *string) string {
	re, _ := regexp.Compile(`(?i)BDAY:.*?\n`)
	result := re.FindString(*contactData)


@@ 117,7 108,9 @@ func parseMain(contactData *string, contactsSlice *[]contactStruct, href, color 
			phoneHome:    parseContactPhoneHome(contactData),
			phoneWork:    parseContactPhoneWork(contactData),
			emailHome:    parseContactEmailHome(contactData),
			emailWork:    parseContactEmailWork(contactData),
			addressHome:  parseContactAddressHome(contactData),
			addressWork:  parseContactAddressWork(contactData),
			birthday:     parseContactBirthday(contactData),
			note:         parseContactNote(contactData),
		}

M qcard => qcard +0 -0