praw-dev / praw

PRAW, an acronym for "Python Reddit API Wrapper", is a python package that allows for simple access to Reddit's API.
http://praw.readthedocs.io/
BSD 2-Clause "Simplified" License
3.51k stars 460 forks source link

is_friend attribute doesn't work for suspended and banned redditors #1241

Closed dequeued0 closed 4 years ago

dequeued0 commented 4 years ago

Issue Description

Checking .is_friend doesn't work for suspended and banned (i.e., shadowbanned by Reddit) redditors. Requesting directly via /api/v1/me/friends does work although an exception is generated if the account is suspended/banned and not in friends.

Checking the entire friends list also works, but is much slower for a large friends list, so I am only calling this way in my workaround code if the previous two methods generate exceptions.

I'm attaching a test script to demonstrate the error. I'm using a similar is_friends() function in my production code. Note that it requires you to add some of the accounts to friends to run the test. I'll also include the output.

I'm not sure about the behavior for deleted redditors that are in friends because I cannot add these accounts to friends and I don't happen to have one present in friends. The behavior seems to be the same as shadowbanned acccounts when the deleted account isn't present in friends.

Finally, note that the errors do differ somewhat depending on whether the account is suspended/deleted vs. banned.

System Information

is_friend_testcase.txt

is_friend_testcase.py.txt

PythonCoderAS commented 4 years ago

It's much easier to embed the files as code:

Is Friend Testcase

testing account (normal account, not in friends): dequeued
2020-01-01T01:17:30 | INFO | test_user | getting user object succeeded for /u/dequeued
dequeued is friend? False

testing account (normal account, in friends): requeued
2020-01-01T01:17:31 | INFO | test_user | getting user object succeeded for /u/requeued
requeued is friend? True

testing account (shadowbanned account, in friends): ELINT_podagric
2020-01-01T01:17:31 | INFO | test_user | getting user object succeeded for /u/ELINT_podagric
2020-01-01T01:17:32 | ERROR | is_friend | exception checking is_friend for /u/ELINT_podagric: received 404 HTTP response
ELINT_podagric is friend? True

testing account (shadowbanned account, not in friends): cassieborowski
2020-01-01T01:17:32 | INFO | test_user | getting user object succeeded for /u/cassieborowski
2020-01-01T01:17:33 | ERROR | is_friend | exception checking is_friend for /u/cassieborowski: received 404 HTTP response
2020-01-01T01:17:33 | ERROR | is_friend | exception checking friends via API for /u/cassieborowski: received 400 HTTP response
cassieborowski is friend? False

testing account (suspended account, in friends): efdi
2020-01-01T01:17:36 | INFO | test_user | getting user object succeeded for /u/efdi
2020-01-01T01:17:36 | ERROR | is_friend | exception checking is_friend for /u/efdi: 'Redditor' object has no attribute 'is_friend'
efdi is friend? True

testing account (suspended account, not in friends): GeneralReposti_Bot
2020-01-01T01:17:37 | INFO | test_user | getting user object succeeded for /u/GeneralReposti_Bot
2020-01-01T01:17:37 | ERROR | is_friend | exception checking is_friend for /u/GeneralReposti_Bot: 'Redditor' object has no attribute 'is_friend'
2020-01-01T01:17:37 | ERROR | is_friend | exception checking friends via API for /u/GeneralReposti_Bot: received 400 HTTP response

Is Friend Testcase Python Code

#!/usr/bin/python

import praw
import requests
import time
from datetime import datetime
import logging

# setup
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s | %(levelname)s | %(funcName)s | %(message)s',
    datefmt='%Y-%m-%dT%H:%M:%S',
)
praw_config = {"user_agent": "linux:SOMETHING:v0.1 (by /u/SOMETHING)"}
r = praw.Reddit("SOMETHING", **praw_config)

def is_friend(user):
    try:
        if type(user) == str:
            return r.redditor(user).is_friend
        else:
            return user.is_friend
    except Exception as e:
        logging.error("exception checking is_friend for /u/{}: {}".format(user, e))
    try:
        return r.get("/api/v1/me/friends/" + str(user)) == str(user)
    except Exception as e:
        logging.error("exception checking friends via API for /u/{}: {}".format(user, e))
    # I don't think the next test ever returns True when the previous test could not return
    # True, but it seems cleaner to return False this way rather than based on an error.
    try:
        return user in r.user.friends()
    except Exception as e:
        logging.error("failed searching entire friends list for /u/{}: {}".format(user, e))
    return None

def test_user(info, name):
    print("testing account ({}): {}".format(info, name))
    try:
        user = r.redditor(name)
        logging.info("getting user object succeeded for /u/{}".format(user))
    except:
        logging.error("getting user object failed for /u/{}: {}".format(user, e))
        user = name
    is_friended = is_friend(user)
    print("{} is friend? {}".format(user, is_friended))
    print

if __name__ == "__main__":
    test_user("normal account, not in friends", "dequeued")
    test_user("normal account, in friends", "requeued")
    test_user("shadowbanned account, in friends", "ELINT_podagric")
    test_user("shadowbanned account, not in friends", "cassieborowski")
    test_user("suspended account, in friends", "efdi")
    test_user("suspended account, not in friends", "GeneralReposti_Bot")
PythonCoderAS commented 4 years ago

@dequeued0 what kind of exception is thrown?

dequeued0 commented 4 years ago

@PythonCoderAS The exceptions are shown in the test case output, the first file.

PythonCoderAS commented 4 years ago

I see. Can you also dump the vars of each Redditor instance, throughvars(<variable holding a Redditor>)?

justcool393 commented 4 years ago

Reddit returns 404s for shadowbanned users. this is pretty much expected and correct behavior. otherwise it wouldn't be a shadowban.

permanently suspended (whether by password reset requirement (sometimes) or otherwise) users only return the name and is_suspended attribute.

Reddit also returns HTTP 400 always for the other endpoint if you're not friends with a certain user, regardless of their shadowban or suspension status. this is also correct behavior.

justcool393 commented 4 years ago

a note on deleted accounts: they will show [deleted] for the username when checking a post or comment (as I believe the use case for this is), so it doesn't really matter anyway.

after 30 days they're removed from your friends list entirely and a note is placed on their account noting this, even though it is inaccessible for the first 30 days or so. Reddit will return 400 in this case for /api/v1/me/friends but with a different error message.

PythonCoderAS commented 4 years ago

I think the best way to resolve this issue is to add a note in documentation for suspended or shadowbanned accounts.

justcool393 commented 4 years ago

This is already documented, no?

This table describes attributes that typically belong to objects of this class. Since attributes are dynamically provided (see Determine Available Attributes of an Object), there is not a guarantee that these attributes will always be present, nor is this list comprehensive in any way.

A user that doesn't exist (as is the case with a shadowbanned user) is obvious to fail. It's easy to verify that it'd fail even on the website

PythonCoderAS commented 4 years ago

I was thinking about putting a note for suspended accounts, which is not documented. It also doesn't hurt to put a notice about shadowbanned, although suspended should definitely be mentioned.

PythonCoderAS commented 4 years ago

Furthermore, the calls for getting user object data always succeed because of lazy-loading. Shadowbanned accounts are the same as an incorrect username.

PythonCoderAS commented 4 years ago

Closing as this is a bug on Reddit's side, not PRAW's side. There is a new method to check for existing friendships, please use that.

justcool393 commented 4 years ago

this isn't a bug, it's intended behavior on reddit's side though...

PythonCoderAS commented 4 years ago

Well therefore it's closed as normal behavior.