Implementing remove tweet and like/upvote functionality in Firebase - javascript

Continuing from this thread, on HN: https://news.ycombinator.com/item?id=5462769
Reading through the firefeed rules file answered a lot of questions for me, except for these two:
Editing an existing tweet isn't allowed (".write": "!data.exists()"). How can you make it not editable, but deletable by the author?
How would you securely handle liking/unliking or upvoting/downvoting? write if authenticated, validate for the increase/decrease by one, if the user hasn't modified this before? How would that work? Would there have to be a child list of people who edited this? I'm just really curious about this specific use case as it seems pretty common in many apps, yet seems to me, would be really complicated to implement in firebase?

1. Not editable but deletable by the author
".write": "!data.exists() || (!newData.exists() && data.child('author') === auth.id)"
2. Liking/Upvoting
On the client, use a transaction which allows you to increment the value safely:
ref.transaction(function(currentValue) {
return (currentValue||0)+1;
}, function(error) {
if( error ) /* failed too many times */
else /* it worked */
});
Security is also straightforward:
".validate": "newData.isNumber() && newData.val() === data.val()+1"
2.5 Ensuring Unique Votes
I'm not sure what this means; the records can't be edited and presumably if they could, only the author would be able to do so; so I don't really understand "modified" in this context: "if the user hasn't modified this before? How would that work?"
To ensure votes are unique, you just store them by user ID. The user can remove their vote by deleting the record.
I'd recommend storing these in a separate path than the sparks and still maintaining a simple increment (the messages that are getting voted up/down) as you don't want to have to retrieve the entire list of voters each time you fetch the spark.
The security rules would look like so:
"votes": {
"$spark_id": {
"$vote": {
".read": "$vote === auth.id",
".write": "$vote === auth.id",
// to allow downvoting in addition to up or delete, just add -1 here
".validate": "newData.val() === 1 || newData.val() === null"
}
}
}
And now add a check to the validate rule for the increment:
".validate": "!root.child('votes').child($spark_id).child(auth.id).exists() && newData.isNumber() && newData.val() === data.val()+1"

Now that Firebase Functions has been released (in beta) to the general public, it seems to be a good option: https://firebase.googleblog.com/2017/03/introducing-cloud-functions-for-firebase.html
The idea is to have each user be allowed to add their name, by key, to an "upvoters" collection for the tweet. They can create or delete their entry --
but there can only be one, since it's by-key and the security rule only allows control of their one key.
When finding of the "upvote count" is to take place, the client could get the full list of upvoters and tally the number. But instead, for performance's sake, we create a Firebase Function which is triggered whenever an upvote entry is added or removed.
All it does then is increase or decrease an "upvote count" property on the tweet. This is the same as before, except that we make a security rule that only lets the cloud-hosted Function modify this field. Thus, the modification is always trusted and safe, and removes the need for the client to receive the list of upvoters just to get the upvote-count.

Related

Hiding messages from a discord user through javascript - Soft blocking

