/
replacebot.go
196 lines (165 loc) · 5.4 KB
/
replacebot.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
package main
import "flag"
import "fmt"
import "regexp"
import "strings"
import irc "github.com/fluffle/goirc/client"
import "log"
const (
BOT_SOURCE = "https://github.com/prattmic/replacebot"
)
var (
nick = flag.String("nick", "replacebot", "Nick of bot")
server = flag.String("server", "chat.freenode.net:6667", "Server (and port) to connect to")
channel = flag.String("channel", "#ncsulug", "Channel to join")
)
var last_message map[string]string
// Yay! Regex!
// Regex for matching {vim,perl,sed}-style replacement lines.
// Input is of the form "user: s/regex[/replacement[/flags]]"
// Where user, replacement, flags, and their assosciated slashes are optional
// The input allows escaped slashes (\/) in the regex or replacement
// When used with FindSubmatch, match[1] = user, match[2] = regex, match[3] = replacement, match[4] = flags
// As a single line: "^(?:(.+?)[:, ]\\s*)?s/((?:\\\\/|[^/])+)(?:/((?:\\\\/|[^/])*)(?:/((?:[ig])*))?)?" */
var search *regexp.Regexp = regexp.MustCompile(
// Start at the beginning of the line
`^` +
// Match arbitraru (optional) username in group (1), with a delimiting character and some whitespace
`(?:(.+?)[:, ]\s*)?` +
// Beginning of search command
`s/` +
// Match search regexp in group (2)
// One or more (you have to provide something to search for!) non-slashes (/), unless slash is escaped (\/)
`((?:\\/|[^/])+)` +
// Everything after the search regexp is optional (`s/blah` is valid and replaces with nothing)
`(?:` +
// Match / separating search and replacement
`/` +
// Match replacement string in group (3)
`((?:\\/|[^/])*)` +
// Everything after replacement is optional (/flags) (`s/blah/bleh` is valid)
`(?:` +
// Match / separating replacement and flags
`/` +
// Zero or more i (ignore case) or g (global replace) flags in group (4)
`([ig]*)` +
// End optional group for (/flags)
`)?` +
// End optional group for (/replacement/flags)
`)?`)
func main() {
flag.Parse()
last_message = make(map[string]string)
c := irc.SimpleClient(*nick)
// Add handlers to do things here!
// e.g. join a channel on connect.
c.HandleFunc("connected",
func(conn *irc.Conn, line *irc.Line) {
log.Printf("Connected to %s as %s", c.Config().Server, c.Config().Me.Nick)
conn.Join(*channel)
})
// And a signal on disconnect
quit := make(chan bool)
c.HandleFunc("disconnected",
func(conn *irc.Conn, line *irc.Line) {
log.Print("Disconnected")
quit <- true
})
// Watch for messages
c.HandleFunc("PRIVMSG", privmsg)
// Tell client to connect.
if err := c.ConnectTo(*server); err != nil {
fmt.Printf("Connection error: %s\n", err)
return
}
// Wait for disconnect
<-quit
}
/* Watch for messages.
* Keep track of last thing every user said, and if they perform a replacement,
* do the replacement for them and respond */
func privmsg(conn *irc.Conn, line *irc.Line) {
channel := line.Args[0]
message := strings.Join(line.Args[1:], "")
log.Printf("%s to %s: %s", line.Nick, channel, message)
match := search.FindStringSubmatch(message)
if match != nil {
log.Printf("Complete match: \"%s\"", match[0])
log.Printf("Matched user: \"%s\"", match[1])
log.Printf("Matched regex: \"%s\"", match[2])
log.Printf("Matched replacement: \"%s\"", match[3])
log.Printf("Matched flags: \"%s\"", match[4])
}
if match != nil {
user := match[1]
replacement_regex := match[2]
repl := match[3]
flags := match[4]
global := false
// Replacement must be given
if replacement_regex == "" {
return
}
// Default to current user
if user == "" {
user = line.Nick
}
// Replace escaped slashes with real ones
replacement_regex = strings.Replace(replacement_regex, "\\/", "/", -1)
repl = strings.Replace(repl, "\\/", "/", -1)
// Ignore case
if strings.Contains(flags, "i") {
replacement_regex = fmt.Sprintf("(?i)%s", replacement_regex)
}
// Global replacement
if strings.Contains(flags, "g") {
global = true
}
// User's last message
m, ok := last_message[user]
if !ok {
return
}
// Apply their regex
regex, err := regexp.Compile(replacement_regex)
if err != nil {
conn.Privmsg(channel, fmt.Sprintf("%s: error compiling regex: %s", line.Nick, err))
return
}
i := 0
fixed := regex.ReplaceAllStringFunc(m, func(s string) string {
if global {
return repl
} else if i < 1 {
// non-global replacement only done once
i = i + 1
return repl
}
return s
})
/* Limit result to one message
* My limited testing indicates that 438 (?)
* characters is the maximum */
if len(fixed) > 438 {
b := []byte(fixed)
fixed = string(b[0:438])
}
// Send out replaced message
conn.Privmsg(channel, fmt.Sprintf("<%s>: %s", user, fixed))
// Consider this corrected message the user's last message
last_message[user] = fixed
} else {
// Not a replacement, just remember this message
last_message[line.Nick] = message
// Send source link
if strings.EqualFold(message, *nick+": source") {
conn.Privmsg(channel, fmt.Sprintf("%s: "+BOT_SOURCE, line.Nick))
} else if strings.EqualFold(message, *nick+": help") {
conn.Privmsg(channel, fmt.Sprintf("%s: I will search and replace your last message when you "+
"use the format s/regex/replacement/flags (don't direct at me). "+
"Flags are i (ignore case) and g (global replacement). "+
"Replacements directed at a user will replace their last message. "+
"See source for more details.", line.Nick))
}
}
}