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.

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