You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

347 lines
10 KiB

  1. package main
  2. import (
  3. "bufio"
  4. "bytes"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "math/rand"
  9. "net/http"
  10. "strings"
  11. "text/template"
  12. "time"
  13. )
  14. type Entry struct {
  15. content string
  16. title string
  17. date string
  18. lastmod string
  19. section string
  20. tags []string
  21. series []string
  22. link string
  23. slug string
  24. replyLink string
  25. replyTitle string
  26. likeLink string
  27. likeTitle string
  28. syndicate []string
  29. language string
  30. translationKey string
  31. images []Image
  32. audio string
  33. filename string
  34. location string
  35. token string
  36. }
  37. type Image struct {
  38. url string
  39. alt string
  40. }
  41. func CreateEntry(contentType ContentType, r *http.Request) (*Entry, error) {
  42. if contentType == WwwForm {
  43. err := r.ParseForm()
  44. if err != nil {
  45. return nil, errors.New("failed to parse Form")
  46. }
  47. return createEntryFromValueMap(r.Form)
  48. } else if contentType == Multipart {
  49. err := r.ParseMultipartForm(1024 * 1024 * 16)
  50. if err != nil {
  51. return nil, errors.New("failed to parse Multipart")
  52. }
  53. return createEntryFromValueMap(r.MultipartForm.Value)
  54. } else if contentType == Json {
  55. decoder := json.NewDecoder(r.Body)
  56. parsedMfItem := &MicroformatItem{}
  57. err := decoder.Decode(&parsedMfItem)
  58. if err != nil {
  59. return nil, errors.New("failed to parse Json")
  60. }
  61. return createEntryFromMicroformat(parsedMfItem)
  62. } else {
  63. return nil, errors.New("unsupported content-type")
  64. }
  65. }
  66. func createEntryFromValueMap(values map[string][]string) (*Entry, error) {
  67. if h, ok := values["h"]; ok && (len(h) != 1 || h[0] != "entry") {
  68. return nil, errors.New("only entry type is supported so far")
  69. }
  70. entry := &Entry{}
  71. if content, ok := values["content"]; ok {
  72. entry.content = content[0]
  73. }
  74. if name, ok := values["name"]; ok {
  75. entry.title = name[0]
  76. }
  77. if category, ok := values["category"]; ok {
  78. entry.tags = category
  79. } else if categories, ok := values["category[]"]; ok {
  80. entry.tags = categories
  81. }
  82. if slug, ok := values["mp-slug"]; ok && len(slug) > 0 && slug[0] != "" {
  83. entry.slug = slug[0]
  84. }
  85. if inReplyTo, ok := values["in-reply-to"]; ok {
  86. entry.replyLink = inReplyTo[0]
  87. }
  88. if likeOf, ok := values["like-of"]; ok {
  89. entry.likeLink = likeOf[0]
  90. }
  91. if bookmarkOf, ok := values["bookmark-of"]; ok {
  92. entry.link = bookmarkOf[0]
  93. }
  94. if syndicate, ok := values["mp-syndicate-to"]; ok {
  95. entry.syndicate = syndicate
  96. } else if syndicates, ok := values["mp-syndicate-to[]"]; ok {
  97. entry.syndicate = syndicates
  98. }
  99. if photo, ok := values["photo"]; ok {
  100. entry.images = append(entry.images, Image{url: photo[0]})
  101. } else if photos, ok := values["photo[]"]; ok {
  102. for _, photo := range photos {
  103. entry.images = append(entry.images, Image{url: photo})
  104. }
  105. }
  106. if photoAlt, ok := values["mp-photo-alt"]; ok {
  107. if len(entry.images) > 0 {
  108. entry.images[0].alt = photoAlt[0]
  109. }
  110. } else if photoAlts, ok := values["mp-photo-alt[]"]; ok {
  111. for i, photoAlt := range photoAlts {
  112. if len(entry.images) > i {
  113. entry.images[i].alt = photoAlt
  114. }
  115. }
  116. }
  117. if audio, ok := values["audio"]; ok {
  118. entry.audio = audio[0]
  119. } else if audio, ok := values["audio[]"]; ok {
  120. entry.audio = audio[0]
  121. }
  122. if token, ok := values["access_token"]; ok {
  123. entry.token = "Bearer " + token[0]
  124. }
  125. err := computeExtraSettings(entry)
  126. if err != nil {
  127. return nil, err
  128. }
  129. return entry, nil
  130. }
  131. func createEntryFromMicroformat(mfEntry *MicroformatItem) (*Entry, error) {
  132. if len(mfEntry.Type) != 1 || mfEntry.Type[0] != "h-entry" {
  133. return nil, errors.New("only entry type is supported so far")
  134. }
  135. entry := &Entry{}
  136. if mfEntry.Properties != nil && len(mfEntry.Properties.Content) == 1 && len(mfEntry.Properties.Content[0]) > 0 {
  137. entry.content = mfEntry.Properties.Content[0]
  138. }
  139. if len(mfEntry.Properties.Name) == 1 {
  140. entry.title = mfEntry.Properties.Name[0]
  141. }
  142. if len(mfEntry.Properties.Category) > 0 {
  143. entry.tags = mfEntry.Properties.Category
  144. }
  145. if len(mfEntry.Properties.MpSlug) == 1 && len(mfEntry.Properties.MpSlug[0]) > 0 {
  146. entry.slug = mfEntry.Properties.MpSlug[0]
  147. }
  148. if len(mfEntry.Properties.InReplyTo) == 1 {
  149. entry.replyLink = mfEntry.Properties.InReplyTo[0]
  150. }
  151. if len(mfEntry.Properties.LikeOf) == 1 {
  152. entry.likeLink = mfEntry.Properties.LikeOf[0]
  153. }
  154. if len(mfEntry.Properties.BookmarkOf) == 1 {
  155. entry.link = mfEntry.Properties.BookmarkOf[0]
  156. }
  157. if len(mfEntry.Properties.MpSyndicateTo) > 0 {
  158. entry.syndicate = mfEntry.Properties.MpSyndicateTo
  159. }
  160. if len(mfEntry.Properties.Photo) > 0 {
  161. for _, photo := range mfEntry.Properties.Photo {
  162. if theString, justString := photo.(string); justString {
  163. entry.images = append(entry.images, Image{url: theString})
  164. } else if thePhoto, isPhoto := photo.(map[string]interface{}); isPhoto {
  165. image := Image{}
  166. // Micropub spec says "value" is correct, but not sure about that
  167. if photoUrl, ok := thePhoto["value"].(string); ok {
  168. image.url = photoUrl
  169. }
  170. if alt, ok := thePhoto["alt"].(string); ok {
  171. image.alt = alt
  172. }
  173. entry.images = append(entry.images, image)
  174. }
  175. }
  176. }
  177. if len(mfEntry.Properties.Audio) > 0 {
  178. entry.audio = mfEntry.Properties.Audio[0]
  179. }
  180. err := computeExtraSettings(entry)
  181. if err != nil {
  182. return nil, err
  183. }
  184. return entry, nil
  185. }
  186. func computeExtraSettings(entry *Entry) error {
  187. now := time.Now()
  188. // Set date
  189. entry.date = now.Format(time.RFC3339)
  190. // Find settings hidden in content
  191. var filteredContent bytes.Buffer
  192. contentScanner := bufio.NewScanner(strings.NewReader(entry.content))
  193. for contentScanner.Scan() {
  194. text := contentScanner.Text()
  195. if strings.HasPrefix(text, "section: ") {
  196. // Section
  197. entry.section = strings.TrimPrefix(text, "section: ")
  198. } else if strings.HasPrefix(text, "title: ") {
  199. // Title
  200. entry.title = strings.TrimPrefix(text, "title: ")
  201. } else if strings.HasPrefix(text, "slug: ") {
  202. // Slug
  203. entry.slug = strings.TrimPrefix(text, "slug: ")
  204. } else if strings.HasPrefix(text, "tags: ") {
  205. // Tags
  206. entry.tags = strings.Split(strings.TrimPrefix(text, "tags: "), ",")
  207. } else if strings.HasPrefix(text, "series: ") {
  208. // Series
  209. entry.series = strings.Split(strings.TrimPrefix(text, "series: "), ",")
  210. } else if strings.HasPrefix(text, "link: ") {
  211. // Link
  212. entry.link = strings.TrimPrefix(text, "link: ")
  213. } else if strings.HasPrefix(text, "reply-link: ") {
  214. // Reply link
  215. entry.replyLink = strings.TrimPrefix(text, "reply-link: ")
  216. } else if strings.HasPrefix(text, "reply-title: ") {
  217. // Reply title
  218. entry.replyTitle = strings.TrimPrefix(text, "reply-title: ")
  219. } else if strings.HasPrefix(text, "like-link: ") {
  220. // Like link
  221. entry.likeLink = strings.TrimPrefix(text, "like-link: ")
  222. } else if strings.HasPrefix(text, "like-title: ") {
  223. // Like title
  224. entry.likeTitle = strings.TrimPrefix(text, "like-title: ")
  225. } else if strings.HasPrefix(text, "language: ") {
  226. // Language
  227. entry.language = strings.TrimPrefix(text, "language: ")
  228. } else if strings.HasPrefix(text, "translationkey: ") {
  229. // Translation key
  230. entry.translationKey = strings.TrimPrefix(text, "translationkey: ")
  231. } else if strings.HasPrefix(text, "images: ") {
  232. // Images
  233. for _, image := range strings.Split(strings.TrimPrefix(text, "images: "), ",") {
  234. entry.images = append(entry.images, Image{url: image})
  235. }
  236. } else if strings.HasPrefix(text, "image-alts: ") {
  237. // Image alt
  238. for i, alt := range strings.Split(strings.TrimPrefix(text, "image-alts: "), ",") {
  239. entry.images[i].alt = alt
  240. }
  241. } else if strings.HasPrefix(text, "audio: ") {
  242. // Audio
  243. entry.audio = strings.TrimPrefix(text, "audio: ")
  244. } else {
  245. _, _ = fmt.Fprintln(&filteredContent, text)
  246. }
  247. }
  248. entry.content = filteredContent.String()
  249. // Check if content contains images or add them
  250. for _, image := range entry.images {
  251. if !strings.Contains(entry.content, image.url) {
  252. if len(image.alt) > 0 {
  253. entry.content += "\n![" + image.alt + "](" + image.url + " \"" + image.alt + "\")\n"
  254. } else {
  255. entry.content += "\n![](" + image.url + ")\n"
  256. }
  257. }
  258. }
  259. // Compute slug if empty
  260. if len(entry.slug) == 0 || entry.slug == "" {
  261. random := generateRandomString(now, 5)
  262. entry.slug = fmt.Sprintf("%v-%02d-%02d-%v", now.Year(), int(now.Month()), now.Day(), random)
  263. }
  264. // Set language
  265. if len(entry.language) == 0 {
  266. entry.language = DefaultLanguage
  267. }
  268. // Compute filename and location
  269. lang := Languages[entry.language]
  270. contentFolder := lang.ContentDir
  271. localizedBlogUrl := BlogUrl
  272. if len(lang.BlogUrl) != 0 {
  273. localizedBlogUrl = lang.BlogUrl
  274. }
  275. if len(entry.section) == 0 {
  276. entry.section = lang.DefaultSection
  277. }
  278. entry.section = strings.ToLower(entry.section)
  279. section := lang.Sections[entry.section]
  280. pathVars := struct {
  281. LocalContentFolder string
  282. LocalBlogUrl string
  283. Year int
  284. Month int
  285. Slug string
  286. Section string
  287. }{
  288. LocalContentFolder: contentFolder,
  289. LocalBlogUrl: localizedBlogUrl,
  290. Year: now.Year(),
  291. Month: int(now.Month()),
  292. Slug: entry.slug,
  293. Section: entry.section,
  294. }
  295. filenameTmpl, err := template.New("filename").Parse(section.FilenameTemplate)
  296. if err != nil {
  297. return errors.New("failed to parse filename template")
  298. }
  299. filename := new(bytes.Buffer)
  300. err = filenameTmpl.Execute(filename, pathVars)
  301. if err != nil {
  302. return errors.New("failed to execute filename template")
  303. }
  304. entry.filename = filename.String()
  305. locationTmpl, err := template.New("location").Parse(section.LocationTemplate)
  306. if err != nil {
  307. return errors.New("failed to parse location template")
  308. }
  309. location := new(bytes.Buffer)
  310. err = locationTmpl.Execute(location, pathVars)
  311. if err != nil {
  312. return errors.New("failed to execute location template")
  313. }
  314. entry.location = location.String()
  315. return nil
  316. }
  317. func generateRandomString(now time.Time, n int) string {
  318. rand.Seed(now.UnixNano())
  319. letters := []rune("abcdefghijklmnopqrstuvwxyz")
  320. b := make([]rune, n)
  321. for i := range b {
  322. b[i] = letters[rand.Intn(len(letters))]
  323. }
  324. return string(b)
  325. }
  326. func WriteEntry(entry *Entry) (location string, err error) {
  327. file, err := WriteHugoPost(entry)
  328. if err != nil {
  329. return
  330. }
  331. err = SelectedStorage.CreateFile(entry.filename, file, entry.title)
  332. if err != nil {
  333. return
  334. }
  335. location = entry.location
  336. return
  337. }