Skip to content

Commit

Permalink
Add ability to edit posts with inline media.
Browse files Browse the repository at this point in the history
  • Loading branch information
LilSpazJoekp committed Dec 22, 2022
1 parent ffe9f71 commit 8d16dff
Show file tree
Hide file tree
Showing 21 changed files with 3,622 additions and 47 deletions.
17 changes: 17 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ Unreleased

- :meth:`.delete_mobile_banner` to delete mobile banners.
- :meth:`.upload_mobile_banner` to upload mobile banners.
- Experimental :meth:`~.Submission._edit_experimental` for adding new inline media or
editing a submission that has inline media.

.. danger::

This method is experimental. It is reliant on undocumented API endpoints and may
result in existing inline media not displaying correctly and/or creating a
malformed body. Use at your own risk. This method may be removed in the future
without warning.

This method is identical to :meth:`.Submission.edit` except for the following:

- The ability to add inline media to existing posts.
- Additional ``preserve_inline_media`` keyword argument to allow PRAW to attempt to
preserve the existing inline media when editing a post. This is an experimental fix
for an issue that occurs when editing a post with inline media would cause the media
to lose their inline appearance.

**Fixed**

Expand Down
1 change: 1 addition & 0 deletions docs/code_overview/models/submission.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ Submission

.. autoclass:: praw.models.Submission
:inherited-members:
:private-members: _edit_experimental
3 changes: 3 additions & 0 deletions praw/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ def _initialize_attributes(self):
self.warn_comment_sort = self._config_boolean(
self._fetch_default("warn_comment_sort", default=True)
)
self.warn_additional_fetch_params = self._config_boolean(
self._fetch_default("warn_additional_fetch_params", default=True)
)
self.kinds = {
x: self._fetch(f"{x}_kind")
for x in [
Expand Down
6 changes: 6 additions & 0 deletions praw/models/reddit/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from urllib.parse import urlparse

from ...endpoints import API_PATH
from ...exceptions import InvalidURL
from ..base import PRAWBase

Expand Down Expand Up @@ -82,6 +83,11 @@ def __ne__(self, other: Any) -> bool:
def _fetch(self): # pragma: no cover
self._fetched = True

def _fetch_data(self):
name, fields, params = self._fetch_info()
path = API_PATH[name].format(**fields)
return self._reddit.request(method="GET", params=params, path=path)

def _reset_attributes(self, *attributes):
for attribute in attributes:
if attribute in self.__dict__:
Expand Down
5 changes: 0 additions & 5 deletions praw/models/reddit/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,11 +360,6 @@ def __setattr__(self, attribute: str, value: Any):
def _fetch_info(self):
return "collection", {}, self._info_params

def _fetch_data(self):
name, fields, params = self._fetch_info()
path = API_PATH[name].format(**fields)
return self._reddit.request(method="GET", params=params, path=path)

def _fetch(self):
data = self._fetch_data()
try:
Expand Down
5 changes: 0 additions & 5 deletions praw/models/reddit/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,6 @@ def __setattr__(
def _fetch_info(self):
return "info", {}, {"id": self.fullname}

def _fetch_data(self):
name, fields, params = self._fetch_info()
path = API_PATH[name].format(**fields)
return self._reddit.request(method="GET", params=params, path=path)

def _fetch(self):
data = self._fetch_data()
data = data["data"]
Expand Down
5 changes: 0 additions & 5 deletions praw/models/reddit/live.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,11 +401,6 @@ def __init__(
def _fetch_info(self):
return "liveabout", {"id": self.id}, None

def _fetch_data(self):
name, fields, params = self._fetch_info()
path = API_PATH[name].format(**fields)
return self._reddit.request(method="GET", params=params, path=path)

def _fetch(self):
data = self._fetch_data()
data = data["data"]
Expand Down
1 change: 1 addition & 0 deletions praw/models/reddit/mixins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Package providing reddit class mixins."""
from json import dumps
from typing import TYPE_CHECKING, Optional
from warnings import warn

from ....const import API_PATH
from ....util import _deprecate_args
Expand Down
5 changes: 0 additions & 5 deletions praw/models/reddit/modmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,6 @@ def _build_conversation_list(self, other_conversations):
def _fetch_info(self):
return "modmail_conversation", {"id": self.id}, self._info_params

def _fetch_data(self):
name, fields, params = self._fetch_info()
path = API_PATH[name].format(**fields)
return self._reddit.request(method="GET", params=params, path=path)

def _fetch(self):
data = self._fetch_data()
other = self._reddit._objector.objectify(data)
Expand Down
5 changes: 0 additions & 5 deletions praw/models/reddit/multi.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,6 @@ def _fetch_info(self):
None,
)

def _fetch_data(self):
name, fields, params = self._fetch_info()
path = API_PATH[name].format(**fields)
return self._reddit.request(method="GET", params=params, path=path)

def _fetch(self):
data = self._fetch_data()
data = data["data"]
Expand Down
5 changes: 0 additions & 5 deletions praw/models/reddit/redditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,6 @@ def _fetch_info(self):
self.name = self._fetch_username(self._fullname)
return "user_about", {"user": self.name}, None

def _fetch_data(self):
name, fields, params = self._fetch_info()
path = API_PATH[name].format(**fields)
return self._reddit.request(method="GET", params=params, path=path)

def _fetch(self):
data = self._fetch_data()
data = data["data"]
Expand Down
158 changes: 156 additions & 2 deletions praw/models/reddit/submission.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Provide the Submission class."""
import re
from json import dumps
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
from urllib.parse import urljoin
from warnings import warn
Expand All @@ -20,6 +22,15 @@
if TYPE_CHECKING: # pragma: no cover
import praw

INLINE_MEDIA_PATTERN = re.compile(
r"\n\n!?(\[.*?])?\(?((https://((preview|i)\.redd\.it|reddit.com/link).*?)|(?!https)([a-zA-Z0-9]+( \".*?\")?))\)?"
)
MEDIA_TYPE_MAPPING = {
"Image": "img",
"RedditVideo": "video",
"AnimatedImage": "gif",
}


class SubmissionFlair:
"""Provide a set of functions pertaining to :class:`.Submission` flair."""
Expand Down Expand Up @@ -587,6 +598,7 @@ def __init__(

super().__init__(reddit, _data=_data)

self._additional_fetch_params = {}
self._comments_by_id = {}

def __setattr__(self, attribute: str, value: Any):
Expand All @@ -605,8 +617,8 @@ def __setattr__(self, attribute: str, value: Any):
and self._reddit.config.warn_comment_sort
):
warn(
"The comments for this submission have already been fetched, "
"so the updated comment_sort will not have any effect"
"The comments for this submission have already been fetched, so the"
" updated comment_sort will not have any effect."
)
super().__setattr__(attribute, value)

Expand All @@ -618,6 +630,84 @@ def _chunk(self, *, chunk_size, other_submissions):
for position in range(0, len(all_submissions), chunk_size):
yield ",".join(all_submissions[position : position + 50])

def _edit_experimental(
self,
body: str,
*,
preserve_inline_media=False,
inline_media: Optional[Dict[str, "praw.models.InlineMedia"]] = None,
) -> Union["praw.models.Submission"]:
"""Replace the body of the object with ``body``.
:param body: The Markdown formatted content for the updated object.
:param preserve_inline_media: Attempt to preserve inline media in ``body``.
.. danger::
This method is experimental. It is reliant on undocumented API endpoints
and may result in existing inline media not displaying correctly and/or
creating a malformed body. Use at your own risk. This method may be
removed in the future without warning.
:param inline_media: A dict of :class:`.InlineMedia` objects where the key is
the placeholder name in ``body``.
:returns: The current instance after updating its attributes.
Example usage:
.. code-block:: python
from praw.models import InlineGif, InlineImage, InlineVideo
submission = reddit.submission("5or86n")
gif = InlineGif(path="path/to/image.gif", caption="optional caption")
image = InlineImage(path="path/to/image.jpg", caption="optional caption")
video = InlineVideo(path="path/to/video.mp4", caption="optional caption")
body = "New body with a gif {gif1} an image {image1} and a video {video1} inline"
media = {"gif1": gif, "image1": image, "video1": video}
submission._edit_experimental(submission.selftext + body, inline_media=media)
"""
data = {
"thing_id": self.fullname,
"validate_on_submit": self._reddit.validate_on_submit,
}
is_richtext_json = False
if INLINE_MEDIA_PATTERN.search(body) and self.media_metadata:
is_richtext_json = True
if inline_media:
body = body.format(
**{
placeholder: self.subreddit._upload_inline_media(media)
for placeholder, media in inline_media.items()
}
)
is_richtext_json = True
if is_richtext_json:
richtext_json = self.subreddit._convert_to_fancypants(body)
if preserve_inline_media:
self._replace_richtext_links(richtext_json)
data["richtext_json"] = dumps(richtext_json)
else:
data["text"] = body
updated = self._reddit.post(API_PATH["edit"], data=data)
if not is_richtext_json:
updated = updated[0]
for attribute in [
"_fetched",
"_reddit",
"_submission",
"replies",
"subreddit",
]:
if attribute in updated.__dict__:
delattr(updated, attribute)
self.__dict__.update(updated.__dict__)
else:
self.__dict__.update(updated)
return self # type: ignore

def _fetch_info(self):
return (
"submission",
Expand All @@ -627,6 +717,7 @@ def _fetch_info(self):

def _fetch_data(self):
name, fields, params = self._fetch_info()
params.update(self._get_fetch_params())
path = API_PATH[name].format(**fields)
return self._reddit.request(method="GET", params=params, path=path)

Expand All @@ -646,6 +737,69 @@ def _fetch(self):

self._fetched = True

def _get_fetch_params(self):
"""Return the fetch parameters."""
params = self._additional_fetch_params.copy()
return params

def _replace_richtext_links(self, richtext_json: dict):
parsed_media_types = {
media_id: MEDIA_TYPE_MAPPING[value["e"]]
for media_id, value in self.media_metadata.items()
}

for index, element in enumerate(richtext_json["document"][:]):
element_items = element.get("c")
if isinstance(element_items, str):
assert element.get("e") in ["gif", "img", "video"], (
"Unexpected richtext JSON schema. Please file a bug report with"
" PRAW."
) # make sure this is an inline element
continue # pragma: no cover
for item in element.get("c"):
if item.get("e") == "link":
ids = set(parsed_media_types)
# remove extra bits from the url
url = item["u"].split("https://")[1].split("?")[0]
# the id is in the url somewhere, so we split by '/' and '.'
matched_id = ids.intersection(re.split(r"[./]", url))
if matched_id:
matched_id = matched_id.pop()
correct_element = {
"e": parsed_media_types[matched_id],
"id": matched_id,
}
if item.get("t") != item.get("u"): # add caption if it exists
correct_element["c"] = item["t"]
richtext_json["document"][index] = correct_element

def add_fetch_param(self, key, value):
"""Add a parameter to be used for the next fetch.
:param key: The key of the fetch parameter.
:param value: The value of the fetch parameter.
For example, to fetch a submission with the ``rtjson`` attribute populated:
.. code-block:: python
submission = reddit.submission("mcqjl8")
submission.add_fetch_param("rtj", "all")
print(submission.rtjson)
"""
if (
hasattr(self, "_fetched")
and self._fetched
and hasattr(self, "_reddit")
and self._reddit.config.warn_additional_fetch_params
):
warn(
f"This {self.__class__.__name__.lower()} has already been fetched, so"
" adding additional fetch parameters will not have any effect."
)
self._additional_fetch_params[key] = value

def mark_visited(self):
"""Mark submission as visited.
Expand Down
5 changes: 0 additions & 5 deletions praw/models/reddit/subreddit.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,11 +574,6 @@ def _convert_to_fancypants(self, markdown_text: str) -> dict:
def _fetch_info(self):
return "subreddit_about", {"subreddit": self}, None

def _fetch_data(self) -> dict:
name, fields, params = self._fetch_info()
path = API_PATH[name].format(**fields)
return self._reddit.request(method="GET", params=params, path=path)

def _fetch(self):
data = self._fetch_data()
data = data["data"]
Expand Down
5 changes: 0 additions & 5 deletions praw/models/reddit/wikipage.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,11 +244,6 @@ def _fetch_info(self):
{"v": self._revision} if self._revision else None,
)

def _fetch_data(self):
name, fields, params = self._fetch_info()
path = API_PATH[name].format(**fields)
return self._reddit.request(method="GET", params=params, path=path)

def _fetch(self):
data = self._fetch_data()
data = data["data"]
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def _sleep(*args):
raise Exception("Call to sleep")


time._sleep = time.sleep
time.sleep = _sleep


Expand Down

0 comments on commit 8d16dff

Please sign in to comment.