# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 <NAME> <<EMAIL>>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

#
# Cisco Webex protocol for WeeChat.
#
# For help, see /help webex
# Happy chat, enjoy :)

import base64
import json
import os
import re
import socket
import subprocess
from html.parser import HTMLParser
from http.server import BaseHTTPRequestHandler
from io import BytesIO
from webexteamssdk import WebexTeamsAPI
import weechat

SCRIPT_NAME = "webex"
SCRIPT_AUTHOR = "<NAME> <<EMAIL>>"
SCRIPT_VERSION = "0.1.2"
SCRIPT_LICENSE = "APACHE2"
SCRIPT_DESC = "Cisco Webex protocol for WeeChat"
SCRIPT_COMMAND = SCRIPT_NAME

webex_config_file = None
webex_config_section = {}
webex_config_option = {}
webex_server = None


# =================================[ config ]=================================

def webex_config_init():
    """ Initialize config file: create sections and options in memory. """
    global webex_config_file, webex_config_section, webex_config_option

    # This will create webex.conf file
    webex_config_file = weechat.config_new(SCRIPT_NAME, "webex_config_reload_cb", "")
    if not webex_config_file:
        return

    # server section
    webex_config_section["server"] = weechat.config_new_section(
        webex_config_file, "server", 0, 0, "", "", "", "", "", "", "", "", "", "")

    webex_config_option["access_token"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "access_token", "string", "Access Token", "", 0, 0,
        "token", "token", 0, "", "", "", "", "", "")
    webex_config_option["autojoin_last"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "autojoin_last", "integer", "Autojoin this number of rooms/chats (0 to disable)", "", 0, 100,
        "0", "0", 0, "", "", "", "", "", "")
    webex_config_option["autojoin_rooms"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "autojoin_rooms", "string", "Rooms to join on startup", "", 0, 0,
        "", "", 0, "", "", "", "", "", "")
    webex_config_option["autojoin_directs"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "autojoin_directs", "string", "1:1 chats to open on startup", "", 0, 0,
        "", "", 0, "", "", "", "", "", "")
    webex_config_option["default_domain"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "default_domain", "string", "Default domain for emails.", "", 0, 0,
        "", "", 0, "", "", "", "", "", "")
    webex_config_option["node_bin_path"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "node_bin_path", "string", "The path to 'node' binary", "", 0, 0,
        "/usr/local/bin/node", "/usr/local/bin/node", 0, "", "", "", "", "", "")
    webex_config_option["websocket_proxy_app"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "websocket_proxy_app", "string", "The path to websocket proxy application 'app.js'", "", 0, 0,
        "/opt/hookbuster/app.js", "/opt/hookbuster/app.js", 0, "", "", "", "", "", "")
    webex_config_option["port"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "port", "integer", "The port used to create the socket (for incoming message using websocket)", "", 1, 65535,
        "8080", "8080", 0, "", "", "", "", "", "")
    webex_config_option["debug"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "debug", "string", "debug mode (yes/no)", "", 0, 0,
        "no", "no", 0, "", "", "", "", "", "")


def webex_config_reload_cb(data, config_file):
    """ Reload config file. """
    return weechat.config_reload(config_file)


def webex_config_read():
    """ Read config file. """
    global webex_config_file
    return weechat.config_read(webex_config_file)


def webex_config_write():
    """ Write config file """
    global webex_config_file
    return weechat.config_write(webex_config_file)


# ================================[ commands ]================================

def webex_hook_commands_and_completions():
    """ Hook commands and completions. """
    weechat.hook_command("wmsg", "Open a chat with a person",
                         "<buddy>",
                         " buddy: user name (like arnaud.morin)",
                         "",
                         "webex_cmd_wmsg", "")
    weechat.hook_command("wj", "Join a room",
                         "<room>",
                         " room: room name",
                         "",
                         "webex_cmd_wj", "")
    weechat.hook_command("wsr", "Search room based on name",
                         "<name>",
                         " name: room name",
                         "",
                         "webex_cmd_wsr", "")
    weechat.hook_command("wsp", "Search user based on name",
                         "<name>",
                         " name: user name (like arnaud)",
                         "",
                         "webex_cmd_wsp", "")
    weechat.hook_command("b", "Switch to buffer based on name",
                         "<name>",
                         " name: user name (like arnaud)",
                         "",
                         "webex_cmd_b", "")
    weechat.hook_command("wreconnect", "Reconnect to webex",
                         "",
                         "",
                         "",
                         "webex_cmd_reconnect", "")
    weechat.hook_command("wim", "Open VIM to allow editing multiline messages",
                         "",
                         "",
                         "",
                         "webex_cmd_wim", "")


def webex_cmd_wmsg(data, buffer, buddy):
    """ Send a message to a person """
    global webex_server
    webex_server.prnt(f"Opening chat with {buddy}")

    # Add domain if not set
    if '@' not in buddy:
        buddy = f"{buddy}@{webex_server.domain}"
    person = webex_server.get_person(buddy)
    if person:
        buddy = Buddy(person)
        webex_server.prnt(f"Found person: {buddy.email}")
        if buddy.domain == webex_server.domain:
            buf_name = buddy.name
        else:
            buf_name = buddy.email
        chat = Chat(webex_server, buf_name, buddy.id, "private")
        webex_server.chats.append(chat)
    else:
        webex_server.prnt(f"No room found with name {buddy}")

    return weechat.WEECHAT_RC_OK


def webex_cmd_b(data, buffer, buddy):
    """ Switch to a buffer """
    global webex_server
    chat = next((x for x in webex_server.chats if buddy.lower() in x.name.lower()), None)
    if chat:
        webex_server.prnt(f"Opening buffer {buddy}")
        weechat.buffer_set(chat.buffer, "display", "1")
    else:
        # Maybe it's a number?
        try:
            number = int(buddy)
            chat = next((x for x in webex_server.chats if number == x.get_number()), None)
            if chat:
                webex_server.prnt(f"Opening buffer {buddy}")
                weechat.buffer_set(chat.buffer, "display", "1")
        except Exception:
            pass

    return weechat.WEECHAT_RC_OK


def webex_cmd_wj(data, buffer, room_name):
    """ Join a room """
    global webex_server
    webex_server.prnt(f"Trying to join {room_name}")

    room = webex_server.search_room(room_name)
    if room:
        webex_server.prnt(f"Found room: {room.title}")
        chat = Chat(webex_server, room.title, room.id, "channel")
        webex_server.chats.append(chat)
    else:
        webex_server.prnt(f"No room found with name {room_name}")

    return weechat.WEECHAT_RC_OK


def webex_cmd_wsr(data, buffer, room_name):
    """ Search a room """
    global webex_server
    webex_server.prnt(f"List of room with '{room_name}' in name:")

    rooms = webex_server.search_rooms(room_name)
    for room in rooms:
        webex_server.prnt(f" - {room.title}")

    return weechat.WEECHAT_RC_OK


def webex_cmd_wsp(data, buffer, name):
    """ Search a person """
    global webex_server
    webex_server.prnt(f"List of people with '{name}' in name:")

    persons = webex_server.search_persons(name)
    for person in persons:
        webex_server.prnt(f" - {person.emails[0]}")

    return weechat.WEECHAT_RC_OK


