Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3c07e42ba2 | ||
|
2cd6376818 | ||
|
5e7f7750de | ||
|
2a223ec6db | ||
|
a99108fff6 | ||
|
8de4698cc4 | ||
|
637319ec2c | ||
|
f9398564cd | ||
|
b57f423eb4 | ||
|
3093f1f2b6 | ||
|
961777e0c1 | ||
|
d7139701f7 | ||
|
c6bac17d48 | ||
|
3638fc5356 | ||
|
aca9176f7f | ||
|
0d5e4f6d3f | ||
|
92a5ffdef8 | ||
|
b3359fccdb | ||
|
d8f7366d1f | ||
|
ff94dc20af | ||
|
a8df0a548f | ||
|
13d0dc4ba4 | ||
|
64125a1aca | ||
|
4feae03092 | ||
|
5f993c2bf8 | ||
|
35bbcbffba | ||
|
5faca54d67 | ||
|
82496b8e04 | ||
|
2d74ec7823 |
@@ -17,7 +17,7 @@ from .client import *
|
||||
|
||||
|
||||
__copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year)
|
||||
__version__ = '1.0.4'
|
||||
__version__ = '1.0.16'
|
||||
__license__ = 'BSD'
|
||||
__author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart'
|
||||
__email__ = 'carpedm20@gmail.com'
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import unicode_literals
|
||||
import requests
|
||||
import urllib
|
||||
import traceback
|
||||
from uuid import uuid1
|
||||
from random import choice
|
||||
from datetime import datetime
|
||||
@@ -72,6 +71,9 @@ class Client(object):
|
||||
# If session cookies aren't set, not properly loaded or gives us an invalid session, then do the login
|
||||
if not session_cookies or not self.setSession(session_cookies) or not self.isLoggedIn():
|
||||
self.login(email, password, max_tries)
|
||||
else:
|
||||
self.email = email
|
||||
self.password = password
|
||||
|
||||
"""
|
||||
INTERNAL REQUEST METHODS
|
||||
@@ -202,7 +204,8 @@ class Client(object):
|
||||
r = self._cleanPost(ReqUrl.LOGIN, data)
|
||||
|
||||
# Usually, 'Checkpoint' will refer to 2FA
|
||||
if 'checkpoint' in r.url and 'Enter Security Code to Continue' in r.text:
|
||||
if ('checkpoint' in r.url and
|
||||
('Enter Security Code to Continue' in r.text or 'Enter Login Code to Continue' in r.text)):
|
||||
r = self._2FA(r)
|
||||
|
||||
# Sometimes Facebook tries to show the user a "Save Device" dialog
|
||||
@@ -427,6 +430,9 @@ class Client(object):
|
||||
for key in j['payload']:
|
||||
k = j['payload'][key]
|
||||
if k['type'] in ['user', 'friend']:
|
||||
if k['id'] in ['0', 0]:
|
||||
# Skip invalid users
|
||||
pass
|
||||
users.append(User(k['id'], first_name=k.get('firstName'), url=k.get('uri'), photo=k.get('thumbSrc'), name=k.get('name'), is_friend=k.get('is_friend'), gender=GENDERS[k.get('gender')]))
|
||||
|
||||
return users
|
||||
@@ -736,7 +742,7 @@ class Client(object):
|
||||
raise Exception('A thread was not in participants: {}'.format(j['payload']))
|
||||
entries.append(participants[k['other_user_fbid']])
|
||||
elif k['thread_type'] == 2:
|
||||
entries.append(Group(k['thread_fbid'], participants=[p.strip('fbid:') for p in k['participants']], photo=k['image_src'], name=k['name']))
|
||||
entries.append(Group(k['thread_fbid'], participants=set([p.strip('fbid:') for p in k['participants']]), photo=k['image_src'], name=k['name']))
|
||||
else:
|
||||
raise Exception('A thread had an unknown thread type: {}'.format(k))
|
||||
|
||||
@@ -825,6 +831,13 @@ class Client(object):
|
||||
except (KeyError, IndexError) as e:
|
||||
raise Exception('Error when sending message: No message IDs could be found: {}'.format(j))
|
||||
|
||||
# update JS token if receive from response
|
||||
if ('jsmods' in j) and ('require' in j['jsmods']):
|
||||
try:
|
||||
self.payloadDefault['fb_dtsg'] = j['jsmods']['require'][0][3][0]
|
||||
except (KeyError, IndexError) as e:
|
||||
log.warning("Error when update fb_dtsg. Facebook might have changed protocol.")
|
||||
|
||||
return message_id
|
||||
|
||||
def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER):
|
||||
@@ -937,7 +950,7 @@ class Client(object):
|
||||
"""
|
||||
Sends a local image to a thread
|
||||
|
||||
:param image_path: URL of an image to upload and send
|
||||
:param image_path: Path of an image to upload and send
|
||||
:param message: Additional message
|
||||
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
|
||||
:param thread_type: See :ref:`intro_threads`
|
||||
@@ -1009,6 +1022,7 @@ class Client(object):
|
||||
:type thread_type: models.ThreadType
|
||||
:raises: Exception if request failed
|
||||
"""
|
||||
|
||||
thread_id, thread_type = self._getThread(thread_id, thread_type)
|
||||
|
||||
if thread_type == ThreadType.USER:
|
||||
@@ -1021,6 +1035,8 @@ class Client(object):
|
||||
data['log_message_data[name]'] = title
|
||||
data['log_message_type'] = 'log:thread-name'
|
||||
|
||||
return self._doSendRequest(data)
|
||||
|
||||
def changeNickname(self, nickname, user_id, thread_id=None, thread_type=ThreadType.USER):
|
||||
"""
|
||||
Changes the nickname of a user in a thread
|
||||
@@ -1273,7 +1289,6 @@ class Client(object):
|
||||
thread_id = str(metadata['threadKey']['threadFbId'])
|
||||
self.onPeopleAdded(mid=mid, added_ids=added_ids, author_id=author_id, thread_id=thread_id,
|
||||
ts=ts, msg=m)
|
||||
continue
|
||||
|
||||
# Left/removed participants
|
||||
elif 'leftParticipantFbId' in delta:
|
||||
@@ -1281,15 +1296,13 @@ class Client(object):
|
||||
thread_id = str(metadata['threadKey']['threadFbId'])
|
||||
self.onPersonRemoved(mid=mid, removed_id=removed_id, author_id=author_id, thread_id=thread_id,
|
||||
ts=ts, msg=m)
|
||||
continue
|
||||
|
||||
# Color change
|
||||
elif delta_type == "change_thread_theme":
|
||||
new_color = ThreadColor(delta["untypedData"]["theme_color"])
|
||||
new_color = graphql_color_to_enum(delta["untypedData"]["theme_color"])
|
||||
thread_id, thread_type = getThreadIdAndThreadType(metadata)
|
||||
self.onColorChange(mid=mid, author_id=author_id, new_color=new_color, thread_id=thread_id,
|
||||
thread_type=thread_type, ts=ts, metadata=metadata, msg=m)
|
||||
continue
|
||||
|
||||
# Emoji change
|
||||
elif delta_type == "change_thread_icon":
|
||||
@@ -1297,7 +1310,6 @@ class Client(object):
|
||||
thread_id, thread_type = getThreadIdAndThreadType(metadata)
|
||||
self.onEmojiChange(mid=mid, author_id=author_id, new_emoji=new_emoji, thread_id=thread_id,
|
||||
thread_type=thread_type, ts=ts, metadata=metadata, msg=m)
|
||||
continue
|
||||
|
||||
# Thread title change
|
||||
elif delta.get("class") == "ThreadName":
|
||||
@@ -1305,7 +1317,6 @@ class Client(object):
|
||||
thread_id, thread_type = getThreadIdAndThreadType(metadata)
|
||||
self.onTitleChange(mid=mid, author_id=author_id, new_title=new_title, thread_id=thread_id,
|
||||
thread_type=thread_type, ts=ts, metadata=metadata, msg=m)
|
||||
continue
|
||||
|
||||
# Nickname change
|
||||
elif delta_type == "change_thread_nickname":
|
||||
@@ -1315,7 +1326,6 @@ class Client(object):
|
||||
self.onNicknameChange(mid=mid, author_id=author_id, changed_for=changed_for,
|
||||
new_nickname=new_nickname,
|
||||
thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m)
|
||||
continue
|
||||
|
||||
# Message delivered
|
||||
elif delta.get("class") == "DeliveryReceipt":
|
||||
@@ -1325,7 +1335,6 @@ class Client(object):
|
||||
thread_id, thread_type = getThreadIdAndThreadType(delta)
|
||||
self.onMessageDelivered(msg_ids=message_ids, delivered_for=delivered_for,
|
||||
thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m)
|
||||
continue
|
||||
|
||||
# Message seen
|
||||
elif delta.get("class") == "ReadReceipt":
|
||||
@@ -1335,7 +1344,6 @@ class Client(object):
|
||||
thread_id, thread_type = getThreadIdAndThreadType(delta)
|
||||
self.onMessageSeen(seen_by=seen_by, thread_id=thread_id, thread_type=thread_type,
|
||||
seen_ts=seen_ts, ts=delivered_ts, metadata=metadata, msg=m)
|
||||
continue
|
||||
|
||||
# Messages marked as seen
|
||||
elif delta.get("class") == "MarkRead":
|
||||
@@ -1347,19 +1355,21 @@ class Client(object):
|
||||
threads = [getThreadIdAndThreadType({"threadKey": thr}) for thr in delta.get("threadKeys")]
|
||||
|
||||
# thread_id, thread_type = getThreadIdAndThreadType(delta)
|
||||
self.onMarkedSeen(threads=threads, seen_ts=seen_ts, delivered_ts=delivered_ts, metadata=delta, msg=m)
|
||||
continue
|
||||
self.onMarkedSeen(threads=threads, seen_ts=seen_ts, ts=delivered_ts, metadata=delta, msg=m)
|
||||
|
||||
# New message
|
||||
elif delta.get("class") == "NewMessage":
|
||||
message = delta.get('body', '')
|
||||
thread_id, thread_type = getThreadIdAndThreadType(metadata)
|
||||
self.onMessage(mid=mid, author_id=author_id, message=message,
|
||||
thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=m, msg=m)
|
||||
continue
|
||||
thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m)
|
||||
|
||||
# Unknown message type
|
||||
else:
|
||||
self.onUnknownMesssageType(msg=m)
|
||||
|
||||
# Inbox
|
||||
if mtype == "inbox":
|
||||
elif mtype == "inbox":
|
||||
self.onInbox(unseen=m["unseen"], unread=m["unread"], recent_unread=m["recent_unread"], msg=m)
|
||||
|
||||
# Typing
|
||||
@@ -1387,6 +1397,14 @@ class Client(object):
|
||||
elif mtype == "deltaflow":
|
||||
pass
|
||||
|
||||
# Chat timestamp
|
||||
elif mtype == "chatproxy-presence":
|
||||
buddylist = {}
|
||||
for _id in m.get('buddyList', {}):
|
||||
payload = m['buddyList'][_id]
|
||||
buddylist[_id] = payload.get('lat')
|
||||
self.onChatTimestamp(buddylist=buddylist, msg=m)
|
||||
|
||||
# Unknown message type
|
||||
else:
|
||||
self.onUnknownMesssageType(msg=m)
|
||||
@@ -1487,7 +1505,7 @@ class Client(object):
|
||||
|
||||
:param exception: The exception that was encountered
|
||||
"""
|
||||
traceback.print_exc()
|
||||
log.exception('Got exception while listening')
|
||||
|
||||
|
||||
def onMessage(self, mid=None, author_id=None, message=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg={}):
|
||||
@@ -1674,6 +1692,14 @@ class Client(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
def onChatTimestamp(self, buddylist={}, msg={}):
|
||||
"""
|
||||
Called when the client receives chat online presence update
|
||||
|
||||
:param buddylist: A list of dicts with friend id and last seen timestamp
|
||||
:param msg: A full set of the data recieved
|
||||
"""
|
||||
log.debug('Chat Timestamps received: {}'.format(buddylist))
|
||||
|
||||
def onUnknownMesssageType(self, msg={}):
|
||||
"""
|
||||
|
@@ -26,9 +26,11 @@ class ConcatJSONDecoder(json.JSONDecoder):
|
||||
def graphql_color_to_enum(color):
|
||||
if color is None:
|
||||
return None
|
||||
if len(color) == 0:
|
||||
return ThreadColor.MESSENGER_BLUE
|
||||
try:
|
||||
return ThreadColor('#{}'.format(color[2:].lower()))
|
||||
except KeyError:
|
||||
except ValueError:
|
||||
raise Exception('Could not get ThreadColor from color: {}'.format(color))
|
||||
|
||||
def get_customization_info(thread):
|
||||
@@ -42,16 +44,18 @@ def get_customization_info(thread):
|
||||
}
|
||||
if thread.get('thread_type') == 'GROUP' or thread.get('is_group_thread') or thread.get('thread_key', {}).get('thread_fbid'):
|
||||
rtn['nicknames'] = {}
|
||||
for k in info['participant_customizations']:
|
||||
for k in info.get('participant_customizations', []):
|
||||
rtn['nicknames'][k['participant_id']] = k.get('nickname')
|
||||
else:
|
||||
elif info.get('participant_customizations'):
|
||||
_id = thread.get('thread_key', {}).get('other_user_id') or thread.get('id')
|
||||
if info['participant_customizations'][0]['participant_id'] == _id:
|
||||
if len(info['participant_customizations']) > 0 and info['participant_customizations'][0]['participant_id'] == _id:
|
||||
rtn['nickname'] = info['participant_customizations'][0]
|
||||
rtn['own_nickname'] = info['participant_customizations'][1]
|
||||
elif info['participant_customizations'][1]['participant_id'] == _id:
|
||||
if len(info['participant_customizations']) > 1:
|
||||
rtn['own_nickname'] = info['participant_customizations'][1]
|
||||
elif len(info['participant_customizations']) > 1 and info['participant_customizations'][1]['participant_id'] == _id:
|
||||
rtn['nickname'] = info['participant_customizations'][1]
|
||||
rtn['own_nickname'] = info['participant_customizations'][0]
|
||||
if len(info['participant_customizations']) > 1:
|
||||
rtn['own_nickname'] = info['participant_customizations'][0]
|
||||
else:
|
||||
raise Exception('No participant matching the user {} found: {}'.format(_id, info['participant_customizations']))
|
||||
return rtn
|
||||
@@ -73,7 +77,8 @@ def graphql_to_message(message):
|
||||
text=message.get('message').get('text'),
|
||||
mentions=[Mention(m.get('entity', {}).get('id'), offset=m.get('offset'), length=m.get('length')) for m in message.get('message').get('ranges', [])],
|
||||
sticker=message.get('sticker'),
|
||||
attachments=message.get('blob_attachments')
|
||||
attachments=message.get('blob_attachments'),
|
||||
extensible_attachment=message.get('extensible_attachment')
|
||||
)
|
||||
|
||||
def graphql_to_user(user):
|
||||
@@ -102,7 +107,7 @@ def graphql_to_group(group):
|
||||
c_info = get_customization_info(group)
|
||||
return Group(
|
||||
group['thread_key']['thread_fbid'],
|
||||
participants=[node['messaging_actor']['id'] for node in group['all_participants']['nodes']],
|
||||
participants=set([node['messaging_actor']['id'] for node in group['all_participants']['nodes']]),
|
||||
nicknames=c_info.get('nicknames'),
|
||||
color=c_info.get('color'),
|
||||
emoji=c_info.get('emoji'),
|
||||
|
@@ -13,13 +13,16 @@ class Thread(object):
|
||||
photo = str
|
||||
#: The name of the thread
|
||||
name = str
|
||||
#: Timestamp of last message
|
||||
last_message_timestamp = str
|
||||
|
||||
def __init__(self, _type, uid, photo=None, name=None):
|
||||
def __init__(self, _type, uid, photo=None, name=None, last_message_timestamp=None):
|
||||
"""Represents a Facebook thread"""
|
||||
self.uid = str(uid)
|
||||
self.type = _type
|
||||
self.photo = photo
|
||||
self.name = name
|
||||
self.last_message_timestamp = last_message_timestamp
|
||||
|
||||
def __repr__(self):
|
||||
return self.__unicode__()
|
||||
@@ -66,8 +69,8 @@ class User(Thread):
|
||||
|
||||
|
||||
class Group(Thread):
|
||||
#: List of the group thread's participant user IDs
|
||||
participants = list
|
||||
#: Unique list (set) of the group thread's participant user IDs
|
||||
participants = set
|
||||
#: Dict, containing user nicknames mapped to their IDs
|
||||
nicknames = dict
|
||||
#: A :class:`ThreadColor`. The groups's message color
|
||||
@@ -75,7 +78,7 @@ class Group(Thread):
|
||||
#: The groups's default emoji
|
||||
emoji = str
|
||||
|
||||
def __init__(self, uid, participants=[], nicknames=[], color=None, emoji=None, **kwargs):
|
||||
def __init__(self, uid, participants=set(), nicknames=[], color=None, emoji=None, **kwargs):
|
||||
"""Represents a Facebook group. Inherits `Thread`"""
|
||||
super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs)
|
||||
self.participants = participants
|
||||
@@ -125,8 +128,10 @@ class Message(object):
|
||||
sticker = str
|
||||
#: A list of attachments
|
||||
attachments = list
|
||||
#: An extensible attachment, e.g. share object
|
||||
extensible_attachment = dict
|
||||
|
||||
def __init__(self, uid, author=None, timestamp=None, is_read=None, reactions=[], text=None, mentions=[], sticker=None, attachments=[]):
|
||||
def __init__(self, uid, author=None, timestamp=None, is_read=None, reactions=[], text=None, mentions=[], sticker=None, attachments=[], extensible_attachment={}):
|
||||
"""Represents a Facebook message"""
|
||||
self.uid = uid
|
||||
self.author = author
|
||||
@@ -137,6 +142,7 @@ class Message(object):
|
||||
self.mentions = mentions
|
||||
self.sticker = sticker
|
||||
self.attachments = attachments
|
||||
self.extensible_attachment = extensible_attachment
|
||||
|
||||
|
||||
class Mention(object):
|
||||
|
@@ -1,3 +1,4 @@
|
||||
requests
|
||||
lxml
|
||||
beautifulsoup4
|
||||
enum34
|
||||
|
2
setup.py
2
setup.py
@@ -18,7 +18,7 @@ with open('README.rst') as f:
|
||||
|
||||
try:
|
||||
requirements = [line.rstrip('\n') for line in open(os.path.join('fbchat.egg-info', 'requires.txt'))]
|
||||
except FileNotFoundError:
|
||||
except IOError:
|
||||
requirements = [line.rstrip('\n') for line in open('requirements.txt')]
|
||||
|
||||
version = None
|
||||
|
Reference in New Issue
Block a user