Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/clientapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ new Irc.Client({
version: 'node.js irc-framework',
enable_chghost: false,
enable_echomessage: false,
enable_multiline: false,
auto_reconnect: true,
auto_reconnect_max_wait: 300000,
auto_reconnect_max_retries: 3,
Expand Down Expand Up @@ -103,6 +104,14 @@ be emitted as a `labeled response` event.
##### `.notice(target, message [, tags [, options]])`
Send a notice to the target, optionally with tags. Accepts the same options as `.say()`.

##### `.sayMultiline(target, lines [, tags])`
##### `.noticeMultiline(target, lines [, tags])`
Send `lines` to `target` as a single
[`draft/multiline`](https://ircv3.net/specs/extensions/multiline) batch when the
network supports the capability. Falls

Requires `enable_multiline: true`.

##### `.tagmsg(target, tags)`
Send a tagged message without content to the target

Expand Down
3 changes: 2 additions & 1 deletion docs/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,8 @@ Also triggers a **message** event with .type = 'privmsg'
message: 'Hello everybody',
tags: [],
time: 000000000,
account: 'account_name'
account: 'account_name',
multiline: false
}
~~~

Expand Down
55 changes: 35 additions & 20 deletions docs/ircv3.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,50 @@
### IRCv3 Support

#### IRCv3.1 support
* CAP
* sasl
* multi-prefix
* account-notify
* away-notify
* extended-join

- CAP
- sasl
- multi-prefix
- account-notify
- away-notify
- extended-join

#### IRCv3.2 support
* CAP
* account-tag
* batch
* chghost
* echo-message
* invite-notify
* sasl
* server-time
* userhost-in-names
* message-tags
* labeled-response

- CAP
- account-tag
- batch
- chghost
- echo-message
- invite-notify
- sasl
- server-time
- userhost-in-names
- message-tags
- labeled-response

#### Drafts

- draft/multiline

#### Extra notes
* chghost

- chghost

Only enabled if the client `enable_chghost` option is `true`. Clients may need to specifically handle this to update their state if the username or hostname suddenly changes.

* echo-message
- echo-message

Only enabled if the client `enable_echomessage` option is `true`. Clients may not be expecting their own messages being echoed back by default so it must be enabled manually.

* labeled-response
- labeled-response

Automatically enabled when `enable_echomessage` is `true` (requires batch, which is always requested). Pass `{ label: true }` as options to `say()`, `notice()`, or `action()` to attach a label. The server's response will trigger a `labeled response` event on the client with the matching label. See [events.md](events.md) for the event format.

- draft/multiline

Only enabled if the client `enable_multiline` option is `true` and the
network advertises the capability. Inbound batches are concatenated and emitted
as a single `privmsg` / `notice` event with `multiline: true` set, using the
prefix and tags from the `BATCH` start command. The parsed `max-bytes` /
`max-lines` limits are reachable via `client.network.multilineLimits()`.
90 changes: 90 additions & 0 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ const User = require('./user');
const Channel = require('./channel');
const { lineBreak } = require('./linebreak');
const MessageTags = require('./messagetags');
const { encode: encodeUTF8 } = require('isomorphic-textencoder');

let batch_reftag_counter = 0;
function generateBatchReftag() {
batch_reftag_counter = (batch_reftag_counter + 1) % 0xffffffff;
return Date.now().toString(36) + batch_reftag_counter.toString(36);
}

let default_transport = null;

Expand Down Expand Up @@ -549,6 +556,89 @@ module.exports = class IrcClient extends EventEmitter {
return this.sendMessage('NOTICE', target, message, tags, options);
}

sendMultiline(commandName, target, lines, tags) {
const limits = this.network.multilineLimits();

if (!limits) {
// as a fallback, split into individual lines
for (let i = 0; i < lines.length; i++) {
this.sendMessage(commandName, target, lines[i], tags);
}
return;
}

const frames = [];
let totalBytes = 0;
const maxBytes = this.options.message_max_length;

for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (i > 0) {
// \n separator between logical lines counts toward max-bytes per spec
totalBytes += 1;
}

if (line === '') {
frames.push({ message: '', concat: false });
continue;
}

const blocks = [...lineBreak(line, {
bytes: maxBytes,
allowBreakingWords: true,
allowBreakingGraphemes: true,
})];

blocks.forEach((block, blockIdx) => {
frames.push({
message: block,
concat: blockIdx > 0,
});
totalBytes += encodeUTF8(block).byteLength;
});
}

if (totalBytes > limits.maxBytes) {
const err = new Error('Multiline batch exceeds max-bytes limit');
err.code = 'MULTILINE_MAX_BYTES';
err.limit = limits.maxBytes;
throw err;
}
if (limits.maxLines !== null && frames.length > limits.maxLines) {
const err = new Error('Multiline batch exceeds max-lines limit');
err.code = 'MULTILINE_MAX_LINES';
err.limit = limits.maxLines;
throw err;
}

const reftag = generateBatchReftag();

const startMsg = new IrcMessage('BATCH', '+' + reftag, 'draft/multiline', target);
if (tags && Object.keys(tags).length) {
startMsg.tags = tags;
}
this.raw(startMsg);

frames.forEach((frame) => {
const msg = new IrcMessage(commandName, target, frame.message);
msg.tags = { batch: reftag };
if (frame.concat) {
msg.tags['draft/multiline-concat'] = true;
}
this.raw(msg);
});

this.raw('BATCH', '-' + reftag);
}

sayMultiline(target, lines, tags) {
return this.sendMultiline('PRIVMSG', target, lines, tags);
}

noticeMultiline(target, lines, tags) {
return this.sendMultiline('NOTICE', target, lines, tags);
}

tagmsg(target, tags = {}) {
const msg = new IrcMessage('TAGMSG', target);
msg.tags = tags;
Expand Down
2 changes: 2 additions & 0 deletions src/commands/handlers/messaging.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const handlers = {
account: command.getTag('account'),
batch: command.batch,
label: command.label || command.getTag('label') || undefined,
multiline: !!command.multiline,
});
}
},
Expand Down Expand Up @@ -115,6 +116,7 @@ const handlers = {
account: command.getTag('account'),
batch: command.batch,
label: command.label || command.getTag('label') || undefined,
multiline: !!command.multiline,
});
}
},
Expand Down
110 changes: 99 additions & 11 deletions src/commands/handlers/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const _ = {
map: require('lodash/map'),
};
const Helpers = require('../../helpers');
const IrcCommand = require('../command');