def webex_cmd_reconnect(data, buffer, access_token):
    """ Reconnect to webex """
    global webex_server, webex_config_option
    if access_token:
        weechat.config_option_set(webex_config_option["access_token"], access_token, 1)

    # Do full reconnect if sock not working
    if not webex_server.sock:
        webex_server.connect()
    else:
        webex_server.connect_webex()
        webex_server.start_websocket(force=True)

    return weechat.WEECHAT_RC_OK


def webex_cmd_wim(data, buffer, args):
    """ Open VIM """
    global webex_server
    chat = next((x for x in webex_server.chats if buffer == x.buffer), None)

    # TODO Maybe add an option for this
    editor = "vim -f"
    path = "/tmp/weechat_longmsg.txt"
    open(path, "w+")

    cmd = editor.split() + [path]
    code = subprocess.Popen(cmd).wait()
    if code != 0:
        os.remove(path)
        weechat.command(buffer, "/window refresh")
        return weechat.WEECHAT_RC_ERROR

    with open(path) as f:
        text = f.read()
        chat.send_message(text)

    os.remove(path)
    weechat.command(buffer, "/window refresh")

    return weechat.WEECHAT_RC_OK


# ================================[ server ]==================================
class Server(object):
    def __init__(self):
        self.chats = []
        self.webexapi = None
        self.buddy = None
        self.hook = None
        self.sock = None
        self.domain = None
        self.buffer = ""
        self.websocket = None
        self.cache = {'rooms': [], 'directs': []}

    def connect(self):
        """ Connect """
        if not self.connect_webex():
            return False

        if not self.buffer:
            bufname = f"{SCRIPT_NAME}.{self.domain}"
            self.buffer = weechat.buffer_search("python", bufname)
            if not self.buffer:
                self.buffer = weechat.buffer_new(bufname,
                                                 "webex_buffer_input_cb", "",
                                                 "webex_buffer_close_cb", "")
            if self.buffer:
                weechat.buffer_set(self.buffer, "short_name", SCRIPT_NAME)
                weechat.buffer_set(self.buffer, "input_multiline", "1")
                weechat.buffer_set(self.buffer, "localvar_set_type", "server")
                weechat.buffer_set(self.buffer, "localvar_set_server", self.domain)
                weechat.buffer_set(self.buffer, "display", "auto")

        self.prnt(f"Bienvenue {self.buddy.name}")

        # Join rooms that are in config
        rooms_to_join = self.get_config_value("autojoin_rooms")
        if rooms_to_join:
            for room_name in rooms_to_join.split(','):
                room = self.search_room(room_name, use_cache=True)
                if room:
                    self.chats.append(Chat(self, room.title, room.id, "channel", auto=False))

        # Join direct chats that are in config
        directs_to_join = self.get_config_value("autojoin_directs")
        if directs_to_join:
            for email in directs_to_join.split(','):
                if '@' not in email:
                    email = f"{email}@{self.domain}"
                buddy = Buddy(self.get_person(email))
                if buddy:
                    if buddy.domain == self.domain:
                        buf_name = buddy.name
                    else:
                        buf_name = buddy.email
                    self.chats.append(Chat(self, buf_name, buddy.id, "private", auto=False))

        # Autojoin last rooms
        autojoin_last = self.get_config_int_value("autojoin_last")
        if autojoin_last > 0:
            rooms = self.list_rooms(type=None, max=autojoin_last)
            if rooms:
                for room in rooms:
                    if room.type == 'group':
                        self.chats.append(Chat(self, room.title, room.id, "channel", auto=False))
                    else:
                        person = self.get_person_from_id(room.id)
                        if person:
                            buddy = Buddy(self.get_person_from_id(room.id))
                            if buddy.domain == self.domain:
                                buf_name = buddy.name
                            else:
                                buf_name = buddy.email
                            self.chats.append(Chat(self, buf_name, buddy.id, "private", auto=False))

        # SOCKET
        # We bind on 127.0.0.1:{port/8080}
        # So we need a proxy pass (like nginx or apache)
        # to forward the request to this socket
        if not self.sock:
            self.prnt("Starting socket")
            try:
                port = self.get_config_int_value("port")
                self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.sock.bind(("127.0.0.1", port))
                self.sock.listen(5)
                self.prnt(f"Socket listening on {self.sock.getsockname()}")
            except Exception as e:
                self.prnt(f"Error while creating the HTTP server: {e}")
                self.disconnect()
                self.sock = None

        # WEECHAT HOOK
        if not self.hook:
            try:
                self.hook = weechat.hook_fd(self.sock.fileno(), 1, 0, 0, "socket_cb", "")
            except Exception as e:
                self.prnt(f"Error while creating the weechat hook: {e}")
                self.disconnect()
                self.hook = None

        # WEBEX HOOKBUSTER / Websocket proxy
        self.start_websocket(force=False)

        return True

    def debug(self, message):
        if self.get_config_value('debug') == 'yes':
            self.prnt(f"[debug] {message}")

    def start_websocket(self, force):
        """(Re-)start the hookbuster/websocket proxy"""
        if force and self.websocket:
            weechat.unhook(self.websocket)
            self.websocket = None

        if not self.websocket:
            self.prnt("Starting websocket proxy")
            token = self.get_config_value("access_token")
            node = self.get_config_value("node_bin_path")
            ws_app = self.get_config_value("websocket_proxy_app")
            port = self.get_config_int_value("port")
            command = f"{node} {ws_app} {token} {port}"
            # self.prnt(command)
            self.websocket = weechat.hook_process(command, 0, '', '')

    def connect_webex(self):
        """Connect to webex"""
        # API
        try:
            self.webexapi = WebexTeamsAPI(access_token=self.get_config_value("access_token"))
        except Exception as e:
            self.prnt(f"Error while trying to connect to webex API: {e}")
            return False

        self.domain = self.get_config_value("default_domain")

        # Test API and grab your name
        try:
            buddy = self.webexapi.people.me()
        except Exception as e:
            self.prnt(f"Error while trying to get me(): {e}")
            return False
        self.buddy = Buddy(buddy)
        self.prnt(f"Bienvenue {self.buddy.name}")

        return True

    def disconnect(self):
        if self.sock:
            self.sock.close()
            self.sock = None
        if self.hook:
            weechat.unhook(self.hook)
            self.hook = None
        if self.websocket:
            weechat.unhook(self.websocket)
            self.websocket = None

    def list_rooms(self, type="group", max=100, use_cache=False):
        """Grab room list from webex"""
        # Force a refresh if cache is empty
        if self.cache['rooms'] == []:
            use_cache = False
        if not use_cache:
            self.cache['rooms'] = self.webexapi.rooms.list(type=type, sortBy="lastactivity", max=max)
        return self.cache['rooms']

    def search_room(self, name, use_cache=False):
        """Search for a room by name. Return first room that match"""
        for room in self.list_rooms(use_cache=use_cache):
            if name in room.title:
                return room

    def search_rooms(self, name):
        """Search for rooms by name. Return all rooms that match"""
        result = []
        for room in self.list_rooms():
            if name.lower() in room.title.lower():
                result.append(room)
        return result

    def get_room_from_id(self, room_id):
        """Get a room from ID"""
        return self.webexapi.rooms.get(room_id)

    def get_person(self, email):
        """Get person from email"""
        try:
            people = list(self.webexapi.people.list(email=email))[0]
            return people
        except Exception:
            return None

    def get_person_from_id(self, id):
        """Get person from id"""
        try:
            return self.webexapi.people.get(id)
        except Exception:
            return None

    def search_persons(self, name):
        """Search for buddies by name. Return all buddies that match"""
        return list(self.webexapi.people.list(displayName=name))

    def get_config_value(self, option):
        """ Get an option """
        global webex_config_option
        return weechat.config_string(webex_config_option[option])

    def get_config_int_value(self, option):
        """ Get an option """
        global webex_config_option
        return weechat.config_integer(webex_config_option[option])

    def prnt(self, message):
        weechat.prnt(self.buffer, message)

    def send_room_message(self, room_id, message):
        self.webexapi.messages.create(roomId=room_id, markdown=message)

    def send_direct_message(self, person_id, message):
        self.webexapi.messages.create(toPersonId=person_id, markdown=message)

    def receive_message(self, raw):
        self.debug(raw)
        try:
            data = json.loads(raw)
            if 'data' in data:
                # Discard message from myself
                if data['data']['personId'] == self.buddy.id:
                    self.debug("Message from myself")
                    pass
                # Messages from a person
                elif data['data']['roomType'] == "direct":
                    chat = next((x for x in self.chats if x.id == data['data']['personId']), None)
                    # If this is first time we are talking to this person
                    # Create a new chat
                    if not chat:
                        buddy = Buddy(self.get_person_from_id(data['data']['personId']))
                        if buddy.domain == self.domain:
                            buf_name = buddy.name
                        else:
                            buf_name = buddy.email
                        chat = Chat(self, buf_name, buddy.id, "private", auto=False)
                        self.chats.append(chat)
                    self.debug(f"Receive a message from a person {chat.name}")
                    chat.receive_message(data['data'])
                # Messages for a room
                elif data['data']['roomType'] == "group":
                    if data['data']['roomId'] in [x.id for x in self.chats] or get_real_room_id(data['data']['roomId']) in [x.real_id for x in self.chats]:
                        # Message for an already opened room
                        chat = next((x for x in self.chats if x.id == data['data']['roomId']), None)
                        if not chat:
                            chat = next((x for x in self.chats if x.real_id == get_real_room_id(data['data']['roomId'])), None)
                        self.debug(f"Receive a message for room {chat.name}")
                        chat.receive_message(data['data'])
                    else:
                        # Message for a non opened room, but maybe with a mention?
                        if f'@{<EMAIL>.buddy.name}' in WebexParser.parse(data['data']['text']):
                            # Create a new chat
                            room = self.get_room_from_id(data['data']['roomId'])
                            if not room:
                                room = self.get_room_from_id(get_real_room_id(data['data']['roomId']))
                            chat = Chat(self, room.title, room.id, "channel", auto=False)
                            self.chats.append(chat)
                            self.debug(f"Receive a message for closed room {chat.name} with mention @{self.buddy.name}")
                            chat.receive_message(data['data'])
        except Exception as e:
            self.prnt(f"Error while receiving data {e}")


