Skip to content

Commit

Permalink
Merge pull request #482 from coasys/notifications
Browse files Browse the repository at this point in the history
Notifications 2 - web hook push
  • Loading branch information
lucksus authored May 29, 2024
2 parents 24d47b4 + 154945c commit 51fcb1e
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 44 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This project _loosely_ adheres to [Semantic Versioning](https://semver.org/spec/
## unreleased

### Added
- App notifications implemented. ADAM apps can register Prolog queries with the executor which will be checked on every perspective change. If the change adds a new match, it will trigger the publishing of a notifications via subscriptions in client interface [PR#475](https://github.com/coasys/ad4m/pull/475), as well as calling a web hook if given [PR#482](https://github.com/coasys/ad4m/pull/482)
- Support ADAM executor hosting service alpha [PR#474](https://github.com/coasys/ad4m/pull/474)
- Complete instructions in README [PR#473](https://github.com/coasys/ad4m/pull/473)

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion connect/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export default class Ad4mConnect {
localStorage.setItem('hosting_token', data.token);

let token = localStorage.getItem('hosting_token');

const response2 = await fetch('https://hosting.ad4m.dev/api/service/info', {
method: 'GET',
headers: {
Expand Down
21 changes: 0 additions & 21 deletions core/src/runtime/RuntimeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,11 @@ export class RuntimeClient {
this.#messageReceivedCallbacks = []
this.#exceptionOccurredCallbacks = []
this.#notificationTriggeredCallbacks = []
this.#notificationRequestedCallbacks = []

if(subscribe) {
this.subscribeMessageReceived()
this.subscribeExceptionOccurred()
this.subscribeNotificationTriggered()
this.subscribeNotificationRequested()
}
}

Expand Down Expand Up @@ -323,10 +321,6 @@ export class RuntimeClient {
this.#notificationTriggeredCallbacks.push(cb)
}

addNotificationRequestedCallback(cb: NotificationRequestedCallback) {
this.#notificationRequestedCallbacks.push(cb)
}

subscribeNotificationTriggered() {
this.#apolloClient.subscribe({
query: gql` subscription {
Expand All @@ -342,21 +336,6 @@ export class RuntimeClient {
})
}

subscribeNotificationRequested() {
this.#apolloClient.subscribe({
query: gql` subscription {
runtimeNotificationRequested { ${NOTIFICATION_FIELDS} }
}
`}).subscribe({
next: result => {
this.#notificationRequestedCallbacks.forEach(cb => {
cb(result.data.runtimeNotificationRequested)
})
},
error: (e) => console.error(e)
})
}

addMessageCallback(cb: MessageCallback) {
this.#messageReceivedCallbacks.push(cb)
}
Expand Down
17 changes: 0 additions & 17 deletions core/src/runtime/RuntimeResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,23 +317,6 @@ export default class RuntimeResolver {
return true
}

@Subscription({topics: RUNTIME_NOTIFICATION_REQUESTED_TOPIC, nullable: true})
runtimeNotificationRequested(): Notification {
return {
id: "test-id",
granted: false,
description: "Test description",
appName: "Test app name",
appUrl: "https://example.com",
appIconPath: "https://fluxsocial.io/favicon",
trigger: "triple(X, ad4m://has_type, flux://message)",
perspectiveIds: ["u983ud-jdhh38d"],
webhookUrl: "https://example.com/webhook",
webhookAuth: "test-auth",

}
}

@Subscription({topics: RUNTIME_NOTIFICATION_TRIGGERED_TOPIC, nullable: true})
runtimeNotificationTriggered(): TriggeredNotification {
return {
Expand Down
2 changes: 0 additions & 2 deletions core/src/subject/SubjectEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ export class SubjectEntity {

private async getData(id?: string) {
const tempId = id ?? this.#baseExpression;
console.log("SubjectEntity: getData")
let data = await this.#perspective.getSubjectData(this.#subjectClass, tempId)
console.log("SubjectEntity got data:", data)
Object.assign(this, data);
this.#baseExpression = tempId;
return this
Expand Down
43 changes: 43 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rust-executor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ scryer-prolog = { version = "0.9.4" }
# scryer-prolog = { path = "../../scryer-prolog", features = ["multi_thread"] }

ad4m-client = { path = "../rust-client", version="0.10.0-prerelease" }
reqwest = { version = "0.11.20", features = ["json", "native-tls"] }

rusqlite = { version = "0.29.0", features = ["bundled"] }
fake = { version = "2.9.2", features = ["derive"] }
Expand Down
16 changes: 15 additions & 1 deletion rust-executor/src/perspectives/perspective_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -900,11 +900,25 @@ impl PerspectiveInstance {
trigger_match: prolog_resolution_to_string(QueryResolution::Matches(matches))
};

let message = serde_json::to_string(&payload).unwrap();

if let Ok(_) = url::Url::parse(&notification.webhook_url) {
log::info!("Notification webhook - posting to {:?}", notification.webhook_url);
let client = reqwest::Client::new();
let res = client.post(&notification.webhook_url)
.bearer_auth(&notification.webhook_auth)
.header("Content-Type", "application/json")
.body(message.clone())
.send()
.await;
log::info!("Notification webhook response: {:?}", res);
}

get_global_pubsub()
.await
.publish(
&RUNTIME_NOTIFICATION_TRIGGERED_TOPIC,
&serde_json::to_string(&payload).unwrap(),
&message,
)
.await;
}
Expand Down
8 changes: 6 additions & 2 deletions tests/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@
"prepare-test:windows": "powershell -ExecutionPolicy Bypass -File ./scripts/build-test-language.ps1 && powershell -ExecutionPolicy Bypass -File ./scripts/prepareTestDirectory.ps1 && deno run --allow-all scripts/get-builtin-test-langs.js && pnpm run inject-language-language && pnpm run publish-test-languages && pnpm run inject-publishing-agent",
"inject-language-language": "node scripts/injectLanguageLanguageBundle.js",
"inject-publishing-agent": "node scripts/injectPublishingAgent.js",
"publish-test-languages": "node --no-warnings=ExperimentalWarning --experimental-specifier-resolution=node --loader ts-node/esm ./utils/publishTestLangs.ts"
"publish-test-languages": "node --no-warnings=ExperimentalWarning --experimental-specifier-resolution=node --loader ts-node/esm ./utils/publishTestLangs.ts",
"test-single-prepare": "node scripts/cleanTestingData.js && pnpm run prepare-test && node scripts/cleanup.js"
},
"devDependencies": {
"@apollo/client": "3.7.10",
"@peculiar/webcrypto": "^1.1.7",
"@coasys/ad4m": "link:../../core",
"@peculiar/webcrypto": "^1.1.7",
"@types/chai": "*",
"@types/chai-as-promised": "*",
"@types/expect": "*",
Expand All @@ -38,11 +39,14 @@
"@types/sinon": "*",
"@types/uuid": "^8.3.0",
"@types/ws": "^7.4.0",
"body-parser": "^1.20.2",
"chai": "*",
"chai-as-promised": "*",
"express": "4.18.2",
"faker": "^5.1.0",
"fs-extra": "11.2.0",
"graphql-ws": "^5.14.2",
"http": "0.0.1-security",
"json-stable-stringify": "^1.1.0",
"kill-process-by-name": "^1.0.5",
"mocha": "*",
Expand Down
113 changes: 113 additions & 0 deletions tests/js/tests/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import { Notification, NotificationInput, TriggeredNotification } from '@coasys/
import sinon from 'sinon';
import { sleep } from '../utils/utils';
import { ExceptionType, Link } from '@coasys/ad4m';
// Imports needed for webhook tests:
// (deactivated for now because these imports break the test suite on CI)
// (( local execution works - I leave this here for manualy local testing ))
//import express from 'express';
//import bodyParser from 'body-parser';
//import { Server } from 'http';

const PERSPECT3VISM_AGENT = "did:key:zQ3shkkuZLvqeFgHdgZgFMUx8VGkgVWsLA83w2oekhZxoCW2n"
const DIFF_SYNC_OFFICIAL = fs.readFileSync("./scripts/perspective-diff-sync-hash").toString();
Expand Down Expand Up @@ -295,5 +301,112 @@ export default function runtimeTests(testContext: TestContext) {
//@ts-ignore
expect(match.Target).to.equal("test://target2")
})



// See comments on the imports at the top
// breaks CI for some reason but works locally
// leaving this here for manual local testing
/*
it("should trigger a notification and call the webhook", async () => {
const ad4mClient = testContext.ad4mClient!
const webhookUrl = 'http://localhost:8080/webhook';
const webhookAuth = 'Test Webhook Auth'
// Setup Express server
const app = express();
app.use(bodyParser.json());
let webhookCalled = false;
let webhookGotAuth = ""
let webhookGotBody = null
app.post('/webhook', (req, res) => {
webhookCalled = true;
webhookGotAuth = req.headers['authorization']?.substring("Bearer ".length)||"";
webhookGotBody = req.body;
res.status(200).send({ success: true });
});
let server: Server|void
let serverRunning = new Promise<void>((done) => {
server = app.listen(8080, () => {
console.log('Test server running on port 8080');
done()
});
})
await serverRunning
let triggerPredicate = "ad4m://notification_webhook"
let notificationPerspective = await ad4mClient.perspective.add("notification test perspective")
let otherPerspective = await ad4mClient.perspective.add("other perspective")
const notification: NotificationInput = {
description: "ad4m://notification predicate used",
appName: "ADAM tests",
appUrl: "Test App URL",
appIconPath: "Test App Icon Path",
trigger: `triple(Source, "${triggerPredicate}", Target)`,
perspectiveIds: [notificationPerspective.uuid],
webhookUrl: webhookUrl,
webhookAuth: webhookAuth
}
// Request to install a new notification
const notificationId = await ad4mClient.runtime.requestInstallNotification(notification);
sleep(1000)
// Grant the notification
const granted = await ad4mClient.runtime.grantNotification(notificationId)
expect(granted).to.be.true
// Ensuring no false positives
await notificationPerspective.add(new Link({source: "control://source", target: "control://target"}))
await sleep(1000)
expect(webhookCalled).to.be.false
// Ensuring only selected perspectives will trigger
await otherPerspective.add(new Link({source: "control://source", predicate: triggerPredicate, target: "control://target"}))
await sleep(1000)
expect(webhookCalled).to.be.false
// Happy path
await notificationPerspective.add(new Link({source: "test://source", predicate: triggerPredicate, target: "test://target1"}))
await sleep(1000)
expect(webhookCalled).to.be.true
expect(webhookGotAuth).to.equal(webhookAuth)
expect(webhookGotBody).to.be.not.be.null
let triggeredNotification = webhookGotBody as unknown as TriggeredNotification
let triggerMatch = JSON.parse(triggeredNotification.triggerMatch)
expect(triggerMatch.length).to.equal(1)
let match = triggerMatch[0]
//@ts-ignore
expect(match.Source).to.equal("test://source")
//@ts-ignore
expect(match.Target).to.equal("test://target1")
// Reset webhookCalled for the next test
webhookCalled = false;
webhookGotAuth = ""
webhookGotBody = null
await notificationPerspective.add(new Link({source: "test://source", predicate: triggerPredicate, target: "test://target2"}))
await sleep(1000)
expect(webhookCalled).to.be.true
expect(webhookGotAuth).to.equal(webhookAuth)
triggeredNotification = webhookGotBody as unknown as TriggeredNotification
triggerMatch = JSON.parse(triggeredNotification.triggerMatch)
expect(triggerMatch.length).to.equal(1)
match = triggerMatch[0]
//@ts-ignore
expect(match.Source).to.equal("test://source")
//@ts-ignore
expect(match.Target).to.equal("test://target2")
// Close the server after the test
//@ts-ignore
server!.close()
})
*/
}
}

0 comments on commit 51fcb1e

Please sign in to comment.