const handlers = {
RPL_LISTSTART: function(command, handler) {
Expand Down Expand Up @@ -338,6 +339,15 @@ const handlers = {
cache.label = label;
}

// https://ircv3.net/specs/extensions/multiline#message-tags-spec
if (cache.type === 'draft/multiline') {
cache.tags = command.tags;
cache.prefix = command.prefix;
cache.nick = command.nick;
cache.ident = command.ident;
cache.hostname = command.hostname;
}

return;
}

Expand Down Expand Up @@ -372,17 +382,22 @@ const handlers = {

handler.emit('batch start', emit_obj);
handler.emit('batch start ' + emit_obj.type, emit_obj);
emit_obj.commands.forEach((c) => {
c.batch = {
id: batch_id,
type: cache.type,
params: cache.params
};
if (emit_obj.label) {
c.label = emit_obj.label;
}
handler.executeCommand(c);
});
if (cache.type === 'draft/multiline') {
handleMultilineBatch(emit_obj, cache, handler);
} else {
emit_obj.commands.forEach((c) => {
c.batch = {
id: batch_id,
type: cache.type,
params: cache.params
};
if (emit_obj.label) {
c.label = emit_obj.label;
}
handler.executeCommand(c);
});
}

handler.emit('batch end', emit_obj);
handler.emit('batch end ' + emit_obj.type, emit_obj);
}
Expand Down Expand Up @@ -417,3 +432,76 @@ function getChanListCache(handler) {

return cache;
}

// https://ircv3.net/specs/extensions/multiline
function handleMultilineBatch(emit_obj, cache, handler) {
const lines = emit_obj.commands;

if (lines.length === 0) {
return;
}

// Use same command (PRIVMSG or NOTICE) and target for all lines
const first = lines[0];
const lineCommand = first.command;
if (lineCommand !== 'PRIVMSG' && lineCommand !== 'NOTICE') {
return;
}

const target = first.params[0];

let allBlank = true;
for (let i = 0; i < lines.length; i++) {
const c = lines[i];
if (c.command !== lineCommand) {
return;
}
if (c.params[0] !== target) {
return;
}
const msg = c.params[c.params.length - 1];
const isBlank = msg === '';
if (!isBlank) {
allBlank = false;
}
if (isBlank && c.tags && c.tags['draft/multiline-concat'] !== undefined) {
// https://ircv3.net/specs/extensions/multiline#batch-types
// "Clients MUST NOT send blank lines with the draft/multiline-concat tag.""
return;
}
}

if (allBlank) {
return;
}

let combined = '';
for (let i = 0; i < lines.length; i++) {
const c = lines[i];
const msg = c.params[c.params.length - 1];
if (i === 0) {
combined = msg;
} else if (c.tags && c.tags['draft/multiline-concat'] !== undefined) {
combined += msg;
} else {
combined += '\n' + msg;
}
}

const tmp = new IrcCommand(lineCommand, {
params: [target, combined],
tags: cache.tags || Object.create(null),
prefix: cache.prefix !== undefined ? cache.prefix : first.prefix,
nick: cache.nick !== undefined ? cache.nick : first.nick,
ident: cache.ident !== undefined ? cache.ident : first.ident,
hostname: cache.hostname !== undefined ? cache.hostname : first.hostname,
});
tmp.batch = {
id: emit_obj.id,
type: emit_obj.type,
params: emit_obj.params,
};
tmp.multiline = true;

handler.executeCommand(tmp);
}
3 changes: 3 additions & 0 deletions src/commands/handlers/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ const handlers = {
// useful alongside echo-message for correlating sent messages
want.push('labeled-response');
}
if (handler.connection.options.enable_multiline) {
want.push('draft/multiline');
}

want = _.uniq(want.concat(handler.request_extra_caps));

Expand Down
Loading
Loading