This repository has been archived by the owner on Nov 6, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathhistory-bot.coffee
227 lines (174 loc) · 6.12 KB
/
history-bot.coffee
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
###
History bot for Martini IRC
- remember when a user logs out
- then when they re-join, invite them to 'catchup'
- user can also 'catchup N' an arbitrary # of lines
- saves max-sized buffer to memory
- ONE CHANNEL AT A TIME - to run multiple channels, run multiple bots
###
irc = require 'irc'
require 'sugar' # for dates
async = require 'async'
argv = require('optimist')
.demand('server').alias('server', 's').describe('server', 'Server')
.demand('channel').alias('channel', 'c').describe('channel', 'Channel')
.demand('botname').alias('botname', 'b').describe('botname', 'Bot Name')
.alias('user', 'u').describe('user', 'Username for server')
.alias('password', 'p').describe('password', 'Password for server')
.describe('port', 'Port').default('port', '6667')
.boolean('ssl').describe('ssl', 'Use SSL').default('ssl', false)
.argv
server = argv.server
channel = argv.channel
try if not channel.match(/^#/) then channel = '#' + channel
botName = argv.botname
console.log "Connecting to #{channel} on #{server}:#{argv.port} as #{botName} " +
(if argv.ssl then "with SSL" else "without SSL")
bot = new irc.Client server, botName,
channels: [ channel ]
autoConnect: false
secure: argv.ssl
userName: argv.u
password: argv.p
selfSigned: true
certExpired: true
port: argv.port
bot.on 'error', (error) ->
unless error.command is 'err_nosuchnick' then console.log 'error:', error
bot.on 'registered', (data) ->
console.log "Joined #{channel}"
# has my nick changed? (e.g. if connected >once on same server)
# - not sure if this is consistent -
if data.args?.length is 2
newNick = data.args[0]
if newNick isnt botName
console.warn "Bot Name changed to #{newNick}!"
botName = newNick
# store messages as hash w/ n:msg
msgs = {}
# current range of msgs
msgCount = 0
msgMin = 1
keepOnly = 1000
# msgCount at which people leave
# (in memory for now, @todo move to redis)
usersLastSaw = {}
# if a user is logged off and logs back on,
# their nick gets a '_' appended to it.
# then they see a long history of msgs that they already saw.
# so ignore the trailing _'s.
_realNick = (nick)->
try
return nick.replace /_*$/, ''
catch e
return nick
# track that user saw the last message.
# use global msgCount as counter.
# async response so we can refactor to redis later.
recordUserSaw = (who, callback)->
who = _realNick who
# don't care about self
if who is botName then return
# don't regress
if usersLastSaw[who]?
usersLastSaw[who] = Math.max(usersLastSaw[who], msgCount)
else
usersLastSaw[who] = msgCount
callback?()
# someone else speaks
bot.on 'message' + channel, (who, message)->
who = _realNick who
# handle 'catchup' requests
if matches = message.match /^catchup( [0-9]*)?$/
catchup who, (matches[1] ? 0)
return
# save everything else
d = Date.create()
msgs[++msgCount] = d.format('{M}/{d}/{yy}') + ' ' + d.format('{12hr}:{mm}{tt}') + " #{who}: #{message}"
# cleanup
if msgCount - msgMin >= keepOnly
for n in [msgMin..(msgCount-keepOnly)]
delete msgs[n]
msgMin = (n + 1) if n >= msgMin
# get the list of users in this channel,
# record that they got the last msg. (caught by event handler.)
bot.send 'NAMES', channel
quitHandler = (who, type = "left")->
who = _realNick who
console.log "#{who} #{type} at msg ##{msgCount}"
recordUserSaw who
# 3 ways to leave
bot.on 'part' + channel, (who, reason)->
quitHandler who, 'left'
bot.on 'kick' + channel, (who, byWho, reason)->
quitHandler who, 'kicked'
bot.on 'quit', (who, reason, channels, message)->
if channel in channels then quitHandler who, 'quit'
# someone joins
bot.on 'join' + channel, (who, message) ->
who = _realNick who
# self? (instead of 'registered' which our new server doesn't like, pre-join)
if who is botName and msgCount is 0
bot.say channel, "#{botName} is watching. When you leave this channel and return, " +
"you can 'catchup' on what you missed, or at any time, 'catchup N' # of lines."
return
console.log "#{who} joined at msg ##{msgCount}"
# auto-catchup, if something new or unknown user.
catchup who
bot.on 'end', ()->
console.log "Connection ended"
# @todo try to reconnect?
bot.on 'close', ()->
console.log "Connection closed"
# names are requested whenever a message is posted.
# track that everyone in the room has seen the last message.
bot.on 'names', (forChannel, names)->
if forChannel isnt channel then return
try
names = Object.keys(names)
# (exclude self)
console.log "updating #{names.length - 1} users (" + names.join(',') + ") to msg ##{msgCount}"
recordUserSaw who for who in names
catch error
console.error "Unable to parse names", error
# (async so we can refactor to redis)
countMissed = (who, callback)->
who = _realNick who
# differentiate 0 (nothing new) from false (don't know the user)
if usersLastSaw[who]?
callback (msgCount - usersLastSaw[who])
else callback false
# (async so we can refactor to redis)
catchup = (who, lastN = 0, callback)->
who = _realNick who
async.waterfall [
(next)->
# actual # of missed lines. may be > when initially mentioned on re-join.
if lastN is 0 then countMissed who, (lastN)-> next null, lastN
else next null, lastN
(lastN, next)->
# countMissed returned 0, means the user is known but hasn't missed anything.
if lastN is 0
console.log "Nothing new to send #{who}"
next true
# user isn't recognized, send a bunch
else if lastN is false then next null, 100
else next null, lastN
(lastN, next)->
# @todo refactor {msgs}.length to redis lookup
# don't try to send more than we have
lastN = Math.min lastN, Object.keys(msgs).length
next null, lastN
(lastN, next)->
console.log "Sending #{who} the last #{lastN} messages"
# private
bot.say who, "Catchup on the last #{lastN} messages:"
for n in [(msgCount-lastN+1)..msgCount]
if msgs[n]? then bot.say who, msgs[n]
next()
],
(error)->
# don't pass back errors
if error instanceof Error then console.error error
callback?()
bot.connect()