Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update post not working with new piazza api #69

Open
andrewbartels1 opened this issue Apr 13, 2024 · 2 comments
Open

Update post not working with new piazza api #69

andrewbartels1 opened this issue Apr 13, 2024 · 2 comments

Comments

@andrewbartels1
Copy link

andrewbartels1 commented Apr 13, 2024

So far I've been able to get posts to create, but not update. Since a semi-recent update to their backend, I've had to monkeypatch the create_post piazza api seen below for working example:

# login to piazza with a specific class
# all class specifics should be given either
# in an .env file (development) or environment
# variables 
piazza_access = Piazza()

piazza_access.user_login(
    email=login_credentials.email,
    password=login_credentials.password.get_secret_value(),
)

# login to that specific course.
course_logged = piazza_access.network(network_id=login_credentials.course_id)

def create_post(
    self,
    post_type,
    post_folders,
    post_subject,
    post_content,
    visible_to,
    is_announcement=0,
    bypass_email=0,
    anonymous=False,
):
    """Create a post

    It seems like if the post has `<p>` tags, then it's treated as HTML,
    but is treated as text otherwise. You'll want to provide `content`
    accordingly.

    :type post_type: str
    :param post_type: 'note', 'question'
    :type post_folders: str
    :param post_folders: Folder to put post into
    :type post_subject: str
    :param post_subject: Subject string
    :type post_content: str
    :param post_content: Content string
    :type visible_to: str
    :param visible_to: Users this post is visible to
    :type is_announcement: bool
    :param is_announcement:
    :type bypass_email: bool
    :param bypass_email:
    :type anonymous: bool
    :param anonymous:
    :rtype: dict
    :returns: Dictionary with information about the created post.
    """
    params = {
        "anonymous": "yes" if anonymous else "no",
        "subject": post_subject,
        "content": post_content,
        "folders": post_folders,
        "type": post_type,
        "config": {
            "bypass_email": bypass_email,
            "is_announcement": is_announcement,
            "feed_groups": visible_to,
        },
    }

    return self._rpc.content_create(params)

course.create_post = types.MethodType(monkeypatch.create_post, course)

