Skip to content

Commit 4ec490e

Browse files
authored
Merge pull request #262 from kiwiirc/casefolding
Add CASEMAPPING and case folding functions
2 parents 8dac6dc + dedb0f9 commit 4ec490e

File tree

8 files changed

+226
-17
lines changed

8 files changed

+226
-17
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ function ExampleMiddleware() {
8383
}
8484
}
8585

86-
if (command === 'message' && event.event.nick.toLowerCase() === 'nickserv') {
86+
if (command === 'message' && client.caseCompare(event.event.nick, 'nickserv')) {
8787
// Handle success/retries/failures
8888
}
8989

docs/clientapi.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,15 @@ Create a channel object with the following methods:
131131
* `part([part_message])`
132132
* `join([key])`
133133

134+
##### `.caseCompare(string1, string2)`
135+
Compare two strings using the networks casemapping setting.
136+
137+
##### `.caseUpper(string)`
138+
Uppercase the characters in string using the networks casemapping setting.
139+
140+
##### `.caseLower(string)`
141+
Lowercase the characters in string using the networks casemapping setting.
142+
134143
##### `.match(match_regex, cb[, message_type])`
135144
Call `cb()` when any incoming message matches `match_regex`.
136145

examples/bot.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function NickservMiddleware() { // eslint-disable-line
1717
}
1818
}
1919

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

