In my main page I have a list of users and i'd like to choose and open a channel to chat with one of them.
I am thinking if use the id is the best way and control an access of a channel like USERID1-USERID2.
But of course, user 2 can open the same channel too, so I'd like to find something more easy to control.
Please, if you want to help me, give me an example in javascript using a firebase url/array.
Thank you!
A common way to handle such 1:1 chat rooms is to generate the room URL based on the user ids. As you already mention, a problem with this is that either user can initiate the chat and in both cases they should end up in the same room.
You can solve this by ordering the user ids lexicographically in the compound key. For example with user names, instead of ids:
var user1 = "Frank"; // UID of user 1
var user2 = "Eusthace"; // UID of user 2
var roomName = 'chat_'+(user1<user2 ? user1+'_'+user2 : user2+'_'+user1);
console.log(user1+', '+user2+' => '+ roomName);
user1 = "Eusthace";
user2 = "Frank";
var roomName = 'chat_'+(user1<user2 ? user1+'_'+user2 : user2+'_'+user1);
console.log(user1+', '+user2+' => '+ roomName);
<script src="https://getfirebug.com/firebug-lite-debug.js"></script>
A common follow-up questions seems to be how to show a list of chat rooms for the current user. The above code does not address that. As is common in NoSQL databases, you need to augment your data model to allow this use-case. If you want to show a list of chat rooms for the current user, you should model your data to allow that. The easiest way to do this is to add a list of chat rooms for each user to the data model:
"userChatrooms" : {
"Frank" : {
"Eusthace_Frank": true
},
"Eusthace" : {
"Eusthace_Frank": true
}
}
If you're worried about the length of the keys, you can consider using a hash codes of the combined UIDs instead of the full UIDs.
This last JSON structure above then also helps to secure access to the room, as you can write your security rules to only allow users access for whom the room is listed under their userChatrooms node:
{
"rules": {
"chatrooms": {
"$chatroomid": {
".read": "
root.child('userChatrooms').child(auth.uid).child(chatroomid).exists()
"
}
}
}
}
In a typical database schema each Channel / ChatGroup has its own node with unique $key (created by Firebase). It shouldn't matter which user opened the channel first but once the node (& corresponding $key) is created, you can just use that as channel id.
Hashing / MD5 strategy of course is other way to do it but then you also have to store that "route" info as well as $key on the same node - which is duplication IMO (unless Im missing something).
We decided on hashing users uid's, which means you can look up any existing conversation,if you know the other persons uid.
Each conversation also stores a list of the uids for their security rules, so even if you can guess the hash, you are protected.
Hashing with js-sha256 module worked for me with directions of Frank van Puffelen and Eduard.
import SHA256 from 'crypto-js/sha256'
let agentId = 312
let userId = 567
let chatHash = SHA256('agent:' + agentId + '_user:' + userId)
Related
I am writting chat app with flutter, I don't know how to set the rule on firebase such only the 2 users in a group chat can write and read. This is how I store message on firebase.
String date = DateTime.now().millisecondsSinceEpoch.toString();
groupId = userId-anotherId
var ref = FirebaseFirestore.instance
.collection('messages')
.doc(groupId)
.collection(groupId)
.doc(date);
FirebaseFirestore.instance.runTransaction((transaction) async {
transaction.set(ref, {
"senderId": userId,
"receiverId": anotherId,
"timestamp": date,
'content': msg,
"type": 'text',
});
});
Thank you for any help.
Contacting database from frontend is not the most secure way.
If you are making this app for learning then it is fine.
But for production app, you should add custom logic like this in your backend and connect your frontend with that API.
Something like this.
Flutter app --> API --> YOUR_LOGIC --> DATABASE
You would need a collection of members for a group chat. That way you can very easy check with the rules just if a member uid is under the path of groupChats/{groupID}/members/{id}. You can even make a function for the firestoe rules like this:
//Checks if user is member of group
function isMember(groupID) {
return get(/databases/$(database)/documents/groupChats/${groupID}/members/$(request.auth.uid)).data==true
}
The databse structure would be:
-
groupChats
--group1
members
--userUid1
--userUid2
--group2
members
--userUid2
--userUid4
You can use a boolean or any data you want.
The solution was very simple. I saw in youtube. enter link description here
This is the security rule:
match /messages/{groupChatId}/{document=**} {
allow write, read:if request.auth.uid in groupChatId.split(" - ") }
Data structure:
houses (collection)
name (string)
users (map)
90c234jc23 (map)
percentage: 100% (string/number)
Rules:
allow read: request.auth.uid in resource.data.users;
The problem is when I try to query houses which user owns:
FirebaseFirestore.getInstance().collection(House.COLLECTION)
// .whereArrayContains(House.USERS_FIELD, currentUser.getUid()) // does not work
.whereEqualTo("users." + currentUser.getUid(), currentUser.getUid()) // does not work either
.get()
No result are returned.
You cannot perform this type of query in firestore as there is no 'map-contains-key' operator. However, there are very simple workarounds for implementing this by making slight adjustments to your datastructure.
Specific Solution
Requirement: For this solution to work, each map value has to be uniquely identifyable in a firestore query, meaning it cannot be a map or an array.
If your case meets the listed requirements, you can go with #Dennis Alund's solution which suggests the following data structure:
{
name: "The Residence",
users: {
uid1: 80,
uid2: 20
}
}
General Solution
If your map values are maps or arrays, you need to add a property to each value which will be constant across all created values of this type. Here is an example:
{
name: "The Residence",
users: {
uid1: {
exists: true,
percentage: 80,
...
},
uid2: {
exists: true,
percentage: 20,
...
},
}
}
Now you can simply use the query:
_firestore.collection('houses').whereEqualTo('users.<uid>.exists', true)
Edit:
As #Johnny Oshika correctly pointed out, you can also use orderBy() to filter by field-name.
You can use orderBy to find documents where map contains a certain key. Using this example document:
{
"users": {
"bob": {},
"sam": {},
}
}
.orderBy('users.bob') will only find documents that contain users.bob.
This query is not working because your users field is a map and not an array.
.whereArrayContains(House.USERS_FIELD, currentUser.getUid())
This query
.whereEqualTo("users." + currentUser.getUid(), currentUser.getUid())
is not working because your map value for users.<uid> is a string that says percentage: xx% and that statement is testing if percentage: xx% === <uid>, which is false.
And that strategy will be problematic since you can not do queries to find items that "are not null" or "strings not empty", etc.
I'm assuming that the percentage is the user's ownership in the house (?). If so, you might have better luck in trying to structure your house document data like this if you want to maintain the same structure of document as in your question
{
name: "The Residence",
users: {
uid1: 80,
uid2: 20
}
}
That will allow you to do a query such as
.whereGreaterThan("users." + currentUser.getUid(), 0)
to find users that has some shares of ownership in that house.
But a fair bit of warning, as soon as you need composite indexes you will start having problems to maintain that structure. You might instead want to consider storing an array of users that owns that house for ease of querying.
I've read the Firebase docs on Stucturing Data. Data storage is cheap, but the user's time is not. We should optimize for get operations, and write in multiple places.
So then I might store a list node and a list-index node, with some duplicated data between the two, at very least the list name.
I'm using ES6 and promises in my javascript app to handle the async flow, mainly of fetching a ref key from firebase after the first data push.
let addIndexPromise = new Promise( (resolve, reject) => {
let newRef = ref.child('list-index').push(newItem);
resolve( newRef.key()); // ignore reject() for brevity
});
addIndexPromise.then( key => {
ref.child('list').child(key).set(newItem);
});
How do I make sure the data stays in sync in all places, knowing my app runs only on the client?
For sanity check, I set a setTimeout in my promise and shut my browser before it resolved, and indeed my database was no longer consistent, with an extra index saved without a corresponding list.
Any advice?
Great question. I know of three approaches to this, which I'll list below.
I'll take a slightly different example for this, mostly because it allows me to use more concrete terms in the explanation.
Say we have a chat application, where we store two entities: messages and users. In the screen where we show the messages, we also show the name of the user. So to minimize the number of reads, we store the name of the user with each chat message too.
users
so:209103
name: "Frank van Puffelen"
location: "San Francisco, CA"
questionCount: 12
so:3648524
name: "legolandbridge"
location: "London, Prague, Barcelona"
questionCount: 4
messages
-Jabhsay3487
message: "How to write denormalized data in Firebase"
user: so:3648524
username: "legolandbridge"
-Jabhsay3591
message: "Great question."
user: so:209103
username: "Frank van Puffelen"
-Jabhsay3595
message: "I know of three approaches, which I'll list below."
user: so:209103
username: "Frank van Puffelen"
So we store the primary copy of the user's profile in the users node. In the message we store the uid (so:209103 and so:3648524) so that we can look up the user. But we also store the user's name in the messages, so that we don't have to look this up for each user when we want to display a list of messages.
So now what happens when I go to the Profile page on the chat service and change my name from "Frank van Puffelen" to just "puf".
Transactional update
Performing a transactional update is the one that probably pops to mind of most developers initially. We always want the username in messages to match the name in the corresponding profile.
Using multipath writes (added on 20150925)
Since Firebase 2.3 (for JavaScript) and 2.4 (for Android and iOS), you can achieve atomic updates quite easily by using a single multi-path update:
function renameUser(ref, uid, name) {
var updates = {}; // all paths to be updated and their new values
updates['users/'+uid+'/name'] = name;
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.once('value', function(snapshot) {
snapshot.forEach(function(messageSnapshot) {
updates['messages/'+messageSnapshot.key()+'/username'] = name;
})
ref.update(updates);
});
}
This will send a single update command to Firebase that updates the user's name in their profile and in each message.
Previous atomic approach
So when the user change's the name in their profile:
var ref = new Firebase('https://mychat.firebaseio.com/');
var uid = "so:209103";
var nameInProfileRef = ref.child('users').child(uid).child('name');
nameInProfileRef.transaction(function(currentName) {
return "puf";
}, function(error, committed, snapshot) {
if (error) {
console.log('Transaction failed abnormally!', error);
} else if (!committed) {
console.log('Transaction aborted by our code.');
} else {
console.log('Name updated in profile, now update it in the messages');
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.on('child_added', function(messageSnapshot) {
messageSnapshot.ref().update({ username: "puf" });
});
}
console.log("Wilma's data: ", snapshot.val());
}, false /* don't apply the change locally */);
Pretty involved and the astute reader will notice that I cheat in the handling of the messages. First cheat is that I never call off for the listener, but I also don't use a transaction.
If we want to securely do this type of operation from the client, we'd need:
security rules that ensure the names in both places match. But the rules need to allow enough flexibility for them to temporarily be different while we're changing the name. So this turns into a pretty painful two-phase commit scheme.
change all username fields for messages by so:209103 to null (some magic value)
change the name of user so:209103 to 'puf'
change the username in every message by so:209103 that is null to puf.
that query requires an and of two conditions, which Firebase queries don't support. So we'll end up with an extra property uid_plus_name (with value so:209103_puf) that we can query on.
client-side code that handles all these transitions transactionally.
This type of approach makes my head hurt. And usually that means that I'm doing something wrong. But even if it's the right approach, with a head that hurts I'm way more likely to make coding mistakes. So I prefer to look for a simpler solution.
Eventual consistency
Update (20150925): Firebase released a feature to allow atomic writes to multiple paths. This works similar to approach below, but with a single command. See the updated section above to read how this works.
The second approach depends on splitting the user action ("I want to change my name to 'puf'") from the implications of that action ("We need to update the name in profile so:209103 and in every message that has user = so:209103).
I'd handle the rename in a script that we run on a server. The main method would be something like this:
function renameUser(ref, uid, name) {
ref.child('users').child(uid).update({ name: name });
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.once('value', function(snapshot) {
snapshot.forEach(function(messageSnapshot) {
messageSnapshot.update({ username: name });
})
});
}
Once again I take a few shortcuts here, such as using once('value' (which is in general a bad idea for optimal performance with Firebase). But overall the approach is simpler, at the cost of not having all data completely updated at the same time. But eventually the messages will all be updated to match the new value.
Not caring
The third approach is the simplest of all: in many cases you don't really have to update the duplicated data at all. In the example we've used here, you could say that each message recorded the name as I used it at that time. I didn't change my name until just now, so it makes sense that older messages show the name I used at that time. This applies in many cases where the secondary data is transactional in nature. It doesn't apply everywhere of course, but where it applies "not caring" is the simplest approach of all.
Summary
While the above are just broad descriptions of how you could solve this problem and they are definitely not complete, I find that each time I need to fan out duplicate data it comes back to one of these basic approaches.
To add to Franks great reply, I implemented the eventual consistency approach with a set of Firebase Cloud Functions. The functions get triggered whenever a primary value (eg. users name) gets changed, and then propagate the changes to the denormalized fields.
It is not as fast as a transaction, but for many cases it does not need to be.
I have a quick question about the best practices for data structure in a firebase database.
I want users of my app to be able to maintain a friends list. The firebase documentation recommends creating a schema (not sure if thats the proper word in this context) that is as flat as possible. Because of this I thought it would be a good idea to separate the friends section from the player section in the database like so:
{
"players":{
"player1id":{
"username":"john",...
},
"player2id": ...,
"player3id": ...
}
"friends": {
"player1id"{
"friends":{
"friend1Id":true,
"friend2Id":true
}
},
}
"player2id"{
"friends":{
"friend1Id":true,
"friend2Id":true
}
},
}
}
So my questions are as follows:
Is this a good design for my schema?
When pulling a friends list for one player, will the friends lists of EVERY player be pulled? and if so, can this be avoided?
Also, what would be the best way to then pull in additional information about the friends once the app has all of their IDs. e.g. getting all of their user names which will be stored as a string in their player profile.
Is this a good design for my schema?
You're already thinking in the right direction. However the "friends" node can be simplified to:
"friends": {
"player1id": {
"friend1Id":true,
"friend2Id":true
}
}
Remember that Firebase node names cannot use the character dot (.). So if your IDs are integer such as 1, 2, and 3 everything is OK, but if the IDs are username be careful (for example "super123" is OK but "super.duper" is not)
When pulling a friends list for one player, will the friends lists of EVERY player be pulled? and if so, can this be avoided?
No. If you pull /friends/1 it obviously won't pull /friends/2 etc.
Also, what would be the best way to then pull in additional information about the friends once the app has all of their IDs. e.g. getting all of their user names which will be stored as a string in their player profile.
Loop through the IDs and fetch the respective nodes from Firebase again. For example if user 1 has friends 2, 3, and 4, then using a for loop fetch /players/2, /players/3, and /players/4
Since firebase pull works asynchronously, you might need to use a counter or some other mechanism so that when the last data is pulled you can continue running the completion code.
In my application i want to store some specific data for a particular parse user and I want to fetch data from it. Should I create a class in data browser using the unique id of user or something else? Can anyone help me? Thanks in advance.
Assuming the use of the Rest API , a minimum prerequisite to exclusive READ/WRITE for a specific parse user would be to do follow:
create new user according to the parse docs with POST to users table...
-d '{"username":"yourName","password":"e8"}' https://api.parse.com/1/users
get the response from above request and parse the OID of the new user and the TOKEN value in the response. Persist them in your app to a durable 'user_prefs' type for future use with something like...
PreferenceManager.getDefaultSharedPreferences(myActivity).edit().put....
When you want to Write objects for that user do the following:
include in headers,
"X-Parse-Session-Token: pd..." // the token you saved
include ACL tag in json for POST of the parse class/object that you are writing...
the user OID within the ACL tag should be the OID value from the response when you created the new parse user
-H "X-Parse-Session-Token: pdqnd......." \
-d '{"ACL": {"$UserOID": {
"read": true,
"write": true
}}}' \
https://api.parse.com/1/classes/MyClass
READS:
Because the ACL for every object written to MyClass is exclusive to the user in '$UserOID, noone else can see them. As long as you include the token value in a header with any read, $UserOID will be the only one with access. The token value, originally returned when you created the new user is logically bound to the userOID and can be used in the header in kind of magic way... No server-session required on the device when the app starts, no explicit user authentication step required to the server, no query expression in a GET - simply provide the token value in the header layer and request all records(all users) it works to get records for just the userID inferred from the token value in the header. On init, the app just has to retrieve the token from 'shared_prefs' on the client side. Server side, the token lease is permanent.
-H "X-Parse-Session-Token: pdqnd......."
include above with every GET. -H "X-Parse-Session-Token: pdqnd......." You will be the only parse user who can see them...
if you want multiple devices bound to one parse user, this is NG.
if you want multiple parse accounts to be accessed from one instance of the app, this is NG.
You have to use relations. First, create a new column (not in your user's class) and the type is a relation, the relation is to the user class.
Lets say you want to add a new post. Use this:
ParseObject post = ...;
ParseUser user = ParseUser.getCurrentUser();
ParseRelation relation = user.getRelation("posts");
relation.add(yourString);
user.saveInBackground();
Code source
Tell me and I will edit this if you don't understand.
I guess you want something like for (to store some specific data for a particular parse user)
var note = new NoteOb();
note.set("text", text);
note.set("creator", currentUser);
note.setACL(new Parse.ACL(currentUser));
note.save(null, {
success:function(note) {
$("#newNoteText").val("");
getMyNotes();
}, error:function(note, error) {
//Should have something nice here...
}
});
And for: I want to fetch data from it
function getMyNotes() {
var query = new Parse.Query(NoteOb);
query.equalTo("creator", currentUser);
query.find({
success:function(notes) {
var s = "";
for(var i=0, len=notes.length; i<len; i++) {
s+= "<p>"+notes[i].get("text")+"</p>";
}
$("#currentNotes").html(s);
}
});
}
Check out this blog , it will give you a better understanding.