-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
199 lines (183 loc) · 5.65 KB
/
main.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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"regexp"
"strconv"
"time"
_ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog"
globallog "github.com/rs/zerolog/log"
"go.mau.fi/util/dbutil"
"gopkg.in/yaml.v3"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/cryptohelper"
"maunium.net/go/mautrix/event"
)
var MSC_REGEX *regexp.Regexp = regexp.MustCompile(`\b(?:MSC|msc)(\d+)\b`)
func main() {
// Arg parsing
configPath := flag.String("config", "./config.yaml", "config file location")
flag.Parse()
// Load configuration
globallog.Info().Str("config_path", *configPath).Msg("Reading config")
configYaml, err := os.ReadFile(*configPath)
if err != nil {
globallog.Fatal().Err(err).Str("config_path", *configPath).Msg("Failed reading the config")
}
var config Configuration
err = yaml.Unmarshal(configYaml, &config)
if err != nil {
globallog.Fatal().Err(err).Msg("Failed to parse configuration YAML")
}
// Setup logging
log, err := config.Logging.Compile()
if err != nil {
globallog.Fatal().Err(err).Msg("Failed to compile logging configuration")
}
// Open the database
db, err := dbutil.NewFromConfig("msclinkbot", config.Database, dbutil.ZeroLogger(*log))
if err != nil {
log.Fatal().Err(err).Msg("couldn't open database")
}
// Log In
client, err := mautrix.NewClient(config.Homeserver, "", "")
if err != nil {
log.Fatal().Err(err).Msg("Failed to create matrix client")
}
client.Log = *log
ctx := log.WithContext(context.TODO())
cryptoHelper, err := cryptohelper.NewCryptoHelper(client, []byte("xyz.hnitbjorg.msc_link_bot"), db)
if err != nil {
log.Fatal().Err(err).Msg("Failed to create crypto helper")
}
password, err := config.GetPassword(log)
if err != nil {
log.Fatal().Err(err).Str("password_file", config.PasswordFile).Msg("Could not read password from file")
}
cryptoHelper.LoginAs = &mautrix.ReqLogin{
Type: mautrix.AuthTypePassword,
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: config.Username.String()},
Password: password,
}
cryptoHelper.DBAccountID = config.Username.String()
err = cryptoHelper.Init(ctx)
if err != nil {
log.Fatal().Err(err).Msg("Failed to initialize crypto helper")
}
client.Crypto = cryptoHelper
syncer := client.Syncer.(mautrix.ExtensibleSyncer)
syncer.OnSync(client.DontProcessOldEvents)
if config.AutoJoin {
syncer.OnEventType(event.StateMember, func(ctx context.Context, evt *event.Event) {
if evt.StateKey == nil || *evt.StateKey != config.Username.String() {
return
}
if evt.Content.AsMember().Membership == event.MembershipInvite {
_, err := client.JoinRoom(ctx, evt.RoomID.String(), "", nil)
if err != nil {
log.Error().Err(err).Str("room_id", evt.RoomID.String()).Msg("Failed to join room")
}
}
})
}
syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) {
retContent := getMsgResponse(log, evt)
if retContent == nil {
return
}
resp, err := client.SendMessageEvent(ctx, evt.RoomID, event.EventMessage, retContent)
if err != nil {
log.Err(err).Msg("couldn't send event")
return
}
log.Info().Str("event_id", resp.EventID.String()).Msg("sent event")
})
err = client.Sync()
if err != nil {
log.Fatal().Err(err).Msg("error syncing")
}
}
// this function assumes evt.Type is EventMessage
// return value is the message content to send back, if any
func getMsgResponse(log *zerolog.Logger, evt *event.Event) *event.MessageEventContent {
// only respond to messages that were sent in the last five minutes so
// that during an initial sync we don't respond to old messages
if time.Unix(evt.Timestamp/1000, evt.Timestamp%1000).Before(time.Now().Add(time.Minute * -5)) {
return nil
}
content := evt.Content.AsMessage()
if content.MsgType != event.MsgText {
return nil
}
mscs := getMSCs(content.Body)
retBody := ""
for i, msc := range mscs {
log.Info().
Str("room_id", evt.RoomID.String()).
Str("event_id", evt.ID.String()).
Uint("msc", msc).
Msg("found MSC")
if i > 0 {
retBody += "\n"
}
retBody += getMSCResponse(log, msc)
}
if retBody == "" {
return nil
}
return &event.MessageEventContent{
MsgType: event.MsgNotice,
Body: retBody,
}
}
func getMSCs(body string) (mscs []uint) {
bodyNoReplies := event.TrimReplyFallbackText(body)
matches := MSC_REGEX.FindAllStringSubmatch(bodyNoReplies, -1)
mscSet := make(map[int]struct{})
for _, match := range matches {
// error can never happen because of %d in regex
msc, _ := strconv.Atoi(match[1])
_, exists := mscSet[msc]
if exists {
// don't add the same MSC twice
continue
}
mscSet[msc] = struct{}{}
mscs = append(mscs, uint(msc))
}
return mscs
}
func getMSCResponse(log *zerolog.Logger, msc uint) string {
mscPR := fmt.Sprintf("https://github.com/matrix-org/matrix-spec-proposals/pull/%d", msc)
resp, err := http.Get(fmt.Sprintf("https://api.github.com/repos/matrix-org/matrix-spec-proposals/pulls/%d", msc))
if err != nil {
log.Warn().Err(err).Uint("msc", msc).Msg("couldn't get MSC details")
return mscPR
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
log := log.With().Uint("msc", msc).Int("status_code", resp.StatusCode).Logger()
byts, err := io.ReadAll(resp.Body)
if err == nil {
log = log.With().Str("body", string(byts)).Logger()
}
log.Warn().Msg("received non-200 status code while fetching MSC details")
return mscPR
}
decoder := json.NewDecoder(resp.Body)
var body struct {
Title string `json:"title"` // only param we care about
}
err = decoder.Decode(&body)
if err != nil {
log.Warn().Err(err).Msg("couldn't decode PR details json")
return mscPR
}
return fmt.Sprintf("%s %s", body.Title, mscPR)
}