#### This works just fine!!!
course.create_post(
            **CreatePostModel(  # pydantic model with needed info
                post_type="note",
                post_folders=["other"],
                visible_to=posted_to,
                post_content=lecture_content_html,
                post_subject="Lecture Slides",
            ).model_dump()

However, no luck for updating posts, anything similar like feeding in cid (post id) and post content, and any permutation of the additional info needed for the create monkeypatch, gives the same response:

def update_post_monkeypatch(
    self,
    cid: int,
    post_type: str,
    post_folders: str,
    post_subject: str,
    post_content: str,
    visible_to: str,
    is_announcement: int = 0,
    bypass_email: int = 1,
    anonymous: bool = False,
):
    """Update a post, the **updated** way!

    It seems like if the post has `<p>` tags, then it's treated as HTML,
    but is treated as text otherwise. You'll want to provide `content`
    accordingly.

    :type post_type: str
    :param post_type: 'note', 'question'
    :type post_folders: str
    :param post_folders: Folder to put post into
    :type post_subject: str
    :param post_subject: Subject string
    :type post_content: str
    :param post_content: Content string
    :type visible_to: str
    :param visible_to: Users this post is visible to
    :type is_announcement: bool
    :param is_announcement:
    :type bypass_email: bool
    :param bypass_email:
    :type anonymous: bool
    :param anonymous:
    :rtype: dict
    :returns: Dictionary with information about the created post.
    """
    # Tried any permutation starting with simplest (cid and content) and iterated, but no luck
    params = {
        "cid": cid,
        "anonymous": "yes" if anonymous else "no",
        "subject": post_subject,
        "content": post_content,
        "folders": post_folders,
        "type": post_type,
        "config": {
            "bypass_email": bypass_email,
            "is_announcement": is_announcement,
            "feed_groups": visible_to,
        },
    }

    return self._rpc.content_update(params)

course.update_post = types.MethodType(update_post_monkeypatch, course)

course.update_post(
            **UpdatePostModel(
                cid=post_id,
                post_type="note",
                post_folders=["other"],
                # visible_to=posted_to,
                post_content=lecture_content_html,
                post_subject="Lecture Slides",
            ).model_dump()
        )

Something to do with this url made to piazza's backend: https://piazza.com/logic/api?method=content.update&aid=<aid-nonce_here>
Response: {\n "result": null,\n "error_codes": [],\n "error": "The post you are looking for cannot be found",\n "aid": "luxl22pj6p74en"\n}

I've tried every permutation or including or excluding things, and haven't been able to make any headway, so I'm a bit stumped! Any ideas? Happy to do some work on this if someone knows a fix.

@kwshi
Copy link
Contributor

kwshi commented Oct 21, 2024

just ran into this issue. seems like the current code for updating posts is outdated. for reference, here's an example payload submitted in the browser (found via network inspector tool in firefox):

{
  "method": "content.update",
  "params": {
    "cid": "m2jjb4r7c2i2t2",
    "type": "question",
    "subject": "Analysis, Fall 2022 #3",
    "content": "<md>babaenenen</md>",
    "anonymous": "no",
    "editor": "md",
    "revision": 2,
    "must_read": false,
    "config": {},
    "folders": [
      "analysis"
    ],
    "visibility": "all"
  }
}

meanwhile, here's the current update_post logic:

def update_post(self, post, content):
"""Update post content by cid
:type post: dict|str|int
:param post: Either the post dict returned by another API method, or
the `cid` field of that post.
:type subject: str
:param content: The content of the followup.
:rtype: dict
:returns: Dictionary with information about the updated post.
"""
try:
cid = post["id"]
except KeyError:
cid = post
except TypeError:
cid = post
params = {
"cid": cid,
# For updates, the content is put into the subject.
"subject": content,
}
return self._rpc.content_update(params)

for starters, it's not the case (at least, not anymore) that the content should be submitted in the subject field-- the browser payload puts it in the content field.

i haven't tried fixing it but it might be the case that we also need to submit a revision key, much like we do in create_instructor_answer...?

@andrewbartels1
Copy link
Author

andrewbartels1 commented Nov 30, 2024

Thanks @kwshi for the tip! Below is a relatively complete working example that ended up working as a hot fix. I'm happy to make this an MR since it's a very small code change.

from pydantic import BaseModel, Field
from piazza_api import Piazza
from typing import Literal, List 
import types

class UpdatePostModel(BaseModel):
    cid: int
    post_type: Literal["note", "question"] = Field(default="note")
    post_folders: List[str] | None = Field(default=None)
    post_subject: str | None = Field(default=None)
    post_content: str
    visible_to: str | None = Field(default=None)
    is_announcement: int = 0
    bypass_email: int = 1
    anonymous: bool = False



def update_post(
    self,
    cid: int,
    post_type: str,
    post_folders: str,
    post_subject: str,
    post_content: str,
    history_size: int,
    is_announcement: int = 0,
    bypass_email: int = 1,
    anonymous: bool = False,
):  # pragma: no cover
    """Update a post, the **updated** way!

    It seems like if the post has `<p>` tags, then it's treated as HTML,
    but is treated as text otherwise. You'll want to provide `content`
    accordingly.

    :type post_type: str
    :param post_type: 'note', 'question'
    :type post_folders: str
    :param post_folders: Folder to put post into
    :type post_subject: str
    :param post_subject: Subject string
    :type post_content: str
    :param post_content: Content string
    :type visible_to: str
    :param visible_to: Users this post is visible to
    :type is_announcement: bool
    :param is_announcement:
    :type bypass_email: bool
    :param bypass_email:
    :type anonymous: bool
    :param anonymous:
    :rtype: dict
    :returns: Dictionary with information about the created post.
    """

    params = {
        "cid": cid,
        "anonymous": "yes" if anonymous else "no",
        "subject": post_subject,
        "content": post_content,
        "folders": post_folders,
        "type": post_type,
        "revision": history_size + 1,  # autoincrement based on a post's history_size on return
        "config": {
            "bypass_email": bypass_email,
            "is_announcement": is_announcement,
        },
    }

    return self._rpc.content_update(params)

 piazza_access = Piazza()

#  this is where you plug in your email and password (had to ask Piazza engineers to
#  whitelist our email depending on the  college/course setup)
 piazza_access.user_login(
      email=login_credentials.email,
      password=login_credentials.password.get_secret_value(),
  )

# login to that specific course.
course_logged = piazza_access.network(network_id=login_credentials.course_id)

# add the monkeypatch from definition above
course.update_post = types.MethodType(update_post_monkeypatch, course)

lecture_content_to_look_for = "Post to Update"

# Assuming you're looking for a post with a specific string in the title/body.
 for post in course.iter_all_posts(
        limit=10, sleep=5
    ): 
        if lecture_content_to_look_for in post["history"][0]["content"]:
            
            # grab the post cid, subject header, folder and other details
            lecture_cid = post["id"]
            lecture_subject = post["history"][0]["subject"]
            lecture_folder = post["folders"]
            lecture_type = post["type"]
            history_size = post["history_size"]  # this is super important to get the `revision` to work.

            # Actually update the post with whatever html content you want.
            returned_content = course.update_post(
                        **UpdatePostModel(
                            cid=post_id,
                            post_type="note",
                            post_folders=["other"],
                            visible_to=posted_to,
                            post_content="<some updated post content here!>",
                            post_subject=lecture_subject + f": rev {history_size}",
                        ).model_dump()
                    )

print(returned_content)  # if no errors, you'll see the post with updated content returned here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants