-
Notifications
You must be signed in to change notification settings - Fork 1
/
votebot.go
158 lines (152 loc) · 5.36 KB
/
votebot.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package main
import (
"context"
"encoding/hex"
"fmt"
"net/http"
"strings"
"github.com/vocdoni/vote-frame/bot"
"github.com/vocdoni/vote-frame/bot/poll"
fapi "github.com/vocdoni/vote-frame/farcasterapi"
"github.com/vocdoni/vote-frame/farcasterapi/neynar"
"github.com/vocdoni/vote-frame/shortener"
"go.vocdoni.io/dvote/httprouter"
"go.vocdoni.io/dvote/httprouter/apirest"
"go.vocdoni.io/dvote/log"
"go.vocdoni.io/dvote/types"
)
// initBot helper function initializes the bot and starts listening for new polls
// to create elections
func initBot(ctx context.Context, handler *vocdoniHandler, api fapi.API,
defaultCensus *CensusInfo,
) (*bot.Bot, error) {
voteBot, err := bot.New(bot.BotConfig{
API: api,
})
if err != nil {
return nil, fmt.Errorf("failed to create bot: %w", err)
}
voteBot.Start(ctx)
// handle new messages in background
go func() {
for {
select {
case <-ctx.Done():
return
case msg := <-voteBot.Messages:
// check if the message is a poll and create an election
user, poll, isPool, err := voteBot.PollMessageHandler(ctx, msg, maxElectionDuration)
if err == nil && isPool {
if err := pollToCast(ctx, handler, poll, user, msg, voteBot, defaultCensus); err != nil {
log.Errorf("error creating election: %s", err)
continue
}
log.Debugw("poll created and reply sent",
"poll", poll,
"userdata", user,
"msg-hash", msg.Hash)
continue
}
// check if the message is a mute request and mute the user
user, parentMsg, isMuteRequest, err := voteBot.MuteRequestHandler(ctx, msg)
if err == nil && isMuteRequest {
if err := mutePollCreator(handler, user, parentMsg); err != nil {
log.Errorf("error muting user: %s", err)
}
log.Debugw("poll creator muted",
"user", user,
"poll", parentMsg.Embeds)
continue
}
}
}
}()
return voteBot, nil
}
// pollToCast helper function creates an election from a poll and sends the poll
// URL to the user replying to the message with the poll frame. If something
// goes wrong it returns an error.
func pollToCast(ctx context.Context, handler *vocdoniHandler, poll *poll.Poll,
user *fapi.Userdata, msg *fapi.APIMessage, voteBot *bot.Bot,
defaultCensus *CensusInfo,
) error {
description := &ElectionDescription{
Question: poll.Question,
Options: poll.Options,
Duration: poll.Duration,
Overwrite: false,
}
profile := &FarcasterProfile{
FID: user.FID,
Username: user.Username,
Custody: user.CustodyAddress,
Verifications: user.VerificationsAddresses,
}
electionID, err := handler.createAndSaveElectionAndProfile(description,
defaultCensus, profile, true, false, "", ElectionSourceBot, nil)
if err != nil {
return fmt.Errorf("error creating election: %w", err)
}
frameUrl := fmt.Sprintf("%s/%s", serverURL, electionID.String())
shortenedUrl, err := shortener.ShortURL(ctx, frameUrl)
if err != nil {
// if shortening fails, use the original url
shortenedUrl = frameUrl
}
if err := voteBot.ReplyWithPollURL(ctx, msg, shortenedUrl); err != nil {
return fmt.Errorf("error replying to poll: %s", err)
}
return nil
}
// mutePollCreator helper function mutes the poll creator user from notifications.
// It extracts the election ID from the parent message embeds (expecting that
// the election URL is included as a frame) and mutes the user that created the
// election for the given user. If something goes wrong it returns an error.
func mutePollCreator(handler *vocdoniHandler, user *fapi.Userdata, parent *fapi.APIMessage) error {
var electionID string
for _, embed := range parent.Embeds {
if strings.HasPrefix(embed, serverURL) {
// get the election ID from the embed URL in the parent message
// removing the server URL prefix, including the slash separator
electionID = strings.TrimPrefix(embed, serverURL+"/")
break
}
}
if electionID == "" {
return fmt.Errorf("election not found in embeds")
}
// get election from the database decoding the election ID from string
bElectionID, err := hex.DecodeString(electionID)
if err != nil {
return fmt.Errorf("error decoding election ID: %w", err)
}
election, err := handler.db.Election(types.HexBytes(bElectionID))
if err != nil {
return fmt.Errorf("error getting election from the database: %w", err)
}
if err := handler.db.AddNotificationMutedUser(user.FID, election.UserID); err != nil {
return fmt.Errorf("error muting user: %w", err)
}
return nil
}
// neynarWebhook helper function returns a function that handles neynar webhooks.
// It verifies the request and handles the webhook using the neynar client.
func neynarWebhook(neynarcli *neynar.NeynarAPI, webhookSecret string) func(*apirest.APIdata, *httprouter.HTTPContext) error {
return func(msg *apirest.APIdata, h *httprouter.HTTPContext) error {
neynarSig := h.Request.Header.Get("X-Neynar-Signature")
verified, err := neynar.VerifyRequest(webhookSecret, neynarSig, msg.Data)
if err != nil {
log.Errorf("error verifying request: %s", err)
return h.Send([]byte("error verifying request"), http.StatusBadRequest)
}
if !verified {
log.Error("request not verified")
return h.Send([]byte("request not verified"), http.StatusUnauthorized)
}
if err := neynarcli.WebhookHandler(msg.Data); err != nil {
log.Errorf("error handling webhook: %s", err)
return fmt.Errorf("error handling webhook: %s", err)
}
return h.Send([]byte("ok"), http.StatusOK)
}
}