Blocking a user is a good feature of discord, but often enough I find myself wanting to block someone temporarily: not see their messages, but only for a certain period of time and without them knowing that I did so. Theoretically such a thing should be possible to do. I found a userscript that you are supposed to paste into the discord console once you start discord that serves the purpose of hiding messages from blocked users completely. Using the same principle, if instead of searching for blocked messages I could search for messages from a specific user, and then hide them, I could soft-block them for the duration of my current session. So the idea of this is to search all messages in a channel, check if a message was posted by a certain user that I want blocked, and if so, hide it.
To do so I have to figure out how to tie messages to their authors. There appears to be a different class for each new message that a user sends. But when a user sends multiple messages in a row, the classes are different. The first message contains the link to the user's avatar, which in turn contains the user's unique discord ID that he could be identified with. But his follow-up messages don't contain his avatar. And I can't find anything else that I could use to identify a user with except for that.
In the image above the classes that have groupStart contain the avatar links, whereas the classes without it are the follow-up messages from the same user that do not. Like so:
What kind of a loop would be able to include both the messages with some specified avatar, and the follow up messages without it?
As I see it, here's how you'll have to do it:
first identify the groupStart message, and then keep looking at the next messages until you reach a new groupStart message.
Luckily JavaScript keeps elements in the order that they're displayed when you use document.getElementsByClassName so you should just be able to set a variable to true when you hit the target, and then back to false when you hit a new person. This is my code:
function block(userid) {
return setInterval(() => {
let deleteMessages = false;
[...document.getElementsByClassName('message-2qnXI6')].forEach(i => {
if(i) {
if(i.className.match('groupStart-23k01U')) {
if(([...i.children].find(i => i.className.match('contents-2mQqc9')).children[0].src || '/1/').split('/').find(i => i.match(/^\d+$/)) === userid) {
i.style.display = 'none'
deleteMessages = true
} else deleteMessages = false
}
else if(deleteMessages) i.style.display = 'none'
}
})
deleteMessages = false
}, 300)
}
The function block sets an interval for 300 seconds to:
First, get all the messages: [...document.getElementsByClassName('message-2qnXI6')]
Then for each message check to see if it's a groupStart element: if(i.className.match('groupStart-23k01U'))
If it is, it'll check to see if it's a message from the blocked user:
if(([...i.children].find(i => i.className.match('contents-2mQqc9')).children[0].src || '/1/').split('/').find(i => i.match(/^\d+$/)) === userid)
If it is, it'll delete that message and set deleteMessages to true:
i.style.display = 'none'
deleteMessages = true
If it isn't, it'll set deleteMessages to false, so that the next messages don't get deleted.
Going back, if the message isn't a groupStart message, it'll check to see if deleteMessages is true (which would be the first message in the group was from a blocked user, and therefore this message is from a blocked user). If it is, it'll delete it.
Everything should be reset with a simple ctrl+r
Just copy and paste this (the minified version of my code):
function block(e){return setInterval(()=>{let t=!1;[...document.getElementsByClassName("message-2qnXI6")].forEach(n=>{n&&(n.className.match("groupStart-23k01U")?([...n.children].find(e=>e.className.match("contents-2mQqc9")).children[0].src||"/1/").split("/").find(e=>e.match(/^\d+$/))===e?(n.style.display="none",t=!0):t=!1:t&&(n.style.display="none"))}),t=!1},300)}
into the console, and then to block someone type block('<userid>') (make sure the user id is a string and not a number), and it should start working. Also, when you do, it should return a number, just type clearInterval(number) to stop it from blocking new messages.
If you're wondering about this line:
if(([...i.children].find(i => i.className.match('contents-2mQqc9')).children[0].src || '/1/').split('/').find(i => i.match(/^\d+$/)) === userid)
There's a few hackey things I did here:
I start by getting an array of all the messages children, then I find the child with the userID in it (i.className.match('contents-2mQqc9')). If I don't I just use a placeholder to avoid errors.
Then I split via /s because the user id is one of the paths. Then I find which part of the path is the user id by checking to see if it's only numbers (i.match(/^\d+$/)). Finally once I've done all that I check to see if it's the same as the userid.
Here's a quick example:
https://cdn.discordapp.com/avatars/847295719724247388/c9af8fff1110025e6aae862492b4aa29.webp?size=256 (this isn't a real link) => ['https:', '', 'cdn.discordapp.com', 'avatars', '847295719724247388', 'c9af8fff1110025e6aae862492b4aa29.webp?size=256'] => '847295719724247388' is made completely of numbers, so: '847295719724247388'
Unfortunately, if you use this on too many messages by the same person in a row discord will really glitch out, so you'll have to watch for that (note this was testing it on 1000+ messages in a row by the same person). It seems like problems start to happen at around 100, but don't become an issue until it's up to 200 - 300, and even then it'll sometimes work if you wait for a second.
Hopefully if this doesn't work for you you'll at least have an idea of what to do.

