Compare commits

...

30 Commits

Author SHA1 Message Date
Mads Marquart
c51a332560 Version up, thanks to @PythonNut 2017-08-27 23:03:50 +02:00
Mads Marquart
a73d2feed6 Merge pull request #193 from PythonNut/master
Fix UNKNOWN gender in graphql requests
2017-08-27 23:02:05 +02:00
PythonNut
6929193e9d Fix UNKNOWN gender in graphql requests 2017-08-13 23:10:11 +00:00
Mads Marquart
fea4ad9e89 Version up, Thanks to ritu99 2017-08-10 15:25:38 +02:00
Mads Marquart
68099049d4 Merge pull request #189 from ritu99/master
Added Message Count to thread information
2017-08-10 15:22:22 +02:00
Ritvik Annam
44cf08bdfd fetchThreadInfo now pulls message_count 2017-08-10 01:15:29 -05:00
Ritvik Annam
9e32cf17a4 fetchThreadList now pulls message_count 2017-08-10 00:53:06 -05:00
Mads Marquart
0661367ebb Properly fixed #182 2017-08-02 23:08:34 +02:00
Mads Marquart
3c07e42ba2 Version up, fixed #182 2017-07-26 23:13:19 +02:00
Mads Marquart
2cd6376818 Merge pull request #178 from Bankde/fix-fail-after-running-for-days
Fix issue when running for long time
2017-07-26 23:03:49 +02:00
Mads Marquart
5e7f7750de Fixed enums in python 2.7, thanks to @liamkirsh 2017-07-12 14:52:15 +02:00
Bankde@hotmail.com
2a223ec6db fix array indexing (I don't know why fb do that) 2017-07-10 10:25:23 +07:00
Mads Marquart
a99108fff6 Version up thanks to @Bankde 2017-07-09 20:55:06 +02:00
Mads Marquart
8de4698cc4 Merge pull request #174 from Bankde/fix-error-in-python2
No FileNotFoundError in py2
2017-07-09 20:53:47 +02:00
Bankde@hotmail.com
637319ec2c add token update 2017-07-10 00:51:51 +07:00
Bankde@hotmail.com
f9398564cd replace FileNotFoundError with IOError so it can work in Py2 2017-07-05 09:18:13 +07:00
Mads Marquart
b57f423eb4 Version up thanks to @aaronlewism 2017-07-01 12:48:02 +02:00
Mads Marquart
3093f1f2b6 Merge pull request #173 from aaronlewism/master
Check for alternate 2Factor page text
2017-07-01 12:46:11 +02:00
Aaron Lewis
961777e0c1 Check for alternate 2Factor page text 2017-06-29 13:21:25 -07:00
Mads Marquart
d7139701f7 Fixed typo, improved formatting. Thanks to @JarbasAI! 2017-06-29 20:04:01 +02:00
Mads Marquart
c6bac17d48 Merge pull request #172 from JarbasAI/patch-1
Add on chat presence event
2017-06-29 19:55:49 +02:00
Mads Marquart
3638fc5356 Made fetchThreadInfo able to fetch own user's info 2017-06-29 19:53:29 +02:00
Jarbas
aca9176f7f Add on chat presence event
Last_seen time stamps were handled in unknown message type, this info is freely available and potentially useful
2017-06-29 17:56:14 +01:00
Mads Marquart
0d5e4f6d3f Version up thanks to @enwar3 2017-06-29 16:03:55 +02:00
Mads Marquart
92a5ffdef8 Merge pull request #170 from OMGWINNING/master
Add extensible_attachment field to Message for fb share objects
2017-06-29 16:02:27 +02:00
Joe Lau
b3359fccdb Add last_message_timestamp to Thread objects 2017-06-28 18:08:45 -07:00
Joe Lau
d8f7366d1f Add extensible_attachment field to Message for fb share objects 2017-06-28 13:19:17 -07:00
Mads Marquart
ff94dc20af Minor cleanup 2017-06-28 16:06:13 +02:00
Mads Marquart
a8df0a548f Minor fixes 2017-06-28 14:42:11 +02:00
Mads Marquart
13d0dc4ba4 Fixed ChangeThreadTitle and ThreadColor.MESSENGER_BLUE 2017-06-28 14:30:29 +02:00
7 changed files with 81 additions and 39 deletions

View File

@@ -17,7 +17,7 @@ from .client import *
__copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year) __copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year)
__version__ = '1.0.6' __version__ = '1.0.19'
__license__ = 'BSD' __license__ = 'BSD'
__author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart' __author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart'
__email__ = 'carpedm20@gmail.com' __email__ = 'carpedm20@gmail.com'

