@evanp/activitypub-bot 0.12.1 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/lib/app.js +19 -1
  2. package/lib/index.js +2 -0
  3. package/lib/routes/collection.js +83 -67
  4. package/lib/routes/inbox.js +8 -0
  5. package/lib/routes/object.js +25 -5
  6. package/package.json +3 -3
  7. package/.github/dependabot.yml +0 -11
  8. package/.github/workflows/main.yml +0 -34
  9. package/.github/workflows/tag.yml +0 -106
  10. package/.nvmrc +0 -1
  11. package/Dockerfile +0 -17
  12. package/docs/activitypub.bot.drawio +0 -110
  13. package/tests/activitydistributor.test.js +0 -606
  14. package/tests/activityhandler.test.js +0 -2276
  15. package/tests/activitypubclient.test.js +0 -210
  16. package/tests/actorstorage.test.js +0 -283
  17. package/tests/app.test.js +0 -17
  18. package/tests/authorizer.test.js +0 -301
  19. package/tests/bot.donothing.test.js +0 -30
  20. package/tests/bot.ok.test.js +0 -101
  21. package/tests/botcontext.test.js +0 -720
  22. package/tests/botdatastorage.test.js +0 -88
  23. package/tests/botfactory.provincebotfactory.test.js +0 -430
  24. package/tests/digester.test.js +0 -56
  25. package/tests/fixtures/bots.js +0 -27
  26. package/tests/fixtures/eventloggingbot.js +0 -57
  27. package/tests/fixtures/provincebotfactory.js +0 -53
  28. package/tests/httpsignature.test.js +0 -199
  29. package/tests/httpsignatureauthenticator.test.js +0 -463
  30. package/tests/index.test.js +0 -10
  31. package/tests/keystorage.test.js +0 -124
  32. package/tests/microsyntax.test.js +0 -123
  33. package/tests/objectcache.test.js +0 -133
  34. package/tests/objectstorage.test.js +0 -149
  35. package/tests/remotekeystorage.test.js +0 -78
  36. package/tests/routes.actor.test.js +0 -214
  37. package/tests/routes.collection.test.js +0 -433
  38. package/tests/routes.health.test.js +0 -41
  39. package/tests/routes.inbox.test.js +0 -136
  40. package/tests/routes.object.test.js +0 -525
  41. package/tests/routes.server.test.js +0 -69
  42. package/tests/routes.sharedinbox.test.js +0 -473
  43. package/tests/routes.webfinger.test.js +0 -68
  44. package/tests/urlformatter.test.js +0 -164
  45. package/tests/utils/digest.js +0 -7
  46. package/tests/utils/nock.js +0 -499
