Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/live-account-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
type: choice
options:
- media
- comment
- upload
- direct
- timeline
Expand Down Expand Up @@ -44,6 +45,10 @@ jobs:
if: github.event.inputs.test_target == 'media' || github.event.inputs.test_target == 'all'
run: pytest -sv tests/live/test_media.py::ClientMediaTestCase tests/live/test_comments.py::ClientCommentRepliesLiveTestCase

- name: Run comment write test
if: github.event.inputs.test_target == 'comment' || github.event.inputs.test_target == 'all'
run: pytest -sv tests/live/test_comments.py::ClientCommentExtendTestCase::test_media_comment

- name: Run upload live tests
if: github.event.inputs.test_target == 'upload' || github.event.inputs.test_target == 'all'
run: |
Expand Down
1 change: 1 addition & 0 deletions docs/usage-guide/comment.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,4 @@ Notes:
* `media_comment_replies()` fetches `inline_child_comments` for a parent comment and paginates with the returned child cursor.
* `comment_pin()` / `comment_unpin()` only work on media owned by the authenticated account.
* Reply creation is supported through `replied_to_comment_id`; reply retrieval is supported through `media_comment_replies()`.
* Comment creation is a write action and can still trigger Instagram spam or trust checks. Reuse saved sessions, keep volume low on new accounts, and stop when Instagram returns feedback/challenge responses.
12 changes: 10 additions & 2 deletions instagrapi/mixins/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,16 @@ def media_comment(self, media_id: str, text: str, replied_to_comment_id: Optiona
media_id = self.media_id(media_id)
data = {
"delivery_class": "organic",
"feed_position": "0",
"container_module": "self_comments_v2_feed_contextual_self_profile", # "comments_v2",
"feed_position": str(random.randint(0, 6)),
"container_module": "feed_timeline",
"media_id": media_id,
"_uid": str(self.user_id),
"tap_source": "button",
"is_2m_enabled": "false",
"is_carousel_bumped_post": "false",
"is_from_swipe": "false",
"floating_context_items": "[]",
"media_pct_watched": "0",
"user_breadcrumb": self.gen_user_breadcrumb(len(text)),
"idempotence_token": self.generate_uuid(),
"comment_text": text,
Expand Down
2 changes: 1 addition & 1 deletion tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def build_test_accounts_url(self, count=None):
parts = urlsplit(TEST_ACCOUNTS_URL)
query = dict(parse_qsl(parts.query, keep_blank_values=True))
if count is None:
query.setdefault("count", "5")
query["count"] = "5"
else:
query["count"] = str(count)
return urlunsplit(
Expand Down
37 changes: 36 additions & 1 deletion tests/live/test_comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,46 @@ def test_media_comment_replies_live(self):


class ClientCommentExtendTestCase(_helpers.ClientPrivateTestCase):
def cleanup_comment(self, media_id, comment_pk):
try:
self.cl.comment_bulk_delete(media_id, [comment_pk])
except Exception as exc:
print(f"Comment live cleanup comment_bulk_delete failed: {exc.__class__.__name__} {exc}")

def cleanup_media(self, media_id):
try:
self.cl.media_delete(media_id)
except Exception as exc:
print(f"Comment live cleanup media_delete failed: {exc.__class__.__name__} {exc}")

def assertCommentAccessible(self, media_id, comment_pk, text, attempts=5, delay=3):
last_error = None
for attempt in range(attempts):
if attempt:
time.sleep(delay)
try:
comments = self.cl.media_comments_v1(media_id, amount=20)
except Exception as exc:
last_error = exc
continue
for item in comments:
if str(item.pk) == str(comment_pk) and item.text == text:
return item
self.fail(f"Comment {comment_pk} was not readable after {attempts} attempts: {last_error}")

def test_media_comment(self):
text = "Test text [%s]" % datetime.now().strftime("%s")
now = datetime.now(tz=UTC())
comment = self.cl.media_comment_v1(2276404890775267248, text)
caption_text = "Comment live fixture [%s]" % datetime.now().strftime("%s")
path = self.copy_media_fixture("examples/kanada.jpg")
media = self.cl.photo_upload(path, caption_text)
self.addCleanup(self.cleanup_media, media.id)
self.assertUploadedMediaAccessible(media, media_type=1, caption_text=caption_text)
media_id = media.id
comment = self.cl.media_comment(media_id, text)
self.addCleanup(self.cleanup_comment, media_id, comment.pk)
self.assertIsInstance(comment, Comment)
self.assertCommentAccessible(media_id, comment.pk, text)
comment = comment.dict()
for key, val in {
"text": text,
Expand Down
38 changes: 38 additions & 0 deletions tests/regression/test_comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@


class CommentRepliesRegressionTestCase(unittest.TestCase):
def _build_logged_in_client(self):
client = Client()
client.authorization_data = {"ds_user_id": "1"}
client.android_device_id = "android-device"
return client

def _reply_payload(self, pk, text="reply", replied_to_comment_id="100"):
return {
"pk": str(pk),
Expand All @@ -15,6 +21,38 @@ def _reply_payload(self, pk, text="reply", replied_to_comment_id="100"):
"comment_like_count": 0,
}

def test_media_comment_posts_current_action_context(self):
client = self._build_logged_in_client()
expected_comment = self._reply_payload("101", text="hello")
with mock.patch.object(
client,
"private_request",
return_value={"comment": expected_comment},
) as private_request:
comment = client.media_comment("123_456", "hello", replied_to_comment_id=100)

self.assertIsInstance(comment, Comment)
endpoint, data = private_request.call_args.args
self.assertEqual(endpoint, "media/123_456/comment/")
self.assertEqual(data["media_id"], "123_456")
self.assertEqual(data["_uid"], "1")
self.assertEqual(data["_uuid"], client.uuid)
self.assertEqual(data["device_id"], "android-device")
self.assertEqual(data["radio_type"], "wifi-none")
self.assertEqual(data["delivery_class"], "organic")
self.assertEqual(data["tap_source"], "button")
self.assertEqual(data["is_2m_enabled"], "false")
self.assertEqual(data["is_carousel_bumped_post"], "false")
self.assertEqual(data["is_from_swipe"], "false")
self.assertEqual(data["floating_context_items"], "[]")
self.assertEqual(data["media_pct_watched"], "0")
self.assertEqual(data["container_module"], "feed_timeline")
self.assertIn(data["feed_position"], {str(i) for i in range(7)})
self.assertEqual(data["comment_text"], "hello")
self.assertEqual(data["replied_to_comment_id"], 100)
self.assertIn("user_breadcrumb", data)
self.assertIn("idempotence_token", data)

def test_media_comment_replies_fetches_inline_child_comments(self):
client = Client()
with mock.patch.object(
Expand Down
26 changes: 26 additions & 0 deletions tests/regression/test_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from tests import helpers as helper_module
from tests.helpers import *


class LiveAccountHelperRegressionTestCase(unittest.TestCase):
def test_build_test_accounts_url_overrides_existing_default_count(self):
case = object.__new__(helper_module.ClientPrivateTestCase)
with mock.patch.object(
helper_module,
"TEST_ACCOUNTS_URL",
"https://accounts.example.test/take?pool=live&count=1",
):
url = case.build_test_accounts_url()

self.assertEqual(url, "https://accounts.example.test/take?pool=live&count=5")

def test_build_test_accounts_url_uses_requested_count(self):
case = object.__new__(helper_module.ClientPrivateTestCase)
with mock.patch.object(
helper_module,
"TEST_ACCOUNTS_URL",
"https://accounts.example.test/take?pool=live&count=1",
):
url = case.build_test_accounts_url(count=8)

self.assertEqual(url, "https://accounts.example.test/take?pool=live&count=8")