View File

@@ -3,7 +3,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import requests import requests
import urllib import urllib
import traceback
from uuid import uuid1 from uuid import uuid1
from random import choice from random import choice
from datetime import datetime 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 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(): if not session_cookies or not self.setSession(session_cookies) or not self.isLoggedIn():
self.login(email, password, max_tries) self.login(email, password, max_tries)
else:
self.email = email
self.password = password
""" """
INTERNAL REQUEST METHODS INTERNAL REQUEST METHODS
@@ -202,7 +204,8 @@ class Client(object):
r = self._cleanPost(ReqUrl.LOGIN, data) r = self._cleanPost(ReqUrl.LOGIN, data)
# Usually, 'Checkpoint' will refer to 2FA # 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) r = self._2FA(r)
# Sometimes Facebook tries to show the user a "Save Device" dialog # Sometimes Facebook tries to show the user a "Save Device" dialog
@@ -737,9 +740,10 @@ class Client(object):
if k['thread_type'] == 1: if k['thread_type'] == 1:
if k['other_user_fbid'] not in participants: if k['other_user_fbid'] not in participants:
raise Exception('A thread was not in participants: {}'.format(j['payload'])) raise Exception('A thread was not in participants: {}'.format(j['payload']))
participants[k['other_user_fbid']].message_count = k['message_count']
entries.append(participants[k['other_user_fbid']]) entries.append(participants[k['other_user_fbid']])
elif k['thread_type'] == 2: elif k['thread_type'] == 2:
entries.append(Group(k['thread_fbid'], participants=set([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'], message_count=k['message_count']))
else: else:
raise Exception('A thread had an unknown thread type: {}'.format(k)) raise Exception('A thread had an unknown thread type: {}'.format(k))
@@ -828,6 +832,13 @@ class Client(object):
except (KeyError, IndexError) as e: except (KeyError, IndexError) as e:
raise Exception('Error when sending message: No message IDs could be found: {}'.format(j)) 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 return message_id
def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER): def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER):
@@ -940,7 +951,7 @@ class Client(object):
""" """
Sends a local image to a thread 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 message: Additional message
:param thread_id: User/Group ID to send to. See :ref:`intro_threads` :param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads` :param thread_type: See :ref:`intro_threads`
@@ -1012,6 +1023,7 @@ class Client(object):
:type thread_type: models.ThreadType :type thread_type: models.ThreadType
:raises: Exception if request failed :raises: Exception if request failed
""" """
thread_id, thread_type = self._getThread(thread_id, thread_type) thread_id, thread_type = self._getThread(thread_id, thread_type)
if thread_type == ThreadType.USER: if thread_type == ThreadType.USER:
@@ -1024,6 +1036,8 @@ class Client(object):
data['log_message_data[name]'] = title data['log_message_data[name]'] = title
data['log_message_type'] = 'log:thread-name' data['log_message_type'] = 'log:thread-name'
return self._doSendRequest(data)
def changeNickname(self, nickname, user_id, thread_id=None, thread_type=ThreadType.USER): def changeNickname(self, nickname, user_id, thread_id=None, thread_type=ThreadType.USER):
""" """
Changes the nickname of a user in a thread Changes the nickname of a user in a thread
@@ -1276,7 +1290,6 @@ class Client(object):
thread_id = str(metadata['threadKey']['threadFbId']) thread_id = str(metadata['threadKey']['threadFbId'])
self.onPeopleAdded(mid=mid, added_ids=added_ids, author_id=author_id, thread_id=thread_id, self.onPeopleAdded(mid=mid, added_ids=added_ids, author_id=author_id, thread_id=thread_id,
ts=ts, msg=m) ts=ts, msg=m)
continue
# Left/removed participants # Left/removed participants
elif 'leftParticipantFbId' in delta: elif 'leftParticipantFbId' in delta:
@@ -1284,7 +1297,6 @@ class Client(object):
thread_id = str(metadata['threadKey']['threadFbId']) thread_id = str(metadata['threadKey']['threadFbId'])
self.onPersonRemoved(mid=mid, removed_id=removed_id, author_id=author_id, thread_id=thread_id, self.onPersonRemoved(mid=mid, removed_id=removed_id, author_id=author_id, thread_id=thread_id,
ts=ts, msg=m) ts=ts, msg=m)
continue
# Color change # Color change
elif delta_type == "change_thread_theme": elif delta_type == "change_thread_theme":
@@ -1292,7 +1304,6 @@ class Client(object):
thread_id, thread_type = getThreadIdAndThreadType(metadata) thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onColorChange(mid=mid, author_id=author_id, new_color=new_color, thread_id=thread_id, 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) thread_type=thread_type, ts=ts, metadata=metadata, msg=m)
continue
# Emoji change # Emoji change
elif delta_type == "change_thread_icon": elif delta_type == "change_thread_icon":
@@ -1300,7 +1311,6 @@ class Client(object):
thread_id, thread_type = getThreadIdAndThreadType(metadata) thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onEmojiChange(mid=mid, author_id=author_id, new_emoji=new_emoji, thread_id=thread_id, 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) thread_type=thread_type, ts=ts, metadata=metadata, msg=m)
continue
# Thread title change # Thread title change
elif delta.get("class") == "ThreadName": elif delta.get("class") == "ThreadName":
@@ -1308,7 +1318,6 @@ class Client(object):
thread_id, thread_type = getThreadIdAndThreadType(metadata) thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onTitleChange(mid=mid, author_id=author_id, new_title=new_title, thread_id=thread_id, 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) thread_type=thread_type, ts=ts, metadata=metadata, msg=m)
continue
# Nickname change # Nickname change
elif delta_type == "change_thread_nickname": elif delta_type == "change_thread_nickname":
@@ -1318,7 +1327,6 @@ class Client(object):
self.onNicknameChange(mid=mid, author_id=author_id, changed_for=changed_for, self.onNicknameChange(mid=mid, author_id=author_id, changed_for=changed_for,
new_nickname=new_nickname, new_nickname=new_nickname,
thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m) thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m)
continue
# Message delivered # Message delivered
elif delta.get("class") == "DeliveryReceipt": elif delta.get("class") == "DeliveryReceipt":
@@ -1328,7 +1336,6 @@ class Client(object):
thread_id, thread_type = getThreadIdAndThreadType(delta) thread_id, thread_type = getThreadIdAndThreadType(delta)
self.onMessageDelivered(msg_ids=message_ids, delivered_for=delivered_for, self.onMessageDelivered(msg_ids=message_ids, delivered_for=delivered_for,
thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m) thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m)
continue
# Message seen # Message seen
elif delta.get("class") == "ReadReceipt": elif delta.get("class") == "ReadReceipt":
@@ -1338,7 +1345,6 @@ class Client(object):
thread_id, thread_type = getThreadIdAndThreadType(delta) thread_id, thread_type = getThreadIdAndThreadType(delta)
self.onMessageSeen(seen_by=seen_by, thread_id=thread_id, thread_type=thread_type, 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) seen_ts=seen_ts, ts=delivered_ts, metadata=metadata, msg=m)
continue
# Messages marked as seen # Messages marked as seen
elif delta.get("class") == "MarkRead": elif delta.get("class") == "MarkRead":
@@ -1351,18 +1357,20 @@ class Client(object):
# thread_id, thread_type = getThreadIdAndThreadType(delta) # thread_id, thread_type = getThreadIdAndThreadType(delta)
self.onMarkedSeen(threads=threads, seen_ts=seen_ts, ts=delivered_ts, metadata=delta, msg=m) self.onMarkedSeen(threads=threads, seen_ts=seen_ts, ts=delivered_ts, metadata=delta, msg=m)
continue
# New message # New message
elif delta.get("class") == "NewMessage": elif delta.get("class") == "NewMessage":
message = delta.get('body', '') message = delta.get('body', '')
thread_id, thread_type = getThreadIdAndThreadType(metadata) thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onMessage(mid=mid, author_id=author_id, message=message, self.onMessage(mid=mid, author_id=author_id, message=message,
thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=m, msg=m) thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m)
continue
# Unknown message type
else:
self.onUnknownMesssageType(msg=m)
# Inbox # Inbox
if mtype == "inbox": elif mtype == "inbox":
self.onInbox(unseen=m["unseen"], unread=m["unread"], recent_unread=m["recent_unread"], msg=m) self.onInbox(unseen=m["unseen"], unread=m["unread"], recent_unread=m["recent_unread"], msg=m)
# Typing # Typing
@@ -1390,6 +1398,14 @@ class Client(object):
elif mtype == "deltaflow": elif mtype == "deltaflow":
pass 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 # Unknown message type
else: else:
self.onUnknownMesssageType(msg=m) self.onUnknownMesssageType(msg=m)
@@ -1490,7 +1506,7 @@ class Client(object):
:param exception: The exception that was encountered :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={}): def onMessage(self, mid=None, author_id=None, message=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg={}):
@@ -1677,6 +1693,14 @@ class Client(object):
""" """
pass 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={}): def onUnknownMesssageType(self, msg={}):
""" """

View File

@@ -26,9 +26,11 @@ class ConcatJSONDecoder(json.JSONDecoder):
def graphql_color_to_enum(color): def graphql_color_to_enum(color):
if color is None: if color is None:
return None return None
if len(color) == 0:
return ThreadColor.MESSENGER_BLUE
try: try:
return ThreadColor('#{}'.format(color[2:].lower())) return ThreadColor('#{}'.format(color[2:].lower()))
except KeyError: except ValueError:
raise Exception('Could not get ThreadColor from color: {}'.format(color)) raise Exception('Could not get ThreadColor from color: {}'.format(color))
def get_customization_info(thread): def get_customization_info(thread):
@@ -42,18 +44,21 @@ 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'): if thread.get('thread_type') == 'GROUP' or thread.get('is_group_thread') or thread.get('thread_key', {}).get('thread_fbid'):
rtn['nicknames'] = {} rtn['nicknames'] = {}
for k in info['participant_customizations']: for k in info.get('participant_customizations', []):
rtn['nicknames'][k['participant_id']] = k.get('nickname') rtn['nicknames'][k['participant_id']] = k.get('nickname')
elif info.get('participant_customizations'): elif info.get('participant_customizations'):
_id = thread.get('thread_key', {}).get('other_user_id') or thread.get('id') uid = thread.get('thread_key', {}).get('other_user_id') or thread.get('id')
if info['participant_customizations'][0]['participant_id'] == _id: pc = info['participant_customizations']
rtn['nickname'] = info['participant_customizations'][0] if len(pc) > 0:
rtn['own_nickname'] = info['participant_customizations'][1] if pc[0].get('participant_id') == uid:
elif info['participant_customizations'][1]['participant_id'] == _id: rtn['nickname'] = pc[0].get('nickname')
rtn['nickname'] = info['participant_customizations'][1] else:
rtn['own_nickname'] = info['participant_customizations'][0] rtn['own_nickname'] = pc[0].get('nickname')
else: if len(pc) > 1:
raise Exception('No participant matching the user {} found: {}'.format(_id, info['participant_customizations'])) if pc[1].get('participant_id') == uid:
rtn['nickname'] = pc[1].get('nickname')
else:
rtn['own_nickname'] = pc[1].get('nickname')
return rtn return rtn
def graphql_to_message(message): def graphql_to_message(message):
@@ -73,7 +78,8 @@ def graphql_to_message(message):
text=message.get('message').get('text'), 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', [])], 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'), 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): def graphql_to_user(user):
@@ -93,7 +99,8 @@ def graphql_to_user(user):
emoji=c_info.get('emoji'), emoji=c_info.get('emoji'),
own_nickname=c_info.get('own_nickname'), own_nickname=c_info.get('own_nickname'),
photo=user['profile_picture'].get('uri'), photo=user['profile_picture'].get('uri'),
name=user.get('name') name=user.get('name'),
message_count=user.get('messages_count')
) )
def graphql_to_group(group): def graphql_to_group(group):
@@ -107,7 +114,8 @@ def graphql_to_group(group):
color=c_info.get('color'), color=c_info.get('color'),
emoji=c_info.get('emoji'), emoji=c_info.get('emoji'),
photo=group['image'].get('uri'), photo=group['image'].get('uri'),
name=group.get('name') name=group.get('name'),
message_count=group.get('messages_count')
) )
def graphql_to_page(page): def graphql_to_page(page):
@@ -121,7 +129,8 @@ def graphql_to_page(page):
city=page.get('city').get('name'), city=page.get('city').get('name'),
category=page.get('category_type'), category=page.get('category_type'),
photo=page['profile_picture'].get('uri'), photo=page['profile_picture'].get('uri'),
name=page.get('name') name=page.get('name'),
message_count=page.get('messages_count')
) )
def graphql_queries_to_json(*queries): def graphql_queries_to_json(*queries):

View File

@@ -13,13 +13,18 @@ class Thread(object):
photo = str photo = str
#: The name of the thread #: The name of the thread
name = str name = str
#: Timestamp of last message
def __init__(self, _type, uid, photo=None, name=None): last_message_timestamp = str
#: Number of messages in the thread
message_count = int
def __init__(self, _type, uid, photo=None, name=None, last_message_timestamp=None, message_count=None):
"""Represents a Facebook thread""" """Represents a Facebook thread"""
self.uid = str(uid) self.uid = str(uid)
self.type = _type self.type = _type
self.photo = photo self.photo = photo
self.name = name self.name = name
self.last_message_timestamp = last_message_timestamp
self.message_count = message_count
def __repr__(self): def __repr__(self):
return self.__unicode__() return self.__unicode__()
@@ -125,8 +130,10 @@ class Message(object):
sticker = str sticker = str
#: A list of attachments #: A list of attachments
attachments = list 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""" """Represents a Facebook message"""
self.uid = uid self.uid = uid
self.author = author self.author = author
@@ -137,6 +144,7 @@ class Message(object):
self.mentions = mentions self.mentions = mentions
self.sticker = sticker self.sticker = sticker
self.attachments = attachments self.attachments = attachments
self.extensible_attachment = extensible_attachment
class Mention(object): class Mention(object):

View File

@@ -48,7 +48,7 @@ GENDERS = {
11: 'unknown_plural', 11: 'unknown_plural',
# For graphql requests # For graphql requests
#'': 'unknown', 'UNKNOWN': 'unknown',
'FEMALE': 'female_singular', 'FEMALE': 'female_singular',
'MALE': 'male_singular', 'MALE': 'male_singular',
#'': 'female_singular_guess', #'': 'female_singular_guess',

View File

@@ -1,3 +1,4 @@
requests requests
lxml lxml
beautifulsoup4 beautifulsoup4
enum34

View File

@@ -18,7 +18,7 @@ with open('README.rst') as f:
try: try:
requirements = [line.rstrip('\n') for line in open(os.path.join('fbchat.egg-info', 'requires.txt'))] 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')] requirements = [line.rstrip('\n') for line in open('requirements.txt')]
version = None version = None