Always when I try to join a Group Channel, I am presented with this error
code: 800101
message: "Connection should be made first."
name: "SendBirdException"
Here is my code for connecting to Group Channel.
I am connecting to the SendBird with connect in another function async, and only then calling the connectToChat
The group is created by another user, and this is the code for when a user tries to join that group.
User Connection works fine.
Group retrieval works fine.
But when I try to connect to the group, it errors out.
public connectToChat(chatId: string) {
return new Promise((s, e) => {
const sendBirdEngine = SendBird.getInstance();
try {
if (ChatService.isReady) {
this.log("Connecting to chat...", chatId);
sendBirdEngine.GroupChannel.getChannel(
chatId,
(groupChannel, error) => {
if (error) {
this.logError(
"An error occured while getting channel",
chatId,
error
);
return;
}
this.log("Got the channel!", chatId, groupChannel);
if (groupChannel.isPublic) {
groupChannel.join((response, err) => {
this.log(
"groupChannel Join",
response
// Always getting error here
);
if (err) {
this.logError("connectToChat", err);
e(false);
return;
} else {
s(true);
this.log("Joined the Chat!", chatId);
}
ChatService.chatRooms[
chatId
] = groupChannel;
this.log(
"Successfully Cached the Channel",
chatId
);
});
} else {
this.logError("[ERROR] Channel is Private");
}
// s(true);
}
);
} else {
this.logError("[ERROR] Chat Service is not ready");
}
} catch (err) {
e(err);
}
});
}
EDIT: Added Full Class File for complete reference
class ChatService {
public static userId: string;
public static chatRooms: {
[index: string]: SendBird.BaseChannel;
} = {};
private static isReady: boolean = false;
constructor(userId ? : string) {
if (userId && !ChatService.userId) {
ChatService.userId = userId;
} else {
this.log("userId already set", ChatService.userId);
}
}
/**
* create
*/
public create() {
return new Promise((s, e) => {
if (!ChatService.isReady) {
// connecting to sendbird here
const sendBirdEngine = new SendBird({
appId: "XXXXX-XXXXXX-XXXXXX",
});
this.log("Engine Initialised!", sendBirdEngine);
// init the user
this.initialiseUser((data: any) => {
s(data);
});
}
});
}
/**
* initialise
*/
public async initialiseUser(onReadyHandler: any) {
const userId = ChatService.userId;
this.log("Starting ChatService", userId);
try {
this.connectUserToEngine((res: any) => {
this.log("connectUser() callback", res);
ChatService.isReady = true;
// this.getListOfChatRooms();
onReadyHandler(true);
});
} catch (err) {
onReadyHandler(false);
this.log("[ChatService Error]", err);
}
}
/**
* connects user to engine
*/
public connectUserToEngine(callback: any) {
const sendBirdEngine = SendBird.getInstance();
const userId = ChatService.userId;
this.log("Connecting user...", userId);
sendBirdEngine.connect(userId, (user: any, error: any) => {
if (error) {
this.log("[Error]", error);
this.log("Reconnecting User in 5 seconds...");
setTimeout(() => {
this.connectUserToEngine(callback);
}, 5000);
return;
} else {
this.log("User Connected", user);
callback(user);
}
});
}
/**
* connect to a particular chat
*/
public connectToChat(chatId: string, onNewMessageListener: any) {
return new Promise((s, e) => {
const sendBirdEngine = SendBird.getInstance();
this.log("Current User", sendBirdEngine.currentUser);
try {
if (ChatService.isReady) {
this.log("Connecting to chat...", chatId);
// this.connectUserToEngine(() => {
sendBirdEngine.GroupChannel.getChannel(
chatId,
(groupChannel, error) => {
if (error) {
this.logError(
"An error occured while getting channel",
chatId,
error
);
return;
}
this.log("Got the channel!", chatId, groupChannel);
if (groupChannel.isPublic) {
groupChannel.join((response, err) => {
this.log(
"groupChannel Join",
response
// err
);
// FIXME: Assuming it always works
if (err) {
this.logError("connectToChat", err);
e(false);
return;
} else {
s(true);
this.log("Joined the Chat!", chatId);
}
ChatService.chatRooms[
chatId
] = groupChannel;
this.log(
"Successfully Cached the Channel",
chatId
);
});
} else {
this.logError("[ERROR] Channel is Private");
}
// s(true);
}
);
// });
} else {
this.logError("[ERROR] Chat Service is not ready");
}
} catch (err) {
e(err);
}
});
}
/**
* connects to all chat rooms
*/
public async connectToAllChatRooms(
chatRooms: string[],
onNewMessageListener: any
) {
try {
this.log("connectToAllChatRooms()", chatRooms);
// connect to all chat rooms
for (const chatRoom of chatRooms) {
const x = await this.connectToChat(
chatRoom,
onNewMessageListener
);
}
this.log("connectToAllChatRooms() done");
return true;
} catch (err) {
this.logError("connectToAllChatRooms", err);
throw new Error(err);
}
}
export default ChatService;
I took a look at your code. You ChatService class might need a little change.
Idea 1
On the ChatService class you have a create() method that is not returning a promise, if a user already exists.
public create() {
return new Promise((s, e) => {
if (!ChatService.isReady) {
// connecting to sendbird here
const sendBirdEngine = new SendBird({
appId: "APP_ID"
});
this.log("Engine Initialised!", sendBirdEngine);
// init the user
this.initialiseUser((data: any) => {
s(data);
});
} else {
// Resolve promise when user already exists
s("Already Connected!");
}
});
}
From there it seems to work as expected. Correct me if I'm wrong but this is how I implement your class.
const initChat = () => {
const url = "CHANNEL_URL";
const chat = new ChatService("USER_ID");
chat.create().then(res => {
chat.connectToChat(url).then((res)=>{
console.log("DONE", res)
})
});
};
Idea 2
Also: Perhaps check to see if you are calling
sendBirdEngine.GroupChannel.getChannel()
Before the connection to the user has completed.
Example
Here is a working example of your code if needed. It needs:
index.js - CHANNEL_URL and USER_ID
ChatService.ts - APP_ID
Related
I created a singleton to store a pool connection during the lifetime of the application :
var { MongoClient } = require("mongodb");
var credentials = require("./../database/credentials.json");
class MongoInstance {
static _db;
static isInitialized() {
return MongoInstance._db ? true : false;
}
static async connect() {
const client = new MongoClient("mongodb://localhost:27017");
try {
await client.connect();
console.log("Connected successfully to server");
MongoInstance._db = client.db(credentials.dbName);
return "done";
} catch (e) {
console.error("MongoDB connection error: ", e);
}
}
static closeConnection() {
if (MongoInstance.isInitialized()) MongoInstance._db.client.close();
}
static get db() {
if (MongoInstance.isInitialized()) return MongoInstance._db;
else {
MongoInstance.connect().then(() => {
return MongoInstance._db;
});
}
}
}
module.exports = MongoInstance;
I call this singleton with the following method :
var MongoInstance = require("./../database/mongodb");
....
const find = async function (collection, limit) {
try {
const query = {};
const sort = { length: -1 };
return await MongoInstance.db
.collection(collection)
.find(query)
.sort(sort)
.limit(+limit)
.toArray();
} catch (e) {
console.error("ERROR MongoDB find(): ", e);
return null;
}
};
But I get this error :
ERROR MongoDB find(): TypeError: Cannot read property 'collection' of undefined
What does it mean ?
Link to the official doc : npm
Just to make it clear router uses the code below and my messages.js are inside api folder....
router.use("/messages", require("./messages"));
so my api call is correct.
Backend for posting the message.... I know conversationId will be null if no conversation exists but... I am trying to send message where conversation exists already and still I am getting cannot read the conversationId of undefined....
// expects {recipientId, text, conversationId } in body
// (conversationId will be null if no conversation exists yet)
router.post("/", async (req, res, next) => {
try {
if (!req.user) {
return res.sendStatus(401);
}
const senderId = req.user.id;
const { recipientId, text, conversationId, sender } = req.body;
// if we already know conversation id, we can save time and just add it to message and return
if (conversationId) {
const message = await Message.create({ senderId, text, conversationId });
return res.json({ message, sender });
}
// if we don't have conversation id, find a conversation to make sure it doesn't already exist
let conversation = await Conversation.findConversation(
senderId,
recipientId
);
if (!conversation) {
// create conversation
conversation = await Conversation.create({
user1Id: senderId,
user2Id: recipientId,
});
if (onlineUsers.includes(sender.id)) {
sender.online = true;
}
}
const message = await Message.create({
senderId,
text,
conversationId: conversation.id,
});
res.json({ message, sender });
} catch (error) {
next(error);
}
});
module.exports = router;
This is the frontend that posts the data to the backend....
const saveMessage = async (body) => {
const { data } = await axios.post("/api/messages", body);
return data;
};
Okay so here is detail information on how I am dispatching it.
class Input extends Component {
constructor(props) {
super(props);
this.state = {
text: "",
};
}
handleChange = (event) => {
this.setState({
text: event.target.value,
});
};
handleSubmit = async (event) => {
event.preventDefault();
// add sender user info if posting to a brand new convo,
// so that the other user will have access to username, profile pic, etc.
const reqBody = {
text: event.target.text.value,
recipientId: this.props.otherUser.id,
conversationId: this.props.conversationId,
sender: this.props.conversationId ? null : this.props.user,
};
await this.props.postMessage(reqBody);
this.setState({
text: "",
});
};
render() {
const { classes } = this.props;
return (
<form className={classes.root} onSubmit={this.handleSubmit}>
<FormControl fullWidth hiddenLabel>
<FilledInput
classes={{ root: classes.input }}
disableUnderline
placeholder="Type something..."
value={this.state.text}
name="text"
onChange={this.handleChange}
/>
</FormControl>
</form>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(Input));
const mapDispatchToProps = (dispatch) => {
return {
postMessage: (message) => {
dispatch(postMessage(message));
},
};
};
// message format to send: {recipientId, text, conversationId}
// conversationId will be set to null if its a brand new conversation
export const postMessage = (body) => (dispatch) => {
try {
const data = saveMessage(body);
if (!body.conversationId) {
dispatch(addConversation(body.recipientId, data.message));
} else {
dispatch(setNewMessage(data.message));
}
sendMessage(data, body);
} catch (error) {
console.error(error);
}
};
So I have attached what I want to do here now....
But I am still getting the problem....
// CONVERSATIONS THUNK CREATORS, this is how I am getting data from the backend
export const fetchConversations = () => async (dispatch) => {
try {
const { data } = await axios.get("/api/conversations");
dispatch(gotConversations(data));
} catch (error) {
console.error(error);
}
};
export const setNewMessage = (message, sender) => {
return {
type: SET_MESSAGE,
payload: { message, sender: sender || null },
};
};
// REDUCER
const reducer = (state = [], action) => {
switch (action.type) {
case GET_CONVERSATIONS:
return action.conversations;
case SET_MESSAGE:
return addMessageToStore(state, action.payload);
case ADD_CONVERSATION:
return addNewConvoToStore(
state,
action.payload.recipientId,
action.payload.newMessage
);
default:
return state;
}
};
I am getting an error saying Cannot read property 'conversationId' of undefined while using a reducer function... Should I give the setintial value of the message to empty?
export const addMessageToStore = (state, payload) => {
const { message, sender } = payload;
// if sender isn't null, that means the message needs to be put in a brand new convo
if (sender !== null) {
const newConvo = {
id: message.conversationId,
otherUser: sender,
messages: [message],
};
newConvo.latestMessageText = message.text;
return [newConvo, ...state];
}
return state.map((convo) => {
if (convo.id === message.conversationId) {
const convoCopy = { ...convo };
convoCopy.messages.push(message);
convoCopy.latestMessageText = message.text;
return convoCopy;
} else {
return convo;
}
});
};
Issue
The saveMessage function is declared async
const saveMessage = async (body) => {
const { data } = await axios.post("/api/messages", body);
return data;
};
but the postMessage action creator isn't async so it doesn't wait for the implicitly returned Promise to resolve before continuing on and dispatching to the store. This means that data.message is undefined since a Promise object doesn't have this as a property.
export const postMessage = (body) => (dispatch) => {
try {
const data = saveMessage(body); // <-- no waiting
if (!body.conversationId) {
dispatch(addConversation(body.recipientId, data.message));
} else {
dispatch(setNewMessage(data.message));
}
sendMessage(data, body);
} catch (error) {
console.error(error);
}
};
Solution
Declare postMessage async as well and await the data response value.
export const postMessage = (body) => async (dispatch) => {
try {
const data = await saveMessage(body); // <-- await response
if (!body.conversationId) {
dispatch(addConversation(body.recipientId, data.message));
} else {
dispatch(setNewMessage(data.message));
}
sendMessage(data, body);
} catch (error) {
console.error(error);
}
};
I currently have a script that checks for an incoming email (in a mailbox) every 30 seconds, using a recursion.
The package I'm using for this testing is imap-simple.
The below script currently does this as required;
var imaps = require('imap-simple');
const { connect } = require('net');
var config = {
imap: {
user: 'qatestspecialist#outlook.com',
password: 'specialistQa',
host: 'imap-mail.outlook.com',
port: 993,
tls: true,
authTimeout: 30000
}
};
module.exports = {
'delete any existing emails...': function () {
imaps.connect(config).then(function (connection) {
connection.openBox('INBOX').then(function () {
var searchCriteria = ['ALL'];
var fetchOptions = { bodies: ['TEXT'], struct: true };
return connection.search(searchCriteria, fetchOptions);
//Loop over each message
}).then(function (messages) {
let taskList = messages.map(function (message) {
return new Promise((res, rej) => {
var parts = imaps.getParts(message.attributes.struct);
parts.map(function (part) {
return connection.getPartData(message, part)
.then(function (partData) {
//Display e-mail body
if (part.disposition == null && part.encoding != "base64"){
console.log(partData);
}
//Mark message for deletion
connection.addFlags(message.attributes.uid, "\Deleted", (err) => {
if (err){
console.log('Problem marking message for deletion');
rej(err);
}
res(); //Final resolve
});
});
});
});
});
return Promise.all(taskList).then(() => {
connection.imap.closeBox(true, (err) => { //Pass in false to avoid delete-flagged messages being removed
if (err){
console.log(err);
}
});
connection.end();
});
});
});
},
'send email to seller and wait for mailbox notification': function (browser) {
// script to send an email to the mailbox...
},
'get new email info': function(browser) {
const createPromise = ms => new Promise((resolve, reject) => {
setTimeout(() => resolve(ms), ms)
});
function findUnseenEmails(connection) {
return connection.openBox('INBOX').then(function () {
var searchCriteria = ['UNSEEN'];
var fetchOptions = {
bodies: ['HEADER', 'TEXT'],
markSeen: false
};
return connection.search(searchCriteria, fetchOptions).then(function (results) {
var subjects = results.map(function (res) {
return res.parts.filter(function (part) {
return part.which === 'HEADER';
})
[0].body.subject[0];
});
return subjects.length > 0 ? subjects : createPromise(30000).then(function() { return findUnseenEmails(connection);
});
});
});
}
imaps.connect(config).then(function (connection) {
return findUnseenEmails(connection)
})
.then((subjects) => console.log(JSON.stringify(subjects)));
},
'Closing the browser': function (browser) {
browser.browserEnd();
}
};
This waits for an email and then displays the email 'header'.
However, the imap connection does not close, and stays open which is stopping my test suite from completing as the associated test never actually finishes.
I've tried adding the imap-simple command connection.end() in several places after the
imaps.connect(config).then(function (connection) {
return findUnseenEmails(connection)
})
part of the script, but it doesn't work.
So I'm just wondering if anyone knows where I should be adding this connection.end() command in order for the connection to be closed once an email has been received?
Any help would be greatly appreciated.
This has now been resolved in another post, using the following;
if (subjects.length > 0) {
connection.end();
return subjects;
} else {
return createPromise(5000).then(function() { return findUnseenEmails(connection)});
}
I wanted to get user information from my collection using their ID to send them notifications. This are my functions in index.ts
export const sendNotifications = functions.firestore
.document('messages/{groupId1}/{groupId2}/{message}')
.onCreate((snapshot, context) =>{
console.log('Starting sendNotification Function');
const doc = snapshot.data();
console.log(doc.content);
console.log(getUserData(doc.idFrom))
return true;
});
export async function getUserData(id: string){
try {
const snapshot = await admin.firestore().collection('users').doc(id).get();
const userData = snapshot.data();
if(userData){
return userData.nickname;
}
} catch (error) {
console.log('Error getting User Information:', error);
return `NOT FOUND: ${error}`
}
}
From my deploy, I get the console log messages, the 'Starting sendNotification Function', then the actual 'doc.content' then an error for my 'getUserData(doc.idFrom)'.
Promise {
<pending>,
domain:
Domain {
domain: null,
_events: { error: [Function] },
_eventsCount: 1,
_maxListeners: undefined,
members: [] } }
Thank you in advance!
You should call your async getUserData() function with await.
The following should do the trick (untested):
export const sendNotifications = functions.firestore
.document('messages/{groupId1}/{groupId2}/{message}')
.onCreate(async (snapshot, context) => {
try {
console.log('Starting sendNotification Function');
const doc = snapshot.data();
console.log(doc.content);
const nickname = await getUserData(doc.idFrom);
// Do something with the nickname value
return true;
} catch (error) {
// ...
}
});
async function getUserData(id: string) {
try {
const snapshot = await admin.firestore().collection('users').doc(id).get();
if (snapshot.exists) {
const userData = snapshot.data();
return userData.nickname;
} else {
//Throw an error
}
} catch (error) {
// I would suggest you throw an error
console.log('Error getting User Information:', error);
return `NOT FOUND: ${error}`;
}
}
Or, if you don't want to have the Cloud Function async, you can do as follows:
export const sendNotifications = functions.firestore
.document('messages/{groupId1}/{groupId2}/{message}')
.onCreate((snapshot, context) => {
console.log('Starting sendNotification Function');
const doc = snapshot.data();
console.log(doc.content);
return getUserData(doc.idFrom)
.then((nickname) => {
// Do something with the nickname value
return true;
})
.catch((error) => {
console.log(error);
return true;
});
});
So I was working on a new component in Angular and in the ngOninit I have the following asynchronous functions below...
This.getUserProfile needs to be finished before I can call this.getPrivateGroup() and this.getPrivateGroup() needs to be finished before I can call this.loadGroupPosts(). I know I could write these functions inside the callback of the asynchronous requests, but I was wondering if there is a way to keep it in ngOnInit to keep it cleaner?
Anyone has an idea?
ngOnInit() {
this.getUserProfile();
// my-workplace depends on a private group and we need to fetch that group and edit
// the group data before we proceed and get the group post
if (this.isItMyWorkplace) {
this.getPrivateGroup();
}
this.loadGroupPosts();
}
getUserProfile() {
this._userService.getUser()
.subscribe((res) => {
this.user = res.user;
console.log('log user', this.user);
this.profileImage = res.user['profile_pic'];
this.profileImage = this.BASE_URL + `/uploads/${this.profileImage}`;
}, (err) => {
this.alert.class = 'alert alert-danger';
if (err.status === 401) {
this.alert.message = err.error.message;
setTimeout(() => {
localStorage.clear();
this._router.navigate(['']);
}, 3000);
} else if (err.status) {
this.alert.class = err.error.message;
} else {
this.alert.message = 'Error! either server is down or no internet connection';
}
});
}
getPrivateGroup() {
console.log('user check', this.user);
this.groupService.getPrivateGroup(`${this.user.first_name}${this.user.last_name}`)
.subscribe((group) => {
console.log('received response', group)
})
}
// !--LOAD ALL THE GROUP POSTS ON INIT--! //
loadGroupPosts() {
this.isLoading$.next(true);
this.postService.getGroupPosts(this.group_id)
.subscribe((res) => {
// console.log('Group posts:', res);
this.posts = res['posts'];
console.log('Group posts:', this.posts);
this.isLoading$.next(false);
this.show_new_posts_badge = 0;
}, (err) => {
swal("Error!", "Error while retrieving the posts " + err, "danger");
});
}
// !--LOAD ALL THE GROUP POSTS ON INIT--! //
You can use basic promises with async/await.
async ngOnInit() {
await this.getUserProfile(); // <-- 1. change
// my-workplace depends on a private group and we need to fetch that group and edit
// the group data before we proceed and get the group post
if (this.isItMyWorkplace) {
this.getPrivateGroup();
}
this.loadGroupPosts();
}
async getUserProfile() {
this._userService.getUser()
.subscribe((res) => {
this.user = res.user;
console.log('log user', this.user);
this.profileImage = res.user['profile_pic'];
this.profileImage = this.BASE_URL + `/uploads/${this.profileImage}`;
return true; // <-- this
}, (err) => {
this.alert.class = 'alert alert-danger';
if (err.status === 401) {
this.alert.message = err.error.message;
setTimeout(() => {
localStorage.clear();
this._router.navigate(['']);
}, 3000);
} else if (err.status) {
this.alert.class = err.error.message;
} else {
this.alert.message = 'Error! either server is down or no internet connection';
}
throw err;
});
}
You could instead leverage RxJS and use a switchMap something like this (syntax NOT checked):
getData(): Observable<string[]> {
return this._userService.getUser()
.pipe(
switchMap(userInfo=> {
return this.getPrivateGroup();
}),
catchError(this.someErrorHandler)
);
}
One way to do is, return the Observable instead of subscribing in the getPrivateGroup()
getPrivateGroup() {
console.log('user check', this.user);
return this.groupService.getPrivateGroup(`${this.user.first_name}${this.user.last_name}`)
}
And then, subscribe to the data where you want the chain the this.loadGroupPosts()
if (this.isItMyWorkplace) {
this.getPrivateGroup().subscribe(group => {
this.group = group; //you probably want to assign the group data
this.loadGroupPosts()});
}
you could also use the 3rd part of your subscribe function when its completed
i am not quite sure if this is a clean solution, in my opinion it is.
ngOnInit() {
this.getUserProfile();
}
getUserProfile() {
this._userService.getUser()
.subscribe((res) => {
this.user = res.user;
console.log('log user', this.user);
this.profileImage = res.user['profile_pic'];
this.profileImage = this.BASE_URL + `/uploads/${this.profileImage}`;
}, (err) => {
this.alert.class = 'alert alert-danger';
if (err.status === 401) {
this.alert.message = err.error.message;
setTimeout(() => {
localStorage.clear();
this._router.navigate(['']);
}, 3000);
} else if (err.status) {
this.alert.class = err.error.message;
} else {
this.alert.message = 'Error! either server is down or no internet connection';
}
}, () => {
// my-workplace depends on a private group and we need to fetch that group and edit
// the group data before we proceed and get the group post
if (this.isItMyWorkplace) {
this.getPrivateGroup();
}
});
}
getPrivateGroup() {
console.log('user check', this.user);
this.groupService.getPrivateGroup(`${this.user.first_name}${this.user.last_name}`)
.subscribe((group) => {
console.log('received response', group)
}, error => {
console.log(error)
}, () => {
this.loadGroupPosts();
})
}
loadGroupPosts() {
this.isLoading$.next(true);
this.postService.getGroupPosts(this.group_id)
.subscribe((res) => {
// console.log('Group posts:', res);
this.posts = res['posts'];
console.log('Group posts:', this.posts);
this.isLoading$.next(false);
this.show_new_posts_badge = 0;
}, (err) => {
swal("Error!", "Error while retrieving the posts " + err, "danger");
});
}