def get_real_room_id(room_encoded):
    """Get a real room ID from a base64 encoded room ID"""
    # I found this from a discussion with cisco
    # roomId car be different is you are comming from a different endpoint (US/EU)
    # This is maybe because of something hardcoded in hookbuster (cisco was not sure)
    # They said they would take a look
    try:
        room_decode = base64.decodebytes(bytes(room_encoded, 'utf-8')).decode("utf-8").split('/')[-1]
    except Exception:
        room_decode = None
    return room_decode


def get_chat_from_buffer(buffer):
    """ Search a chat from a buffer. """
    global webex_server
    for chat in webex_server.chats:
        if chat.buffer == buffer:
            return chat
    return None


def get_chat_from_name(name):
    """ Search a chat from a name. """
    global webex_server
    for chat in webex_server.chats:
        print(chat.name)
        if chat.name == name:
            return chat
    return None


# =================================[ chats ]==================================

class Chat:
    """ Class to manage private chat or rooms. """

    def __init__(self, server, name, id, type="channel", auto=True):
        """ Init chat """
        self.server = server
        # TODO(arnaud) maybe add an option to remove the # in front of channels
        self.name = f"#{name}" if type == 'channel' else f"{name}"
        self.id = f"{id}"
        self.buffer = weechat.buffer_search("python", self.id)
        self.type = type    # can be channel or private
        if not self.buffer:
            self.buffer = weechat.buffer_new(self.name,
                                             "webex_buffer_input_cb", "",
                                             "webex_buffer_close_cb", "")
        if self.buffer:
            weechat.buffer_set(self.buffer, "title", self.name)
            weechat.buffer_set(self.buffer, "short_name", self.name)
            weechat.buffer_set(self.buffer, "input_multiline", "1")
            weechat.buffer_set(self.buffer, "localvar_set_type", self.type)     # I use this in external plugin (notification/autosort)
            weechat.hook_signal_send("logger_backlog",
                                     weechat.WEECHAT_HOOK_SIGNAL_POINTER, self.buffer)
            if auto:
                weechat.buffer_set(self.buffer, "display", "auto")

        # Store real ID if possible
        if self.type == "channel":
            self.real_id = get_real_room_id(self.id)
        else:
            self.real_id = None

    def get_number(self):
        return weechat.buffer_get_integer(self.buffer, "number")

    def prnt(self, message):
        """ Print a message in the buffer """
        weechat.prnt(self.buffer, message)

    def receive_message(self, message):
        """ Receive a message from someone """
        try:
            buddy_name, buddy_domain = message['personEmail'].split('@')
            if buddy_domain != self.server.domain:
                buddy_name = message['personEmail']
            weechat.prnt_date_tags(self.buffer, 0,
                                   "notify_private,nick_%s,prefix_nick_%s,log1" %
                                   (buddy_name,
                                    weechat.config_string(weechat.config_get("weechat.color.chat_nick_other"))),
                                   "%s%s\t%s" % (weechat.color("chat_nick_other"),
                                                 buddy_name,
                                                 WebexParser.parse(message['text'])))
        except Exception as e:
            self.prnt(f"Error while receiving a message: {e}")

    def send_message(self, message):
        """ Send message """
        parsed_message = WebexMentionParser.parse(message, self.server.domain)
        if self.type == "channel":
            self.server.send_room_message(self.id, parsed_message)
        else:
            self.server.send_direct_message(self.id, parsed_message)
        weechat.prnt_date_tags(self.buffer, 0,
                               "notify_none,no_highlight,nick_%s,prefix_nick_%s,log1" %
                               (self.server.buddy.name,
                                weechat.config_string(weechat.config_get("weechat.color.chat_nick_self"))),
                               "%s%s\t%s" % (weechat.color("chat_nick_self"),
                                             self.server.buddy.name,
                                             message))

    def delete(self):
        """ Delete chat. """
        if self.buffer:
            self.buffer = None


