Skip to content
Open
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
2 changes: 1 addition & 1 deletion app/components/channel-nav.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
{{/if}}
{{else}}
<LinkTo @route="channel" @model={{channel}} class="hc-sidebar-item">
#&thinsp;{{channel.shortName}}
#&thinsp;{{channel.sidebarName}}
</LinkTo>
{{/if}}
<a {{on "click" (fn @onLeaveChannel channel)}} title="Leave {{channel.displayName}}"
Expand Down
20 changes: 14 additions & 6 deletions app/models/base_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,30 @@ export default class BaseChannel {
id = this.name;
break;
case 'IRC':
id = `${this.account.server.hostname}/${this.name}`;
// Use the same value as the internal `id` (name@host), so the
// Sockethub channel ID and the internal channel ID never diverge.
id = this.id;
break;
}
return id;
}

get slug () {
// This could be based on server type in the future. E.g. IRC would be
// server URL, while Campfire would be another id.
return this.id.replace(/#/g,'');
// Strip a single leading '#' only for single-hash IRC channels, so they
// keep clean URLs (e.g. #kosmos-dev -> kosmos-dev@server). Multi-hash
// channels preserve all '#' characters (e.g. ##kosmos-dev ->
// ##kosmos-dev@server, URL-encoded as %23%23kosmos-dev@server).
return this.id.replace(/^#(?=[^#])/, '');
}

get shortName () {
get sidebarName () {
switch (this.protocol) {
case 'IRC':
return this.name.replace(/#/g,'');
// Strip a single leading '#' so the sidebar (which already renders a
// leading '#') shows all hashes of the channel name, e.g.
// #kosmos-dev -> kosmos-dev (renders as "# kosmos-dev"),
// ##kosmos-dev -> #kosmos-dev (renders as "# #kosmos-dev").
return this.name.replace(/^#/, '');
case 'XMPP':
return this.name.match(/^(.+)@/)[1];
default:
Expand Down
9 changes: 7 additions & 2 deletions app/models/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@ export default class Channel extends BaseChannel {
}

get publicLogsBaseUrl () {
// TODO needs to get hostname from channel meta info for federated protocols
return `${config.publicLogs.defaultBaseUrl}/${this.account.server.hostname.toLowerCase()}/channels/${this.shortName}`;
// Mirror the slug's leading-# rule: strip a single leading '#' only for
// single-# channels (keeps existing log URLs working); preserve all '#' for
// multi-# channels and percent-encode them so they don't become URL fragments.
const channelName = this.name
.replace(/^#(?=[^#])/, '')
.replace(/#/g, '%23');
return `${config.publicLogs.defaultBaseUrl}/${this.account.server.hostname.toLowerCase()}/channels/${channelName}`;
}

}
8 changes: 4 additions & 4 deletions app/routes/base_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ export default class BaseChannelRoute extends Route {
}

model (params) {
let channel = this.coms.channels.find(ch => ch.slug === params.slug);
const slug = decodeURIComponent(params.slug);
let channel = this.coms.channels.find(ch => ch.slug === slug);
if (channel) return channel;

const channelId = decodeURIComponent(params.slug);
const domain = channelId.match(/@([^/]+)/)[1];
const domain = slug.match(/@([^/]+)/)[1];
const randomChannelForDomain = this.coms.channels.find(ch => ch.domain === domain);

if (randomChannelForDomain) {
channel = this.createChannelOrUserChannel(randomChannelForDomain.account, channelId);
channel = this.createChannelOrUserChannel(randomChannelForDomain.account, slug);
return channel;
} else {
const firstChannel = this.coms.channels.firstObject;
Expand Down
9 changes: 7 additions & 2 deletions app/routes/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ export default class ChannelRoute extends BaseChannel {
createChannelOrUserChannel (account, channelId) {
let channel;
switch(account.protocol) {
case 'IRC':
channel = this.coms.createChannel(account, '#'+channelId.match(/^(.+)@/)[1]);
case 'IRC': {
let name = channelId.match(/^(.+)@/)[1];
// Old/persisted slugs for single-hash channels have no leading '#';
// multi-hash slugs already carry their '#' characters.
if (!name.startsWith('#')) name = `#${name}`;
channel = this.coms.createChannel(account, name);
break;
}
case 'XMPP':
channel = this.coms.createChannel(account, channelId);
break;
Expand Down
2 changes: 1 addition & 1 deletion app/services/sockethub-irc.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export default class SockethubIrcService extends Service {
}

handlePresenceUpdate (message) {
const match = message.target.id.match(/(.+)\//);
const match = message.target.id.match(/@(.+)$/);
if (!match) { console.warn('Could not parse hostname from presence message', message); return; }

let channel = this.coms.channels.find(ch => ch.sockethubChannelId === message.target.id);
Expand Down
47 changes: 47 additions & 0 deletions tests/unit/models/base-channel-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,53 @@ module('Unit | Model | base-channel', function (hooks) {
assert.strictEqual(model.slug, 'kosmos-dev@irc.libera.chat');
});

test('#slug strips only one leading hash for multi-hash channels', function (assert) {
const model = new BaseChannel({
account: ircAccount,
name: '##kosmos-dev'
});

assert.strictEqual(model.slug, '##kosmos-dev@irc.libera.chat');
});

test('#id and #sockethubChannelId preserve all hashes for IRC channels', function (assert) {
let model = new BaseChannel({
account: ircAccount,
name: '#kosmos-dev'
});
assert.strictEqual(model.id, '#kosmos-dev@irc.libera.chat', 'id preserves single hash');
assert.strictEqual(model.sockethubChannelId, '#kosmos-dev@irc.libera.chat', 'sockethubChannelId matches id');

model = new BaseChannel({
account: ircAccount,
name: '##kosmos-dev'
});
assert.strictEqual(model.id, '##kosmos-dev@irc.libera.chat', 'id preserves all hashes');
assert.strictEqual(model.sockethubChannelId, '##kosmos-dev@irc.libera.chat', 'sockethubChannelId matches id');
});

test('#sidebarName (IRC) strips a single leading hash', function (assert) {
let model = new BaseChannel({
account: ircAccount,
name: '#kosmos-dev'
});
assert.strictEqual(model.sidebarName, 'kosmos-dev', 'strips the single leading hash');

model = new BaseChannel({
account: ircAccount,
name: '##kosmos-dev'
});
assert.strictEqual(model.sidebarName, '#kosmos-dev', 'preserves remaining hashes');
});

test('#sidebarName (XMPP) returns the local part', function (assert) {
const model = new BaseChannel({
account: xmppAccount,
name: 'kosmos-dev@kosmos.chat'
});
assert.strictEqual(model.sidebarName, 'kosmos-dev');
});

//
// unreadMessagesClass
//
Expand Down
18 changes: 11 additions & 7 deletions tests/unit/models/channel-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,21 @@ module('Unit | Model | channel', function (hooks) {
assert.strictEqual(channel.formattedTopic.toString(), 'never gonna &lt;marquee&gt;give you up&lt;/marquee&gt;');
});

test('#shortName', function (assert) {
let channel = new Channel({
test('#publicLogsBaseUrl keeps single-hash channels unencoded', function (assert) {
const channel = new Channel({
account: ircAccount,
name: '#kosmos-dev'
});
assert.strictEqual(channel.shortName, 'kosmos-dev', 'returns name without hash for IRC');
assert.strictEqual(channel.publicLogsBaseUrl, 'https://storage.5apps.com/kosmos/public/chat-messages/irc.libera.chat/channels/kosmos-dev');
});

channel = new Channel({
account: xmppAccount,
name: 'kosmos-dev@kosmos.chat'
test('#publicLogsBaseUrl percent-encodes all hashes for multi-hash channels', function (assert) {
const channel = new Channel({
account: ircAccount,
name: '##kosmos-dev'
});
assert.strictEqual(channel.shortName, 'kosmos-dev', 'returns name without MUC domain for XMPP');
const baseUrl = channel.publicLogsBaseUrl;
assert.ok(!baseUrl.includes('#'), 'logs URL has no fragment');
assert.strictEqual(baseUrl, 'https://storage.5apps.com/kosmos/public/chat-messages/irc.libera.chat/channels/%23%23kosmos-dev');
});
});
53 changes: 53 additions & 0 deletions tests/unit/routes/channel-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import Service from '@ember/service';
import ChannelRoute from 'hyperchannel/routes/channel';
import Channel from 'hyperchannel/models/channel';
import { ircAccount } from '../../fixtures/accounts';

// Stubs the coms service so createChannelOrUserChannel's channelName argument
// can be captured without joining a real channel. Returns a real Channel
// instance so id/sockethubChannelId getters resolve.
class ComsStub extends Service {
createChannelCalls = [];
createChannel (account, channelName) {
this.createChannelCalls.push({ account, channelName });
return new Channel({ account, name: channelName });
}
}

module('Unit | Route | channel', function (hooks) {
setupTest(hooks);

hooks.beforeEach(function () {
this.owner.register('service:coms', ComsStub);
});

test('createChannelOrUserChannel reconstructs a single-hash IRC channel name', function (assert) {
const route = new ChannelRoute();
route.coms = this.owner.lookup('service:coms');

// Slug for #kosmos-dev (single leading hash stripped) -> kosmos-dev@server
const channel = route.createChannelOrUserChannel(ircAccount, 'kosmos-dev@irc.libera.chat');

assert.strictEqual(route.coms.createChannelCalls.length, 1);
assert.strictEqual(route.coms.createChannelCalls[0].channelName, '#kosmos-dev');
assert.strictEqual(channel.name, '#kosmos-dev');
assert.strictEqual(channel.id, '#kosmos-dev@irc.libera.chat');
assert.strictEqual(channel.sockethubChannelId, '#kosmos-dev@irc.libera.chat');
});

test('createChannelOrUserChannel reconstructs a multi-hash IRC channel name', function (assert) {
const route = new ChannelRoute();
route.coms = this.owner.lookup('service:coms');

// Slug for ##kosmos-dev (all hashes preserved) -> ##kosmos-dev@server
const channel = route.createChannelOrUserChannel(ircAccount, '##kosmos-dev@irc.libera.chat');

assert.strictEqual(route.coms.createChannelCalls.length, 1);
assert.strictEqual(route.coms.createChannelCalls[0].channelName, '##kosmos-dev');
assert.strictEqual(channel.name, '##kosmos-dev');
assert.strictEqual(channel.id, '##kosmos-dev@irc.libera.chat');
assert.strictEqual(channel.sockethubChannelId, '##kosmos-dev@irc.libera.chat');
});
});
21 changes: 21 additions & 0 deletions tests/unit/services/sockethub-irc-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,27 @@ module('Unit | Service | sockethub irc', function (hooks) {
assert.ok(channel.connected);
});

test('#handlePresenceUpdate extracts hostname from the name@host ID format and adds the user', function (assert) {
const channel = new Channel({
account: ircAccount,
name: '##kosmos-dev'
});
const service = this.owner.lookup('service:sockethub-irc');
service.coms = {
channels: [channel],
accounts: [ircAccount]
};

service.handlePresenceUpdate({
target: { id: '##kosmos-dev@irc.libera.chat', type: 'room' },
actor: { id: 'newuser@irc.libera.chat', name: 'newuser' }
});

assert.ok(channel.connected, 'marks the channel connected');
assert.ok(channel.userList.includes('newuser'), 'adds the incoming user');
assert.ok(channel.userList.includes(ircAccount.nickname), 'adds the own nickname');
});

// FIXME this test randomly fails with error "Assertion occured after test had finished."
// skip('#join sends the join activity to Sockethub for a room channel', function(assert) {
// const done = assert.async();
Expand Down
Loading