1// SPDX-FileCopyrightText: 2022 Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: BSD-2-Clause
4
5package main
6
7import (
8 "errors"
9 "fmt"
10 "log"
11 "net/url"
12 "strings"
13 "text/template"
14
15 "github.com/dgraph-io/badger/v3"
16 "golang.org/x/exp/maps"
17 "golang.org/x/exp/slices"
18)
19
20// Create shortens a given URL with an optional name. If a name is provided,
21// that name will be used. Otherwise, 4-character string will be generated and
22// used instead.
23func (m model) create(name string, url string) string {
24 err := m.database.Update(func(txn *badger.Txn) error {
25 return txn.Set([]byte(name), []byte(url))
26 })
27 if err != nil {
28 // TODO: return an error instead so it can be handled like an error
29 return fmt.Sprint(err)
30 }
31
32 log.Println("\"" + url + "\" mapped to \"" + name + "\"")
33 return fmt.Sprint("URL mapped to ", name, "\n")
34}
35
36// read() accepts a start variable for the first link in a set (implying reverse
37// iteration), an end variable for the last link in a set (implying forward
38// iteration), and a count variable for the number of requested entries. The
39// requested set is returned as JSON.
40func (m model) read(start string, end string, count int) (map[string]string, error) {
41 links := make(map[string]string)
42
43 if !(count > 0) {
44 return nil, errors.New("Count parameter is required")
45 }
46
47 err := m.database.View(func(txn *badger.Txn) error {
48 opts := badger.DefaultIteratorOptions
49 opts.PrefetchSize = 20
50 if start != "" && end == "" {
51 // Start value provided, iterate backwards
52 opts.Reverse = true
53 }
54 iterator := txn.NewIterator(opts)
55 defer iterator.Close()
56 if start == "" && end == "" {
57 iterator.Rewind()
58 } else if start != "" && end == "" {
59 // Set position to "start"
60 iterator.Seek([]byte(start))
61 // If "start" exists, move to next value
62 if iterator.Valid() {
63 iterator.Next()
64 }
65 } else if start == "" && end != "" {
66 // Start value provided, iterate forwards
67 // Set position to "end"
68 iterator.Seek([]byte(end))
69 // If "end" exists, move to next value
70 if iterator.Valid() {
71 iterator.Next()
72 }
73 } else {
74 return errors.New("Only provide start OR end parameters, not both")
75 }
76 for i := 0; i < count; i++ {
77 if iterator.Valid() {
78 err := iterator.Item().Value(func(v []byte) error {
79 links[string(iterator.Item().Key())] = string(v)
80 return nil
81 })
82 if err != nil {
83 return err
84 }
85 iterator.Next()
86 }
87 }
88 return nil
89 })
90 if err != nil {
91 return nil, err
92 }
93
94 return links, nil
95}
96
97// Update modifies a shortened link
98func (m model) update(name string, url string, oldName string) string {
99 m.create(name, url)
100 if name != oldName {
101 m.delete(oldName)
102 }
103 return fmt.Sprint("\"" + url + "\" mapped to \"" + name + "\"")
104}
105
106// Delete removes a shortened URL from the database given its name.
107func (m model) delete(name string) string {
108 err := m.database.Update(func(txn *badger.Txn) error {
109 return txn.Delete([]byte(name))
110 })
111 if err != nil {
112 return fmt.Sprint(err)
113 }
114
115 log.Println("\"" + name + "\" has been deleted")
116 return fmt.Sprint("\"" + name + "\" has been deleted\n")
117}
118
119// nameExists returns true if the provided name is already stored in the
120// database and false if it's not.
121func (m model) nameExists(name string) bool {
122 err := m.database.View(func(txn *badger.Txn) error {
123 _, err := txn.Get([]byte(name))
124 if err != nil {
125 return err
126 }
127 return nil
128 })
129 return err == nil
130}
131
132// unauthenticated serves the unauthenticated home page to the visitor
133func unauthenticated() string {
134 home, err := templates.ReadFile("templates/home_unauthenticated.html")
135 if err != nil {
136 log.Fatalln(err)
137 }
138 return string(home)
139}
140
141// authenticated serves the authenticated dashboard to the user
142func (m model) authenticated(links map[string]string) string {
143 dash, err := templates.ReadFile("templates/home_authenticated.html")
144 if err != nil {
145 log.Fatalln(err)
146 }
147 tmpl, err := template.New("authenticated").Parse(string(dash))
148 if err != nil {
149 log.Println(err)
150 }
151
152 // Maps have no order so we're loading the keys into a slice and sorting the slice
153 linksKeys := maps.Keys(links)
154 slices.Sort(linksKeys)
155
156 type pageData struct {
157 Table string // HTML Table inserted to home page
158 Start string // Start
159 End string
160 }
161
162 var homePage pageData
163
164 // And ranging through the keys to pull ordered values from the map
165 for _, k := range linksKeys {
166 v := links[k]
167 homePage.Table = homePage.Table + fmt.Sprintf(`<tr>
168 <td><p>%s</p></td>
169 <td><p>%s</p></td>
170 <td><a class="button" href="/?action=edit&name=%s&url=%s">Edit</a><a class="button" href="/?action=delete&name=%s">Delete</a></td>
171</tr>`, k, v, k, url.QueryEscape(string(v)), k)
172 }
173
174 homePage.Start = linksKeys[0]
175 homePage.End = linksKeys[len(linksKeys)-1]
176
177 page := new(strings.Builder)
178 err = tmpl.Execute(page, homePage)
179 if err != nil {
180 log.Println(err)
181 }
182 return page.String()
183}
184
185type EditFields struct {
186 URL string
187 Name string
188}
189
190// update serves the editing interface to the user
191func (m model) edit(name string, url string) string {
192 fields := EditFields{url, name}
193 editPage, err := templates.ReadFile("templates/edit.html")
194 if err != nil {
195 log.Fatalln(err)
196 }
197 tmpl, err := template.New("editPage").Parse(string(editPage))
198 if err != nil {
199 log.Println(err)
200 }
201 page := new(strings.Builder)
202 err = tmpl.Execute(page, fields)
203 if err != nil {
204 log.Println(err)
205 }
206 return page.String()
207}