package main import ( "bufio" "bytes" "encoding/json" "errors" "fmt" "io/ioutil" "math/rand" "net/http" "net/url" "strings" "time" ) type Entry struct { content string title string date string lastmod string section string tags []string link string slug string replyLink string replyTitle string likeLink string likeTitle string syndicate []string language string translationKey string images []Image audio string filename string location string token string } type Image struct { url string alt string } func CreateEntry(contentType ContentType, r *http.Request) (*Entry, error) { if contentType == WwwForm { bodyString, err := parseRequestBody(r) if err != nil { return nil, err } bodyValues, err := url.ParseQuery(bodyString) if err != nil { return nil, errors.New("failed to parse query") } return createEntryFromValueMap(bodyValues) } else if contentType == Multipart { err := r.ParseMultipartForm(1024 * 1024 * 16) if err != nil { return nil, errors.New("failed to parse Multipart") } return createEntryFromValueMap(r.MultipartForm.Value) } else if contentType == Json { bodyString, err := parseRequestBody(r) if err != nil { return nil, err } parsedMfItem := &MicroformatItem{} err = json.Unmarshal([]byte(bodyString), &parsedMfItem) if err != nil { return nil, errors.New("failed to parse Json") } return createEntryFromMicroformat(parsedMfItem) } else { return nil, errors.New("unsupported content-type") } } func parseRequestBody(r *http.Request) (string, error) { defer r.Body.Close() bodyBytes, err := ioutil.ReadAll(r.Body) if err != nil { return "", errors.New("failed to read body") } return string(bodyBytes), nil } func createEntryFromValueMap(values map[string][]string) (*Entry, error) { if h, ok := values["h"]; ok && (len(h) != 1 || h[0] != "entry") { return nil, errors.New("only entry type is supported so far") } entry := &Entry{} if content, ok := values["content"]; ok { entry.content = content[0] } if name, ok := values["name"]; ok { entry.title = name[0] } if category, ok := values["category"]; ok { entry.tags = category } else if categories, ok := values["category[]"]; ok { entry.tags = categories } if slug, ok := values["mp-slug"]; ok && len(slug) > 0 && slug[0] != "" { entry.slug = slug[0] } if inReplyTo, ok := values["in-reply-to"]; ok { entry.replyLink = inReplyTo[0] } if likeOf, ok := values["like-of"]; ok { entry.likeLink = likeOf[0] } if bookmarkOf, ok := values["bookmark-of"]; ok { entry.link = bookmarkOf[0] } if syndicate, ok := values["mp-syndicate-to"]; ok { entry.syndicate = syndicate } else if syndicates, ok := values["mp-syndicate-to[]"]; ok { entry.syndicate = syndicates } if photo, ok := values["photo"]; ok { entry.images = append(entry.images, Image{url: photo[0]}) } else if photos, ok := values["photo[]"]; ok { for _, photo := range photos { entry.images = append(entry.images, Image{url: photo}) } } if photoAlt, ok := values["mp-photo-alt"]; ok { if len(entry.images) > 0 { entry.images[0].alt = photoAlt[0] } } else if photoAlts, ok := values["mp-photo-alt[]"]; ok { for i, photoAlt := range photoAlts { if len(entry.images) > i { entry.images[i].alt = photoAlt } } } if audio, ok := values["audio"]; ok { entry.audio = audio[0] } else if audio, ok := values["audio[]"]; ok { entry.audio = audio[0] } if token, ok := values["access_token"]; ok { entry.token = "Bearer " + token[0] } err := computeExtraSettings(entry) if err != nil { return nil, err } return entry, nil } func createEntryFromMicroformat(mfEntry *MicroformatItem) (*Entry, error) { if len(mfEntry.Type) != 1 || mfEntry.Type[0] != "h-entry" { return nil, errors.New("only entry type is supported so far") } entry := &Entry{} if mfEntry.Properties != nil && len(mfEntry.Properties.Content) == 1 && len(mfEntry.Properties.Content[0]) > 0 { entry.content = mfEntry.Properties.Content[0] } if len(mfEntry.Properties.Name) == 1 { entry.title = mfEntry.Properties.Name[0] } if len(mfEntry.Properties.Category) > 0 { entry.tags = mfEntry.Properties.Category } if len(mfEntry.Properties.MpSlug) == 1 && len(mfEntry.Properties.MpSlug[0]) > 0 { entry.slug = mfEntry.Properties.MpSlug[0] } if len(mfEntry.Properties.InReplyTo) == 1 { entry.replyLink = mfEntry.Properties.InReplyTo[0] } if len(mfEntry.Properties.LikeOf) == 1 { entry.likeLink = mfEntry.Properties.LikeOf[0] } if len(mfEntry.Properties.BookmarkOf) == 1 { entry.link = mfEntry.Properties.BookmarkOf[0] } if len(mfEntry.Properties.MpSyndicateTo) > 0 { entry.syndicate = mfEntry.Properties.MpSyndicateTo } if len(mfEntry.Properties.Photo) > 0 { for _, photo := range mfEntry.Properties.Photo { if theString, justString := photo.(string); justString { entry.images = append(entry.images, Image{url: theString}) } else if thePhoto, isPhoto := photo.(map[string]interface{}); isPhoto { image := Image{} // Micropub spec says "value" is correct, but not sure about that if photoUrl, ok := thePhoto["value"].(string); ok { image.url = photoUrl } if alt, ok := thePhoto["alt"].(string); ok { image.alt = alt } entry.images = append(entry.images, image) } } } if len(mfEntry.Properties.Audio) > 0 { entry.audio = mfEntry.Properties.Audio[0] } err := computeExtraSettings(entry) if err != nil { return nil, err } return entry, nil } func computeExtraSettings(entry *Entry) error { now := time.Now() // Set date entry.date = now.Format(time.RFC3339) // Find settings hidden in content var filteredContent bytes.Buffer contentScanner := bufio.NewScanner(strings.NewReader(entry.content)) for contentScanner.Scan() { text := contentScanner.Text() if strings.HasPrefix(text, "section: ") { // Section entry.section = strings.TrimPrefix(text, "section: ") } else if strings.HasPrefix(text, "title: ") { // Title entry.title = strings.TrimPrefix(text, "title: ") } else if strings.HasPrefix(text, "slug: ") { // Slug entry.slug = strings.TrimPrefix(text, "slug: ") } else if strings.HasPrefix(text, "tags: ") { // Tags entry.tags = strings.Split(strings.TrimPrefix(text, "tags: "), ",") } else if strings.HasPrefix(text, "link: ") { // Link entry.link = strings.TrimPrefix(text, "link: ") } else if strings.HasPrefix(text, "reply-link: ") { // Reply link entry.replyLink = strings.TrimPrefix(text, "reply-link: ") } else if strings.HasPrefix(text, "reply-title: ") { // Reply title entry.replyTitle = strings.TrimPrefix(text, "reply-title: ") } else if strings.HasPrefix(text, "like-link: ") { // Like link entry.likeLink = strings.TrimPrefix(text, "like-link: ") } else if strings.HasPrefix(text, "like-title: ") { // Like title entry.likeTitle = strings.TrimPrefix(text, "like-title: ") } else if strings.HasPrefix(text, "language: ") { // Language entry.language = strings.TrimPrefix(text, "language: ") } else if strings.HasPrefix(text, "translationkey: ") { // Translation key entry.translationKey = strings.TrimPrefix(text, "translationkey: ") } else if strings.HasPrefix(text, "images: ") { // Images for _, image := range strings.Split(strings.TrimPrefix(text, "images: "), ",") { entry.images = append(entry.images, Image{url: image}) } } else if strings.HasPrefix(text, "audio: ") { // Audio entry.audio = strings.TrimPrefix(text, "audio: ") } else { _, _ = fmt.Fprintln(&filteredContent, text) } } entry.content = filteredContent.String() // Check if content contains images or add them for _, image := range entry.images { if !strings.Contains(entry.content, image.url) { if len(image.alt) > 0 { entry.content += "\n![" + image.alt + "](" + image.url + " \"" + image.alt + "\")\n" } else { entry.content += "\n![](" + image.url + ")\n" } } } // Compute slug if empty if len(entry.slug) == 0 || entry.slug == "" { random := generateRandomString(now, 5) entry.slug = fmt.Sprintf("%v-%02d-%02d-%v", now.Year(), int(now.Month()), now.Day(), random) } // Compute filename and location contentFolder := "content" localizedBlogUrl := BlogUrl if len(entry.language) > 0 && entry.language != "en" { // Append language to content folder: "content-de" for language "de" contentFolder += "-" + entry.language // Append language to BlogUrl localizedBlogUrl += entry.language + "/" } if len(entry.section) < 1 { entry.section = "micro" } entry.section = strings.ToLower(entry.section) if entry.section == "thoughts" || entry.section == "links" || entry.section == "micro" { entry.filename = fmt.Sprintf("%v/%v/%02d/%02d/%v.md", contentFolder, entry.section, now.Year(), int(now.Month()), entry.slug) entry.location = fmt.Sprintf("%v%v/%02d/%02d/%v/", localizedBlogUrl, entry.section, now.Year(), int(now.Month()), entry.slug) } else { entry.filename = fmt.Sprintf("%v/%v/%v.md", contentFolder, entry.section, entry.slug) entry.location = fmt.Sprintf("%v%v/%v/", localizedBlogUrl, entry.section, entry.slug) } return nil } func generateRandomString(now time.Time, n int) string { rand.Seed(now.UnixNano()) letters := []rune("abcdefghijklmnopqrstuvwxyz") b := make([]rune, n) for i := range b { b[i] = letters[rand.Intn(len(letters))] } return string(b) } func WriteEntry(entry *Entry) (location string, err error) { file, err := WriteHugoPost(entry) if err != nil { return } err = SelectedStorage.CreateFile(entry.filename, file, entry.title) if err != nil { return } location = entry.location return } func analyzeURL(url string) (filePath string, section string, slug string, lang string, err error) { if !strings.HasPrefix(url, BlogUrl) { return } contentFolder := "content" path := "" if strings.HasPrefix(url, BlogUrl+"de/") { lang = "de" // German content folder contentFolder += "-" + lang path = strings.TrimSuffix(strings.TrimPrefix(url, BlogUrl+lang+"/"), "/") } else { path = strings.TrimSuffix(strings.TrimPrefix(url, BlogUrl), "/") } pathParts := strings.Split(path, "/") filePath = contentFolder + "/" + path + ".md" section = pathParts[0] slug = pathParts[len(pathParts)-1] return } func ReadEntry(url string) (entry *Entry, err error) { filePath, section, slug, lang, err := analyzeURL(url) if err != nil { return } fileContent, exists, err := SelectedStorage.ReadFile(filePath) if err != nil || !exists { err = errors.New("failed to read file or entry doesn't exist") return } entry, err = ReadHugoPost(fileContent) if entry != nil { entry.location = url entry.filename = filePath entry.section = section entry.slug = slug entry.language = lang } return }