-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathirc.lua
275 lines (257 loc) · 7.46 KB
/
irc.lua
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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
local irc = safeinit(...)
local Gs = require "state"
local buffers = require "buffers"
local i18n = require "i18n"
local tests = require "tests"
local ui = require "ui"
local util = require "util"
irc.RPL_ISUPPORT = "005"
irc.RPL_LIST = "322"
irc.RPL_LISTEND = "323"
irc.RPL_TOPIC = "332"
irc.RPL_NAMREPLY = "353"
irc.RPL_ENDOFMOTD = "376"
irc.ERR_NOMOTD = "422"
irc.ERR_NICKNAMEINUSE = "433"
function irc.writecmd(...)
local args = {...}
if #args >= 2 then
args[#args] = ":" .. args[#args]
end
local cmd = table.concat(args, " ")
if config.debug then
print("=>", ui.escape(cmd))
end
capi.writesock(cmd)
irc.newcmd(":"..Gs.user.."!@ "..cmd, false)
end
function irc.parsecmd(line)
local data = {}
local pos = 1
if string.sub(line, 1, 1) == ":" then
pos = string.find(line, " ")
if not pos then return end -- invalid message
data.prefix = string.sub(line, 2, pos-1)
pos = pos+1
local excl = string.find(data.prefix, "!")
if excl then
data.user = string.sub(data.prefix, 1, excl-1)
end
end
while pos <= string.len(line) do
local nextpos = nil
if string.sub(line, pos, pos) ~= ":" then
nextpos = string.find(line, " ", pos+1)
else
pos = pos+1
end
if not nextpos then
nextpos = string.len(line)+1
end
table.insert(data, string.sub(line, pos, nextpos-1))
pos = nextpos+1
end
local cmd = string.upper(data[1])
if (cmd == "PRIVMSG" or cmd == "NOTICE") and string.sub(data[3], 1, 1) == "\1" then
data.ctcp = {}
local inner = string.gsub(string.sub(data[3], 2), "\1$", "")
local split = string.find(inner, " ", 2)
if split then
data.ctcp.cmd = string.upper(string.sub(inner, 1, split-1))
data.ctcp.params = string.sub(inner, split+1)
else
data.ctcp.cmd = string.upper(inner)
end
end
return data
end
-- Called for new commands, both from the server and from the client.
function irc.newcmd(line, remote)
local args = irc.parsecmd(line)
local from = args.user
local cmd = string.upper(args[1])
local to = args[2] -- not always valid!
if not remote and not (cmd == "PRIVMSG" or cmd == "NOTICE" or cmd == "QUIT") then
-- (afaik) all other messages are echoed back at us
return
end
if cmd == "PRIVMSG" or cmd == "NOTICE" then
if to == "*" then return end
if string.match(to, Gs.prefix_pat) then
to = string.sub(to, 2)
end
if not args.ctcp or args.ctcp.cmd == "ACTION" then
if to == Gs.user then -- direct message
buffers:push(from, line, {urgency=1})
else
local msg
if not args.ctcp then
msg = args[3]
else
msg = args.ctcp.params or ""
end
if string.match(msg, util.nick_pattern(Gs.user)) then
buffers:push(to, line, {urgency=2})
elseif from == Gs.user then
buffers:push(to, line, {forceshow=true})
else
buffers:push(to, line)
end
end
end
if cmd == "PRIVMSG" and args.ctcp and remote then
if args.ctcp.cmd == "VERSION" then
irc.writecmd("NOTICE", from, "\1VERSION hewwo\1")
elseif args.ctcp.cmd == "PING" then
irc.writecmd("NOTICE", from, args[3])
end
end
elseif cmd == "JOIN" then
buffers:push(to, line, {urgency=-1})
if from == Gs.user then
Gs.buffers[to].connected = true
end
Gs.buffers[to].users[from] = true
elseif cmd == "PART" then
buffers:push(to, line, {urgency=-1})
buffers:leave(to, from)
elseif cmd == "KICK" then
buffers:push(to, line)
buffers:leave(to, args[3])
elseif cmd == "INVITE" then
buffers:push(from, line, {urgency=2})
elseif cmd == "QUIT" then
local forceshow = (from == Gs.user)
local bufs = {}
for chan,buf in pairs(Gs.buffers) do
if buf.users[from] then
table.insert(bufs, chan)
buf.users[from] = nil
end
end
buffers:push(bufs, line, {forceshow=forceshow, urgency=-1})
elseif cmd == "NICK" then
local forceshow = false
local bufs = {}
if from == Gs.user then
forceshow = true
Gs.user = to
end
for chan,buf in pairs(Gs.buffers) do
if buf.users[from] then
table.insert(bufs, chan)
buf.users[from] = nil
buf.users[to] = true
end
end
buffers:push(bufs, line, {forceshow=forceshow, urgency=-1})
elseif cmd == irc.RPL_ISUPPORT then
for i=3,(#args-1) do
local token = args[i]
local minus = string.match(token, "^-")
local key = string.match(token, "^-?(%w+)")
local value = string.match(token, "=(.*)") or true
-- for now i don't support escapes, they're not used on town anyways
if minus then value = nil end
Gs.ISUPPORT[key] = value
if key == "PREFIX" then
local prefixes = string.match(value, "%)(.*)") or "@"
Gs.prefix_pat = "^["..util.patescape(prefixes).."]"
end
end
elseif cmd == irc.RPL_ENDOFMOTD or cmd == irc.ERR_NOMOTD then
Gs.active = true
print(i18n.connected)
print()
elseif cmd == irc.RPL_TOPIC then
Gs.topics[args[3]] = args[4]
buffers:push(args[3], line, {urgency=-1})
elseif cmd == "TOPIC" then
local forceshow = (from == Gs.user)
buffers:push(to, line, {forceshow=forceshow, oldtopic=Gs.topics[to]})
Gs.topics[to] = args[3]
elseif cmd == irc.RPL_LIST or cmd == irc.RPL_LISTEND then
-- TODO list output should probably be pushed into a server buffer
-- but switching away from the current buffer could confuse users?
if ext.reason == "list" then ext.setpipe(true) end
ui.printcmd({line=line, ts=os.time()})
if ext.reason == "list" then ext.setpipe(false) end
elseif cmd == irc.ERR_NICKNAMEINUSE then
local hi = ui.highlight
if Gs.active then
printf("%s is taken, leaving your nick as %s", hi(args[3]), hi(Gs.user))
else
local new = util.nicksucc(Gs.user)
printf("%s is taken, trying %s", hi(Gs.user), hi(new))
Gs.user = new
irc.writecmd("NICK", new)
end
elseif string.sub(cmd, 1, 1) == "4" then
-- TODO the user should never see this. they should instead see friendlier
-- messages with instructions how to proceed
printf("irc error %s: %s", cmd, args[#args])
elseif cmd == "PING" then
irc.writecmd("PONG", to)
elseif cmd == irc.RPL_NAMREPLY then
to = args[4]
buffers:make(to)
for nick in string.gmatch(args[5], "[^ ]+") do
if string.match(nick, Gs.prefix_pat) then
nick = string.sub(nick, 2)
end
Gs.buffers[to].users[nick] = true
end
end
end
-- NOTE: for incoming data, prefer `not irc.is_channel`
function irc.is_nick(s)
-- https://modern.ircdocs.horse/#clients implemented verbatim
-- with the exception of CHANTYPES, because taken literally, that would
-- imply that on some servers "#tildetown" is a nickname, and "dzwdz" is
-- a channel. I assume it's #&.
-- In Soviet Russia, #tildetown joins you!
if not s or s == "" then return false end
if string.find(s, "[ ,*?!@]") then return false end
if string.find(s, "^[$:#&]") then return false end
return true
end
function irc.is_channel(s)
-- https://modern.ircdocs.horse/#channels
-- ignoring CHANTYPES for the reasons mentioned in irc.is_nick
if not s then return false end
if not string.find(s, "^[&#]") then return false end
if string.find(s, "[ \x07,]") then return false end
return true
end
tests.run(function(t)
local chans = {
"#tildetown", "#", "##",
}
local nicks = {
"bx", "cymen", "dzwdz", "ixera", "juspib", "natalia",
"x",
"dzwdz[m]",
"a$woo",
}
local neither = {
"",
"dzwdz [m]", " dzwdz[m]",
"panic!atthedisco", "panic!", "!panic",
"$woo",
" #hello", "# hello", "#hello ",
"#a,#b",
}
for _,v in ipairs(chans) do
t(irc.is_nick(v), false)
t(irc.is_channel(v), true)
end
for _,v in ipairs(nicks) do
t(irc.is_nick(v), true)
t(irc.is_channel(v), false)
end
for _,v in ipairs(neither) do
t(irc.is_nick(v), false)
t(irc.is_channel(v), false)
end
end)
return irc