Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notifications 2 - web hook push #482

Merged
merged 31 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
116de5f
Add test for the notification webhook feature
lucksus May 24, 2024
dbbf399
Add missing declarations to new test
lucksus May 24, 2024
756af0e
chore: Add reqwest crate for HTTP requests and update dependencies
fayeed May 24, 2024
78ea9de
Test npm script for running a single test suite
lucksus May 24, 2024
69fb8bc
Merge branch 'dev' into notifications
lucksus May 24, 2024
5407622
Use express to fix webhook test
lucksus May 24, 2024
99cb1f8
Assert webhookAuth is delivered
lucksus May 24, 2024
ae8855b
Remove runtimeNotificationRequested subscription from JS definition a…
lucksus May 24, 2024
fb95124
Remove debug logs
lucksus May 24, 2024
2012f2f
Cargo.lock
lucksus May 24, 2024
2997a48
Assert notification body in webhook
lucksus May 24, 2024
8dbe294
Remove unused nock dep
lucksus May 24, 2024
8a0cc5d
Actually send notification data in webhook post
lucksus May 24, 2024
d4ba047
Set content-type and only post webhook if webhook_url actually is a w…
lucksus May 24, 2024
f9cb998
Fix webhook test assertion
lucksus May 24, 2024
8bb60de
fix: try connecting to websocket once
fayeed May 24, 2024
10022e5
pnpm-lock
lucksus May 24, 2024
5cbc30c
changelog
lucksus May 24, 2024
d2980f4
Merge branch 'notifications' of github.com:perspect3vism/ad4m-executo…
fayeed May 24, 2024
2149021
fix: Update Ad4mConnect class to assign the ad4mClient property
fayeed May 24, 2024
46f8933
fix: Reduce delay for reconnecting to websocket in Ad4mConnect class
fayeed May 24, 2024
8d244a2
Merge branch 'dev' into notifications
lucksus May 24, 2024
4823a42
Merge branch 'dev' into notifications
fayeed May 27, 2024
0392570
chore: Reduce delay for reconnecting to websocket in Ad4mConnect class
fayeed May 27, 2024
5e9a639
Revert "chore: Reduce delay for reconnecting to websocket in Ad4mConn…
lucksus May 27, 2024
f9890aa
Test if CI still breaks with new test commented out
lucksus May 27, 2024
9052a6c
Add imports back in
lucksus May 27, 2024
d13c5ec
Add packages used in new test to package.json & pnpm.lock
lucksus May 28, 2024
df77cd7
Add webhook test back in
lucksus May 28, 2024
6c4a49a
Deactivate new webhook test again due to CI issue, but leave in for m…
lucksus May 29, 2024
154945c
Merge branch 'dev' into notifications
lucksus May 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
})
*/
}
}