diff --git a/AGENTS.md b/AGENTS.md index 46abf31..05bbc9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,12 +72,12 @@ Commands parse parameters by splitting on commas: `message.getTextParameters()` Runtime config at `~/Library/Application Support/Jared/config.json`. Schema: ```json { - "routes": { "routename": { "disabled": true } }, + "disabledCommands": { "/ping": true }, "webhooks": [{ "url": "https://...", "routes": [] }], "webServer": { "port": 3005 } } ``` -Route names in `config.routes` are lowercased keys matching `Route.name.lowercased()`. +`disabledCommands` keys are lowercased command names (e.g. `"/ping"`). Legacy `"routes": { "name": { "disabled": true } }` configs are decoded as aliases automatically. ## Entities & Recipients diff --git a/Documentation/config-sample.json b/Documentation/config-sample.json index ef6170b..6e00902 100644 --- a/Documentation/config-sample.json +++ b/Documentation/config-sample.json @@ -1,8 +1,6 @@ { - "routes": { - "/remote": { - "disabled": true - } + "disabledCommands": { + "/remote": true }, "webhooks": [ { diff --git a/Documentation/readme.md b/Documentation/readme.md index 5534f8e..d93117b 100644 --- a/Documentation/readme.md +++ b/Documentation/readme.md @@ -2,7 +2,6 @@ Welcome to the Jared documentation hub. Here you'll find a glossary of Jared's API documentation. -+ [Routes](routes.md) + [Webhooks](webhooks.md) + [n8n Integration](n8n.md) + [REST API](restapi.md) diff --git a/Documentation/routes.md b/Documentation/routes.md deleted file mode 100644 index 4ae0d94..0000000 --- a/Documentation/routes.md +++ /dev/null @@ -1,34 +0,0 @@ -# Routes - -By using a webhook configuration, you can define routes which are used to define triggers for messages. - -## Parameters - -`name`: A string for the name of the route. This must be globally unique. -`comparisons`: A dictionary of `[Comapare: [String]]` that defines triggers for your route. The following is a list of `Compare` types: - -+ `startsWith`: String starts with -+ `contains`: String contains -+ `is`: String matches exactly -+ `containsURL`: String contains this URL -+ `isReaction`: Message is a reaction message (tapback) - - -`parameterSyntax`: A string that describes how to exercise your route. -`description`: A string that describes the purpose of this route -`call`: A callback method to execute when the route is triggered. When using webhooks, your webhook endpoint is called automatically. - -## Example -The following is an example configuration of one of Jared's built in routes, `/send`. - -``` -{ - "name": "/send", - "comparisons": { - "startsWith": ["/send"] - } - "parameterSyntax": "/send,[number of times],[send delay],[message to send]", - "description": "Send a message repeatedly" -} -``` - diff --git a/Documentation/webhooks.md b/Documentation/webhooks.md index c408a11..cb036ca 100644 --- a/Documentation/webhooks.md +++ b/Documentation/webhooks.md @@ -61,6 +61,22 @@ Add webhook entries to the `webhooks` array in `config.json` (`~/Library/Applica } ``` +### Disabling built-in commands + +Add a `disabledCommands` map to `config.json` to prevent specific slash commands from being routed: + +```json +{ + "disabledCommands": { + "/remote": true + } +} +``` + +Keys are lowercased command names (e.g. `"/ping"`, `"/send"`). A value of `true` disables that command; `false` or absent leaves it enabled. + +**Legacy `routes` key** — configs using the old `"routes": { "name": { "disabled": true } }` format continue to load correctly. The decoder maps legacy entries to `disabledCommands` automatically. + ### Backward compatibility The simple format from Jared v1.6.x loads unchanged. All new fields have defaults: diff --git a/Jared/Configuration.swift b/Jared/Configuration.swift index ad2f4d8..6968453 100644 --- a/Jared/Configuration.swift +++ b/Jared/Configuration.swift @@ -9,30 +9,41 @@ import Foundation struct ConfigurationFile: Decodable { - let routes: [String: RouteConfiguration] + let disabledCommands: [String: Bool] let webhooks: [RichWebhook] let webServer: WebserverConfiguration - init(routes: [String: RouteConfiguration] = [:], + init(disabledCommands: [String: Bool] = [:], webhooks: [RichWebhook] = [], webServer: WebserverConfiguration = WebserverConfiguration(port: 3000)) { - self.routes = routes + self.disabledCommands = disabledCommands self.webhooks = webhooks self.webServer = webServer } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) - routes = (try? c.decodeIfPresent([String: RouteConfiguration].self, forKey: .routes)) ?? [:] - webhooks = (try? c.decodeIfPresent([RichWebhook].self, forKey: .webhooks)) ?? [] - webServer = (try? c.decodeIfPresent(WebserverConfiguration.self, forKey: .webServer)) ?? WebserverConfiguration(port: 3000) + // Accept new "disabledCommands" key; fall back to legacy "routes" map for existing configs. + if let newMap = try? c.decodeIfPresent([String: Bool].self, forKey: .disabledCommands) { + disabledCommands = newMap + } else if let legacyRoutes = try? c.decodeIfPresent([String: LegacyRouteConfiguration].self, forKey: .routes) { + disabledCommands = legacyRoutes.mapValues { $0.disabled } + } else { + disabledCommands = [:] + } + webhooks = (try? c.decodeIfPresent([RichWebhook].self, forKey: .webhooks)) ?? [] + webServer = (try? c.decodeIfPresent(WebserverConfiguration.self, forKey: .webServer)) ?? WebserverConfiguration(port: 3000) } private enum CodingKeys: String, CodingKey { - case routes, webhooks, webServer + case disabledCommands, routes, webhooks, webServer } } +private struct LegacyRouteConfiguration: Decodable { + let disabled: Bool +} + struct WebserverConfiguration: Decodable { let port: Int let bearerToken: String? @@ -56,6 +67,3 @@ struct WebserverConfiguration: Decodable { } } -struct RouteConfiguration: Decodable { - let disabled: Bool -} diff --git a/Jared/PluginManager.swift b/Jared/PluginManager.swift index bdc6cc0..ae732dd 100644 --- a/Jared/PluginManager.swift +++ b/Jared/PluginManager.swift @@ -57,11 +57,7 @@ class PluginManager: RouteProvider, PluginController, ConfigurationApplier { } func enabled(routeName: String) -> Bool { - if let routeConfig = config.routes[routeName.lowercased()] { - return !routeConfig.disabled - } else { - return true - } + return !(config.disabledCommands[routeName.lowercased()] ?? false) } func getAllModules() -> [RoutingModule] { diff --git a/JaredTests/ConfigurationWatcherTests.swift b/JaredTests/ConfigurationWatcherTests.swift index fb94d69..023cb5c 100644 --- a/JaredTests/ConfigurationWatcherTests.swift +++ b/JaredTests/ConfigurationWatcherTests.swift @@ -13,7 +13,7 @@ class ConfigurationWatcherTests: XCTestCase { try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) let configURL = dir.appendingPathComponent("config.json") let initial = """ - {"routes":{},"webhooks":[],"webServer":{"port":3000}} + {"webhooks":[],"webServer":{"port":3000}} """ try initial.write(to: configURL, atomically: true, encoding: .utf8) @@ -24,7 +24,7 @@ class ConfigurationWatcherTests: XCTestCase { watcher.start() let updated = """ - {"routes":{"ping":{"disabled":true}},"webhooks":[],"webServer":{"port":3000}} + {"disabledCommands":{"/ping":true},"webhooks":[],"webServer":{"port":3000}} """ try updated.write(to: configURL, atomically: true, encoding: .utf8) @@ -36,7 +36,7 @@ class ConfigurationWatcherTests: XCTestCase { try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) let configURL = dir.appendingPathComponent("config.json") let initial = """ - {"routes":{},"webhooks":[],"webServer":{"port":3000}} + {"webhooks":[],"webServer":{"port":3000}} """ try initial.write(to: configURL, atomically: true, encoding: .utf8) @@ -53,13 +53,31 @@ class ConfigurationWatcherTests: XCTestCase { watcher.start() let updated = """ - {"routes":{},"webhooks":[],"webServer":{"port":4000}} + {"webhooks":[],"webServer":{"port":4000}} """ try updated.write(to: configURL, atomically: true, encoding: .utf8) wait(for: [expectation], timeout: 3.0) XCTAssertEqual(applier.applied.last?.webServer.port, 4000) } + func testDisabledCommandsNewKey() throws { + let json = """ + {"disabledCommands":{"/ping":true,"/send":false},"webhooks":[],"webServer":{"port":3000}} + """ + let config = try JSONDecoder().decode(ConfigurationFile.self, from: Data(json.utf8)) + XCTAssertTrue(config.disabledCommands["/ping"] == true) + XCTAssertTrue(config.disabledCommands["/send"] == false) + } + + func testDisabledCommandsLegacyRoutesKey() throws { + let json = """ + {"routes":{"/ping":{"disabled":true},"/send":{"disabled":false}},"webhooks":[],"webServer":{"port":3000}} + """ + let config = try JSONDecoder().decode(ConfigurationFile.self, from: Data(json.utf8)) + XCTAssertTrue(config.disabledCommands["/ping"] == true) + XCTAssertTrue(config.disabledCommands["/send"] == false) + } + func testCallbackNotFiredBeforeStart() throws { let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)