package/lib/app.js CHANGED
@@ -140,12 +140,30 @@ export async function makeApp (databaseUrl, origin, bots, logLevel = 'silent') {
140
140
  app.use('/', serverRouter)
141
141
  app.use('/', userRouter)
142
142
  app.use('/', collectionRouter)
143
- app.use('/', objectRouter)
144
143
  app.use('/', inboxRouter)
144
+ app.use('/', objectRouter)
145
145
  app.use('/', healthRouter)
146
146
  app.use('/', webfingerRouter)
147
147
  app.use('/', sharedInboxRouter)
148
148
 
149
+ app.use(async (req, res) => {
150
+ if (req.accepts('json')) {
151
+ const status = 404
152
+ const title = http.STATUS_CODES[status] || 'Not Found'
153
+ res.status(status)
154
+ res.type('application/problem+json')
155
+ res.json({
156
+ type: 'about:blank',
157
+ title,
158
+ status,
159
+ detail: 'Not found',
160
+ instance: req.originalUrl
161
+ })
162
+ } else {
163
+ res.status(404).send('Not found')
164
+ }
165
+ })
166
+
149
167
  app.use((err, req, res, next) => {
150
168
  const { logger } = req.app.locals
151
169
  let status = 500
package/lib/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export { makeApp } from './app.js'
2
2
  export { default as Bot } from './bot.js'
3
3
  export { default as BotFactory } from './botfactory.js'
4
+ export { default as OKBot } from './bots/ok.js'
5
+ export { default as DoNothingBot } from './bots/donothing.js'
@@ -2,7 +2,9 @@ import express from 'express'
2
2
  import as2 from '../activitystreams.js'
3
3
  import createHttpError from 'http-errors'
4
4
  import BotMaker from '../botmaker.js'
5
+ import assert from 'node:assert'
5
6
 
7
+ const collections = ['outbox', 'liked', 'followers', 'following']
6
8
  const router = express.Router()
7
9
 
8
10
  async function filterAsync (array, asyncPredicate) {
@@ -16,81 +18,95 @@ async function filterAsync (array, asyncPredicate) {
16
18
  return array.filter((_, idx) => booleans[idx])
17
19
  }
18
20
 
19
- router.get('/user/:username/:collection', async (req, res, next) => {
20
- const { username, collection } = req.params
21
- const { actorStorage, bots } = req.app.locals
22
- const bot = await BotMaker.makeBot(bots, username)
23
- if (!bot) {
24
- return next(createHttpError(404, `User ${username} not found`))
25
- }
26
- if (collection === 'inbox') {
27
- return next(createHttpError(403, `No access to ${collection} collection`))
28
- }
29
- if (!['outbox', 'liked', 'followers', 'following'].includes(req.params.collection)) {
30
- return next(createHttpError(404,
31
- `No such collection ${collection} for user ${username}`))
21
+ // This got tricky because Express 5 doesn't let us add regexes to our routes,
22
+ // so the page routes conflict with the object routes in ./object.js. This
23
+ // format lets us define fixed routes for the 4 user collections that support
24
+ // GET
25
+
26
+ function collectionHandler (collection) {
27
+ assert.ok(collections.includes(collection))
28
+ return async (req, res, next) => {
29
+ const { username } = req.params
30
+ const { actorStorage, bots } = req.app.locals
31
+ const bot = await BotMaker.makeBot(bots, username)
32
+ if (!bot) {
33
+ return next(createHttpError(404, `User ${username} not found`))
34
+ }
35
+ const coll = await actorStorage.getCollection(username, collection)
36
+ res.status(200)
37
+ res.type(as2.mediaType)
38
+ res.end(await coll.prettyWrite({ useOriginalContext: true }))
32
39
  }
33
- const coll = await actorStorage.getCollection(username, collection)
34
- res.status(200)
35
- res.type(as2.mediaType)
36
- res.end(await coll.prettyWrite({ useOriginalContext: true }))
37
- })
40
+ }
38
41
 
39
- router.get('/user/:username/:collection/:n(\\d+)', async (req, res, next) => {
40
- const { username, collection, n } = req.params
41
- const { actorStorage, bots, authorizer, objectStorage, formatter, client } = req.app.locals
42
- const bot = await BotMaker.makeBot(bots, username)
42
+ function collectionPageHandler (collection) {
43
+ assert.ok(['outbox', 'liked', 'followers', 'following'].includes(collection))
44
+ return async (req, res, next) => {
45
+ const { username, n } = req.params
46
+ let pageNo
47
+ try {
48
+ pageNo = parseInt(n)
49
+ } catch (err) {
50
+ return next(createHttpError(400, `Invalid page ${n}`))
51
+ }
52
+ const { actorStorage, bots, authorizer, objectStorage, formatter, client } = req.app.locals
53
+ const bot = await BotMaker.makeBot(bots, username)
43
54
 
44
- if (!bot) {
45
- return next(createHttpError(404, `User ${username} not found`))
46
- }
55
+ if (!bot) {
56
+ return next(createHttpError(404, `User ${username} not found`))
57
+ }
47
58
 
48
- if (collection === 'inbox') {
49
- return next(createHttpError(403, `No access to ${collection} collection`))
50
- }
51
- if (!['outbox', 'liked', 'followers', 'following'].includes(collection)) {
52
- return next(createHttpError(404,
53
- `No such collection ${collection} for user ${username}`))
54
- }
55
- if (!await actorStorage.hasPage(username, collection, parseInt(n))) {
56
- return next(createHttpError(404, `No such page ${n} for collection ${collection} for user ${username}`))
57
- }
59
+ if (collection === 'inbox') {
60
+ return next(createHttpError(403, `No access to ${collection} collection`))
61
+ }
62
+ if (!await actorStorage.hasPage(username, collection, parseInt(n))) {
63
+ return next(createHttpError(404, `No such page ${n} for collection ${collection} for user ${username}`))
64
+ }
58
65
 
59
- let exported = null
66
+ let exported = null
60
67
 
61
- try {
62
- const id = req.auth?.subject
63
- const remote = (id) ? await as2.import({ id }) : null
64
- const page = await actorStorage.getCollectionPage(
65
- username,
66
- collection,
67
- parseInt(n)
68
- )
69
- exported = await page.export({ useOriginalContext: true })
68
+ try {
69
+ const id = req.auth?.subject
70
+ const remote = (id) ? await as2.import({ id }) : null
71
+ const page = await actorStorage.getCollectionPage(
72
+ username,
73
+ collection,
74
+ pageNo
75
+ )
76
+ exported = await page.export({ useOriginalContext: true })
70
77
 
71
- if (['outbox', 'liked'].includes(collection)) {
72
- exported.items = await filterAsync(exported.items, async (id) => {
73
- const object = (formatter.isLocal(id))
74
- ? await objectStorage.read(id)
75
- : await client.get(id)
76
- if (!object) {
77
- req.log.warn({ id }, 'could not load object')
78
- return false
79
- }
80
- req.log.debug({ id, object }, 'loaded object')
81
- return await authorizer.canRead(remote, object)
82
- })
78
+ if (['outbox', 'liked'].includes(collection)) {
79
+ exported.items = await filterAsync(exported.items, async (id) => {
80
+ const object = (formatter.isLocal(id))
81
+ ? await objectStorage.read(id)
82
+ : await client.get(id)
83
+ if (!object) {
84
+ req.log.warn({ id }, 'could not load object')
85
+ return false
86
+ }
87
+ req.log.debug({ id, object }, 'loaded object')
88
+ return await authorizer.canRead(remote, object)
89
+ })
90
+ }
91
+ } catch (error) {
92
+ req.log.error(
93
+ { err: error, username, collection, n },
94
+ 'error loading collection page'
95
+ )
96
+ return next(createHttpError(500, 'Error loading collection page'))
83
97
  }
84
- } catch (error) {
85
- req.log.error(
86
- { err: error, username, collection, n },
87
- 'error loading collection page'
88
- )
89
- return next(createHttpError(500, 'Error loading collection page'))
98
+ res.status(200)
99
+ res.type(as2.mediaType)
100
+ res.end(JSON.stringify(exported))
90
101
  }
91
- res.status(200)
92
- res.type(as2.mediaType)
93
- res.end(JSON.stringify(exported))
94
- })
102
+ }
103
+
104
+ for (const collection of collections) {
105
+ router.get(`/user/:username/${collection}`, collectionHandler(collection))
106
+ router.get(
107
+ `/user/:username/${collection}/:n`,
108
+ collectionPageHandler(collection)
109
+ )
110
+ }
95
111
 
96
112
  export default router
@@ -67,4 +67,12 @@ router.post('/user/:username/inbox', async (req, res, next) => {
67
67
  res.send('OK')
68
68
  })
69
69
 
70
+ router.get('/user/:username/inbox', async (req, res, next) => {
71
+ return next(createHttpError(403, `No access to inbox collection`))
72
+ })
73
+
74
+ router.get('/user/:username/inbox/:n', async (req, res, next) => {
75
+ return next(createHttpError(403, `No access to inbox collection`))
76
+ })
77
+
70
78
  export default router
@@ -6,8 +6,11 @@ const router = express.Router()
6
6
 
7
7
  export default router
8
8
 
9
- router.get('/user/:username/:type/:nanoid([A-Za-z0-9_\\-]{21})', async (req, res, next) => {
9
+ router.get('/user/:username/:type/:nanoid', async (req, res, next) => {
10
10
  const { username, type, nanoid } = req.params
11
+ if (!nanoid.match(/^[A-Za-z0-9_\\-]{21}$/)) {
12
+ return next(createHttpError(400, `Invalid nanoid ${nanoid}`))
13
+ }
11
14
  const { objectStorage, formatter, authorizer } = req.app.locals
12
15
  const id = formatter.format({ username, type, nanoid })
13
16
  const object = await objectStorage.read(id)
@@ -23,12 +26,16 @@ router.get('/user/:username/:type/:nanoid([A-Za-z0-9_\\-]{21})', async (req, res
23
26
  res.end(await object.prettyWrite())
24
27
  })
25
28
 
26
- router.get('/user/:username/:type/:nanoid([A-Za-z0-9_\\-]{21})/:collection', async (req, res, next) => {
29
+ router.get('/user/:username/:type/:nanoid/:collection', async (req, res, next) => {
27
30
  const { objectStorage, formatter, authorizer } = req.app.locals
28
31
  if (!['replies', 'likes', 'shares', 'thread'].includes(req.params.collection)) {
29
32
  return next(createHttpError(404, 'Not Found'))
30
33
  }
31
- const id = formatter.format({ username: req.params.username, type: req.params.type, nanoid: req.params.nanoid })
34
+ const { username, type, nanoid } = req.params
35
+ if (!nanoid.match(/^[A-Za-z0-9_\\-]{21}$/)) {
36
+ return next(createHttpError(400, `Invalid nanoid ${nanoid}`))
37
+ }
38
+ const id = formatter.format({ username, type, nanoid })
32
39
  const object = await objectStorage.read(id)
33
40
  if (!object) {
34
41
  return next(createHttpError(404, 'Not Found'))
@@ -43,8 +50,17 @@ router.get('/user/:username/:type/:nanoid([A-Za-z0-9_\\-]{21})/:collection', asy
43
50
  res.end(await collection.prettyWrite({ useOriginalContext: true }))
44
51
  })
45
52
 
46
- router.get('/user/:username/:type/:nanoid([A-Za-z0-9_\\-]{21})/:collection/:n(\\d+)', async (req, res, next) => {
53
+ router.get('/user/:username/:type/:nanoid/:collection/:n', async (req, res, next) => {
47
54
  const { username, type, nanoid, collection, n } = req.params
55
+ let pageNo
56
+ try {
57
+ pageNo = parseInt(n)
58
+ } catch (err) {
59
+ return next(createHttpError(400, `Invalid page ${n}`))
60
+ }
61
+ if (!nanoid.match(/^[A-Za-z0-9_\\-]{21}$/)) {
62
+ return next(createHttpError(400, `Invalid nanoid ${nanoid}`))
63
+ }
48
64
  const { objectStorage, formatter, authorizer } = req.app.locals
49
65
  if (!['replies', 'likes', 'shares', 'thread'].includes(req.params.collection)) {
50
66
  return next(createHttpError(404, 'Not Found'))
@@ -58,7 +74,11 @@ router.get('/user/:username/:type/:nanoid([A-Za-z0-9_\\-]{21})/:collection/:n(\\
58
74
  if (!await authorizer.canRead(remote, object)) {
59
75
  return next(createHttpError(403, 'Forbidden'))
60
76
  }
61
- const collectionPage = await objectStorage.getCollectionPage(id, collection, parseInt(n))
77
+ const collectionPage = await objectStorage.getCollectionPage(
78
+ id,
79
+ collection,
80
+ pageNo
81
+ )
62
82
  const exported = await collectionPage.export({ useOriginalContext: true })
63
83
  if (!Array.isArray(exported.items)) {
64
84
  exported.items = [exported.items]
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@evanp/activitypub-bot",
3
- "version": "0.12.1",
3
+ "version": "0.13.1",
4
4
  "description": "server-side ActivityPub bot framework",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "bin": {
8
- "activitypub-bot": "./bin/activitypub-bot.js"
8
+ "activitypub-bot": "bin/activitypub-bot.js"
9
9
  },
10
10
  "scripts": {
11
11
  "test": "NODE_ENV=test node --test",
@@ -30,7 +30,7 @@
30
30
  "dependencies": {
31
31
  "@isaacs/ttlcache": "^2.1.4",
32
32
  "activitystrea.ms": "3.2",
33
- "express": "^4.22.1",
33
+ "express": "^5.2.1",
34
34
  "http-errors": "^2.0.0",
35
35
  "humanhash": "^1.0.4",
36
36
  "lru-cache": "^11.1.0",
@@ -1,11 +0,0 @@
1
- # To get started with Dependabot version updates, you'll need to specify which
2
- # package ecosystems to update and where the package manifests are located.
3
- # Please see the documentation for all configuration options:
4
- # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5
-
6
- version: 2
7
- updates:
8
- - package-ecosystem: "npm" # See documentation for possible values
9
- directory: "/" # Location of package manifests
10
- schedule:
11
- interval: "weekly"
@@ -1,34 +0,0 @@
1
- name: Run tests on main
2
-
3
- on:
4
- workflow_dispatch:
5
- push:
6
- branches:
7
- - main
8
-
9
- permissions:
10
- contents: read
11
- packages: write
12
-
13
- jobs:
14
-
15
- runTests:
16
- runs-on: ubuntu-latest
17
- strategy:
18
- matrix:
19
- node-version: [20, 22, 24, 25]
20
- steps:
21
-
22
- - uses: actions/checkout@v4
23
-
24
- - uses: actions/setup-node@v4
25
- with:
26
- node-version: ${{ matrix.node-version }}
27
- cache: npm
28
- cache-dependency-path: package-lock.json
29
-
30
- - name: Install dependencies
31
- run: npm ci
32
-
33
- - name: Run tests
34
- run: npm run test
@@ -1,106 +0,0 @@
1
- name: Build and Push Docker Image
2
-
3
- on:
4
- push:
5
- tags:
6
- - 'v[0-9]+.[0-9]+.[0-9]+'
7
-
8
- permissions:
9
- contents: read
10
- packages: write
11
-
12
- jobs:
13
-
14
- runTests:
15
- runs-on: ubuntu-latest
16
- strategy:
17
- matrix:
18
- node-version: [20, 22, 24, 25]
19
- steps:
20
-
21
- - uses: actions/checkout@v4
22
-
23
- - uses: actions/setup-node@v4
24
- with:
25
- node-version: ${{ matrix.node-version }}
26
- cache: npm
27
- cache-dependency-path: package-lock.json
28
-
29
- - name: Install dependencies
30
- run: npm ci
31
-
32
- - name: Run tests
33
- run: npm run test
34
-
35
- publish-npm:
36
- needs: runTests
37
- runs-on: ubuntu-latest
38
-
39
- permissions:
40
- contents: read
41
- id-token: write
42
-
43
- steps:
44
- - name: Checkout code
45
- uses: actions/checkout@v4
46
-
47
- - name: Set up Node.js
48
- uses: actions/setup-node@v4
49
- with:
50
- node-version: 25
51
- registry-url: https://registry.npmjs.org
52
- cache: npm
53
- cache-dependency-path: package-lock.json
54
-
55
- - name: Publish to NPM
56
- run: npm publish --access public --provenance
57
-
58
- build-and-push:
59
- needs: publish-npm
60
- runs-on: ubuntu-latest
61
-
62
- permissions:
63
- contents: read
64
- packages: write
65
-
66
- steps:
67
- - name: Checkout code
68
- uses: actions/checkout@v4
69
-
70
- - name: Set up Docker Buildx
71
- uses: docker/setup-buildx-action@v3
72
-
73
- - name: Extract version components from tag
74
- id: get_version
75
- run: |
76
- FULL_VERSION=${GITHUB_REF#refs/tags/v}
77
- MAJOR=$(echo $FULL_VERSION | cut -d. -f1)
78
- MINOR=$(echo $FULL_VERSION | cut -d. -f2)
79
- PATCH=$(echo $FULL_VERSION | cut -d. -f3)
80
-
81
- echo "FULL_VERSION=$FULL_VERSION" >> $GITHUB_OUTPUT
82
- echo "MAJOR_VERSION=$MAJOR" >> $GITHUB_OUTPUT
83
- echo "MAJOR_MINOR_VERSION=$MAJOR.$MINOR" >> $GITHUB_OUTPUT
84
-
85
- - name: Login to GitHub Container Registry
86
- uses: docker/login-action@v3
87
- with:
88
- registry: ghcr.io
89
- username: ${{ github.actor }}
90
- password: ${{ secrets.GITHUB_TOKEN }}
91
-
92
- - name: Build and push Docker image
93
- uses: docker/build-push-action@v5
94
- with:
95
- context: .
96
- push: true
97
- platforms: linux/amd64,linux/arm64
98
- build-args: |
99
- PACKAGE_VERSION=${{ steps.get_version.outputs.FULL_VERSION }}
100
- tags: |
101
- ghcr.io/${{ github.repository }}:${{ steps.get_version.outputs.FULL_VERSION }}
102
- ghcr.io/${{ github.repository }}:${{ steps.get_version.outputs.MAJOR_MINOR_VERSION }}
103
- ghcr.io/${{ github.repository }}:${{ steps.get_version.outputs.MAJOR_VERSION }}
104
- ghcr.io/${{ github.repository }}:${{ github.sha }}
105
- cache-from: type=gha
106
- cache-to: type=gha,mode=max
package/.nvmrc DELETED
@@ -1 +0,0 @@
1
- 24
package/Dockerfile DELETED
@@ -1,17 +0,0 @@
1
- FROM node:24-alpine
2
-
3
- WORKDIR /app
4
-
5
- RUN apk add --no-cache libstdc++ sqlite sqlite-libs
6
-
7
- ARG PACKAGE_VERSION
8
-
9
- ENV DATABASE_URL=
10
- ENV ORIGIN=
11
- ENV PORT=
12
- ENV BOTS_CONFIG_FILE=
13
- ENV LOG_LEVEL=
14
-
15
- RUN npm install -g @evanp/activitypub-bot@${PACKAGE_VERSION:-latest}
16
-
17
- CMD ["activitypub-bot"]
@@ -1,110 +0,0 @@
1
- <mxfile host="app.diagrams.net" modified="2024-01-14T17:20:53.624Z" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0" etag="UDjGppbSRivOII49-YrU" version="22.1.16" type="github">
2
- <diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">
3
- <mxGraphModel dx="2074" dy="1057" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
4
- <root>
5
- <mxCell id="WIyWlLk6GJQsqaUBKTNV-0" />
6
- <mxCell id="WIyWlLk6GJQsqaUBKTNV-1" parent="WIyWlLk6GJQsqaUBKTNV-0" />
7
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--0" value="BotServer" style="swimlane;fontStyle=2;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
8
- <mxGeometry x="220" y="120" width="160" height="138" as="geometry">
9
- <mxRectangle x="230" y="140" width="160" height="26" as="alternateBounds" />
10
- </mxGeometry>
11
- </mxCell>
12
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--1" value="Name" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--0" vertex="1">
13
- <mxGeometry y="26" width="160" height="26" as="geometry" />
14
- </mxCell>
15
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--2" value="Phone Number" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rounded=0;shadow=0;html=0;" parent="zkfFHV4jXpPFQw0GAbJ--0" vertex="1">
16
- <mxGeometry y="52" width="160" height="26" as="geometry" />
17
- </mxCell>
18
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--3" value="Email Address" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rounded=0;shadow=0;html=0;" parent="zkfFHV4jXpPFQw0GAbJ--0" vertex="1">
19
- <mxGeometry y="78" width="160" height="26" as="geometry" />
20
- </mxCell>
21
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--4" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--0" vertex="1">
22
- <mxGeometry y="104" width="160" height="8" as="geometry" />
23
- </mxCell>
24
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--5" value="Purchase Parking Pass" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--0" vertex="1">
25
- <mxGeometry y="112" width="160" height="26" as="geometry" />
26
- </mxCell>
27
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--6" value="Activity" style="swimlane;fontStyle=0;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
28
- <mxGeometry x="120" y="360" width="160" height="138" as="geometry">
29
- <mxRectangle x="130" y="380" width="160" height="26" as="alternateBounds" />
30
- </mxGeometry>
31
- </mxCell>
32
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--7" value="Student Number" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--6" vertex="1">
33
- <mxGeometry y="26" width="160" height="26" as="geometry" />
34
- </mxCell>
35
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--8" value="Average Mark" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rounded=0;shadow=0;html=0;" parent="zkfFHV4jXpPFQw0GAbJ--6" vertex="1">
36
- <mxGeometry y="52" width="160" height="26" as="geometry" />
37
- </mxCell>
38
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--9" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--6" vertex="1">
39
- <mxGeometry y="78" width="160" height="8" as="geometry" />
40
- </mxCell>
41
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--10" value="Is Eligible To Enroll" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontStyle=4" parent="zkfFHV4jXpPFQw0GAbJ--6" vertex="1">
42
- <mxGeometry y="86" width="160" height="26" as="geometry" />
43
- </mxCell>
44
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--11" value="Get Seminars Taken" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--6" vertex="1">
45
- <mxGeometry y="112" width="160" height="26" as="geometry" />
46
- </mxCell>
47
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--12" value="" style="endArrow=block;endSize=10;endFill=0;shadow=0;strokeWidth=1;rounded=0;edgeStyle=elbowEdgeStyle;elbow=vertical;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="zkfFHV4jXpPFQw0GAbJ--6" target="zkfFHV4jXpPFQw0GAbJ--0" edge="1">
48
- <mxGeometry width="160" relative="1" as="geometry">
49
- <mxPoint x="200" y="203" as="sourcePoint" />
50
- <mxPoint x="200" y="203" as="targetPoint" />
51
- </mxGeometry>
52
- </mxCell>
53
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--13" value="Object" style="swimlane;fontStyle=0;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
54
- <mxGeometry x="330" y="360" width="160" height="70" as="geometry">
55
- <mxRectangle x="340" y="380" width="170" height="26" as="alternateBounds" />
56
- </mxGeometry>
57
- </mxCell>
58
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--14" value="Salary" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--13" vertex="1">
59
- <mxGeometry y="26" width="160" height="26" as="geometry" />
60
- </mxCell>
61
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--15" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--13" vertex="1">
62
- <mxGeometry y="52" width="160" height="8" as="geometry" />
63
- </mxCell>
64
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--16" value="" style="endArrow=block;endSize=10;endFill=0;shadow=0;strokeWidth=1;rounded=0;edgeStyle=elbowEdgeStyle;elbow=vertical;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="zkfFHV4jXpPFQw0GAbJ--13" target="zkfFHV4jXpPFQw0GAbJ--0" edge="1">
65
- <mxGeometry width="160" relative="1" as="geometry">
66
- <mxPoint x="210" y="373" as="sourcePoint" />
67
- <mxPoint x="310" y="271" as="targetPoint" />
68
- </mxGeometry>
69
- </mxCell>
70
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--17" value="Bot" style="swimlane;fontStyle=0;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
71
- <mxGeometry x="508" y="120" width="160" height="216" as="geometry">
72
- <mxRectangle x="550" y="140" width="160" height="26" as="alternateBounds" />
73
- </mxGeometry>
74
- </mxCell>
75
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--23" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--17" vertex="1">
76
- <mxGeometry y="26" width="160" height="8" as="geometry" />
77
- </mxCell>
78
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--24" value="+ powerUp(object)" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--17" vertex="1">
79
- <mxGeometry y="34" width="160" height="26" as="geometry" />
80
- </mxCell>
81
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--25" value="+ powerDown(object)" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--17" vertex="1">
82
- <mxGeometry y="60" width="160" height="26" as="geometry" />
83
- </mxCell>
84
- <mxCell id="dP12apakX7S5GaSoqi7g-0" value="+ onReply(object)" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="zkfFHV4jXpPFQw0GAbJ--17">
85
- <mxGeometry y="86" width="160" height="26" as="geometry" />
86
- </mxCell>
87
- <mxCell id="dP12apakX7S5GaSoqi7g-1" value="+ onMention(object)" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="zkfFHV4jXpPFQw0GAbJ--17">
88
- <mxGeometry y="112" width="160" height="26" as="geometry" />
89
- </mxCell>
90
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--26" value="" style="endArrow=open;shadow=0;strokeWidth=1;rounded=0;endFill=1;edgeStyle=elbowEdgeStyle;elbow=vertical;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="zkfFHV4jXpPFQw0GAbJ--0" target="zkfFHV4jXpPFQw0GAbJ--17" edge="1">
91
- <mxGeometry x="0.5" y="41" relative="1" as="geometry">
92
- <mxPoint x="380" y="192" as="sourcePoint" />
93
- <mxPoint x="540" y="192" as="targetPoint" />
94
- <mxPoint x="-40" y="32" as="offset" />
95
- </mxGeometry>
96
- </mxCell>
97
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--27" value="1" style="resizable=0;align=left;verticalAlign=bottom;labelBackgroundColor=none;fontSize=12;" parent="zkfFHV4jXpPFQw0GAbJ--26" connectable="0" vertex="1">
98
- <mxGeometry x="-1" relative="1" as="geometry">
99
- <mxPoint y="4" as="offset" />
100
- </mxGeometry>
101
- </mxCell>
102
- <mxCell id="zkfFHV4jXpPFQw0GAbJ--28" value="0.." style="resizable=0;align=right;verticalAlign=bottom;labelBackgroundColor=none;fontSize=12;" parent="zkfFHV4jXpPFQw0GAbJ--26" connectable="0" vertex="1">
103
- <mxGeometry x="1" relative="1" as="geometry">
104
- <mxPoint x="-7" y="4" as="offset" />
105
- </mxGeometry>
106
- </mxCell>
107
- </root>
108
- </mxGraphModel>
109
- </diagram>
110
- </mxfile>