I have been encountering an issue for a long time but I haven't found any solution yet, despite reading the protocol version 5 standard and the emqx documentation.
I want to publish messages with a time limit to simulate a situation where my device is unavailable so that, after the time limit has expired, the broker will delete the message and my device will not receive it.
I want the reason that my device will not be available (and therefore will not receive messages), to be because it is in a closed area, like a tunnel, or in a area with no cellular cove-range and not because it initiated a disconnect from the broker or because the “keepalive” value has expired.
My understanding is that I can use the “messageExpiryInterval” property (in protocol 5) to implement my goal.
I used EMQX broker as follows:
const connectUrl = 'mqtt://broker.emqx.io:1883';
Along with the following connection configuration:
const client = mqtt.connect(connectUrl, {
clientId: 'mqtt_dani_pub',
protocolVersion: 5,
keepalive: 1800,
clean: true
});
And when sending a message, I put the following values:
const options = {
qos: 0,
retain: false,
properties: { messageExpiryInterval: 30 }
};
As you can see, I used a high value, 1800, for “keepalive” to ensure that the device will be connected to the broker for a long time.
To simulate this situation, I used one publisher on one PC, and one subscriber on another PC.
The scenario is as follows:
Connect publisher and subscriber to emqx broker
a. Verify MQTT v5 protocol.
Publish the message (when the subscriber is connected) to emqx with MessageExpiryInterval: 30
a. Subscribe receive the message
Turn OFF the Wi-Fi on the subscriber computer.
Publish the message to emqx with MessageExpiryInterval: 30
Wait for 120 seconds
Turn ON the Wi-Fi on the subscriber computer.
a. Subscriber does not receive the message ==> but it does get the message!
In addition to this, I saw in the standard of protocol 5 section 3.3.2.3.3 (Message Expiry Interval - https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.pdf ) that the PUBLISH packet sent to a Client by the Server MUST contain a Message Expiry Interval set to the received value minus the time that the Application Message has been waiting in the Server, so this may be the problem.
my publisher js code:
import mqtt, { MqttClient } from 'mqtt';
import * as readline from 'node:readline'
import { stdin, stdout } from 'process';
const connectUrl = 'mqtt://broker.emqx.io:1883';
const clientId = 'mqtt_dani_pub';
const topic = 'dani/test';
const subject = 'Publisher';
const rl = readline.createInterface({
input:stdin,
output:stdout
});
const client = mqtt.connect(connectUrl, {
clientId,
protocolVersion: 5,
keepalive: 1800,
clean: true
});
client.on('connect', () => {
console.log(`${subject} client connected..`)
client.subscribe([topic], () => {
console.log(`Subscribe to topic '${topic}'`);
})
});
const options = {
qos: 0,
retain: false,
properties: { messageExpiryInterval: 30 }
};
const publishMsg = (message) => {
client.publish(topic,
`${clientId} - ${Date.now()} - ${message}`,
options,
(error) => {
if (error) {
console.error(error)
}
}
);
};
const input2topic = () => {
return new Promise(resolve => {
rl.question(`send message to topic ${topic}: `,
(input) => {
if(input !== 'exit'){
console.log(`writing to topic ${topic}..`);
publishMsg(input);
resolve(true);
} else{
console.log('exit...');
resolve(false);
}
});
});
}
const main = async () => {
publishMsg('first message');
let flag = true;
while(flag){
await new Promise(resolve => setTimeout(resolve, 1000));
flag = await input2topic();
}
rl.close();
client.end();
}
main();
my subscriber js code:
import mqtt, { MqttClient } from 'mqtt';
const connectUrl = 'mqtt://broker.emqx.io:1883';
const clientId = 'mqtt_dani_sub';
const topic = 'dani/test';
const subject = 'Subscriber';
const client = mqtt.connect(connectUrl, {
clientId,
keepalive: 1800,
protocolVersion: 5,
})
client.on('connect', () => {
console.log(`${subject} client connected`)
client.subscribe([topic], {qos: 0}, () => {
console.log(`Subscribe to topic '${topic}'`)
})
});
client.on('message', (topic, payload, packet) => {
console.log('\nReceived Message:', {
...packet,
message: payload.toString(),
msg_length: payload.toString().length,
time: new Date(),
});
});
Related
I have developed a angular app and a server using express running on node. I used socket.io to establish communication between the two over sockets. To test the latency between the two, I did a round trip test where I calculated time when the message was sent by server, the time received at client and then at the time server receives the final response. One abnormality I came across was when I checked these values generated, the time reported by angular app consistently remains to be offset. I got similar results when I had both the server and the angular app running on the same device and two different devices over a VPN. How can I fix this issue?
Event Diagram (Green: Angular app, Blue: Server)
Here is some time data I have collected of the same:
(Sent -> time when msg sent by server, UI -> time when angular app received and sent response, Recv -> time when server received response from angular app)
Sent: 1659354564134 UI: 1659354564158 Recv: 1659354564148
Sent: 1659354569144 UI: 1659354569157 Recv: 1659354569147
Sent: 1659354574147 UI: 1659354574160 Recv: 1659354574150
Sent: 1659354579159 UI: 1659354579173 Recv: 1659354579162
Server Code(index.js):
io.on("connection", (socket) => {
n = 0;
console.log("A user connected with id: %s", socket.id);
socket.on("message", (data) => {
data = JSON.parse(data);
let now = Date.now();
console.log("Sent: %s UI: %s Recv: %s", data["sent"], data["recv"], now);
});
let interval = 5000;
// For sending timestamp only continuously at a set interval
setInterval(() => {
let now = Date.now();
socket.send(JSON.stringify({ sent: now }));
console.log(now, "^");
}, interval);
});
Client Code(Angular: communication.service.ts):
export class CommunicationService {
public socket!: Socket;
public date = new Date();
public serverTimeOffset = 0;
public connect() {
this.socket = io('http://localhost:3000');
this.socket.on('connect', () => {
console.log('Connected');
});
this.socket.on('disconnect', () => {
console.log('Disconnected');
});
this.socket.on('connect_error', () => {
setTimeout(() => {
this.socket.connect();
}, 1000);
});
this.socket.on('message', (event) => {
let data = JSON.parse(event);
let now = Date.now();
this.socket.send(JSON.stringify({ sent: data['sent'], recv: now }));
}
});
}
}
I have a server backend written in Python with Flask-SocketIO. I'm utilizing it's room feature to make private conversations. Upon a join room event the server fires the following function to let the frontend know where to send messages to specific user:
socketio.emit('room name response', {'roomName': room_name, 'recipient': recipient}, to=sid)
where sid is the private room created only for the user when connecting to a socket. Then I want to keep this information in React state in a map, like this:
function ChatWindow({ username, token }) {
const [responses, setResponses] = useState([]);
const [roomsMap, setRoomsMap] = useState(new Map());
const [currentRoom, setCurrentRoom] = useState("");
const [messageValue, setMessageValue] = useState("");
var socket = null;
useEffect(() => {
socket = socketIOClient(ENDPOINT);
});
useEffect(() => {
socket.on("global response", (data) => {
setResponses((responses) => [...responses, data]);
});
socket.on("room name response", (data) => {
console.log(`joined ${data.roomName} with ${data.recipient}`);
setCurrentRoom((currentRoom) => data.roomName);
setRoomsMap((roomsMap) => roomsMap.set(data.recipient, data.roomName));
});
return () => socket.close();
}, []);
const sendMessage = () => {
if (messageValue.length < 1) {
return;
}
socket.emit("global message", {
user_name: username,
message: messageValue,
timestamp: Date.now(),
});
setMessageValue("");
};
const joinRoom = (recipient) => {
socket.emit("join", {
token: token,
username: username,
recipient: recipient,
});
// setCurrentRoom(() => roomsMap.get(recipient));
};
const leaveRoom = (recipient) => {
socket.emit("leave", {
token: token,
username: username,
recipient: recipient,
});
const newRooms = roomsMap;
newRooms.delete(recipient);
console.log(`left room with ${recipient}`);
newRooms.forEach((val, key) => console.log(`${val}:${key}`));
setRoomsMap(newRooms);
};
const checkUser = (userToCheck) => {
if (userToCheck === username) {
return styles.chatFromUser;
} else {
return styles.chatToUser;
}
};
return (...);
}
export default ChatWindow;
Sadly, React doesnt react to the socket emitting message, even though it can be seen in network tab in developer tools. The global response works fine.
When I alter the backend function to:
socketio.emit('room name response', {'roomName': room_name, 'recipient': recipient})
React suddenly works as expected. I'm trying to understand why it happens, especially when the browser seems to see the incoming messages as stated above, so it's most likely my bad coding or some React/Javascript thing.
Thank You for any help in advance.
The problem was that socket sometimes was created multiple times, therefore, the socket that useEffect was currently listening wasn't necessarily the one in the room. So I made one, global socket to fix this and whole thing now works.
How do I run/execute an instance of a node file by providing some parameters from another programs by knowing the location and content of file.
When I add a device from my web application which also run on node (adonis js), it create a device with provided params like, device_name, type, lat, lng etc. when device successful added, an instance or copy of the following code should starts automatically with provided params when device was added.
file which need to started with given parameters
// c:/device/test.js
var mqtt = require('mqtt');
function UserModule() {
var client = mqtt.connect('mqtt://test.mosquitto.org');
client.on('connect', function () {
let latitude = 777
let lngitude = 999
setInterval(function() {
let device_data = {
'device_name':'{params.devcie_name}',
'topic_topic':'MyProject/{params.type_of_device}/{params.user_id}/{params.topic_name}',
'type':'GPS',
'data':{
'lat':'35.'+latitude++,
'lng':'-74.'+lngitude++,
'alt':22,
'acc':25.10101,
'name': 'my car2',
'type': 'car'
},
'status': 'OK'
}
client.publish(device_data.topic_topic, JSON.stringify(device_data));
console.log('data published for', device_data.device_name);
}, 1000);
});
}
module.exports = UserModule;
controller for adding device
//app/controllers/http/devicecontroller.js
async store({ params, request, response, view, session, auth }) {
try {
const deviceData = request.only(['cat_id', 'device_name', 'type', 'device_type_id'])
deviceData.device_owner_id = auth.current.user.id
deviceData.is_deleted = 0
deviceData.is_active = 1
const device = new Device();
let rules = Config.get('rules.device')
let messages = Config.get('messages.device')
const validation = await validate(deviceData, rules, messages)
if (validation.fails()) {
console.log(JSON.stringify(validation.messages()))
session.flash({ type: 'danger', message: validation.messages()[0].message })
return response.redirect('back')
}
let dev = await deviceService.addDevice(deviceData);
session.flash({ type: 'success', message: 'Device added successfully' })
//here to run a code which execute that file
//sudo-code
File f = new File('c:/device/test.js')
let content = await f.readAll()
content = string.format(content, params1, params2, params3..)
f.content = content;
f.eecute()
return response.redirect('/dashboard/device-manage')
} catch (error) {
session.flash({ type: 'danger', message: error.message })
return response.redirect('back')
}
}
Ho I execute the code every time when I add a device, every time with new parameters, means executing same file with new instance with new parameters.
To start a new process, use the node package child_process
https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options
To send messages to that child_process, use child_process.fork
const cp = require('child_process');
const n = cp.fork(`${__dirname}/sub.js`);
n.on('message', (m) => {
console.log('PARENT got message:', m);
});
// Causes the child to print: CHILD got message: { hello: 'world' }
n.send({ hello: 'world' });
https://nodejs.org/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback
I'm developing a chat app and i have some trouble with socket.io.
Stuff works great and quick. But as soon as more people are logged in - everyone see's all the messages and rendering gets messy.
I'm sending a unique ID with each message, which i would love to create rooms with where people can chat with each other (1 on 1)
THis happens first:
const socketMessage = {
type: type,
body: body,
author: author,
date: Date.now(),
id: chatId
};
const io = require('socket.io-client');
const socket = io(process.env.REACT_APP_WS_URL);
socket.emit('message-sent', socketMessage);
THis goes to the server where this happens:
socket.on('message-sent', data => {
socket.broadcast.emit('new-chat-message', data);
});
WHich then goes into a custom hook and adds to the messages-array.
export default function useGetChatMessages(_id) {
const [chatMessages, setChatMessages] = React.useState([]);
React.useEffect(() => {
const io = require('socket.io-client');
const socket = io(process.env.REACT_APP_WS_URL);
socket.emit('send-chat-id', _id);
socket.on('chat-messages', data => {
setChatMessages(data.messages);
});
socket.on('new-chat-message', message => {
setChatMessages(messages => [...messages, message]);
notification.play();
});
}, [_id]);
return chatMessages;
}
I know the broadcast part is wrong on my server but i have tried lots of things:
Sending the chatId with the message and then do socket.join(chatId) at several stages.
Then socket.to(chatId).emit(...) and so on.
But i can not get it right.
Can someone help?
Thanks a lot in advance!! :-)
Actually, you could use the socket io feature called namespaces.
Look an pretty simple example:
In the client side you would do something like this:
const socket = io.connect(socket_url, { query: "chatRoomId=123&userId=983912" });
And in the server side something like this:
io.on('connection', (socket) => {
const chatRoomId = socket.handshake.query['chatRoomId'];
const userId = socket.handshake.query['userId'];
const user = find(userId) // Pretend :)
socket.join(chatRoomId);
socket.on('new_message', (id, msg) => {
io.to(chatRoomId).emit(`${user.name} says ${msg}`);
});
socket.on('disconnect', () => {
socket.leave(chatRoomId)
console.log('user disconnected');
});
});
First of all please note that this is not about creating a bot.
My goal is to create an application that will simply listen to any number of telegram channels that the account I will provide it with is subscribed to and retrieve all messages sent to those channels (as if I was a normal user). My guess is that I will need to
Authenticate myself using my account's phone number
Be able to setup a callback listener either per channel or a general listener for all incoming messages
I've been looking around the telegram api for a couple of days now and I am extremely confused as to how it works. After giving up on it, I started looking at readymade implementations, mostly for NodeJS but was still not able to find a concrete solution. I'm testing some things with the telegram-js api but running it directly using node didn't work. Does it need to run in a browser? Is there any more streamlined approach to this? Preferably something with good documentation.
PS: I'm fluent in Java and Javascript mostly so I've prioritized libraries based on those languages.
EDIT:
Here is the code that I've written (essentially copied an example)
var { Telegram } = require("../libs/telegram");
var TypeLanguage = require("telegram-tl-node") ;
var MTProto = require("telegram-mt-node");
var schema = require("../libs/api-schema.json");
const APP_ID = "111111";
const APP_HASH = "fb6da8f6abdf876abd6a9d7bf6";
const SERVER = { host: "111.111.111.11", port: "443" };
const config = {
id: APP_ID,
hash: APP_HASH,
version: '0.0.1',
lang_code: 'en',
authKey: null
};
let telegram = new Telegram(MTProto, TypeLanguage);
telegram.useSchema(schema);
addPublicKeys(telegram);
let connection = new MTProto.net.HttpConnection(SERVER);
let client = telegram.createClient();
client.setConnection(connection);
connection.connect(function() {
let ready = client.setup(config);
ready.then(function(client) {
// it never resolves this promise
function callback(response) {
console.log(response);
}
client.callApi("help.getConfig").then(callback, callback);
});
});
It uses those 2 libs:
telegram-mt-node
telegram-tl-node
Late answer but might help others.
You can utilize mtproto-core to authenticate with a regular telegram account and listen to updates (or do anything you can with telegram clients, really)
Here is a sample script I've written that listens to new messages from channels/supergroups the user is subscribed to:
const { MTProto, getSRPParams } = require('#mtproto/core');
const prompts = require('prompts');
const api_id = ...; // insert api_id here
const api_hash = ' ... '; // insert api_hash here
async function getPhone() {
return (await prompts({
type: 'text',
name: 'phone',
message: 'Enter your phone number:'
})).phone
}
async function getCode() {
// you can implement your code fetching strategy here
return (await prompts({
type: 'text',
name: 'code',
message: 'Enter the code sent:',
})).code
}
async function getPassword() {
return (await prompts({
type: 'text',
name: 'password',
message: 'Enter Password:',
})).password
}
const mtproto = new MTProto({
api_id,
api_hash,
});
function startListener() {
console.log('[+] starting listener')
mtproto.updates.on('updates', ({ updates }) => {
const newChannelMessages = updates.filter((update) => update._ === 'updateNewChannelMessage').map(({ message }) => message) // filter `updateNewChannelMessage` types only and extract the 'message' object
for (const message of newChannelMessages) {
// printing new channel messages
console.log(`[${message.to_id.channel_id}] ${message.message}`)
}
});
}
// checking authentication status
mtproto
.call('users.getFullUser', {
id: {
_: 'inputUserSelf',
},
})
.then(startListener) // means the user is logged in -> so start the listener
.catch(async error => {
// The user is not logged in
console.log('[+] You must log in')
const phone_number = await getPhone()
mtproto.call('auth.sendCode', {
phone_number: phone_number,
settings: {
_: 'codeSettings',
},
})
.catch(error => {
if (error.error_message.includes('_MIGRATE_')) {
const [type, nextDcId] = error.error_message.split('_MIGRATE_');
mtproto.setDefaultDc(+nextDcId);
return sendCode(phone_number);
}
})
.then(async result => {
return mtproto.call('auth.signIn', {
phone_code: await getCode(),
phone_number: phone_number,
phone_code_hash: result.phone_code_hash,
});
})
.catch(error => {
if (error.error_message === 'SESSION_PASSWORD_NEEDED') {
return mtproto.call('account.getPassword').then(async result => {
const { srp_id, current_algo, srp_B } = result;
const { salt1, salt2, g, p } = current_algo;
const { A, M1 } = await getSRPParams({
g,
p,
salt1,
salt2,
gB: srp_B,
password: await getPassword(),
});
return mtproto.call('auth.checkPassword', {
password: {
_: 'inputCheckPasswordSRP',
srp_id,
A,
M1,
},
});
});
}
})
.then(result => {
console.log('[+] successfully authenticated');
// start listener since the user has logged in now
startListener()
});
})
You can find the values for api_id and api_hash from https://my.telegram.org.
On the first run the script prompts the user for phone_number, code, and password.
[+] You must log in
√ Enter your phone number: ... <phone_number>
√ Enter the code sent: ... <code>
√ Enter Password: ... <2FA password>
and after the authentication is over
a sample run outputs:
[+] starting listener
[13820XXXXX] Ja
[13820XXXXX] Bis bald guys��
[13820XXXXX] Ja. �
[13820XXXXX] Bis später
[13820XXXXX] Jaaa�
The way I've checked the authentication status was taken from here.
Alternative libraries worth checking out that are active the time of writing (and can be used to create the same behavior with): Airgram (tdlib) and GramJs
I used gram.js library and essentially did this:
import { TelegramClient } from 'telegram'
TelegramClient().addEventHandler(handler, { chats: [1234567890] })
The bot does NOT need to be a member of the channel you want to listen to.
My code runs as a Node.js app.
You need to first create a token by talking to #BotFather with Telegram.
here is my working code by using gramjs and its purely on nodejs.
Getting all the messages from all the channels without any delay's.
import {
TelegramClient
} from "telegram";
import {
NewMessage,
NewMessageEvent
} from "telegram/events";
import {
StringSession
} from "telegram/sessions";
const input = require("input");
const apiId = 1233456677;
const apiHash = "xxxxxxxxxxxxxxxxx";
let stringSession = new StringSession("xxxxxxxxxxxxxxxxx");
(async() => {
console.log("Loading interactive example...");
const client = new TelegramClient(stringSession, apiId, apiHash, {
connectionRetries: 5,
});
await client.start({
phoneNumber: async() => await input.text("Please enter your number: "),
password: async() => await input.text("Please enter your password: "),
phoneCode: async() =>
await input.text("Please enter the code you received: "),
onError: (err) => console.log(err),
});
console.log("You should now be connected.");
const session: any = client.session.save();
stringSession = new StringSession(session); // Save this string to avoid logging in again - specially in nodemon
console.log(client.session.save()); // --> you can also copy this session from your console once you get it and paste it in line number 8 - new StringSession("XXXXXXXXXXXXXX")
// once you saved add the JWT Token on line no. 8 as mention above next time you will getting directly connected.
await client.sendMessage("me", {
message: "Hello!"
});
async function handler(event: NewMessageEvent) {
console.log("[newmessage]", event);
}
client.addEventHandler(handler, new NewMessage({}));
})();
Note - Ignore the "Run Code Snippet" as found it best way to add whole code instead of formatting.
You can use the gram.js library in the following way:
Install these:
npm install properties-reader
npm install telegram
npm install input
Then get your apiId and apiHash from Telegram Auth in the App Configuration section.
Create a file config.properties with a content similar to that:
[telegram]
apiId=12345678
apiHash=12345678901234567890123456789012
Inside of your nodejs code you can listen to a specific chat like this (see chatId inside of the code below):
const PropertiesReader = require('properties-reader');
const configs = PropertiesReader('config.properties');
getProp = (bundle, key) => {return configs.get(`${bundle}.${key}`);}
const { TelegramClient } = require("telegram");
const { StoreSession } = require("telegram/sessions");
const { NewMessage } = require("telegram/events");
const { EditedMessage } = require("telegram/events/EditedMessage");
const input = require("input");
const apiId = getProp("telegram", "apiId")
const apiHash = getProp("telegram", "apiHash")
const storeSession = new StoreSession("telegram_session"); // see: https://painor.gitbook.io/gramjs/getting-started/authorization#store-session
(async () => {
console.log("Loading interactive example...");
const client = new TelegramClient(storeSession, apiId, apiHash, {
connectionRetries: 5,
});
await client.start({
phoneNumber: async () => await input.text("Please enter your number: "),
password: async () => await input.text("Please enter your password: "),
phoneCode: async () =>
await input.text("Please enter the code you received: "),
onError: (err) => console.log(err),
});
console.log("You should now be connected.");
client.session.save(); // Save the session to avoid logging in again
async function eventPrint(event) {
// see 'node_modules/telegram/tl/custom/message.d.ts'
const message = event.message
const isNew = message.editDate === undefined
const text = message.text
const date = new Date(message.date*1000)
console.log(`The message is ${isNew ? 'new' : 'an update'}`)
console.log(`The text is: ${text}`)
console.log(`The date is: ${date}`)
}
// to get the chatId:
// option 1: open telegram on a web browser, go to the chat, and look the url in the address bar
// option 2: open telegram app, copy link to any message, it should be something like: https://t.me/c/1234567890/12345, the first number after "/c/" is the chatId
const chatId = 1234567890
client.addEventHandler(eventPrint, new NewMessage({ chats: [chatId] }));
client.addEventHandler(eventPrint, new EditedMessage({ chats: [chatId] }));
})();