Skip to content

Commit

Permalink
Merge pull request #255 from praw-dev/edit_inline_media
Browse files Browse the repository at this point in the history
Add ability to edit posts with inline media
  • Loading branch information
LilSpazJoekp committed Oct 14, 2023
2 parents acc25c1 + 9ea0e23 commit a5d427c
Show file tree
Hide file tree
Showing 19 changed files with 3,488 additions and 51 deletions.
17 changes: 17 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@ Unreleased
templates.
- :meth:`~.SubredditRedditorFlairTemplates.reorder` to reorder a subreddit's redditor
flair templates.
- 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 Async 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
3 changes: 3 additions & 0 deletions asyncpraw/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,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 asyncpraw/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 AsyncPRAWBase

Expand Down Expand Up @@ -86,6 +87,11 @@ def __str__(self) -> str:
async def _fetch(self): # pragma: no cover
self._fetched = True

async def _fetch_data(self):
name, fields, params = self._fetch_info()
path = API_PATH[name].format(**fields)
return await 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 asyncpraw/models/reddit/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,11 +570,6 @@ async def _fetch(self):
self.__dict__.update(other.__dict__)
self._fetched = True

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

def _fetch_info(self):
return "collection", {}, self._info_params

Expand Down
5 changes: 0 additions & 5 deletions asyncpraw/models/reddit/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,6 @@ async def _fetch(self):
self.__dict__.update(other.__dict__)
self._fetched = True

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

def _fetch_info(self):
return "info", {}, {"id": self.fullname}

Expand Down
5 changes: 0 additions & 5 deletions asyncpraw/models/reddit/live.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,11 +392,6 @@ async def _fetch(self):
self.__dict__.update(other.__dict__)
self._fetched = True

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

def _fetch_info(self):
return "liveabout", {"id": self.id}, None

Expand Down
5 changes: 0 additions & 5 deletions asyncpraw/models/reddit/modmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,6 @@ async def _fetch(self):
self.__dict__.update(other.__dict__)
self._fetched = True

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

def _fetch_info(self):
return "modmail_conversation", {"id": self.id}, self._info_params

Expand Down
9 changes: 2 additions & 7 deletions asyncpraw/models/reddit/multi.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,14 @@ async def _ensure_author_fetched(self):
await self._author._fetch()

async def _fetch(self):
await self._ensure_author_fetched()
data = await self._fetch_data()
data = data["data"]
other = type(self)(self._reddit, _data=data)
self.__dict__.update(other.__dict__)
self._fetched = True

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

async def _fetch_info(self):
await self._ensure_author_fetched()
def _fetch_info(self):
return (
"multireddit_api",
{"multi": self.name, "user": self._author.name},
Expand Down
11 changes: 3 additions & 8 deletions asyncpraw/models/reddit/redditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,20 +178,15 @@ def __setattr__(self, name: str, value: Any):
super().__setattr__(name, value)

async def _fetch(self):
if hasattr(self, "_fullname"):
self.name = await self._fetch_username(self._fullname)
data = await self._fetch_data()
data = data["data"]
other = type(self)(self._reddit, _data=data)
self.__dict__.update(other.__dict__)
self._fetched = True

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

async def _fetch_info(self):
if hasattr(self, "_fullname"):
self.name = await self._fetch_username(self._fullname)
def _fetch_info(self):
return "user_about", {"user": self.name}, None

async def _fetch_username(self, fullname):
Expand Down
156 changes: 154 additions & 2 deletions asyncpraw/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 asyncpraw

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 @@ -573,6 +584,7 @@ def __init__(

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

self._additional_fetch_params = {}
self._comments_by_id = {}
self.comments = CommentForest(self)
"""Provide an instance of :class:`.CommentForest`.
Expand Down Expand Up @@ -625,8 +637,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 @@ -638,6 +650,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])

async def _edit_experimental(
self,
body: str,
*,
preserve_inline_media=False,
inline_media: Optional[Dict[str, "asyncpraw.models.InlineMedia"]] = None,
) -> Union["asyncpraw.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 asyncpraw.models import InlineGif, InlineImage, InlineVideo
submission = await 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}
await 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: await self.subreddit._upload_inline_media(media)
for placeholder, media in inline_media.items()
}
)
is_richtext_json = True
if is_richtext_json:
richtext_json = await 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 = await 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

async def _fetch(self):
data = await self._fetch_data()
submission_listing, comment_listing = data
Expand All @@ -655,6 +745,7 @@ async def _fetch(self):

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

Expand All @@ -665,6 +756,67 @@ def _fetch_info(self):
{"limit": self.comment_limit, "sort": self.comment_sort},
)

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"
" Async 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: str, value: str):
"""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 = await reddit.submission("mcqjl8", fetch=False)
submission.add_fetch_param("rtj", "all")
await submission.load()
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."
f" Initialize the {self.__class__.__name__} instance with the parameter"
" `fetch=False` to use additional fetch parameters."
)
self._additional_fetch_params[key] = value

@_deprecate_args(
"subreddit",
"title",
Expand Down
5 changes: 0 additions & 5 deletions asyncpraw/models/reddit/subreddit.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,11 +923,6 @@ async def _fetch(self):
self.__dict__.update(other.__dict__)
self._fetched = True

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

def _fetch_info(self):
return "subreddit_about", {"subreddit": self}, None

Expand Down
5 changes: 0 additions & 5 deletions asyncpraw/models/reddit/wikipage.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,11 +275,6 @@ async def _fetch(self):
self.__dict__.update(data)
self._fetched = True

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

def _fetch_info(self):
return (
"wiki_page",
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:: asyncpraw.models.Submission
:inherited-members:
:private-members: _edit_experimental
Loading

0 comments on commit a5d427c

Please sign in to comment.