diff --git a/README.md b/README.md
index de65332..194404f 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-![LapsePy](./icon.png)
+![LapsePy](https://github.com/quintindunn/lapsepy/blob/main/icon.png?raw=true)
![](https://img.shields.io/pypi/l/lapsepy)
diff --git a/examples/add_reaction/add_reaction_01.py b/examples/add_reaction/add_reaction_01.py
new file mode 100644
index 0000000..08065e9
--- /dev/null
+++ b/examples/add_reaction/add_reaction_01.py
@@ -0,0 +1,10 @@
+import os
+from lapsepy.lapse import Lapse
+
+if __name__ == '__main__':
+ lapse = Lapse(refresh_token=os.getenv("REFRESH_TOKEN"))
+
+ msg_id = input("MSG ID: ")
+ reaction = input("Reaction: ")
+
+ lapse.add_reaction(msg_id=msg_id, reaction=reaction)
diff --git a/examples/add_reaction/add_reaction_02.py b/examples/add_reaction/add_reaction_02.py
new file mode 100644
index 0000000..d5ff5e2
--- /dev/null
+++ b/examples/add_reaction/add_reaction_02.py
@@ -0,0 +1,12 @@
+import os
+from lapsepy.lapse import Lapse
+
+if __name__ == '__main__':
+ lapse = Lapse(refresh_token=os.getenv("REFRESH_TOKEN"))
+
+ count = int(input("How many: "))
+ msg_id = input("MSG ID: ")
+ reaction = input("Reaction: ")
+
+ for _ in range(count):
+ lapse.add_reaction(msg_id=msg_id, reaction=reaction)
diff --git a/examples/create_status_update/create_status_update_01.py b/examples/create_status_update/create_status_update_01.py
new file mode 100644
index 0000000..a98accc
--- /dev/null
+++ b/examples/create_status_update/create_status_update_01.py
@@ -0,0 +1,9 @@
+import os
+
+from lapsepy.lapse import Lapse
+
+if __name__ == '__main__':
+ lapse = Lapse(os.getenv("REFRESH_TOKEN"))
+
+ msg = input("Message: ")
+ lapse.create_status_update(text=msg)
diff --git a/examples/delete_comment/delete_comment.py b/examples/delete_comment/delete_comment.py
new file mode 100644
index 0000000..7055536
--- /dev/null
+++ b/examples/delete_comment/delete_comment.py
@@ -0,0 +1,10 @@
+import os
+from lapsepy.lapse import Lapse
+
+if __name__ == '__main__':
+ lapse = Lapse(refresh_token=os.getenv("REFRESH_TOKEN"))
+
+ msg_id = input("Message ID: ")
+ comment = input("Comment: ")
+
+ lapse.delete_comment(msg_id=msg_id, comment_id=comment)
diff --git a/examples/get_friends_feed/get_friends_feed_1.py b/examples/get_friends_feed/get_friends_feed_1.py
index 3639ae4..667c5fa 100644
--- a/examples/get_friends_feed/get_friends_feed_1.py
+++ b/examples/get_friends_feed/get_friends_feed_1.py
@@ -1,5 +1,7 @@
import os
+import requests.exceptions
+
from lapsepy.lapse import Lapse
if __name__ == '__main__':
@@ -11,10 +13,33 @@
if not os.path.isdir("./out"):
os.mkdir("./out")
- for profile in friends_feed:
- profile.load_profile_picture(quality=100, height=None)
- profile.profile_picture.save(f"./out/{profile.username}.jpg")
- for snap in profile.media:
- snap.load_snap(quality=100, fl_keep_iptc=True)
- save_path = f"./out/{snap.filtered_id.replace('/', '_')}.jpg"
- snap.filtered.save(save_path)
+ for friend_node in friends_feed:
+ profile = friend_node.profile
+
+ # Get profile picture
+ try:
+ profile.load_profile_picture(quality=100, height=None)
+ profile.profile_picture.save(f"./out/{profile.username}.jpg")
+ except requests.exceptions.HTTPError:
+ print(f"Failed getting profile picture for {profile.username}")
+
+ # Get all images from collections
+ for entry in friend_node.entries:
+ entry.load_snap(quality=100, fl_keep_iptc=True)
+ save_path = f"./out/{entry.filtered_id.replace('/', '_')}.jpg"
+ entry.filtered.save(save_path)
+
+ # Get profile music if user has profile music.
+ if profile.profile_music is not None:
+ profile.profile_music.load()
+ profile_music = profile.profile_music
+
+ # Save artwork if exists
+ if profile_music.artwork:
+ save_path = f"./out/{profile.username}_music.png"
+ profile_music.artwork.save(save_path)
+
+ # Save song
+ save_path = f"./out/{profile.username}_music.mp3"
+ with open(save_path, 'wb') as f:
+ f.write(profile_music.song)
diff --git a/examples/remove_reaction/remove_reaction_01.py b/examples/remove_reaction/remove_reaction_01.py
new file mode 100644
index 0000000..7287f8a
--- /dev/null
+++ b/examples/remove_reaction/remove_reaction_01.py
@@ -0,0 +1,10 @@
+import os
+from lapsepy.lapse import Lapse
+
+if __name__ == '__main__':
+ lapse = Lapse(refresh_token=os.getenv("REFRESH_TOKEN"))
+
+ msg_id = input("MSG ID: ")
+ reaction = input("Reaction: ")
+
+ lapse.remove_reaction(msg_id=msg_id, reaction=reaction)
diff --git a/examples/remove_status_update/remove_status_update_01.py b/examples/remove_status_update/remove_status_update_01.py
new file mode 100644
index 0000000..cd379e6
--- /dev/null
+++ b/examples/remove_status_update/remove_status_update_01.py
@@ -0,0 +1,9 @@
+import os
+
+from lapsepy import Lapse
+
+if __name__ == '__main__':
+ lapse = Lapse(os.getenv("REFRESH_TOKEN"))
+
+ msg_to_remove = input("MSG ID: ")
+ lapse.remove_status_update(msg_to_remove)
diff --git a/examples/send_comment/send_comment.py b/examples/send_comment/send_comment.py
new file mode 100644
index 0000000..26f9209
--- /dev/null
+++ b/examples/send_comment/send_comment.py
@@ -0,0 +1,10 @@
+import os
+from lapsepy.lapse import Lapse
+
+if __name__ == '__main__':
+ lapse = Lapse(refresh_token=os.getenv("REFRESH_TOKEN"))
+
+ msg_id = input("Message ID: ")
+ comment = input("Comment: ")
+
+ lapse.send_comment(msg_id=msg_id, text=comment)
diff --git a/examples/send_kudos/send_kudos_1.py b/examples/send_kudos/send_kudos_1.py
new file mode 100644
index 0000000..578320d
--- /dev/null
+++ b/examples/send_kudos/send_kudos_1.py
@@ -0,0 +1,14 @@
+import os
+from PIL import Image
+from lapsepy.lapse import Lapse
+
+if __name__ == '__main__':
+ lapse = Lapse(refresh_token=os.getenv("REFRESH_TOKEN"))
+
+ # Develop in 15 seconds
+ friend_id = input("Friend UUID: ")
+
+ # Get friend object
+ friend = lapse.get_profile_by_id(friend_id)
+
+ lapse.send_kudos(user=friend)
diff --git a/lapsepy/__init__.py b/lapsepy/__init__.py
index 51bd283..8af1e1d 100644
--- a/lapsepy/__init__.py
+++ b/lapsepy/__init__.py
@@ -3,7 +3,7 @@
Date: 10/22/23
"""
-__version__ = '0.1.1'
+__version__ = '0.2.1'
from .journal import Journal
from .auth.refresher import refresh
diff --git a/lapsepy/journal/factory/friends_factory.py b/lapsepy/journal/factory/friends_factory.py
index 6120cff..5d88831 100644
--- a/lapsepy/journal/factory/friends_factory.py
+++ b/lapsepy/journal/factory/friends_factory.py
@@ -11,7 +11,7 @@ class FriendsFeedItemsGQL(BaseGQL):
Gets items from friends feed.
"""
- def __init__(self, start_cursor: str | None = None):
+ def __init__(self, last: int = 10):
super().__init__(
operation_name="FriendsFeedItemsGraphQLQuery",
query="query FriendsFeedItemsGraphQLQuery($first: Int, $after: String, $last: Int, $before: String) { "
@@ -31,60 +31,72 @@ def __init__(self, start_cursor: str | None = None):
"FriendsFeedItemProfileCompletedV1 { ...FriendsFeedItemProfileCompletedDetails } ... on "
"FriendsFeedItemProfilePhotoUpdatedV1 { ...FriendsFeedItemProfilePhotoUpdatedDetails } ... on "
"FriendsFeedItemSelectsUpdatedV1 { ...FriendsFeedItemSelectsUpdatedDetails } ... on "
+ "FriendsFeedItemStatusUpdatedV1 { ...FriendsFeedItemStatusUpdatedDetails } ... on "
"FriendsFeedItemTaggedMediaSharedV2 { ...FriendsFeedItemTaggedMediaSharedDetails } } comments("
- "first: 3) { __typename edges { __typename node { __typename ...MediaCommentDetails } } totalCount "
- "} reactions { __typename ...MediaReactionDetails } user { __typename ...ProfileDetails } timestamp "
- "{ __typename isoString } }\nfragment FriendsFeedItemAlbumUpdatedDetails on "
- "FriendsFeedItemAlbumUpdatedV1 { __typename id title mediaIds totalCount }\nfragment "
- "FriendsFeedItemBioUpdatedDetails on FriendsFeedItemBioUpdatedV1 { __typename bio }\nfragment "
- "FriendsFeedItemEmojisUpdatedDetails on FriendsFeedItemEmojisUpdatedV1 { __typename emojis "
- "}\nfragment FriendsFeedItemFriendSuggestionsDetails on FriendsFeedItemFriendSuggestionsV1 { "
- "__typename suggestions { __typename ...FriendSuggestionDetails } }\nfragment "
- "FriendSuggestionDetails on FriendSuggestion { __typename profile { __typename ...ProfileDetails "
- "invitedBy(last: 3) { __typename edges { __typename node { __typename ...ProfileDetails } } } } "
- "reason }\nfragment ProfileDetails on Profile { __typename id displayName profilePhotoName username "
- "bio emojis { __typename emojis } friendStatus isBlocked blockedMe hashedPhoneNumber joinedAt { "
- "__typename isoString } kudos { __typename emoji totalCount lastSentAt { __typename isoString } } "
- "selectsVideo { __typename ...RecapVideoDetails } music { __typename ...ProfileMusicDetails } tags "
- "{ __typename type text } }\nfragment RecapVideoDetails on RecapVideo { __typename id videoFilename "
- "totalDuration interval media { __typename imageFilename } }\nfragment ProfileMusicDetails on "
- "ProfileMusic { __typename artist artworkUrl duration songTitle songUrl }\nfragment "
+ "first: 3) { __typename edges { __typename cursor node { __typename ...MediaCommentDetails } } "
+ "totalCount } reactions { __typename ...MediaReactionDetails } user { __typename "
+ "...CoreProfileFragment } timestamp { __typename isoString } }\nfragment "
+ "FriendsFeedItemAlbumUpdatedDetails on FriendsFeedItemAlbumUpdatedV1 { __typename id title mediaIds "
+ "totalCount }\nfragment FriendsFeedItemBioUpdatedDetails on FriendsFeedItemBioUpdatedV1 { "
+ "__typename bio }\nfragment FriendsFeedItemEmojisUpdatedDetails on FriendsFeedItemEmojisUpdatedV1 { "
+ "__typename emojis }\nfragment FriendsFeedItemFriendSuggestionsDetails on "
+ "FriendsFeedItemFriendSuggestionsV1 { __typename suggestions { __typename "
+ "...FriendSuggestionDetails } }\nfragment FriendSuggestionDetails on FriendSuggestion { __typename "
+ "profile { __typename ...ViewProfileSummaryFragment invitedBy(last: 3) { __typename edges { "
+ "__typename cursor node { __typename ...CoreProfileFragment } } } } reason }\nfragment "
+ "ViewProfileSummaryFragment on Profile { __typename ...CoreProfileFragment "
+ "...ViewProfileSelectsFragment ...ViewProfileMusicFragment bio emojis { __typename emojis } kudos { "
+ "__typename emoji totalCount lastSentAt { __typename isoString } } tags { __typename type text } "
+ "}\nfragment CoreProfileFragment on Profile { __typename id displayName profilePhotoName username "
+ "friendStatus isBlocked blockedMe hashedPhoneNumber joinedAt { __typename isoString } }\nfragment "
+ "ViewProfileSelectsFragment on Profile { __typename selectsVideo { __typename "
+ "...CoreRecapVideoFragment } }\nfragment CoreRecapVideoFragment on RecapVideo { __typename id "
+ "videoFilename totalDuration interval }\nfragment ViewProfileMusicFragment on Profile { __typename "
+ "music { __typename ...ProfileMusicDetails } }\nfragment ProfileMusicDetails on ProfileMusic { "
+ "__typename artist artworkUrl duration songTitle songUrl }\nfragment "
"FriendsFeedItemFriendRequestsDetails on FriendsFeedItemFriendRequestsV1 { __typename requests { "
"__typename ...FriendRequestDetails } }\nfragment FriendRequestDetails on FriendRequest { "
- "__typename profile { __typename ...ProfileDetails invitedBy(last: 3) { __typename edges { "
- "__typename cursor node { __typename ...ProfileDetails } } } } }\nfragment "
+ "__typename profile { __typename ...ViewProfileSummaryFragment invitedBy(last: 3) { __typename "
+ "edges { __typename cursor node { __typename ...CoreProfileFragment } } } } }\nfragment "
"FriendsFeedItemKudosUpdatedDetails on FriendsFeedItemKudosUpdatedV1 { __typename empty }\nfragment "
"FriendsFeedItemMediaFeaturedDetails on FriendsFeedItemMediaFeaturedV1 { __typename media { "
- "__typename ...MediaDetails } }\nfragment MediaDetails on Media { __typename id takenAt { "
- "__typename isoString } takenBy { __typename ...ProfileDetails } commentsCount developsAt { "
- "__typename isoString } destroyedAt { __typename isoString } deletedAt { __typename isoString } "
- "partyId timeCapsuleId timezone content { __typename filtered original } submittedToTeam reactions "
- "{ __typename ...MediaReactionDetails } comments(first: 3) { __typename edges { __typename node { "
- "__typename ...MediaCommentDetails } } } tags(first: 3) { __typename edges { __typename node { "
- "__typename ...MediaTagDetails } } totalCount } featured faceFrames { __typename xPos yPos width "
- "height } }\nfragment MediaReactionDetails on MediaReaction { __typename emoji hasReacted count "
- "}\nfragment MediaCommentDetails on MediaComment { __typename id author { __typename "
- "...ProfileDetails } media { __typename id } createdAt { __typename isoString } deletedAt { "
- "__typename isoString } text isLiked likeCount }\nfragment MediaTagDetails on MediaTag { __typename "
- "frame { __typename position { __typename xPos yPos } size { __typename width height } } taggedAt { "
- "__typename isoString } taggedBy { __typename id } ... on MediaContactTag { hashedPhoneNumber } ... "
- "on MediaProfileTag { profile { __typename id displayName username profilePhotoName friendStatus } "
- "shared } }\nfragment FriendsFeedItemMediaSharedDetails on FriendsFeedItemMediaSharedV1 { "
- "__typename entries { __typename ...FriendsFeedItemMediaSharedEntryDetails } }\nfragment "
- "FriendsFeedItemMediaSharedEntryDetails on FriendsFeedItemMediaSharedEntryV1 { __typename id seen "
- "media { __typename ...MediaDetails } }\nfragment FriendsFeedItemMusicUpdatedDetails on "
+ "__typename ...CoreMediaFragment ...MediaWithReactionsFragment } }\nfragment CoreMediaFragment on "
+ "Media { __typename id takenAt { __typename isoString } takenBy { __typename ...CoreProfileFragment "
+ "} deletedAt { __typename isoString } }\nfragment MediaWithReactionsFragment on Media { __typename "
+ "reactions { __typename ...MediaReactionDetails } }\nfragment MediaReactionDetails on MediaReaction "
+ "{ __typename emoji hasReacted count }\nfragment FriendsFeedItemMediaSharedDetails on "
+ "FriendsFeedItemMediaSharedV1 { __typename entries { __typename "
+ "...FriendsFeedItemMediaSharedEntryDetails } }\nfragment FriendsFeedItemMediaSharedEntryDetails on "
+ "FriendsFeedItemMediaSharedEntryV1 { __typename id seen media { __typename "
+ "...FullMediaPreviewFragment } }\nfragment FullMediaPreviewFragment on Media { __typename "
+ "...CoreMediaFragment ...MediaWithMetadataFragment ...MediaWithCommentsPreviewFragment "
+ "...MediaWithReactionsFragment ...MediaWithTagsPreviewFragment }\nfragment "
+ "MediaWithMetadataFragment on Media { __typename developsAt { __typename isoString } timezone "
+ "content { __typename filtered original } submittedToTeam featured faceFrames { __typename xPos "
+ "yPos width height } }\nfragment MediaWithCommentsPreviewFragment on Media { __typename "
+ "commentsCount comments(first: 3) { __typename edges { __typename cursor node { __typename "
+ "...MediaCommentDetails } } } }\nfragment MediaCommentDetails on MediaComment { __typename id "
+ "author { __typename ...CoreProfileFragment } media { __typename id } createdAt { __typename "
+ "isoString } deletedAt { __typename isoString } text isLiked likeCount }\nfragment "
+ "MediaWithTagsPreviewFragment on Media { __typename tags(first: 3) { __typename edges { __typename "
+ "cursor node { __typename ...MediaTagDetails } } totalCount } }\nfragment MediaTagDetails on "
+ "MediaTag { __typename frame { __typename position { __typename xPos yPos } size { __typename width "
+ "height } } taggedAt { __typename isoString } taggedBy { __typename id } ... on MediaContactTag { "
+ "hashedPhoneNumber } ... on MediaProfileTag { profile { __typename id displayName username "
+ "profilePhotoName friendStatus } shared } }\nfragment FriendsFeedItemMusicUpdatedDetails on "
"FriendsFeedItemMusicUpdatedV1 { __typename artist artworkUrl songTitle }\nfragment "
"FriendsFeedItemProfileCompletedDetails on FriendsFeedItemProfileCompletedV1 { __typename photoName "
- "selectsVideo { __typename ...RecapVideoDetails } }\nfragment "
+ "selectsVideo { __typename ...CoreRecapVideoFragment } }\nfragment "
"FriendsFeedItemProfilePhotoUpdatedDetails on FriendsFeedItemProfilePhotoUpdatedV1 { __typename "
"profilePhotoName }\nfragment FriendsFeedItemSelectsUpdatedDetails on "
"FriendsFeedItemSelectsUpdatedV1 { __typename imageFilename selectsVideo { __typename "
- "...RecapVideoDetails } }\nfragment FriendsFeedItemTaggedMediaSharedDetails on "
- "FriendsFeedItemTaggedMediaSharedV2 { __typename sharedMedia { __typename "
- "...FriendsFeedItemMediaSharedDetails } }")
+ "...CoreRecapVideoFragment } }\nfragment FriendsFeedItemStatusUpdatedDetails on "
+ "FriendsFeedItemStatusUpdatedV1 { __typename body { __typename text } }\nfragment "
+ "FriendsFeedItemTaggedMediaSharedDetails on FriendsFeedItemTaggedMediaSharedV2 { __typename "
+ "sharedMedia { __typename ...FriendsFeedItemMediaSharedDetails } }")
- self.last = 10
- self.before = start_cursor
+ self.last = last
+ self.before = None
self.variables = {}
@@ -156,3 +168,20 @@ def _render_variables(self):
"mutualLimit": self.mutual_limit,
"popularLimit": self.popular_limit
}
+
+
+class SendKudosGQL(BaseGQL):
+ def __init__(self, user_id: str):
+ super().__init__("SendKudosGraphQLMutation", "mutation SendKudosGraphQLMutation($input: SendKudosInput!) "
+ "{ sendKudos(input: $input) { __typename success } }")
+
+ self.user_id = user_id
+
+ self.variables = {}
+
+ self._render_variables()
+
+ def _render_variables(self):
+ self.variables['input'] = {
+ "id": self.user_id
+ }
diff --git a/lapsepy/journal/factory/media_factory.py b/lapsepy/journal/factory/media_factory.py
index 20a8f1a..2ee7bbd 100644
--- a/lapsepy/journal/factory/media_factory.py
+++ b/lapsepy/journal/factory/media_factory.py
@@ -125,3 +125,123 @@ def _render_variables(self):
self.variables["filename"] = f"{self.file_uuid}/filtered_0.heic"
else:
self.variables["filename"] = f"instant/{self.file_uuid}.heic"
+
+
+class StatusUpdateGQL(BaseGQL):
+ def __init__(self, text: str, msg_id: str):
+ super().__init__("CreateStatusUpdateGraphQLMutation",
+ "mutation CreateStatusUpdateGraphQLMutation($input: CreateStatusUpdateInput!) "
+ "{ createStatusUpdate(input: $input) { __typename success } }")
+ self.text = text
+ self.msg_id = msg_id
+
+ self.variables = {}
+
+ self._render_variables()
+
+ def _render_variables(self):
+ self.variables['input'] = {
+ "body": {
+ "text": self.text
+ },
+ "id": self.msg_id
+ }
+
+
+class RemoveFriendsFeedItemGQL(BaseGQL):
+ def __init__(self, msg_id: str, iso_string: str):
+ super().__init__("RemoveFriendsFeedItem",
+ "mutation RemoveFriendsFeedItem($input: RemoveFriendsFeedItemInput!) "
+ "{ removeFriendsFeedItem(input: $input) { __typename success } }")
+ self.msg_id = msg_id
+ self.iso_string = iso_string
+
+ self.variables = {}
+
+ self._render_variables()
+
+ def _render_variables(self):
+ self.variables['input'] = {
+ "id": self.msg_id,
+ "removedAt": {
+ "isoString": self.iso_string
+ }
+ }
+
+
+class AddReactionGQL(BaseGQL):
+ def __init__(self, msg_id: str, reaction: str):
+ super().__init__("AddReactionGraphQLMutation",
+ "mutation AddReactionGraphQLMutation($input: AddMediaReactionInput!) "
+ "{ addMediaReaction(input: $input) { __typename success } }")
+ self.msg_id = msg_id
+ self.reaction = reaction
+
+ self.variables = {}
+
+ self._render_variables()
+
+ def _render_variables(self):
+ self.variables['input'] = {
+ "id": self.msg_id,
+ "reaction": self.reaction
+ }
+
+
+class RemoveReactionGQL(BaseGQL):
+ def __init__(self, msg_id: str, reaction: str):
+ super().__init__("RemoveReactionGraphQLMutation",
+ "mutation RemoveReactionGraphQLMutation($input: RemoveMediaReactionInput!) "
+ "{ removeMediaReaction(input: $input) { __typename success } }")
+ self.msg_id = msg_id
+ self.reaction = reaction
+
+ self.variables = {}
+
+ self._render_variables()
+
+ def _render_variables(self):
+ self.variables['input'] = {
+ "id": self.msg_id,
+ "reaction": self.reaction
+ }
+
+
+class SendCommentGQL(BaseGQL):
+ def __init__(self, comment_id: str, msg_id: str, text: str):
+ super().__init__("SendCommentGraphQLMutation",
+ "mutation SendCommentGraphQLMutation($input: SendMediaCommentInput!) "
+ "{ sendMediaComment(input: $input) { __typename success } }")
+ self.comment_id = comment_id
+ self.msg_id = msg_id
+ self.text = text
+
+ self.variables = {}
+
+ self._render_variables()
+
+ def _render_variables(self):
+ self.variables['input'] = {
+ "id": self.comment_id,
+ "mediaId": self.msg_id,
+ "text": self.text
+ }
+
+
+class DeleteCommentGQL(BaseGQL):
+ def __init__(self, comment_id: str, msg_id: str):
+ super().__init__("DeleteCommentGraphQLMutation",
+ "mutation DeleteCommentGraphQLMutation($input: DeleteMediaCommentInput!) "
+ "{ deleteMediaComment(input: $input) { __typename success } }")
+ self.comment_id = comment_id
+ self.msg_id = msg_id
+
+ self.variables = {}
+
+ self._render_variables()
+
+ def _render_variables(self):
+ self.variables['input'] = {
+ "id": self.comment_id,
+ "mediaId": self.msg_id,
+ }
diff --git a/lapsepy/journal/journal.py b/lapsepy/journal/journal.py
index f9668d7..8aae69d 100644
--- a/lapsepy/journal/journal.py
+++ b/lapsepy/journal/journal.py
@@ -4,6 +4,7 @@
"""
import io
+import uuid
from .common.exceptions import sync_journal_exception_router, SyncJournalException
@@ -13,12 +14,13 @@
import requests
-from .factory.friends_factory import FriendsFeedItemsGQL, ProfileDetailsGQL
-from .factory.media_factory import ImageUploadURLGQL, CreateMediaGQL, SendInstantsGQL
+from .factory.friends_factory import FriendsFeedItemsGQL, ProfileDetailsGQL, SendKudosGQL
+from .factory.media_factory import ImageUploadURLGQL, CreateMediaGQL, SendInstantsGQL, StatusUpdateGQL, \
+ RemoveFriendsFeedItemGQL, AddReactionGQL, RemoveReactionGQL, SendCommentGQL, DeleteCommentGQL
from lapsepy.journal.factory.profile_factory import SaveBioGQL, SaveDisplayNameGQL, SaveUsernameGQL, SaveEmojisGQL, \
SaveDOBGQL
-from .structures import Profile, Snap
+from .structures import Snap, Profile, ProfileMusic, FriendsFeed, FriendNode
import logging
@@ -164,7 +166,6 @@ def upload_instant(self, im: Image.Image, user_id: str, file_uuid: str | None =
if file_uuid is None:
# UUID in testing always started with "01HDCWT" with a total length of 26 chars.
file_uuid = "01HDCWT" + str(uuid4()).upper().replace("-", "")[:19]
- print(file_uuid)
if im_id is None:
# UUID in testing always started with "01HDCWT" with a total length of 26 chars.
@@ -178,57 +179,82 @@ def upload_instant(self, im: Image.Image, user_id: str, file_uuid: str | None =
time_limit=time_limit).to_dict()
self._sync_journal_call(query)
- def get_friends_feed(self, count: int = 10) -> list[Profile]:
+ def create_status_update(self, text: str, msg_id: str | None):
+ """
+ Creates a status update on your Journal
+ :param text: Msg of the text to send
+ :param msg_id: Leave None if you don't know what you're doing. FORMAT: STATUS_UPDATE:<(str(uuid.uuid4))>
+ :return:
+ """
+ if msg_id is None:
+ msg_id = f"STATUS_UPDATE:{uuid.uuid4()}"
+ query = StatusUpdateGQL(text=text, msg_id=msg_id).to_dict()
+ response = self._sync_journal_call(query)
+
+ if not response.get("data", {}).get("createStatusUpdate", {}).get("success"):
+ raise SyncJournalException("Error create new status.")
+
+ def remove_status_update(self, msg_id: str, removed_at: datetime | None):
+ """
+ Removes a status update
+ :param msg_id: ID of the status update
+ :param removed_at: datetime object of when it was removed
+ :return:
+ """
+ if removed_at is None:
+ removed_at = datetime.now()
+ removed_at = format_iso_time(removed_at)
+
+ query = RemoveFriendsFeedItemGQL(msg_id=msg_id, iso_string=removed_at).to_dict()
+ response = self._sync_journal_call(query)
+
+ if not response.get("data", {}).get("removeFriendsFeedItem", {}).get("success"):
+ raise SyncJournalException("Failed removing status.")
+
+ def send_kudos(self, user_id: str):
+ """
+ Sends kudos (vibes) to a given user
+ :param user_id: id of the user to send kudos to.
+ :return:
+ """
+ query = SendKudosGQL(user_id=user_id).to_dict()
+ response = self._sync_journal_call(query)
+
+ if not response.get("data", {}).get("sendKudos", {}).get("success"):
+ raise SyncJournalException("Error sending kudos, could you already have reached your daily limit?")
+
+ def get_friends_feed(self, count: int = 10) -> FriendsFeed:
"""
Gets your friend upload feed.
:param count: How many collection to grab.
:return: A list of profiles
"""
- cursor = None
+ # Get all the user's friends in the range.
+ query = FriendsFeedItemsGQL(last=count).to_dict()
+ response = self._sync_journal_call(query)
- profiles = {}
- entry_ids = []
+ nodes: list[dict] = [i['node'] for i in response['data']['friendsFeedItems']['edges']]
- # If it started to repeat itself.
- maxed = False
- for _ in range(1, count, 10):
- logger.debug(f"Getting friends feed starting from cursor: {cursor or 'INITIAL'}")
- query = FriendsFeedItemsGQL(cursor).to_dict()
- response = self._sync_journal_call(query)
+ friend_nodes = []
- # Where to query the new data from
- cursor = response['data']['friendsFeedItems']['pageInfo']['endCursor']
- if cursor is None:
- logger.debug("Reached max cursor depth.")
- break
+ for node in nodes:
+ profile_data = node.get("user")
+ profile = Profile.from_dict(profile_data)
- # Trim useless data from response
- feed_data = [i['node'] for i in response['data']['friendsFeedItems']['edges']]
+ timestamp = node.get("timestamp", {}).get("isoString")
- # Create Profile objects which hold the media data in Profile.media
- for node in feed_data:
- username = node.get('user').get('username')
- if username in profiles.keys():
- profile = profiles[username]
- else:
- profile = Profile.from_dict(node.get("user"))
- profiles[username] = profile
+ entries = node.get("content").get("entries")
- for entry in node['content']['entries']:
- eid = entry['id']
- if eid in entry_ids:
- logger.warn("Found duplicate of media, must've reached the end already...")
- maxed = True
- break
- entry_ids.append(eid)
- snap = Snap.from_dict(entry)
- profile.media.append(snap)
+ node_entry_objs = []
+ for entry in entries:
+ snap = Snap.from_dict(entry)
+ node_entry_objs.append(snap)
+ profile.media.append(snap)
- if maxed:
- break
+ friend_nodes.append(FriendNode(profile=profile, iso_string=timestamp, entries=node_entry_objs))
- return list(profiles.values())
+ return FriendsFeed(friend_nodes)
def get_profile_by_id(self, user_id: str, album_limit: int = 6, friends_limit: int = 10) -> Profile:
"""
@@ -249,6 +275,18 @@ def get_profile_by_id(self, user_id: str, album_limit: int = 6, friends_limit: i
pd = response.get("data", {}).get("profile", {})
def generate_profile_object(profile_data: dict) -> Profile:
+ music = profile_data.get("music")
+ if music is not None:
+ profile_music = ProfileMusic(
+ artist=music.get("artist"),
+ artwork_url=music.get("artworkUrl"),
+ duration=music.get("duration"),
+ song_title=music.get("songTitle"),
+ song_url=music.get("songUrl")
+ )
+ else:
+ profile_music = None
+
return Profile(
bio=profile_data.get('bio'),
blocked_me=profile_data.get('blockedMe'),
@@ -261,6 +299,7 @@ def generate_profile_object(profile_data: dict) -> Profile:
tags=profile_data.get("tags"),
user_id=profile_data.get('id'),
username=profile_data.get('username'),
+ profile_music=profile_music
)
profile = generate_profile_object(pd)
@@ -341,3 +380,58 @@ def modify_dob(self, dob: str):
if not response.get('data', {}).get("saveDateOfBirth", {}).get("success"):
raise SyncJournalException("Error saving date of birth.")
+
+ def add_reaction(self, msg_id: str, reaction: str):
+ """
+ Adds a reaction to a message
+ :param msg_id: ID of msg to send reaction to.
+ :param reaction: Reaction to send.
+ :return:
+ """
+ query = AddReactionGQL(msg_id=msg_id, reaction=reaction).to_dict()
+ response = self._sync_journal_call(query)
+
+ if not response.get('data', {}).get("addMediaReaction", {}).get("success"):
+ raise SyncJournalException("Error adding reaction.")
+
+ def remove_reaction(self, msg_id: str, reaction: str):
+ """
+ removes a reaction from a message
+ :param msg_id: ID of msg to remove reaction from.
+ :param reaction: Reaction to remove.
+ :return:
+ """
+ query = RemoveReactionGQL(msg_id=msg_id, reaction=reaction).to_dict()
+ response = self._sync_journal_call(query)
+
+ if not response.get('data', {}).get("removeMediaReaction", {}).get("success"):
+ raise SyncJournalException("Error removing reaction.")
+
+ def create_comment(self, msg_id: str, text: str, comment_id: str | None = None):
+ """
+ Adds a comment to a post
+ :param comment_id: id of the comment, leave as None unless you know what you're doing
+ :param msg_id: id of the message
+ :param text: text to send in the comment
+ :return:
+ """
+ if comment_id is None:
+ comment_id = "01HEH" + str(uuid4()).upper().replace("-", "")[:20]
+ query = SendCommentGQL(comment_id=comment_id, msg_id=msg_id, text=text).to_dict()
+ response = self._sync_journal_call(query)
+
+ if not response.get('data', {}).get("sendMediaComment", {}).get("success"):
+ raise SyncJournalException("Error sending comment.")
+
+ def delete_comment(self, msg_id: str, comment_id: str):
+ """
+ Deletes a comment from a lapsepy post
+ :param msg_id: ID of the post
+ :param comment_id: ID of the comment
+ :return:
+ """
+ query = DeleteCommentGQL(msg_id=msg_id, comment_id=comment_id).to_dict()
+ response = self._sync_journal_call(query)
+
+ if not response.get('data', {}).get("deleteMediaComment", {}).get("success"):
+ raise SyncJournalException("Error deleting comment.")
diff --git a/lapsepy/journal/structures/__init__.py b/lapsepy/journal/structures/__init__.py
new file mode 100644
index 0000000..c54c325
--- /dev/null
+++ b/lapsepy/journal/structures/__init__.py
@@ -0,0 +1,8 @@
+"""
+Author: Quintin Dunn
+Date: 10/27/23
+"""
+
+from .profile import Profile, ProfileMusic
+from .snap import Snap
+from .friendsfeed import FriendsFeed, FriendNode
diff --git a/lapsepy/journal/structures/comment.py b/lapsepy/journal/structures/comment.py
new file mode 100644
index 0000000..5965a41
--- /dev/null
+++ b/lapsepy/journal/structures/comment.py
@@ -0,0 +1,17 @@
+from datetime import datetime
+
+from .core import Media
+
+from .profile import Profile
+
+
+class Comment:
+ def __init__(self, author: Profile, create_at: datetime, comment_id: str, likes: int, is_liked: bool, media:
+ Media, text: str):
+ self.author: Profile = author
+ self.created_at: create_at = create_at
+ self.comment_id: comment_id = comment_id
+ self.likes: int = likes
+ self.is_liked: bool = is_liked
+ self.media: Media = media
+ self.text: str = text
diff --git a/lapsepy/journal/structures/core.py b/lapsepy/journal/structures/core.py
new file mode 100644
index 0000000..da67200
--- /dev/null
+++ b/lapsepy/journal/structures/core.py
@@ -0,0 +1,2 @@
+class Media:
+ pass
diff --git a/lapsepy/journal/structures/friendsfeed.py b/lapsepy/journal/structures/friendsfeed.py
new file mode 100644
index 0000000..34429f6
--- /dev/null
+++ b/lapsepy/journal/structures/friendsfeed.py
@@ -0,0 +1,23 @@
+from datetime import datetime
+
+from .profile import Profile
+from .snap import Snap
+
+
+def _dt_from_iso(dt_str: str):
+ return datetime.fromisoformat(dt_str)
+
+
+class FriendsFeed:
+ def __init__(self, nodes: list["FriendNode"]):
+ self.nodes: list[FriendNode] = nodes
+
+ def __iter__(self):
+ return iter(self.nodes)
+
+
+class FriendNode:
+ def __init__(self, profile: Profile, iso_string: str, entries: list[Snap]):
+ self.profile = profile
+ self.timestamp: datetime = _dt_from_iso(iso_string)
+ self.entries: list[Snap] = entries
diff --git a/lapsepy/journal/structures/profile.py b/lapsepy/journal/structures/profile.py
new file mode 100644
index 0000000..46a4a09
--- /dev/null
+++ b/lapsepy/journal/structures/profile.py
@@ -0,0 +1,143 @@
+import io
+import logging
+import requests
+
+from datetime import datetime
+
+from PIL import Image
+import io
+
+from .snap import Snap
+
+logger = logging.getLogger("lapsepy.journal.structures.py")
+
+
+def _dt_from_iso(dt_str: str):
+ return datetime.fromisoformat(dt_str)
+
+
+class Profile:
+ def __init__(self, user_id: str, username: str, display_name: str, profile_photo_name: str, bio: str | None,
+ emojis: list[str], is_friends: bool, blocked_me: bool, kudos: int, tags: list[dict],
+ is_blocked: bool = False, friends: list["Profile"] = None, profile_music: "ProfileMusic" = None):
+ if friends is None:
+ friends = []
+
+ self.bio: str = bio
+ self.blocked_me: bool = blocked_me
+ self.user_display_name: str = display_name
+ self.emojis: list[str] = emojis
+ self.is_friends: bool = is_friends
+ self.kudos = kudos
+ self.profile_photo_name: str = profile_photo_name
+ self.tags = tags
+ self.user_id: str = user_id
+ self.username: str = username
+ self.media: list[Snap] = []
+ self.is_blocked = is_blocked
+
+ self.friends: list["Profile"] = friends
+ self.profile_music = profile_music
+
+ self.profile_picture: Image.Image | None = None
+
+ @staticmethod
+ def from_dict(profile_data: dict) -> "Profile":
+ """
+ Generates a Profile object from a dictionary with the necessary profile data
+ :param profile_data: Dictionary containing the necessary data.
+ :return: Profile object prefilled with the data.
+ """
+ logger.debug("Creating new Profile object from dictionary.")
+
+ pd = profile_data
+
+ music = pd.get("music")
+ if music is not None:
+ profile_music = ProfileMusic(
+ artist=music.get("artist"),
+ artwork_url=music.get("artworkUrl"),
+ duration=music.get("duration"),
+ song_title=music.get("songTitle"),
+ song_url=music.get("songUrl")
+ )
+ else:
+ profile_music = None
+
+ return Profile(
+ bio=pd.get('bio'),
+ blocked_me=pd.get('blockedMe'),
+ display_name=pd.get('displayName'),
+ emojis=pd.get("emojis", {}).get("emojis"),
+ is_friends=pd.get("friendStatus") == "FRIENDS",
+ kudos=pd.get("kudos", {}).get("totalCount", -1),
+ profile_photo_name=pd.get('profilePhotoName'),
+ tags=pd.get("tags"),
+ user_id=pd.get('id'),
+ username=pd.get('username'),
+ profile_music=profile_music
+ )
+
+ def load_profile_picture(self, quality: int = 100, height: int | None = None) -> Image.Image:
+ """
+ Loads the Profile's profile picture into memory by making an HTTP request to Lapse's servers.
+ :param quality: Quality of the image (1-100)
+ seek https://cloudinary.com/documentation/transformation_reference#q_quality for more information.
+ :param height: Height of the image in pixels, width is determined by image aspect ratio. Leave as None to get
+ original height.
+
+ :return: Pillow image.
+ """
+ url = f"https://image.production.journal-api.lapse.app/image/upload/q_{quality}"
+ url += f",h_{height}" if height is not None else ""
+ url += f"//{self.profile_photo_name}.jpg"
+
+ logger.debug(f"Getting profile image from \"{url}\"")
+
+ request = requests.get(url)
+ request.raise_for_status()
+
+ bytes_io = io.BytesIO(request.content)
+ image = Image.open(bytes_io)
+
+ self.profile_picture = image
+
+ return image
+
+ def send_instant(self, ctx, im: Image, file_uuid: str | None = None, im_id: str | None = None,
+ caption: str | None = None, time_limit: int = 10):
+ return ctx.upload_instant(im=im, user=self, file_uuid=file_uuid, im_id=im_id, caption=caption,
+ time_limit=time_limit)
+
+ def __str__(self):
+ return f"
"
+
+
+class ProfileMusic:
+ def __init__(self, artist: str, artwork_url: str, duration: int, song_title: str, song_url: str):
+ self.artist = artist
+ self.artwork_url = artwork_url
+ self.duration = duration
+ self.song_title = song_title
+ self.song_url = song_url
+
+ self.song: None | bytes = None
+ self.artwork: None | Image.Image = None
+
+ def load(self):
+ """
+ Loads the song, and artwork into memory
+ :return: None
+ """
+ # Get song
+ request = requests.get(self.song_url)
+ request.raise_for_status()
+ self.song = request.content
+
+ # Get artwork
+ if self.artwork_url:
+ request = requests.get(self.artwork_url)
+ request.raise_for_status()
+
+ bytes_io = io.BytesIO(request.content)
+ self.artwork = Image.open(bytes_io)
\ No newline at end of file
diff --git a/lapsepy/journal/structures.py b/lapsepy/journal/structures/snap.py
similarity index 57%
rename from lapsepy/journal/structures.py
rename to lapsepy/journal/structures/snap.py
index 3868726..ff7a748 100644
--- a/lapsepy/journal/structures.py
+++ b/lapsepy/journal/structures/snap.py
@@ -17,86 +17,6 @@ def _dt_from_iso(dt_str: str):
return datetime.fromisoformat(dt_str)
-class Profile:
- def __init__(self, user_id: str, username: str, display_name: str, profile_photo_name: str, bio: str | None,
- emojis: list[str], is_friends: bool, blocked_me: bool, kudos: int, tags: list[dict],
- is_blocked: bool = False, friends: list["Profile"] = None):
- if friends is None:
- friends = []
-
- self.bio: str = bio
- self.blocked_me: bool = blocked_me
- self.user_display_name: str = display_name
- self.emojis: list[str] = emojis
- self.is_friends: bool = is_friends
- self.kudos = kudos
- self.profile_photo_name: str = profile_photo_name
- self.tags = tags
- self.user_id: str = user_id
- self.username: str = username
- self.media: list[Snap] = []
- self.is_blocked = is_blocked
-
- self.friends: list["Profile"] = friends
-
- self.profile_picture: Image.Image | None = None
-
- @staticmethod
- def from_dict(profile_data: dict) -> "Profile":
- """
- Generates a Profile object from a dictionary with the necessary profile data
- :param profile_data: Dictionary containing the necessary data.
- :return: Profile object prefilled with the data.
- """
- logger.debug("Creating new Profile object from dictionary.")
-
- pd = profile_data
- return Profile(
- bio=pd.get('bio'),
- blocked_me=pd.get('blockedMe'),
- display_name=pd.get('displayName'),
- emojis=pd.get("emojis", {}).get("emojis"),
- is_friends=pd.get("friendStatus") == "FRIENDS",
- kudos=pd.get("kudos", {}).get("totalCount", -1),
- profile_photo_name=pd.get('profilePhotoName'),
- tags=pd.get("tags"),
- user_id=pd.get('id'),
- username=pd.get('username'),
- )
-
- def load_profile_picture(self, quality: int = 100, height: int | None = None) -> Image.Image:
- """
- Loads the Profile's profile picture into memory by making an HTTP request to Lapse's servers.
- :param quality: Quality of the image (1-100)
- seek https://cloudinary.com/documentation/transformation_reference#q_quality for more information.
- :param height: Height of the image in pixels, width is determined by image aspect ratio. Leave as None to get
- original height.
-
- :return: Pillow image.
- """
- url = f"https://image.production.journal-api.lapse.app/image/upload/q_{quality}"
- url += f",h_{height}" if height is not None else ""
- url += f"//{self.profile_photo_name}.jpg"
-
- logger.debug(f"Getting profile image from \"{url}\"")
-
- request = requests.get(url)
- bytes_io = io.BytesIO(request.content)
- image = Image.open(bytes_io)
-
- self.profile_picture = image
-
- return image
-
- def send_instant(self, ctx, im: Image, file_uuid: str | None = None, im_id: str | None = None,
- caption: str | None = None, time_limit: int = 10):
- return ctx.upload_instant(im=im, user=self, file_uuid=file_uuid, im_id=im_id, caption=caption,
- time_limit=time_limit)
-
- def __str__(self):
- return f""
-
-
class Snap:
BASE_URL = "https://image.production.journal-api.lapse.app/image/upload/"
diff --git a/lapsepy/lapse/lapse.py b/lapsepy/lapse/lapse.py
index c48fba1..11f8c7d 100644
--- a/lapsepy/lapse/lapse.py
+++ b/lapsepy/lapse/lapse.py
@@ -8,7 +8,7 @@
from lapsepy.auth.refresher import refresh
from lapsepy.journal.journal import Journal
from lapsepy.journal.common.exceptions import AuthTokenExpired
-from lapsepy.journal.structures import Profile
+from lapsepy.journal.structures.profile import Profile
import logging
@@ -86,6 +86,35 @@ def upload_instant(self, im: Image, user: str | Profile, file_uuid: str | None =
return self.journal.upload_instant(im=im, user_id=user, file_uuid=file_uuid, im_id=im_id, caption=caption,
time_limit=time_limit)
+ def create_status_update(self, text: str, msg_id: str | None = None):
+ """
+ Creates a status update on your Journal
+ :param text: Msg of the text to send
+ :param msg_id: Leave None if you don't know what you're doing. FORMAT: STATUS_UPDATE:<(str(uuid.uuid4))>
+ :return:
+ """
+ return self.journal.create_status_update(text=text, msg_id=msg_id)
+
+ def remove_status_update(self, msg_id: str, removed_at: datetime | None = None):
+ """
+ Removes a status update
+ :param msg_id: ID of the status update
+ :param removed_at: datetime object of when it was removed
+ :return:
+ """
+ return self.journal.remove_status_update(msg_id=msg_id, removed_at=removed_at)
+
+ def send_kudos(self, user: str | Profile):
+ """
+ Sends kudos (vibes) to a user.
+ :param user: ID / Object of user to send it to.
+ :return:
+ """
+ if isinstance(user, Profile):
+ user = user.user_id
+
+ self.journal.send_kudos(user)
+
def get_friends_feed(self, count: int = 10):
"""
Gets your friend upload feed.
@@ -147,3 +176,40 @@ def update_dob(self, dob: str):
:return: None
"""
return self.journal.modify_dob(dob=dob)
+
+ def add_reaction(self, msg_id: str, reaction: str):
+ """
+ Adds a reaction to a message
+ :param msg_id: ID of msg to send reaction to.
+ :param reaction: Reaction to send.
+ :return:
+ """
+ return self.journal.add_reaction(msg_id=msg_id, reaction=reaction)
+
+ def remove_reaction(self, msg_id: str, reaction: str):
+ """
+ removes a reaction from a message
+ :param msg_id: ID of msg to remove reaction from.
+ :param reaction: Reaction to remove.
+ :return:
+ """
+ return self.journal.remove_reaction(msg_id=msg_id, reaction=reaction)
+
+ def send_comment(self, msg_id: str, text: str, comment_id: str | None = None):
+ """
+ Adds a comment to a post
+ :param comment_id: id of the comment, leave as None unless you know what you're doing
+ :param msg_id: id of the message
+ :param text: text to send in the comment
+ :return:
+ """
+ return self.journal.create_comment(msg_id=msg_id, text=text, comment_id=comment_id)
+
+ def delete_comment(self, msg_id: str, comment_id: str):
+ """
+ Deletes a comment from a lapsepy post
+ :param msg_id: ID of the post
+ :param comment_id: ID of the comment
+ :return:
+ """
+ return self.journal.delete_comment(msg_id=msg_id, comment_id=comment_id)
diff --git a/setup.py b/setup.py
index c33d54b..27430d5 100644
--- a/setup.py
+++ b/setup.py
@@ -1,26 +1,31 @@
from setuptools import setup, find_packages
-VERSION = "0.1.1"
+VERSION = "0.2.1"
DESCRIPTION = "A Python API wrapper for the social media app Lapse."
-LONG_DESCRIPTION = "An unofficial API wrapper for the social media app Lapse."
+
+with open("README.md", 'r') as f:
+ LONG_DESCRIPTION = f.read()
with open("requirements.txt", 'r', encoding="utf-16") as f:
requirements = [i.strip() for i in f.readlines()]
+LICENSE = "MIT"
+
setup(name='lapsepy',
version=VERSION,
description=DESCRIPTION,
long_description_content_type="text/markdown",
long_description=LONG_DESCRIPTION,
+ license=LICENSE,
author="Quintin Dunn",
author_email="dunnquintin07@gmail.com",
url="https://github.com/quintindunn/lapsepy",
packages=find_packages(),
keywords=['social media', 'lapsepy', 'api', 'api wrapper'],
classifiers=[
- 'Development Status :: 5 - Production/Stable',
- 'Operating System :: Microsoft :: Windows :: Windows 10',
- 'Programming Language :: Python :: 3',
+ 'Development Status :: 5 - Production/Stable',
+ 'Operating System :: Microsoft :: Windows :: Windows 10',
+ 'Programming Language :: Python :: 3',
],
install_requires=requirements
)