Structure
Server - serves both as server and database which stores messages of all clients. Client - connects to server and can send private messages on arbitrary address, corresponding to node address in given network. Also it serves for showing and receiving messages which were sent by other clients on this address.
Writing your own python service using cellframe-sdk (t-dApp)
Server Part
The first thing is to define a constant which contains number UID (Unique-ID) of the service. It must be unique and not lower than 100. Values from 0 to 100 are reserved for cellframe-network services. In this example we chose number 477 because we like it and it’s unique. It is denoted by SERVICE-UID variable.
There is a list of imported functions from our SDK and default libraries. Below there is chosen UID.
from DAP.Core import logIt
from CellFrame.Network import Service, ServiceUID
from CellFrame.Chain import GlobalDB
from datetime import datetime
import json
SERVICE_UID = 477
logIt
- function which writes textual data in main log cellframe-node
Service, ServiceUID
- modules which encapsulate work with internal service representation and UID
GlobalDB
- module for working with cellframe-node built-in distributed key-value database Global-DB
To simpify forward work we will describe python wrapper above C-wrappers which work with database. In this example it calls GDBTable and includes main functions for working with tables in Global-DB.
class GDBTable:
def __init__(self, table):
self.table = table
def set(self, key, value):
GlobalDB.set(key, self.table, value.encode("utf-8"))
def get(self, key, default):
res = GlobalDB.get(key, self.table)
if not res:
return default
return res.decode("utf-8")
def delete(self, key):
GlobalDB.delete(key, self.table)
def group_list(self):
return GlobalDB.grLoad(self.table) or []
set
- inserts string value (previously converted into bytes) in table using key
get
- takes data from table by key and convert it into string value
delete
- deletes key from table
group_list
- returnes list of all objects-records in specified table
Pay attention that key is always a string while data is arbitrary set of bytes. Because in our service we work with textual messages and textual data, we implicitly convert them into bytes inside this wrapper.
List of support functions
safe_json_loads
- function which safely downloads json file. If data is correct it returns dict, otherwise - None
def safe_json_loads(data):
try:
return json.loads(data.decode("utf-8"))
except:
return None
to_bytes
- decoding strings into bytes
def to_bytes(data):
return data.encode("utf-8")
json_dump
- serialization of JSON object into string
def json_dump(data):
return json.dumps(data)
Function init
- entering point into plugin code
init
- is the first function which is called when cellframe-node is starting and python plugins (t-dApps) are downloading.
Service object is being initialized in our case. Constructor receives UID, service name and set of callback-functions, which will be called by this service if needed. For proper functionality of service we need to decrive several callbacks-functions. At this moment they are empty (related to payment methods) but one main custom_data
is active.
def init():
logIt.notice("=== Start mail-service plugin ===")
srv_object = Service(
ServiceUID(SERVICE_UID),
"mail_service",
requested,
response_success,
response_error,
next_success,
custom_data
)
return 0
List of callback-functions for service working
requested
- payment callback for signing the first check
def requested(srv, usage_id, client_remote, data):
print("=== requested ==")
logIt.info("[server] func requested")
return 0
response_success
- signature is completed successfully
def response_success(srv, usage_id, client_remote, data):
print("=== success ==")
logIt.notice("[server] func response success")
return 0
response_error
- mistake has emerged in signing process
def response_error(srv, usage_id, client_remote, data):
print("=== error custom_data input ==")
logIt.warning("[server] func response error")
next_success
- payment callback for signing next checks
def next_success(srv, usage_id, client_remote, data):
print("=== next_success ==")
logIt.notice("[server] func next success")
return 0
In this service we don’t use payment system for simplify, but in the next cases payment callbacks also will be meanful.
custom_data
- is a callback with handler of arbitrary data which were sent by encrypted connection. As in other callbacks, the first parameters being transmitted are service object, client identificator for payment, structure for gaining information about client and byte array of received data. In this case we use only variable with data
.
Our service protocol assumes exchange in JSON format, that’s why we make an attempt to deserializate and return error to client if deserialization wasn’t successfull. If format is correct - we extract “method” from clients request which containts something client needs from service, and then we check can our service accomplish such “method”. In case everything went well, the list of handlers is formed, and we request specified handler from list depends on chosen method.
As a result we send that which specified handler has returned. It must be a bytes object which is being sent back to client.
def custom_data(srv, usage_id, client_remote, data):
print("=== SRV custom_data input ==")
request = safe_json_loads(data)
if not request:
return to_bytes(json_dump({"status":"invalid_request"}))
method = request.get("method")
if not method:
return to_bytes(json_dump({"status":"no_method"}))
handlers = {"hello": method_hello,
"store": method_store,
"list": method_list,
"get": method_get}
if method not in handlers:
return to_bytes(json_dump({"method":method,
"status":"unknown_method"}))
return handlers[method](request)
List of handlers for different requests
method_hello
- method which reports to client that connection was established successfully
def method_hello(req):
return to_bytes(json_dump({"method":"hello",
"status":"success"}))
method list
- this method receives JSON object from client, from which is extracted field “me”. This field contains client node address, if there is no such field - the error is being returned. If everything is good then begins creation of copy of previously described class for working with GDB where table for specified user is opened. After this there occurs reading of all records and returning of the list to client. It is supposed that in database we store properly serialized messages.
def method_list(req):
me = req.get("me", None)
if not me:
return to_bytes(json_dump({"method":"store",
"status":"no_sender"}))
MAILBOX = GDBTable(f"local.mailserver.mailbox.{me}")
items = [safe_json_loads(i.value) for i in MAILBOX.group_list()]
return to_bytes(json_dump({"method":"list",
"status":"success", "items": items}))
method_get
- performs almost the same function as “method_list”, but also affords to get content by using specified index
def method_get(req):
me = req.get("me",None)
if not me:
return to_bytes(json_dump({"method":"get",
"status":"no_sender"}))
mid = req.get("id", None)
if mid == None:
return to_bytes(json_dump({"method":"get",
"status":"no_id"}))
MAILBOX = GDBTable(f"local.mailserver.mailbox.{me}")
msg = MAILBOX.get(str(mid), None)
if not msg:
return to_bytes(json_dump({"method":"get",
"status":"no_msg"}))
try:
msg = safe_json_loads(msg.encode("utf-8"))
return to_bytes(json_dump({"method":"get",
"status":"success", **msg}))
except:
traceback.print_exc()
method_store
- saves message in database. It alsi extracts the same field “me” as a sender
, the very message and reciever rcpt
. During message sending there adds new record in GDB table of receiver with new index, message, receiver and sending time.
def method_store(req):
sender = req.get("me",None)
if not sender:
return to_bytes(json_dump({"method":"store",
"status":"no_sender"}))
message = req.get("message",None)
if not message:
return to_bytes(json_dump({"method":"store",
"status":"no_message"}))
rcpt = req.get("rcpt",None)
if not rcpt:
return to_bytes(json_dump({"method":"store",
"status":"no_rcpt"}))
try:
MAILBOX = GDBTable(f"local.mailserver.mailbox.{rcpt}")
new_id = len(MAILBOX.group_list())
MAILBOX.set(str(new_id), json_dump( {"id": new_id,
"message" : message,
"timestamp": int(datetime.timestamp(datetime.now())),
"from": sender}))
except:
traceback.print_exc()
return to_bytes(json_dump({"method":"store",
"status":"success"}))
Client part
In client as in service the first thing to do is import set of modules from cellframe SDK and specify UID of the service.
There we have a list of necessary imported library functions from our SDK.
from DAP.Core import logIt
from CellFrame import AppCliServer
from CellFrame.Network import Net, ServiceUID, ServiceClient
from CellFrame.Chain import GlobalDB
from DAP.Core import logIt
from threading import Event
from datetime import datetime
import json
SERVICE_UID = 477
logIt
- function which writes textual data in main log cellframe-node
AppCliServer
- module for working with cellframe-node-cli on the client side
Net, Service, ServiceUID
- modules which work with network and UID
GlobalDB
- module for working with database
Because of client interaction with server performs asynchronously while using callbacks which server uses too, it is necessary to import synchronization mechanisms.
As we did on the server side we need to describe wrapper-class for working with global database.
class GDBTable:
def __init__(self, table):
self.table = table
def set(self, key, value):
GlobalDB.set(key, self.table, value.encode("utf-8"))
def get(self, key, default):
res = GlobalDB.get(key, self.table)
if not res:
return default
return res.decode("utf-8")
def delete(self, key):
GlobalDB.delete(key, self.table)
def group_list(self):
return GlobalDB.grLoad(self.table) or []
set
- inserts string value (previously converted into bytes) in table using key
get
- takes data from table by key and convert it into string value
delete
- deletes key from table
group_list
- returnes list of all objects-records in specified table
Let’s describe two tables in global-DB with names config
and aliases
for storing connection parameters and list of aliases.
CONFIG = GDBTable("local.mailclient.config")
ALIASES = GDBTable("local.mailclient.aliases")
List of additional functions
safe_json_loads
- function for safe downloading of JSON file. If data is correct it returns pyObject in string format
def safe_json_loads(data):
try:
return json.loads(data.decode("utf-8"))
except:
return None
to_bytes
- decoding string into bytes
def to_bytes(data):
return data.encode("utf-8")
json_rump
- receiving string from JSON object
def json_dump(data):
return json.dumps(data)
find_arg
- parsing of command line args: realizes searching by argv
parameters of some option val
, in case of success returnes true
def find_arg(val, argv):
for i, arg in enumerate(argv):
if arg == val:
return True
return False
find_arg_value
- parsing of command line args: also realizes searching by argv
of some option val
and returnes it’s value
def find_arg_value(val, argv):
for i, arg in enumerate(argv):
if arg == val:
if len(argv) > i+1:
return argv[i+1]
return None
Mechanism of forming and sending requests on server
This mechanism consists of 2 functions: make_service_requests_sync_cfg
и make_service_request_sync
.
make_service_requests_sync_cfg
- receives dictionary as input with data, which is required to send to service (request,data
) and ID of cli-client (indexStrReply
). There could be many requests to node simultaneously so it’s necessary to know which is to asnwer.
This function reads configuration parameters for connection to service (IP, port
, network name
) from “CONFIG” table in GDB. After data extraction there occurs validity check, and then if everything is good, is requested the second function which related with request sending and answer receiving.
def make_service_request_sync_cfg(data, indexStrReply):
addr = CONFIG.get("RELAY_ADDRESS", None)
port = int(CONFIG.get("RELAY_PORT", None))
netname = CONFIG.get("NET_NAME", None)
if None in [addr, port, netname]:
AppCliServer.setReplyText("Relay not configured!", indexStrReply)
return
return make_service_request_sync(data, addr, port, netname, indexStrReply)
make_service_request_sync
- is a function which realizes blocking performation of commands by service. Gets all the parameters which previous function also get (data
, addr
, port
, network name
, indexStrReply
)
This function is synchronous because service operates with set of callbacks which are requested by different events. To synchronize and put in order sequence request-answer, we will use mechanism of multi-threaded synchronization “Event”. This primitive allows us to atomically check flag state, by which we are going to define was there any received data from server or not.
In the very function on the first stage we create network object using name transmitted to function. Then we check is there network in the node in which client want to interact. After this we extract address of local node from this object. Local node address - is “me” in JSON-request by which server-provider defines from where he is supposed to read messages and who is the sender.
def make_service_request_sync(data, ip, port, netname, indexStrReply):
net = Net.byName(netname)
if not net:
AppCliServer.setReplyText(f"No such network [{netname}]!", indexStrReply)
return
#tell server who are we
data["me"] = str(net.getCurAddr())
After this we will create class with data CallbackPayload
which object is going to be transmitted in every callback of our service-client. In this class we will store fields-properties which are necessary inside callbacks we are going to call in future.
class CallbackPayload:
def __init__(self, request, netname, ip, port, evt, closed_evt):
self.request = request
self.netname = netname
self.ip = ip
self.port = port
self.indexStrReply = indexStrReply
self.response = "",
self.has_response = evt
self.closed = closed_evt
request
- dictionary with request to server (after this it will be serialized in byte array)
netname
- string name of network in which request was sent
ip
- address where request was sent
port
- port where request was sent
indexStrReply
- number of cli-client in case if callback would like to give it message
response
- future answer from service
has_response
- event that says about answer appearance
closed
- were there connections break from the server side
Then create the very object payload
in which we will transmit all functions parameters were given in input.
payload = CallbackPayload(data, netname, ip, port, Event(),
Event())
The next step is creating object of our SDK - ServiceClient, name it client
. This object works with address and specified port and it will call specified callback on every event. Into this object we transmit previously created payload
which will be used in every callback.
client = ServiceClient(net, ip, port, callback_connected,
callback_disconnected,
callback_check,
callback_sign,
callback_success,
callback_error,
callback_data,
payload)
At the moment of client creation cellframe-node initializes opening of stream with service node using given IP address. If connection was successfull, node will use callback connected
. This all occurs in cellframe-node stream, not in the stream of python-plugin, that’s why after client creation we wait for event has_response
which will be created in one of callbacks.
payload.has_response.wait()
Waiting answer from server and it’s following handling realizes this way.
The very process of request sending is built in the following way: during installation of encrypted P2P connection between client and server is requested connected
callback inside which is occured transmition request on server. As soon as server handled request and sent answer - there will be requested callback_data
from the client, in which will be established response receiving flag has_response
and wrote the very answer in corresponding field of structure payload
.
As answer is gained, method wait
turns off blockage and then will be requested method which closes stream for client.
client.close()
After this is requested callback disconnected
from the service side and the network session can be considerate ended. Then given answer on request will return back in command handler which has initiated request sending.
return safe_json_loads(payload.response)
List of callbacks for client’s work
callback_connected
- in this callback is formed request into service with data from client including service UID. In case service and client UID are not the same then the procedure will be done with mistake.
def callback_connected(serviceClient, payload):
logIt.notice("Python client connected")
ch_uid = ServiceUID(SERVICE_UID)
data = json_dump(payload.request).encode("utf-8")
print(data)
serviceClient.write(ch_uid, data)
callback_disconnected
- is requested in case if provider has closed session and broken connection, it establishes answer receiving flag and closes connection stream with service.
def callback_disconnected(serviceClient, payload):
logIt.notice("Python client disconnected")
payload.has_response.set()
payload.closed.set()
callback_check
- callback for checking connection with server “a la” KeepAlive (not in usage, will be reworked)
def callback_check(serviceClient, payload):
logIt.notice("Python client successfully checked the service")
callback_sign
- callback which is requested when server uses service, if not then error occurs (also not in usage)
def callback_sign(serviceClient, txCondRec, payload):
logIt.notice("Siging receipt by python client")
callback_success
- callback for signing check of payment serivce (not in usage)
def callback_success(serviceClient, txCondHash, payload):
logIt.notice("Python client successfully requested the service")
callback_error
- callback which messages error code if such occurs, also establishes answer receiving flag
def callback_error(serviceClient, errorNum, payload):
logIt.warning(f"Python client got error {errorNum:#x}")
payload.has_response.set()
callback_data
- callback for receiving answer from service with following data recording in payload
and establishing answer receiving flag
def callback_data(serviceClient, data, payload):
logIt.notice(f"Python client custom data readback!")
payload.response = data
payload.has_response.set()
init
function
Function init
performs client initialization. Unlike from server, this client includes only task for command line handlers through AppCliServer
, with it’s help it’s easy to inject new service commands in default request handler cellframe-node-cli.
def init():
logIt.notice("Init demoClient")
userProfile = None
AppCliServer.cmdItemCreate("mail", cmd_message_srv_client, "mail-server client commands",
"""
Set up a mail-server node. Your node should be in the same net!
mail relay -ip <ip> -port <port> -net <net>
Send a message
mail send -m MESSAGE -rcpt NODE_ADDRESS | ALIAS
Create alias:
mail alias NODE_ADDRESS ALIAS
Working with messages.
mail list
mail get ID
""")
return 0
AppCliServer.cmdItemCreate
- allows to register new method for cli.
- The first argument is name of the new method - “mail”
- The second argument is callback which handles all requests from user, requested for “mail”
- The third arguments is full name of the new method
- The fourth argument is “help” of the new method
List of available cli commands
help
or ?
- shows commands help
relay
- request on client configuration (connection to server with IP
and port
in denitire cellframe network)
relay -ip <ip> -port <port> -net <net>
send
- to send messsage on definite address or alias
send -m MESSAGE -rcpt NODE_ADDRESS | ALIAS
alias
- to create alias, for example, name “Test” corresponds to address ABSC:ASSF:XXXX:XXXX, in this case instead of specify node address every time it’s enough just specify the very alias “Test”
mail alias NODE_ADDRESS ALIAS
list
- returnes list with messages ID’s
list
get
- opens content of chosen message usin ID from the list
get ID
If request “help mail” comes then client receives “help” which is decribed in AppCliServer.cmdItemCreate
in init
function. If request ”? mail” comes then client receives “help” from callback.
Callback for handling request from client
Callback for request handling takes as input set of parameters argv
, which are returned by node after dealing with cli-request and it’s ID indexStrReply
def cmd_message_srv_client(argv, indexStrReply):
hlp = """
Set up a mail-server node. Your node should be in the same net!
mail relay -ip <ip> -port <port> -net <net>
Send a message
mail send -m MESSAGE -rcpt NODE_ADDRESS | ALIAS
Create alias:
mail alias NODE_ADDRESS ALIAS
Working with messages.
mail list
mail get ID
"""
if find_arg("?",argv):
AppCliServer.setReplyText(hlp, indexStrReply)
return
commands = {"relay": command_relay,
"send": command_send,
"alias": command_alias,
"list": command_list,
"get": command_get }
if len(argv) < 2 or argv[1] not in commands:
AppCliServer.setReplyText("Invalid command", indexStrReply)
return
commands[argv[1]](argv[2:], indexStrReply)
In the very callback on the first stage is checked help
request in condition if find_arg(“?”,argv)
where is searched symbol ”?” among accepted arguments. If there were found concidence specified command AppCliServer.setReplyTest()
returnes to node corresponding text and serial number of request. The following text then is transmitted as answer to cli command.
If there wasn’t any help
request then we will cerate object with requests and their handlers.
commands = {"relay": command_relay,
"send": command_send,
"alias": command_alias,
"list": command_list,
"get": command_get }
After this all is checked validity of transmitted arguments and availability of commands from “commands” dictionary among them, in case of mistake client receives corresponding message.
AppCliServer.setReplyText("Invalid command", indexStrReply)
If everything is good, then is performed request to “commands” dictionary and calling of corresponding handler.
commands[argv[1]](argv[2:], indexStrReply)
Firstly corresponding command (relay,send, etc)argv[1]
is extracted, then all arguments (-ip,-port, etc) argv[2:]
for this command are transmitted together with ID of cli-client indexStrReply
.
Command handlers
command_relay
- configuration method for connection to service. Receives as input list of arguments (which user has transmitted to cli) and extracts next parameters ip
, port
, network name
. All parameters are checked on validity before they will be used. In case of mistake - user will get corresponding answer in terminal.
After all checks request on connection to service is fromed using method make_service_request_sync
which returns answer about successfull operation. If connection request came with error or didn’t come at all then corresponding message is output, otherwise, we message that everything is good and write network settings in GDB table which we called “CONFIG”.
def command_relay(args, indexStrReply):
ip = find_arg_value("-ip",args)
if not ip:
AppCliServer.setReplyText("IP param not found",indexStrReply)
return
port = find_arg_value("-port", args)
if not port:
AppCliServer.setReplyText("port param not found", indexStrReply)
return
netname = find_arg_value("-net", args)
if not netname:
AppCliServer.setReplyText("net param not found", indexStrReply)
return
if not Net.byName(netname):
AppCliServer.setReplyText(f"No such network [{netname}]!", indexStrReply)
return
net = Net.byName(netname)
if not net:
AppCliServer.setReplyText(f"Unknown network {netname}", indexStrReply)
return
res = make_service_request_sync({
"method":"hello",
},
ip,
int(port),
netname,
indexStrReply)
if not res:
AppCliServer.setReplyText(f"Relay {ip}:{port} [{netname}] not responding.", indexStrReply)
return
status = res.get("status", None)
if not status or status != "success":
AppCliServer.setReplyText(f"Relay {ip}:{port} [{netname}****] respondes to hello with [{status}] status.", indexStrReply)
return
CONFIG.set("RELAY_ADDRESS", ip)
CONFIG.set("RELAY_PORT", str(port))
CONFIG.set("NET_NAME",netname)
AppCliServer.setReplyText(f"Relay service configured as{ip}:{port}[{netname}], your address is:{str(net.getCurAddr())},indexStrReply)
command_send
sends message to node address or alias. Extracts fields with message text -m
and address/alias of receiver -rcpt
(all parameters are also checked on validity) from set of parameters args
. In case there was specified “alias” then we take address from ALIAS table by name, otherwise we take value from rcpt
field as address.
rcpt = ALIASES.get(rcpt, None) or rcpt
After all preparations we form request in service with help of the command make_service_request_sync
, read the answer and return result in terminal.
def command_send(args, indexStrReply):
msg = find_arg_value("-m", args)
if not msg:
AppCliServer.setReplyText("-m param not found", indexStrReply)
return
rcpt = find_arg_value("-rcpt", args)
if not rcpt:
AppCliServer.setReplyText("-rcpt param not found", indexStrReply)
return
rcpt = ALIASES.get(rcpt, None) or rcpt
res = make_service_request_sync_cfg({
"method":"store",
"message": msg,
"rcpt": rcpt
},
indexStrReply)
status = res.get("status", None)
if not status or status != "success":
AppCliServer.setReplyText(f"Relay respondes with [{status}] status.", indexStrReply)
return
AppCliServer.setReplyText(f"Success", indexStrReply)
command_alias
allows to create couple “NODE_ADDRESS - NAME”. On the first stage it checks validity of trasmitted parameters, and if everything is good then writes to table ALIASES new value or replaces oln one.
def command_alias(args, indexStrReply):
if len(args) < 2:
AppCliServer.setReplyText("No alias or node address specified", indexStrReply)
return
ALIASES.set(args[0], args[1])
AppCliServer.setReplyText(f"Recepient {args[1]} is now known as {args[0]}", indexStrReply)
command_list
- makes request in service to get list of messages for local client (you). At this moment request make_service_request_sync
is formed and then both it’s status and content is checked. If list is empty then client gets message “Mailbox is empty”. If it is not empty then corresponding messages are output in string format with next content: ID (ordered number of message), from, timestamp. These messages user then receives.
def command_list(args, indexStrReply):
res = make_service_request_sync_cfg({
"method":"list",
},
indexStrReply)
status = res.get("status", None)
if not status or status != "success":
AppCliServer.setReplyText(f"Relay respondes with [{status}] status.", indexStrReply)
return
resp_str = ""
if not res["items"]:
resp_str = "Mailbox is empty"
for item in res["items"]:
resp_str += f"{item['id']}: from {item['from']} at {datetime.fromtimestamp(item['timestamp'])}\n"
AppCliServer.setReplyText(resp_str, indexStrReply)
command_get
- command for reading definite message by ID. On the first stage is checked validity of arguments, and then is formed request into service make_service_request_sync
, status is extracted. If everything is good then user receives message in next format - From: <node addr or alias> at <timestamp> <message>
def command_get(args, indexStrReply):
if len(args) < 1:
AppCliServer.setReplyText(f"No message-id provided", indexStrReply)
return
res = make_service_request_sync_cfg({
"method":"get",
"id": int(args[0])
},
indexStrReply)
status = res.get("status", None)
if not status or status != "success":
AppCliServer.setReplyText(f"Relay respondes with [{status}] status.", indexStrReply)
return
resp_str = f"From: {res['from']} at {datetime.fromtimestamp(res['timestamp'])}\n{res['message']}"
AppCliServer.setReplyText(resp_str, indexStrReply)
Manifest, structure and installation
File “manifest” is intended to store support information about version, name, developer, programm description etc.
manifest.json for client
{
"name": "cmsClient",
"version": "1.0",
"author": "DEMLABS (C) 2023",
"dependencies": [],
"description": "Client plugin for messaging."
}
manifest.json for service
{
"name": "cmsService",
"version": "1.0",
"author": "DEMLABS (C) 2023",
"dependencies": [],
"description": "Service plugin for messaging."
}
The most important part in this file is name name
of our plagin. For availability of initialization from the node side the name of the plugin should be the same with name of the file with code and the very directory where file is situated. This file must lie near the main plugin installation file.
Installation from the service side
(Installation on Linux, pay attention that insallation on other OS’s can be different)
Plugin repository: https://dapps.cellframe.net/node/t-dapp-cms.zip
We must have node with “white” IP address. Then you should download plugin from repository and put it in corresponding folder ” /opt//cellframe-node/var/lib/plugins/” (showed on screen). After this you must uncomment next strings in the end of the file “/opt/cellframe-node/etc/cellframe-node.cfg”:
# Plugins
[plugins]
enabled=true
# Load Python-based plugins
py_load=true
# Path to Pyhon-based plugins
py_path=/opt/cellframe-node/var/lib/plugins
After this all it is enough to reset node and wait it’s initialization and that’s all, the service is active. To verify that plugin has loaded, you can call command in terminal, which will show list of node plugins cellframe-node-cli plugins list
.
denis@denis-virtual-machine:~$ cellframe-node-cli plugins list
| Name plugin | Version | Author(s) |
| cmsService | 1.0 | DEMLABS (C) 2023 |
Also during successfull plugin initialization in the node logs must be appear message which our service sends in process of initialization.
logIt.notice(”= Start mail-service plugin =“)
Logs are located in “/opt/cellframe-node/var/log/cellframe-node.log”
Installation from the client side
(Installation on Linux, pay attention that insallation on other OS’s can be different)
Client setup is like service setup except we don’t need node with “white” IP address and instead of cmsService
we have to use cmsClient
Example of usage
After service and two different client setup we can start interaction. Below there is example of work in terminal from one of the clients.
- Check node version
denis@denis-virtual-machine:~$ cellframe-node-cli version
cellframe-node version 5.2-312
- Let’s check has our plugin downloaded in the node
denis@denis-virtual-machine:~$ cellframe-node-cli plugins list
| Name plugin | Version | Author(s) |
| cmsClient | 1.0 | DEMLABS (C) 2023 |
- Perform request on configuration of our client and make first request into service
denis@denis-virtual-machine:~$ cellframe-node-cli mail relay -ip 208.85.17.16 -port 8079 -net raiden
Relay service configured as 208.85.17.16:8079 [raiden], your address is: 3234::D530::87C0::3471
- Check “mailbox” if there is any messages
denis@denis-virtual-machine:~$ cellframe-node-cli mail list
0: from 05D0::A2E3::7A23::492E at 2023-10-10 11:08:43
- Read received message
denis@denis-virtual-machine:~$ cellframe-node-cli mail get 0
From: 05D0::A2E3::7A23::492E at 2023-10-10 11:08:43
Hello from cellframe-mail
- Let’s create alias for more comfortable interaction with the second client
denis@denis-virtual-machine:~$ cellframe-node-cli mail alias Dima 05D0::A2E3::7A23::492E
Recepient 05D0::A2E3::7A23::492E is now known as Dima
- Send message to our second client
denis@denis-virtual-machine:~$ cellframe-node-cli mail send -m “Hi, Dima” -rcpt Dima Success