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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
|
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This implements the Client-to-Client Protocol (CTCP), a subprotocol of IRC.
* REVISED AND UPDATED CTCP SPECIFICATION
* http://www.alien.net.au/irc/ctcp.txt
*/
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { l10nHelper } from "resource:///modules/imXPCOMUtils.sys.mjs";
import { ircHandlerPriorities } from "resource:///modules/ircHandlerPriorities.sys.mjs";
import { displayMessage } from "resource:///modules/ircUtils.sys.mjs";
const lazy = {};
XPCOMUtils.defineLazyGetter(lazy, "_", () =>
l10nHelper("chrome://chat/locale/irc.properties")
);
// Split into a CTCP message which is a single command and a single parameter:
// <command> " " <parameter>
// The high level dequote is to unescape \001 in the message content.
export function CTCPMessage(aMessage, aRawCTCPMessage) {
let message = Object.assign({}, aMessage);
message.ctcp = {};
message.ctcp.rawMessage = aRawCTCPMessage;
// High/CTCP level dequote: replace the quote char \134 followed by a or \134
// with \001 or \134, respectively. Any other character after \134 is replaced
// with itself.
let dequotedCTCPMessage = message.ctcp.rawMessage.replace(
/\\(.|$)/g,
aStr => {
if (aStr[1]) {
return aStr[1] == "a" ? "\x01" : aStr[1];
}
return "";
}
);
let separator = dequotedCTCPMessage.indexOf(" ");
// If there's no space, then only a command is given.
// Do not capitalize the command, case sensitive
if (separator == -1) {
message.ctcp.command = dequotedCTCPMessage;
message.ctcp.param = "";
} else {
message.ctcp.command = dequotedCTCPMessage.slice(0, separator);
message.ctcp.param = dequotedCTCPMessage.slice(separator + 1);
}
return message;
}
// This is the CTCP handler for IRC protocol, it will call each CTCP handler.
export var ircCTCP = {
name: "CTCP",
// Slightly above default RFC 2812 priority.
priority: ircHandlerPriorities.HIGH_PRIORITY,
isEnabled: () => true,
// CTCP uses only PRIVMSG and NOTICE commands.
commands: {
PRIVMSG: ctcpHandleMessage,
NOTICE: ctcpHandleMessage,
},
};
// Parse the message and call all CTCP handlers on the message.
function ctcpHandleMessage(message, ircHandlers) {
// If there are no CTCP handlers, then don't parse the CTCP message.
if (!ircHandlers.hasCTCPHandlers) {
return false;
}
// The raw CTCP message is in the last parameter of the IRC message.
let rawCTCPParam = message.params.slice(-1)[0];
// Split the raw message into the multiple CTCP messages and pull out the
// command and parameters.
let ctcpMessages = [];
let otherMessage = rawCTCPParam.replace(
// eslint-disable-next-line no-control-regex
/\x01([^\x01]*)\x01/g,
function (aMatch, aMsg) {
if (aMsg) {
ctcpMessages.push(new CTCPMessage(message, aMsg));
}
return "";
}
);
// If no CTCP messages were found, return false.
if (!ctcpMessages.length) {
return false;
}
// If there's some message left, send it back through the IRC handlers after
// stripping out the CTCP information. I highly doubt this will ever happen,
// but just in case. ;)
if (otherMessage) {
message.params.pop();
message.params.push(otherMessage);
ircHandlers.handleMessage(this, message);
}
// Loop over each raw CTCP message.
for (let message of ctcpMessages) {
if (!ircHandlers.handleCTCPMessage(this, message)) {
this.WARN(
"Unhandled CTCP message: " +
message.ctcp.rawMessage +
"\nin IRC message: " +
message.rawMessage
);
// For unhandled CTCP message, respond with a NOTICE ERRMSG that echoes
// back the original command.
this.sendCTCPMessage(message.origin, true, "ERRMSG", [
message.ctcp.rawMessage,
":Unhandled CTCP command",
]);
}
}
// We have handled this message as much as we can.
return true;
}
// This is the the basic CTCP protocol.
export var ctcpBase = {
// Parameters
name: "CTCP",
priority: ircHandlerPriorities.DEFAULT_PRIORITY,
isEnabled: () => true,
// These represent CTCP commands.
commands: {
ACTION(aMessage) {
// ACTION <text>
// Display message in conversation
return displayMessage(
this,
aMessage,
{ action: true },
aMessage.ctcp.param
);
},
// Used when an error needs to be replied with.
ERRMSG(aMessage) {
this.WARN(
aMessage.origin +
" failed to handle CTCP message: " +
aMessage.ctcp.param
);
return true;
},
// This is commented out since CLIENTINFO automatically returns the
// supported CTCP parameters and this is not supported.
// Returns the user's full name, and idle time.
// "FINGER": function(aMessage) { return false; },
// Dynamic master index of what a client knows.
CLIENTINFO(message, ircHandlers) {
if (message.command == "PRIVMSG") {
// Received a CLIENTINFO request, respond with the support CTCP
// messages.
let info = new Set();
for (let handler of ircHandlers._ctcpHandlers) {
for (let command in handler.commands) {
info.add(command);
}
}
let supportedCtcp = [...info].join(" ");
this.LOG(
"Reporting support for the following CTCP messages: " + supportedCtcp
);
this.sendCTCPMessage(message.origin, true, "CLIENTINFO", supportedCtcp);
} else {
// Received a CLIENTINFO response, store the information for future
// use.
let info = message.ctcp.param.split(" ");
this.setWhois(message.origin, { clientInfo: info });
}
return true;
},
// Used to measure the delay of the IRC network between clients.
PING(aMessage) {
// PING timestamp
if (aMessage.command == "PRIVMSG") {
// Received PING request, send PING response.
this.LOG(
"Received PING request from " +
aMessage.origin +
'. Sending PING response: "' +
aMessage.ctcp.param +
'".'
);
this.sendCTCPMessage(
aMessage.origin,
true,
"PING",
aMessage.ctcp.param
);
return true;
}
return this.handlePingReply(aMessage.origin, aMessage.ctcp.param);
},
// These are commented out since CLIENTINFO automatically returns the
// supported CTCP parameters and this is not supported.
// An encryption protocol between clients without any known reference.
// "SED": function(aMessage) { return false; },
// Where to obtain a copy of a client.
// "SOURCE": function(aMessage) { return false; },
// Gets the local date and time from other clients.
TIME(aMessage) {
if (aMessage.command == "PRIVMSG") {
// TIME
// Received a TIME request, send a human readable response.
let now = new Date().toString();
this.LOG(
"Received TIME request from " +
aMessage.origin +
'. Sending TIME response: "' +
now +
'".'
);
this.sendCTCPMessage(aMessage.origin, true, "TIME", ":" + now);
} else {
// TIME :<human-readable-time-string>
// Received a TIME reply, display it.
// Remove the : prefix, if it exists and display the result.
let time = aMessage.ctcp.param.slice(aMessage.ctcp.param[0] == ":");
this.getConversation(aMessage.origin).writeMessage(
aMessage.origin,
lazy._("ctcp.time", aMessage.origin, time),
{ system: true, tags: aMessage.tags }
);
}
return true;
},
// This is commented out since CLIENTINFO automatically returns the
// supported CTCP parameters and this is not supported.
// A string set by the user (never the client coder)
// "USERINFO": function(aMessage) { return false; },
// The version and type of the client.
VERSION(aMessage) {
if (aMessage.command == "PRIVMSG") {
// VERSION
// Received VERSION request, send VERSION response.
let version = Services.appinfo.name + " " + Services.appinfo.version;
this.LOG(
"Received VERSION request from " +
aMessage.origin +
'. Sending VERSION response: "' +
version +
'".'
);
this.sendCTCPMessage(aMessage.origin, true, "VERSION", version);
} else if (aMessage.command == "NOTICE" && aMessage.ctcp.param.length) {
// VERSION #:#:#
// Received VERSION response, display to the user.
let response = lazy._(
"ctcp.version",
aMessage.origin,
aMessage.ctcp.param
);
this.getConversation(aMessage.origin).writeMessage(
aMessage.origin,
response,
{
system: true,
tags: aMessage.tags,
}
);
}
return true;
},
},
};
|