guestbook.gno
2.92 Kb ยท 127 lines
1// Realm guestbook contains an implementation of a simple guestbook.
2// Come and sign yourself up!
3package guestbook
4
5import (
6 "std"
7 "strconv"
8 "strings"
9 "time"
10 "unicode"
11
12 "gno.land/p/demo/avl"
13 "gno.land/p/demo/seqid"
14)
15
16// Signature is a single entry in the guestbook.
17type Signature struct {
18 Message string
19 Author std.Address
20 Time time.Time
21}
22
23const (
24 maxMessageLength = 140
25 maxPerPage = 25
26)
27
28var (
29 signatureID seqid.ID
30 guestbook avl.Tree // id -> Signature
31 hasSigned avl.Tree // address -> struct{}
32)
33
34func init() {
35 Sign("You reached the end of the guestbook!")
36}
37
38const (
39 errNotAUser = "this guestbook can only be signed by users"
40 errAlreadySigned = "you already signed the guestbook!"
41 errInvalidCharacterInMessage = "invalid character in message"
42)
43
44// Sign signs the guestbook, with the specified message.
45func Sign(message string) {
46 crossing()
47 prev := std.PreviousRealm()
48 switch {
49 case !prev.IsUser():
50 panic(errNotAUser)
51 case hasSigned.Has(prev.Address().String()):
52 panic(errAlreadySigned)
53 }
54 message = validateMessage(message)
55
56 guestbook.Set(signatureID.Next().Binary(), Signature{
57 Message: message,
58 Author: prev.Address(),
59 // NOTE: time.Now() will yield the "block time", which is deterministic.
60 Time: time.Now(),
61 })
62 hasSigned.Set(prev.Address().String(), struct{}{})
63}
64
65func validateMessage(msg string) string {
66 if len(msg) > maxMessageLength {
67 panic("Keep it brief! (max " + strconv.Itoa(maxMessageLength) + " bytes!)")
68 }
69 out := ""
70 for _, ch := range msg {
71 switch {
72 case unicode.IsLetter(ch),
73 unicode.IsNumber(ch),
74 unicode.IsSpace(ch),
75 unicode.IsPunct(ch):
76 out += string(ch)
77 default:
78 panic(errInvalidCharacterInMessage)
79 }
80 }
81 return out
82}
83
84func Render(maxID string) string {
85 var bld strings.Builder
86
87 bld.WriteString("# Guestbook ๐\n\n[Come sign the guestbook!](./guestbook$help&func=Sign)\n\n---\n\n")
88
89 var maxIDBinary string
90 if maxID != "" {
91 mid, err := seqid.FromString(maxID)
92 if err != nil {
93 panic(err)
94 }
95
96 // AVL iteration is exclusive, so we need to decrease the ID value to get the "true" maximum.
97 mid--
98 maxIDBinary = mid.Binary()
99 }
100
101 var lastID seqid.ID
102 var printed int
103 guestbook.ReverseIterate("", maxIDBinary, func(key string, val any) bool {
104 sig := val.(Signature)
105 message := strings.ReplaceAll(sig.Message, "\n", "\n> ")
106 bld.WriteString("> " + message + "\n>\n")
107 idValue, ok := seqid.FromBinary(key)
108 if !ok {
109 panic("invalid seqid id")
110 }
111
112 bld.WriteString("> _Written by " + sig.Author.String() + " at " + sig.Time.Format(time.DateTime) + "_ (#" + idValue.String() + ")\n\n---\n\n")
113 lastID = idValue
114
115 printed++
116 // stop after exceeding limit
117 return printed >= maxPerPage
118 })
119
120 if printed == 0 {
121 bld.WriteString("No messages!")
122 } else if printed >= maxPerPage {
123 bld.WriteString("<p style='text-align:right'><a href='./guestbook:" + lastID.String() + "'>Next page</a></p>")
124 }
125
126 return bld.String()
127}