From 3473e6ed6ddff5a8e63c7b5c3b61611befdeb05d Mon Sep 17 00:00:00 2001 From: Kumi Date: Sat, 13 Jan 2024 16:46:59 +0100 Subject: [PATCH] Add initial project structure with Knuddels API integration Introduced the foundational codebase for a new Python wrapper to interact with the Knuddels.de API. This includes a .gitignore file to exclude virtual environments and compiled Python files, basic project metadata in pyproject.toml, and license information. Core functionality is added in the `api.py` file within a `Knuddels` class, enabling login, session handling, and GraphQL queries for message management. Accompanying `users.py` and `messages.py` modules define relevant data classes. Also set up a test script to verify login and data retrieval flows. --- .gitignore | 3 + LICENSE | 0 README.md | 0 pyproject.toml | 23 +++++ src/pyknuddels/__init__.py | 0 src/pyknuddels/classes/__init__.py | 0 src/pyknuddels/classes/api.py | 147 +++++++++++++++++++++++++++++ src/pyknuddels/classes/messages.py | 86 +++++++++++++++++ src/pyknuddels/classes/users.py | 40 ++++++++ test.py | 22 +++++ 10 files changed, 321 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/pyknuddels/__init__.py create mode 100644 src/pyknuddels/classes/__init__.py create mode 100644 src/pyknuddels/classes/api.py create mode 100644 src/pyknuddels/classes/messages.py create mode 100644 src/pyknuddels/classes/users.py create mode 100644 test.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25636a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +venv/ +*.pyc +__pycache__/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..137100a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "pyknuddels" +version = "0.1.0" +authors = [ + { name="Kumi", email="pyknuddels@kumi.email" }, +] +description = "Simple Python wrapper to fetch data from Knuddels.de" +readme = "README.md" +license = { file="LICENSE" } +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[project.urls] +"Homepage" = "https://kumig.it/kumitterer/pyknuddels" +"Bug Tracker" = "https://kumig.it/kumitterer/pyknuddels/issues" \ No newline at end of file diff --git a/src/pyknuddels/__init__.py b/src/pyknuddels/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pyknuddels/classes/__init__.py b/src/pyknuddels/classes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pyknuddels/classes/api.py b/src/pyknuddels/classes/api.py new file mode 100644 index 0000000..5839321 --- /dev/null +++ b/src/pyknuddels/classes/api.py @@ -0,0 +1,147 @@ +import logging +import json +import uuid + +from urllib.request import Request, urlopen +from urllib.parse import urlencode +from urllib.error import HTTPError + +from .messages import Conversation, Message +from .users import User + + +class Knuddels: + def __init__(self, username: str, password: str): + self.username = username + self.password = password + self.jwt = None + self.session_token = None + self.session_expiry = None + + def login(self): + LOGIN_URL = "https://www.knuddels.de/logincheck.html" + + LOGIN_DATA = { + "nick": self.username, + "pwd": self.password, + "resultAsJSON": True, + "isAjax": True, + } + + LOGIN_DATA = urlencode(LOGIN_DATA).encode("utf-8") + + try: + response = urlopen(Request(LOGIN_URL, data=LOGIN_DATA)) + + response = json.loads(response.read()) + self.jwt = response["jwt"] + + logging.info("Successfully logged in as " + self.username) + + return True + + except HTTPError as error: + logging.error(f"Login failed with status code {response.status}") + + return False + + def graphql(self, query: str, autorefresh: bool = True): + GRAPHQL_URL = "https://api-de.knuddels.de/api-gateway/graphql" + + if autorefresh and ( + (not self.session_expiry) + or (self.session_expiry < self.current_server_time()) + ): + self.refresh_session_token() + + headers = {} + headers["authorization"] = f"Bearer {self.session_token or self.jwt}" + + try: + response = urlopen( + Request( + GRAPHQL_URL, + data=query.replace("\n", "\\n").encode("utf-8"), + headers=headers, + ) + ) + return json.loads(response.read()) + + except HTTPError as error: + logging.error(f"GraphQL request failed with status code {error.status}") + print(error.read()) + + return None + + def refresh_session_token(self, assert_login: bool = True): + REFRESH_SESSION_TOKEN_QUERY = '{"operationName":"RefreshSessionToken","variables":{"sessionInfo":{"type":"K3GraphQl","clientVersion":{"major":7,"minor":2,"patch":1,"buildInfo":"c021cdc230b2e7c9fdb0d9d9ea635cb0b59c2c3e"},"platform":"Web","osInfo":{"type":"Unknown","version":"Unknown"},"deviceInfo":{"manufacturer":"Unknown","model":"Unknown"},"deviceIdentifiers":[],"clientState":"Active"}},"query":"query RefreshSessionToken($sessionInfo: SessionInfo!, $oldSessionToken: SessionToken) {\n login {\n refreshSession(sessionInfo: $sessionInfo, token: $oldSessionToken) {\n ... on RefreshSessionSuccess {\n expiry\n token\n __typename\n }\n ...RefreshSessionError\n __typename\n }\n __typename\n }\n}\n\nfragment RefreshSessionError on RefreshSessionError {\n formattedErrorMessage\n user {\n ...UserWithLockInfo\n __typename\n }\n __typename\n}\n\nfragment UserWithLockInfo on User {\n id\n nick\n lockInfo {\n ... on UnlockedLockInfo {\n __typename\n }\n ... on TemporaryLockInfo {\n lockReason\n lockedUntilDate\n __typename\n }\n ... on PermanentLockInfo {\n lockReason\n __typename\n }\n __typename\n }\n __typename\n}\n"}' + + if assert_login and not self.jwt: + self.login() + + response = self.graphql(REFRESH_SESSION_TOKEN_QUERY, autorefresh=False) + + try: + self.session_token = response["data"]["login"]["refreshSession"]["token"] + self.session_expiry = response["data"]["login"]["refreshSession"]["expiry"] + return True + + except KeyError: + logging.error("Failed to refresh session token") + return False + + def messenger_overview(self, limit=10000, filter_by_state="ALL"): + MESSENGER_OVERVIEW_QUERY = '{"operationName":"MessengerOverview","variables":{"limit":%i,"before":null,"filterByState":"%s","pixelDensity":1},"query":"query MessengerOverview($limit: Int = 20, $before: UtcTimestamp = null, $pixelDensity: Float!, $filterByState: MessengerConversationState = ALL) {\n messenger {\n conversations(limit: $limit, before: $before, filterByState: $filterByState) {\n conversations {\n ...FullConversationWithoutMessages\n __typename\n }\n hasMore\n __typename\n }\n __typename\n }\n}\n\nfragment FullConversationWithoutMessages on MessengerConversation {\n id\n visibility\n otherParticipants {\n ...MessengerOverviewUser\n __typename\n }\n readState {\n markedAsUnread\n unreadMessageCount\n lastReadConversationMessage {\n id\n __typename\n }\n __typename\n }\n latestConversationMessage {\n ...ConversationMessage\n __typename\n }\n __typename\n}\n\nfragment MessengerOverviewUser on User {\n ...MessengerBasicUser\n age\n albumPhotosUrl\n canReceiveMessages\n city\n distance\n gender\n id\n isOnline\n currentOnlineChannelName\n latestOnlineChannelName\n lastOnlineTime\n nick\n profilePicture {\n urlCustomSizeSquare(pixelDensity: $pixelDensity, size: 60)\n __typename\n }\n profilePictureOverlays\n readMe\n relationshipStatus\n sexualOrientation\n onlineMinutes\n isAppBot\n isLockedByAutomaticComplaint\n automaticComplaintCommand\n isReportable\n interest\n latestClient\n authenticityClassification\n __typename\n}\n\nfragment MessengerBasicUser on User {\n id\n nick\n isOnline\n canSendImages\n menteeStatus\n __typename\n}\n\nfragment ConversationMessage on ConversationMessage {\n id\n timestamp\n sender {\n ...MessengerBasicUser\n __typename\n }\n content {\n ...ConversationMessageContent\n __typename\n }\n __typename\n}\n\nfragment ConversationMessageContent on ConversationMessageContent {\n ... on ConversationTextMessageContent {\n ...ConversationTextMessageContent\n __typename\n }\n ... on ConversationQuotedMessageContent {\n ...ConversationQuotedMessageContent\n __typename\n }\n ... on ConversationForwardedMessageContent {\n ...ConversationForwardedMessageContent\n __typename\n }\n ... on ConversationImageMessageContent {\n ...ConversationImageMessageContent\n __typename\n }\n ... on ConversationSnapMessageContent {\n ...ConversationSnapMessageContent\n __typename\n }\n ... on ConversationVisiblePhotoCommentMessageContent {\n ...ConversationVisiblePhotoCommentMessageContent\n __typename\n }\n ... on ConversationHiddenPhotoCommentMessageContent {\n ...ConversationHiddenPhotoCommentMessageContent\n __typename\n }\n ... on ConversationDeletedPhotoCommentMessageContent {\n ...ConversationDeletedPhotoCommentMessageContent\n __typename\n }\n ... on ConversationKnuddelTransferMessageContent {\n ...ConversationKnuddelTransferMessageContent\n __typename\n }\n ... on ConversationMentorAchievedMessageContent {\n ...ConversationMentorAchievedMessageContent\n __typename\n }\n ... on ConversationPrivateSystemMessageContent {\n ...ConversationPrivateSystemMessageContent\n __typename\n }\n ... on ConversationBirthdayMessageContent {\n ...ConversationBirthdayMessageContent\n __typename\n }\n ... on ConversationNicknameChangeMessageContent {\n ...ConversationNicknameChangeMessageContent\n __typename\n }\n __typename\n}\n\nfragment ConversationTextMessageContent on ConversationTextMessageContent {\n formattedText\n starred\n __typename\n}\n\nfragment ConversationQuotedMessageContent on ConversationQuotedMessageContent {\n starred\n formattedText\n nestedMessage {\n ...ConversationNestedMessage\n __typename\n }\n __typename\n}\n\nfragment ConversationNestedMessage on ConversationNestedMessage {\n sender {\n ...MessengerBasicUser\n __typename\n }\n timestamp\n content {\n ...ConversationNestedMessageContent\n __typename\n }\n __typename\n}\n\nfragment ConversationNestedMessageContent on ConversationNestedMessageContent {\n ... on ConversationTextMessageContent {\n starred\n formattedText\n __typename\n }\n ... on ConversationImageMessageContent {\n starred\n image {\n url\n __typename\n }\n imageAccepted\n sensitiveContentClassification\n __typename\n }\n __typename\n}\n\nfragment ConversationForwardedMessageContent on ConversationForwardedMessageContent {\n starred\n nestedMessage {\n ...ConversationNestedMessage\n __typename\n }\n __typename\n}\n\nfragment ConversationImageMessageContent on ConversationImageMessageContent {\n starred\n image {\n url\n __typename\n }\n imageAccepted\n sensitiveContentClassification\n __typename\n}\n\nfragment ConversationSnapMessageContent on ConversationSnapMessageContent {\n snap {\n url\n photoId\n duration\n decryptionKey\n __typename\n }\n imageAccepted\n sensitiveContentClassification\n __typename\n}\n\nfragment ConversationVisiblePhotoCommentMessageContent on ConversationVisiblePhotoCommentMessageContent {\n albumPhotoId\n commentId\n photoUrl\n formattedText\n __typename\n}\n\nfragment ConversationHiddenPhotoCommentMessageContent on ConversationHiddenPhotoCommentMessageContent {\n albumPhotoId\n photoUrl\n formattedText\n __typename\n}\n\nfragment ConversationDeletedPhotoCommentMessageContent on ConversationDeletedPhotoCommentMessageContent {\n unused\n __typename\n}\n\nfragment ConversationKnuddelTransferMessageContent on ConversationKnuddelTransferMessageContent {\n knuddelAmount\n __typename\n}\n\nfragment ConversationMentorAchievedMessageContent on ConversationMentorAchievedMessageContent {\n unused\n __typename\n}\n\nfragment ConversationPrivateSystemMessageContent on ConversationPrivateSystemMessageContent {\n icon\n formattedText\n collapse\n __typename\n}\n\nfragment ConversationBirthdayMessageContent on ConversationBirthdayMessageContent {\n unused\n __typename\n}\n\nfragment ConversationNicknameChangeMessageContent on ConversationNicknameChangeMessageContent {\n oldNick\n newNick\n __typename\n}\n"}' + + response = self.graphql(MESSENGER_OVERVIEW_QUERY % (limit, filter_by_state)) + + try: + return response["data"]["messenger"]["conversations"]["conversations"] + except KeyError: + logging.error("Failed to get messenger overview") + + def get_conversation(self, conversation_id: int, limit: int = 10000): + GET_CONVERSATION_QUERY = '{"operationName":"GetConversation","variables":{"messageCount":%i,"beforeMessageId":null,"id":"%i","pixelDensity":1},"query":"query GetConversation($id: ID!, $messageCount: Int = 50, $beforeMessageId: ID = null, $pixelDensity: Float!) {\n messenger {\n conversation(id: $id) {\n ...FullConversation\n __typename\n }\n __typename\n }\n}\n\nfragment FullConversation on MessengerConversation {\n id\n visibility\n otherParticipants {\n ...MessengerFullUser\n __typename\n }\n readState {\n markedAsUnread\n unreadMessageCount\n lastReadConversationMessage {\n id\n __typename\n }\n __typename\n }\n latestConversationMessage {\n ...ConversationMessage\n __typename\n }\n conversationMessages(limit: $messageCount, beforeMessageId: $beforeMessageId) {\n messages {\n ...ConversationMessage\n __typename\n }\n hasMore\n __typename\n }\n __typename\n}\n\nfragment MessengerFullUser on User {\n ...MessengerOverviewUser\n ignoreState\n isIgnoring\n isAllowedByContactFilter\n __typename\n}\n\nfragment MessengerOverviewUser on User {\n ...MessengerBasicUser\n age\n albumPhotosUrl\n canReceiveMessages\n city\n distance\n gender\n id\n isOnline\n currentOnlineChannelName\n latestOnlineChannelName\n lastOnlineTime\n nick\n profilePicture {\n urlCustomSizeSquare(pixelDensity: $pixelDensity, size: 60)\n __typename\n }\n profilePictureOverlays\n readMe\n relationshipStatus\n sexualOrientation\n onlineMinutes\n isAppBot\n isLockedByAutomaticComplaint\n automaticComplaintCommand\n isReportable\n interest\n latestClient\n authenticityClassification\n __typename\n}\n\nfragment MessengerBasicUser on User {\n id\n nick\n isOnline\n canSendImages\n menteeStatus\n __typename\n}\n\nfragment ConversationMessage on ConversationMessage {\n id\n timestamp\n sender {\n ...MessengerBasicUser\n __typename\n }\n content {\n ...ConversationMessageContent\n __typename\n }\n __typename\n}\n\nfragment ConversationMessageContent on ConversationMessageContent {\n ... on ConversationTextMessageContent {\n ...ConversationTextMessageContent\n __typename\n }\n ... on ConversationQuotedMessageContent {\n ...ConversationQuotedMessageContent\n __typename\n }\n ... on ConversationForwardedMessageContent {\n ...ConversationForwardedMessageContent\n __typename\n }\n ... on ConversationImageMessageContent {\n ...ConversationImageMessageContent\n __typename\n }\n ... on ConversationSnapMessageContent {\n ...ConversationSnapMessageContent\n __typename\n }\n ... on ConversationVisiblePhotoCommentMessageContent {\n ...ConversationVisiblePhotoCommentMessageContent\n __typename\n }\n ... on ConversationHiddenPhotoCommentMessageContent {\n ...ConversationHiddenPhotoCommentMessageContent\n __typename\n }\n ... on ConversationDeletedPhotoCommentMessageContent {\n ...ConversationDeletedPhotoCommentMessageContent\n __typename\n }\n ... on ConversationKnuddelTransferMessageContent {\n ...ConversationKnuddelTransferMessageContent\n __typename\n }\n ... on ConversationMentorAchievedMessageContent {\n ...ConversationMentorAchievedMessageContent\n __typename\n }\n ... on ConversationPrivateSystemMessageContent {\n ...ConversationPrivateSystemMessageContent\n __typename\n }\n ... on ConversationBirthdayMessageContent {\n ...ConversationBirthdayMessageContent\n __typename\n }\n ... on ConversationNicknameChangeMessageContent {\n ...ConversationNicknameChangeMessageContent\n __typename\n }\n __typename\n}\n\nfragment ConversationTextMessageContent on ConversationTextMessageContent {\n formattedText\n starred\n __typename\n}\n\nfragment ConversationQuotedMessageContent on ConversationQuotedMessageContent {\n starred\n formattedText\n nestedMessage {\n ...ConversationNestedMessage\n __typename\n }\n __typename\n}\n\nfragment ConversationNestedMessage on ConversationNestedMessage {\n sender {\n ...MessengerBasicUser\n __typename\n }\n timestamp\n content {\n ...ConversationNestedMessageContent\n __typename\n }\n __typename\n}\n\nfragment ConversationNestedMessageContent on ConversationNestedMessageContent {\n ... on ConversationTextMessageContent {\n starred\n formattedText\n __typename\n }\n ... on ConversationImageMessageContent {\n starred\n image {\n url\n __typename\n }\n imageAccepted\n sensitiveContentClassification\n __typename\n }\n __typename\n}\n\nfragment ConversationForwardedMessageContent on ConversationForwardedMessageContent {\n starred\n nestedMessage {\n ...ConversationNestedMessage\n __typename\n }\n __typename\n}\n\nfragment ConversationImageMessageContent on ConversationImageMessageContent {\n starred\n image {\n url\n __typename\n }\n imageAccepted\n sensitiveContentClassification\n __typename\n}\n\nfragment ConversationSnapMessageContent on ConversationSnapMessageContent {\n snap {\n url\n photoId\n duration\n decryptionKey\n __typename\n }\n imageAccepted\n sensitiveContentClassification\n __typename\n}\n\nfragment ConversationVisiblePhotoCommentMessageContent on ConversationVisiblePhotoCommentMessageContent {\n albumPhotoId\n commentId\n photoUrl\n formattedText\n __typename\n}\n\nfragment ConversationHiddenPhotoCommentMessageContent on ConversationHiddenPhotoCommentMessageContent {\n albumPhotoId\n photoUrl\n formattedText\n __typename\n}\n\nfragment ConversationDeletedPhotoCommentMessageContent on ConversationDeletedPhotoCommentMessageContent {\n unused\n __typename\n}\n\nfragment ConversationKnuddelTransferMessageContent on ConversationKnuddelTransferMessageContent {\n knuddelAmount\n __typename\n}\n\nfragment ConversationMentorAchievedMessageContent on ConversationMentorAchievedMessageContent {\n unused\n __typename\n}\n\nfragment ConversationPrivateSystemMessageContent on ConversationPrivateSystemMessageContent {\n icon\n formattedText\n collapse\n __typename\n}\n\nfragment ConversationBirthdayMessageContent on ConversationBirthdayMessageContent {\n unused\n __typename\n}\n\nfragment ConversationNicknameChangeMessageContent on ConversationNicknameChangeMessageContent {\n oldNick\n newNick\n __typename\n}\n"}' + + response = self.graphql( + GET_CONVERSATION_QUERY % (int(limit), int(conversation_id)) + ) + + try: + conversation = response["data"]["messenger"]["conversation"] + messages = conversation["conversationMessages"]["messages"] + other_participants = conversation["otherParticipants"] + + return Conversation( + id=conversation["id"], + messages=[Message.from_dict(message) for message in messages], + other_participants=[ + User.from_dict(user) for user in other_participants + ], + ) + except KeyError as e: + logging.error("Failed to get conversation: " + str(e)) + + def send_message(self, conversation_id, text): + SEND_MESSAGE_QUERY = '{"operationName":"MessengerSendMessage","variables":{"id":"%i","text":"%s","messageCorrelationId":"%s"},"query":"mutation MessengerSendMessage($id: ID!, $text: String!, $messageCorrelationId: ID!) {\n messenger {\n sendMessage(\n conversationId: $id\n text: $text\n messageCorrelationId: $messageCorrelationId\n ) {\n error {\n type\n filterReason\n __typename\n }\n __typename\n }\n __typename\n }\n}\n"}' + + correlation_id = str(uuid.uuid4()) + response = self.graphql( + SEND_MESSAGE_QUERY % (conversation_id, text, correlation_id) + ) + + try: + return response["data"]["messenger"]["sendMessage"]["error"] is None + except KeyError: + logging.error("Failed to send message") + return False + + def current_server_time(self): + CURRENT_SERVER_TIME_QUERY = '{"operationName":"CurrentServerTime","variables":{},"query":"query CurrentServerTime {\n currentTime\n}\n"}' + + try: + return self.graphql(CURRENT_SERVER_TIME_QUERY, autorefresh=False)["data"][ + "currentTime" + ] + except KeyError: + logging.error("Failed to get current server time") diff --git a/src/pyknuddels/classes/messages.py b/src/pyknuddels/classes/messages.py new file mode 100644 index 0000000..a5b144e --- /dev/null +++ b/src/pyknuddels/classes/messages.py @@ -0,0 +1,86 @@ +from .users import User + +from typing import List, Optional + + +class MessageContent: + text: str + + def __init__(self, formatted_text: dict) -> None: + self.formatted_text = formatted_text + + def get_text(self) -> str: + return self.formatted_text["text"]["text"] + + def get_formatted_text(self) -> str: + return self.formatted_text + + @classmethod + def from_dict(cls, data: dict): + obj = cls(data["formattedText"]) + return obj + + def __repr__(self) -> str: + return f"" + + +class Message: + sender: User + content: dict + + def __init__(self, sender: User, content: MessageContent) -> None: + self.sender = sender + self.content = content + + def get_sender(self) -> User: + return self.sender + + def get_content(self) -> dict: + return self.content + + @classmethod + def from_dict(cls, data: dict): + obj = cls(User.from_dict(data["sender"]), MessageContent.from_dict(data["content"])) + + return obj + + def __repr__(self) -> str: + return f"" + +class Conversation: + id: int + visibility: str + messages: List[Message] = [] + otherParticipants: List[User] = [] + + def __init__( + self, id: int, messages: List[Message], other_participants: List[User] + ) -> None: + self.id = id + self.messages = messages + self.other_participants = other_participants + + def get_messages(self) -> List[Message]: + return self.messages + + def get_other_participants(self) -> List[User]: + return self.other_participants + + def get_other_participant(self) -> User: + return self.other_participants[0] + + def get_message_count(self) -> int: + return len(self.messages) + + @classmethod + def from_dict(cls, data: dict): + obj = cls(data["id"], [], []) + obj.visibility = data["visibility"] + + for participant in data["otherParticipants"]: + obj.other_participants.append(User.from_dict(participant)) + + return obj + + def __repr__(self) -> str: + return f"" \ No newline at end of file diff --git a/src/pyknuddels/classes/users.py b/src/pyknuddels/classes/users.py new file mode 100644 index 0000000..06753d9 --- /dev/null +++ b/src/pyknuddels/classes/users.py @@ -0,0 +1,40 @@ +from typing import Optional + +class User: + id: int + nick: str + isOnline: bool + canSendImages: bool + menteeStatus: str + age: int + albumPhotosUrl: str + canReceiveMessages: bool + city: str + distance: Optional[int] + gender: str + currentOnlineChannelName: str + latestOnlineChannelName: str + lastOnlineTime: int + profilePicture: str + readMe: dict + relationshipStatus: str + sexualOrientation: str + onlineMinutes: int + isAppBot: bool + isLockedByAutomaticComplaint: bool + automaticComplaintCommand: str + isReportable: bool + interest: Optional[str] + latestClient: Optional[str] #? + authenticityClassification: str + + @classmethod + def from_dict(cls, data: dict): + obj = cls() + for key, value in data.items(): + setattr(obj, key, value) + + return obj + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..1e9824a --- /dev/null +++ b/test.py @@ -0,0 +1,22 @@ +import logging +import os + +logging.basicConfig( + format="[%(asctime)s] [%(levelname)s] %(message)s", + datefmt="%d.%m.%Y %H:%M:%S", + level=logging.INFO + ) + +from pyknuddels.classes.api import Knuddels +from pyknuddels.classes.messages import Conversation, Message + +knuddels = Knuddels(os.environ["KN_USER"], os.environ["KN_PASSWORD"]) + +knuddels.login() + +conversations = knuddels.messenger_overview(1) + +for conversation in conversations: + obj = knuddels.get_conversation(conversation["id"]) + obj = Conversation.from_dict(conversation) + print(obj) \ No newline at end of file