What's wrong with this logic? Node JS

I built a mini cms app with Node JS. I allow users to edit their own profile and admins to edit all profiles. I have a weird problem with the logic - If I use this syntax, I get an error (401) when an admin tries to edit other user's profile:
if (!loggedUser.isAdmin || foundUser.id !== loggedUser.id) {
res.status(401).json();
} else {
// Save Updated User
foundUser.username = req.body.username;
foundUser.birthday = req.body.birthday;
foundUser.personalWeb = req.body.personalWeb;
foundUser.location = req.body.location;
foundUser.save().then(() => res.status(200).json(200));
}
But if I use this syntax, the permissions work just fine:
if (loggedUser.isAdmin || foundUser.id === loggedUser.id) {
// Save Updated User
foundUser.username = req.body.username;
foundUser.profileImg = req.body.profileImg;
foundUser.personalWeb = req.body.personalWeb;
foundUser.location = req.body.location;
foundUser.save().then(() => res.status(200).json(200));
} else {
res.status(401).json();
}
Can someone please explain what's the differnce between the two conditions?
!loggedUser.isAdmin || foundUser.id !== loggedUser.id and loggedUser.isAdmin || foundUser.id === loggedUser.id are not boolean inverses of each other.
The first is saying "if the user is not an admin or the found user's id does not match the logged in user's id." In the case of an admin you would expect their id to not match the found user's id.
I think that your second code block is easier to read and you should keep it, but if you wanted to do the negative condition first it would be:
!loggedUser.isAdmin && foundUser.id !== loggedUser.id
That is: "if the logged in user is not an admin and the found user's id does not match the logged in user's id."
This is also the boolean inverse:
!(loggedUser.isAdmin || foundUser.id === loggedUser.id)
// expands to
!loggedUser.isAdmin && foundUser.id !== loggedUser.id
It was surprisingly difficult for me to find good documentation or descriptions of boolean negation, but this article explains the concepts well I think: http://www.math.toronto.edu/preparing-for-calculus/3_logic/we_3_negation.html
Although you can simplify boolean expressions, I think it's best to write them in a way that makes the most sense to read back for you and your development team, so I suggest you use the first block since it's easy to read. Failing that, leave a comment about what the expression is trying to accomplish.
It's because your foundUser.id !== loggedUser.id is evaluating to true when editing any user that's not you.
To add to that, any non admin user will get a 401 due to the first condition evaluating to true.
With || as long as one condition is met, the body will execute and then it's done. It won't move on to the else body if only one condition is false. Both need to be false
Personally I would just use your second example. It's more readable.

Using permissions without repeating code

