Looks like 2FA works in facebook messenger :o
This commit is contained in:
parent
7130c7b7d6
commit
07f9f640f0
7 changed files with 144 additions and 42 deletions
|
@ -71,6 +71,10 @@ type Connector interface {
|
|||
// in which case the backend is free to re-use the ID or select a new one.
|
||||
Send(event *Event) (string, error)
|
||||
|
||||
// Used to send user commands directly
|
||||
// (first use case: receive 2-factor authentication codes)
|
||||
UserCommand(string)
|
||||
|
||||
// Close the connection
|
||||
Close()
|
||||
}
|
||||
|
|
13
connector/external/external.go
vendored
13
connector/external/external.go
vendored
|
@ -51,10 +51,12 @@ const (
|
|||
LEAVE = "leave"
|
||||
SEARCH = "search"
|
||||
SEND = "send"
|
||||
USER_COMMAND = "user_command"
|
||||
CLOSE = "close"
|
||||
|
||||
// external -> ezbr
|
||||
SAVE_CONFIG = "save_config"
|
||||
SYSTEM_MESSAGE = "system_message"
|
||||
JOINED = "joined"
|
||||
LEFT = "left"
|
||||
USER_INFO_UPDATED = "user_info_updated"
|
||||
|
@ -249,7 +251,7 @@ func (m *extMessageWithData) UnmarshalJSON(jj []byte) error {
|
|||
}
|
||||
m.Data = sr.Data
|
||||
return nil
|
||||
case JOINED, LEFT, CACHE_PUT, CACHE_GET, REP_OK, REP_ERROR:
|
||||
case SYSTEM_MESSAGE, JOINED, LEFT, CACHE_PUT, CACHE_GET, REP_OK, REP_ERROR:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("Invalid message type for message from external program: '%s'", c.MsgType)
|
||||
|
@ -377,6 +379,8 @@ func (ext *External) handleCmd(msg *extMessageWithData) {
|
|||
switch msg.MsgType {
|
||||
case SAVE_CONFIG:
|
||||
ext.handler.SaveConfig(msg.Data.(Configuration))
|
||||
case SYSTEM_MESSAGE:
|
||||
ext.handler.SystemMessage(msg.Value)
|
||||
case JOINED:
|
||||
ext.handler.Joined(msg.Room)
|
||||
case LEFT:
|
||||
|
@ -475,3 +479,10 @@ func (ext *External) Send(event *Event) (string, error) {
|
|||
}
|
||||
return rep.EventId, nil
|
||||
}
|
||||
|
||||
func (ext *External) UserCommand(cm string) {
|
||||
ext.cmd(extMessage{
|
||||
MsgType: USER_COMMAND,
|
||||
Value: cm,
|
||||
}, nil)
|
||||
}
|
||||
|
|
|
@ -271,6 +271,10 @@ func (irc *IRC) Send(event *Event) (string, error) {
|
|||
return "", nil
|
||||
}
|
||||
|
||||
func (irc *IRC) UserCommand(cm string) {
|
||||
irc.handler.SystemMessage("Command not supported.")
|
||||
}
|
||||
|
||||
func (irc *IRC) Close() {
|
||||
conn := irc.conn
|
||||
irc.conn = nil
|
||||
|
|
|
@ -334,6 +334,10 @@ func (mm *Mattermost) Send(event *Event) (string, error) {
|
|||
return created_post.Id, nil
|
||||
}
|
||||
|
||||
func (mm *Mattermost) UserCommand(cm string) {
|
||||
mm.handler.SystemMessage("Command not supported.")
|
||||
}
|
||||
|
||||
func (mm *Mattermost) Close() {
|
||||
if mm.conn != nil {
|
||||
mm.conn.WsQuit = true
|
||||
|
|
|
@ -367,6 +367,10 @@ func (xm *XMPP) Send(event *Event) (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func (xm *XMPP) UserCommand(cmd string) {
|
||||
xm.handler.SystemMessage("Command not supported.")
|
||||
}
|
||||
|
||||
func (xm *XMPP) Close() {
|
||||
if xm.conn != nil {
|
||||
xm.conn.Close()
|
||||
|
|
142
external/messenger.py
vendored
142
external/messenger.py
vendored
|
@ -29,10 +29,12 @@ INVITE = "invite"
|
|||
LEAVE = "leave"
|
||||
SEARCH = "search"
|
||||
SEND = "send"
|
||||
USER_COMMAND = "user_command"
|
||||
CLOSE = "close"
|
||||
|
||||
# external -> ezbr
|
||||
SAVE_CONFIG = "save_config"
|
||||
SYSTEM_MESSAGE = "system_message"
|
||||
JOINED = "joined"
|
||||
LEFT = "left"
|
||||
USER_INFO_UPDATED = "user_info_updated"
|
||||
|
@ -71,9 +73,9 @@ def stripFbLinkPrefix(url):
|
|||
# ---- MESSENGER CLIENT CLASS THAT HANDLES EVENTS ----
|
||||
|
||||
class MessengerBridgeClient(fbchat.Client):
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, bridge, *args, **kwargs):
|
||||
self.bridge = bridge
|
||||
super(MessengerBridgeClient, self).__init__(*args, **kwargs)
|
||||
self.bridge = None
|
||||
|
||||
def setBridge(self, bridge):
|
||||
self.bridge = bridge
|
||||
|
@ -87,9 +89,20 @@ class MessengerBridgeClient(fbchat.Client):
|
|||
self.bridge.onPersonRemoved(*args, **kwargs)
|
||||
def onTitleChange(self, *args, **kwargs):
|
||||
self.bridge.onTitleChange(*args, **kwargs)
|
||||
def on2FACode(self, *args, **kwargs):
|
||||
return self.bridge.on2FACode(*args, **kwargs)
|
||||
|
||||
# ---- SEPARATE THREADS FOR INITIAL SYNC & CLIENT LISTEN ----
|
||||
|
||||
class LoginThread(threading.Thread):
|
||||
def __init__(self, bridge, *args, **kwargs):
|
||||
super(LoginThread, self).__init__(*args, **kwargs)
|
||||
|
||||
self.bridge = bridge
|
||||
|
||||
def run(self):
|
||||
self.bridge.processLogin()
|
||||
|
||||
class SyncerThread(threading.Thread):
|
||||
def __init__(self, bridge, thread_queue, *args, **kwargs):
|
||||
super(SyncerThread, self).__init__(*args, **kwargs)
|
||||
|
@ -121,6 +134,9 @@ class MessengerBridge:
|
|||
def __init__(self):
|
||||
self.init_backlog_length = 100
|
||||
|
||||
self.config = None
|
||||
self.login_in_progress = None
|
||||
|
||||
# We cache maps between two kinds of identifiers:
|
||||
# - facebook uids of users
|
||||
# - identifiers for the bridge, which are the username when defined (otherwise equal to above)
|
||||
|
@ -135,6 +151,9 @@ class MessengerBridge:
|
|||
# caches for the people that are in rooms so that we don't send JOINED every time (map keys = "<userId>--<threadId>")
|
||||
self.others_joined_map = {}
|
||||
|
||||
# queue for thread syncing
|
||||
self.sync_thread_queue = queue.Queue(100)
|
||||
|
||||
def getUserId(self, user):
|
||||
retval = None
|
||||
if user.url is not None and not "?" in user.url:
|
||||
|
@ -222,48 +241,21 @@ class MessengerBridge:
|
|||
sys.stdout.write(msgstr + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
def system_message(self, msg):
|
||||
self.write({
|
||||
"_type": SYSTEM_MESSAGE,
|
||||
"value": msg,
|
||||
})
|
||||
|
||||
def handle_cmd(self, cmd):
|
||||
ty = cmd["_type"]
|
||||
if ty == CONFIGURE:
|
||||
self.init_backlog_length = int(cmd["data"]["initial_backlog"])
|
||||
|
||||
has_pickle = "client_pickle" in cmd["data"] and len(cmd["data"]["client_pickle"]) > 0
|
||||
if has_pickle:
|
||||
data = base64.b64decode(cmd["data"]["client_pickle"])
|
||||
data = zlib.decompress(data)
|
||||
self.client = pickle.loads(data)
|
||||
if self.login_in_progress is None:
|
||||
self.config = cmd["data"]
|
||||
self.login_in_progress = queue.Queue(1)
|
||||
LoginThread(self).start()
|
||||
else:
|
||||
email, password = cmd["data"]["email"], cmd["data"]["password"]
|
||||
self.client = MessengerBridgeClient(email=email, password=password, max_tries=1)
|
||||
## TODO: save client in new client_pickle config value
|
||||
|
||||
if not self.client.isLoggedIn():
|
||||
return {"_type": REP_ERROR, "error": "Unable to login (invalid pickle?)"}
|
||||
|
||||
if not has_pickle:
|
||||
new_config = cmd["data"]
|
||||
data = pickle.dumps(self.client)
|
||||
data = zlib.compress(data)
|
||||
new_config["client_pickle"] = base64.b64encode(data).decode('ascii')
|
||||
self.write({"_type": SAVE_CONFIG, "data": new_config})
|
||||
|
||||
self.client.setBridge(self)
|
||||
|
||||
self.my_user_id = self.getUserIdFromUid(self.client.uid)
|
||||
|
||||
threads = self.client.fetchThreadList(limit=10)
|
||||
# ensure we have a correct mapping for bridged user IDs to fb uids
|
||||
# (this should be fast)
|
||||
for thread in threads:
|
||||
if thread.type == ThreadType.USER:
|
||||
self.getUserId(thread)
|
||||
|
||||
self.sync_thread_queue = queue.Queue(100)
|
||||
SyncerThread(self, self.sync_thread_queue).start()
|
||||
for thread in reversed(threads):
|
||||
self.sync_thread_queue.put(thread)
|
||||
|
||||
ClientListenThread(self.client).start()
|
||||
return {"_type": REP_ERROR, "error": "Already logging in (CONFIGURE sent twice)"}
|
||||
|
||||
elif ty == CLOSE:
|
||||
self.close()
|
||||
|
@ -272,6 +264,9 @@ class MessengerBridge:
|
|||
return {"_type": REP_OK, "user": self.my_user_id}
|
||||
|
||||
elif ty == JOIN:
|
||||
if self.client is None:
|
||||
pass
|
||||
else:
|
||||
self.ensure_i_joined(cmd["room"])
|
||||
|
||||
elif ty == LEAVE:
|
||||
|
@ -331,6 +326,9 @@ class MessengerBridge:
|
|||
elif ty == REP_OK and cmd["_id"] in self.cache_gets:
|
||||
self.cache_gets[cmd["_id"]].put(cmd["value"])
|
||||
|
||||
elif ty == USER_COMMAND:
|
||||
self.handleUserCommand(cmd["value"])
|
||||
|
||||
else:
|
||||
return {"_type": REP_ERROR, "error": "Not implemented"}
|
||||
|
||||
|
@ -354,6 +352,51 @@ class MessengerBridge:
|
|||
def cache_put(self, key, value):
|
||||
self.write({"_type": CACHE_PUT, "key": key, "value": value})
|
||||
|
||||
# ---- Process login (called from separate thread) ----
|
||||
|
||||
def processLogin(self):
|
||||
self.init_backlog_length = int(self.config["initial_backlog"])
|
||||
|
||||
has_pickle = "client_pickle" in self.config and len(self.config["client_pickle"]) > 0
|
||||
if has_pickle:
|
||||
data = base64.b64decode(self.config["client_pickle"])
|
||||
data = zlib.decompress(data)
|
||||
self.client = pickle.loads(data)
|
||||
else:
|
||||
email, password = self.config["email"], self.config["password"]
|
||||
self.client = MessengerBridgeClient(bridge=self, email=email, password=password, max_tries=1)
|
||||
|
||||
if not self.client.isLoggedIn():
|
||||
self.system_message("Unable to login (invalid pickle? dunno)")
|
||||
else:
|
||||
self.system_message("Login complete, will now sync threads.")
|
||||
|
||||
if not has_pickle:
|
||||
self.client.setBridge(None)
|
||||
data = pickle.dumps(self.client)
|
||||
data = zlib.compress(data)
|
||||
self.config["client_pickle"] = base64.b64encode(data).decode('ascii')
|
||||
self.write({"_type": SAVE_CONFIG, "data": self.config})
|
||||
|
||||
self.client.setBridge(self)
|
||||
|
||||
self.my_user_id = self.getUserIdFromUid(self.client.uid)
|
||||
|
||||
threads = self.client.fetchThreadList(limit=10)
|
||||
# ensure we have a correct mapping for bridged user IDs to fb uids
|
||||
# (this should be fast)
|
||||
for thread in threads:
|
||||
if thread.type == ThreadType.USER:
|
||||
self.getUserId(thread)
|
||||
|
||||
SyncerThread(self, self.sync_thread_queue).start()
|
||||
for thread in reversed(threads):
|
||||
self.sync_thread_queue.put(thread)
|
||||
|
||||
ClientListenThread(self.client).start()
|
||||
|
||||
self.login_in_progress = None
|
||||
|
||||
# ---- Info sync ----
|
||||
|
||||
def ensure_i_joined(self, thread_id):
|
||||
|
@ -566,6 +609,25 @@ class MessengerBridge:
|
|||
"data": {"name": new_title},
|
||||
})
|
||||
|
||||
def on2FACode(self, *args, **kwargs):
|
||||
if self.login_in_progress is None:
|
||||
self.system_message("Facebook messenger requests 2 factor authentication, but we have a bug so that won't work.")
|
||||
return None
|
||||
else:
|
||||
self.system_message("Facebook messenger requests 2 factor authentication. Enter it by saying: cmd messenger 2fa <your code> (replace messenger by your account name if you have several messenger accounts)")
|
||||
uc = self.login_in_progress.get(block=True)
|
||||
return uc["2fa_code"]
|
||||
|
||||
def handleUserCommand(self, cmd):
|
||||
cmd = cmd.split(' ')
|
||||
if cmd[0] == "2fa":
|
||||
if self.login_in_progress is not None:
|
||||
self.login_in_progress.put({"2fa_code": cmd[1]})
|
||||
else:
|
||||
self.system_message("2FA code not required at this point.")
|
||||
else:
|
||||
self.system_message("Invalid user command.")
|
||||
|
||||
# ---- CLI ----
|
||||
|
||||
def createClientPickle():
|
||||
|
|
13
server.go
13
server.go
|
@ -259,6 +259,7 @@ func handleSystemMessage(mxid string, msg string) {
|
|||
ezbrSystemSend(mxid, "- join <protocol or account> <room id>: join public chat room")
|
||||
ezbrSystemSend(mxid, "- talk <protocol or account> <user id>: open private conversation to contact")
|
||||
ezbrSystemSend(mxid, "- search <protocol or account> <name>: search for users by name")
|
||||
ezbrSystemSend(mxid, "- cmd <protocol or account> <command>: send special command to account")
|
||||
case "list", "account", "accounts":
|
||||
one := false
|
||||
if accts, ok := registeredAccounts[mxid]; ok {
|
||||
|
@ -332,6 +333,18 @@ func handleSystemMessage(mxid string, msg string) {
|
|||
} else {
|
||||
ezbrSystemSendf(mxid, "No account with name or using protocol %s", cmd[1])
|
||||
}
|
||||
case "cmd":
|
||||
if len(cmd) < 3 {
|
||||
ezbrSystemSendf(mxid, "Usage: %s <protocol or account> <name>", cmd[0])
|
||||
return
|
||||
}
|
||||
|
||||
account := findAccount(mxid, cmd[1])
|
||||
if account != nil {
|
||||
account.Conn.UserCommand(strings.Join(cmd[2:], " "))
|
||||
} else {
|
||||
ezbrSystemSendf(mxid, "No account with name or using protocol %s", cmd[1])
|
||||
}
|
||||
default:
|
||||
ezbrSystemSend(mxid, "Unrecognized command. Type `help` if you need some help!")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue