The backend for the AiteStory website
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.

339 lines
8.3 KiB

7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
  1. package model
  2. import (
  3. "database/sql"
  4. "errors"
  5. "fmt"
  6. "net/url"
  7. "time"
  8. "git.aiterp.net/AiteRP/aitestory/formparser"
  9. "git.aiterp.net/AiteRP/aitestory/server"
  10. "git.aiterp.net/gisle/wrouter/generate"
  11. "github.com/microcosm-cc/bluemonday"
  12. "github.com/russross/blackfriday"
  13. )
  14. // PageTypes describes how the source is rendered. For now it's only markdown,
  15. // but who knows what the future holds.
  16. var PageTypes = []string{
  17. "Markdown",
  18. }
  19. // PageMinDate is the earliest date possible. Stories from Matriarch Eriana's childhood
  20. // are thus not going to happen.
  21. var PageMinDate, _ = time.Parse(time.RFC3339, "1753-01-01T00:00:00Z")
  22. // Page is the model describing the individual articles posted
  23. // by users.
  24. type Page struct {
  25. ID string `json:"id"`
  26. Name string `json:"name"`
  27. Author string `json:"author"`
  28. Category string `json:"category"`
  29. FictionalDate time.Time `json:"fictionalDate"`
  30. PublishDate time.Time `json:"publishDate"`
  31. EditDate time.Time `json:"editDate"`
  32. Dated bool `json:"dated"`
  33. Published bool `json:"published"`
  34. Unlisted bool `json:"unlisted"`
  35. Specific bool `json:"specific"`
  36. Indexed bool `json:"indexed"`
  37. BackgroundURL string `json:"backgroundUrl"`
  38. Type string `json:"type"`
  39. Source string `json:"source"`
  40. Tags []Tag `json:"tags"`
  41. prevTags []Tag
  42. cachedOutput string
  43. }
  44. // Defaults fills in the default details for a page, suited for populating a form
  45. func (page *Page) Defaults() {
  46. page.Category = PageCategories[0].Key
  47. page.Dated = true
  48. page.Published = true
  49. page.Unlisted = false
  50. page.Specific = false
  51. page.Indexed = true
  52. page.BackgroundURL = ""
  53. page.Type = PageTypes[0]
  54. page.Source = ""
  55. }
  56. // Insert adds the page to the database
  57. func (page *Page) Insert() error {
  58. const insertPage = `
  59. INSERT INTO page (
  60. id, name, author, category, fictional_date,
  61. publish_date, edit_date, dated, published,
  62. unlisted, page.specific, indexed, type, source
  63. ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
  64. `
  65. const insertTag = `INSERT INTO page_tag (page_id,tag_id,page_tag.primary) VALUES (?, ?, ?)`
  66. db := server.Main.DB
  67. if page.ID == "" {
  68. page.generateID()
  69. }
  70. // Do the thing
  71. _, err := db.Exec(insertPage,
  72. page.ID, page.Name, page.Author, page.Category, page.FictionalDate, page.PublishDate,
  73. page.EditDate, page.Dated, page.Published, page.Unlisted, page.Specific, page.Indexed,
  74. page.Type, page.Source,
  75. )
  76. if err != nil {
  77. return err
  78. }
  79. // Insert tags
  80. for i, tag := range page.Tags {
  81. _, err := db.Exec(insertTag, page.ID, tag.ID, i == 0)
  82. if err != nil {
  83. page.Delete()
  84. return err
  85. }
  86. }
  87. return nil
  88. }
  89. // Update saves the page to the database
  90. func (page *Page) Update() error {
  91. const updatePage = `
  92. UPDATE page SET
  93. name=?,category=?,fictional_date=?,publish_date=?,
  94. edit_date=?,dated=?,published=?,unlisted=?,page.specific=?,
  95. indexed=?,type=?,source=?
  96. WHERE id=?
  97. `
  98. const clearTags = `DELETE FROM page_tag WHERE page_id=?`
  99. const insertTag = `INSERT INTO page_tag (page_id,tag_id,page_tag.primary) VALUES (?, ?, ?)`
  100. db := server.Main.DB
  101. if page.ID == "" {
  102. return errors.New("no id")
  103. }
  104. // Do the thing
  105. _, err := db.Exec(updatePage,
  106. page.Name, page.Category, page.FictionalDate, page.PublishDate,
  107. page.EditDate, page.Dated, page.Published, page.Unlisted, page.Specific, page.Indexed,
  108. page.Type, page.Source, page.ID,
  109. )
  110. if err != nil {
  111. return err
  112. }
  113. // Stop now if the tages haven't changed
  114. if len(page.prevTags) == len(page.Tags) {
  115. change := false
  116. for i, tag := range page.prevTags {
  117. if tag.ID != page.prevTags[i].ID {
  118. change = true
  119. break
  120. }
  121. }
  122. if !change {
  123. return nil
  124. }
  125. }
  126. // Re-tag (can be optimized if need arise)
  127. _, err = db.Exec(clearTags, page.ID)
  128. if err != nil {
  129. return err
  130. }
  131. for i, tag := range page.Tags {
  132. _, err := db.Exec(insertTag, page.ID, tag.ID, i == 0)
  133. if err != nil {
  134. return err
  135. }
  136. }
  137. return nil
  138. }
  139. // Delete removes the page from the database
  140. func (page *Page) Delete() error {
  141. db := server.Main.DB
  142. // Do the thing
  143. results, err := db.Exec("DELETE FROM `page` WHERE id=? LIMIT 1", page.ID)
  144. if err != nil {
  145. return err
  146. }
  147. // Count the stuffs that were done things to
  148. affected, err := results.RowsAffected()
  149. if err != nil {
  150. return err
  151. }
  152. if affected == 0 {
  153. return errors.New("page not found")
  154. }
  155. return nil
  156. }
  157. // Content parses the content of the page
  158. func (page *Page) Content() (string, error) {
  159. if page.cachedOutput != "" {
  160. return page.cachedOutput, nil
  161. }
  162. if page.Type == "Markdown" {
  163. // TODO: Convert [[Ehanis Tioran]] to [Ehanis Tioran](https://wiki.aiterp.net/index.php?title=Ehanis%20Tioran)
  164. unsafe := blackfriday.MarkdownCommon([]byte(page.Source))
  165. page.cachedOutput = string(bluemonday.UGCPolicy().SanitizeBytes(unsafe))
  166. return page.cachedOutput, nil
  167. }
  168. return "", fmt.Errorf("Page type '%s' is not supported", page.Type)
  169. }
  170. // ParseForm validates the values in a form and sets the page's values whenever possible regardless
  171. // so that it can be pushed to the viewmodel to allow the user to correct their mistakes without fear
  172. // of losing their hard work
  173. func (page *Page) ParseForm(form url.Values) []error {
  174. errors := make([]error, 0, 4)
  175. page.cachedOutput = ""
  176. err := formparser.String(form.Get("name"), &page.Name, 2, 192)
  177. if err != nil {
  178. errors = append(errors, fmt.Errorf("Name: %s", err))
  179. }
  180. err = formparser.Select(form.Get("category"), &page.Category, pageCategories, page.Category != "")
  181. if err != nil {
  182. errors = append(errors, fmt.Errorf("Category: %s", err))
  183. }
  184. err = formparser.Date(form.Get("fictionalDate"), &page.FictionalDate, !page.FictionalDate.IsZero())
  185. if err != nil {
  186. errors = append(errors, fmt.Errorf("Fictonal Date: %s", err))
  187. }
  188. page.Dated = form.Get("dated") != ""
  189. page.Published = form.Get("published") != ""
  190. page.Unlisted = form.Get("unlisted") != ""
  191. page.Specific = form.Get("specific") != ""
  192. page.Indexed = form.Get("indexed") != ""
  193. err = formparser.String(form.Get("backgroundUrl"), &page.BackgroundURL, 0, 255)
  194. if err != nil {
  195. errors = append(errors, fmt.Errorf("Background URL: %s", err))
  196. }
  197. err = formparser.Select(form.Get("type"), &page.Type, PageTypes, page.Type != "")
  198. if err != nil {
  199. errors = append(errors, fmt.Errorf("Type: %s", err))
  200. }
  201. err = formparser.String(form.Get("source"), &page.Source, 0, 102400)
  202. if err != nil {
  203. errors = append(errors, fmt.Errorf("Content is too long, max: 100 KB (~16k words)"))
  204. }
  205. if len(errors) == 0 {
  206. errors = nil
  207. }
  208. return errors
  209. }
  210. // Standardize page ID generation
  211. func (page *Page) generateID() {
  212. page.ID = generate.FriendlyID(16)
  213. }
  214. // FindPage finds a page by ID. The Header model handles
  215. // listning pages
  216. func FindPage(id string) (*Page, error) {
  217. const selectPage = `
  218. SELECT id,name,author,category,fictional_date,publish_date,edit_date,dated,published,
  219. unlisted,page.specific,indexed,type,source,background_url
  220. FROM page
  221. WHERE id=?
  222. `
  223. const selectPageTags = `
  224. SELECT tag.id,tag.type,tag.name
  225. FROM page_tag
  226. RIGHT JOIN tag ON (tag.id = page_tag.tag_id)
  227. WHERE page_tag.page_id = ?
  228. `
  229. db := server.Main.DB
  230. rows, err := db.Query(selectPage, id)
  231. if err != nil {
  232. return nil, err
  233. }
  234. defer rows.Close()
  235. if !rows.Next() {
  236. return nil, errors.New("not found")
  237. }
  238. page := new(Page)
  239. err = parsePage(page, rows)
  240. if err != nil {
  241. return nil, err
  242. }
  243. rows, err = db.Query(selectPageTags, page.ID)
  244. if err != nil {
  245. return nil, err
  246. }
  247. page.Tags = make([]Tag, 0, 64)
  248. for rows.Next() {
  249. tag := Tag{}
  250. rows.Scan(&tag.ID, &tag.Type, &tag.Name)
  251. page.Tags = append(page.Tags, tag)
  252. }
  253. return page, nil
  254. }
  255. func parsePage(page *Page, rows *sql.Rows) error {
  256. var fictionalDate, publishDate, editDate string
  257. var bgURL *string
  258. err := rows.Scan(
  259. &page.ID, &page.Name, &page.Author, &page.Category, &fictionalDate,
  260. &publishDate, &editDate, &page.Dated, &page.Published, &page.Unlisted,
  261. &page.Specific, &page.Indexed, &page.Type, &page.Source, &bgURL,
  262. )
  263. if err != nil {
  264. return err
  265. }
  266. if bgURL != nil {
  267. page.BackgroundURL = *bgURL
  268. }
  269. page.FictionalDate, err = time.Parse("2006-01-02 15:04:05", fictionalDate)
  270. if err != nil {
  271. return err
  272. }
  273. page.PublishDate, err = time.Parse("2006-01-02 15:04:05", publishDate)
  274. if err != nil {
  275. return err
  276. }
  277. page.EditDate, err = time.Parse("2006-01-02 15:04:05", editDate)
  278. if err != nil {
  279. return err
  280. }
  281. return nil
  282. }