I have a function that requires the user to be a owner or an admin. Some commands are only available to owners and not admins but owners can also run admin commands.
I find that I'm just repeating the same code for both parts. Is there a way to consolidate this so I don't violate the rule of repeating myself?
Here's a general scheme of what I have:
request(checkPermissionRequest, function (error, response, body) {
// Determine who is allowed to run the script
switch (body[0].permission) {
// Owner
case 0:
if (isOwner(user)) {
// Script code
}
break;
// Admin
case 1:
if (isOwner(user) || isAdmin(user)) {
// Script code
}
break;
}
});
UPDATE
I can see how my above explanation has caused some confusion so I'll try to attempt to clear it up. There are multiple available scripts and some are for owner and some for admins (which can also be run by owner). These permissions are saved in a database and I get it by making a request. This is why I can't just set things for owners and things for admins and owners because I don't know what permission the script is until I get the result from the database. These permissions are set by the owners and I cannot keep track of them. I modified the code to try to show more clarity.
The way that you've designed your scripts, your permissions levels are represented by integers. Let's use that to our advantage.
Let's define a helper function like this:
function getUserPermissions(user) {
if(isOwner(user) return 0;
if(isAdmin(user) return 1;
return 2; // or some arbitrarily high value- heck, even Number.MAX_SAFE_INTEGER
}
Then, instead of needing a switch and a bunch of if statements, you can simply do something like this:
request(checkPermissionRequest, function (error, response, body) {
// Determine who is allowed to run the script
if(getUserPermissions(user) < parseInt(body[0].permission) {
// do the script
}
});
If following this pattern, you could also consider switching the 0 and 1, if you're able, so that a higher numeric value indicates a higher level of permissions -- if only for the sake of readability, as it would allow a -1 value to indicate someone with no permissions. In that case, you would want to change the if condition to check for getUserPermissions(user) > parseInt(body[0].permission rather than <.
On the other hand, if you stay with the slightly less readable < option, that allows for adding other levels of permission with less access than admin as 3 and so on, without having to increment the existing Owner and Admin levels.
if (isOwner(user) || (isAdmin(user) && script.permission == 1)) {
// Script code
}
You could nest the if, and just exclude 'admin' from the first chunk of data.
if (isOwner(user) || isAdmin(user)) {
//some permissions for both
if(isOwner(user)) {
//more permissions just for owner
}
}

Firebase convert generated ID to numeric ID

This is an old question but no answers found. Old answers' source links are dead, old functions are deprecated.
I've look at these topics (1, 2) amongst others, and also current Firebase guide.
function writeUserData(name, win, loss) {
firebase.database().ref('users/').push({
name: name,
wins: win,
losses: loss
});
}
I need to pull or somehow convert the unique ID from each push to numeric ID: 1, 2
UPDATE:
Here's what I'm trying to do:
2 users can login at the same time from 2 different browsers.
1st user hits button 'log-in' or 'start' will be written as users/1/{user's attributes} on firebase.
if there's an existing user
(somehow I need to check that on the user-end), the next user hits
'start' will be written as users/2/{user's attributes} on firebase. Else (no existing user), the user will be written as user1. This is where I think I need push instead of set. I can use set to add more users from 1 browser, but if i open the app from another browser/chrome incognito, the app will just reset the existing users.
if a user refreshes or closes the browser, that user will be removed from firebase so another user can log-in.
I've tried different methods (loop, creating new var...) to achieve this.
Rather than .push, try to use .set or .update. In that way you can set your own generated key. But as already suggested, this may not be the best approach.

Is localStorage thread safe?

I'm curious about the possibility of damaging localStorage entry by overwriting it in two browser tabs simultaneously. Should I create a mutex for local storage?
I was already thinking of such pseudo-class:
LocalStorageMan.prototype.v = LocalStorageMan.prototype.value = function(name, val) {
//Set inner value
this.data[name] = val;
//Delay any changes if the local storage is being changed
if(localStorage[this.name+"__mutex"]==1) {
setTimeout(function() {this.v(name, val);}, 1);
return null; //Very good point #Lightness Races in Orbit
}
//Lock the mutext to prevent overwriting
localStorage[this.name+"__mutex"] = 1;
//Save serialized data
localStorage[this.name] = this.serializeData;
//Allow usage from another tabs
localStorage[this.name+"__mutex"] = 0;
}
The function above implies local storage manager that is managing one specific key of the local storage - localStorage["test"] for example. I want to use this for greasomonkey userscripts where avoiding conlicts is a priority.
Yes, it is thread safe. However, your code isn't atomic and that's your problem there. I'll get to thread safety of localStorage but first, how to fix your problem.
Both tabs can pass the if check together and write to the item overwriting each other. The correct way to handle this problem is using StorageEvents.
These let you notify other windows when a key has changed in localStorage, effectively solving the problem for you in a built in message passing safe way. Here is a nice read about them. Let's give an example:
// tab 1
localStorage.setItem("Foo","Bar");
// tab 2
window.addEventListener("storage",function(e){
alert("StorageChanged!"); // this will run when the localStorage is changed
});
Now, what I promised about thread safety :)
As I like - let's observe this from two angles - from the specification and using the implementation.
The specification
Let's show it's thread safe by specification.
If we check the specification of Web Storage we can see that it specifically notes:
Because of the use of the storage mutex, multiple browsing contexts will be able to access the local storage areas simultaneously in such a manner that scripts cannot detect any concurrent script execution.
Thus, the length attribute of a Storage object, and the value of the various properties of that object, cannot change while a script is executing, other than in a way that is predictable by the script itself.
It even elaborates further:
Whenever the properties of a localStorage attribute's Storage object are to be examined, returned, set, or deleted, whether as part of a direct property access, when checking for the presence of a property, during property enumeration, when determining the number of properties present, or as part of the execution of any of the methods or attributes defined on the Storage interface, the user agent must first obtain the storage mutex.
Emphasis mine. It also notes that some implementors don't like this as a note.
In practice
Let's show it's thread safe in implementation.
Choosing a random browser, I chose WebKit (because I didn't know where that code is located there before). If we check at WebKit's implementation of Storage we can see that it has its fare share of mutexes.
Let's take it from the start. When you call setItem or assign, this happens:
void Storage::setItem(const String& key, const String& value, ExceptionCode& ec)
{
if (!m_storageArea->canAccessStorage(m_frame)) {
ec = SECURITY_ERR;
return;
}
if (isDisabledByPrivateBrowsing()) {
ec = QUOTA_EXCEEDED_ERR;
return;
}
bool quotaException = false;
m_storageArea->setItem(m_frame, key, value, quotaException);
if (quotaException)
ec = QUOTA_EXCEEDED_ERR;
}
Next, this happens in StorageArea:
void StorageAreaImpl::setItem(Frame* sourceFrame, const String& key, const String& value, bool& quotaException)
{
ASSERT(!m_isShutdown);
ASSERT(!value.isNull());
blockUntilImportComplete();
String oldValue;
RefPtr<StorageMap> newMap = m_storageMap->setItem(key, value, oldValue, quotaException);
if (newMap)
m_storageMap = newMap.release();
if (quotaException)
return;
if (oldValue == value)
return;
if (m_storageAreaSync)
m_storageAreaSync->scheduleItemForSync(key, value);
dispatchStorageEvent(key, oldValue, value, sourceFrame);
}
Note that blockUntilImportComplete here. Let's look at that:
void StorageAreaSync::blockUntilImportComplete()
{
ASSERT(isMainThread());
// Fast path. We set m_storageArea to 0 only after m_importComplete being true.
if (!m_storageArea)
return;
MutexLocker locker(m_importLock);
while (!m_importComplete)
m_importCondition.wait(m_importLock);
m_storageArea = 0;
}
They also went as far as add a nice note:
// FIXME: In the future, we should allow use of StorageAreas while it's importing (when safe to do so).
// Blocking everything until the import is complete is by far the simplest and safest thing to do, but
// there is certainly room for safe optimization: Key/length will never be able to make use of such an
// optimization (since the order of iteration can change as items are being added). Get can return any
// item currently in the map. Get/remove can work whether or not it's in the map, but we'll need a list
// of items the import should not overwrite. Clear can also work, but it'll need to kill the import
// job first.
Explaining this works, but it can be more efficient.
No, it's not. Mutex was removed from the spec, and this warning was added instead:
The localStorage getter provides access to shared state. This
specification does not define the interaction with other browsing
contexts in a multiprocess user agent, and authors are encouraged to
assume that there is no locking mechanism. A site could, for instance,
try to read the value of a key, increment its value, then write it
back out, using the new value as a unique identifier for the session;
if the site does this twice in two different browser windows at the
same time, it might end up using the same "unique" identifier for both
sessions, with potentially disastrous effects.
See HTML Spec: 12 Web storage

Categories