src/channel.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ module.exports = class IrcChannel {
3030

3131
this.users = [];
3232
irc_client.on('userlist', (event) => {
33-
if (event.channel.toLowerCase() === this.name.toLowerCase()) {
33+
if (irc_client.caseCompare(event.channel, this.name)) {
3434
this.users = event.users;
3535
}
3636
});
@@ -42,25 +42,25 @@ module.exports = class IrcChannel {
4242
irc_client.on('part', (event) => {
4343
if (event.channel === this.name) {
4444
this.users = _.filter(this.users, function(o) {
45-
return o.nick.toLowerCase() !== event.nick.toLowerCase();
45+
return !irc_client.caseCompare(event.nick, o.nick);
4646
});
4747
}
4848
});
4949
irc_client.on('kick', (event) => {
5050
if (event.channel === this.name) {
5151
this.users = _.filter(this.users, function(o) {
52-
return o.nick.toLowerCase() !== event.kicked.toLowerCase();
52+
return !irc_client.caseCompare(event.kicked, o.nick);
5353
});
5454
}
5555
});
5656
irc_client.on('quit', (event) => {
5757
this.users = _.filter(this.users, function(o) {
58-
return o.nick.toLowerCase() !== event.nick.toLowerCase();
58+
return !irc_client.caseCompare(event.nick, o.nick);
5959
});
6060
});
6161
irc_client.on('nick', (event) => {
6262
_.find(this.users, function(o) {
63-
if (o.nick.toLowerCase() === event.nick.toLowerCase()) {
63+
if (irc_client.caseCompare(event.nick, o.nick)) {
6464
o.nick = event.new_nick;
6565
return true;
6666
}
@@ -76,7 +76,7 @@ module.exports = class IrcChannel {
7676
}
7777
*/
7878

79-
if (event.target.toLowerCase() !== this.name.toLowerCase()) {
79+
if (irc_client.caseCompare(event.target, this.name)) {
8080
return;
8181
}
8282

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

9999
if (!user) {
@@ -175,7 +175,7 @@ module.exports = class IrcChannel {
175175
});
176176

177177
this.irc_client.on('privmsg', (event) => {
178-
if (event.target.toLowerCase() === this.name.toLowerCase()) {
178+
if (this.irc_client.caseCompare(event.target, this.name)) {
179179
read_queue.push(event);
180180

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

190190
updateUsers(cb) {
191191
const updateUserList = (event) => {
192-
if (event.channel.toLowerCase() === this.name.toLowerCase()) {
192+
if (this.irc_client.caseCompare(event.channel, this.name)) {
193193
this.irc_client.removeListener('userlist', updateUserList);
194194
if (typeof cb === 'function') { cb(this); }
195195
}

src/client.js

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,13 @@ module.exports = class IrcClient extends EventEmitter {
127127
});
128128

129129
client.on('away', function(event) {
130-
if (event.nick.toLowerCase() === client.user.nick.toLowerCase()) {
130+
if (client.caseCompare(event.nick, client.user.nick)) {
131131
client.user.away = true;
132132
}
133133
});
134134

135135
client.on('back', function(event) {
136-
if (event.nick.toLowerCase() === client.user.nick.toLowerCase()) {
136+
if (client.caseCompare(event.nick, client.user.nick)) {
137137
client.user.away = false;
138138
}
139139
});
@@ -461,7 +461,7 @@ module.exports = class IrcClient extends EventEmitter {
461461
}
462462

463463
function onInviteList(event) {
464-
if (event.channel.toLowerCase() === channel.toLowerCase()) {
464+
if (client.caseCompare(event.channel, channel)) {
465465
unbindEvents();
466466
if (typeof cb === 'function') {
467467
cb(event);
@@ -524,7 +524,7 @@ module.exports = class IrcClient extends EventEmitter {
524524
const raw = ['MODE', channel, 'b'];
525525

526526
this.on('banlist', function onBanlist(event) {
527-
if (event.channel.toLowerCase() === channel.toLowerCase()) {
527+
if (client.caseCompare(event.channel, channel)) {
528528
client.removeListener('banlist', onBanlist);
529529
if (typeof cb === 'function') {
530530
cb(event);
@@ -611,7 +611,7 @@ module.exports = class IrcClient extends EventEmitter {
611611
});
612612

613613
this.on('whois', function onWhois(event) {
614-
if (event.nick.toLowerCase() === target.toLowerCase()) {
614+
if (client.caseCompare(event.nick, target)) {
615615
client.removeListener('whois', onWhois);
616616
if (typeof cb === 'function') {
617617
cb(event);
@@ -637,7 +637,7 @@ module.exports = class IrcClient extends EventEmitter {
637637
});
638638

639639
this.on('whowas', function onWhowas(event) {
640-
if (event.nick.toLowerCase() === target.toLowerCase()) {
640+
if (client.caseCompare(event.nick, target)) {
641641
client.removeListener('whowas', onWhowas);
642642
if (typeof cb === 'function') {
643643
cb(event);
@@ -755,4 +755,85 @@ module.exports = class IrcClient extends EventEmitter {
755755
matchAction(match_regex, cb) {
756756
return this.match(match_regex, cb, 'action');
757757
}
758+
759+
caseCompare(string1, string2) {
760+
const length = string1.length;
761+
762+
if (length !== string2.length) {
763+
return false;
764+
}
765+
766+
const upperBound = this._getCaseMappingUpperAsciiBound();
767+
768+
for (let i = 0; i < length; i++) {
769+
let charCode1 = string1.charCodeAt(i);
770+
let charCode2 = string2.charCodeAt(i);
771+
772+
if (charCode1 >= 65 && charCode1 <= upperBound) {
773+
charCode1 += 32;
774+
}
775+
776+
if (charCode2 >= 65 && charCode2 <= upperBound) {
777+
charCode2 += 32;
778+
}
779+
780+
if (charCode1 !== charCode2) {
781+
return false;
782+
}
783+
}
784+
785+
return true;
786+
}
787+
788+
caseLower(string) {
789+
const upperBound = this._getCaseMappingUpperAsciiBound();
790+
let result = '';
791+
792+
for (let i = 0; i < string.length; i++) {
793+
const charCode = string.charCodeAt(i);
794+
795+
// ASCII character from 'A' to upper bound defined above
796+
if (charCode >= 65 && charCode <= upperBound) {
797+
// All the relevant uppercase characters are exactly
798+
// 32 bytes apart from lowercase ones, so we simply add 32
799+
// and get the equivalent character in lower case
800+
result += String.fromCharCode(charCode + 32);
801+
} else {
802+
result += string[i];
803+
}
804+
}
805+
806+
return result;
807+
}
808+
809+
caseUpper(string) {
810+
const upperBound = this._getCaseMappingUpperAsciiBound() + 32;
811+
let result = '';
812+
813+
for (let i = 0; i < string.length; i++) {
814+
const charCode = string.charCodeAt(i);
815+
816+
// ASCII character from 'a' to upper bound defined above
817+
if (charCode >= 97 && charCode <= upperBound) {
818+
// All the relevant lowercase characters are exactly
819+
// 32 bytes apart from lowercase ones, so we simply subtract 32
820+
// and get the equivalent character in upper case
821+
result += String.fromCharCode(charCode - 32);
822+
} else {
823+
result += string[i];
824+
}
825+
}
826+
827+
return result;
828+
}
829+
830+
_getCaseMappingUpperAsciiBound() {
831+
if (this.network.options.CASEMAPPING === 'ascii') {
832+
return 90; // 'Z'
833+
} else if (this.network.options.CASEMAPPING === 'strict-rfc1459') {
834+
return 93; // ']'
835+
}
836+
837+
return 94; // '^' - default casemapping=rfc1459
838+
}
758839
};

src/commands/handlers/registration.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ const handlers = {
7272
handler.network.options.STATUSMSG = handler.network.options.STATUSMSG.split('');
7373
} else if (option[0] === 'CHANMODES') {
7474
handler.network.options.CHANMODES = option[1].split(',');
75+
} else if (option[0] === 'CASEMAPPING') {
76+
handler.network.options.CASEMAPPING = option[1];
7577
} else if (option[0] === 'NETWORK') {
7678
handler.network.name = option[1];
7779
} else if (option[0] === 'NAMESX' && !handler.network.cap.isEnabled('multi-prefix')) {

src/networkinfo.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ function NetworkInfo() {
1818

1919
// Network provided options
2020
this.options = {
21+
CASEMAPPING: 'rfc1459',
2122
PREFIX: [
2223
{ symbol: '~', mode: 'q' },
2324
{ symbol: '&', mode: 'a' },

test/casefolding.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
'use strict';
2+
/* globals describe, it */
3+
const chai = require('chai');
4+
const IrcClient = require('../src/client');
5+
const expect = chai.expect;
6+
7+
chai.use(require('chai-subset'));
8+
9+
describe('src/client.js', function() {
10+
describe('caseLower', function() {
11+
it('CASEMAPPING=rfc1459', function() {
12+
const client = new IrcClient();
13+
14+
expect(client.network.options.CASEMAPPING).to.equal('rfc1459'); // default
15+
expect(client.caseLower('ABCDEFGHIJKLMNOPQRSTUVWXYZ')).to.equal('abcdefghijklmnopqrstuvwxyz');
16+
expect(client.caseLower('ÀTEST[]^\\')).to.equal('Àtest{}~|');
17+
expect(client.caseLower('Àtest{}~|')).to.equal('Àtest{}~|');
18+
expect(client.caseLower('@?A_`#&')).to.equal('@?a_`#&');
19+
});
20+
21+
it('CASEMAPPING=strict-rfc1459', function() {
22+
const client = new IrcClient();
23+
client.network.options.CASEMAPPING = 'strict-rfc1459';
24+
25+
expect(client.caseLower('ABCDEFGHIJKLMNOPQRSTUVWXYZ')).to.equal('abcdefghijklmnopqrstuvwxyz');
26+
expect(client.caseLower('ÀTEST[]^\\')).to.equal('Àtest{}^|');
27+
expect(client.caseLower('Àtest{}^|')).to.equal('Àtest{}^|');
28+
expect(client.caseLower('@?A^_`#&')).to.equal('@?a^_`#&');
29+
});
30+
31+
it('CASEMAPPING=ascii', function() {
32+
const client = new IrcClient();
33+
client.network.options.CASEMAPPING = 'ascii';
34+
35+
expect(client.caseLower('ABCDEFGHIJKLMNOPQRSTUVWXYZ')).to.equal('abcdefghijklmnopqrstuvwxyz');
36+
expect(client.caseLower('ÀTEST[]^\\{}~|#&')).to.equal('Àtest[]^\\{}~|#&');
37+
expect(client.caseLower('ПРИВЕТ, как дела? 👋')).to.equal('ПРИВЕТ, как дела? 👋');
38+
});
39+
});
40+
41+
describe('caseUpper', function() {
42+
it('CASEMAPPING=rfc1459', function() {
43+
const client = new IrcClient();
44+
45+
expect(client.network.options.CASEMAPPING).to.equal('rfc1459'); // default
46+
expect(client.caseUpper('abcdefghijklmnopqrstuvwxyz')).to.equal('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
47+
expect(client.caseUpper('ÀTEST{}~|')).to.equal('ÀTEST[]^\\');
48+
expect(client.caseUpper('ÀTEST[]^\\')).to.equal('ÀTEST[]^\\');
49+
expect(client.caseUpper('@?a_`#&')).to.equal('@?A_`#&');
50+
});
51+
52+
it('CASEMAPPING=strict-rfc1459', function() {
53+
const client = new IrcClient();
54+
client.network.options.CASEMAPPING = 'strict-rfc1459';
55+
56+
expect(client.caseUpper('abcdefghijklmnopqrstuvwxyz')).to.equal('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
57+
expect(client.caseUpper('ÀTEST{}~|')).to.equal('ÀTEST[]~\\');
58+
expect(client.caseUpper('ÀTEST[]^\\')).to.equal('ÀTEST[]^\\');
59+
expect(client.caseUpper('@?a^~_`#&')).to.equal('@?A^~_`#&');
60+
});
61+
62+
it('CASEMAPPING=ascii', function() {
63+
const client = new IrcClient();
64+
client.network.options.CASEMAPPING = 'ascii';
65+
66+
expect(client.caseUpper('abcdefghijklmnopqrstuvwxyz')).to.equal('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
67+
expect(client.caseUpper('Àtest[]^\\{}~|#&')).to.equal('ÀTEST[]^\\{}~|#&');
68+
expect(client.caseUpper('ПРИВЕТ, как дела? 👋')).to.equal('ПРИВЕТ, как дела? 👋');
69+
});
70+
});
71+
72+
/* eslint-disable no-unused-expressions */
73+
describe('caseCompare', function() {
74+
it('CASEMAPPING=rfc1459', function() {
75+
const client = new IrcClient();
76+
77+
expect(client.network.options.CASEMAPPING).to.equal('rfc1459'); // default
78+
79+
expect(client.caseCompare('abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')).to.be.true;
80+
expect(client.caseCompare('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')).to.be.true;
81+
expect(client.caseCompare('Àtest{}~|', 'ÀTEST[]^\\')).to.be.true;
82+
expect(client.caseCompare('ÀTEST[]^\\', 'Àtest{}~|')).to.be.true;
83+
expect(client.caseCompare('Àtest{}~|', 'Àtest{}~|')).to.be.true;
84+
expect(client.caseCompare('@?A_`#&', '@?a_`#&')).to.be.true;
85+
});
86+
87+
it('CASEMAPPING=strict-rfc1459', function() {
88+
const client = new IrcClient();
89+
client.network.options.CASEMAPPING = 'strict-rfc1459';
90+
91+
expect(client.caseCompare('abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')).to.be.true;
92+
expect(client.caseCompare('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')).to.be.true;
93+
expect(client.caseCompare('Àtest{}^|', 'ÀTEST[]^\\')).to.be.true;
94+
expect(client.caseCompare('ÀTEST[]^\\', 'Àtest{}^|')).to.be.true;
95+
expect(client.caseCompare('Àtest{}^|', 'Àtest{}^|')).to.be.true;
96+
expect(client.caseCompare('@?A^_`#&', '@?a^_`#&')).to.be.true;
97+
});
98+
99+
it('CASEMAPPING=ascii', function() {
100+
const client = new IrcClient();
101+
client.network.options.CASEMAPPING = 'ascii';
102+
103+
expect(client.caseCompare('abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')).to.be.true;
104+
expect(client.caseCompare('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')).to.be.true;
105+
expect(client.caseCompare('Àtest[]^\\{}~|#&', 'ÀTEST[]^\\{}~|#&')).to.be.true;
106+
expect(client.caseCompare('ÀTEST[]^\\{}~|#&', 'Àtest[]^\\{}~|#&')).to.be.true;
107+
expect(client.caseCompare('ПРИВЕТ, как дела? 👋', 'ПРИВЕТ, как дела? 👋')).to.be.true;
108+
expect(client.caseCompare('#HELLO1', '#HELLO2')).to.be.false;
109+
expect(client.caseCompare('#HELLO', '#HELLO2')).to.be.false;
110+
expect(client.caseCompare('#HELLO', '#HELL')).to.be.false;
111+
expect(client.caseCompare('#HELL', '#HELLO')).to.be.false;
112+
expect(client.caseCompare('#HELLOZ', '#HELLOZ')).to.be.true;
113+
expect(client.caseCompare('#HELLOZ[', '#HELLOZ{')).to.be.false;
114+
});
115+
});
116+
});

0 commit comments

Comments
 (0)