this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: make users and channels unique

+248 -2
+96
src/commands.test.ts
··· 1 + import { describe, expect, test, beforeEach } from "bun:test"; 2 + import { channelMappings, userMappings } from "./lib/db"; 3 + 4 + describe("channel mappings uniqueness", () => { 5 + beforeEach(() => { 6 + // Clean up mappings before each test 7 + const channels = channelMappings.getAll(); 8 + for (const channel of channels) { 9 + channelMappings.delete(channel.slack_channel_id); 10 + } 11 + }); 12 + 13 + test("prevents duplicate IRC channel mappings", () => { 14 + channelMappings.create("C001", "#test"); 15 + 16 + const existing = channelMappings.getByIrcChannel("#test"); 17 + expect(existing).not.toBeNull(); 18 + expect(existing?.slack_channel_id).toBe("C001"); 19 + 20 + // Trying to map a different Slack channel to the same IRC channel should be prevented 21 + const duplicate = channelMappings.getByIrcChannel("#test"); 22 + expect(duplicate).not.toBeNull(); 23 + expect(duplicate?.slack_channel_id).toBe("C001"); 24 + }); 25 + 26 + test("prevents duplicate Slack channel mappings", () => { 27 + channelMappings.create("C001", "#test"); 28 + 29 + const existing = channelMappings.getBySlackChannel("C001"); 30 + expect(existing).not.toBeNull(); 31 + expect(existing?.irc_channel).toBe("#test"); 32 + 33 + // The same Slack channel should keep its original mapping 34 + channelMappings.create("C001", "#new"); 35 + const updated = channelMappings.getBySlackChannel("C001"); 36 + expect(updated?.irc_channel).toBe("#new"); 37 + }); 38 + 39 + test("allows different channels to map to different IRC channels", () => { 40 + channelMappings.create("C001", "#test1"); 41 + channelMappings.create("C002", "#test2"); 42 + 43 + const mapping1 = channelMappings.getBySlackChannel("C001"); 44 + const mapping2 = channelMappings.getBySlackChannel("C002"); 45 + 46 + expect(mapping1?.irc_channel).toBe("#test1"); 47 + expect(mapping2?.irc_channel).toBe("#test2"); 48 + }); 49 + }); 50 + 51 + describe("user mappings uniqueness", () => { 52 + beforeEach(() => { 53 + // Clean up mappings before each test 54 + const users = userMappings.getAll(); 55 + for (const user of users) { 56 + userMappings.delete(user.slack_user_id); 57 + } 58 + }); 59 + 60 + test("prevents duplicate IRC nick mappings", () => { 61 + userMappings.create("U001", "testnick"); 62 + 63 + const existing = userMappings.getByIrcNick("testnick"); 64 + expect(existing).not.toBeNull(); 65 + expect(existing?.slack_user_id).toBe("U001"); 66 + 67 + // Trying to map a different Slack user to the same IRC nick should be prevented 68 + const duplicate = userMappings.getByIrcNick("testnick"); 69 + expect(duplicate).not.toBeNull(); 70 + expect(duplicate?.slack_user_id).toBe("U001"); 71 + }); 72 + 73 + test("prevents duplicate Slack user mappings", () => { 74 + userMappings.create("U001", "testnick"); 75 + 76 + const existing = userMappings.getBySlackUser("U001"); 77 + expect(existing).not.toBeNull(); 78 + expect(existing?.irc_nick).toBe("testnick"); 79 + 80 + // The same Slack user should keep its original mapping 81 + userMappings.create("U001", "newnick"); 82 + const updated = userMappings.getBySlackUser("U001"); 83 + expect(updated?.irc_nick).toBe("newnick"); 84 + }); 85 + 86 + test("allows different users to map to different IRC nicks", () => { 87 + userMappings.create("U001", "nick1"); 88 + userMappings.create("U002", "nick2"); 89 + 90 + const mapping1 = userMappings.getBySlackUser("U001"); 91 + const mapping2 = userMappings.getBySlackUser("U002"); 92 + 93 + expect(mapping1?.irc_nick).toBe("nick1"); 94 + expect(mapping2?.irc_nick).toBe("nick2"); 95 + }); 96 + });
+44
src/commands.ts
··· 86 86 } 87 87 88 88 try { 89 + // Check if IRC channel is already linked 90 + const existingIrcMapping = channelMappings.getByIrcChannel(ircChannel); 91 + if (existingIrcMapping) { 92 + context.respond({ 93 + response_type: "ephemeral", 94 + text: `❌ IRC channel ${ircChannel} is already bridged to <#${existingIrcMapping.slack_channel_id}>`, 95 + replace_original: true, 96 + }); 97 + return; 98 + } 99 + 100 + // Check if Slack channel is already linked 101 + const existingSlackMapping = channelMappings.getBySlackChannel(slackChannelId); 102 + if (existingSlackMapping) { 103 + context.respond({ 104 + response_type: "ephemeral", 105 + text: `❌ This channel is already bridged to ${existingSlackMapping.irc_channel}`, 106 + replace_original: true, 107 + }); 108 + return; 109 + } 110 + 89 111 channelMappings.create(slackChannelId, ircChannel); 90 112 ircClient.join(ircChannel); 91 113 ··· 280 302 } 281 303 282 304 try { 305 + // Check if IRC nick is already linked 306 + const existingIrcMapping = userMappings.getByIrcNick(ircNick); 307 + if (existingIrcMapping) { 308 + context.respond({ 309 + response_type: "ephemeral", 310 + text: `❌ IRC nick *${ircNick}* is already linked to <@${existingIrcMapping.slack_user_id}>`, 311 + replace_original: true, 312 + }); 313 + return; 314 + } 315 + 316 + // Check if Slack user is already linked 317 + const existingSlackMapping = userMappings.getBySlackUser(slackUserId); 318 + if (existingSlackMapping) { 319 + context.respond({ 320 + response_type: "ephemeral", 321 + text: `❌ You are already linked to IRC nick *${existingSlackMapping.irc_nick}*`, 322 + replace_original: true, 323 + }); 324 + return; 325 + } 326 + 283 327 userMappings.create(slackUserId, ircNick); 284 328 console.log(`Created user mapping: ${slackUserId} -> ${ircNick}`); 285 329
+108 -2
src/lib/db.ts
··· 6 6 CREATE TABLE IF NOT EXISTS channel_mappings ( 7 7 id INTEGER PRIMARY KEY AUTOINCREMENT, 8 8 slack_channel_id TEXT NOT NULL UNIQUE, 9 - irc_channel TEXT NOT NULL, 9 + irc_channel TEXT NOT NULL UNIQUE, 10 10 created_at INTEGER DEFAULT (strftime('%s', 'now')) 11 11 ) 12 12 `); ··· 15 15 CREATE TABLE IF NOT EXISTS user_mappings ( 16 16 id INTEGER PRIMARY KEY AUTOINCREMENT, 17 17 slack_user_id TEXT NOT NULL UNIQUE, 18 - irc_nick TEXT NOT NULL, 18 + irc_nick TEXT NOT NULL UNIQUE, 19 19 created_at INTEGER DEFAULT (strftime('%s', 'now')) 20 20 ) 21 21 `); 22 + 23 + // Migration: Add unique constraints if they don't exist 24 + // SQLite doesn't support ALTER TABLE to add constraints, so we need to recreate the table 25 + function migrateSchema() { 26 + // Check if irc_channel has unique constraint by examining table schema 27 + const channelSchema = db 28 + .query("SELECT sql FROM sqlite_master WHERE type='table' AND name='channel_mappings'") 29 + .get() as { sql: string } | null; 30 + 31 + const hasIrcChannelUnique = channelSchema?.sql?.includes("irc_channel TEXT NOT NULL UNIQUE") ?? false; 32 + 33 + if (!hasIrcChannelUnique && channelSchema) { 34 + // Check if table has any data with duplicate irc_channel values 35 + const duplicates = db.query( 36 + "SELECT irc_channel, COUNT(*) as count FROM channel_mappings GROUP BY irc_channel HAVING count > 1", 37 + ).all(); 38 + 39 + if (duplicates.length > 0) { 40 + console.warn( 41 + "Warning: Found duplicate IRC channel mappings. Keeping only the most recent mapping for each IRC channel.", 42 + ); 43 + for (const dup of duplicates as { irc_channel: string }[]) { 44 + // Delete all but the most recent mapping for this IRC channel 45 + db.run( 46 + `DELETE FROM channel_mappings 47 + WHERE irc_channel = ? 48 + AND id NOT IN ( 49 + SELECT id FROM channel_mappings 50 + WHERE irc_channel = ? 51 + ORDER BY created_at DESC 52 + LIMIT 1 53 + )`, 54 + [dup.irc_channel, dup.irc_channel], 55 + ); 56 + } 57 + } 58 + 59 + // Recreate the table with unique constraint 60 + db.run(` 61 + CREATE TABLE channel_mappings_new ( 62 + id INTEGER PRIMARY KEY AUTOINCREMENT, 63 + slack_channel_id TEXT NOT NULL UNIQUE, 64 + irc_channel TEXT NOT NULL UNIQUE, 65 + created_at INTEGER DEFAULT (strftime('%s', 'now')) 66 + ) 67 + `); 68 + 69 + db.run( 70 + "INSERT INTO channel_mappings_new SELECT * FROM channel_mappings", 71 + ); 72 + db.run("DROP TABLE channel_mappings"); 73 + db.run("ALTER TABLE channel_mappings_new RENAME TO channel_mappings"); 74 + console.log("Migrated channel_mappings table to add unique constraint on irc_channel"); 75 + } 76 + 77 + // Check if irc_nick has unique constraint by examining table schema 78 + const userSchema = db 79 + .query("SELECT sql FROM sqlite_master WHERE type='table' AND name='user_mappings'") 80 + .get() as { sql: string } | null; 81 + 82 + const hasIrcNickUnique = userSchema?.sql?.includes("irc_nick TEXT NOT NULL UNIQUE") ?? false; 83 + 84 + if (!hasIrcNickUnique && userSchema) { 85 + // Check if table has any data with duplicate irc_nick values 86 + const duplicates = db.query( 87 + "SELECT irc_nick, COUNT(*) as count FROM user_mappings GROUP BY irc_nick HAVING count > 1", 88 + ).all(); 89 + 90 + if (duplicates.length > 0) { 91 + console.warn( 92 + "Warning: Found duplicate IRC nick mappings. Keeping only the most recent mapping for each IRC nick.", 93 + ); 94 + for (const dup of duplicates as { irc_nick: string }[]) { 95 + // Delete all but the most recent mapping for this IRC nick 96 + db.run( 97 + `DELETE FROM user_mappings 98 + WHERE irc_nick = ? 99 + AND id NOT IN ( 100 + SELECT id FROM user_mappings 101 + WHERE irc_nick = ? 102 + ORDER BY created_at DESC 103 + LIMIT 1 104 + )`, 105 + [dup.irc_nick, dup.irc_nick], 106 + ); 107 + } 108 + } 109 + 110 + // Recreate the table with unique constraint 111 + db.run(` 112 + CREATE TABLE user_mappings_new ( 113 + id INTEGER PRIMARY KEY AUTOINCREMENT, 114 + slack_user_id TEXT NOT NULL UNIQUE, 115 + irc_nick TEXT NOT NULL UNIQUE, 116 + created_at INTEGER DEFAULT (strftime('%s', 'now')) 117 + ) 118 + `); 119 + 120 + db.run("INSERT INTO user_mappings_new SELECT * FROM user_mappings"); 121 + db.run("DROP TABLE user_mappings"); 122 + db.run("ALTER TABLE user_mappings_new RENAME TO user_mappings"); 123 + console.log("Migrated user_mappings table to add unique constraint on irc_nick"); 124 + } 125 + } 126 + 127 + migrateSchema(); 22 128 23 129 db.run(` 24 130 CREATE TABLE IF NOT EXISTS thread_timestamps (