this repo has no description

feat: make users and channels unique

dunkirk.sh 46b19049 cbf29d18

verified
Changed files
+248 -2
src
+96
src/commands.test.ts
···
+
import { describe, expect, test, beforeEach } from "bun:test";
+
import { channelMappings, userMappings } from "./lib/db";
+
+
describe("channel mappings uniqueness", () => {
+
beforeEach(() => {
+
// Clean up mappings before each test
+
const channels = channelMappings.getAll();
+
for (const channel of channels) {
+
channelMappings.delete(channel.slack_channel_id);
+
}
+
});
+
+
test("prevents duplicate IRC channel mappings", () => {
+
channelMappings.create("C001", "#test");
+
+
const existing = channelMappings.getByIrcChannel("#test");
+
expect(existing).not.toBeNull();
+
expect(existing?.slack_channel_id).toBe("C001");
+
+
// Trying to map a different Slack channel to the same IRC channel should be prevented
+
const duplicate = channelMappings.getByIrcChannel("#test");
+
expect(duplicate).not.toBeNull();
+
expect(duplicate?.slack_channel_id).toBe("C001");
+
});
+
+
test("prevents duplicate Slack channel mappings", () => {
+
channelMappings.create("C001", "#test");
+
+
const existing = channelMappings.getBySlackChannel("C001");
+
expect(existing).not.toBeNull();
+
expect(existing?.irc_channel).toBe("#test");
+
+
// The same Slack channel should keep its original mapping
+
channelMappings.create("C001", "#new");
+
const updated = channelMappings.getBySlackChannel("C001");
+
expect(updated?.irc_channel).toBe("#new");
+
});
+
+
test("allows different channels to map to different IRC channels", () => {
+
channelMappings.create("C001", "#test1");
+
channelMappings.create("C002", "#test2");
+
+
const mapping1 = channelMappings.getBySlackChannel("C001");
+
const mapping2 = channelMappings.getBySlackChannel("C002");
+
+
expect(mapping1?.irc_channel).toBe("#test1");
+
expect(mapping2?.irc_channel).toBe("#test2");
+
});
+
});
+
+
describe("user mappings uniqueness", () => {
+
beforeEach(() => {
+
// Clean up mappings before each test
+
const users = userMappings.getAll();
+
for (const user of users) {
+
userMappings.delete(user.slack_user_id);
+
}
+
});
+
+
test("prevents duplicate IRC nick mappings", () => {
+
userMappings.create("U001", "testnick");
+
+
const existing = userMappings.getByIrcNick("testnick");
+
expect(existing).not.toBeNull();
+
expect(existing?.slack_user_id).toBe("U001");
+
+
// Trying to map a different Slack user to the same IRC nick should be prevented
+
const duplicate = userMappings.getByIrcNick("testnick");
+
expect(duplicate).not.toBeNull();
+
expect(duplicate?.slack_user_id).toBe("U001");
+
});
+
+
test("prevents duplicate Slack user mappings", () => {
+
userMappings.create("U001", "testnick");
+
+
const existing = userMappings.getBySlackUser("U001");
+
expect(existing).not.toBeNull();
+
expect(existing?.irc_nick).toBe("testnick");
+
+
// The same Slack user should keep its original mapping
+
userMappings.create("U001", "newnick");
+
const updated = userMappings.getBySlackUser("U001");
+
expect(updated?.irc_nick).toBe("newnick");
+
});
+
+
test("allows different users to map to different IRC nicks", () => {
+
userMappings.create("U001", "nick1");
+
userMappings.create("U002", "nick2");
+
+
const mapping1 = userMappings.getBySlackUser("U001");
+
const mapping2 = userMappings.getBySlackUser("U002");
+
+
expect(mapping1?.irc_nick).toBe("nick1");
+
expect(mapping2?.irc_nick).toBe("nick2");
+
});
+
});
+44
src/commands.ts
···
}
try {
+
// Check if IRC channel is already linked
+
const existingIrcMapping = channelMappings.getByIrcChannel(ircChannel);
+
if (existingIrcMapping) {
+
context.respond({
+
response_type: "ephemeral",
+
text: `❌ IRC channel ${ircChannel} is already bridged to <#${existingIrcMapping.slack_channel_id}>`,
+
replace_original: true,
+
});
+
return;
+
}
+
+
// Check if Slack channel is already linked
+
const existingSlackMapping = channelMappings.getBySlackChannel(slackChannelId);
+
if (existingSlackMapping) {
+
context.respond({
+
response_type: "ephemeral",
+
text: `❌ This channel is already bridged to ${existingSlackMapping.irc_channel}`,
+
replace_original: true,
+
});
+
return;
+
}
+
channelMappings.create(slackChannelId, ircChannel);
ircClient.join(ircChannel);
···
}
try {
+
// Check if IRC nick is already linked
+
const existingIrcMapping = userMappings.getByIrcNick(ircNick);
+
if (existingIrcMapping) {
+
context.respond({
+
response_type: "ephemeral",
+
text: `❌ IRC nick *${ircNick}* is already linked to <@${existingIrcMapping.slack_user_id}>`,
+
replace_original: true,
+
});
+
return;
+
}
+
+
// Check if Slack user is already linked
+
const existingSlackMapping = userMappings.getBySlackUser(slackUserId);
+
if (existingSlackMapping) {
+
context.respond({
+
response_type: "ephemeral",
+
text: `❌ You are already linked to IRC nick *${existingSlackMapping.irc_nick}*`,
+
replace_original: true,
+
});
+
return;
+
}
+
userMappings.create(slackUserId, ircNick);
console.log(`Created user mapping: ${slackUserId} -> ${ircNick}`);
+108 -2
src/lib/db.ts
···
CREATE TABLE IF NOT EXISTS channel_mappings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slack_channel_id TEXT NOT NULL UNIQUE,
-
irc_channel TEXT NOT NULL,
+
irc_channel TEXT NOT NULL UNIQUE,
created_at INTEGER DEFAULT (strftime('%s', 'now'))
)
`);
···
CREATE TABLE IF NOT EXISTS user_mappings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slack_user_id TEXT NOT NULL UNIQUE,
-
irc_nick TEXT NOT NULL,
+
irc_nick TEXT NOT NULL UNIQUE,
created_at INTEGER DEFAULT (strftime('%s', 'now'))
)
`);
+
+
// Migration: Add unique constraints if they don't exist
+
// SQLite doesn't support ALTER TABLE to add constraints, so we need to recreate the table
+
function migrateSchema() {
+
// Check if irc_channel has unique constraint by examining table schema
+
const channelSchema = db
+
.query("SELECT sql FROM sqlite_master WHERE type='table' AND name='channel_mappings'")
+
.get() as { sql: string } | null;
+
+
const hasIrcChannelUnique = channelSchema?.sql?.includes("irc_channel TEXT NOT NULL UNIQUE") ?? false;
+
+
if (!hasIrcChannelUnique && channelSchema) {
+
// Check if table has any data with duplicate irc_channel values
+
const duplicates = db.query(
+
"SELECT irc_channel, COUNT(*) as count FROM channel_mappings GROUP BY irc_channel HAVING count > 1",
+
).all();
+
+
if (duplicates.length > 0) {
+
console.warn(
+
"Warning: Found duplicate IRC channel mappings. Keeping only the most recent mapping for each IRC channel.",
+
);
+
for (const dup of duplicates as { irc_channel: string }[]) {
+
// Delete all but the most recent mapping for this IRC channel
+
db.run(
+
`DELETE FROM channel_mappings
+
WHERE irc_channel = ?
+
AND id NOT IN (
+
SELECT id FROM channel_mappings
+
WHERE irc_channel = ?
+
ORDER BY created_at DESC
+
LIMIT 1
+
)`,
+
[dup.irc_channel, dup.irc_channel],
+
);
+
}
+
}
+
+
// Recreate the table with unique constraint
+
db.run(`
+
CREATE TABLE channel_mappings_new (
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
+
slack_channel_id TEXT NOT NULL UNIQUE,
+
irc_channel TEXT NOT NULL UNIQUE,
+
created_at INTEGER DEFAULT (strftime('%s', 'now'))
+
)
+
`);
+
+
db.run(
+
"INSERT INTO channel_mappings_new SELECT * FROM channel_mappings",
+
);
+
db.run("DROP TABLE channel_mappings");
+
db.run("ALTER TABLE channel_mappings_new RENAME TO channel_mappings");
+
console.log("Migrated channel_mappings table to add unique constraint on irc_channel");
+
}
+
+
// Check if irc_nick has unique constraint by examining table schema
+
const userSchema = db
+
.query("SELECT sql FROM sqlite_master WHERE type='table' AND name='user_mappings'")
+
.get() as { sql: string } | null;
+
+
const hasIrcNickUnique = userSchema?.sql?.includes("irc_nick TEXT NOT NULL UNIQUE") ?? false;
+
+
if (!hasIrcNickUnique && userSchema) {
+
// Check if table has any data with duplicate irc_nick values
+
const duplicates = db.query(
+
"SELECT irc_nick, COUNT(*) as count FROM user_mappings GROUP BY irc_nick HAVING count > 1",
+
).all();
+
+
if (duplicates.length > 0) {
+
console.warn(
+
"Warning: Found duplicate IRC nick mappings. Keeping only the most recent mapping for each IRC nick.",
+
);
+
for (const dup of duplicates as { irc_nick: string }[]) {
+
// Delete all but the most recent mapping for this IRC nick
+
db.run(
+
`DELETE FROM user_mappings
+
WHERE irc_nick = ?
+
AND id NOT IN (
+
SELECT id FROM user_mappings
+
WHERE irc_nick = ?
+
ORDER BY created_at DESC
+
LIMIT 1
+
)`,
+
[dup.irc_nick, dup.irc_nick],
+
);
+
}
+
}
+
+
// Recreate the table with unique constraint
+
db.run(`
+
CREATE TABLE user_mappings_new (
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
+
slack_user_id TEXT NOT NULL UNIQUE,
+
irc_nick TEXT NOT NULL UNIQUE,
+
created_at INTEGER DEFAULT (strftime('%s', 'now'))
+
)
+
`);
+
+
db.run("INSERT INTO user_mappings_new SELECT * FROM user_mappings");
+
db.run("DROP TABLE user_mappings");
+
db.run("ALTER TABLE user_mappings_new RENAME TO user_mappings");
+
console.log("Migrated user_mappings table to add unique constraint on irc_nick");
+
}
+
}
+
+
migrateSchema();
db.run(`
CREATE TABLE IF NOT EXISTS thread_timestamps (