Skip to content

Commit

Permalink
Merge pull request #262 from kiwiirc/casefolding
Browse files Browse the repository at this point in the history
Add CASEMAPPING and case folding functions
  • Loading branch information
prawnsalad committed Mar 7, 2021
2 parents 8dac6dc + dedb0f9 commit 4ec490e
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 17 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ function ExampleMiddleware() {
}
}

if (command === 'message' && event.event.nick.toLowerCase() === 'nickserv') {
if (command === 'message' && client.caseCompare(event.event.nick, 'nickserv')) {
// Handle success/retries/failures
}

Expand Down
9 changes: 9 additions & 0 deletions docs/clientapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,15 @@ Create a channel object with the following methods:
* `part([part_message])`
* `join([key])`

##### `.caseCompare(string1, string2)`
Compare two strings using the networks casemapping setting.

##### `.caseUpper(string)`
Uppercase the characters in string using the networks casemapping setting.

##### `.caseLower(string)`
Lowercase the characters in string using the networks casemapping setting.

##### `.match(match_regex, cb[, message_type])`
Call `cb()` when any incoming message matches `match_regex`.

Expand Down
2 changes: 1 addition & 1 deletion examples/bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function NickservMiddleware() { // eslint-disable-line
}
}

if (command === 'PRIVMSG' && event.params[0].toLowerCase() === 'nickserv') {
if (command === 'PRIVMSG' && client.caseCompare(event.params[0], 'nickserv')) {
// Handle success/retries/failures
}

Expand Down
18 changes: 9 additions & 9 deletions src/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ module.exports = class IrcChannel {

this.users = [];
irc_client.on('userlist', (event) => {
if (event.channel.toLowerCase() === this.name.toLowerCase()) {
if (irc_client.caseCompare(event.channel, this.name)) {
this.users = event.users;
}
});
Expand All @@ -42,25 +42,25 @@ module.exports = class IrcChannel {
irc_client.on('part', (event) => {
if (event.channel === this.name) {
this.users = _.filter(this.users, function(o) {
return o.nick.toLowerCase() !== event.nick.toLowerCase();
return !irc_client.caseCompare(event.nick, o.nick);
});
}
});
irc_client.on('kick', (event) => {
if (event.channel === this.name) {
this.users = _.filter(this.users, function(o) {
return o.nick.toLowerCase() !== event.kicked.toLowerCase();
return !irc_client.caseCompare(event.kicked, o.nick);
});
}
});
irc_client.on('quit', (event) => {
this.users = _.filter(this.users, function(o) {
return o.nick.toLowerCase() !== event.nick.toLowerCase();
return !irc_client.caseCompare(event.nick, o.nick);
});
});
irc_client.on('nick', (event) => {
_.find(this.users, function(o) {
if (o.nick.toLowerCase() === event.nick.toLowerCase()) {
if (irc_client.caseCompare(event.nick, o.nick)) {
o.nick = event.new_nick;
return true;
}
Expand All @@ -76,7 +76,7 @@ module.exports = class IrcChannel {
}
*/

if (event.target.toLowerCase() !== this.name.toLowerCase()) {
if (irc_client.caseCompare(event.target, this.name)) {
return;
}

Expand All @@ -93,7 +93,7 @@ module.exports = class IrcChannel {
} else { // It's a user mode
// Find the user affected
const user = _.find(this.users, u =>
u.nick.toLowerCase() === mode.param.toLowerCase()
irc_client.caseCompare(u.nick, mode.param)
);

if (!user) {
Expand Down Expand Up @@ -175,7 +175,7 @@ module.exports = class IrcChannel {
});

this.irc_client.on('privmsg', (event) => {
if (event.target.toLowerCase() === this.name.toLowerCase()) {
if (this.irc_client.caseCompare(event.target, this.name)) {
read_queue.push(event);

if (is_reading) {
Expand All @@ -189,7 +189,7 @@ module.exports = class IrcChannel {

updateUsers(cb) {
const updateUserList = (event) => {
if (event.channel.toLowerCase() === this.name.toLowerCase()) {
if (this.irc_client.caseCompare(event.channel, this.name)) {
this.irc_client.removeListener('userlist', updateUserList);
if (typeof cb === 'function') { cb(this); }
}
Expand Down
93 changes: 87 additions & 6 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,13 @@ module.exports = class IrcClient extends EventEmitter {
});

client.on('away', function(event) {
if (event.nick.toLowerCase() === client.user.nick.toLowerCase()) {
if (client.caseCompare(event.nick, client.user.nick)) {
client.user.away = true;
}
});

client.on('back', function(event) {
if (event.nick.toLowerCase() === client.user.nick.toLowerCase()) {
if (client.caseCompare(event.nick, client.user.nick)) {
client.user.away = false;
}
});
Expand Down Expand Up @@ -461,7 +461,7 @@ module.exports = class IrcClient extends EventEmitter {
}

function onInviteList(event) {
if (event.channel.toLowerCase() === channel.toLowerCase()) {
if (client.caseCompare(event.channel, channel)) {
unbindEvents();
if (typeof cb === 'function') {
cb(event);
Expand Down Expand Up @@ -524,7 +524,7 @@ module.exports = class IrcClient extends EventEmitter {
const raw = ['MODE', channel, 'b'];

this.on('banlist', function onBanlist(event) {
if (event.channel.toLowerCase() === channel.toLowerCase()) {
if (client.caseCompare(event.channel, channel)) {
client.removeListener('banlist', onBanlist);
if (typeof cb === 'function') {
cb(event);
Expand Down Expand Up @@ -611,7 +611,7 @@ module.exports = class IrcClient extends EventEmitter {
});

this.on('whois', function onWhois(event) {
if (event.nick.toLowerCase() === target.toLowerCase()) {
if (client.caseCompare(event.nick, target)) {
client.removeListener('whois', onWhois);
if (typeof cb === 'function') {
cb(event);
Expand All @@ -637,7 +637,7 @@ module.exports = class IrcClient extends EventEmitter {
});

this.on('whowas', function onWhowas(event) {
if (event.nick.toLowerCase() === target.toLowerCase()) {
if (client.caseCompare(event.nick, target)) {
client.removeListener('whowas', onWhowas);
if (typeof cb === 'function') {
cb(event);
Expand Down Expand Up @@ -755,4 +755,85 @@ module.exports = class IrcClient extends EventEmitter {
matchAction(match_regex, cb) {
return this.match(match_regex, cb, 'action');
}

caseCompare(string1, string2) {
const length = string1.length;

if (length !== string2.length) {
return false;
}

const upperBound = this._getCaseMappingUpperAsciiBound();

for (let i = 0; i < length; i++) {
let charCode1 = string1.charCodeAt(i);
let charCode2 = string2.charCodeAt(i);

if (charCode1 >= 65 && charCode1 <= upperBound) {
charCode1 += 32;
}

if (charCode2 >= 65 && charCode2 <= upperBound) {
charCode2 += 32;
}

if (charCode1 !== charCode2) {
return false;
}
}

return true;
}

caseLower(string) {
const upperBound = this._getCaseMappingUpperAsciiBound();
let result = '';

for (let i = 0; i < string.length; i++) {
const charCode = string.charCodeAt(i);

// ASCII character from 'A' to upper bound defined above
if (charCode >= 65 && charCode <= upperBound) {
// All the relevant uppercase characters are exactly
// 32 bytes apart from lowercase ones, so we simply add 32
// and get the equivalent character in lower case
result += String.fromCharCode(charCode + 32);
} else {
result += string[i];
}
}

return result;
}

caseUpper(string) {
const upperBound = this._getCaseMappingUpperAsciiBound() + 32;
let result = '';

for (let i = 0; i < string.length; i++) {
const charCode = string.charCodeAt(i);

// ASCII character from 'a' to upper bound defined above
if (charCode >= 97 && charCode <= upperBound) {
// All the relevant lowercase characters are exactly
// 32 bytes apart from lowercase ones, so we simply subtract 32
// and get the equivalent character in upper case
result += String.fromCharCode(charCode - 32);
} else {
result += string[i];
}
}

return result;
}

_getCaseMappingUpperAsciiBound() {
if (this.network.options.CASEMAPPING === 'ascii') {
return 90; // 'Z'
} else if (this.network.options.CASEMAPPING === 'strict-rfc1459') {
return 93; // ']'
}

return 94; // '^' - default casemapping=rfc1459
}
};
2 changes: 2 additions & 0 deletions src/commands/handlers/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ const handlers = {
handler.network.options.STATUSMSG = handler.network.options.STATUSMSG.split('');
} else if (option[0] === 'CHANMODES') {
handler.network.options.CHANMODES = option[1].split(',');
} else if (option[0] === 'CASEMAPPING') {
handler.network.options.CASEMAPPING = option[1];
} else if (option[0] === 'NETWORK') {
handler.network.name = option[1];
} else if (option[0] === 'NAMESX' && !handler.network.cap.isEnabled('multi-prefix')) {
Expand Down
1 change: 1 addition & 0 deletions src/networkinfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function NetworkInfo() {

// Network provided options
this.options = {
CASEMAPPING: 'rfc1459',
PREFIX: [
{ symbol: '~', mode: 'q' },
{ symbol: '&', mode: 'a' },
Expand Down
116 changes: 116 additions & 0 deletions test/casefolding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
'use strict';
/* globals describe, it */
const chai = require('chai');
const IrcClient = require('../src/client');
const expect = chai.expect;

chai.use(require('chai-subset'));

describe('src/client.js', function() {
describe('caseLower', function() {
it('CASEMAPPING=rfc1459', function() {
const client = new IrcClient();

expect(client.network.options.CASEMAPPING).to.equal('rfc1459'); // default
expect(client.caseLower('ABCDEFGHIJKLMNOPQRSTUVWXYZ')).to.equal('abcdefghijklmnopqrstuvwxyz');
expect(client.caseLower('ÀTEST[]^\\')).to.equal('Àtest{}~|');
expect(client.caseLower('Àtest{}~|')).to.equal('Àtest{}~|');
expect(client.caseLower('@?A_`#&')).to.equal('@?a_`#&');
});

it('CASEMAPPING=strict-rfc1459', function() {
const client = new IrcClient();
client.network.options.CASEMAPPING = 'strict-rfc1459';

expect(client.caseLower('ABCDEFGHIJKLMNOPQRSTUVWXYZ')).to.equal('abcdefghijklmnopqrstuvwxyz');
expect(client.caseLower('ÀTEST[]^\\')).to.equal('Àtest{}^|');
expect(client.caseLower('Àtest{}^|')).to.equal('Àtest{}^|');
expect(client.caseLower('@?A^_`#&')).to.equal('@?a^_`#&');
});

it('CASEMAPPING=ascii', function() {
const client = new IrcClient();
client.network.options.CASEMAPPING = 'ascii';

expect(client.caseLower('ABCDEFGHIJKLMNOPQRSTUVWXYZ')).to.equal('abcdefghijklmnopqrstuvwxyz');
expect(client.caseLower('ÀTEST[]^\\{}~|#&')).to.equal('Àtest[]^\\{}~|#&');
expect(client.caseLower('ПРИВЕТ, как дела? 👋')).to.equal('ПРИВЕТ, как дела? 👋');
});
});

describe('caseUpper', function() {
it('CASEMAPPING=rfc1459', function() {
const client = new IrcClient();

expect(client.network.options.CASEMAPPING).to.equal('rfc1459'); // default
expect(client.caseUpper('abcdefghijklmnopqrstuvwxyz')).to.equal('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
expect(client.caseUpper('ÀTEST{}~|')).to.equal('ÀTEST[]^\\');
expect(client.caseUpper('ÀTEST[]^\\')).to.equal('ÀTEST[]^\\');
expect(client.caseUpper('@?a_`#&')).to.equal('@?A_`#&');
});

it('CASEMAPPING=strict-rfc1459', function() {
const client = new IrcClient();
client.network.options.CASEMAPPING = 'strict-rfc1459';

expect(client.caseUpper('abcdefghijklmnopqrstuvwxyz')).to.equal('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
expect(client.caseUpper('ÀTEST{}~|')).to.equal('ÀTEST[]~\\');
expect(client.caseUpper('ÀTEST[]^\\')).to.equal('ÀTEST[]^\\');
expect(client.caseUpper('@?a^~_`#&')).to.equal('@?A^~_`#&');
});

it('CASEMAPPING=ascii', function() {
const client = new IrcClient();
client.network.options.CASEMAPPING = 'ascii';

expect(client.caseUpper('abcdefghijklmnopqrstuvwxyz')).to.equal('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
expect(client.caseUpper('Àtest[]^\\{}~|#&')).to.equal('ÀTEST[]^\\{}~|#&');
expect(client.caseUpper('ПРИВЕТ, как дела? 👋')).to.equal('ПРИВЕТ, как дела? 👋');
});
});

/* eslint-disable no-unused-expressions */
describe('caseCompare', function() {
it('CASEMAPPING=rfc1459', function() {
const client = new IrcClient();

expect(client.network.options.CASEMAPPING).to.equal('rfc1459'); // default

expect(client.caseCompare('abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')).to.be.true;
expect(client.caseCompare('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')).to.be.true;
expect(client.caseCompare('Àtest{}~|', 'ÀTEST[]^\\')).to.be.true;
expect(client.caseCompare('ÀTEST[]^\\', 'Àtest{}~|')).to.be.true;
expect(client.caseCompare('Àtest{}~|', 'Àtest{}~|')).to.be.true;
expect(client.caseCompare('@?A_`#&', '@?a_`#&')).to.be.true;
});

it('CASEMAPPING=strict-rfc1459', function() {
const client = new IrcClient();
client.network.options.CASEMAPPING = 'strict-rfc1459';

expect(client.caseCompare('abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')).to.be.true;
expect(client.caseCompare('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')).to.be.true;
expect(client.caseCompare('Àtest{}^|', 'ÀTEST[]^\\')).to.be.true;
expect(client.caseCompare('ÀTEST[]^\\', 'Àtest{}^|')).to.be.true;
expect(client.caseCompare('Àtest{}^|', 'Àtest{}^|')).to.be.true;
expect(client.caseCompare('@?A^_`#&', '@?a^_`#&')).to.be.true;
});

it('CASEMAPPING=ascii', function() {
const client = new IrcClient();
client.network.options.CASEMAPPING = 'ascii';

expect(client.caseCompare('abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')).to.be.true;
expect(client.caseCompare('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')).to.be.true;
expect(client.caseCompare('Àtest[]^\\{}~|#&', 'ÀTEST[]^\\{}~|#&')).to.be.true;
expect(client.caseCompare('ÀTEST[]^\\{}~|#&', 'Àtest[]^\\{}~|#&')).to.be.true;
expect(client.caseCompare('ПРИВЕТ, как дела? 👋', 'ПРИВЕТ, как дела? 👋')).to.be.true;
expect(client.caseCompare('#HELLO1', '#HELLO2')).to.be.false;
expect(client.caseCompare('#HELLO', '#HELLO2')).to.be.false;
expect(client.caseCompare('#HELLO', '#HELL')).to.be.false;
expect(client.caseCompare('#HELL', '#HELLO')).to.be.false;
expect(client.caseCompare('#HELLOZ', '#HELLOZ')).to.be.true;
expect(client.caseCompare('#HELLOZ[', '#HELLOZ{')).to.be.false;
});
});
});

0 comments on commit 4ec490e

Please sign in to comment.