# ================================[ buddy ]=================================
class Buddy(object):
    def __init__(self, data):
        self.id = data.id
        self.email = data.emails[0]
        self.name, self.domain = self.parse_email(self.email)

    def parse_email(self, email):
        """Parse email to grab name"""
        return email.split('@')


# ================================[ HTTP ]=================================

class HTTPRequest(BaseHTTPRequestHandler):
    def __init__(self, raw_http_request):
        self.rfile = BytesIO(raw_http_request.encode('utf-8'))
        self.raw_requestline = self.rfile.readline()
        self.error_code = self.error_message = None
        self.parse_request()

        # Headers
        self.headers = dict(self.headers)
        # Data
        try:
            self.data = raw_http_request[raw_http_request.index('\n\n') + 2:].rstrip()
        except ValueError:
            self.data = None


def http_reply(conn, code, extra_header, message, mimetype='text/html'):
    """Send a HTTP reply to client."""
    global urlserver_settings
    if extra_header:
        extra_header += '\r\n'
    s = 'HTTP/1.1 %s\r\n' \
        '%s' \
        'Content-Type: %s\r\n' \
        'Content-Length: %d\r\n' \
        '\r\n' \
        % (code, extra_header, mimetype, len(message))
    msg = None
    if type(message) is bytes:
        msg = s.encode('utf-8') + message
    else:
        msg = s.encode('utf-8') + message.encode('utf-8')
    conn.sendall(msg)


# ================================[ parsers ]=================================
class WebexParser(HTMLParser):
    """A simple HTML parser that throws away anything but newlines and links"""

    @staticmethod
    def parse(data):
        """Create a temporary parser and parse a single string"""
        parser = WebexParser()
        parser.feed(data)
        parser.close()
        return parser.result

    def reset(self):
        """Forget all state, called from __init__"""
        HTMLParser.reset(self)
        self.result = ''
        self.linktarget = ''
        self.linkstart = 0
        self.insparkmention = False

    def handle_starttag(self, tag, attrs):
        """Called when a start tag is encountered"""
        if tag == 'br':
            self.result += '\n'
        elif tag == 'a':
            attrs = dict(attrs)
            if 'href' in attrs:
                self.result += '['
                self.linktarget = attrs['href']
                self.linkstart = len(self.result)
        elif tag == 'spark-mention':
            self.result += '@'
            self.insparkmention = True

    def handle_endtag(self, tag):
        """Called when an end tag is encountered"""
        if tag == 'a':
            if self.linktarget:
                if self.result[self.linkstart:] == self.linktarget:
                    self.result += ']'
                else:
                    self.result += ']({})'.format(self.linktarget)
                self.linktarget = ''
        elif tag == 'spark-mention':
            self.insparkmention = False

    def handle_data(self, data):
        """Called for character data (i.e. text)"""
        if self.insparkmention:
            self.result += data.replace(" ", ".").lower()
        else:
            self.result += data


class WebexMentionParser(object):

    @staticmethod
    def parse(data, default_domain):
        """Parse data to replace @xxx.yyy with correct spark mention"""
        result = ''
        if '@' in data:
            # We have @ in data, but it can be a mail address
            # Let split sentence in words
            for w in data.split():
                # If word starts with @ (a mention)
                if w.startswith('@'):
                    # If we want to mention all
                    if w == '@all':
                        result += ' <@all>'
                    # If mention is already with full email
                    elif '@' in w[1:]:
                        result += ' ' + re.sub(r'@(\S+)', r'<@personEmail:\1>', w)
                    # If not, add default domain
                    else:
                        result += ' ' + re.sub(r'@(\S+)', r'<@personEmail:\1@{}>'.format(default_domain), w)
                else:
                    # It was not a mention
                    result += f' {w}'
            return result.strip()
        else:
            # Nothing to do
            return data


# ================================[ callbacks ]=================================

def webex_unload_cb():
    """ Function called when script is unloaded. """
    global webex_server
    webex_server.prnt("Unloading")
    webex_config_write()
    webex_server.disconnect()
    return weechat.WEECHAT_RC_OK


def webex_buffer_input_cb(data, buffer, input_data):
    """ Callback called for input data on a buffer. """
    chat = get_chat_from_buffer(buffer)
    if chat:
        chat.send_message(input_data)

    return weechat.WEECHAT_RC_OK


def webex_buffer_close_cb(data, buffer):
    """ Callback called when a jabber buffer is closed. """
    global webex_server
    chat = get_chat_from_buffer(buffer)
    if chat:
        # Unset the buffer
        chat.delete()
        # Delete the chat from server.chats
        webex_server.chats.remove(chat)

    return weechat.WEECHAT_RC_OK


def socket_cb(data, fd):
    global webex_server
    data = None
    if webex_server.sock.fileno() == int(fd):
        conn, addr = webex_server.sock.accept()
        # Grab data
        try:
            conn.settimeout(0.3)
            data = conn.recv(4096).decode('utf-8')
            data = data.replace('\r\n', '\n')
        except Exception:
            return weechat.WEECHAT_RC_OK

        http_request = HTTPRequest(data)
        webex_server.receive_message(http_request.data)

        http_reply(conn, "200 OK", "", "OK")
        conn.close()
    return weechat.WEECHAT_RC_OK


# ==================================[ main ]==================================

if __name__ == "__main__":
    if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION,
                        SCRIPT_LICENSE, SCRIPT_DESC,
                        "webex_unload_cb", ""):

        webex_hook_commands_and_completions()
        webex_config_init()
        webex_config_read()

        webex_server = Server()
        webex_server.connect()

# -*- coding: utf-8 -*-
#
# Copyright (C) 2021 <NAME> <<EMAIL>>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

#
# Cisco Webex protocol for WeeChat.
#
# For help, see /help webex
# Happy chat, enjoy :)

import base64
import json
import os
import re
import socket
import subprocess
from html.parser import HTMLParser
from http.server import BaseHTTPRequestHandler
from io import BytesIO
from webexteamssdk import WebexTeamsAPI
import weechat

SCRIPT_NAME = "webex"
SCRIPT_AUTHOR = "<NAME> <<EMAIL>>"
SCRIPT_VERSION = "0.1.2"
SCRIPT_LICENSE = "APACHE2"
SCRIPT_DESC = "Cisco Webex protocol for WeeChat"
SCRIPT_COMMAND = SCRIPT_NAME

webex_config_file = None
webex_config_section = {}
webex_config_option = {}
webex_server = None


# =================================[ config ]=================================

