aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--connector/connector.go4
-rw-r--r--connector/external/external.go13
-rw-r--r--connector/irc/irc.go4
-rw-r--r--connector/mattermost/mattermost.go4
-rw-r--r--connector/xmpp/xmpp.go4
-rwxr-xr-xexternal/messenger.py144
-rw-r--r--server.go13
7 files changed, 144 insertions, 42 deletions
diff --git a/connector/connector.go b/connector/connector.go
index 55e0e34..0819bd7 100644
--- a/connector/connector.go
+++ b/connector/connector.go
@@ -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()
}
diff --git a/connector/external/external.go b/connector/external/external.go
index 9aae0f1..92339b9 100644
--- a/connector/external/external.go
+++ b/connector/external/external.go
@@ -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)
+}
diff --git a/connector/irc/irc.go b/connector/irc/irc.go
index 77388e7..1327eaa 100644
--- a/connector/irc/irc.go
+++ b/connector/irc/irc.go
@@ -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
diff --git a/connector/mattermost/mattermost.go b/connector/mattermost/mattermost.go
index 9f8bbaf..86bf6b5 100644
--- a/connector/mattermost/mattermost.go
+++ b/connector/mattermost/mattermost.go
@@ -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
diff --git a/connector/xmpp/xmpp.go b/connector/xmpp/xmpp.go
index 64c53d9..2d0260c 100644
--- a/connector/xmpp/xmpp.go
+++ b/connector/xmpp/xmpp.go
@@ -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()
diff --git a/external/messenger.py b/external/messenger.py
index 76ca90d..9ac7d26 100755
--- a/external/messenger.py
+++ b/external/messenger.py
@@ -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,7 +264,10 @@ class MessengerBridge:
return {"_type": REP_OK, "user": self.my_user_id}
elif ty == JOIN:
- self.ensure_i_joined(cmd["room"])
+ if self.client is None:
+ pass
+ else:
+ self.ensure_i_joined(cmd["room"])
elif ty == LEAVE:
thread_id = cmd["room"]
@@ -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():
diff --git a/server.go b/server.go
index e81f0b3..2ed598e 100644
--- a/server.go
+++ b/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!")
}