diff --git a/docs/api.rst b/docs/api.rst index f1d8b35..274bf24 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -28,8 +28,6 @@ Messages .. autoclass:: Mention .. autoclass:: EmojiSize(Enum) :undoc-members: -.. autoclass:: MessageReaction(Enum) - :undoc-members: Exceptions ---------- @@ -56,11 +54,7 @@ Miscellaneous .. autoclass:: ThreadLocation(Enum) :undoc-members: -.. autoclass:: ThreadColor(Enum) - :undoc-members: .. autoclass:: ActiveStatus() -.. autoclass:: TypingStatus(Enum) - :undoc-members: .. autoclass:: QuickReply .. autoclass:: QuickReplyText diff --git a/docs/intro.rst b/docs/intro.rst index 6f8f849..46fa972 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -70,8 +70,8 @@ corresponds to the ID of a single user, and the ID of a group respectively:: Some functions don't require a thread type, so in these cases you just provide the thread ID:: thread = fbchat.Thread(session=session, id="") - thread.set_color(ThreadColor.BILOBA_FLOWER) - thread.set_color(ThreadColor.MESSENGER_BLUE) + thread.set_color("#a695c7") + thread.set_color("#67b868") .. _intro_message_ids: @@ -82,12 +82,10 @@ Message IDs Every message you send on Facebook has a unique ID, and every action you do in a thread, like changing a nickname or adding a person, has a unique ID too. -Some of ``fbchat``'s functions require these ID's, like `Client.react_to_message`, -and some of then provide this ID, like `Client.send`. This snippet shows how to send a message, and then use the returned ID to react to that message with a 😍 emoji:: - message_id = thread.send(Message(text='message')) - client.react_to_message(message_id, MessageReaction.LOVE) + message = thread.send_text("A message!") + message.react("😍") .. _intro_interacting: diff --git a/examples/interract.py b/examples/interract.py index 16c6f7a..ec2471f 100644 --- a/examples/interract.py +++ b/examples/interract.py @@ -54,8 +54,8 @@ thread.set_nickname(fbchat.User(session=session, id=""), " (3, 5) attrs_default = attr.s(slots=True, kw_only=kw_only) -class Enum(aenum.Enum): - """Used internally to support enumerations""" - - def __repr__(self): - # For documentation: - return "{}.{}".format(type(self).__name__, self.name) - - @classmethod - def _extend_if_invalid(cls, value): - try: - return cls(value) - except ValueError: - log.warning( - "Failed parsing {.__name__}({!r}). Extending enum.".format(cls, value) - ) - aenum.extend_enum(cls, "UNKNOWN_{}".format(value).upper(), value) - return cls(value) - - # Frozen, so that it can be used in sets @attr.s(frozen=True, slots=True, kw_only=kw_only) class Image: diff --git a/fbchat/_group.py b/fbchat/_group.py index 4c7c8c1..d548b89 100644 --- a/fbchat/_group.py +++ b/fbchat/_group.py @@ -150,7 +150,7 @@ class GroupData(Group): participants = attr.ib(factory=set) #: A dictionary, containing user nicknames mapped to their IDs nicknames = attr.ib(factory=dict) - #: A `ThreadColor`. The groups's message color + #: The groups's message color color = attr.ib(None) #: The groups's default emoji emoji = attr.ib(None) @@ -189,8 +189,8 @@ class GroupData(Group): ] ), nicknames=c_info.get("nicknames"), - color=c_info.get("color"), - emoji=c_info.get("emoji"), + color=c_info["color"], + emoji=c_info["emoji"], admins=set([node.get("id") for node in data.get("thread_admins")]), approval_mode=bool(data.get("approval_mode")) if data.get("approval_mode") is not None diff --git a/fbchat/_message.py b/fbchat/_message.py index f7e273d..6cefdc8 100644 --- a/fbchat/_message.py +++ b/fbchat/_message.py @@ -1,11 +1,12 @@ import attr +import enum from string import Formatter -from ._core import log, attrs_default, Enum +from ._core import log, attrs_default from . import _util, _session, _attachment, _location, _file, _quick_reply, _sticker from typing import Optional -class EmojiSize(Enum): +class EmojiSize(enum.Enum): """Used to specify the size of a sent emoji.""" LARGE = "369239383222810" @@ -29,19 +30,6 @@ class EmojiSize(Enum): return None -class MessageReaction(Enum): - """Used to specify a message reaction.""" - - HEART = "❤" - LOVE = "😍" - SMILE = "😆" - WOW = "😮" - SAD = "😢" - ANGRY = "😠" - YES = "👍" - NO = "👎" - - @attrs_default class Mention: """Represents a ``@mention``.""" @@ -74,6 +62,9 @@ class Mention: } +SENDABLE_REACTIONS = ("❤", "😍", "😆", "😮", "😢", "😠", "👍", "👎") + + @attrs_default class Message: """Represents a Facebook message.""" @@ -93,18 +84,26 @@ class Message: data = {"message_id": self.id} j = self.session._payload_post("/messaging/unsend_message/?dpr=1", data) - def react(self, reaction: Optional[MessageReaction]): + def react(self, reaction: Optional[str]): """React to the message, or removes reaction. + Currently, you can use "❤", "😍", "😆", "😮", "😢", "😠", "👍" or "👎". It + should be possible to add support for more, but we haven't figured that out yet. + Args: - reaction: Reaction emoji to use, if ``None`` removes reaction + reaction: Reaction emoji to use, or if ``None``, removes reaction. """ + if reaction and reaction not in SENDABLE_REACTIONS: + raise ValueError( + "Invalid reaction! Please use one of: {}".format(SENDABLE_REACTIONS) + ) + data = { "action": "ADD_REACTION" if reaction else "REMOVE_REACTION", "client_mutation_id": "1", "actor_id": self.session.user_id, "message_id": self.id, - "reaction": reaction.value if reaction else None, + "reaction": reaction, } data = { "doc_id": 1491398900900362, @@ -190,7 +189,7 @@ class MessageData(Message): is_read = attr.ib(None) #: A list of people IDs who read the message, works only with `Client.fetch_thread_messages` read_by = attr.ib(factory=list) - #: A dictionary with user's IDs as keys, and their `MessageReaction` as values + #: A dictionary with user's IDs as keys, and their reaction as values reactions = attr.ib(factory=dict) #: A `Sticker` sticker = attr.ib(None) @@ -266,8 +265,7 @@ class MessageData(Message): if _util.millis_to_datetime(int(receipt["watermark"])) >= created_at ], reactions={ - str(r["user"]["id"]): MessageReaction._extend_if_invalid(r["reaction"]) - for r in data["message_reactions"] + str(r["user"]["id"]): r["reaction"] for r in data["message_reactions"] }, sticker=_sticker.Sticker._from_graphql(data.get("sticker")), attachments=attachments, diff --git a/fbchat/_plan.py b/fbchat/_plan.py index 9eff6ac..3cfbf89 100644 --- a/fbchat/_plan.py +++ b/fbchat/_plan.py @@ -1,10 +1,11 @@ import attr import datetime -from ._core import attrs_default, Enum +import enum +from ._core import attrs_default from . import _exception, _util, _session -class GuestStatus(Enum): +class GuestStatus(enum.Enum): INVITED = 1 GOING = 2 DECLINED = 3 diff --git a/fbchat/_thread.py b/fbchat/_thread.py index 34475db..628cb3e 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -2,12 +2,13 @@ import abc import attr import collections import datetime -from ._core import attrs_default, Enum, Image +import enum +from ._core import attrs_default, Image from . import _util, _exception, _session, _graphql, _attachment, _file, _plan -from typing import MutableMapping, Any, Iterable, Tuple +from typing import MutableMapping, Any, Iterable, Tuple, Optional -class ThreadLocation(Enum): +class ThreadLocation(enum.Enum): """Used to specify where a thread is located (inbox, pending, archived, other).""" INBOX = "INBOX" @@ -16,39 +17,29 @@ class ThreadLocation(Enum): OTHER = "OTHER" -class ThreadColor(Enum): - """Used to specify a thread colors.""" - - MESSENGER_BLUE = "#0084ff" - VIKING = "#44bec7" - GOLDEN_POPPY = "#ffc300" - RADICAL_RED = "#fa3c4c" - SHOCKING = "#d696bb" - PICTON_BLUE = "#6699cc" - FREE_SPEECH_GREEN = "#13cf13" - PUMPKIN = "#ff7e29" - LIGHT_CORAL = "#e68585" - MEDIUM_SLATE_BLUE = "#7646ff" - DEEP_SKY_BLUE = "#20cef5" - FERN = "#67b868" - CAMEO = "#d4a88c" - BRILLIANT_ROSE = "#ff5ca1" - BILOBA_FLOWER = "#a695c7" - TICKLE_ME_PINK = "#ff7ca8" - MALACHITE = "#1adb5b" - RUBY = "#f01d6a" - DARK_TANGERINE = "#ff9c19" - BRIGHT_TURQUOISE = "#0edcde" - - @classmethod - def _from_graphql(cls, color): - if color is None: - return None - if not color: - return cls.MESSENGER_BLUE - color = color[2:] # Strip the alpha value - value = "#{}".format(color.lower()) - return cls._extend_if_invalid(value) +DEFAULT_COLOR = "#0084ff" +SETABLE_COLORS = ( + DEFAULT_COLOR, + "#44bec7", + "#ffc300", + "#fa3c4c", + "#d696bb", + "#6699cc", + "#13cf13", + "#ff7e29", + "#e68585", + "#7646ff", + "#20cef5", + "#67b868", + "#d4a88c", + "#ff5ca1", + "#a695c7", + "#ff7ca8", + "#1adb5b", + "#f01d6a", + "#ff9c19", + "#0edcde", +) class ThreadABC(metaclass=abc.ABCMeta): @@ -375,22 +366,50 @@ class ThreadABC(metaclass=abc.ABCMeta): "/messaging/save_thread_nickname/?source=thread_settings&dpr=1", data ) - def set_color(self, color: ThreadColor): + def set_color(self, color: str): """Change thread color. + The new color must be one of the following: + + "#0084ff", "#44bec7", "#ffc300", "#fa3c4c", "#d696bb", "#6699cc", "#13cf13", + "#ff7e29", "#e68585", "#7646ff", "#20cef5", "#67b868", "#d4a88c", "#ff5ca1", + "#a695c7", "#ff7ca8", "#1adb5b", "#f01d6a", "#ff9c19" or "#0edcde". + + The default is "#0084ff". + + This list is subject to change in the future! + Args: color: New thread color """ - data = { - "color_choice": color.value if color != ThreadColor.MESSENGER_BLUE else "", - "thread_or_other_fbid": self.id, - } + if color not in SETABLE_COLORS: + raise ValueError( + "Invalid color! Please use one of: {}".format(SETABLE_COLORS) + ) + + # Set color to "" if DEFAULT_COLOR. Just how the endpoint works... + if color == DEFAULT_COLOR: + color = "" + + data = {"color_choice": color, "thread_or_other_fbid": self.id} j = self.session._payload_post( "/messaging/save_thread_color/?source=thread_settings&dpr=1", data ) + # def set_theme(self, theme_id: str): + # data = { + # "client_mutation_id": "0", + # "actor_id": self.session.user_id, + # "thread_id": self.id, + # "theme_id": theme_id, + # "source": "SETTINGS", + # } + # j = self.session._graphql_requests( + # _graphql.from_doc_id("1768656253222505", {"data": data}) + # ) + def set_emoji(self, emoji: str): - """Change thread color. + """Change thread emoji. Args: emoji: New thread emoji @@ -541,15 +560,22 @@ class ThreadABC(metaclass=abc.ABCMeta): ) return j + @staticmethod + def _parse_color(inp: Optional[str]) -> str: + if not inp: + return DEFAULT_COLOR + # Strip the alpha value, and lower the string + return "#{}".format(inp[2:].lower()) + @staticmethod def _parse_customization_info(data: Any) -> MutableMapping[str, Any]: - if data is None or data.get("customization_info") is None: - return {} + if not data or not data.get("customization_info"): + return {"emoji": None, "color": DEFAULT_COLOR} info = data["customization_info"] rtn = { "emoji": info.get("emoji"), - "color": ThreadColor._from_graphql(info.get("outgoing_bubble_color")), + "color": ThreadABC._parse_color(info.get("outgoing_bubble_color")), } if ( data.get("thread_type") == "GROUP" diff --git a/fbchat/_user.py b/fbchat/_user.py index ef26967..081cead 100644 --- a/fbchat/_user.py +++ b/fbchat/_user.py @@ -1,5 +1,5 @@ import attr -from ._core import log, attrs_default, Enum, Image +from ._core import log, attrs_default, Image from . import _util, _session, _plan, _thread @@ -33,13 +33,6 @@ GENDERS = { } -class TypingStatus(Enum): - """Used to specify whether the user is typing or has stopped typing.""" - - STOPPED = 0 - TYPING = 1 - - @attrs_default class User(_thread.ThreadABC): """Represents a Facebook user. Implements `ThreadABC`.""" @@ -110,7 +103,7 @@ class UserData(User): nickname = attr.ib(None) #: The clients nickname, as seen by the user own_nickname = attr.ib(None) - #: A `ThreadColor`. The message color + #: The message color color = attr.ib(None) #: The default emoji emoji = attr.ib(None) @@ -136,8 +129,8 @@ class UserData(User): gender=GENDERS.get(data["gender"]), affinity=data.get("viewer_affinity"), nickname=c_info.get("nickname"), - color=c_info.get("color"), - emoji=c_info.get("emoji"), + color=c_info["color"], + emoji=c_info["emoji"], own_nickname=c_info.get("own_nickname"), photo=Image._from_uri(data["profile_picture"]), name=data["name"], @@ -186,8 +179,8 @@ class UserData(User): is_friend=user["is_viewer_friend"], gender=GENDERS.get(user["gender"]), nickname=c_info.get("nickname"), - color=c_info.get("color"), - emoji=c_info.get("emoji"), + color=c_info["color"], + emoji=c_info["emoji"], own_nickname=c_info.get("own_nickname"), photo=Image._from_uri(user["big_image_src"]), message_count=data["messages_count"], diff --git a/pyproject.toml b/pyproject.toml index f586ea2..50c5ea0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ maintainer = "Mads Marquart" maintainer-email = "madsmtm@gmail.com" home-page = "https://github.com/carpedm20/fbchat/" requires = [ - "aenum~=2.0", "attrs>=19.1", "requests~=2.19", "beautifulsoup4~=4.0", diff --git a/tests/test_core.py b/tests/test_core.py deleted file mode 100644 index a35404c..0000000 --- a/tests/test_core.py +++ /dev/null @@ -1,14 +0,0 @@ -import pytest -from fbchat._core import Enum - - -@pytest.mark.filterwarnings("ignore::DeprecationWarning") -def test_enum_extend_if_invalid(): - class TestEnum(Enum): - A = 1 - B = 2 - - assert TestEnum._extend_if_invalid(1) == TestEnum.A - assert TestEnum._extend_if_invalid(3) == TestEnum.UNKNOWN_3 - assert TestEnum._extend_if_invalid(3) == TestEnum.UNKNOWN_3 - assert TestEnum(3) == TestEnum.UNKNOWN_3 diff --git a/tests/test_group.py b/tests/test_group.py index 5fc7135..ebdfed9 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -35,7 +35,7 @@ def test_group_from_graphql(session): plan=None, participants={"1234", "2345", "3456"}, nicknames={}, - color=None, + color="#0084ff", emoji="😀", admins={"1234"}, approval_mode=False, diff --git a/tests/test_message_management.py b/tests/test_message_management.py index 9db09a4..66440b4 100644 --- a/tests/test_message_management.py +++ b/tests/test_message_management.py @@ -1,16 +1,11 @@ import pytest -from fbchat import Message, MessageReaction +from fbchat import Message from utils import subset pytestmark = pytest.mark.online -def test_set_reaction(client): - mid = client.send(Message(text="This message will be reacted to")) - client.react_to_message(mid, MessageReaction.LOVE) - - def test_delete_messages(client): text1 = "This message will stay" text2 = "This message will be removed" diff --git a/tests/test_thread.py b/tests/test_thread.py index 6fc597c..344b42e 100644 --- a/tests/test_thread.py +++ b/tests/test_thread.py @@ -1,20 +1,19 @@ import pytest import fbchat -from fbchat import ThreadColor, ThreadABC, Thread +from fbchat import ThreadABC, Thread -def test_thread_color_from_graphql(): - assert None is ThreadColor._from_graphql(None) - assert ThreadColor.MESSENGER_BLUE is ThreadColor._from_graphql("") - assert ThreadColor.VIKING is ThreadColor._from_graphql("FF44BEC7") - assert ThreadColor._from_graphql("DEADBEEF") is getattr( - ThreadColor, "UNKNOWN_#ADBEEF" - ) +def test_parse_color(): + assert "#0084ff" == ThreadABC._parse_color(None) + assert "#0084ff" == ThreadABC._parse_color("") + assert "#44bec7" == ThreadABC._parse_color("FF44BEC7") + assert "#adbeef" == ThreadABC._parse_color("DEADBEEF") def test_thread_parse_customization_info_empty(): - assert {} == ThreadABC._parse_customization_info(None) - assert {} == ThreadABC._parse_customization_info({"customization_info": None}) + default = {"color": "#0084ff", "emoji": None} + assert default == ThreadABC._parse_customization_info(None) + assert default == ThreadABC._parse_customization_info({"customization_info": None}) def test_thread_parse_customization_info_group(): @@ -34,7 +33,7 @@ def test_thread_parse_customization_info_group(): } expected = { "emoji": "🎉", - "color": ThreadColor.BRILLIANT_ROSE, + "color": "#ff5ca1", "nicknames": {"123456789": "A", "987654321": "B"}, } assert expected == ThreadABC._parse_customization_info(data) @@ -55,7 +54,7 @@ def test_thread_parse_customization_info_user(): "thread_type": "ONE_TO_ONE", # ... Other irrelevant fields } - expected = {"emoji": None, "color": None, "own_nickname": "A", "nickname": "B"} + expected = {"emoji": None, "color": "#0084ff", "own_nickname": "A", "nickname": "B"} assert expected == ThreadABC._parse_customization_info(data) diff --git a/tests/test_thread_interraction.py b/tests/test_thread_interraction.py index df05fdb..babd376 100644 --- a/tests/test_thread_interraction.py +++ b/tests/test_thread_interraction.py @@ -1,6 +1,6 @@ import pytest -from fbchat import Message, FBchatFacebookError, TypingStatus, ThreadColor +from fbchat import Message, FBchatFacebookError from utils import random_hex, subset from os import path @@ -91,14 +91,10 @@ def test_change_image_remote(client1, group, catch_event): ) -@pytest.mark.parametrize( - "color", - [x for x in ThreadColor if x in [ThreadColor.MESSENGER_BLUE, ThreadColor.PUMPKIN]], -) -def test_change_color(client, catch_event, compare, color): +def test_change_color(client, catch_event, compare): with catch_event("on_color_change") as x: - client.change_thread_color(color) - assert compare(x, new_color=color) + client.change_thread_color("#44bec7") + assert compare(x, new_color="#44bec7") @pytest.mark.xfail(raises=FBchatFacebookError, reason="Should fail, but doesn't") @@ -109,7 +105,7 @@ def test_change_color_invalid(client): client.change_thread_color(InvalidColor()) -@pytest.mark.parametrize("status", TypingStatus) +@pytest.mark.parametrize("status", [True, False]) def test_typing_status(client, catch_event, compare, status): with catch_event("on_typing") as x: client.set_typing_status(status) diff --git a/tests/test_user.py b/tests/test_user.py index 3685c24..a26eedd 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -27,6 +27,7 @@ def test_user_from_graphql(session): is_friend=True, gender="female_singular", affinity=0.4560002, + color="#0084ff", ) == UserData._from_graphql(session, data) @@ -152,7 +153,7 @@ def test_user_from_thread_fetch(session): gender="female_singular", nickname="A", own_nickname="B", - color=None, + color="#0084ff", emoji=None, ) == UserData._from_thread_fetch(session, data)