def webex_config_init():
    """ Initialize config file: create sections and options in memory. """
    global webex_config_file, webex_config_section, webex_config_option

    # This will create webex.conf file
    webex_config_file = weechat.config_new(SCRIPT_NAME, "webex_config_reload_cb", "")
    if not webex_config_file:
        return

    # server section
    webex_config_section["server"] = weechat.config_new_section(
        webex_config_file, "server", 0, 0, "", "", "", "", "", "", "", "", "", "")

    webex_config_option["access_token"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "access_token", "string", "Access Token", "", 0, 0,
        "token", "token", 0, "", "", "", "", "", "")
    webex_config_option["autojoin_last"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "autojoin_last", "integer", "Autojoin this number of rooms/chats (0 to disable)", "", 0, 100,
        "0", "0", 0, "", "", "", "", "", "")
    webex_config_option["autojoin_rooms"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "autojoin_rooms", "string", "Rooms to join on startup", "", 0, 0,
        "", "", 0, "", "", "", "", "", "")
    webex_config_option["autojoin_directs"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "autojoin_directs", "string", "1:1 chats to open on startup", "", 0, 0,
        "", "", 0, "", "", "", "", "", "")
    webex_config_option["default_domain"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "default_domain", "string", "Default domain for emails.", "", 0, 0,
        "", "", 0, "", "", "", "", "", "")
    webex_config_option["node_bin_path"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "node_bin_path", "string", "The path to 'node' binary", "", 0, 0,
        "/usr/local/bin/node", "/usr/local/bin/node", 0, "", "", "", "", "", "")
    webex_config_option["websocket_proxy_app"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "websocket_proxy_app", "string", "The path to websocket proxy application 'app.js'", "", 0, 0,
        "/opt/hookbuster/app.js", "/opt/hookbuster/app.js", 0, "", "", "", "", "", "")
    webex_config_option["port"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "port", "integer", "The port used to create the socket (for incoming message using websocket)", "", 1, 65535,
        "8080", "8080", 0, "", "", "", "", "", "")
    webex_config_option["debug"] = weechat.config_new_option(
        webex_config_file, webex_config_section["server"],
        "debug", "string", "debug mode (yes/no)", "", 0, 0,
        "no", "no", 0, "", "", "", "", "", "")


def webex_config_reload_cb(data, config_file):
    """ Reload config file. """
    return weechat.config_reload(config_file)


def webex_config_read():
    """ Read config file. """
    global webex_config_file
    return weechat.config_read(webex_config_file)


def webex_config_write():
    """ Write config file """
    global webex_config_file
    return weechat.config_write(webex_config_file)


# ================================[ commands ]================================

def webex_hook_commands_and_completions():
    """ Hook commands and completions. """
    weechat.hook_command("wmsg", "Open a chat with a person",
                         "<buddy>",
                         " buddy: user name (like arnaud.morin)",
                         "",
                         "webex_cmd_wmsg", "")
    weechat.hook_command("wj", "Join a room",
                         "<room>",
                         " room: room name",
                         "",
                         "webex_cmd_wj", "")
    weechat.hook_command("wsr", "Search room based on name",
                         "<name>",
                         " name: room name",
                         "",
                         "webex_cmd_wsr", "")
    weechat.hook_command("wsp", "Search user based on name",
                         "<name>",
                         " name: user name (like arnaud)",
                         "",
                         "webex_cmd_wsp", "")
    weechat.hook_command("b", "Switch to buffer based on name",
                         "<name>",
                         " name: user name (like arnaud)",
                         "",
                         "webex_cmd_b", "")
    weechat.hook_command("wreconnect", "Reconnect to webex",
                         "",
                         "",
                         "",
                         "webex_cmd_reconnect", "")
    weechat.hook_command("wim", "Open VIM to allow editing multiline messages",
                         "",
                         "",
                         "",
                         "webex_cmd_wim", "")


def webex_cmd_wmsg(data, buffer, buddy):
    """ Send a message to a person """
    global webex_server
    webex_server.prnt(f"Opening chat with {buddy}")

    # Add domain if not set
    if '@' not in buddy:
        buddy = f"{buddy}@{webex_server.domain}"
    person = webex_server.get_person(buddy)
    if person:
        buddy = Buddy(person)
        webex_server.prnt(f"Found person: {buddy.email}")
        if buddy.domain == webex_server.domain:
            buf_name = buddy.name
        else:
            buf_name = buddy.email
        chat = Chat(webex_server, buf_name, buddy.id, "private")
        webex_server.chats.append(chat)
    else:
        webex_server.prnt(f"No room found with name {buddy}")

    return weechat.WEECHAT_RC_OK


def webex_cmd_b(data, buffer, buddy):
    """ Switch to a buffer """
    global webex_server
    chat = next((x for x in webex_server.chats if buddy.lower() in x.name.lower()), None)
    if chat:
        webex_server.prnt(f"Opening buffer {buddy}")
        weechat.buffer_set(chat.buffer, "display", "1")
    else:
        # Maybe it's a number?
        try:
            number = int(buddy)
            chat = next((x for x in webex_server.chats if number == x.get_number()), None)
            if chat:
                webex_server.prnt(f"Opening buffer {buddy}")
                weechat.buffer_set(chat.buffer, "display", "1")
        except Exception:
            pass

    return weechat.WEECHAT_RC_OK


def webex_cmd_wj(data, buffer, room_name):
    """ Join a room """
    global webex_server
    webex_server.prnt(f"Trying to join {room_name}")

    room = webex_server.search_room(room_name)
    if room:
        webex_server.prnt(f"Found room: {room.title}")
        chat = Chat(webex_server, room.title, room.id, "channel")
        webex_server.chats.append(chat)
    else:
        webex_server.prnt(f"No room found with name {room_name}")

    return weechat.WEECHAT_RC_OK


def webex_cmd_wsr(data, buffer, room_name):
    """ Search a room """
    global webex_server
    webex_server.prnt(f"List of room with '{room_name}' in name:")

    rooms = webex_server.search_rooms(room_name)
    for room in rooms:
        webex_server.prnt(f" - {room.title}")

    return weechat.WEECHAT_RC_OK


def webex_cmd_wsp(data, buffer, name):
    """ Search a person """
    global webex_server
    webex_server.prnt(f"List of people with '{name}' in name:")

    persons = webex_server.search_persons(name)
    for person in persons:
        webex_server.prnt(f" - {person.emails[0]}")

    return weechat.WEECHAT_RC_OK


def webex_cmd_reconnect(data, buffer, access_token):
    """ Reconnect to webex """
    global webex_server, webex_config_option
    if access_token:
        weechat.config_option_set(webex_config_option["access_token"], access_token, 1)

    # Do full reconnect if sock not working
    if not webex_server.sock:
        webex_server.connect()
    else:
        webex_server.connect_webex()
        webex_server.start_websocket(force=True)

    return weechat.WEECHAT_RC_OK


def webex_cmd_wim(data, buffer, args):
    """ Open VIM """
    global webex_server
    chat = next((x for x in webex_server.chats if buffer == x.buffer), None)

    # TODO Maybe add an option for this
    editor = "vim -f"
    path = "/tmp/weechat_longmsg.txt"
    open(path, "w+")

    cmd = editor.split() + [path]
    code = subprocess.Popen(cmd).wait()
    if code != 0:
        os.remove(path)
        weechat.command(buffer, "/window refresh")
        return weechat.WEECHAT_RC_ERROR

    with open(path) as f:
        text = f.read()
        chat.send_message(text)

    os.remove(path)
    weechat.command(buffer, "/window refresh")

    return weechat.WEECHAT_RC_OK


