package lists import ( "encoding/json" "errors" "fmt" "io/ioutil" "log" "net/http" "net/url" "sort" "strconv" "fagott.pw/charakterin" "fagott.pw/grilist/cache" "fagott.pw/grilist/eventlogging" "fagott.pw/grilist/frontend" "fagott.pw/grilist/grilist" "fagott.pw/grilist/models" "fagott.pw/grilist/modules/grils" "fagott.pw/grilist/util" "github.com/julienschmidt/httprouter" ) // Module und so. type Module struct { g *grilist.Grilist c *cache.Cache grils *grils.GrilsModule } // ListGrils ist die Sort-Interface Implementation für Grils einer Liste. type ListGrils []*models.ListGril func (l ListGrils) Len() int { return len(l) } func (l ListGrils) Swap(i, j int) { l[i], l[j] = l[j], l[i] } func (l ListGrils) Less(i, j int) bool { return l[i].Order < l[j].Order } // Name gibt den Namen des Moduls zurück func (m *Module) Name() string { return "Lists" } // Init initialisiert das Modul func (m *Module) Init(g *grilist.Grilist) { m.g = g gm, ok := g.Modules["Grils"] if !ok { log.Fatal("lists: grils module not found") } grilsModule, ok := gm.(*grils.GrilsModule) if !ok { log.Fatal("lists: error with grils module") } m.grils = grilsModule m.g.Router.GET("/list/:id", m.viewList) m.g.Router.POST("/list/:id/order", m.updateGrilOrder) m.g.Router.POST("/list/:id", m.addGrilToList) m.g.Router.DELETE("/list/:id/order", m.removeGrilFromList) m.g.Router.GET("/list/:id/settings", m.viewListSettings) m.g.Router.POST("/list/:id/settings", m.updateListSettings) m.g.Router.GET("/new/list", m.displayCreateList) m.g.Router.POST("/new/list", m.createList) m.g.Router.GET("/list/:id/delete", m.deleteList) m.g.Router.GET("/api/lists/user", m.APIgetUserLists) m.c = cache.New() } func (m *Module) getListGrils(list *models.List) error { rows, err := m.g.DB.Query(`SELECT gril_id, "order" FROM grilist.lists_grils WHERE list_id = $1 ORDER BY "order" ASC`, list.ID) if err != nil { return err } defer rows.Close() list.Grils = list.Grils[:0] for rows.Next() { var grilID int lg := &models.ListGril{} if err := rows.Scan(&grilID, &lg.Order); err != nil { log.Println("error scanning row in getListGrils:", err) continue } gril, err := m.grils.FromID(grilID) if err != nil { log.Println("error getting listGril:", err) continue } lg.Gril = gril list.Grils = append(list.Grils, lg) } sort.Sort(ListGrils(list.Grils)) return nil } func (m *Module) getLists(whereClause string, params ...interface{}) ([]*models.List, error) { var lists []*models.List rows, err := m.g.DB.Query(fmt.Sprintf(`SELECT id, name, description, fork_of, updated_at, user_id FROM grilist.lists WHERE %s`, whereClause), params...) if err != nil { return nil, err } defer rows.Close() for rows.Next() { list := &models.List{} var ownerID int if err := rows.Scan(&list.ID, &list.Name, &list.Description, &list.ForkOf, &list.UpdatedAt, &ownerID); err != nil { log.Println("error scanning row in getLists:", err) continue } // Owner kriegn owner, err := m.g.Charakterin.GetUserByID(ownerID) if err != nil { log.Println("error retreiving owner of list", err) continue } list.Owner = owner m.c.Insert(list.ID, list) lists = append(lists, list) } return lists, nil } // GetUserLists gibt die Listen eines Benutzers zurück. func (m *Module) GetUserLists(u *charakterin.User, withGrils bool) []*models.List { lists, err := m.getLists(`user_id = $1`, u.ID) if err != nil { log.Println(err) } if withGrils { for _, list := range lists { if err := m.getListGrils(list); err != nil { log.Println(err) } } } return lists } // FromID sucht nach der Liste mit der gegebenen ID und gibt sie, falls sie existiert, zurück. func (m *Module) FromID(id int, withGrils bool) (*models.List, error) { if lst, ok := m.c.Get(id); ok { l := lst.(*models.List) // einen neuen User holen, wir wollen ja sicher gehen und so. if owner, err := m.g.Charakterin.GetUserByID(l.Owner.ID); err == nil { l.Owner = owner } // Potenzieller Optimierungsbedarf: neue query, wenn die Liste leer ist. updateGrilOrder nutzt derzeit aber genau dieses verhalten. if withGrils && len(l.Grils) == 0 { if err := m.getListGrils(l); err != nil { return nil, err } } return l, nil } lists, err := m.getLists(`id = $1`, id) if err != nil { return nil, err } if withGrils { for _, list := range lists { if err := m.getListGrils(list); err != nil { return nil, err } } } if len(lists) == 0 { return nil, errors.New("no list found") } return lists[0], nil } func ListsToCards(lists []*models.List) []frontend.Card { var cards []frontend.Card for _, list := range lists { cards = append(cards, frontend.Card{ Title: list.Name, Description: list.Description, Size: "medium", Actions: []frontend.Action{ frontend.Action{ Name: "anguckieren", Link: fmt.Sprintf("/list/%d", list.ID), }, frontend.Action{ Name: fmt.Sprintf(`von %s`, list.Owner.ID, list.Owner.GetName(), list.Owner.GetName()), Link: "#", Disabled: true, }, }, }) } return cards } // ProvideDashboardData gibt Daten für das Dashboard bezogen auf den Benutzer zurück func (m *Module) ProvideDashboardData(user *charakterin.User) []grilist.DashboardCategory { var categories []grilist.DashboardCategory // Neue Listen lists, err := m.getLists(`1=1 ORDER BY id DESC LIMIT 5`) if err != nil { log.Println(err) return categories } categories = append(categories, grilist.DashboardCategory{ Title: "Neueste Listen", Cards: ListsToCards(lists), }) if user == nil { return categories } // Listen des Benutzers lists = m.GetUserLists(user, false) categories = append(categories, grilist.DashboardCategory{ Title: "Meine Listen", Cards: ListsToCards(lists), }) return categories } func (m *Module) viewList(w http.ResponseWriter, r *http.Request, p httprouter.Params) { user, _ := m.g.Charakterin.GetUserFromRequest(r) el := m.g.EventLogger(r) id, err := util.ParseIDFromParams(p) if err != nil { log.Println("redir") http.Redirect(w, r, "/", 302) return } list, err := m.FromID(id, true) if err != nil { log.Println("redir") http.Redirect(w, r, "/", 302) return } data := m.g.Renderer.DefaultData() data["user"] = user data["list"] = list if user != nil && user.ID == list.Owner.ID { data["isOwner"] = true } else { data["isOwner"] = false } m.g.Renderer.RenderPage("list", w, data) el.ViewList(user, eventlogging.ViewListData{ ListID: list.ID, }) } func (m *Module) deleteList(w http.ResponseWriter, r *http.Request, p httprouter.Params) { el := m.g.EventLogger(r) user, err := m.g.Charakterin.GetUserFromRequest(r) if err != nil { log.Println("invalid deleteList user") http.Redirect(w, r, fmt.Sprintf("/list/%s", p.ByName("id")), 403) return } id, err := util.ParseIDFromParams(p) if err != nil { log.Println("invalid deleteList ID") http.Redirect(w, r, "/", 400) return } list, err := m.FromID(id, false) if err != nil { log.Println("invalid deleteList ID (could not get list)") http.Redirect(w, r, fmt.Sprintf("/list/%d", id), 500) return } // checken, ob der user auch list owner ist if user.ID != list.Owner.ID { log.Println("invalid deleteList request: user not list owner") http.Redirect(w, r, fmt.Sprintf("/list/%d", id), 403) return } // aus der Datenbank loeschen _, err = m.g.DB.Query(`DELETE FROM grilist.lists WHERE id = $1`, id) if err != nil { log.Println("could not delete list:", err) data := m.g.Renderer.DefaultData() data["user"] = user data["list"] = list data["error"] = "Liste konnte nicht geloescht werden." m.g.Renderer.RenderPage("list_settings", w, data) return } // aus dem Cache loeschen m.c.Remove(list.ID) log.Printf("list %d has been deleted by the owner %d(%s)", list.ID, list.Owner.ID, list.Owner.GetName()) http.Redirect(w, r, "/", 302) el.DeleteList(user, eventlogging.DeleteListData{ ListID: list.ID, }) } func (m *Module) viewListSettings(w http.ResponseWriter, r *http.Request, p httprouter.Params) { user, err := m.g.Charakterin.GetUserFromRequest(r) if err != nil { log.Println("invalid viewListSettings user") http.Redirect(w, r, fmt.Sprintf("/list/%s", p.ByName("id")), 403) return } id, err := strconv.Atoi(p.ByName("id")) if err != nil { log.Println("invalid viewListSettings ID") http.Redirect(w, r, "/", 400) return } list, err := m.FromID(id, false) if err != nil { log.Println("invalid viewListSettings ID (could not get list)") http.Redirect(w, r, fmt.Sprintf("/list/%d", id), 500) return } // checken, ob der user auch list owner ist if user.ID != list.Owner.ID { log.Println("invalid viewListSettings request: user not list owner") http.Redirect(w, r, fmt.Sprintf("/list/%d", id), 403) return } data := m.g.Renderer.DefaultData() data["user"] = user data["list"] = list m.g.Renderer.RenderPage("list_settings", w, data) } func (m *Module) updateListSettings(w http.ResponseWriter, r *http.Request, p httprouter.Params) { el := m.g.EventLogger(r) user, err := m.g.Charakterin.GetUserFromRequest(r) if err != nil { log.Println("invalid updateListSettings user") http.Redirect(w, r, fmt.Sprintf("/list/%s", p.ByName("id")), 403) return } id, err := util.ParseIDFromParams(p) if err != nil { log.Println("invalid updateListSettings ID") http.Redirect(w, r, "/", 400) return } list, err := m.FromID(id, false) if err != nil { log.Println("invalid updateListSettings ID (could not get list)") http.Redirect(w, r, fmt.Sprintf("/list/%d", id), 500) return } // checken, ob der user auch list owner ist if user.ID != list.Owner.ID { log.Println("invalid updateListSettings request: user not list owner") http.Redirect(w, r, fmt.Sprintf("/list/%d", id), 403) return } renderWithError := func(message string, nameError, descriptionError bool) { data := m.g.Renderer.DefaultData() data["user"] = user data["list"] = list data["error"] = message data["error_name"] = nameError data["error_description"] = descriptionError m.g.Renderer.RenderPage("list_settings", w, data) } values, err := readBody(r) if err != nil { log.Println("invalid POST data in updateListSettings") renderWithError("invalid post data", false, false) return } name := values.Get("name") if len(name) < 3 { renderWithError("Name zu kurz", true, false) return } else if len(name) > 32 { renderWithError("Name zu lang", true, false) return } description := values.Get("description") if len(description) < 3 { renderWithError("Beschreibung zu kurz", false, true) return } else if len(description) > 100 { renderWithError("Beschreibung zu lang", false, true) return } var oldName string var oldDescription string err = m.g.DB.QueryRow("SELECT name, description FROM grilist.lists WHERE id = $1;", id).Scan(&oldName, &oldDescription) if err != nil { log.Println("could not get list info", id) renderWithError("interner fehler", false, false) return } if list.Name != name && list.Description != description { _, err = m.g.DB.Query(`UPDATE grilist.lists SET name = $2, description = $3 WHERE id = $1`, id, name, description) if err != nil { log.Println("could not update list:", err) renderWithError("interner fehler", false, false) return } } else if list.Name != name || list.Description != description { field := "name" value := name if list.Description != description { field = "description" value = description } _, err = m.g.DB.Query(fmt.Sprintf(`UPDATE grilist.lists SET %s = $2 WHERE id = $1`, field), id, value) if err != nil { log.Println("could not update list:", err) renderWithError("interner fehler", false, false) return } } list.Name = name list.Description = description data := m.g.Renderer.DefaultData() data["user"] = user data["list"] = list data["notification"] = frontend.NewNotification("Aenderungen gespeichert") m.g.Renderer.RenderPage("list_settings", w, data) el.EditList(user, eventlogging.EditListData{ ListID: id, OldName: oldName, OldDescription: oldDescription, NewName: name, NewDescription: description, }) } func (m *Module) addGrilToList(w http.ResponseWriter, r *http.Request, p httprouter.Params) { el := m.g.EventLogger(r) user, err := m.g.Charakterin.GetUserFromRequest(r) if err != nil { http.Error(w, "403", http.StatusForbidden) return } listID, err := util.ParseIDFromParams(p) if err != nil { log.Println("invalid list id") return } values, err := readBody(r) if err != nil { log.Println("invalid POST data") return } grilID, err := strconv.Atoi(values.Get("id")) if err != nil { log.Println("invalid gril id") return } list, err := m.FromID(listID, true) if err != nil { http.Error(w, "invalid list", 404) return } if list.Owner.ID != user.ID { http.Error(w, "403", http.StatusForbidden) return } rank := 0 if len(list.Grils) > 0 { rank = list.Grils[len(list.Grils)-1].Order + 1 } // rein in die DB damit _, err = m.g.DB.Query(`INSERT INTO grilist.lists_grils(list_id, gril_id, "order") VALUES($1, $2, $3)`, listID, grilID, rank) if err != nil { log.Println("error inserting gril into list:", err) http.Error(w, "could not insert gril", 500) return } gril, err := m.grils.FromID(grilID) if err != nil { log.Println("inserted gril into list but couldnt get gril afterwards:", err) http.Error(w, "error after insert", 500) return } lg := &models.ListGril{gril, rank} data := m.g.Renderer.DefaultData() data["Index"] = len(list.Grils) value := make(map[string]interface{}) value["IsListOwner"] = true value["Gril"] = lg data["Value"] = value list.Grils = append(list.Grils, lg) m.g.Renderer.RenderPage("list_gril", w, data) el.AddGrilToList(user, eventlogging.AddGrilToListData{ ListID: list.ID, GrilID: gril.ID, }) } func (m *Module) displayCreateList(w http.ResponseWriter, r *http.Request, p httprouter.Params) { user, err := m.g.Charakterin.GetUserFromRequest(r) if err != nil { http.Redirect(w, r, "/", 302) return } data := m.g.Renderer.DefaultData() data["user"] = user m.g.Renderer.RenderPage("create_list", w, data) } func (m *Module) createList(w http.ResponseWriter, r *http.Request, p httprouter.Params) { el := m.g.EventLogger(r) user, err := m.g.Charakterin.GetUserFromRequest(r) if err != nil { log.Println(err) http.Error(w, "500", http.StatusInternalServerError) return } values, err := readBody(r) if err != nil { log.Println(err) http.Error(w, "500", http.StatusInternalServerError) return } var id int err = m.g.DB.QueryRow(`INSERT INTO grilist.lists(user_id, name, description) VALUES($1, $2, $3) RETURNING id`, user.ID, values.Get("name"), values.Get("description")).Scan(&id) if err != nil { log.Println(err) http.Error(w, "500", http.StatusInternalServerError) return } http.Redirect(w, r, fmt.Sprintf("/list/%d", id), 302) el.CreateList(user, eventlogging.CreateListData{ ListID: id, Name: values.Get("name"), Description: values.Get("description"), }) } func (m *Module) updateGrilOrder(w http.ResponseWriter, r *http.Request, p httprouter.Params) { el := m.g.EventLogger(r) user, err := m.g.Charakterin.GetUserFromRequest(r) if err != nil { http.Error(w, "403", http.StatusForbidden) return } listID, err := util.ParseIDFromParams(p) if err != nil { http.Error(w, "invalid list ID (type mismatch)", 400) return } values, err := readBody(r) if err != nil { http.Error(w, "invalid POST data", 400) return } grilID, err := strconv.Atoi(values.Get("gril")) if err != nil { http.Error(w, "invalid gril ID", 404) return } pos, err := strconv.Atoi(values.Get("pos")) if err != nil { http.Error(w, "invalid position", 400) return } var oldOrder int err = m.g.DB.QueryRow( `SELECT "order" FROM grilist.lists_grils WHERE list_id = $1 AND gril_id = $2;`, listID, grilID).Scan(&oldOrder) if err != nil { log.Println(err) http.Error(w, "Internal Server Error", 500) return } // rein in die DB damit _, err = m.g.DB.Exec(`SELECT grilist.set_gril_order($1, $2, $3, $4)`, user.ID, listID, grilID, pos) if err != nil { log.Println("error reordering gril:", err) http.Error(w, "could not update gril order", 500) return } // wenn die liste im cache ist, die Grils clearen, damit beim naechsten aufruf die Gril-Liste neu geholt wird. if l, ok := m.c.Get(listID); ok { ls := l.(*models.List) ls.Grils = ls.Grils[:0] } w.WriteHeader(200) w.Write([]byte("ok")) el.ChangeGrilOrder(user, eventlogging.ChangeGrilOrderData{ ListID: listID, GrilID: grilID, OldOrder: oldOrder, NewOrder: pos, }) } func (m *Module) removeGrilFromList(w http.ResponseWriter, r *http.Request, p httprouter.Params) { el := m.g.EventLogger(r) var oldOrder int user, err := m.g.Charakterin.GetUserFromRequest(r) if err != nil { http.Error(w, "Unauthorized", 401) return } listID, err := util.ParseIDFromParams(p) if err != nil { http.Error(w, "invalid list ID (type mismatch)", 400) return } list, err := m.FromID(listID, false) if err != nil { http.Error(w, "list not found", 404) return } values, err := readBody(r) if err != nil { http.Error(w, "invalid POST data", 400) return } grilID, err := strconv.Atoi(values.Get("gril")) if err != nil { http.Error(w, "invalid gril ID", 404) return } err = m.g.DB.QueryRow(`DELETE FROM grilist.lists_grils WHERE list_id=$1 AND gril_id=$2 RETURNING "order"`, listID, grilID).Scan(&oldOrder) if err != nil { log.Println("error removing gril:", err) http.Error(w, "could not remove gril", 500) return } _, err = m.g.DB.Exec(`UPDATE grilist.lists_grils SET "order" = "order" - 1 WHERE list_id = $1 AND "order" > $2`, listID, oldOrder) if err != nil { log.Println("error removing gril:", err) http.Error(w, "could not remove gril", 500) return } list.Grils = list.Grils[:0] w.WriteHeader(200) w.Write([]byte("ok")) el.DeleteGrilFromList(user, eventlogging.DeleteGrilFromListData{ ListID: listID, GrilID: grilID, }) } func (m *Module) APIgetUserLists(w http.ResponseWriter, r *http.Request, p httprouter.Params) { user, err := m.g.Charakterin.GetUserFromRequest(r) if err != nil { http.Error(w, "403", http.StatusForbidden) return } lists := m.GetUserLists(user, false) var pubLists []models.List for _, list := range lists { // owner wegen SICHERHEIT rausfiltern (nodumppassword2k16) // list.Owner = nil var tempList models.List tempList.Description = list.Description tempList.ForkOf = list.ForkOf tempList.ID = list.ID tempList.Name = list.Name tempList.UpdatedAt = list.UpdatedAt pubLists = append(pubLists, tempList) } data, err := json.Marshal(&pubLists) if err != nil { log.Println(err) http.Error(w, "500", http.StatusInternalServerError) return } w.WriteHeader(200) w.Write(data) } // New erstellt eine neue Instanz des Modules func New() *Module { return &Module{} } func readBody(r *http.Request) (url.Values, error) { defer r.Body.Close() data, err := ioutil.ReadAll(r.Body) if err != nil { return nil, err } values, err := url.ParseQuery(string(data)) if err != nil { return nil, err } return values, nil }