From 042c5acf3684641624ee03d4d292abcab86f5e14 Mon Sep 17 00:00:00 2001 From: megrogan Date: Wed, 14 Jan 2026 17:44:03 +0000 Subject: [PATCH 1/2] Load installation events when motoko bot starts --- motoko/examples/.gitignore | 2 + motoko/examples/deploy_prod.sh | 21 ++++++++++ motoko/examples/moderator/events.mo | 3 +- motoko/examples/moderator/main.mo | 39 +++++++++++++++++-- motoko/src/api/oc/installationEvents.mo | 44 +++++++++++++++++++++ motoko/src/client.mo | 7 ++++ motoko/src/client/installationEvents.mo | 39 +++++++++++++++++++ motoko/src/installationEvents.mo | 52 +++++++++++++++++++++++++ motoko/src/installationRegistry.mo | 24 ++++++++++-- rs/sdk/src/installation_registry.rs | 15 +++---- 10 files changed, 229 insertions(+), 17 deletions(-) create mode 100644 motoko/examples/.gitignore create mode 100755 motoko/examples/deploy_prod.sh create mode 100644 motoko/src/api/oc/installationEvents.mo create mode 100644 motoko/src/client/installationEvents.mo create mode 100644 motoko/src/installationEvents.mo diff --git a/motoko/examples/.gitignore b/motoko/examples/.gitignore new file mode 100644 index 00000000..30558d05 --- /dev/null +++ b/motoko/examples/.gitignore @@ -0,0 +1,2 @@ +canister_ids.json +oc_public_key.pem \ No newline at end of file diff --git a/motoko/examples/deploy_prod.sh b/motoko/examples/deploy_prod.sh new file mode 100755 index 00000000..e3e8079c --- /dev/null +++ b/motoko/examples/deploy_prod.sh @@ -0,0 +1,21 @@ +# The bot to deploy +BOT=$1 + +# Get the OpenChat public key +OC_PUBLIC_KEY=$(curl -s https://oc.app/public-key) + +if [ $? -ne 0 ]; then + echo "OpenChat is not running on https://oc.app/public-key." + exit 1 +fi + +# Deploy the bot +dfx deploy --ic $BOT --argument "(\"$OC_PUBLIC_KEY\")" || exit 1 + +# Get the canister ID +CANISTER_ID=$(dfx canister --ic id $BOT) || exit 1 + +echo "" +echo "Principal: $CANISTER_ID" +echo "Endpoint: https://$CANISTER_ID.raw.icp0.io" +echo "" diff --git a/motoko/examples/moderator/events.mo b/motoko/examples/moderator/events.mo index e00e3e1c..ed2ac53b 100644 --- a/motoko/examples/moderator/events.mo +++ b/motoko/examples/moderator/events.mo @@ -39,10 +39,11 @@ module { apiGateway = eventWrapper.api_gateway; grantedCommandPermissions = Permissions.fromRaw(event.granted_command_permissions); grantedAutonomousPermissions = Permissions.fromRaw(event.granted_autonomous_permissions); + lastUpdated = eventWrapper.timestamp; }); }; case (#Uninstalled event) { - state.installationRegistry.remove(event.location); + state.installationRegistry.remove(event.location, eventWrapper.timestamp); }; }; }; diff --git a/motoko/examples/moderator/main.mo b/motoko/examples/moderator/main.mo index bb830fdb..605d1e64 100644 --- a/motoko/examples/moderator/main.mo +++ b/motoko/examples/moderator/main.mo @@ -1,9 +1,14 @@ -import Http "mo:http-types"; -import Sdk "mo:openchat-bot-sdk"; - +import Debug "mo:base/Debug"; import Definition "definition"; import Events "events"; +import Http "mo:http-types"; +import InstallationEvents "mo:openchat-bot-sdk/installationEvents"; +import Nat "mo:base/Nat"; +import Permissions "mo:openchat-bot-sdk/api/common/permissions"; +import Principal "mo:base/Principal"; +import Sdk "mo:openchat-bot-sdk"; import State "state"; +import Timer "mo:base/Timer"; persistent actor class ModeratorBot(key : Text) { var stableState = State.new(); @@ -26,4 +31,32 @@ persistent actor class ModeratorBot(key : Text) { system func preupgrade() { stableState := State.toStable(state); }; + + private func onInitOrUpgrade() { + ignore Timer.setTimer(#seconds 0, func () : async () { + let userIndexCanisterId = Principal.fromText("vizcg-th777-77774-qaaea-cai"); + ignore InstallationEvents.load(userIndexCanisterId, func (botId, events) { + Debug.print("Loaded " # Nat.toText(events.size()) # " events"); + for (event in events.vals()) { + switch (event) { + case (#Installed e) { + state.installationRegistry.insert(e.location, { + apiGateway = e.api_gateway; + grantedCommandPermissions = Permissions.fromRaw(e.granted_permissions); + grantedAutonomousPermissions = Permissions.fromRaw(e.granted_autonomous_permissions); + lastUpdated = e.timestamp; + }); + }; + case (#Uninstalled e) { + state.installationRegistry.remove(e.location, e.timestamp); + }; + }; + }; + + state.botId := ?botId; + }); + }); + }; + + onInitOrUpgrade(); }; diff --git a/motoko/src/api/oc/installationEvents.mo b/motoko/src/api/oc/installationEvents.mo new file mode 100644 index 00000000..56f64c05 --- /dev/null +++ b/motoko/src/api/oc/installationEvents.mo @@ -0,0 +1,44 @@ +import B "../common/base"; +import I "../common/installationLocation"; +import P "../common/permissions"; + +module { + public type Actor = actor { + bot_installation_events : (Args) -> async Response; + }; + + public type Args = { + from : Nat32; + size : Nat16; + }; + + public type Response = { + #Success : SuccessResult; + #Error : B.OCError; + }; + + public type SuccessResult = { + bot_id : B.UserId; + events : [BotInstallationEvent]; + }; + + public type BotInstallationEvent = { + #Installed : BotInstalled; + #Uninstalled : BotUninstalled; + }; + + public type BotInstalled = { + location : I.InstallationLocation; + api_gateway : B.CanisterId; + granted_permissions : P.RawPermissions; + granted_autonomous_permissions : P.RawPermissions; + installed_by : B.UserId; + timestamp : B.TimestampMillis; + }; + + public type BotUninstalled = { + location : I.InstallationLocation; + uninstalled_by : B.UserId; + timestamp : B.TimestampMillis; + }; +}; diff --git a/motoko/src/client.mo b/motoko/src/client.mo index 913bb68a..b92177e1 100644 --- a/motoko/src/client.mo +++ b/motoko/src/client.mo @@ -15,6 +15,7 @@ import DeleteMessages "client/deleteMessages"; import SendMessage "client/sendMessage"; import AddReaction "client/addReaction"; import CommunitySummary "client/communitySummary"; +import InstallationEvents "client/installationEvents"; import InviteUsers "client/inviteUsers"; import RemoveUser "client/removeUser"; import Members "client/members"; @@ -77,4 +78,10 @@ module { Members.Builder(context, memberTypes); }; }; + + public class UserIndexClient(userIndexCanisterId : B.CanisterId) { + public func installationEvents() : InstallationEvents.Builder { + InstallationEvents.Builder(userIndexCanisterId); + }; + } }; diff --git a/motoko/src/client/installationEvents.mo b/motoko/src/client/installationEvents.mo new file mode 100644 index 00000000..0a515515 --- /dev/null +++ b/motoko/src/client/installationEvents.mo @@ -0,0 +1,39 @@ +import Error "mo:base/Error"; +import Principal "mo:base/Principal"; +import Result "mo:base/Result"; +import B "../api/common/base"; +import InstallationEvents "../api/oc/installationEvents"; + +module { + public class Builder(userIndexCanisterId : B.CanisterId) = this { + var from : Nat32 = 0; + var size : Nat16 = 5000; + + public func withFrom(value : Nat32) : Builder { + from := value; + this; + }; + + public func withSize(value : Nat16) : Builder { + size := value; + this; + }; + + public func execute() : async Result.Result { + let botApiActor = actor (Principal.toText(userIndexCanisterId)) : InstallationEvents.Actor; + + try { + let response = await botApiActor.bot_installation_events({ + from; + size; + }); + + #ok response; + } catch (error) { + #err((Error.code(error), Error.message(error))); + }; + }; + }; + + public type Result = Result.Result; +}; diff --git a/motoko/src/installationEvents.mo b/motoko/src/installationEvents.mo new file mode 100644 index 00000000..eafa797c --- /dev/null +++ b/motoko/src/installationEvents.mo @@ -0,0 +1,52 @@ +import Array "mo:base/Array"; +import Base "api/common/base"; +import Client "client"; +import Debug "mo:base/Debug"; +import I "api/oc/installationEvents"; +import Nat16 "mo:base/Nat16"; +import Timer "mo:base/Timer"; + +module { + type EventsCallback = (Base.UserId, [I.BotInstallationEvent]) -> (); + + let pageSize : Nat16 = 5_000; + + public func load(userIndexCanisterId: Base.CanisterId, callback : EventsCallback) : async () { + ignore loadPage(userIndexCanisterId, [], 0, callback); + }; + + func loadPage( + userIndexCanisterId: Base.CanisterId, + events: [I.BotInstallationEvent], + from : Nat32, + callback : EventsCallback) + : async () { + let response = await Client.UserIndexClient(userIndexCanisterId) + .installationEvents() + .withFrom(from) + .withSize(pageSize) + .execute(); + + switch (response) { + case (#ok(#Success result)) { + let count = result.events.size(); + let aggregatedEvents = Array.append(events, result.events); + + if (count == Nat16.toNat(pageSize)) { + ignore Timer.setTimer(#seconds 0, func () : async () { + ignore loadPage( + userIndexCanisterId, + aggregatedEvents, + from + Nat16.toNat32(pageSize), + callback); + }); + } else { + callback(result.bot_id, aggregatedEvents); + } + }; + case other { + Debug.print("Failed to load installation events: " # debug_show(other)); + }; + }; + }; +}; \ No newline at end of file diff --git a/motoko/src/installationRegistry.mo b/motoko/src/installationRegistry.mo index 08d1ee71..4f254a6e 100644 --- a/motoko/src/installationRegistry.mo +++ b/motoko/src/installationRegistry.mo @@ -15,12 +15,27 @@ module { InstallationLocation.equal, InstallationLocation.hash); - public func insert(location: InstallationLocation, details: InstallationRecord) { - map.put(location, details); + public func insert(location: InstallationLocation, newRecord: InstallationRecord) { + switch (map.get(location)) { + case (?record) { + if (newRecord.lastUpdated <= record.lastUpdated) { + return; + } + }; + case null {}; + }; + + map.put(location, newRecord); }; - public func remove(location: InstallationLocation) { - map.delete(location); + public func remove(location: InstallationLocation, timestamp: Base.TimestampMillis) { + let ?record = map.get(location) else { + return; + }; + + if (timestamp > record.lastUpdated) { + map.delete(location); + } }; public func get(location: InstallationLocation) : ?InstallationRecord { @@ -40,5 +55,6 @@ module { apiGateway: Base.CanisterId; grantedCommandPermissions: Permissions.Permissions; grantedAutonomousPermissions: Permissions.Permissions; + lastUpdated : Base.TimestampMillis; }; }; diff --git a/rs/sdk/src/installation_registry.rs b/rs/sdk/src/installation_registry.rs index cf2fb790..5742cb56 100644 --- a/rs/sdk/src/installation_registry.rs +++ b/rs/sdk/src/installation_registry.rs @@ -22,16 +22,13 @@ impl InstallationRegistry { } pub fn insert(&mut self, location: InstallationLocation, new_record: InstallationRecord) { - if let Some(record) = self.locations.get_mut(&location) { - if new_record.last_updated > record.last_updated { - record.api_gateway = new_record.api_gateway; - record.granted_command_permissions = new_record.granted_command_permissions; - record.granted_autonomous_permissions = new_record.granted_autonomous_permissions; - record.last_updated = new_record.last_updated; - } - } else { - self.locations.insert(location, new_record); + if let Some(record) = self.locations.get_mut(&location) + && new_record.last_updated <= record.last_updated + { + return; } + + self.locations.insert(location, new_record); } pub fn remove(&mut self, location: &InstallationLocation, timestamp: TimestampMillis) { From c9cfdc202dcd947263d7d61b72bf40fe1d7fb87a Mon Sep 17 00:00:00 2001 From: megrogan Date: Wed, 14 Jan 2026 17:52:40 +0000 Subject: [PATCH 2/2] Bump mops version --- motoko/mops.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/motoko/mops.toml b/motoko/mops.toml index dd2d4c7a..fd85c48c 100644 --- a/motoko/mops.toml +++ b/motoko/mops.toml @@ -11,7 +11,7 @@ csprng = "1.1.0" [package] name = "openchat-bot-sdk" -version = "0.7.0" +version = "0.8.0" description = "An SDK for developing bots which plugin to the OpenChat bot framework + examples" repository = "https://github.com/open-chat-labs/open-chat-bots" keywords = [ "openchat", "sdk", "bots", "examples" ]