# ================================[ server ]==================================
class Server(object):
    def __init__(self):
        self.chats = []
        self.webexapi = None
        self.buddy = None
        self.hook = None
        self.sock = None
        self.domain = None
        self.buffer = ""
        self.websocket = None
        self.cache = {'rooms': [], 'directs': []}

    def connect(self):
        """ Connect """
        if not self.connect_webex():
            return False

        if not self.buffer:
            bufname = f"{SCRIPT_NAME}.{self.domain}"
            self.buffer = weechat.buffer_search("python", bufname)
            if not self.buffer:
                self.buffer = weechat.buffer_new(bufname,
                                                 "webex_buffer_input_cb", "",
                                                 "webex_buffer_close_cb", "")
            if self.buffer:
                weechat.buffer_set(self.buffer, "short_name", SCRIPT_NAME)
                weechat.buffer_set(self.buffer, "input_multiline", "1")
                weechat.buffer_set(self.buffer, "localvar_set_type", "server")
                weechat.buffer_set(self.buffer, "localvar_set_server", self.domain)
                weechat.buffer_set(self.buffer, "display", "auto")

        self.prnt(f"Bienvenue {self.buddy.name}")

        # Join rooms that are in config
        rooms_to_join = self.get_config_value("autojoin_rooms")
        if rooms_to_join:
            for room_name in rooms_to_join.split(','):
                room = self.search_room(room_name, use_cache=True)
                if room:
                    self.chats.append(Chat(self, room.title, room.id, "channel", auto=False))

        # Join direct chats that are in config
        directs_to_join = self.get_config_value("autojoin_directs")
        if directs_to_join:
            for email in directs_to_join.split(','):
                if '@' not in email:
                    email = f"{email}@{self.domain}"
                buddy = Buddy(self.get_person(email))
                if buddy:
                    if buddy.domain == self.domain:
                        buf_name = buddy.name
                    else:
                        buf_name = buddy.email
                    self.chats.append(Chat(self, buf_name, buddy.id, "private", auto=False))

        # Autojoin last rooms
        autojoin_last = self.get_config_int_value("autojoin_last")
        if autojoin_last > 0:
            rooms = self.list_rooms(type=None, max=autojoin_last)
            if rooms:
                for room in rooms:
                    if room.type == 'group':
                        self.chats.append(Chat(self, room.title, room.id, "channel", auto=False))
                    else:
                        person = self.get_person_from_id(room.id)
                        if person:
                            buddy = Buddy(self.get_person_from_id(room.id))
                            if buddy.domain == self.domain:
                                buf_name = buddy.name
                            else:
                                buf_name = buddy.email
                            self.chats.append(Chat(self, buf_name, buddy.id, "private", auto=False))

        # SOCKET
        # We bind on 127.0.0.1:{port/8080}
        # So we need a proxy pass (like nginx or apache)
        # to forward the request to this socket
        if not self.sock:
            self.prnt("Starting socket")
            try:
                port = self.get_config_int_value("port")
                self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.sock.bind(("127.0.0.1", port))
                self.sock.listen(5)
                self.prnt(f"Socket listening on {self.sock.getsockname()}")
            except Exception as e:
                self.prnt(f"Error while creating the HTTP server: {e}")
                self.disconnect()
                self.sock = None

        # WEECHAT HOOK
        if not self.hook:
            try:
                self.hook = weechat.hook_fd(self.sock.fileno(), 1, 0, 0, "socket_cb", "")
            except Exception as e:
                self.prnt(f"Error while creating the weechat hook: {e}")
                self.disconnect()
                self.hook = None

        # WEBEX HOOKBUSTER / Websocket proxy
        self.start_websocket(force=False)

        return True

    def debug(self, message):
        if self.get_config_value('debug') == 'yes':
            self.prnt(f"[debug] {message}")

    def start_websocket(self, force):
        """(Re-)start the hookbuster/websocket proxy"""
        if force and self.websocket:
            weechat.unhook(self.websocket)
            self.websocket = None

        if not self.websocket:
            self.prnt("Starting websocket proxy")
            token = self.get_config_value("access_token")
            node = self.get_config_value("node_bin_path")
            ws_app = self.get_config_value("websocket_proxy_app")
            port = self.get_config_int_value("port")
            command = f"{node} {ws_app} {token} {port}"
            # self.prnt(command)
            self.websocket = weechat.hook_process(command, 0, '', '')

    def connect_webex(self):
        """Connect to webex"""
        # API
        try:
            self.webexapi = WebexTeamsAPI(access_token=self.get_config_value("access_token"))
        except Exception as e:
            self.prnt(f"Error while trying to connect to webex API: {e}")
            return False

        self.domain = self.get_config_value("default_domain")

        # Test API and grab your name
        try:
            buddy = self.webexapi.people.me()
        except Exception as e:
            self.prnt(f"Error while trying to get me(): {e}")
            return False
        self.buddy = Buddy(buddy)
        self.prnt(f"Bienvenue {self.buddy.name}")

        return True

    def disconnect(self):
        if self.sock:
            self.sock.close()
            self.sock = None
        if self.hook:
            weechat.unhook(self.hook)
            self.hook = None
        if self.websocket:
            weechat.unhook(self.websocket)
            self.websocket = None

    def list_rooms(self, type="group", max=100, use_cache=False):
        """Grab room list from webex"""
        # Force a refresh if cache is empty
        if self.cache['rooms'] == []:
            use_cache = False
        if not use_cache:
            self.cache['rooms'] = self.webexapi.rooms.list(type=type, sortBy="lastactivity", max=max)
        return self.cache['rooms']

    def search_room(self, name, use_cache=False):
        """Search for a room by name. Return first room that match"""
        for room in self.list_rooms(use_cache=use_cache):
            if name in room.title:
                return room

    def search_rooms(self, name):
        """Search for rooms by name. Return all rooms that match"""
        result = []
        for room in self.list_rooms():
            if name.lower() in room.title.lower():
                result.append(room)
        return result

    def get_room_from_id(self, room_id):
        """Get a room from ID"""
        return self.webexapi.rooms.get(room_id)

    def get_person(self, email):
        """Get person from email"""
        try:
            people = list(self.webexapi.people.list(email=email))[0]
            return people
        except Exception:
            return None

    def get_person_from_id(self, id):
        """Get person from id"""
        try:
            return self.webexapi.people.get(id)
        except Exception:
            return None

    def search_persons(self, name):
        """Search for buddies by name. Return all buddies that match"""
        return list(self.webexapi.people.list(displayName=name))

    def get_config_value(self, option):
        """ Get an option """
        global webex_config_option
        return weechat.config_string(webex_config_option[option])

    def get_config_int_value(self, option):
        """ Get an option """
        global webex_config_option
        return weechat.config_integer(webex_config_option[option])

    def prnt(self, message):
        weechat.prnt(self.buffer, message)

    def send_room_message(self, room_id, message):
        self.webexapi.messages.create(roomId=room_id, markdown=message)

    def send_direct_message(self, person_id, message):
        self.webexapi.messages.create(toPersonId=person_id, markdown=message)

    def receive_message(self, raw):
        self.debug(raw)
        try:
            data = json.loads(raw)
            if 'data' in data:
                # Discard message from myself
                if data['data']['personId'] == self.buddy.id:
                    self.debug("Message from myself")
                    pass
                # Messages from a person
                elif data['data']['roomType'] == "direct":
                    chat = next((x for x in self.chats if x.id == data['data']['personId']), None)
                    # If this is first time we are talking to this person
                    # Create a new chat
                    if not chat:
                        buddy = Buddy(self.get_person_from_id(data['data']['personId']))
                        if buddy.domain == self.domain:
                            buf_name = buddy.name
                        else:
                            buf_name = buddy.email
                        chat = Chat(self, buf_name, buddy.id, "private", auto=False)
                        self.chats.append(chat)
                    self.debug(f"Receive a message from a person {chat.name}")
                    chat.receive_message(data['data'])
                # Messages for a room
                elif data['data']['roomType'] == "group":
                    if data['data']['roomId'] in [x.id for x in self.chats] or get_real_room_id(data['data']['roomId']) in [x.real_id for x in self.chats]:
                        # Message for an already opened room
                        chat = next((x for x in self.chats if x.id == data['data']['roomId']), None)
                        if not chat:
                            chat = next((x for x in self.chats if x.real_id == get_real_room_id(data['data']['roomId'])), None)
                        self.debug(f"Receive a message for room {chat.name}")
                        chat.receive_message(data['data'])
                    else:
                        # Message for a non opened room, but maybe with a mention?
                        if f'@{<EMAIL>.buddy.name}' in WebexParser.parse(data['data']['text']):
                            # Create a new chat
                            room = self.get_room_from_id(data['data']['roomId'])
                            if not room:
                                room = self.get_room_from_id(get_real_room_id(data['data']['roomId']))
                            chat = Chat(self, room.title, room.id, "channel", auto=False)
                            self.chats.append(chat)
                            self.debug(f"Receive a message for closed room {chat.name} with mention @{self.buddy.name}")
                            chat.receive_message(data['data'])
        except Exception as e:
            self.prnt(f"Error while receiving data {e}")


