polls.gno
3.49 Kb ยท 168 lines
1package polls
2
3import (
4 "time"
5
6 "gno.land/p/nt/avl"
7 "gno.land/p/nt/seqid"
8 pollsv1 "gno.land/p/zenao/polls/v1"
9 "gno.land/p/zenao/realmid"
10)
11
12var (
13 polls *avl.Tree // string (seqid.ID) -> *Poll
14 id seqid.ID
15)
16
17func init() {
18 polls = avl.NewTree()
19}
20
21type AuthFn func() (userID string, authorized bool)
22
23type Poll struct {
24 ID seqid.ID
25 Question string
26 Kind pollsv1.PollKind
27 Results *avl.Tree // string (options) -> avl.Tree of string (realmid of users) -> struct{}
28
29 Duration int64
30
31 CreatedAt int64
32 CreatedBy string
33
34 authFunc AuthFn
35}
36
37func NewPoll(_ realm, question string, kind pollsv1.PollKind, duration int64, options []string, authFunc AuthFn) *Poll {
38 if len(options) < 2 {
39 panic("poll must have at least 2 options")
40 }
41 if len(options) > 8 {
42 panic("poll must have at most 8 options")
43 }
44 minDuration := int64(time.Minute) * 15 / int64(time.Second)
45 maxDuration := int64(time.Hour) * 24 * 30 / int64(time.Second)
46 if duration < minDuration {
47 panic("duration must be at least 15 minutes")
48 }
49 if duration > maxDuration {
50 panic("duration must be at most 1 month")
51 }
52 poll := &Poll{
53 ID: id.Next(),
54 Question: question,
55 Kind: kind,
56 Results: avl.NewTree(),
57 Duration: duration,
58 CreatedAt: time.Now().Unix(),
59 CreatedBy: realmid.Previous(),
60 authFunc: authFunc,
61 }
62 for _, option := range options {
63 if option == "" {
64 panic("option cannot be empty")
65 }
66 if len(option) > 128 {
67 panic("option cannot be longer than 128 characters")
68 }
69 if poll.Results.Has(option) {
70 panic("duplicate option")
71 }
72 poll.Results.Set(option, avl.NewTree())
73 }
74
75 polls.Set(poll.ID.String(), poll)
76
77 return poll
78}
79
80func Vote(cur realm, pollID uint64, option string) {
81 id := seqid.ID(pollID)
82 pollRaw, ok := polls.Get(id.String())
83 if !ok {
84 panic("poll not found")
85 }
86
87 poll := pollRaw.(*Poll)
88 poll.Vote(cur, option)
89}
90
91func GetInfo(pollID uint64, user string) *pollsv1.Poll {
92 id := seqid.ID(pollID)
93 pollRaw, ok := polls.Get(id.String())
94 if !ok {
95 panic("poll not found")
96 }
97 poll := pollRaw.(*Poll)
98 return poll.GetInfo(user)
99}
100
101func (p *Poll) Vote(_ realm, option string) {
102 callerID := p.auth()
103
104 optionRaw, ok := p.Results.Get(option)
105 if !ok {
106 panic("invalid option")
107 }
108
109 if !p.IsRunning() {
110 panic("poll is not running")
111 }
112
113 // Remove previous choice if multiple answers are not allowed
114 if p.Kind != pollsv1.POLL_KIND_MULTIPLE_CHOICE {
115 p.Results.Iterate("", "", func(key string, value interface{}) bool {
116 choices := value.(*avl.Tree)
117 if choices.Has(callerID) {
118 choices.Remove(callerID)
119 return true
120 }
121 return false
122 })
123 }
124
125 choices := optionRaw.(*avl.Tree)
126 if choices.Has(callerID) {
127 choices.Remove(callerID)
128 } else {
129 choices.Set(callerID, struct{}{})
130 }
131}
132
133func (p *Poll) GetInfo(user string) *pollsv1.Poll {
134 info := &pollsv1.Poll{
135 Question: p.Question,
136 Results: []*pollsv1.PollResult{},
137 Kind: p.Kind,
138 Duration: p.Duration,
139 CreatedAt: p.CreatedAt,
140 CreatedBy: p.CreatedBy,
141 }
142 p.Results.Iterate("", "", func(key string, value interface{}) bool {
143 count := value.(*avl.Tree)
144 info.Results = append(info.Results, &pollsv1.PollResult{
145 Option: key,
146 Count: uint32(count.Size()),
147 HasUserVoted: count.Has(user),
148 })
149 return false
150 })
151 return info
152}
153
154func (p *Poll) IsRunning() bool {
155 return time.Now().Unix() < p.CreatedAt+p.Duration
156}
157
158func (p *Poll) auth() string {
159 if p.authFunc == nil {
160 return realmid.Previous()
161 }
162
163 id, ok := p.authFunc()
164 if !ok {
165 panic("the user is not allowed to interact with this poll")
166 }
167 return id
168}