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 prev := std.PrevRealm()
47 switch {
48 case !prev.IsUser():
49 panic(errNotAUser)
50 case hasSigned.Has(prev.Addr().String()):
51 panic(errAlreadySigned)
52 }
53 message = validateMessage(message)
54
55 guestbook.Set(signatureID.Next().Binary(), Signature{
56 Message: message,
57 Author: prev.Addr(),
58 // NOTE: time.Now() will yield the "block time", which is deterministic.
59 Time: time.Now(),
60 })
61 hasSigned.Set(prev.Addr().String(), struct{}{})
62}
63
64func validateMessage(msg string) string {
65 if len(msg) > maxMessageLength {
66 panic("Keep it brief! (max " + strconv.Itoa(maxMessageLength) + " bytes!)")
67 }
68 out := ""
69 for _, ch := range msg {
70 switch {
71 case unicode.IsLetter(ch),
72 unicode.IsNumber(ch),
73 unicode.IsSpace(ch),
74 unicode.IsPunct(ch):
75 out += string(ch)
76 default:
77 panic(errInvalidCharacterInMessage)
78 }
79 }
80 return out
81}
82
83func Render(maxID string) string {
84 var bld strings.Builder
85
86 bld.WriteString("# Guestbook ๐\n\n[Come sign the guestbook!](./guestbook$help&func=Sign)\n\n---\n\n")
87
88 var maxIDBinary string
89 if maxID != "" {
90 mid, err := seqid.FromString(maxID)
91 if err != nil {
92 panic(err)
93 }
94
95 // AVL iteration is exclusive, so we need to decrease the ID value to get the "true" maximum.
96 mid--
97 maxIDBinary = mid.Binary()
98 }
99
100 var lastID seqid.ID
101 var printed int
102 guestbook.ReverseIterate("", maxIDBinary, func(key string, val interface{}) bool {
103 sig := val.(Signature)
104 message := strings.ReplaceAll(sig.Message, "\n", "\n> ")
105 bld.WriteString("> " + message + "\n>\n")
106 idValue, ok := seqid.FromBinary(key)
107 if !ok {
108 panic("invalid seqid id")
109 }
110
111 bld.WriteString("> _Written by " + sig.Author.String() + " at " + sig.Time.Format(time.DateTime) + "_ (#" + idValue.String() + ")\n\n---\n\n")
112 lastID = idValue
113
114 printed++
115 // stop after exceeding limit
116 return printed >= maxPerPage
117 })
118
119 if printed == 0 {
120 bld.WriteString("No messages!")
121 } else if printed >= maxPerPage {
122 bld.WriteString("<p style='text-align:right'><a href='./guestbook:" + lastID.String() + "'>Next page</a></p>")
123 }
124
125 return bld.String()
126}
guestbook.gno
2.90 Kb ยท 126 lines