def get_real_room_id(room_encoded):
    """Get a real room ID from a base64 encoded room ID"""
    # I found this from a discussion with cisco
    # roomId car be different is you are comming from a different endpoint (US/EU)
    # This is maybe because of something hardcoded in hookbuster (cisco was not sure)
    # They said they would take a look
    try:
        room_decode = base64.decodebytes(bytes(room_encoded, 'utf-8')).decode("utf-8").split('/')[-1]
    except Exception:
        room_decode = None
    return room_decode


def get_chat_from_buffer(buffer):
    """ Search a chat from a buffer. """
    global webex_server
    for chat in webex_server.chats:
        if chat.buffer == buffer:
            return chat
    return None


def get_chat_from_name(name):
    """ Search a chat from a name. """
    global webex_server
    for chat in webex_server.chats:
        print(chat.name)
        if chat.name == name:
            return chat
    return None


# =================================[ chats ]==================================

class Chat:
    """ Class to manage private chat or rooms. """

    def __init__(self, server, name, id, type="channel", auto=True):
        """ Init chat """
        self.server = server
        # TODO(arnaud) maybe add an option to remove the # in front of channels
        self.name = f"#{name}" if type == 'channel' else f"{name}"
        self.id = f"{id}"
        self.buffer = weechat.buffer_search("python", self.id)
        self.type = type    # can be channel or private
        if not self.buffer:
            self.buffer = weechat.buffer_new(self.name,
                                             "webex_buffer_input_cb", "",
                                             "webex_buffer_close_cb", "")
        if self.buffer:
            weechat.buffer_set(self.buffer, "title", self.name)
            weechat.buffer_set(self.buffer, "short_name", self.name)
            weechat.buffer_set(self.buffer, "input_multiline", "1")
            weechat.buffer_set(self.buffer, "localvar_set_type", self.type)     # I use this in external plugin (notification/autosort)
            weechat.hook_signal_send("logger_backlog",
                                     weechat.WEECHAT_HOOK_SIGNAL_POINTER, self.buffer)
            if auto:
                weechat.buffer_set(self.buffer, "display", "auto")

        # Store real ID if possible
        if self.type == "channel":
            self.real_id = get_real_room_id(self.id)
        else:
            self.real_id = None

    def get_number(self):
        return weechat.buffer_get_integer(self.buffer, "number")

    def prnt(self, message):
        """ Print a message in the buffer """
        weechat.prnt(self.buffer, message)

    def receive_message(self, message):
        """ Receive a message from someone """
        try:
            buddy_name, buddy_domain = message['personEmail'].split('@')
            if buddy_domain != self.server.domain:
                buddy_name = message['personEmail']
            weechat.prnt_date_tags(self.buffer, 0,
                                   "notify_private,nick_%s,prefix_nick_%s,log1" %
                                   (buddy_name,
                                    weechat.config_string(weechat.config_get("weechat.color.chat_nick_other"))),
                                   "%s%s\t%s" % (weechat.color("chat_nick_other"),
                                                 buddy_name,
                                                 WebexParser.parse(message['text'])))
        except Exception as e:
            self.prnt(f"Error while receiving a message: {e}")

    def send_message(self, message):
        """ Send message """
        parsed_message = WebexMentionParser.parse(message, self.server.domain)
        if self.type == "channel":
            self.server.send_room_message(self.id, parsed_message)
        else:
            self.server.send_direct_message(self.id, parsed_message)
        weechat.prnt_date_tags(self.buffer, 0,
                               "notify_none,no_highlight,nick_%s,prefix_nick_%s,log1" %
                               (self.server.buddy.name,
                                weechat.config_string(weechat.config_get("weechat.color.chat_nick_self"))),
                               "%s%s\t%s" % (weechat.color("chat_nick_self"),
                                             self.server.buddy.name,
                                             message))

    def delete(self):
        """ Delete chat. """
        if self.buffer:
            self.buffer = None


# ================================[ buddy ]=================================
class Buddy(object):
    def __init__(self, data):
        self.id = data.id
        self.email = data.emails[0]
        self.name, self.domain = self.parse_email(self.email)

    def parse_email(self, email):
        """Parse email to grab name"""
        return email.split('@')


# ================================[ HTTP ]=================================

class HTTPRequest(BaseHTTPRequestHandler):
    def __init__(self, raw_http_request):
        self.rfile = BytesIO(raw_http_request.encode('utf-8'))
        self.raw_requestline = self.rfile.readline()
        self.error_code = self.error_message = None
        self.parse_request()

        # Headers
        self.headers = dict(self.headers)
        # Data
        try:
            self.data = raw_http_request[raw_http_request.index('\n\n') + 2:].rstrip()
        except ValueError:
            self.data = None


def http_reply(conn, code, extra_header, message, mimetype='text/html'):
    """Send a HTTP reply to client."""
    global urlserver_settings
    if extra_header:
        extra_header += '\r\n'
    s = 'HTTP/1.1 %s\r\n' \
        '%s' \
        'Content-Type: %s\r\n' \
        'Content-Length: %d\r\n' \
        '\r\n' \
        % (code, extra_header, mimetype, len(message))
    msg = None
    if type(message) is bytes:
        msg = s.encode('utf-8') + message
    else:
        msg = s.encode('utf-8') + message.encode('utf-8')
    conn.sendall(msg)


