mirror of
https://github.com/fluencelabs/fluent-pad
synced 2025-04-24 08:22:14 +00:00
Aqua HLL + AppConfig (#5)
* Switch to app.config based deployment * Rewrite user online status checks to peer is_connected api * Rewrite fluentpad into aquamarine
This commit is contained in:
parent
dffe660fa4
commit
f430b725d2
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.sh text=auto eol=lf
|
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.wasm
|
36
app/app.config.json
Normal file
36
app/app.config.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"services": {
|
||||
"history": {
|
||||
"dependencies": ["history_inmemory"],
|
||||
"node": "12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3"
|
||||
},
|
||||
"user_list": {
|
||||
"dependencies": ["user_list_inmemory"],
|
||||
"node": "12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3"
|
||||
}
|
||||
},
|
||||
"modules": {
|
||||
"history_inmemory": {
|
||||
"file": "history.wasm",
|
||||
"config": {
|
||||
"preopened_files": ["/tmp"],
|
||||
"mapped_dirs": { "history": "/tmp" }
|
||||
}
|
||||
},
|
||||
"user_list_inmemory": {
|
||||
"file": "user_list.wasm",
|
||||
"config": {}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"set_tetraplet": {
|
||||
"file": "set_tetraplet.air",
|
||||
"variables": {
|
||||
"function": "is_authenticated",
|
||||
"json_path": "$.is_authenticated"
|
||||
},
|
||||
"node": "12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3"
|
||||
}
|
||||
},
|
||||
"script_storage": {}
|
||||
}
|
42
app/app.config.with_storage.json
Normal file
42
app/app.config.with_storage.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"services": {
|
||||
"history": {
|
||||
"dependencies": ["history_inmemory"],
|
||||
"node": "12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3"
|
||||
},
|
||||
"user_list": {
|
||||
"dependencies": ["user_list_inmemory"],
|
||||
"node": "12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3"
|
||||
}
|
||||
},
|
||||
"modules": {
|
||||
"history_inmemory": {
|
||||
"file": "history.wasm",
|
||||
"config": {
|
||||
"preopened_files": ["/tmp"],
|
||||
"mapped_dirs": { "history": "/tmp" }
|
||||
}
|
||||
},
|
||||
"user_list_inmemory": {
|
||||
"file": "user_list.wasm",
|
||||
"config": {}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"set_tetraplet": {
|
||||
"file": "set_tetraplet.air",
|
||||
"variables": {
|
||||
"function": "is_authenticated",
|
||||
"json_path": "$.[\"is_authenticated\"]"
|
||||
},
|
||||
"node": "12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3"
|
||||
}
|
||||
},
|
||||
"script_storage": {
|
||||
"remove_disconnected": {
|
||||
"file": "remove_disconnected.air",
|
||||
"interval": 10,
|
||||
"node": "12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3"
|
||||
}
|
||||
}
|
||||
}
|
17
app/remove_disconnected.air
Normal file
17
app/remove_disconnected.air
Normal file
@ -0,0 +1,17 @@
|
||||
(seq
|
||||
(call %init_peer_id% ({{user_list}} "is_authenticated") [] token)
|
||||
(seq
|
||||
(call %init_peer_id% ({{user_list}} "get_users") [] all_users)
|
||||
(fold all_users.$.users! u
|
||||
(par
|
||||
(seq
|
||||
(call u.$.relay_id! ("peer" "is_connected") [u.$.peer_id!] is_connected)
|
||||
(match is_connected false
|
||||
(call %init_peer_id% ({{user_list}} "leave") [u.$.peer_id!])
|
||||
)
|
||||
)
|
||||
(next u)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
7
app/set_tetraplet.air
Normal file
7
app/set_tetraplet.air
Normal file
@ -0,0 +1,7 @@
|
||||
(seq
|
||||
(call init_relay ("op" "identity") [])
|
||||
(seq
|
||||
(call history__node (history "set_tetraplet") [user_list__node user_list function json_path] auth_result)
|
||||
(call %init_peer_id% (returnService "run") [auth_result])
|
||||
)
|
||||
)
|
20
build.sh
Normal file
20
build.sh
Normal file
@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
|
||||
(
|
||||
cd services/user-list-inmemory
|
||||
cargo update
|
||||
fce build --release
|
||||
)
|
||||
|
||||
(
|
||||
cd services/history-inmemory
|
||||
cargo update
|
||||
fce build --release
|
||||
)
|
||||
|
||||
rm -f app/user_list.wasm
|
||||
rm -f app/history.wasm
|
||||
|
||||
cp services/user-list-inmemory/target/wasm32-wasi/release/user_list.wasm app/
|
||||
cp services/history-inmemory/target/wasm32-wasi/release/history.wasm app/
|
||||
|
2
client/.gitignore
vendored
2
client/.gitignore
vendored
@ -22,3 +22,5 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.eslintcache
|
||||
|
||||
*.jar
|
77
client/aqua/app.aqua
Normal file
77
client/aqua/app.aqua
Normal file
@ -0,0 +1,77 @@
|
||||
import "@fluencelabs/aqua-lib/builtin.aqua"
|
||||
import "fluent-pad.aqua"
|
||||
import "history.aqua"
|
||||
|
||||
|
||||
func join(user: User) -> EmptyServiceResult:
|
||||
app <- AppConfig.getApp()
|
||||
on app.user_list.peer_id:
|
||||
UserList app.user_list.service_id
|
||||
res <- UserList.join(user)
|
||||
<- res
|
||||
|
||||
func getUserList() -> []User:
|
||||
app <- AppConfig.getApp()
|
||||
on app.user_list.peer_id:
|
||||
UserList app.user_list.service_id
|
||||
allUsers <- UserList.get_users()
|
||||
<- allUsers.users
|
||||
|
||||
func initAfterJoin(me: User) -> []User:
|
||||
allUsers <- getUserList()
|
||||
for user <- allUsers par:
|
||||
on user.relay_id:
|
||||
isOnline <- Peer.is_connected(user.peer_id)
|
||||
if isOnline:
|
||||
on user.peer_id via user.relay_id:
|
||||
FluentPad.notifyUserAdded(me, true)
|
||||
else:
|
||||
Op.identity()
|
||||
par FluentPad.notifyUserAdded(user, isOnline)
|
||||
<- allUsers
|
||||
|
||||
|
||||
func updateOnlineStatuses():
|
||||
allUsers <- getUserList()
|
||||
for user <- allUsers par:
|
||||
on user.peer_id via user.relay_id:
|
||||
isOnline <- Peer.is_connected(user.peer_id)
|
||||
FluentPad.notifyOnline(user.peer_id, isOnline)
|
||||
|
||||
func leave():
|
||||
app <- AppConfig.getApp()
|
||||
on app.user_list.peer_id:
|
||||
UserList app.user_list.service_id
|
||||
res <- UserList.leave(%init_peer_id%)
|
||||
allUsers <- getUserList()
|
||||
for user <- allUsers par:
|
||||
on user.peer_id via user.relay_id:
|
||||
FluentPad.notifyUserRemoved(%init_peer_id%)
|
||||
|
||||
func auth() -> AuthResult:
|
||||
app <- AppConfig.getApp()
|
||||
on app.user_list.peer_id:
|
||||
UserList app.user_list.service_id
|
||||
res <- UserList.is_authenticated()
|
||||
<- res
|
||||
|
||||
func getHistory() -> GetEntriesServiceResult:
|
||||
app <- AppConfig.getApp()
|
||||
authRes <- auth()
|
||||
on app.history.peer_id:
|
||||
History app.history.service_id
|
||||
res <- History.get_all(authRes.is_authenticated)
|
||||
<- res
|
||||
|
||||
func addEntry(entry: string, init_peer_id: PeerId) -> AddServiceResult:
|
||||
app <- AppConfig.getApp()
|
||||
authRes <- auth()
|
||||
on app.history.peer_id:
|
||||
History app.history.service_id
|
||||
res <- History.add(entry, authRes.is_authenticated)
|
||||
allUsers <- getUserList()
|
||||
for user <- allUsers par:
|
||||
if user.peer_id != init_peer_id:
|
||||
on user.peer_id via user.relay_id:
|
||||
FluentPad.notifyTextUpdate(entry, authRes.is_authenticated)
|
||||
<- res
|
3
client/aqua/common.aqua
Normal file
3
client/aqua/common.aqua
Normal file
@ -0,0 +1,3 @@
|
||||
data EmptyServiceResult:
|
||||
ret_code: s32
|
||||
err_msg: string
|
19
client/aqua/fluent-pad.aqua
Normal file
19
client/aqua/fluent-pad.aqua
Normal file
@ -0,0 +1,19 @@
|
||||
import "@fluencelabs/aqua-lib/builtin.aqua"
|
||||
import "user-list.aqua"
|
||||
|
||||
data ServiceInstance:
|
||||
peer_id: PeerId
|
||||
service_id: string
|
||||
|
||||
data App:
|
||||
history: ServiceInstance
|
||||
user_list: ServiceInstance
|
||||
|
||||
service FluentPad("fluence/fluent-pad"):
|
||||
notifyOnline(userPeerId: string, isOnline: bool)
|
||||
notifyUserAdded(currentUser: User, isOnline: bool)
|
||||
notifyUserRemoved(userPeerId: PeerId)
|
||||
notifyTextUpdate(changes: string, isAuthorized: bool)
|
||||
|
||||
service AppConfig("fluence/get-config"):
|
||||
getApp: -> App
|
21
client/aqua/history.aqua
Normal file
21
client/aqua/history.aqua
Normal file
@ -0,0 +1,21 @@
|
||||
import "common.aqua"
|
||||
|
||||
data AddServiceResult:
|
||||
ret_code: s32
|
||||
err_msg: string
|
||||
entry_id: u64
|
||||
|
||||
data HistoryEntry:
|
||||
id: u64
|
||||
body: string
|
||||
|
||||
data GetEntriesServiceResult:
|
||||
ret_code: s32
|
||||
err_msg: string
|
||||
entries: []HistoryEntry
|
||||
|
||||
service History:
|
||||
get_all: bool -> GetEntriesServiceResult
|
||||
get_last: u64, bool -> GetEntriesServiceResult
|
||||
add: string, bool -> AddServiceResult
|
||||
set_tetraplet: string, string, string, string -> EmptyServiceResult
|
24
client/aqua/user-list.aqua
Normal file
24
client/aqua/user-list.aqua
Normal file
@ -0,0 +1,24 @@
|
||||
import "@fluencelabs/aqua-lib/builtin.aqua"
|
||||
import "common.aqua"
|
||||
|
||||
data User:
|
||||
peer_id: PeerId
|
||||
relay_id: PeerId
|
||||
name: string
|
||||
|
||||
data GetUsersServiceResult:
|
||||
users: []User
|
||||
ret_code: s32
|
||||
err_msg: string
|
||||
|
||||
data AuthResult:
|
||||
ret_code: s32
|
||||
err_msg: string
|
||||
is_authenticated: bool
|
||||
|
||||
service UserList:
|
||||
is_authenticated: -> AuthResult
|
||||
get_users: -> GetUsersServiceResult
|
||||
join: User -> EmptyServiceResult
|
||||
leave: string -> EmptyServiceResult -- user peerId
|
||||
is_exists: string -> () -- user peerId
|
1878
client/package-lock.json
generated
1878
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,57 +1,60 @@
|
||||
{
|
||||
"name": "fluent-pad",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fluencelabs/fluence": "0.9.31",
|
||||
"@fluencelabs/fluence-network-environment": "1.0.8",
|
||||
"@testing-library/jest-dom": "^5.11.9",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^12.6.3",
|
||||
"@types/jest": "^26.0.19",
|
||||
"@types/node": "^12.19.16",
|
||||
"@types/react": "^16.14.3",
|
||||
"@types/react-dom": "^16.9.10",
|
||||
"automerge": "^0.14.2",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"lodash": "^4.17.20",
|
||||
"node-sass": "^4.14.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-scripts": "4.0.1",
|
||||
"react-toastify": "^7.0.3",
|
||||
"typescript": "^4.1.3",
|
||||
"web-vitals": "^0.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12
|
||||
"name": "fluent-pad",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fluencelabs/fluence": "^0.9.43",
|
||||
"@fluencelabs/fluence-network-environment": "1.0.8",
|
||||
"@testing-library/jest-dom": "^5.11.9",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^12.6.3",
|
||||
"@types/jest": "^26.0.19",
|
||||
"@types/node": "^12.19.16",
|
||||
"@types/react": "^16.14.3",
|
||||
"@types/react-dom": "^16.9.10",
|
||||
"automerge": "^0.14.2",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"lodash": "^4.17.20",
|
||||
"node-sass": "^4.14.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-scripts": "4.0.1",
|
||||
"react-toastify": "^7.0.3",
|
||||
"typescript": "^4.1.3",
|
||||
"web-vitals": "^0.2.4"
|
||||
},
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.168"
|
||||
}
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"compile-aqua": "aqua-cli -i ./aqua/ -o ./src/aqua/"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12
|
||||
},
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fluencelabs/aqua-cli": "^0.1.1-94",
|
||||
"@fluencelabs/aqua-lib": "0.1.1",
|
||||
"@types/lodash": "^4.14.168"
|
||||
}
|
||||
}
|
||||
|
56
client/src/app.json
Normal file
56
client/src/app.json
Normal file
@ -0,0 +1,56 @@
|
||||
{
|
||||
"services": {
|
||||
"history": {
|
||||
"dependencies": [
|
||||
"history_inmemory"
|
||||
],
|
||||
"node": "12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3",
|
||||
"hashDependencies": [
|
||||
"hash:6ee648216089b876a34353f485f1bf19dd863b861f0d798a0ac0cb65cc3b4e2f"
|
||||
],
|
||||
"blueprint_id": "5df598924434974291d98c6d72e0922f2e8cbd7f7f291ca44f1b2f8e198f421b",
|
||||
"id": "afed8e34-f399-4e55-9d1f-c5c58296ad7a"
|
||||
},
|
||||
"user_list": {
|
||||
"dependencies": [
|
||||
"user_list_inmemory"
|
||||
],
|
||||
"node": "12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3",
|
||||
"hashDependencies": [
|
||||
"hash:691ebcd74409bd931a9a68d0e0001f8d9d4b8482c8aaac3184e28c2a322e5a82"
|
||||
],
|
||||
"blueprint_id": "d46fb511eb5a5dcea7b5a6777e04f95d48cbbe9fde040bd6bd69b4d9cd8fbb88",
|
||||
"id": "5622f669-3699-430f-9f4e-a1cb117cc0e2"
|
||||
}
|
||||
},
|
||||
"modules": {
|
||||
"history_inmemory": {
|
||||
"file": "history.wasm",
|
||||
"config": {
|
||||
"preopened_files": [
|
||||
"/tmp"
|
||||
],
|
||||
"mapped_dirs": {
|
||||
"history": "/tmp"
|
||||
}
|
||||
},
|
||||
"hash": "6ee648216089b876a34353f485f1bf19dd863b861f0d798a0ac0cb65cc3b4e2f"
|
||||
},
|
||||
"user_list_inmemory": {
|
||||
"file": "user_list.wasm",
|
||||
"config": {},
|
||||
"hash": "691ebcd74409bd931a9a68d0e0001f8d9d4b8482c8aaac3184e28c2a322e5a82"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"set_tetraplet": {
|
||||
"file": "set_tetraplet.air",
|
||||
"variables": {
|
||||
"function": "is_authenticated",
|
||||
"json_path": "$.is_authenticated"
|
||||
},
|
||||
"node": "12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3"
|
||||
}
|
||||
},
|
||||
"script_storage": {}
|
||||
}
|
@ -1,292 +0,0 @@
|
||||
import { FluenceClient, Particle, sendParticle, sendParticleAsFetch } from '@fluencelabs/fluence';
|
||||
|
||||
import {
|
||||
fluentPadServiceId,
|
||||
historyNodePeerId,
|
||||
historyServiceId,
|
||||
notifyOnlineFnName,
|
||||
notifyTextUpdateFnName,
|
||||
notifyUserAddedFnName,
|
||||
notifyUserRemovedFnName,
|
||||
userListNodePeerId,
|
||||
userListServiceId,
|
||||
} from './constants';
|
||||
|
||||
export interface ServiceResult {
|
||||
ret_code: number;
|
||||
err_msg: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
peer_id: string;
|
||||
relay_id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Entry {
|
||||
id: number;
|
||||
body: string;
|
||||
}
|
||||
|
||||
interface GetUsersResult extends ServiceResult {
|
||||
users: Array<User>;
|
||||
}
|
||||
|
||||
interface GetEntriesResult extends ServiceResult {
|
||||
entries: Entry[];
|
||||
}
|
||||
|
||||
const throwIfError = (result: ServiceResult) => {
|
||||
if (result.ret_code !== 0) {
|
||||
throw new Error(result.err_msg);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateOnlineStatuses = async (client: FluenceClient) => {
|
||||
const particle = new Particle(
|
||||
`
|
||||
(seq
|
||||
(call myRelay ("op" "identity") [])
|
||||
(seq
|
||||
(call userlistNode (userlist "get_users") [] allUsers)
|
||||
(fold allUsers.$.users! u
|
||||
(par
|
||||
(seq
|
||||
(call u.$.relay_id! ("op" "identity") [])
|
||||
(seq
|
||||
(call u.$.peer_id! ("op" "identity") [])
|
||||
(seq
|
||||
(call u.$.relay_id! ("op" "identity") [])
|
||||
(seq
|
||||
(call myRelay ("op" "identity") [])
|
||||
(call myPeerId (fluentPadServiceId notifyOnline) [u.$.peer_id!])
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(next u)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
`,
|
||||
{
|
||||
userlistNode: userListNodePeerId,
|
||||
userlist: userListServiceId,
|
||||
myRelay: client.relayPeerId,
|
||||
myPeerId: client.selfPeerId,
|
||||
fluentPadServiceId: fluentPadServiceId,
|
||||
notifyOnline: notifyOnlineFnName,
|
||||
},
|
||||
);
|
||||
|
||||
sendParticle(client, particle);
|
||||
};
|
||||
|
||||
export const notifySelfAdded = (client: FluenceClient, name: string) => {
|
||||
const particle = new Particle(
|
||||
`
|
||||
(seq
|
||||
(call myRelay ("op" "identity") [])
|
||||
(seq
|
||||
(call userlistNode (userlist "get_users") [] allUsers)
|
||||
(fold allUsers.$.users! u
|
||||
(par
|
||||
(seq
|
||||
(call u.$.relay_id! ("op" "identity") [])
|
||||
(call u.$.peer_id! (fluentPadServiceId notifyUserAdded) [myUser setOnline])
|
||||
)
|
||||
(next u)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
`,
|
||||
{
|
||||
userlistNode: userListNodePeerId,
|
||||
userlist: userListServiceId,
|
||||
myRelay: client.relayPeerId,
|
||||
myPeerId: client.selfPeerId,
|
||||
fluentPadServiceId: fluentPadServiceId,
|
||||
notifyUserAdded: notifyUserAddedFnName,
|
||||
myUser: [
|
||||
{
|
||||
name: name,
|
||||
peer_id: client.selfPeerId,
|
||||
relay_id: client.relayPeerId,
|
||||
},
|
||||
],
|
||||
setOnline: true,
|
||||
},
|
||||
);
|
||||
|
||||
sendParticle(client, particle);
|
||||
};
|
||||
|
||||
export const getUserList = async (client: FluenceClient) => {
|
||||
const particle = new Particle(
|
||||
`
|
||||
(seq
|
||||
(call myRelay ("op" "identity") [])
|
||||
(seq
|
||||
(call userlistNode (userlist "get_users") [] allUsers)
|
||||
(seq
|
||||
(call myRelay ("op" "identity") [])
|
||||
(call myPeerId (fluentPadServiceId notifyUserAdded) [allUsers.$.users!])
|
||||
)
|
||||
)
|
||||
)
|
||||
`,
|
||||
{
|
||||
userlistNode: userListNodePeerId,
|
||||
userlist: userListServiceId,
|
||||
myRelay: client.relayPeerId,
|
||||
myPeerId: client.selfPeerId,
|
||||
fluentPadServiceId: fluentPadServiceId,
|
||||
notifyUserAdded: notifyUserAddedFnName,
|
||||
immediately: true,
|
||||
},
|
||||
);
|
||||
|
||||
await sendParticle(client, particle);
|
||||
};
|
||||
|
||||
export const join = async (client: FluenceClient, nickName: string) => {
|
||||
const particle = new Particle(
|
||||
`
|
||||
(seq
|
||||
(call myRelay ("op" "identity") [])
|
||||
(seq
|
||||
(call userlistNode (userlist "join") [user] result)
|
||||
(seq
|
||||
(call myRelay ("op" "identity") [])
|
||||
(call myPeerId ("_callback" "join") [result])
|
||||
)
|
||||
)
|
||||
)
|
||||
`,
|
||||
{
|
||||
myRelay: client.relayPeerId,
|
||||
myPeerId: client.selfPeerId,
|
||||
user: {
|
||||
name: nickName,
|
||||
peer_id: client.selfPeerId,
|
||||
relay_id: client.relayPeerId,
|
||||
},
|
||||
userlist: userListServiceId,
|
||||
userlistNode: userListNodePeerId,
|
||||
},
|
||||
);
|
||||
|
||||
const [result] = await sendParticleAsFetch<[ServiceResult]>(client, particle, 'join');
|
||||
throwIfError(result);
|
||||
};
|
||||
|
||||
export const leave = async (client: FluenceClient) => {
|
||||
const particle = new Particle(
|
||||
`
|
||||
(seq
|
||||
(call myRelay ("op" "identity") [])
|
||||
(seq
|
||||
(call userlistNode (userlist "leave") [myPeerId])
|
||||
(seq
|
||||
(call userlistNode (userlist "get_users") [] allUsers)
|
||||
(fold allUsers.$.users! u
|
||||
(par
|
||||
(seq
|
||||
(call u.$.relay_id! ("op" "identity") [])
|
||||
(call u.$.peer_id! (fluentPadServiceId notifyUserRemoved) [myPeerId])
|
||||
)
|
||||
(next u)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
`,
|
||||
{
|
||||
userlistNode: userListNodePeerId,
|
||||
userlist: userListServiceId,
|
||||
myRelay: client.relayPeerId,
|
||||
myPeerId: client.selfPeerId,
|
||||
fluentPadServiceId: fluentPadServiceId,
|
||||
notifyUserRemoved: notifyUserRemovedFnName,
|
||||
},
|
||||
);
|
||||
|
||||
await sendParticle(client, particle);
|
||||
};
|
||||
|
||||
export const getHistory = async (client: FluenceClient) => {
|
||||
const particle = new Particle(
|
||||
`
|
||||
(seq
|
||||
(call myRelay ("op" "identity") [])
|
||||
(seq
|
||||
(call userlistNode (userlist "is_authenticated") [] token)
|
||||
(seq
|
||||
(call historyNode (history "get_all") [token.$.["is_authenticated"]] entries)
|
||||
(seq
|
||||
(call myRelay ("op" "identity") [])
|
||||
(call myPeerId ("_callback" "get_history") [entries])
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
`,
|
||||
{
|
||||
myRelay: client.relayPeerId,
|
||||
myPeerId: client.selfPeerId,
|
||||
userlist: userListServiceId,
|
||||
history: historyServiceId,
|
||||
userlistNode: userListNodePeerId,
|
||||
historyNode: historyNodePeerId,
|
||||
},
|
||||
);
|
||||
|
||||
const [result] = await sendParticleAsFetch<[GetEntriesResult]>(client, particle, 'get_history');
|
||||
throwIfError(result);
|
||||
return result.entries;
|
||||
};
|
||||
|
||||
export const addEntry = async (client: FluenceClient, entry: string) => {
|
||||
const particle = new Particle(
|
||||
`
|
||||
(seq
|
||||
(call myRelay ("op" "identity") [])
|
||||
(seq
|
||||
(call userlistNode (userlist "is_authenticated") [] token)
|
||||
(seq
|
||||
(call userlistNode (userlist "get_users") [] allUsers)
|
||||
(seq
|
||||
(call historyNode (history "add") [entry token.$.["is_authenticated"]])
|
||||
(fold allUsers.$.users! u
|
||||
(par
|
||||
(seq
|
||||
(call u.$.relay_id! ("op" "identity") [])
|
||||
(call u.$.peer_id! (fluentPadServiceId notifyTextUpdate) [myPeerId entry token.$.["is_authenticated"]])
|
||||
)
|
||||
(next u)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
`,
|
||||
|
||||
{
|
||||
userlistNode: userListNodePeerId,
|
||||
historyNode: historyNodePeerId,
|
||||
entry: entry,
|
||||
userlist: userListServiceId,
|
||||
history: historyServiceId,
|
||||
myRelay: client.relayPeerId,
|
||||
myPeerId: client.selfPeerId,
|
||||
fluentPadServiceId: fluentPadServiceId,
|
||||
notifyTextUpdate: notifyTextUpdateFnName,
|
||||
},
|
||||
);
|
||||
|
||||
await sendParticle(client, particle);
|
||||
};
|
@ -1,5 +1,4 @@
|
||||
|
||||
|
||||
import config from 'src/app.json';
|
||||
import { testNet } from '@fluencelabs/fluence-network-environment';
|
||||
|
||||
export const fluentPadServiceId = 'fluence/fluent-pad';
|
||||
@ -9,10 +8,23 @@ export const notifyUserAddedFnName = 'notifyUserAdded';
|
||||
export const notifyUserRemovedFnName = 'notifyUserRemoved';
|
||||
export const notifyTextUpdateFnName = 'notifyTextUpdate';
|
||||
|
||||
export const historyServiceId = '64ea579e-b863-4a42-b80c-e7b5ec1ab7fa';
|
||||
export const userListServiceId = '91041afe-0c3c-451a-9003-6bb92a570aae';
|
||||
export const userList = {
|
||||
peer_id: config.services.user_list.node,
|
||||
service_id: config.services.user_list.id,
|
||||
};
|
||||
|
||||
export const userListNodePeerId = testNet[3].peerId;
|
||||
export const historyNodePeerId = testNet[3].peerId;
|
||||
export const history = {
|
||||
peer_id: config.services.history.node,
|
||||
service_id: config.services.history.id,
|
||||
};
|
||||
|
||||
export const relayNode = testNet[0];
|
||||
export const fluentPadApp = {
|
||||
user_list: userList,
|
||||
history: history,
|
||||
};
|
||||
|
||||
// export const relayNode = testNet[0];
|
||||
export const relayNode = {
|
||||
multiaddr: '/ip4/127.0.0.1/tcp/4310/ws/p2p/12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3',
|
||||
peerId: '12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3',
|
||||
};
|
||||
|
@ -44,7 +44,7 @@ export class SyncClient<T = TextDoc> {
|
||||
const msg = JSON.parse(changes);
|
||||
this.connection.receiveMsg(msg);
|
||||
} catch (e) {
|
||||
console.log('Couldnt receive message', changes);
|
||||
console.error('Couldnt receive message', changes);
|
||||
}
|
||||
}
|
||||
|
||||
|
636
client/src/aqua/app.ts
Normal file
636
client/src/aqua/app.ts
Normal file
@ -0,0 +1,636 @@
|
||||
/**
|
||||
*
|
||||
* This file is auto-generated. Do not edit manually: changes may be erased.
|
||||
* Generated by Aqua compiler: https://github.com/fluencelabs/aqua/.
|
||||
* If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues
|
||||
*
|
||||
*/
|
||||
import { FluenceClient, PeerIdB58 } from '@fluencelabs/fluence';
|
||||
import { RequestFlowBuilder } from '@fluencelabs/fluence/dist/api.unstable';
|
||||
|
||||
export async function join(
|
||||
client: FluenceClient,
|
||||
user: { name: string; peer_id: string; relay_id: string },
|
||||
): Promise<{ err_msg: string; ret_code: number }> {
|
||||
let request;
|
||||
const promise = new Promise<{ err_msg: string; ret_code: number }>((resolve, reject) => {
|
||||
request = new RequestFlowBuilder()
|
||||
.disableInjections()
|
||||
.withRawScript(
|
||||
`
|
||||
(xor
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(call %init_peer_id% ("getDataSrv" "relay") [] relay)
|
||||
(call %init_peer_id% ("getDataSrv" "user") [] user)
|
||||
)
|
||||
(seq
|
||||
(call %init_peer_id% ("fluence/get-config" "getApp") [] app)
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call app.$.user_list.peer_id! (app.$.user_list.service_id! "join") [user] res)
|
||||
)
|
||||
)
|
||||
)
|
||||
(call relay ("op" "identity") [])
|
||||
)
|
||||
(call %init_peer_id% ("callbackSrv" "response") [res])
|
||||
)
|
||||
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error%])
|
||||
)
|
||||
|
||||
`,
|
||||
)
|
||||
.configHandler((h) => {
|
||||
h.on('getDataSrv', 'relay', () => {
|
||||
return client.relayPeerId!;
|
||||
});
|
||||
h.on('getRelayService', 'hasRelay', () => {
|
||||
// Not Used
|
||||
return client.relayPeerId !== undefined;
|
||||
});
|
||||
h.on('getDataSrv', 'user', () => {
|
||||
return user;
|
||||
});
|
||||
h.onEvent('callbackSrv', 'response', (args) => {
|
||||
const [res] = args;
|
||||
resolve(res);
|
||||
});
|
||||
|
||||
h.onEvent('errorHandlingSrv', 'error', (args) => {
|
||||
// assuming error is the single argument
|
||||
const [err] = args;
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
.handleScriptError(reject)
|
||||
.handleTimeout(() => {
|
||||
reject('Request timed out for join');
|
||||
})
|
||||
.build();
|
||||
});
|
||||
await client.initiateFlow(request);
|
||||
return promise;
|
||||
}
|
||||
|
||||
export async function getUserList(
|
||||
client: FluenceClient,
|
||||
): Promise<{ name: string; peer_id: string; relay_id: string }[]> {
|
||||
let request;
|
||||
const promise = new Promise<{ name: string; peer_id: string; relay_id: string }[]>((resolve, reject) => {
|
||||
request = new RequestFlowBuilder()
|
||||
.disableInjections()
|
||||
.withRawScript(
|
||||
`
|
||||
(xor
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(call %init_peer_id% ("getDataSrv" "relay") [] relay)
|
||||
(seq
|
||||
(call %init_peer_id% ("fluence/get-config" "getApp") [] app)
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call app.$.user_list.peer_id! (app.$.user_list.service_id! "get_users") [] allUsers)
|
||||
)
|
||||
)
|
||||
)
|
||||
(call relay ("op" "identity") [])
|
||||
)
|
||||
(call %init_peer_id% ("callbackSrv" "response") [allUsers.$.users!])
|
||||
)
|
||||
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error%])
|
||||
)
|
||||
|
||||
`,
|
||||
)
|
||||
.configHandler((h) => {
|
||||
h.on('getDataSrv', 'relay', () => {
|
||||
return client.relayPeerId!;
|
||||
});
|
||||
h.on('getRelayService', 'hasRelay', () => {
|
||||
// Not Used
|
||||
return client.relayPeerId !== undefined;
|
||||
});
|
||||
|
||||
h.onEvent('callbackSrv', 'response', (args) => {
|
||||
const [res] = args;
|
||||
resolve(res);
|
||||
});
|
||||
|
||||
h.onEvent('errorHandlingSrv', 'error', (args) => {
|
||||
// assuming error is the single argument
|
||||
const [err] = args;
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
.handleScriptError(reject)
|
||||
.handleTimeout(() => {
|
||||
reject('Request timed out for getUserList');
|
||||
})
|
||||
.build();
|
||||
});
|
||||
await client.initiateFlow(request);
|
||||
return promise;
|
||||
}
|
||||
|
||||
export async function initAfterJoin(
|
||||
client: FluenceClient,
|
||||
me: { name: string; peer_id: string; relay_id: string },
|
||||
): Promise<{ name: string; peer_id: string; relay_id: string }[]> {
|
||||
let request;
|
||||
const promise = new Promise<{ name: string; peer_id: string; relay_id: string }[]>((resolve, reject) => {
|
||||
request = new RequestFlowBuilder()
|
||||
.disableInjections()
|
||||
.withRawScript(
|
||||
`
|
||||
(xor
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(call %init_peer_id% ("getDataSrv" "relay") [] relay)
|
||||
(call %init_peer_id% ("getDataSrv" "me") [] me)
|
||||
)
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(call %init_peer_id% ("fluence/get-config" "getApp") [] app)
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call app.$.user_list.peer_id! (app.$.user_list.service_id! "get_users") [] allUsers)
|
||||
)
|
||||
)
|
||||
(call relay ("op" "identity") [])
|
||||
)
|
||||
(fold allUsers.$.users! user
|
||||
(par
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call user.$.relay_id! ("peer" "is_connected") [user.$.peer_id!] isOnline)
|
||||
)
|
||||
(call relay ("op" "identity") [])
|
||||
)
|
||||
(par
|
||||
(xor
|
||||
(match isOnline true
|
||||
(seq
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call user.$.relay_id! ("op" "identity") [])
|
||||
)
|
||||
(call user.$.peer_id! ("fluence/fluent-pad" "notifyUserAdded") [me true])
|
||||
)
|
||||
)
|
||||
(call %init_peer_id% ("op" "identity") [])
|
||||
)
|
||||
(call %init_peer_id% ("fluence/fluent-pad" "notifyUserAdded") [user isOnline])
|
||||
)
|
||||
)
|
||||
(next user)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(call %init_peer_id% ("callbackSrv" "response") [allUsers])
|
||||
)
|
||||
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error%])
|
||||
)
|
||||
|
||||
`,
|
||||
)
|
||||
.configHandler((h) => {
|
||||
h.on('getDataSrv', 'relay', () => {
|
||||
return client.relayPeerId!;
|
||||
});
|
||||
h.on('getRelayService', 'hasRelay', () => {
|
||||
// Not Used
|
||||
return client.relayPeerId !== undefined;
|
||||
});
|
||||
h.on('getDataSrv', 'me', () => {
|
||||
return me;
|
||||
});
|
||||
h.onEvent('callbackSrv', 'response', (args) => {
|
||||
const [res] = args;
|
||||
resolve(res);
|
||||
});
|
||||
|
||||
h.onEvent('errorHandlingSrv', 'error', (args) => {
|
||||
// assuming error is the single argument
|
||||
const [err] = args;
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
.handleScriptError(reject)
|
||||
.handleTimeout(() => {
|
||||
reject('Request timed out for initAfterJoin');
|
||||
})
|
||||
.build();
|
||||
});
|
||||
await client.initiateFlow(request);
|
||||
return promise;
|
||||
}
|
||||
|
||||
export async function updateOnlineStatuses(client: FluenceClient): Promise<void> {
|
||||
let request;
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
request = new RequestFlowBuilder()
|
||||
.disableInjections()
|
||||
.withRawScript(
|
||||
`
|
||||
(xor
|
||||
(seq
|
||||
(call %init_peer_id% ("getDataSrv" "relay") [] relay)
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(call %init_peer_id% ("fluence/get-config" "getApp") [] app)
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call app.$.user_list.peer_id! (app.$.user_list.service_id! "get_users") [] allUsers)
|
||||
)
|
||||
)
|
||||
(call relay ("op" "identity") [])
|
||||
)
|
||||
(fold allUsers.$.users! user
|
||||
(par
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call user.$.relay_id! ("op" "identity") [])
|
||||
)
|
||||
(call user.$.peer_id! ("peer" "is_connected") [user.$.peer_id!] isOnline)
|
||||
)
|
||||
(call user.$.relay_id! ("op" "identity") [])
|
||||
)
|
||||
(call relay ("op" "identity") [])
|
||||
)
|
||||
(call %init_peer_id% ("fluence/fluent-pad" "notifyOnline") [user.$.peer_id! isOnline])
|
||||
)
|
||||
(next user)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error%])
|
||||
)
|
||||
|
||||
`,
|
||||
)
|
||||
.configHandler((h) => {
|
||||
h.on('getDataSrv', 'relay', () => {
|
||||
return client.relayPeerId!;
|
||||
});
|
||||
h.on('getRelayService', 'hasRelay', () => {
|
||||
// Not Used
|
||||
return client.relayPeerId !== undefined;
|
||||
});
|
||||
|
||||
h.onEvent('errorHandlingSrv', 'error', (args) => {
|
||||
// assuming error is the single argument
|
||||
const [err] = args;
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
.handleScriptError(reject)
|
||||
.handleTimeout(() => {
|
||||
reject('Request timed out for updateOnlineStatuses');
|
||||
})
|
||||
.build();
|
||||
});
|
||||
await client.initiateFlow(request);
|
||||
return Promise.race([promise, Promise.resolve()]);
|
||||
}
|
||||
|
||||
export async function leave(client: FluenceClient): Promise<void> {
|
||||
let request;
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
request = new RequestFlowBuilder()
|
||||
.disableInjections()
|
||||
.withRawScript(
|
||||
`
|
||||
(xor
|
||||
(seq
|
||||
(call %init_peer_id% ("getDataSrv" "relay") [] relay)
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(call %init_peer_id% ("fluence/get-config" "getApp") [] app)
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call app.$.user_list.peer_id! (app.$.user_list.service_id! "leave") [%init_peer_id%] res)
|
||||
)
|
||||
)
|
||||
(call relay ("op" "identity") [])
|
||||
)
|
||||
(seq
|
||||
(call %init_peer_id% ("fluence/get-config" "getApp") [] app0)
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call app0.$.user_list.peer_id! (app0.$.user_list.service_id! "get_users") [] allUsers)
|
||||
)
|
||||
)
|
||||
)
|
||||
(call relay ("op" "identity") [])
|
||||
)
|
||||
(fold allUsers.$.users! user
|
||||
(par
|
||||
(seq
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call user.$.relay_id! ("op" "identity") [])
|
||||
)
|
||||
(call user.$.peer_id! ("fluence/fluent-pad" "notifyUserRemoved") [%init_peer_id%])
|
||||
)
|
||||
(next user)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error%])
|
||||
)
|
||||
|
||||
`,
|
||||
)
|
||||
.configHandler((h) => {
|
||||
h.on('getDataSrv', 'relay', () => {
|
||||
return client.relayPeerId!;
|
||||
});
|
||||
h.on('getRelayService', 'hasRelay', () => {
|
||||
// Not Used
|
||||
return client.relayPeerId !== undefined;
|
||||
});
|
||||
|
||||
h.onEvent('errorHandlingSrv', 'error', (args) => {
|
||||
// assuming error is the single argument
|
||||
const [err] = args;
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
.handleScriptError(reject)
|
||||
.handleTimeout(() => {
|
||||
reject('Request timed out for leave');
|
||||
})
|
||||
.build();
|
||||
});
|
||||
await client.initiateFlow(request);
|
||||
return Promise.race([promise, Promise.resolve()]);
|
||||
}
|
||||
|
||||
export async function auth(
|
||||
client: FluenceClient,
|
||||
): Promise<{ err_msg: string; is_authenticated: boolean; ret_code: number }> {
|
||||
let request;
|
||||
const promise = new Promise<{ err_msg: string; is_authenticated: boolean; ret_code: number }>((resolve, reject) => {
|
||||
request = new RequestFlowBuilder()
|
||||
.disableInjections()
|
||||
.withRawScript(
|
||||
`
|
||||
(xor
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(call %init_peer_id% ("getDataSrv" "relay") [] relay)
|
||||
(seq
|
||||
(call %init_peer_id% ("fluence/get-config" "getApp") [] app)
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call app.$.user_list.peer_id! (app.$.user_list.service_id! "is_authenticated") [] res)
|
||||
)
|
||||
)
|
||||
)
|
||||
(call relay ("op" "identity") [])
|
||||
)
|
||||
(call %init_peer_id% ("callbackSrv" "response") [res])
|
||||
)
|
||||
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error%])
|
||||
)
|
||||
|
||||
`,
|
||||
)
|
||||
.configHandler((h) => {
|
||||
h.on('getDataSrv', 'relay', () => {
|
||||
return client.relayPeerId!;
|
||||
});
|
||||
h.on('getRelayService', 'hasRelay', () => {
|
||||
// Not Used
|
||||
return client.relayPeerId !== undefined;
|
||||
});
|
||||
|
||||
h.onEvent('callbackSrv', 'response', (args) => {
|
||||
const [res] = args;
|
||||
resolve(res);
|
||||
});
|
||||
|
||||
h.onEvent('errorHandlingSrv', 'error', (args) => {
|
||||
// assuming error is the single argument
|
||||
const [err] = args;
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
.handleScriptError(reject)
|
||||
.handleTimeout(() => {
|
||||
reject('Request timed out for auth');
|
||||
})
|
||||
.build();
|
||||
});
|
||||
await client.initiateFlow(request);
|
||||
return promise;
|
||||
}
|
||||
|
||||
export async function getHistory(
|
||||
client: FluenceClient,
|
||||
): Promise<{ entries: { body: string; id: number }[]; err_msg: string; ret_code: number }> {
|
||||
let request;
|
||||
const promise = new Promise<{ entries: { body: string; id: number }[]; err_msg: string; ret_code: number }>(
|
||||
(resolve, reject) => {
|
||||
request = new RequestFlowBuilder()
|
||||
.disableInjections()
|
||||
.withRawScript(
|
||||
`
|
||||
(xor
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(call %init_peer_id% ("getDataSrv" "relay") [] relay)
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(call %init_peer_id% ("fluence/get-config" "getApp") [] app)
|
||||
(seq
|
||||
(call %init_peer_id% ("fluence/get-config" "getApp") [] app0)
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call app0.$.user_list.peer_id! (app0.$.user_list.service_id! "is_authenticated") [] res0)
|
||||
)
|
||||
)
|
||||
)
|
||||
(call relay ("op" "identity") [])
|
||||
)
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call app.$.history.peer_id! (app.$.history.service_id! "get_all") [res0.$.is_authenticated!] res)
|
||||
)
|
||||
)
|
||||
)
|
||||
(call relay ("op" "identity") [])
|
||||
)
|
||||
(call %init_peer_id% ("callbackSrv" "response") [res])
|
||||
)
|
||||
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error%])
|
||||
)
|
||||
|
||||
`,
|
||||
)
|
||||
.configHandler((h) => {
|
||||
h.on('getDataSrv', 'relay', () => {
|
||||
return client.relayPeerId!;
|
||||
});
|
||||
h.on('getRelayService', 'hasRelay', () => {
|
||||
// Not Used
|
||||
return client.relayPeerId !== undefined;
|
||||
});
|
||||
|
||||
h.onEvent('callbackSrv', 'response', (args) => {
|
||||
const [res] = args;
|
||||
resolve(res);
|
||||
});
|
||||
|
||||
h.onEvent('errorHandlingSrv', 'error', (args) => {
|
||||
// assuming error is the single argument
|
||||
const [err] = args;
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
.handleScriptError(reject)
|
||||
.handleTimeout(() => {
|
||||
reject('Request timed out for getHistory');
|
||||
})
|
||||
.build();
|
||||
},
|
||||
);
|
||||
await client.initiateFlow(request);
|
||||
return promise;
|
||||
}
|
||||
|
||||
export async function addEntry(
|
||||
client: FluenceClient,
|
||||
entry: string,
|
||||
init_peer_id: string,
|
||||
): Promise<{ entry_id: number; err_msg: string; ret_code: number }> {
|
||||
let request;
|
||||
const promise = new Promise<{ entry_id: number; err_msg: string; ret_code: number }>((resolve, reject) => {
|
||||
request = new RequestFlowBuilder()
|
||||
.disableInjections()
|
||||
.withRawScript(
|
||||
`
|
||||
(xor
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(call %init_peer_id% ("getDataSrv" "relay") [] relay)
|
||||
(call %init_peer_id% ("getDataSrv" "entry") [] entry)
|
||||
)
|
||||
(call %init_peer_id% ("getDataSrv" "init_peer_id") [] init_peer_id)
|
||||
)
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(seq
|
||||
(call %init_peer_id% ("fluence/get-config" "getApp") [] app)
|
||||
(seq
|
||||
(call %init_peer_id% ("fluence/get-config" "getApp") [] app0)
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call app0.$.user_list.peer_id! (app0.$.user_list.service_id! "is_authenticated") [] res0)
|
||||
)
|
||||
)
|
||||
)
|
||||
(call relay ("op" "identity") [])
|
||||
)
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call app.$.history.peer_id! (app.$.history.service_id! "add") [entry res0.$.is_authenticated!] res)
|
||||
)
|
||||
)
|
||||
(call relay ("op" "identity") [])
|
||||
)
|
||||
(seq
|
||||
(call %init_peer_id% ("fluence/get-config" "getApp") [] app1)
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call app1.$.user_list.peer_id! (app1.$.user_list.service_id! "get_users") [] allUsers)
|
||||
)
|
||||
)
|
||||
)
|
||||
(call relay ("op" "identity") [])
|
||||
)
|
||||
(fold allUsers.$.users! user
|
||||
(par
|
||||
(mismatch user.$.peer_id! init_peer_id
|
||||
(seq
|
||||
(seq
|
||||
(call relay ("op" "identity") [])
|
||||
(call user.$.relay_id! ("op" "identity") [])
|
||||
)
|
||||
(call user.$.peer_id! ("fluence/fluent-pad" "notifyTextUpdate") [entry res0.$.is_authenticated!])
|
||||
)
|
||||
)
|
||||
(next user)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(call %init_peer_id% ("callbackSrv" "response") [res])
|
||||
)
|
||||
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error%])
|
||||
)
|
||||
|
||||
`,
|
||||
)
|
||||
.configHandler((h) => {
|
||||
h.on('getDataSrv', 'relay', () => {
|
||||
return client.relayPeerId!;
|
||||
});
|
||||
h.on('getRelayService', 'hasRelay', () => {
|
||||
// Not Used
|
||||
return client.relayPeerId !== undefined;
|
||||
});
|
||||
h.on('getDataSrv', 'entry', () => {
|
||||
return entry;
|
||||
});
|
||||
h.on('getDataSrv', 'init_peer_id', () => {
|
||||
return init_peer_id;
|
||||
});
|
||||
h.onEvent('callbackSrv', 'response', (args) => {
|
||||
const [res] = args;
|
||||
resolve(res);
|
||||
});
|
||||
|
||||
h.onEvent('errorHandlingSrv', 'error', (args) => {
|
||||
// assuming error is the single argument
|
||||
const [err] = args;
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
.handleScriptError(reject)
|
||||
.handleTimeout(() => {
|
||||
reject('Request timed out for addEntry');
|
||||
})
|
||||
.build();
|
||||
});
|
||||
await client.initiateFlow(request);
|
||||
return promise;
|
||||
}
|
@ -5,11 +5,24 @@ import './App.scss';
|
||||
|
||||
import { FluenceClientContext } from '../app/FluenceClientContext';
|
||||
import { UserList } from './UserList';
|
||||
import * as api from 'src/app/api';
|
||||
import { CollaborativeEditor } from './CollaborativeEditor';
|
||||
import { relayNode } from 'src/app/constants';
|
||||
import { withErrorHandlingAsync } from './util';
|
||||
import { toast } from 'react-toastify';
|
||||
import { fluentPadApp, relayNode } from 'src/app/constants';
|
||||
import { CheckResponse, withErrorHandlingAsync } from './util';
|
||||
import { join, leave } from 'src/aqua/app';
|
||||
|
||||
const createClientEx = async (relay) => {
|
||||
const client = await createClient(relay);
|
||||
client.aquaCallHandler.on('fluence/get-config', 'getApp', () => {
|
||||
return fluentPadApp;
|
||||
});
|
||||
client.aquaCallHandler.on('fluence/get-config', 'get_init_peer_id', () => {
|
||||
return client.selfPeerId;
|
||||
});
|
||||
client.aquaCallHandler.on('fluence/get-config', 'get_init_relay', () => {
|
||||
return client.relayPeerId!;
|
||||
});
|
||||
return client;
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
const [client, setClient] = useState<FluenceClient | null>(null);
|
||||
@ -17,7 +30,7 @@ const App = () => {
|
||||
const [nickName, setNickName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
createClient(relayNode)
|
||||
createClientEx(relayNode)
|
||||
.then((client) => setClient(client))
|
||||
.catch((err) => console.log('Client initialization failed', err));
|
||||
}, []);
|
||||
@ -28,8 +41,14 @@ const App = () => {
|
||||
}
|
||||
|
||||
await withErrorHandlingAsync(async () => {
|
||||
await api.join(client, nickName);
|
||||
setIsInRoom(true);
|
||||
const res = await join(client, {
|
||||
peer_id: client.selfPeerId,
|
||||
relay_id: client.relayPeerId!,
|
||||
name: nickName,
|
||||
});
|
||||
if (CheckResponse(res)) {
|
||||
setIsInRoom(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -39,7 +58,7 @@ const App = () => {
|
||||
}
|
||||
|
||||
await withErrorHandlingAsync(async () => {
|
||||
await api.leave(client);
|
||||
await leave(client);
|
||||
setIsInRoom(false);
|
||||
});
|
||||
};
|
||||
|
@ -5,8 +5,8 @@ import { PeerIdB58, subscribeToEvent } from '@fluencelabs/fluence';
|
||||
import { fluentPadServiceId, notifyTextUpdateFnName } from 'src/app/constants';
|
||||
import { useFluenceClient } from '../app/FluenceClientContext';
|
||||
import { getUpdatedDocFromText, initDoc, SyncClient } from '../app/sync';
|
||||
import * as api from 'src/app/api';
|
||||
import { withErrorHandlingAsync } from './util';
|
||||
import { addEntry, getHistory } from 'src/aqua/app';
|
||||
|
||||
const broadcastUpdates = _.debounce((text: string, syncClient: SyncClient) => {
|
||||
let doc = syncClient.getDoc();
|
||||
@ -28,15 +28,17 @@ export const CollaborativeEditor = () => {
|
||||
|
||||
syncClient.handleSendChanges = (changes: string) => {
|
||||
withErrorHandlingAsync(async () => {
|
||||
await api.addEntry(client, changes);
|
||||
const res = await addEntry(client, changes, client.selfPeerId);
|
||||
if (res.ret_code !== 0) {
|
||||
throw new Error(
|
||||
`Failed to add message to history service, code=${res.ret_code}, message=${res.err_msg}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const unsub = subscribeToEvent(client, fluentPadServiceId, notifyTextUpdateFnName, (args, tetraplets) => {
|
||||
const [authorPeerId, changes, isAuthorized] = args as [PeerIdB58, string, boolean];
|
||||
if (authorPeerId === client.selfPeerId) {
|
||||
return;
|
||||
}
|
||||
const [changes, isAuthorized] = args as [string, boolean];
|
||||
|
||||
if (changes) {
|
||||
syncClient.receiveChanges(changes);
|
||||
@ -47,8 +49,8 @@ export const CollaborativeEditor = () => {
|
||||
|
||||
// don't block
|
||||
withErrorHandlingAsync(async () => {
|
||||
const res = await api.getHistory(client);
|
||||
for (let e of res) {
|
||||
const res = await getHistory(client);
|
||||
for (let e of res.entries) {
|
||||
syncClient.receiveChanges(e.body);
|
||||
}
|
||||
|
||||
|
@ -6,64 +6,61 @@ import {
|
||||
notifyUserRemovedFnName,
|
||||
} from 'src/app/constants';
|
||||
import { useFluenceClient } from '../app/FluenceClientContext';
|
||||
import * as api from 'src/app/api';
|
||||
import { PeerIdB58, subscribeToEvent } from '@fluencelabs/fluence';
|
||||
import { withErrorHandlingAsync } from './util';
|
||||
import { initAfterJoin, updateOnlineStatuses } from 'src/aqua/app';
|
||||
|
||||
interface User {
|
||||
id: PeerIdB58;
|
||||
name: string;
|
||||
isOnline: boolean;
|
||||
shouldBecomeOnline: boolean;
|
||||
}
|
||||
|
||||
const turnUserAsOfflineCandidate = (u: User): User => {
|
||||
return {
|
||||
...u,
|
||||
isOnline: u.shouldBecomeOnline,
|
||||
shouldBecomeOnline: false,
|
||||
};
|
||||
};
|
||||
interface ApiUser {
|
||||
name: string;
|
||||
peer_id: string;
|
||||
relay_id: string;
|
||||
}
|
||||
|
||||
const refreshTimeoutMs = 2000;
|
||||
const refreshOnlineStatusTimeoutMs = 10000;
|
||||
|
||||
export const UserList = (props: { selfName: string }) => {
|
||||
const client = useFluenceClient()!;
|
||||
const [users, setUsers] = useState<Map<PeerIdB58, User>>(new Map());
|
||||
|
||||
const updateOnlineStatus = (user, onlineStatus) => {
|
||||
setUsers((prev) => {
|
||||
const result = new Map(prev);
|
||||
const u = result.get(user);
|
||||
if (u) {
|
||||
result.set(user, { ...u, isOnline: onlineStatus });
|
||||
}
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const listRefreshTimer = setInterval(() => {
|
||||
setUsers((prev) => {
|
||||
const newUsers = Array.from(prev).map(
|
||||
([key, user]) => [key, turnUserAsOfflineCandidate(user)] as const,
|
||||
);
|
||||
return new Map(newUsers);
|
||||
});
|
||||
|
||||
// don't block
|
||||
withErrorHandlingAsync(async () => {
|
||||
await api.updateOnlineStatuses(client);
|
||||
// await updateOnlineStatuses(client);
|
||||
});
|
||||
}, refreshTimeoutMs);
|
||||
}, refreshOnlineStatusTimeoutMs);
|
||||
|
||||
const unsub1 = subscribeToEvent(client, fluentPadServiceId, notifyUserAddedFnName, (args, _) => {
|
||||
const [users, setOnline] = args as [api.User[], boolean];
|
||||
const [user, isOnline] = args as [ApiUser, boolean];
|
||||
setUsers((prev) => {
|
||||
const u = user;
|
||||
const result = new Map(prev);
|
||||
for (let u of users) {
|
||||
if (result.has(u.peer_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isCurrentUser = u.peer_id === client.selfPeerId;
|
||||
|
||||
result.set(u.peer_id, {
|
||||
name: u.name,
|
||||
id: u.peer_id,
|
||||
isOnline: isCurrentUser || setOnline,
|
||||
shouldBecomeOnline: isCurrentUser || setOnline,
|
||||
});
|
||||
if (result.has(u.peer_id)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result.set(u.peer_id, {
|
||||
name: u.name,
|
||||
id: u.peer_id,
|
||||
isOnline: isOnline,
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
});
|
||||
@ -78,26 +75,17 @@ export const UserList = (props: { selfName: string }) => {
|
||||
});
|
||||
|
||||
const unsub3 = subscribeToEvent(client, fluentPadServiceId, notifyOnlineFnName, (args, _) => {
|
||||
const [userOnline] = args as [PeerIdB58[]];
|
||||
setUsers((prev) => {
|
||||
const result = new Map(prev);
|
||||
|
||||
for (let u of userOnline) {
|
||||
const toSetOnline = result.get(u);
|
||||
if (toSetOnline) {
|
||||
toSetOnline.shouldBecomeOnline = true;
|
||||
toSetOnline.isOnline = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
const [user, onlineStatus] = args as [PeerIdB58, boolean];
|
||||
updateOnlineStatus(user, onlineStatus);
|
||||
});
|
||||
|
||||
// don't block
|
||||
withErrorHandlingAsync(async () => {
|
||||
await api.getUserList(client);
|
||||
await api.notifySelfAdded(client, props.selfName);
|
||||
await initAfterJoin(client, {
|
||||
name: props.selfName,
|
||||
peer_id: client.selfPeerId,
|
||||
relay_id: client.relayPeerId!,
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
@ -1,5 +1,15 @@
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
export const CheckResponse = (response: { err_msg: string; ret_code: number }): boolean => {
|
||||
if (response.ret_code !== 0) {
|
||||
console.error(response.err_msg);
|
||||
toast.error('Something went wrong: ' + response.err_msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const withErrorHandling = (fn: () => void): void => {
|
||||
try {
|
||||
fn();
|
||||
|
@ -6,7 +6,7 @@ import { setLogLevel } from '@fluencelabs/fluence';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
setLogLevel('INFO');
|
||||
setLogLevel('trace');
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
|
5
deploy_docker.sh
Normal file
5
deploy_docker.sh
Normal file
@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
docker kill fluence_node
|
||||
docker run -d --rm --name fluence_node -e RUST_LOG="info" -p 1210:1210 -p 4310:4310 fluencelabs/fluence -t 1210 -w 4310 -k gKdiCSUr1TFGFEgu2t8Ch1XEUsrN5A2UfBLjSZvfci9SPR3NvZpACfcpPGC3eY4zma1pk7UvYv5zb1VjvPHwCjj
|
||||
fldist deploy_app --env local -s Fs6nQaGEsM5EgnprUbUtoLYWhUC8o6QK1gseP9pfhzUm -i app/app.config.json -o client/src/app.json
|
3
package-lock.json
generated
Normal file
3
package-lock.json
generated
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"lockfileVersion": 1
|
||||
}
|
@ -1 +0,0 @@
|
||||
{"name":"history"}
|
@ -1 +0,0 @@
|
||||
{"name":"user-list"}
|
@ -1,21 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
(
|
||||
cd user-list-inmemory
|
||||
cargo update
|
||||
fce build --release
|
||||
)
|
||||
(
|
||||
cd history-inmemory
|
||||
cargo update
|
||||
fce build --release
|
||||
)
|
||||
|
||||
rm -f artifacts/user-list.wasm
|
||||
rm -f artifacts/history.wasm
|
||||
mkdir -p artifacts
|
||||
cp user-list-inmemory/target/wasm32-wasi/release/user-list.wasm artifacts/
|
||||
echo '{"name":"user-list"}' > artifacts/user-list.json
|
||||
cp history-inmemory/target/wasm32-wasi/release/history.wasm artifacts/
|
||||
echo '{"name":"history"}' > artifacts/history.json
|
||||
|
12
services/history-inmemory/Cargo.lock
generated
12
services/history-inmemory/Cargo.lock
generated
@ -4,9 +4,9 @@ version = 3
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.38"
|
||||
version = "1.0.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1"
|
||||
checksum = "81cddc5f91628367664cc7c69714ff08deee8a3efc54623011c772544d7b2767"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
@ -112,9 +112,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.87"
|
||||
version = "0.2.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "265d751d31d6780a3f956bb5b8022feba2d94eeee5a84ba64f4212eedca42213"
|
||||
checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@ -243,9 +243,9 @@ checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.61"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed22b90a0e734a23a7610f4283ac9e5acfb96cbb30dfefa540d66f866f1c09c5"
|
||||
checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
14
services/user-list-inmemory/Cargo.lock
generated
14
services/user-list-inmemory/Cargo.lock
generated
@ -4,9 +4,9 @@ version = 3
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.38"
|
||||
version = "1.0.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1"
|
||||
checksum = "81cddc5f91628367664cc7c69714ff08deee8a3efc54623011c772544d7b2767"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
@ -98,9 +98,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.87"
|
||||
version = "0.2.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "265d751d31d6780a3f956bb5b8022feba2d94eeee5a84ba64f4212eedca42213"
|
||||
checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@ -229,9 +229,9 @@ checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.61"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed22b90a0e734a23a7610f4283ac9e5acfb96cbb30dfefa540d66f866f1c09c5"
|
||||
checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -245,7 +245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
||||
|
||||
[[package]]
|
||||
name = "user-list"
|
||||
name = "user_list"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
|
@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "user-list"
|
||||
name = "user_list"
|
||||
version = "0.1.0"
|
||||
authors = ["Fluence Labs"]
|
||||
edition = "2018"
|
||||
|
||||
[[bin]]
|
||||
name = "user-list"
|
||||
name = "user_list"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
|
Loading…
x
Reference in New Issue
Block a user