# ================================[ parsers ]=================================
class WebexParser(HTMLParser):
    """A simple HTML parser that throws away anything but newlines and links"""

    @staticmethod
    def parse(data):
        """Create a temporary parser and parse a single string"""
        parser = WebexParser()
        parser.feed(data)
        parser.close()
        return parser.result

    def reset(self):
        """Forget all state, called from __init__"""
        HTMLParser.reset(self)
        self.result = ''
        self.linktarget = ''
        self.linkstart = 0
        self.insparkmention = False

    def handle_starttag(self, tag, attrs):
        """Called when a start tag is encountered"""
        if tag == 'br':
            self.result += '\n'
        elif tag == 'a':
            attrs = dict(attrs)
            if 'href' in attrs:
                self.result += '['
                self.linktarget = attrs['href']
                self.linkstart = len(self.result)
        elif tag == 'spark-mention':
            self.result += '@'
            self.insparkmention = True

    def handle_endtag(self, tag):
        """Called when an end tag is encountered"""
        if tag == 'a':
            if self.linktarget:
                if self.result[self.linkstart:] == self.linktarget:
                    self.result += ']'
                else:
                    self.result += ']({})'.format(self.linktarget)
                self.linktarget = ''
        elif tag == 'spark-mention':
            self.insparkmention = False

    def handle_data(self, data):
        """Called for character data (i.e. text)"""
        if self.insparkmention:
            self.result += data.replace(" ", ".").lower()
        else:
            self.result += data


class WebexMentionParser(object):

    @staticmethod
    def parse(data, default_domain):
        """Parse data to replace @xxx.yyy with correct spark mention"""
        result = ''
        if '@' in data:
            # We have @ in data, but it can be a mail address
            # Let split sentence in words
            for w in data.split():
                # If word starts with @ (a mention)
                if w.startswith('@'):
                    # If we want to mention all
                    if w == '@all':
                        result += ' <@all>'
                    # If mention is already with full email
                    elif '@' in w[1:]:
                        result += ' ' + re.sub(r'@(\S+)', r'<@personEmail:\1>', w)
                    # If not, add default domain
                    else:
                        result += ' ' + re.sub(r'@(\S+)', r'<@personEmail:\1@{}>'.format(default_domain), w)
                else:
                    # It was not a mention
                    result += f' {w}'
            return result.strip()
        else:
            # Nothing to do
            return data


# ================================[ callbacks ]=================================

def webex_unload_cb():
    """ Function called when script is unloaded. """
    global webex_server
    webex_server.prnt("Unloading")
    webex_config_write()
    webex_server.disconnect()
    return weechat.WEECHAT_RC_OK


def webex_buffer_input_cb(data, buffer, input_data):
    """ Callback called for input data on a buffer. """
    chat = get_chat_from_buffer(buffer)
    if chat:
        chat.send_message(input_data)

    return weechat.WEECHAT_RC_OK


def webex_buffer_close_cb(data, buffer):
    """ Callback called when a jabber buffer is closed. """
    global webex_server
    chat = get_chat_from_buffer(buffer)
    if chat:
        # Unset the buffer
        chat.delete()
        # Delete the chat from server.chats
        webex_server.chats.remove(chat)

    return weechat.WEECHAT_RC_OK


def socket_cb(data, fd):
    global webex_server
    data = None
    if webex_server.sock.fileno() == int(fd):
        conn, addr = webex_server.sock.accept()
        # Grab data
        try:
            conn.settimeout(0.3)
            data = conn.recv(4096).decode('utf-8')
            data = data.replace('\r\n', '\n')
        except Exception:
            return weechat.WEECHAT_RC_OK

        http_request = HTTPRequest(data)
        webex_server.receive_message(http_request.data)

        http_reply(conn, "200 OK", "", "OK")
        conn.close()
    return weechat.WEECHAT_RC_OK


# ==================================[ main ]==================================

if __name__ == "__main__":
    if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION,
                        SCRIPT_LICENSE, SCRIPT_DESC,
                        "webex_unload_cb", ""):

        webex_hook_commands_and_completions()
        webex_config_init()
        webex_config_read()

        webex_server = Server()
        webex_server.connect()

# -*- coding: utf-8 -*- # # Copyright (C) 2021 <NAME> <<EMAIL>> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program.  If not, see <http://www.gnu.org/licenses/>. # # # Cisco Webex protocol for WeeChat. # # For help, see /help webex # Happy chat, enjoy :) # =================================[ config ]================================= Initialize config file: create sections and options in memory. # This will create webex.conf file # server section Reload config file. Read config file. Write config file # ================================[ commands ]================================ Hook commands and completions. Send a message to a person # Add domain if not set Switch to a buffer # Maybe it's a number? Join a room Search a room Search a person Reconnect to webex # Do full reconnect if sock not working Open VIM # TODO Maybe add an option for this # ================================[ server ]================================== Connect # Join rooms that are in config # Join direct chats that are in config # Autojoin last rooms # SOCKET # We bind on 127.0.0.1:{port/8080} # So we need a proxy pass (like nginx or apache) # to forward the request to this socket # WEECHAT HOOK # WEBEX HOOKBUSTER / Websocket proxy (Re-)start the hookbuster/websocket proxy # self.prnt(command) Connect to webex # API # Test API and grab your name Grab room list from webex # Force a refresh if cache is empty Search for a room by name. Return first room that match Search for rooms by name. Return all rooms that match Get a room from ID Get person from email Get person from id Search for buddies by name. Return all buddies that match Get an option Get an option # Discard message from myself # Messages from a person # If this is first time we are talking to this person # Create a new chat # Messages for a room # Message for an already opened room # Message for a non opened room, but maybe with a mention? # Create a new chat Get a real room ID from a base64 encoded room ID # I found this from a discussion with cisco # roomId car be different is you are comming from a different endpoint (US/EU) # This is maybe because of something hardcoded in hookbuster (cisco was not sure) # They said they would take a look Search a chat from a buffer. Search a chat from a name. # =================================[ chats ]================================== Class to manage private chat or rooms. Init chat # TODO(arnaud) maybe add an option to remove the # in front of channels # can be channel or private # I use this in external plugin (notification/autosort) # Store real ID if possible Print a message in the buffer Receive a message from someone Send message Delete chat. # ================================[ buddy ]================================= Parse email to grab name # ================================[ HTTP ]================================= # Headers # Data Send a HTTP reply to client. # ================================[ parsers ]================================= A simple HTML parser that throws away anything but newlines and links Create a temporary parser and parse a single string Forget all state, called from __init__ Called when a start tag is encountered Called when an end tag is encountered Called for character data (i.e. text) Parse data to replace @xxx.yyy with correct spark mention # We have @ in data, but it can be a mail address # Let split sentence in words # If word starts with @ (a mention) # If we want to mention all # If mention is already with full email # If not, add default domain # It was not a mention # Nothing to do # ================================[ callbacks ]================================= Function called when script is unloaded. Callback called for input data on a buffer. Callback called when a jabber buffer is closed. # Unset the buffer # Delete the chat from server.chats # Grab data # ==================================[ main ]==================================