Beispiel #1
0
    def init(self) -> None:
        self.name = None
        self.network = None
        self.network_name = None

        self.commands = CommandManager()

        self.mx_register("m.room.message", self.on_mx_message)
Beispiel #2
0
    def init(self) -> None:
        self.name = None
        self.network = None
        self.network_id = None
        self.network_name = None  # deprecated
        self.media = []
        self.lazy_members = {}  # allow lazy joining your own ghost for echo

        self.commands = CommandManager()

        if type(self) == PrivateRoom:
            cmd = CommandParser(prog="WHOIS",
                                description="WHOIS the other user")
            self.commands.register(cmd, self.cmd_whois)

        cmd = CommandParser(
            prog="MAXLINES",
            description=
            "set maximum number of lines per message until truncation or pastebin"
        )
        cmd.add_argument("lines", type=int, nargs="?", help="Number of lines")
        self.commands.register(cmd, self.cmd_maxlines)

        cmd = CommandParser(
            prog="PASTEBIN",
            description="enable or disable automatic pastebin of long messages"
        )
        cmd.add_argument("--enable",
                         dest="enabled",
                         action="store_true",
                         help="Enable pastebin")
        cmd.add_argument("--disable",
                         dest="enabled",
                         action="store_false",
                         help="Disable pastebin (messages will be truncated)")
        cmd.set_defaults(enabled=None)
        self.commands.register(cmd, self.cmd_pastebin)

        self.mx_register("m.room.message", self.on_mx_message)
        self.mx_register("m.room.redaction", self.on_mx_redaction)
Beispiel #3
0
    def init(self):
        self.commands = CommandManager()

        cmd = CommandParser(prog="NETWORKS",
                            description="list available networks")
        self.commands.register(cmd, self.cmd_networks)

        cmd = CommandParser(prog="SERVERS",
                            description="list servers for a network")
        cmd.add_argument("network", help="network name (see NETWORKS)")
        self.commands.register(cmd, self.cmd_servers)

        cmd = CommandParser(prog="OPEN",
                            description="open network for connecting")
        cmd.add_argument("name", help="network name (see NETWORKS)")
        self.commands.register(cmd, self.cmd_open)

        cmd = CommandParser(
            prog="QUIT",
            description="disconnect from all networks",
            epilog=
            ("For quickly leaving all networks and removing configurations in a single command.\n"
             "\n"
             "Additionally this will close current DM session with the bridge.\n"
             ),
        )
        self.commands.register(cmd, self.cmd_quit)

        if self.serv.is_admin(self.user_id):
            cmd = CommandParser(prog="MASKS", description="list allow masks")
            self.commands.register(cmd, self.cmd_masks)

            cmd = CommandParser(
                prog="ADDMASK",
                description="add new allow mask",
                epilog=
                ("For anyone else than the owner to use this bridge they need to be allowed to talk with the bridge bot.\n"
                 "This is accomplished by adding an allow mask that determines their permission level when using the bridge.\n"
                 "\n"
                 "Only admins can manage networks, normal users can just connect.\n"
                 ),
            )
            cmd.add_argument(
                "mask",
                help="Matrix ID mask (eg: @friend:contoso.com or *:contoso.com)"
            )
            cmd.add_argument("--admin",
                             help="Admin level access",
                             action="store_true")
            self.commands.register(cmd, self.cmd_addmask)

            cmd = CommandParser(
                prog="DELMASK",
                description="delete allow mask",
                epilog=
                ("Note: Removing a mask only prevents starting a new DM with the bridge bot. Use FORGET for ending existing"
                 " sessions."),
            )
            cmd.add_argument(
                "mask",
                help="Matrix ID mask (eg: @friend:contoso.com or *:contoso.com)"
            )
            self.commands.register(cmd, self.cmd_delmask)

            cmd = CommandParser(prog="ADDNETWORK",
                                description="add new network")
            cmd.add_argument("name", help="network name")
            self.commands.register(cmd, self.cmd_addnetwork)

            cmd = CommandParser(prog="DELNETWORK",
                                description="delete network")
            cmd.add_argument("name", help="network name")
            self.commands.register(cmd, self.cmd_delnetwork)

            cmd = CommandParser(prog="ADDSERVER",
                                description="add server to a network")
            cmd.add_argument("network", help="network name")
            cmd.add_argument("address", help="server address")
            cmd.add_argument("port",
                             nargs="?",
                             type=int,
                             help="server port",
                             default=6667)
            cmd.add_argument("--tls",
                             action="store_true",
                             help="use TLS encryption",
                             default=False)
            cmd.add_argument(
                "--tls-insecure",
                action="store_true",
                help=
                "ignore TLS verification errors (hostname, self-signed, expired)",
                default=False,
            )
            self.commands.register(cmd, self.cmd_addserver)

            cmd = CommandParser(prog="DELSERVER",
                                description="delete server from a network")
            cmd.add_argument("network", help="network name")
            cmd.add_argument("address", help="server address")
            cmd.add_argument("port",
                             nargs="?",
                             type=int,
                             help="server port",
                             default=6667)
            self.commands.register(cmd, self.cmd_delserver)

            cmd = CommandParser(prog="STATUS", description="list active users")
            self.commands.register(cmd, self.cmd_status)

            cmd = CommandParser(
                prog="FORGET",
                description=
                "remove all connections and configuration of a user",
                epilog=
                ("Kills all connections of this user, removes all user set configuration and makes the bridge leave all rooms"
                 " where this user is in.\n"
                 "If the user still has an allow mask they can DM the bridge again to reconfigure and reconnect.\n"
                 "\n"
                 "This is meant as a way to kick users after removing an allow mask or resetting a user after losing access to"
                 " existing account/rooms for any reason.\n"),
            )
            cmd.add_argument("user",
                             help="Matrix ID (eg: @ex-friend:contoso.com)")
            self.commands.register(cmd, self.cmd_forget)

            cmd = CommandParser(prog="DISPLAYNAME",
                                description="change bridge displayname")
            cmd.add_argument("displayname", help="new bridge displayname")
            self.commands.register(cmd, self.cmd_displayname)

            cmd = CommandParser(prog="AVATAR",
                                description="change bridge avatar")
            cmd.add_argument("url", help="new avatar URL (mxc:// format)")
            self.commands.register(cmd, self.cmd_avatar)

        self.mx_register("m.room.message", self.on_mx_message)
Beispiel #4
0
class ControlRoom(Room):
    commands: CommandManager

    def init(self):
        self.commands = CommandManager()

        cmd = CommandParser(prog="NETWORKS",
                            description="list available networks")
        self.commands.register(cmd, self.cmd_networks)

        cmd = CommandParser(prog="SERVERS",
                            description="list servers for a network")
        cmd.add_argument("network", help="network name (see NETWORKS)")
        self.commands.register(cmd, self.cmd_servers)

        cmd = CommandParser(prog="OPEN",
                            description="open network for connecting")
        cmd.add_argument("name", help="network name (see NETWORKS)")
        self.commands.register(cmd, self.cmd_open)

        cmd = CommandParser(
            prog="QUIT",
            description="disconnect from all networks",
            epilog=
            ("For quickly leaving all networks and removing configurations in a single command.\n"
             "\n"
             "Additionally this will close current DM session with the bridge.\n"
             ),
        )
        self.commands.register(cmd, self.cmd_quit)

        if self.serv.is_admin(self.user_id):
            cmd = CommandParser(prog="MASKS", description="list allow masks")
            self.commands.register(cmd, self.cmd_masks)

            cmd = CommandParser(
                prog="ADDMASK",
                description="add new allow mask",
                epilog=
                ("For anyone else than the owner to use this bridge they need to be allowed to talk with the bridge bot.\n"
                 "This is accomplished by adding an allow mask that determines their permission level when using the bridge.\n"
                 "\n"
                 "Only admins can manage networks, normal users can just connect.\n"
                 ),
            )
            cmd.add_argument(
                "mask",
                help="Matrix ID mask (eg: @friend:contoso.com or *:contoso.com)"
            )
            cmd.add_argument("--admin",
                             help="Admin level access",
                             action="store_true")
            self.commands.register(cmd, self.cmd_addmask)

            cmd = CommandParser(
                prog="DELMASK",
                description="delete allow mask",
                epilog=
                ("Note: Removing a mask only prevents starting a new DM with the bridge bot. Use FORGET for ending existing"
                 " sessions."),
            )
            cmd.add_argument(
                "mask",
                help="Matrix ID mask (eg: @friend:contoso.com or *:contoso.com)"
            )
            self.commands.register(cmd, self.cmd_delmask)

            cmd = CommandParser(prog="ADDNETWORK",
                                description="add new network")
            cmd.add_argument("name", help="network name")
            self.commands.register(cmd, self.cmd_addnetwork)

            cmd = CommandParser(prog="DELNETWORK",
                                description="delete network")
            cmd.add_argument("name", help="network name")
            self.commands.register(cmd, self.cmd_delnetwork)

            cmd = CommandParser(prog="ADDSERVER",
                                description="add server to a network")
            cmd.add_argument("network", help="network name")
            cmd.add_argument("address", help="server address")
            cmd.add_argument("port",
                             nargs="?",
                             type=int,
                             help="server port",
                             default=6667)
            cmd.add_argument("--tls",
                             action="store_true",
                             help="use TLS encryption",
                             default=False)
            cmd.add_argument(
                "--tls-insecure",
                action="store_true",
                help=
                "ignore TLS verification errors (hostname, self-signed, expired)",
                default=False,
            )
            self.commands.register(cmd, self.cmd_addserver)

            cmd = CommandParser(prog="DELSERVER",
                                description="delete server from a network")
            cmd.add_argument("network", help="network name")
            cmd.add_argument("address", help="server address")
            cmd.add_argument("port",
                             nargs="?",
                             type=int,
                             help="server port",
                             default=6667)
            self.commands.register(cmd, self.cmd_delserver)

            cmd = CommandParser(prog="STATUS", description="list active users")
            self.commands.register(cmd, self.cmd_status)

            cmd = CommandParser(
                prog="FORGET",
                description=
                "remove all connections and configuration of a user",
                epilog=
                ("Kills all connections of this user, removes all user set configuration and makes the bridge leave all rooms"
                 " where this user is in.\n"
                 "If the user still has an allow mask they can DM the bridge again to reconfigure and reconnect.\n"
                 "\n"
                 "This is meant as a way to kick users after removing an allow mask or resetting a user after losing access to"
                 " existing account/rooms for any reason.\n"),
            )
            cmd.add_argument("user",
                             help="Matrix ID (eg: @ex-friend:contoso.com)")
            self.commands.register(cmd, self.cmd_forget)

            cmd = CommandParser(prog="DISPLAYNAME",
                                description="change bridge displayname")
            cmd.add_argument("displayname", help="new bridge displayname")
            self.commands.register(cmd, self.cmd_displayname)

            cmd = CommandParser(prog="AVATAR",
                                description="change bridge avatar")
            cmd.add_argument("url", help="new avatar URL (mxc:// format)")
            self.commands.register(cmd, self.cmd_avatar)

        self.mx_register("m.room.message", self.on_mx_message)

    def is_valid(self) -> bool:
        if self.user_id is None:
            return False

        if len(self.members) != 2:
            return False

        return True

    async def show_help(self):
        self.send_notice_html(
            f"<b>Howdy, stranger!</b> You have been granted access to the IRC bridge of <b>{self.serv.server_name}</b>."
        )

        try:
            return await self.commands.trigger("HELP")
        except CommandParserError as e:
            return self.send_notice(str(e))

    async def on_mx_message(self, event) -> bool:
        if event["content"]["msgtype"] != "m.text" or event[
                "user_id"] == self.serv.user_id:
            return True

        try:
            return await self.commands.trigger(event["content"]["body"])
        except CommandParserError as e:
            return self.send_notice(str(e))

    def networks(self):
        networks = {}

        for network, config in self.serv.config["networks"].items():
            config["name"] = network
            networks[network.lower()] = config

        return networks

    async def cmd_masks(self, args):
        msg = "Configured masks:\n"

        for mask, value in self.serv.config["allow"].items():
            msg += "\t{} -> {}\n".format(mask, value)

        self.send_notice(msg)

    async def cmd_addmask(self, args):
        masks = self.serv.config["allow"]

        if args.mask in masks:
            return self.send_notice("Mask already exists")

        masks[args.mask] = "admin" if args.admin else "user"
        await self.serv.save()

        self.send_notice("Mask added.")

    async def cmd_delmask(self, args):
        masks = self.serv.config["allow"]

        if args.mask not in masks:
            return self.send_notice("Mask does not exist")

        del masks[args.mask]
        await self.serv.save()

        self.send_notice("Mask removed.")

    async def cmd_networks(self, args):
        networks = self.serv.config["networks"]

        self.send_notice("Configured networks:")

        for network, data in networks.items():
            self.send_notice(f"\t{network} ({len(data['servers'])} servers)")

    async def cmd_addnetwork(self, args):
        networks = self.networks()

        if args.name.lower() in networks:
            return self.send_notice("Network already exists")

        self.serv.config["networks"][args.name] = {"servers": []}
        await self.serv.save()

        self.send_notice("Network added.")

    async def cmd_delnetwork(self, args):
        networks = self.networks()

        if args.name.lower() not in networks:
            return self.send_notice("Network does not exist")

        # FIXME: check if anyone is currently connected

        # FIXME: if no one is currently connected, leave from all network related rooms

        del self.serv.config["networks"][args.name]
        await self.serv.save()

        return self.send_notice("Network removed.")

    async def cmd_servers(self, args):
        networks = self.networks()

        if args.network.lower() not in networks:
            return self.send_notice("Network does not exist")

        network = networks[args.network.lower()]

        self.send_notice(f"Configured servers for {network['name']}:")

        for server in network["servers"]:
            with_tls = ""
            if server["tls"]:
                if "tls_insecure" in server and server["tls_insecure"]:
                    with_tls = "with insecure TLS"
                else:
                    with_tls = "with TLS"
            self.send_notice(
                f"\t{server['address']}:{server['port']} {with_tls}")

    async def cmd_addserver(self, args):
        networks = self.networks()

        if args.network.lower() not in networks:
            return self.send_notice("Network does not exist")

        network = networks[args.network.lower()]
        address = args.address.lower()

        for server in network["servers"]:
            if server["address"] == address and server["port"] == args.port:
                return self.send_notice("This server already exists.")

        self.serv.config["networks"][network["name"]]["servers"].append({
            "address":
            address,
            "port":
            args.port,
            "tls":
            args.tls,
            "tls_insecure":
            args.tls_insecure
        })
        await self.serv.save()

        self.send_notice("Server added.")

    async def cmd_delserver(self, args):
        networks = self.networks()

        if args.network.lower() not in networks:
            return self.send_notice("Network does not exist")

        network = networks[args.network.lower()]
        address = args.address.lower()

        to_pop = -1
        for i, server in enumerate(network["servers"]):
            if server["address"] == address and server["port"] == args.port:
                to_pop = i
                break

        if to_pop == -1:
            return self.send_notice("No such server.")

        self.serv.config["networks"][network["name"]]["servers"].pop(to_pop)
        await self.serv.save()

        self.send_notice("Server deleted.")

    async def cmd_status(self, args):
        users = set()

        for room in self.serv.find_rooms():
            users.add(room.user_id)

        users = list(users)
        users.sort()

        self.send_notice(f"I have {len(users)} known users:")
        for user in users:
            ncontrol = len(self.serv.find_rooms("ControlRoom", user))

            self.send_notice(f"\t{user} ({ncontrol} open control rooms):")

            for network in self.serv.find_rooms("NetworkRoom", user):
                connected = "not connected"
                channels = "not on any channel"
                privates = "not in any DMs"

                if network.conn and network.conn.connected:
                    connected = f"connected as {network.conn.real_nickname} ({network.get_username()})"

                nchannels = 0
                nprivates = 0

                for room in network.rooms.values():
                    if type(room).__name__ == "PrivateRoom":
                        nprivates += 1
                    if type(room).__name__ == "ChannelRoom":
                        nchannels += 1

                if nprivates > 0:
                    privates = f"in {nprivates} DMs"

                if nchannels > 0:
                    channels = f"on {nchannels} channels"

                self.send_notice(
                    f"\t\t{network.name}, {connected}, {channels}, {privates}")

    async def cmd_forget(self, args):
        if args.user == self.user_id:
            return self.send_notice("I can't forget you, silly!")

        rooms = self.serv.find_rooms(None, args.user)

        if len(rooms) == 0:
            return self.send_notice(
                "No such user. See STATUS for list of users.")

        # disconnect each network room in first pass
        for room in rooms:
            if type(room) == NetworkRoom and room.conn and room.conn.connected:
                self.send_notice(
                    f"Disconnecting {args.user} from {room.name}...")
                await room.cmd_disconnect(Namespace())

        self.send_notice(
            f"Leaving all {len(rooms)} rooms {args.user} was in...")

        # then just forget everything
        for room in rooms:
            self.serv.unregister_room(room.id)

            try:
                await self.serv.api.post_room_leave(room.id)
            except MatrixError:
                pass
            try:
                await self.serv.api.post_room_forget(room.id)
            except MatrixError:
                pass

        self.send_notice(f"Done, I have forgotten about {args.user}")

    async def cmd_displayname(self, args):
        try:
            await self.serv.api.put_user_displayname(self.serv.user_id,
                                                     args.displayname)
        except MatrixError as e:
            self.send_notice(f"Failed to set displayname: {str(e)}")

    async def cmd_avatar(self, args):
        try:
            await self.serv.api.put_user_avatar_url(self.serv.user_id,
                                                    args.url)
        except MatrixError as e:
            self.send_notice(f"Failed to set avatar: {str(e)}")

    async def cmd_open(self, args):
        networks = self.networks()
        name = args.name.lower()

        if name not in networks:
            return self.send_notice("Network does not exist")

        network = networks[name]

        for room in self.serv.find_rooms(NetworkRoom, self.user_id):
            if room.name == network["name"]:
                if self.user_id not in room.members:
                    self.send_notice(f"Inviting back to {room.name}")
                    await self.serv.api.post_room_invite(room.id, self.user_id)
                else:
                    self.send_notice(f"You are already in {room.name}")
                return

        self.send_notice(f"You have been invited to {network['name']}")
        await NetworkRoom.create(self.serv, network["name"], self.user_id)

    async def cmd_quit(self, args):
        rooms = self.serv.find_rooms(None, self.user_id)

        # disconnect each network room in first pass
        for room in rooms:
            if type(room) == NetworkRoom and room.conn and room.conn.connected:
                self.send_notice(f"Disconnecting from {room.name}...")
                await room.cmd_disconnect(Namespace())

        self.send_notice("Closing all channels and private messages...")

        # then just forget everything
        for room in rooms:
            if room.id == self.id:
                continue

            self.serv.unregister_room(room.id)

            try:
                await self.serv.api.post_room_leave(room.id)
            except MatrixError:
                pass
            try:
                await self.serv.api.post_room_forget(room.id)
            except MatrixError:
                pass

        self.send_notice("Goodbye!")
        await asyncio.sleep(1)
        raise RoomInvalidError("Leaving")
    def init(self):
        self.name = None
        self.connected = False
        self.nick = None
        self.username = None
        self.ircname = None
        self.password = None
        self.sasl_username = None
        self.sasl_password = None
        self.autocmd = None

        self.commands = CommandManager()
        self.conn = None
        self.rooms = {}
        self.connlock = asyncio.Lock()
        self.disconnect = True
        self.real_host = "?" * 63  # worst case default
        self.keys = {}  # temp dict of join channel keys

        cmd = CommandParser(
            prog="NICK",
            description="set/change nickname",
            epilog=
            ("You can always see your current nickname on the network without arguments.\n"
             "If connected new nickname will be sent to the server immediately. It may be rejected and an underscore appended"
             " to it automatically.\n"),
        )
        cmd.add_argument("nick", nargs="?", help="new nickname")
        self.commands.register(cmd, self.cmd_nick)

        cmd = CommandParser(
            prog="USERNAME",
            description="set username",
            epilog=
            ("Setting a new username requires reconnecting to the network.\n"
             "\n"
             "Note: If you are a local user it will be replaced by the local part of your Matrix ID.\n"
             "Federated users are generated a shortened digest of their Matrix ID. Bridge admins have an"
             " exception where username will be respected and sent as their ident.\n"
             ),
        )
        cmd.add_argument("username", nargs="?", help="new username")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored username")
        self.commands.register(cmd, self.cmd_username)

        cmd = CommandParser(
            prog="IRCNAME",
            description="set ircname (realname)",
            epilog=(
                "Setting a new ircname requires reconnecting to the network.\n"
            ),
        )
        cmd.add_argument("ircname", nargs="?", help="new ircname")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored ircname")
        self.commands.register(cmd, self.cmd_ircname)

        cmd = CommandParser(
            prog="PASSWORD",
            description="set server password",
            epilog=
            ("You can store your network password using this command and it will be automatically offered on connect.\n"
             "Some networks allow using this to identify with NickServ on connect without sending a separate message.\n"
             "\n"
             "Note: Bridge administrators can trivially see the stored password if they want to.\n"
             ),
        )
        cmd.add_argument("password", nargs="?", help="new password")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored password")
        self.commands.register(cmd, self.cmd_password)

        cmd = CommandParser(
            prog="SASL",
            description="set SASL PLAIN credentials",
            epilog=
            ("If the network supports SASL authentication you can configure them with this command.\n"
             "\n"
             "Note: Bridge administrators can trivially see the stored password if they want to.\n"
             ),
        )
        cmd.add_argument("--username", help="SASL username")
        cmd.add_argument("--password", help="SASL password")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored credentials")
        self.commands.register(cmd, self.cmd_sasl)

        cmd = CommandParser(
            prog="AUTOCMD",
            description="run commands on connect",
            epilog=
            ("If the network you are connecting to does not support server password to identify you automatically"
             " can set this to send a command before joining channels.\n"
             "\n"
             'Example (QuakeNet): AUTOCMD "UMODE +x; MSG [email protected] auth foo bar"\n'
             "Example (OFTC): AUTOCMD NICKSERV identify foo bar\n"),
        )
        cmd.add_argument("command",
                         nargs="*",
                         help="commands separated with ';'")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored command")
        self.commands.register(cmd, self.cmd_autocmd)

        cmd = CommandParser(
            prog="CONNECT",
            description="connect to network",
            epilog=
            ("When this command is invoked the connection to this network will be persisted across disconnects and"
             " bridge restart.\n"
             "Only if the server KILLs your connection it will stay disconnected until CONNECT is invoked again.\n"
             "\n"
             "If you want to cancel automatic reconnect you need to issue the DISCONNECT command.\n"
             ),
        )
        self.commands.register(cmd, self.cmd_connect)

        cmd = CommandParser(
            prog="DISCONNECT",
            description="disconnect from network",
            epilog=
            ("In addition to disconnecting from an active network connection this will also cancel any automatic"
             "reconnection attempt.\n"),
        )
        self.commands.register(cmd, self.cmd_disconnect)

        cmd = CommandParser(prog="RECONNECT",
                            description="reconnect to network")
        self.commands.register(cmd, self.cmd_reconnect)

        cmd = CommandParser(
            prog="RAW",
            description="send raw IRC commands",
            epilog=
            ("Arguments (text) are not quoted in any way so it's possible to send ANY command to the server.\n"
             "This is meant as a last resort if the bridge does not have built-in support for some IRC command.\n"
             "\n"
             "Note: You may need to use colon (:) for multi-word arguments, see the IRC RFC for details.\n"
             ),
        )
        cmd.add_argument("text", nargs="+", help="raw text")
        self.commands.register(cmd, self.cmd_raw)

        cmd = CommandParser(
            prog="QUERY",
            description="start a private chat",
            epilog=
            ("Creates a new DM with the target nick. They do not need to be connected for this command to work.\n"
             ),
        )
        cmd.add_argument("nick", help="target nickname")
        cmd.add_argument("message", nargs="*", help="optional message")
        self.commands.register(cmd, self.cmd_query)

        cmd = CommandParser(
            prog="MSG",
            description="send a message without opening a DM",
            epilog=
            ("If the target nick does not exist on the network an error reply may be generated by the server.\n"
             ),
        )
        cmd.add_argument("nick", help="target nickname")
        cmd.add_argument("message", nargs="+", help="message")
        self.commands.register(cmd, self.cmd_msg)

        cmd = CommandParser(
            prog="NICKSERV",
            description="send a message to NickServ (if supported by network)",
            epilog="Alias: NS",
        )
        cmd.add_argument("message", nargs="+", help="message")
        self.commands.register(cmd, self.cmd_nickserv, ["NS"])

        cmd = CommandParser(
            prog="CHANSERV",
            description="send a message to ChanServ (if supported by network)",
            epilog="Alias: CS",
        )
        cmd.add_argument("message", nargs="+", help="message")
        self.commands.register(cmd, self.cmd_chanserv, ["CS"])

        cmd = CommandParser(
            prog="JOIN",
            description="join a channel",
            epilog=
            ("Any channels joined will be persisted between reconnects.\n"
             "\n"
             "Note: Bridge administrators can trivially see the stored channel key if they want to.\n"
             ),
        )
        cmd.add_argument("channel", help="target channel")
        cmd.add_argument("key", nargs="?", help="channel key")
        self.commands.register(cmd, self.cmd_join)

        cmd = CommandParser(
            prog="PLUMB",
            description="plumb a room",
            epilog=
            ("Plumbs a channel in single-puppeted mode. This will make the bridge join the room and then join the"
             " configured IRC channel.\n"),
        )
        cmd.add_argument(
            "room",
            help="target Matrix room ID (eg. !uniqueid:your-homeserver)")
        cmd.add_argument("channel", help="target channel")
        cmd.add_argument("key", nargs="?", help="channel key")
        self.commands.register(cmd, self.cmd_plumb)

        cmd = CommandParser(prog="UMODE", description="set user modes")
        cmd.add_argument("flags", help="user mode flags")
        self.commands.register(cmd, self.cmd_umode)

        cmd = CommandParser(
            prog="WAIT",
            description="wait specified amount of time",
            epilog=("Use with AUTOCMD to add delays between commands."),
        )
        cmd.add_argument("seconds", help="how many seconds to wait")
        self.commands.register(cmd, self.cmd_wait)

        self.mx_register("m.room.message", self.on_mx_message)
class NetworkRoom(Room):
    # configuration stuff
    name: str
    connected: bool
    nick: str
    username: str
    ircname: str
    password: str
    sasl_username: str
    sasl_password: str
    autocmd: str

    # state
    commands: CommandManager
    conn: Any
    rooms: Dict[str, Room]
    connecting: bool
    real_host: str

    def init(self):
        self.name = None
        self.connected = False
        self.nick = None
        self.username = None
        self.ircname = None
        self.password = None
        self.sasl_username = None
        self.sasl_password = None
        self.autocmd = None

        self.commands = CommandManager()
        self.conn = None
        self.rooms = {}
        self.connlock = asyncio.Lock()
        self.disconnect = True
        self.real_host = "?" * 63  # worst case default
        self.keys = {}  # temp dict of join channel keys

        cmd = CommandParser(
            prog="NICK",
            description="set/change nickname",
            epilog=
            ("You can always see your current nickname on the network without arguments.\n"
             "If connected new nickname will be sent to the server immediately. It may be rejected and an underscore appended"
             " to it automatically.\n"),
        )
        cmd.add_argument("nick", nargs="?", help="new nickname")
        self.commands.register(cmd, self.cmd_nick)

        cmd = CommandParser(
            prog="USERNAME",
            description="set username",
            epilog=
            ("Setting a new username requires reconnecting to the network.\n"
             "\n"
             "Note: If you are a local user it will be replaced by the local part of your Matrix ID.\n"
             "Federated users are generated a shortened digest of their Matrix ID. Bridge admins have an"
             " exception where username will be respected and sent as their ident.\n"
             ),
        )
        cmd.add_argument("username", nargs="?", help="new username")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored username")
        self.commands.register(cmd, self.cmd_username)

        cmd = CommandParser(
            prog="IRCNAME",
            description="set ircname (realname)",
            epilog=(
                "Setting a new ircname requires reconnecting to the network.\n"
            ),
        )
        cmd.add_argument("ircname", nargs="?", help="new ircname")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored ircname")
        self.commands.register(cmd, self.cmd_ircname)

        cmd = CommandParser(
            prog="PASSWORD",
            description="set server password",
            epilog=
            ("You can store your network password using this command and it will be automatically offered on connect.\n"
             "Some networks allow using this to identify with NickServ on connect without sending a separate message.\n"
             "\n"
             "Note: Bridge administrators can trivially see the stored password if they want to.\n"
             ),
        )
        cmd.add_argument("password", nargs="?", help="new password")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored password")
        self.commands.register(cmd, self.cmd_password)

        cmd = CommandParser(
            prog="SASL",
            description="set SASL PLAIN credentials",
            epilog=
            ("If the network supports SASL authentication you can configure them with this command.\n"
             "\n"
             "Note: Bridge administrators can trivially see the stored password if they want to.\n"
             ),
        )
        cmd.add_argument("--username", help="SASL username")
        cmd.add_argument("--password", help="SASL password")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored credentials")
        self.commands.register(cmd, self.cmd_sasl)

        cmd = CommandParser(
            prog="AUTOCMD",
            description="run commands on connect",
            epilog=
            ("If the network you are connecting to does not support server password to identify you automatically"
             " can set this to send a command before joining channels.\n"
             "\n"
             'Example (QuakeNet): AUTOCMD "UMODE +x; MSG [email protected] auth foo bar"\n'
             "Example (OFTC): AUTOCMD NICKSERV identify foo bar\n"),
        )
        cmd.add_argument("command",
                         nargs="*",
                         help="commands separated with ';'")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored command")
        self.commands.register(cmd, self.cmd_autocmd)

        cmd = CommandParser(
            prog="CONNECT",
            description="connect to network",
            epilog=
            ("When this command is invoked the connection to this network will be persisted across disconnects and"
             " bridge restart.\n"
             "Only if the server KILLs your connection it will stay disconnected until CONNECT is invoked again.\n"
             "\n"
             "If you want to cancel automatic reconnect you need to issue the DISCONNECT command.\n"
             ),
        )
        self.commands.register(cmd, self.cmd_connect)

        cmd = CommandParser(
            prog="DISCONNECT",
            description="disconnect from network",
            epilog=
            ("In addition to disconnecting from an active network connection this will also cancel any automatic"
             "reconnection attempt.\n"),
        )
        self.commands.register(cmd, self.cmd_disconnect)

        cmd = CommandParser(prog="RECONNECT",
                            description="reconnect to network")
        self.commands.register(cmd, self.cmd_reconnect)

        cmd = CommandParser(
            prog="RAW",
            description="send raw IRC commands",
            epilog=
            ("Arguments (text) are not quoted in any way so it's possible to send ANY command to the server.\n"
             "This is meant as a last resort if the bridge does not have built-in support for some IRC command.\n"
             "\n"
             "Note: You may need to use colon (:) for multi-word arguments, see the IRC RFC for details.\n"
             ),
        )
        cmd.add_argument("text", nargs="+", help="raw text")
        self.commands.register(cmd, self.cmd_raw)

        cmd = CommandParser(
            prog="QUERY",
            description="start a private chat",
            epilog=
            ("Creates a new DM with the target nick. They do not need to be connected for this command to work.\n"
             ),
        )
        cmd.add_argument("nick", help="target nickname")
        cmd.add_argument("message", nargs="*", help="optional message")
        self.commands.register(cmd, self.cmd_query)

        cmd = CommandParser(
            prog="MSG",
            description="send a message without opening a DM",
            epilog=
            ("If the target nick does not exist on the network an error reply may be generated by the server.\n"
             ),
        )
        cmd.add_argument("nick", help="target nickname")
        cmd.add_argument("message", nargs="+", help="message")
        self.commands.register(cmd, self.cmd_msg)

        cmd = CommandParser(
            prog="NICKSERV",
            description="send a message to NickServ (if supported by network)",
            epilog="Alias: NS",
        )
        cmd.add_argument("message", nargs="+", help="message")
        self.commands.register(cmd, self.cmd_nickserv, ["NS"])

        cmd = CommandParser(
            prog="CHANSERV",
            description="send a message to ChanServ (if supported by network)",
            epilog="Alias: CS",
        )
        cmd.add_argument("message", nargs="+", help="message")
        self.commands.register(cmd, self.cmd_chanserv, ["CS"])

        cmd = CommandParser(
            prog="JOIN",
            description="join a channel",
            epilog=
            ("Any channels joined will be persisted between reconnects.\n"
             "\n"
             "Note: Bridge administrators can trivially see the stored channel key if they want to.\n"
             ),
        )
        cmd.add_argument("channel", help="target channel")
        cmd.add_argument("key", nargs="?", help="channel key")
        self.commands.register(cmd, self.cmd_join)

        cmd = CommandParser(
            prog="PLUMB",
            description="plumb a room",
            epilog=
            ("Plumbs a channel in single-puppeted mode. This will make the bridge join the room and then join the"
             " configured IRC channel.\n"),
        )
        cmd.add_argument(
            "room",
            help="target Matrix room ID (eg. !uniqueid:your-homeserver)")
        cmd.add_argument("channel", help="target channel")
        cmd.add_argument("key", nargs="?", help="channel key")
        self.commands.register(cmd, self.cmd_plumb)

        cmd = CommandParser(prog="UMODE", description="set user modes")
        cmd.add_argument("flags", help="user mode flags")
        self.commands.register(cmd, self.cmd_umode)

        cmd = CommandParser(
            prog="WAIT",
            description="wait specified amount of time",
            epilog=("Use with AUTOCMD to add delays between commands."),
        )
        cmd.add_argument("seconds", help="how many seconds to wait")
        self.commands.register(cmd, self.cmd_wait)

        self.mx_register("m.room.message", self.on_mx_message)

    @staticmethod
    async def create(serv, name, user_id):
        room_id = await serv.create_room(name,
                                         "Network room for {}".format(name),
                                         [user_id])
        room = NetworkRoom(room_id, user_id, serv, [serv.user_id, user_id])
        room.from_config({"name": name})
        await room.save()
        serv.register_room(room)
        await room.show_help()
        return room

    def from_config(self, config: dict):
        if "name" in config:
            self.name = config["name"]
        else:
            raise Exception("No name key in config for NetworkRoom")

        if "connected" in config:
            self.connected = config["connected"]

        if "nick" in config:
            self.nick = config["nick"]

        if "username" in config:
            self.username = config["username"]

        if "ircname" in config:
            self.ircname = config["ircname"]

        if "password" in config:
            self.password = config["password"]

        if "sasl_username" in config:
            self.sasl_username = config["sasl_username"]

        if "sasl_password" in config:
            self.sasl_password = config["sasl_password"]

        if "autocmd" in config:
            self.autocmd = config["autocmd"]

    def to_config(self) -> dict:
        return {
            "name": self.name,
            "connected": self.connected,
            "nick": self.nick,
            "username": self.username,
            "ircname": self.ircname,
            "password": self.password,
            "sasl_username": self.sasl_username,
            "sasl_password": self.sasl_password,
            "autocmd": self.autocmd,
        }

    def is_valid(self) -> bool:
        if self.name is None:
            return False

        # if user leaves network room and it's not connected we can clean it up
        if not self.in_room(self.user_id) and not self.connected:
            return False

        return True

    async def show_help(self):
        self.send_notice_html(
            "Welcome to the network room for <b>{}</b>!".format(self.name))

        try:
            return await self.commands.trigger("HELP")
        except CommandParserError as e:
            return self.send_notice(str(e))

    async def on_mx_message(self, event) -> None:
        if event["content"]["msgtype"] != "m.text" or event[
                "user_id"] == self.serv.user_id:
            return True

        try:
            return await self.commands.trigger(event["content"]["body"])
        except CommandParserError as e:
            return self.send_notice(str(e))

    async def cmd_connect(self, args) -> None:
        await self.connect()

    async def cmd_disconnect(self, args) -> None:
        if not self.disconnect:
            self.send_notice("Aborting connection attempt after backoff.")
            self.disconnect = True

        if self.connected:
            self.connected = False
            await self.save()

        if self.conn:
            self.send_notice("Disconnecting...")
            self.conn.disconnect()

    @connected
    async def cmd_reconnect(self, args) -> None:
        self.send_notice("Reconnecting...")
        self.conn.disconnect()
        await self.connect()

    @connected
    async def cmd_raw(self, args) -> None:
        self.conn.send_raw(" ".join(args.text))

    @connected
    async def cmd_query(self, args) -> None:
        # TODO: validate nick doesn't look like a channel
        target = args.nick.lower()
        message = " ".join(args.message)

        if target in self.rooms:
            room = self.rooms[target]
            await self.serv.api.post_room_invite(room.id, self.user_id)
            self.send_notice("Inviting back to private chat with {}.".format(
                args.nick))
        else:
            room = PrivateRoom.create(self, args.nick)
            self.rooms[room.name] = room
            self.send_notice(
                "You have been invited to private chat with {}.".format(
                    args.nick))

        if len(message) > 0:
            self.conn.privmsg(target, message)
            self.send_notice(
                f"Sent out-of-room message to {target}: {message}")

    @connected
    async def cmd_msg(self, args) -> None:
        # TODO: validate nick doesn't look like a channel
        target = args.nick.lower()
        message = " ".join(args.message)

        self.conn.privmsg(target, message)
        self.send_notice(f"{self.conn.real_nickname} -> {target}: {message}")

    @connected
    async def cmd_nickserv(self, args) -> None:
        message = " ".join(args.message)

        self.send_notice(f"{self.conn.real_nickname} -> NickServ: {message}")
        self.conn.send_raw("NICKSERV " + message)

    @connected
    async def cmd_chanserv(self, args) -> None:
        message = " ".join(args.message)

        self.send_notice(f"{self.conn.real_nickname} -> ChanServ: {message}")
        self.conn.send_raw("CHANSERV " + message)

    @connected
    async def cmd_join(self, args) -> None:
        channel = args.channel

        if re.match(r"^[A-Za-z0-9]", channel):
            channel = "#" + channel

        # cache key so we can store later if join succeeds
        self.keys[channel.lower()] = args.key

        self.conn.join(channel, args.key)

    @connected
    async def cmd_plumb(self, args) -> None:
        channel = args.channel

        if re.match(r"^[A-Za-z0-9]", channel):
            channel = "#" + channel

        if not self.serv.is_admin(self.user_id):
            self.send_notice("Plumbing is currently reserved for admins only.")
            return

        room = await PlumbedRoom.create(id=args.room,
                                        network=self,
                                        channel=channel,
                                        key=args.key)
        self.conn.join(room.name, room.key)

    @connected
    async def cmd_umode(self, args) -> None:
        self.conn.mode(self.conn.real_nickname, args.flags)

    async def cmd_wait(self, args) -> None:
        try:
            seconds = float(args.seconds)
            if seconds > 0 and seconds < 30:
                await asyncio.sleep(seconds)
            else:
                self.send_notice(f"Unreasonable wait time: {args.seconds}")
        except ValueError:
            self.send_notice(f"Invalid wait time: {args.seconds}")

    def get_nick(self):
        if self.nick:
            return self.nick

        return self.user_id.split(":")[0][1:]

    async def cmd_nick(self, args) -> None:
        if args.nick is None:
            nick = self.get_nick()
            if self.conn and self.conn.connected:
                self.send_notice(
                    f"Current nickname: {self.conn.real_nickname} (configured: {nick})"
                )
            else:
                self.send_notice(f"Configured nickname: {nick}")
            return

        self.nick = args.nick
        await self.save()
        self.send_notice("Nickname set to {}".format(self.nick))

        if self.conn and self.conn.connected:
            self.conn.nick(args.nick)

    def get_username(self):
        # allow admins to spoof
        if self.serv.is_admin(self.user_id) and self.username:
            return self.username

        parts = self.user_id.split(":")

        # return mxid digest if federated
        if parts[1] != self.serv.server_name:
            return ("mx-" + b32encode(
                hashlib.sha1(self.user_id.encode("utf-8")).digest()).decode(
                    "utf-8").replace("=", "")[:13].lower())

        # return local part of mx id for local users
        return parts[0][1:]

    async def cmd_username(self, args) -> None:
        if args.remove:
            self.username = None
            await self.save()
            self.send_notice("Username removed.")
            return

        if args.username is None:
            self.send_notice(f"Configured username: {str(self.username)}")
            return

        self.username = args.username
        await self.save()
        self.send_notice(f"Username set to {self.username}")

    async def cmd_ircname(self, args) -> None:
        if args.remove:
            self.ircname = None
            await self.save()
            self.send_notice("Ircname removed.")
            return

        if args.ircname is None:
            self.send_notice(f"Configured ircname: {str(self.ircname)}")
            return

        self.ircname = args.ircname
        await self.save()
        self.send_notice(f"Ircname set to {self.ircname}")

    async def cmd_password(self, args) -> None:
        if args.remove:
            self.password = None
            await self.save()
            self.send_notice("Password removed.")
            return

        if args.password is None:
            self.send_notice(
                f"Configured password: {self.password if self.password else ''}"
            )
            return

        self.password = args.password
        await self.save()
        self.send_notice(f"Password set to {self.password}")

    async def cmd_sasl(self, args) -> None:
        if args.remove:
            self.sasl_username = None
            self.sasl_password = None
            await self.save()
            self.send_notice("SASL credentials removed.")
            return

        if args.username is None and args.password is None:
            self.send_notice(f"SASL username: {self.sasl_username}")
            self.send_notice(f"SASL password: {self.sasl_password}")
            return

        if args.username:
            self.sasl_username = args.username

        if args.password:
            self.sasl_password = args.password

        await self.save()
        self.send_notice("SASL credentials updated.")

    async def cmd_autocmd(self, args) -> None:
        autocmd = " ".join(args.command)

        if args.remove:
            self.autocmd = None
            await self.save()
            self.send_notice("Autocmd removed.")
            return

        if autocmd == "":
            self.send_notice(
                f"Configured autocmd: {self.autocmd if self.autocmd else ''}")
            return

        self.autocmd = autocmd
        await self.save()
        self.send_notice(f"Autocmd set to {self.autocmd}")

    async def connect(self) -> None:
        if self.connlock.locked():
            self.send_notice("Already connecting.")
            return

        async with self.connlock:
            await self._connect()

    async def _connect(self) -> None:
        self.disconnect = False

        if self.conn and self.conn.connected:
            self.send_notice("Already connected.")
            return

        # attach loose sub-rooms to us
        for room in self.serv.find_rooms(PrivateRoom, self.user_id):
            if room.name not in self.rooms and room.network_name == self.name:
                logging.debug(
                    f"NetworkRoom {self.id} attaching PrivateRoom {room.id}")
                room.network = self
                self.rooms[room.name] = room

        for room in self.serv.find_rooms(ChannelRoom, self.user_id):
            if room.name not in self.rooms and room.network_name == self.name:
                logging.debug(
                    f"NetworkRoom {self.id} attaching ChannelRoom {room.id}")
                room.network = self
                self.rooms[room.name] = room

        for room in self.serv.find_rooms(PlumbedRoom, self.user_id):
            if room.name not in self.rooms and room.network_name == self.name:
                logging.debug(
                    f"NetworkRoom {self.id} attaching PlumbedRoom {room.id}")
                room.network = self
                self.rooms[room.name] = room

        # force cleanup
        if self.conn:
            self.conn.close()
            self.conn = None

        network = self.serv.config["networks"][self.name]

        backoff = 10

        while not self.disconnect:
            if self.name not in self.serv.config["networks"]:
                self.send_notice(
                    "This network does not exist on this bridge anymore.")
                return

            if len(network["servers"]) == 0:
                self.connected = False
                self.send_notice("No servers to connect for this network.")
                await self.save()
                return

            for i, server in enumerate(network["servers"]):
                if i > 0:
                    await asyncio.sleep(10)

                try:
                    with_tls = ""
                    ssl_ctx = False
                    if server["tls"]:
                        ssl_ctx = ssl.create_default_context()
                        if "tls_insecure" in server and server["tls_insecure"]:
                            with_tls = " with insecure TLS"
                            ssl_ctx.check_hostname = False
                            ssl_ctx.verify_mode = ssl.CERT_NONE
                        else:
                            with_tls = " with TLS"
                            ssl_ctx.verify_mode = ssl.CERT_REQUIRED

                    self.send_notice(
                        f"Connecting to {server['address']}:{server['port']}{with_tls}..."
                    )

                    if self.sasl_username and self.sasl_password:
                        self.send_notice(
                            f"Using SASL credentials for username {self.sasl_username}"
                        )

                    reactor = HeisenReactor(loop=asyncio.get_event_loop())
                    irc_server = reactor.server()
                    irc_server.buffer_class = buffer.LenientDecodingLineBuffer
                    factory = irc.connection.AioFactory(ssl=ssl_ctx)
                    self.conn = await irc_server.connect(
                        server["address"],
                        server["port"],
                        self.get_nick(),
                        self.password,
                        username=self.username,
                        ircname=self.ircname,
                        connect_factory=factory,
                        sasl_username=self.sasl_username,
                        sasl_password=self.sasl_password,
                    )

                    self.conn.add_global_handler("disconnect",
                                                 self.on_disconnect)

                    self.conn.add_global_handler("welcome", self.on_welcome)
                    self.conn.add_global_handler("umodeis", self.on_umodeis)
                    self.conn.add_global_handler("channelmodeis",
                                                 self.on_pass0)
                    self.conn.add_global_handler("channelcreate",
                                                 self.on_pass0)
                    self.conn.add_global_handler("notopic", self.on_pass0)
                    self.conn.add_global_handler("currenttopic", self.on_pass0)
                    self.conn.add_global_handler("topicinfo", self.on_pass0)
                    self.conn.add_global_handler("namreply", self.on_pass1)
                    self.conn.add_global_handler("endofnames", self.on_pass0)
                    self.conn.add_global_handler("banlist", self.on_pass0)
                    self.conn.add_global_handler("endofbanlist", self.on_pass0)

                    # 400-599
                    self.conn.add_global_handler("nosuchnick", self.on_pass_if)
                    self.conn.add_global_handler("nosuchchannel",
                                                 self.on_pass_if)
                    self.conn.add_global_handler("cannotsendtochan",
                                                 self.on_pass_if)
                    self.conn.add_global_handler("nicknameinuse",
                                                 self.on_nicknameinuse)
                    self.conn.add_global_handler("usernotinchannel",
                                                 self.on_pass1)
                    self.conn.add_global_handler("notonchannel", self.on_pass0)
                    self.conn.add_global_handler("useronchannel",
                                                 self.on_pass1)
                    self.conn.add_global_handler("nologin", self.on_pass1)
                    self.conn.add_global_handler("keyset", self.on_pass)
                    self.conn.add_global_handler("channelisfull", self.on_pass)
                    self.conn.add_global_handler("inviteonlychan",
                                                 self.on_pass)
                    self.conn.add_global_handler("bannedfromchan",
                                                 self.on_pass)
                    self.conn.add_global_handler("badchannelkey",
                                                 self.on_pass0)
                    self.conn.add_global_handler("badchanmask", self.on_pass)
                    self.conn.add_global_handler("nochanmodes", self.on_pass)
                    self.conn.add_global_handler("banlistfull", self.on_pass)
                    self.conn.add_global_handler("cannotknock", self.on_pass)
                    self.conn.add_global_handler("chanoprivsneeded",
                                                 self.on_pass0)

                    # protocol
                    # FIXME: error
                    self.conn.add_global_handler("join", self.on_join)
                    self.conn.add_global_handler("join",
                                                 self.on_join_update_host)
                    self.conn.add_global_handler("kick", self.on_pass)
                    self.conn.add_global_handler("mode", self.on_pass)
                    self.conn.add_global_handler("part", self.on_pass)
                    self.conn.add_global_handler("privmsg", self.on_privmsg)
                    self.conn.add_global_handler("privnotice",
                                                 self.on_privnotice)
                    self.conn.add_global_handler("pubmsg", self.on_pass)
                    self.conn.add_global_handler("pubnotice", self.on_pass)
                    self.conn.add_global_handler("quit", self.on_quit)
                    self.conn.add_global_handler("invite", self.on_invite)
                    # FIXME: action
                    self.conn.add_global_handler("topic", self.on_pass)
                    self.conn.add_global_handler("nick", self.on_nick)
                    self.conn.add_global_handler("umode", self.on_umode)

                    self.conn.add_global_handler("kill", self.on_kill)
                    self.conn.add_global_handler("error", self.on_error)

                    # generated
                    self.conn.add_global_handler("ctcp", self.on_ctcp)
                    self.conn.add_global_handler("action",
                                                 lambda conn, event: None)

                    # anything not handled above
                    self.conn.add_global_handler("unhandled_events",
                                                 self.on_server_message)

                    if not self.connected:
                        self.connected = True
                        await self.save()

                    self.disconnect = False

                    return
                except TimeoutError:
                    self.send_notice("Connection timed out.")
                except irc.client.ServerConnectionError as e:
                    self.send_notice(str(e))
                    logging.exception("Failed to connect")
                    self.disconnect = True
                except Exception as e:
                    self.send_notice(f"Failed to connect: {str(e)}")
                    logging.exception("Failed to connect")

            if not self.disconnect:
                self.send_notice(
                    f"Tried all servers, waiting {backoff} seconds before trying again."
                )
                await asyncio.sleep(backoff)

                if backoff < 60:
                    backoff += 5

        self.send_notice("Connection aborted.")

    def on_disconnect(self, conn, event) -> None:
        self.conn.disconnect()
        self.conn.close()
        self.conn = None

        if self.connected and not self.disconnect:
            self.send_notice("Disconnected, reconnecting...")

            async def later():
                await asyncio.sleep(10)
                if not self.disconnect:
                    await self.connect()

            asyncio.ensure_future(later())
        else:
            self.send_notice("Disconnected.")

    @ircroom_event()
    def on_pass(self, conn, event) -> None:
        logging.warning(
            f"IRC room event '{event.type}' fell through, target was from command."
        )
        source = self.source_text(conn, event)
        args = " ".join(event.arguments)
        source = self.source_text(conn, event)
        target = str(event.target)
        self.send_notice_html(f"<b>{source} {event.type} {target}</b> {args}")

    @ircroom_event()
    def on_pass_if(self, conn, event) -> None:
        self.send_notice(" ".join(event.arguments))

    @ircroom_event()
    def on_pass_or_ignore(self, conn, event) -> None:
        pass

    @ircroom_event(target_arg=0)
    def on_pass0(self, conn, event) -> None:
        logging.warning(
            f"IRC room event '{event.type}' fell through, target was '{event.arguments[0]}'."
        )
        self.send_notice(" ".join(event.arguments))

    @ircroom_event(target_arg=1)
    def on_pass1(self, conn, event) -> None:
        logging.warning(
            f"IRC room event '{event.type}' fell through, target was '{event.arguments[1]}'."
        )
        self.send_notice(" ".join(event.arguments))

    def on_server_message(self, conn, event) -> None:
        self.send_notice(" ".join(event.arguments))

    def on_umodeis(self, conn, event) -> None:
        self.send_notice(f"Your user mode is: {event.arguments[0]}")

    def on_umode(self, conn, event) -> None:
        self.send_notice(
            f"User mode changed for {event.target}: {event.arguments[0]}")

    def source_text(self, conn, event) -> str:
        source = None

        if event.source is not None:
            source = str(event.source.nick)

            if event.source.user is not None and event.source.host is not None:
                source += f" ({event.source.user}@{event.source.host})"
        else:
            source = conn.server

        return source

    @ircroom_event()
    def on_privnotice(self, conn, event) -> None:
        # show unhandled notices in server room
        source = self.source_text(conn, event)
        self.send_notice_html(
            f"Notice from <b>{source}:</b> {event.arguments[0]}")

    @ircroom_event()
    def on_ctcp(self, conn, event) -> None:
        # show unhandled ctcps in server room
        source = self.source_text(conn, event)
        self.send_notice_html(
            f"<b>{source}</b> requested <b>CTCP {event.arguments[0]}</b> (ignored)"
        )

    def on_welcome(self, conn, event) -> None:
        self.on_server_message(conn, event)

        async def later():
            await asyncio.sleep(2)

            if self.autocmd is not None:
                self.send_notice(
                    "Executing autocmd and waiting a bit before joining channels..."
                )
                await self.commands.trigger(self.autocmd,
                                            allowed=[
                                                "RAW", "MSG", "NICKSERV", "NS",
                                                "CHANSERV", "CS", "UMODE",
                                                "WAIT"
                                            ])
                await asyncio.sleep(4)

            channels = []
            keys = []

            for room in self.rooms.values():
                if type(room) is ChannelRoom or type(room) is PlumbedRoom:
                    channels.append(room.name)
                    keys.append(room.key if room.key else "")

            if len(channels) > 0:
                self.send_notice(f"Joining channels {', '.join(channels)}")
                self.conn.join(",".join(channels), ",".join(keys))

        asyncio.ensure_future(later())

    @ircroom_event()
    def on_privmsg(self, conn, event) -> None:
        # slightly backwards
        target = event.source.nick.lower()

        if target not in self.rooms:

            async def later():
                # reuse query command to create a room
                await self.cmd_query(
                    Namespace(nick=event.source.nick, message=[]))

                # push the message
                room = self.rooms[target]
                room.on_privmsg(conn, event)

            asyncio.ensure_future(later())
        else:
            room = self.rooms[target]
            if not room.in_room(self.user_id):
                asyncio.ensure_future(
                    self.serv.api.post_room_invite(self.rooms[target].id,
                                                   self.user_id))

    @ircroom_event()
    def on_join(self, conn, event) -> None:
        target = event.target.lower()

        logging.debug(
            f"Handling JOIN to {target} by {event.source.nick} (we are {self.conn.real_nickname})"
        )

        # create a ChannelRoom in response to JOIN
        if event.source.nick == self.conn.real_nickname and target not in self.rooms:
            logging.debug(
                "Pre-flight check for JOIN ok, going to create it...")
            self.rooms[target] = ChannelRoom.create(self, event.target)

            # pass this event through
            self.rooms[target].on_join(conn, event)

    def on_join_update_host(self, conn, event) -> None:
        # update for split long
        if event.source.nick == self.conn.real_nickname and self.real_host != event.source.host:
            self.real_host = event.source.host
            logging.debug(f"Self host updated to '{self.real_host}'")

    def on_quit(self, conn, event) -> None:
        irc_user_id = self.serv.irc_user_id(self.name, event.source.nick)

        # leave channels
        for room in self.rooms.values():
            if type(room) is ChannelRoom or type(room) is PlumbedRoom:
                room._remove_puppet(irc_user_id)

    def on_nick(self, conn, event) -> None:
        old_irc_user_id = self.serv.irc_user_id(self.name, event.source.nick)
        new_irc_user_id = self.serv.irc_user_id(self.name, event.target)

        # special case where only cases change, ensure will update displayname sometime in the future
        if old_irc_user_id == new_irc_user_id:
            asyncio.ensure_future(
                self.serv.ensure_irc_user_id(self.name, event.target))

        # leave and join channels
        for room in self.rooms.values():
            if type(room) is ChannelRoom or type(room) is PlumbedRoom:
                room.rename(event.source.nick, event.target)

    def on_nicknameinuse(self, conn, event) -> None:
        newnick = event.arguments[0] + "_"
        self.conn.nick(newnick)
        self.send_notice(
            f"Nickname {event.arguments[0]} is in use, trying {newnick}")

    def on_invite(self, conn, event) -> None:
        self.send_notice_html("<b>{}</b> has invited you to <b>{}</b>".format(
            event.source.nick, event.arguments[0]))

    @ircroom_event()
    def on_kill(self, conn, event) -> None:
        if event.target == conn.real_nickname:
            source = self.source_text(conn, event)
            self.send_notice_html(
                f"Killed by <b>{source}</b>: {event.arguments[0]}")

            # do not reconnect after KILL
            self.connected = False

    def on_error(self, conn, event) -> None:
        self.send_notice_html(f"<b>ERROR</b>: {event.target}")
Beispiel #7
0
    def init(self):
        self.name = None
        self.connected = False
        self.nick = None
        self.username = None
        self.ircname = None
        self.password = None
        self.autocmd = None

        self.commands = CommandManager()
        self.conn = None
        self.rooms = {}
        self.connlock = asyncio.Lock()
        self.disconnect = True
        self.real_host = "?" * 63  # worst case default

        cmd = CommandParser(prog="NICK", description="Change nickname")
        cmd.add_argument("nick", nargs="?", help="new nickname")
        self.commands.register(cmd, self.cmd_nick)

        cmd = CommandParser(prog="USERNAME", description="Change username")
        cmd.add_argument("username", nargs="?", help="new username")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored username")
        self.commands.register(cmd, self.cmd_username)

        cmd = CommandParser(prog="IRCNAME", description="Change ircname")
        cmd.add_argument("ircname", nargs="?", help="new ircname")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored ircname")
        self.commands.register(cmd, self.cmd_ircname)

        cmd = CommandParser(prog="PASSWORD", description="Set server password")
        cmd.add_argument("password", nargs="?", help="new password")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored password")
        self.commands.register(cmd, self.cmd_password)

        cmd = CommandParser(
            prog="AUTOCMD",
            description="Run a RAW IRC command on connect (to identify)")
        cmd.add_argument("command", nargs="*", help="raw IRC command")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored command")
        self.commands.register(cmd, self.cmd_autocmd)

        cmd = CommandParser(prog="CONNECT", description="Connect to network")
        self.commands.register(cmd, self.cmd_connect)

        cmd = CommandParser(prog="DISCONNECT",
                            description="Disconnect from network")
        self.commands.register(cmd, self.cmd_disconnect)

        cmd = CommandParser(prog="RECONNECT",
                            description="Reconnect to network")
        self.commands.register(cmd, self.cmd_reconnect)

        cmd = CommandParser(prog="RAW", description="Send raw IRC commands")
        cmd.add_argument("text", nargs="+", help="raw text")
        self.commands.register(cmd, self.cmd_raw)

        cmd = CommandParser(prog="QUERY", description="Start a private chat")
        cmd.add_argument("nick", help="target nickname")
        cmd.add_argument("message", nargs="*", help="optional message")
        self.commands.register(cmd, self.cmd_query)

        cmd = CommandParser(prog="MSG",
                            description="Send a message without opening a DM")
        cmd.add_argument("nick", help="target nickname")
        cmd.add_argument("message", nargs="+", help="message")
        self.commands.register(cmd, self.cmd_msg)

        cmd = CommandParser(prog="JOIN", description="Join a channel")
        cmd.add_argument("channel", help="target channel")
        cmd.add_argument("key", nargs="?", help="channel key")
        self.commands.register(cmd, self.cmd_join)

        self.mx_register("m.room.message", self.on_mx_message)
Beispiel #8
0
class PrivateRoom(Room):
    # irc nick of the other party, name for consistency
    name: str
    network: Optional[NetworkRoom]
    network_id: str
    network_name: Optional[str]
    media: List[List[str]]

    max_lines = 0
    use_pastebin = True
    force_forward = False

    commands: CommandManager

    def init(self) -> None:
        self.name = None
        self.network = None
        self.network_id = None
        self.network_name = None  # deprecated
        self.media = []
        self.lazy_members = {}  # allow lazy joining your own ghost for echo

        self.commands = CommandManager()

        if type(self) == PrivateRoom:
            cmd = CommandParser(prog="WHOIS",
                                description="WHOIS the other user")
            self.commands.register(cmd, self.cmd_whois)

        cmd = CommandParser(
            prog="MAXLINES",
            description=
            "set maximum number of lines per message until truncation or pastebin"
        )
        cmd.add_argument("lines", type=int, nargs="?", help="Number of lines")
        self.commands.register(cmd, self.cmd_maxlines)

        cmd = CommandParser(
            prog="PASTEBIN",
            description="enable or disable automatic pastebin of long messages"
        )
        cmd.add_argument("--enable",
                         dest="enabled",
                         action="store_true",
                         help="Enable pastebin")
        cmd.add_argument("--disable",
                         dest="enabled",
                         action="store_false",
                         help="Disable pastebin (messages will be truncated)")
        cmd.set_defaults(enabled=None)
        self.commands.register(cmd, self.cmd_pastebin)

        self.mx_register("m.room.message", self.on_mx_message)
        self.mx_register("m.room.redaction", self.on_mx_redaction)

    def from_config(self, config: dict) -> None:
        if "max_lines" in config:
            self.max_lines = config["max_lines"]

        if "use_pastebin" in config:
            self.use_pastebin = config["use_pastebin"]

        if "name" not in config:
            raise Exception("No name key in config for ChatRoom")

        self.name = config["name"]

        if "network_id" in config:
            self.network_id = config["network_id"]

        if "media" in config:
            self.media = config["media"]

        # only used for migration
        if "network" in config:
            self.network_name = config["network"]

        if self.network_name is None and self.network_id is None:
            raise Exception(
                "No network or network_id key in config for PrivateRoom")

    def to_config(self) -> dict:
        return {
            "name": self.name,
            "network": self.network_name,
            "network_id": self.network_id,
            "media": self.media[:5],
            "max_lines": self.max_lines,
            "use_pastebin": self.use_pastebin,
        }

    @staticmethod
    def create(network: NetworkRoom, name: str) -> "PrivateRoom":
        logging.debug(
            f"PrivateRoom.create(network='{network.name}', name='{name}')")
        irc_user_id = network.serv.irc_user_id(network.name, name)
        room = PrivateRoom(
            None,
            network.user_id,
            network.serv,
            [irc_user_id, network.serv.user_id],
            [],
        )
        room.name = name.lower()
        room.network = network
        room.network_id = network.id
        room.network_name = network.name

        room.max_lines = network.serv.config["max_lines"]
        room.use_pastebin = network.serv.config["use_pastebin"]

        asyncio.ensure_future(room._create_mx(name))
        return room

    async def _create_mx(self, displayname) -> None:
        if self.id is None:
            irc_user_id = await self.network.serv.ensure_irc_user_id(
                self.network.name, displayname, update_cache=False)
            self.id = await self.network.serv.create_room(
                "{} ({})".format(displayname, self.network.name),
                "Private chat with {} on {}".format(displayname,
                                                    self.network.name),
                [self.network.user_id, irc_user_id],
            )
            self.serv.register_room(self)
            await self.az.intent.user(irc_user_id).ensure_joined(self.id)
            await self.save()
            # start event queue now that we have an id
            self._queue.start()

            # attach to network space
            if self.network.space:
                await self.network.space.attach(self.id)

    def is_valid(self) -> bool:
        if self.network_id is None and self.network_name is None:
            return False

        if self.name is None:
            return False

        if self.user_id is None:
            return False

        if not self.in_room(self.user_id):
            return False

        return True

    def cleanup(self) -> None:
        logging.debug(f"Cleaning up network connected room {self.id}.")

        # cleanup us from network space if we have it
        if self.network and self.network.space:
            asyncio.ensure_future(self.network.space.detach(self.id))

        # cleanup us from network rooms
        if self.network and self.name in self.network.rooms:
            logging.debug(
                f"... and we are attached to network {self.network.id}, detaching."
            )
            del self.network.rooms[self.name]

            # if leaving this room invalidated the network, clean it up
            if not self.network.is_valid():
                logging.debug(
                    f"... and we invalidated network {self.network.id} while cleaning up."
                )
                self.network.serv.unregister_room(self.network.id)
                self.network.cleanup()
                asyncio.ensure_future(
                    self.network.serv.leave_room(self.network.id,
                                                 self.network.members))

        super().cleanup()

    def send_notice(
        self,
        text: str,
        user_id: Optional[str] = None,
        formatted=None,
        fallback_html: Optional[str] = None,
        forward=False,
    ):
        if (self.force_forward or forward
                or self.network.forward) and user_id is None:
            self.network.send_notice(text=f"{self.name}: {text}",
                                     formatted=formatted,
                                     fallback_html=fallback_html)
        else:
            super().send_notice(text=text,
                                user_id=user_id,
                                formatted=formatted,
                                fallback_html=fallback_html)

    def send_notice_html(self,
                         text: str,
                         user_id: Optional[str] = None,
                         forward=False) -> None:
        if (self.force_forward or forward
                or self.network.forward) and user_id is None:
            self.network.send_notice_html(text=f"{self.name}: {text}")
        else:
            super().send_notice_html(text=text, user_id=user_id)

    def pills(self):
        # if pills are disabled, don't generate any
        if self.network.pills_length < 1:
            return None

        ret = {}
        ignore = list(map(lambda x: x.lower(), self.network.pills_ignore))

        # push our own name first
        lnick = self.network.conn.real_nickname.lower()
        if self.user_id in self.displaynames and len(
                lnick) >= self.network.pills_length and lnick not in ignore:
            ret[lnick] = (self.user_id, self.displaynames[self.user_id])

        # assuming displayname of a puppet matches nick
        for member in self.members:
            if not member.startswith(
                    "@" + self.serv.puppet_prefix) or not member.endswith(
                        ":" + self.serv.server_name):
                continue

            if member in self.displaynames:
                nick = self.displaynames[member]
                lnick = nick.lower()
                if len(nick
                       ) >= self.network.pills_length and lnick not in ignore:
                    ret[lnick] = (member, nick)

        return ret

    def on_privmsg(self, conn, event) -> None:
        if self.network is None:
            return

        irc_user_id = self.serv.irc_user_id(self.network.name,
                                            event.source.nick)

        (plain, formatted) = parse_irc_formatting(event.arguments[0],
                                                  self.pills())

        # ignore relaymsgs by us
        if event.tags:
            for tag in event.tags:
                if tag["key"] == "draft/relaymsg" and tag[
                        "value"] == self.network.conn.real_nickname:
                    return

        if event.source.nick == self.network.conn.real_nickname:
            source_irc_user_id = self.serv.irc_user_id(self.network.name,
                                                       event.source.nick)

            if self.lazy_members is None:
                self.send_message(f"You said: {plain}",
                                  formatted=(f"You said: {formatted}"
                                             if formatted else None))
                return
            elif source_irc_user_id not in self.lazy_members:
                # if we are a PM room, remove all other IRC users than the target
                if type(self) == PrivateRoom:
                    target_irc_user_id = self.serv.irc_user_id(
                        self.network.name, self.name)

                    for user_id in self.members:
                        if user_id.startswith(
                                "@" + self.serv.puppet_prefix
                        ) and user_id != target_irc_user_id:
                            if user_id in self.lazy_members:
                                del self.lazy_members[user_id]
                            self.leave(user_id)

                # add self to lazy members list so it'll echo
                self.lazy_members[source_irc_user_id] = event.source.nick

        self.send_message(
            plain,
            irc_user_id,
            formatted=formatted,
            fallback_html=
            f"<b>Message from {str(event.source)}</b>: {html.escape(plain)}",
        )

        # lazy update displayname if we detect a change
        if (not self.serv.is_user_cached(irc_user_id, event.source.nick)
                and irc_user_id not in (self.lazy_members or {})
                and irc_user_id in self.members):
            asyncio.ensure_future(
                self.serv.ensure_irc_user_id(self.network.name,
                                             event.source.nick))

    def on_privnotice(self, conn, event) -> None:
        if self.network is None:
            return

        (plain, formatted) = parse_irc_formatting(event.arguments[0])

        if event.source.nick == self.network.conn.real_nickname:
            self.send_notice(
                f"You noticed: {plain}",
                formatted=(f"You noticed: {formatted}" if formatted else None))
            return

        # if the local user has left this room notify in network
        if self.user_id not in self.members:
            source = self.network.source_text(conn, event)
            self.network.send_notice_html(
                f"Notice from <b>{source}:</b> {formatted if formatted else html.escape(plain)}"
            )
            return

        irc_user_id = self.serv.irc_user_id(self.network.name,
                                            event.source.nick)
        self.send_notice(
            plain,
            irc_user_id,
            formatted=formatted,
            fallback_html=
            f"<b>Notice from {str(event.source)}</b>: {formatted if formatted else html.escape(plain)}",
        )

    def on_ctcp(self, conn, event) -> None:
        if self.network is None:
            return

        # ignore relaymsgs by us
        if event.tags:
            for tag in event.tags:
                if tag["key"] == "draft/relaymsg" and tag[
                        "value"] == self.network.conn.real_nickname:
                    return

        irc_user_id = self.serv.irc_user_id(self.network.name,
                                            event.source.nick)

        command = event.arguments[0].upper()

        if command == "ACTION" and len(event.arguments) > 1:
            (plain, formatted) = parse_irc_formatting(event.arguments[1])

            if event.source.nick == self.network.conn.real_nickname:
                self.send_emote(f"(you) {plain}")
                return

            self.send_emote(
                plain,
                irc_user_id,
                fallback_html=
                f"<b>Emote from {str(event.source)}</b>: {html.escape(plain)}")
        else:
            (plain,
             formatted) = parse_irc_formatting(" ".join(event.arguments))
            self.send_notice_html(
                f"<b>{str(event.source)}</b> requested <b>CTCP {html.escape(plain)}</b> (ignored)"
            )

    def on_ctcpreply(self, conn, event) -> None:
        if self.network is None:
            return

        (plain, formatted) = parse_irc_formatting(" ".join(event.arguments))
        self.send_notice_html(
            f"<b>{str(event.source)}</b> sent <b>CTCP REPLY {html.escape(plain)}</b> (ignored)"
        )

    async def _process_event_content(self, event, prefix, reply_to=None):
        content = event.content

        if content.formatted_body:
            lines = str(await
                        self.parser.parse(content.formatted_body)).split("\n")
        elif content.body:
            body = content.body

            for user_id, displayname in self.displaynames.items():
                body = body.replace(user_id, displayname)

                # FluffyChat prefixes mentions in fallback with @
                body = body.replace("@" + displayname, displayname)

            lines = body.split("\n")
        else:
            logging.warning(
                "_process_event_content called with no usable body")
            return

        # drop all whitespace-only lines
        lines = [x for x in lines if not re.match(r"^\s*$", x)]

        # handle replies
        if reply_to and reply_to.sender != event.sender:
            # resolve displayname
            sender = reply_to.sender
            if sender in self.displaynames:
                sender = self.displaynames[sender]

            # prefix first line with nickname of the reply_to source
            first_line = sender + ": " + lines.pop(0)
            lines.insert(0, first_line)

        messages = []

        for i, line in enumerate(lines):
            # prefix first line if needed
            if i == 0 and prefix and len(prefix) > 0:
                line = prefix + line

            # filter control characters except ZWSP
            line = "".join(
                c for c in line
                if unicodedata.category(c)[0] != "C" or c == "\u200B")

            messages += split_long(
                self.network.conn.real_nickname,
                self.network.real_user,
                self.network.real_host,
                self.name,
                line,
            )

        return messages

    async def _send_message(self, event, func, prefix=""):
        # try to find out if this was a reply
        reply_to = None
        if event.content.get_reply_to():
            rel_event = event

            # traverse back all edits
            while rel_event.content.get_edit():
                rel_event = await self.az.intent.get_event(
                    self.id, rel_event.content.get_edit())

            # see if the original is a reply
            if rel_event.content.get_reply_to():
                reply_to = await self.az.intent.get_event(
                    self.id, rel_event.content.get_reply_to())

        if event.content.get_edit():
            messages = await self._process_event_content(
                event, prefix, reply_to)
            event_id = event.content.relates_to.event_id
            prev_event = self.last_messages[event.sender]
            if prev_event and prev_event.event_id == event_id:
                old_messages = await self._process_event_content(
                    prev_event, prefix, reply_to)

                mlen = max(len(messages), len(old_messages))
                edits = []
                for i in range(0, mlen):
                    try:
                        old_msg = old_messages[i]
                    except IndexError:
                        old_msg = ""
                    try:
                        new_msg = messages[i]
                    except IndexError:
                        new_msg = ""

                    edit = line_diff(old_msg, new_msg)
                    if edit:
                        edits.append(prefix + edit)

                # use edits only if one line was edited
                if len(edits) == 1:
                    messages = edits

                # update last message _content_ to current so re-edits work
                self.last_messages[event.sender].content = event.content
            else:
                # last event was not found so we fall back to full message BUT we can reconstrut enough of it
                self.last_messages[event.sender] = event
        else:
            # keep track of the last message
            self.last_messages[event.sender] = event
            messages = await self._process_event_content(
                event, prefix, reply_to)

        for i, message in enumerate(messages):
            if self.max_lines > 0 and i == self.max_lines - 1 and len(
                    messages) > self.max_lines:
                self.react(event.event_id, "\u2702")  # scissors

                if self.use_pastebin:
                    content_uri = await self.az.intent.upload_media(
                        "\n".join(messages).encode("utf-8"),
                        mime_type="text/plain; charset=UTF-8")

                    if self.max_lines == 1:
                        func(
                            self.name,
                            f"{prefix}{self.serv.mxc_to_url(str(content_uri))} (long message, {len(messages)} lines)",
                        )
                    else:
                        func(
                            self.name,
                            f"... long message truncated: {self.serv.mxc_to_url(str(content_uri))} ({len(messages)} lines)",
                        )
                    self.react(event.event_id, "\U0001f4dd")  # memo

                    self.media.append([event.event_id, str(content_uri)])
                    await self.save()
                else:
                    if self.max_lines == 1:
                        # best effort is to send the first line and give up
                        func(self.name, message)
                    else:
                        func(self.name, "... long message truncated")

                return

            func(self.name, message)

        # show number of lines sent to IRC
        if self.max_lines == 0 and len(messages) > 1:
            self.react(event.event_id, f"\u2702 {len(messages)} lines")

    async def on_mx_message(self, event) -> None:
        if event.sender != self.user_id:
            return

        if self.network is None or self.network.conn is None or not self.network.conn.connected:
            self.send_notice("Not connected to network.")
            return

        if str(event.content.msgtype) == "m.emote":
            await self._send_message(event, self.network.conn.action)
        elif str(event.content.msgtype) in [
                "m.image", "m.file", "m.audio", "m.video"
        ]:
            self.network.conn.privmsg(
                self.name,
                self.serv.mxc_to_url(event.content.url, event.content.body))
            self.react(event.event_id, "\U0001F517")  # link
            self.media.append([event.event_id, event.content.url])
            await self.save()
        elif str(event.content.msgtype) == "m.text":
            # allow commanding the appservice in rooms
            match = re.match(r"^\s*@?([^:,\s]+)[\s:,]*(.+)$",
                             event.content.body)
            if match and match.group(
                    1).lower() == self.serv.registration["sender_localpart"]:
                try:
                    await self.commands.trigger(match.group(2))
                except CommandParserError as e:
                    self.send_notice(str(e))
                finally:
                    return

            await self._send_message(event, self.network.conn.privmsg)

        await self.az.intent.send_receipt(event.room_id, event.event_id)

    async def on_mx_redaction(self, event) -> None:
        for media in self.media:
            if media[0] == event.redacts:
                url = urlparse(media[1])
                if self.serv.synapse_admin:
                    try:
                        await self.az.intent.api.request(
                            Method.POST, SynapseAdminPath.v1.media.quarantine[
                                url.netloc][url.path[1:]])

                        self.network.send_notice(
                            f"Associated media {media[1]} for redacted event {event.redacts} "
                            + f"in room {self.name} was quarantined.")
                    except Exception:
                        self.network.send_notice(
                            f"Failed to quarantine media! Associated media {media[1]} "
                            +
                            f"for redacted event {event.redacts} in room {self.name} is left available."
                        )
                else:
                    self.network.send_notice(
                        f"No permission to quarantine media! Associated media {media[1]} "
                        +
                        f"for redacted event {event.redacts} in room {self.name} is left available."
                    )
                return

    @connected
    async def cmd_whois(self, args) -> None:
        self.network.conn.whois(f"{self.name} {self.name}")

    async def cmd_maxlines(self, args) -> None:
        if args.lines is not None:
            self.max_lines = args.lines
            await self.save()

        self.send_notice(f"Max lines is {self.max_lines}")

    async def cmd_pastebin(self, args) -> None:
        if args.enabled is not None:
            self.use_pastebin = args.enabled
            await self.save()

        self.send_notice(
            f"Pastebin is {'enabled' if self.use_pastebin else 'disabled'}")
Beispiel #9
0
class NetworkRoom(Room):
    # configuration stuff
    name: str
    connected: bool
    nick: str
    username: str
    ircname: str
    password: str
    autocmd: str

    # state
    commands: CommandManager
    conn: Any
    rooms: Dict[str, Room]
    connecting: bool
    real_host: str

    def init(self):
        self.name = None
        self.connected = False
        self.nick = None
        self.username = None
        self.ircname = None
        self.password = None
        self.autocmd = None

        self.commands = CommandManager()
        self.conn = None
        self.rooms = {}
        self.connlock = asyncio.Lock()
        self.disconnect = True
        self.real_host = "?" * 63  # worst case default

        cmd = CommandParser(prog="NICK", description="Change nickname")
        cmd.add_argument("nick", nargs="?", help="new nickname")
        self.commands.register(cmd, self.cmd_nick)

        cmd = CommandParser(prog="USERNAME", description="Change username")
        cmd.add_argument("username", nargs="?", help="new username")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored username")
        self.commands.register(cmd, self.cmd_username)

        cmd = CommandParser(prog="IRCNAME", description="Change ircname")
        cmd.add_argument("ircname", nargs="?", help="new ircname")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored ircname")
        self.commands.register(cmd, self.cmd_ircname)

        cmd = CommandParser(prog="PASSWORD", description="Set server password")
        cmd.add_argument("password", nargs="?", help="new password")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored password")
        self.commands.register(cmd, self.cmd_password)

        cmd = CommandParser(
            prog="AUTOCMD",
            description="Run a RAW IRC command on connect (to identify)")
        cmd.add_argument("command", nargs="*", help="raw IRC command")
        cmd.add_argument("--remove",
                         action="store_true",
                         help="remove stored command")
        self.commands.register(cmd, self.cmd_autocmd)

        cmd = CommandParser(prog="CONNECT", description="Connect to network")
        self.commands.register(cmd, self.cmd_connect)

        cmd = CommandParser(prog="DISCONNECT",
                            description="Disconnect from network")
        self.commands.register(cmd, self.cmd_disconnect)

        cmd = CommandParser(prog="RECONNECT",
                            description="Reconnect to network")
        self.commands.register(cmd, self.cmd_reconnect)

        cmd = CommandParser(prog="RAW", description="Send raw IRC commands")
        cmd.add_argument("text", nargs="+", help="raw text")
        self.commands.register(cmd, self.cmd_raw)

        cmd = CommandParser(prog="QUERY", description="Start a private chat")
        cmd.add_argument("nick", help="target nickname")
        cmd.add_argument("message", nargs="*", help="optional message")
        self.commands.register(cmd, self.cmd_query)

        cmd = CommandParser(prog="MSG",
                            description="Send a message without opening a DM")
        cmd.add_argument("nick", help="target nickname")
        cmd.add_argument("message", nargs="+", help="message")
        self.commands.register(cmd, self.cmd_msg)

        cmd = CommandParser(prog="JOIN", description="Join a channel")
        cmd.add_argument("channel", help="target channel")
        cmd.add_argument("key", nargs="?", help="channel key")
        self.commands.register(cmd, self.cmd_join)

        self.mx_register("m.room.message", self.on_mx_message)

    @staticmethod
    async def create(serv, name, user_id):
        room_id = await serv.create_room(name,
                                         "Network room for {}".format(name),
                                         [user_id])
        room = NetworkRoom(room_id, user_id, serv, [serv.user_id, user_id])
        room.from_config({"name": name})
        await room.save()
        serv.register_room(room)
        await room.show_help()
        return room

    def from_config(self, config: dict):
        if "name" in config:
            self.name = config["name"]
        else:
            raise Exception("No name key in config for NetworkRoom")

        if "connected" in config:
            self.connected = config["connected"]

        if "nick" in config:
            self.nick = config["nick"]

        if "username" in config:
            self.username = config["username"]

        if "ircname" in config:
            self.ircname = config["ircname"]

        if "password" in config:
            self.password = config["password"]

        if "autocmd" in config:
            self.autocmd = config["autocmd"]

    def to_config(self) -> dict:
        return {
            "name": self.name,
            "connected": self.connected,
            "nick": self.nick,
            "username": self.username,
            "ircname": self.ircname,
            "password": self.password,
            "autocmd": self.autocmd,
        }

    def is_valid(self) -> bool:
        if self.name is None:
            return False

        # if user leaves network room and it's not connected we can clean it up
        if not self.in_room(self.user_id) and not self.connected:
            return False

        return True

    async def show_help(self):
        self.send_notice_html(
            "Welcome to the network room for <b>{}</b>!".format(self.name))

        try:
            return await self.commands.trigger("HELP")
        except CommandParserError as e:
            return self.send_notice(str(e))

    async def on_mx_message(self, event) -> None:
        if event["content"]["msgtype"] != "m.text" or event[
                "user_id"] == self.serv.user_id:
            return True

        try:
            return await self.commands.trigger(event["content"]["body"])
        except CommandParserError as e:
            return self.send_notice(str(e))

    async def cmd_connect(self, args) -> None:
        await self.connect()

    async def cmd_disconnect(self, args) -> None:
        if not self.disconnect:
            self.send_notice("Aborting connection attempt after backoff.")
            self.disconnect = True

        if self.connected:
            self.connected = False
            await self.save()

        if self.conn:
            self.send_notice("Disconnecting...")
            self.conn.disconnect()

    @connected
    async def cmd_reconnect(self, args) -> None:
        self.send_notice("Reconnecting...")
        self.conn.disconnect()
        await self.connect()

    @connected
    async def cmd_raw(self, args) -> None:
        self.conn.send_raw(" ".join(args.text))

    @connected
    async def cmd_query(self, args) -> None:
        # TODO: validate nick doesn't look like a channel
        target = args.nick.lower()
        message = " ".join(args.message)

        if target in self.rooms:
            room = self.rooms[target]
            await self.serv.api.post_room_invite(room.id, self.user_id)
            self.send_notice("Inviting back to private chat with {}.".format(
                args.nick))
        else:
            room = PrivateRoom.create(self, args.nick)
            self.rooms[room.name] = room
            self.send_notice(
                "You have been invited to private chat with {}.".format(
                    args.nick))

        if len(message) > 0:
            self.conn.privmsg(target, message)
            self.send_notice(
                f"Sent out-of-room message to {target}: {message}")

    @connected
    async def cmd_msg(self, args) -> None:
        # TODO: validate nick doesn't look like a channel
        target = args.nick.lower()
        message = " ".join(args.message)

        self.conn.privmsg(target, message)
        self.send_notice(f"{self.conn.real_nickname} -> {target}: {message}")

    @connected
    async def cmd_join(self, args) -> None:
        channel = args.channel

        if re.match(r"^[A-Za-z0-9]", channel):
            channel = "#" + channel

        self.conn.join(channel, args.key)

    def get_nick(self):
        if self.nick:
            return self.nick

        return self.user_id.split(":")[0][1:]

    async def cmd_nick(self, args) -> None:
        if args.nick is None:
            nick = self.get_nick()
            if self.conn and self.conn.connected:
                self.send_notice(
                    f"Current nickname: {self.conn.real_nickname} (configured: {nick})"
                )
            else:
                self.send_notice(f"Configured nickname: {nick}")
            return

        self.nick = args.nick
        await self.save()
        self.send_notice("Nickname set to {}".format(self.nick))

        if self.conn and self.conn.connected:
            self.conn.nick(args.nick)

    def get_username(self):
        # allow admins to spoof
        if self.serv.is_admin(self.user_id) and self.username:
            return self.username

        parts = self.user_id.split(":")

        # disallow identd response for remote users
        if parts[1] != self.serv.server_name:
            return None

        # return local part of mx id for local users
        return parts[0][1:]

    async def cmd_username(self, args) -> None:
        if args.remove:
            self.username = None
            await self.save()
            self.send_notice("Username removed.")
            return

        if args.username is None:
            self.send_notice(f"Configured username: {str(self.username)}")
            return

        self.username = args.username
        await self.save()
        self.send_notice(f"Username set to {self.username}")

    async def cmd_ircname(self, args) -> None:
        if args.remove:
            self.ircname = None
            await self.save()
            self.send_notice("Ircname removed.")
            return

        if args.ircname is None:
            self.send_notice(f"Configured ircname: {str(self.ircname)}")
            return

        self.ircname = args.ircname
        await self.save()
        self.send_notice(f"Ircname set to {self.ircname}")

    async def cmd_password(self, args) -> None:
        if args.remove:
            self.password = None
            await self.save()
            self.send_notice("Password removed.")
            return

        if args.password is None:
            self.send_notice(
                f"Configured password: {self.password if self.password else ''}"
            )
            return

        self.password = args.password
        await self.save()
        self.send_notice(f"Password set to {self.password}")

    async def cmd_autocmd(self, args) -> None:
        autocmd = " ".join(args.command)

        if args.remove:
            self.autocmd = None
            await self.save()
            self.send_notice("Autocmd removed.")
            return

        if autocmd == "":
            self.send_notice(
                f"Configured autocmd: {self.autocmd if self.autocmd else ''}")
            return

        self.autocmd = autocmd
        await self.save()
        self.send_notice(f"Autocmd set to {self.autocmd}")

    async def connect(self) -> None:
        if self.connlock.locked():
            self.send_notice("Already connecting.")
            return

        async with self.connlock:
            await self._connect()

    async def _connect(self) -> None:
        self.disconnect = False

        if self.conn and self.conn.connected:
            self.send_notice("Already connected.")
            return

        # attach loose sub-rooms to us
        for room in self.serv.find_rooms(PrivateRoom, self.user_id):
            if room.name not in self.rooms and room.network_name == self.name:
                logging.debug(
                    f"NetworkRoom {self.id} attaching PrivateRoom {room.id}")
                room.network = self
                self.rooms[room.name] = room

        for room in self.serv.find_rooms(ChannelRoom, self.user_id):
            if room.name not in self.rooms and room.network_name == self.name:
                logging.debug(
                    f"NetworkRoom {self.id} attaching ChannelRoom {room.id}")
                room.network = self
                self.rooms[room.name] = room

        # force cleanup
        if self.conn:
            self.conn = None

        network = self.serv.config["networks"][self.name]

        backoff = 10

        while not self.disconnect:
            if self.name not in self.serv.config["networks"]:
                self.send_notice(
                    "This network does not exist on this bridge anymore.")
                return

            if len(network["servers"]) == 0:
                self.connected = False
                self.send_notice("No servers to connect for this network.")
                await self.save()
                return

            for i, server in enumerate(network["servers"]):
                if i > 0:
                    await asyncio.sleep(10)

                try:
                    self.send_notice(
                        f"Connecting to {server['address']}:{server['port']}{' with TLS' if server['tls'] else ''}..."
                    )

                    reactor = irc.client_aio.AioReactor(
                        loop=asyncio.get_event_loop())
                    irc_server = reactor.server()
                    irc_server.buffer_class = buffer.LenientDecodingLineBuffer
                    factory = irc.connection.AioFactory(ssl=server["tls"])
                    self.conn = await irc_server.connect(
                        server["address"],
                        server["port"],
                        self.get_nick(),
                        self.password,
                        username=self.username,
                        ircname=self.ircname,
                        connect_factory=factory,
                    )

                    self.conn.add_global_handler("disconnect",
                                                 self.on_disconnect)

                    # 001-099
                    self.conn.add_global_handler("welcome", self.on_welcome)
                    self.conn.add_global_handler("yourhost",
                                                 self.on_server_message)
                    self.conn.add_global_handler("created",
                                                 self.on_server_message)
                    self.conn.add_global_handler("myinfo",
                                                 self.on_server_message)
                    self.conn.add_global_handler("featurelist",
                                                 self.on_server_message)
                    self.conn.add_global_handler("020", self.on_server_message)

                    # 200-299
                    self.conn.add_global_handler("tracelink",
                                                 self.on_server_message)
                    self.conn.add_global_handler("traceconnecting",
                                                 self.on_server_message)
                    self.conn.add_global_handler("tracehandshake",
                                                 self.on_server_message)
                    self.conn.add_global_handler("traceunknown",
                                                 self.on_server_message)
                    self.conn.add_global_handler("traceoperator",
                                                 self.on_server_message)
                    self.conn.add_global_handler("traceuser",
                                                 self.on_server_message)
                    self.conn.add_global_handler("traceserver",
                                                 self.on_server_message)
                    self.conn.add_global_handler("traceservice",
                                                 self.on_server_message)
                    self.conn.add_global_handler("tracenewtype",
                                                 self.on_server_message)
                    self.conn.add_global_handler("traceclass",
                                                 self.on_server_message)
                    self.conn.add_global_handler("tracereconnect",
                                                 self.on_server_message)
                    self.conn.add_global_handler("statslinkinfo",
                                                 self.on_server_message)
                    self.conn.add_global_handler("statscommands",
                                                 self.on_server_message)
                    self.conn.add_global_handler("statscline",
                                                 self.on_server_message)
                    self.conn.add_global_handler("statsnline",
                                                 self.on_server_message)
                    self.conn.add_global_handler("statsiline",
                                                 self.on_server_message)
                    self.conn.add_global_handler("statskline",
                                                 self.on_server_message)
                    self.conn.add_global_handler("statsqline",
                                                 self.on_server_message)
                    self.conn.add_global_handler("statsyline",
                                                 self.on_server_message)
                    self.conn.add_global_handler("endofstats",
                                                 self.on_server_message)
                    self.conn.add_global_handler("umodeis", self.on_umodeis)
                    self.conn.add_global_handler("serviceinfo",
                                                 self.on_server_message)
                    self.conn.add_global_handler("endofservices",
                                                 self.on_server_message)
                    self.conn.add_global_handler("service",
                                                 self.on_server_message)
                    self.conn.add_global_handler("servlist",
                                                 self.on_server_message)
                    self.conn.add_global_handler("servlistend",
                                                 self.on_server_message)
                    self.conn.add_global_handler("statslline",
                                                 self.on_server_message)
                    self.conn.add_global_handler("statsuptime",
                                                 self.on_server_message)
                    self.conn.add_global_handler("statsoline",
                                                 self.on_server_message)
                    self.conn.add_global_handler("statshline",
                                                 self.on_server_message)
                    self.conn.add_global_handler("luserconns",
                                                 self.on_server_message)
                    self.conn.add_global_handler("luserclient",
                                                 self.on_server_message)
                    self.conn.add_global_handler("luserop",
                                                 self.on_server_message)
                    self.conn.add_global_handler("luserunknown",
                                                 self.on_server_message)
                    self.conn.add_global_handler("luserchannels",
                                                 self.on_server_message)
                    self.conn.add_global_handler("luserme",
                                                 self.on_server_message)
                    self.conn.add_global_handler("adminme",
                                                 self.on_server_message)
                    self.conn.add_global_handler("adminloc1",
                                                 self.on_server_message)
                    self.conn.add_global_handler("adminloc2",
                                                 self.on_server_message)
                    self.conn.add_global_handler("adminemail",
                                                 self.on_server_message)
                    self.conn.add_global_handler("tracelog",
                                                 self.on_server_message)
                    self.conn.add_global_handler("endoftrace",
                                                 self.on_server_message)
                    self.conn.add_global_handler("tryagain",
                                                 self.on_server_message)
                    self.conn.add_global_handler("n_local",
                                                 self.on_server_message)
                    self.conn.add_global_handler("n_global",
                                                 self.on_server_message)

                    # 300-399
                    self.conn.add_global_handler("none",
                                                 self.on_server_message)
                    self.conn.add_global_handler("away",
                                                 self.on_server_message)
                    self.conn.add_global_handler("userhost",
                                                 self.on_server_message)
                    self.conn.add_global_handler("ison",
                                                 self.on_server_message)
                    self.conn.add_global_handler("unaway",
                                                 self.on_server_message)
                    self.conn.add_global_handler("nowaway",
                                                 self.on_server_message)
                    self.conn.add_global_handler("whoisuser",
                                                 self.on_server_message)
                    self.conn.add_global_handler("whoisserver",
                                                 self.on_server_message)
                    self.conn.add_global_handler("whoisoperator",
                                                 self.on_server_message)
                    self.conn.add_global_handler("whowasuser",
                                                 self.on_server_message)
                    self.conn.add_global_handler("endofwho",
                                                 self.on_server_message)
                    self.conn.add_global_handler("whoischanop",
                                                 self.on_server_message)
                    self.conn.add_global_handler("whoisidle",
                                                 self.on_server_message)
                    self.conn.add_global_handler("endofwhois",
                                                 self.on_server_message)
                    self.conn.add_global_handler("whoischannels",
                                                 self.on_server_message)
                    self.conn.add_global_handler("liststart",
                                                 self.on_server_message)
                    self.conn.add_global_handler("list",
                                                 self.on_server_message)
                    self.conn.add_global_handler("listend",
                                                 self.on_server_message)
                    self.conn.add_global_handler("channelmodeis",
                                                 self.on_pass0)
                    self.conn.add_global_handler("channelcreate",
                                                 self.on_pass0)
                    self.conn.add_global_handler("whoisaccount",
                                                 self.on_server_message)
                    self.conn.add_global_handler("notopic", self.on_pass)
                    self.conn.add_global_handler("currenttopic", self.on_pass0)
                    # self.conn.add_global_handler("topicinfo", self.on_server_message) # not needed right now
                    self.conn.add_global_handler("inviting",
                                                 self.on_server_message)
                    self.conn.add_global_handler("summoning",
                                                 self.on_server_message)
                    self.conn.add_global_handler("invitelist",
                                                 self.on_server_message)
                    self.conn.add_global_handler("endofinvitelist",
                                                 self.on_server_message)
                    self.conn.add_global_handler("exceptlist",
                                                 self.on_server_message)
                    self.conn.add_global_handler("endofexceptlist",
                                                 self.on_server_message)
                    self.conn.add_global_handler("version",
                                                 self.on_server_message)
                    self.conn.add_global_handler("whoreply",
                                                 self.on_server_message)
                    self.conn.add_global_handler("namreply", self.on_pass1)
                    self.conn.add_global_handler("whospcrpl",
                                                 self.on_server_message)
                    self.conn.add_global_handler("killdone",
                                                 self.on_server_message)
                    self.conn.add_global_handler("closing",
                                                 self.on_server_message)
                    self.conn.add_global_handler("closeend",
                                                 self.on_server_message)
                    self.conn.add_global_handler("links",
                                                 self.on_server_message)
                    self.conn.add_global_handler("endoflinks",
                                                 self.on_server_message)
                    self.conn.add_global_handler("endofnames", self.on_pass0)
                    self.conn.add_global_handler("banlist", self.on_pass0)
                    self.conn.add_global_handler("endofbanlist", self.on_pass0)
                    self.conn.add_global_handler("endofwhowas",
                                                 self.on_server_message)
                    self.conn.add_global_handler("info",
                                                 self.on_server_message)
                    self.conn.add_global_handler("motd",
                                                 self.on_server_message)
                    self.conn.add_global_handler("infostart",
                                                 self.on_server_message)
                    self.conn.add_global_handler("endofinfo",
                                                 self.on_server_message)
                    self.conn.add_global_handler("motdstart",
                                                 self.on_server_message)
                    self.conn.add_global_handler("endofmotd",
                                                 self.on_server_message)
                    self.conn.add_global_handler("youreoper",
                                                 self.on_server_message)
                    self.conn.add_global_handler(
                        "396", self.on_server_message)  # new host

                    # 400-599
                    self.conn.add_global_handler("nosuchnick", self.on_pass_if)
                    self.conn.add_global_handler("nosuchserver",
                                                 self.on_server_message)
                    self.conn.add_global_handler("nosuchchannel",
                                                 self.on_pass_if)
                    self.conn.add_global_handler("cannotsendtochan",
                                                 self.on_pass_if)
                    self.conn.add_global_handler("toomanychannels",
                                                 self.on_server_message)
                    self.conn.add_global_handler("wasnosuchnick",
                                                 self.on_server_message)
                    self.conn.add_global_handler("toomanytargets",
                                                 self.on_server_message)
                    self.conn.add_global_handler("noorigin",
                                                 self.on_server_message)
                    self.conn.add_global_handler("invalidcapcmd",
                                                 self.on_server_message)
                    self.conn.add_global_handler("norecipient",
                                                 self.on_server_message)
                    self.conn.add_global_handler("notexttosend",
                                                 self.on_server_message)
                    self.conn.add_global_handler("notoplevel",
                                                 self.on_server_message)
                    self.conn.add_global_handler("wildtoplevel",
                                                 self.on_server_message)
                    self.conn.add_global_handler("unknowncommand",
                                                 self.on_server_message)
                    self.conn.add_global_handler("nomotd",
                                                 self.on_server_message)
                    self.conn.add_global_handler("noadmininfo",
                                                 self.on_server_message)
                    self.conn.add_global_handler("fileerror",
                                                 self.on_server_message)
                    self.conn.add_global_handler("nonicknamegiven",
                                                 self.on_server_message)
                    self.conn.add_global_handler("erroneusnickname",
                                                 self.on_server_message)
                    self.conn.add_global_handler("nicknameinuse",
                                                 self.on_nicknameinuse)
                    self.conn.add_global_handler("nickcollision",
                                                 self.on_server_message)
                    self.conn.add_global_handler("unavailresource",
                                                 self.on_server_message)
                    self.conn.add_global_handler("unavailresource",
                                                 self.on_server_message)
                    self.conn.add_global_handler("usernotinchannel",
                                                 self.on_pass1)
                    self.conn.add_global_handler("notonchannel", self.on_pass0)
                    self.conn.add_global_handler("useronchannel",
                                                 self.on_pass1)
                    self.conn.add_global_handler("nologin", self.on_pass1)
                    self.conn.add_global_handler("summondisabled",
                                                 self.on_server_message)
                    self.conn.add_global_handler("usersdisabled",
                                                 self.on_server_message)
                    self.conn.add_global_handler("notregistered",
                                                 self.on_server_message)
                    self.conn.add_global_handler("needmoreparams",
                                                 self.on_server_message)
                    self.conn.add_global_handler("alreadyregistered",
                                                 self.on_server_message)
                    self.conn.add_global_handler("nopermforhost",
                                                 self.on_server_message)
                    self.conn.add_global_handler("passwdmismatch",
                                                 self.on_server_message)
                    self.conn.add_global_handler("yourebannedcreep",
                                                 self.on_server_message)
                    self.conn.add_global_handler("youwillbebanned",
                                                 self.on_server_message)
                    self.conn.add_global_handler("keyset", self.on_pass)
                    self.conn.add_global_handler("channelisfull", self.on_pass)
                    self.conn.add_global_handler("unknownmode",
                                                 self.on_server_message)
                    self.conn.add_global_handler("inviteonlychan",
                                                 self.on_pass)
                    self.conn.add_global_handler("bannedfromchan",
                                                 self.on_pass)
                    self.conn.add_global_handler("badchannelkey",
                                                 self.on_pass0)
                    self.conn.add_global_handler("badchanmask", self.on_pass)
                    self.conn.add_global_handler("nochanmodes", self.on_pass)
                    self.conn.add_global_handler("banlistfull", self.on_pass)
                    self.conn.add_global_handler("cannotknock", self.on_pass)
                    self.conn.add_global_handler("noprivileges",
                                                 self.on_server_message)
                    self.conn.add_global_handler("chanoprivsneeded",
                                                 self.on_pass)
                    self.conn.add_global_handler("cantkillserver",
                                                 self.on_server_message)
                    self.conn.add_global_handler("restricted",
                                                 self.on_server_message)
                    self.conn.add_global_handler("uniqopprivsneeded",
                                                 self.on_server_message)
                    self.conn.add_global_handler("nooperhost",
                                                 self.on_server_message)
                    self.conn.add_global_handler("noservicehost",
                                                 self.on_server_message)
                    self.conn.add_global_handler("umodeunknownflag",
                                                 self.on_server_message)
                    self.conn.add_global_handler("usersdontmatch",
                                                 self.on_server_message)

                    # protocol
                    # FIXME: error
                    self.conn.add_global_handler("join", self.on_join)
                    self.conn.add_global_handler("join",
                                                 self.on_join_update_host)
                    self.conn.add_global_handler("kick", self.on_pass)
                    self.conn.add_global_handler("mode", self.on_pass)
                    self.conn.add_global_handler("part", self.on_pass)
                    self.conn.add_global_handler("privmsg", self.on_privmsg)
                    self.conn.add_global_handler("privnotice",
                                                 self.on_privnotice)
                    self.conn.add_global_handler("pubmsg", self.on_pass)
                    self.conn.add_global_handler("pubnotice", self.on_pass)
                    self.conn.add_global_handler("quit", self.on_quit)
                    self.conn.add_global_handler("invite", self.on_invite)
                    # FIXME: action
                    self.conn.add_global_handler("topic", self.on_pass)
                    self.conn.add_global_handler("nick", self.on_nick)
                    self.conn.add_global_handler("umode", self.on_umode)

                    self.conn.add_global_handler("kill", self.on_kill)
                    self.conn.add_global_handler("error", self.on_error)

                    # generated
                    self.conn.add_global_handler("ctcp", self.on_ctcp)

                    if not self.connected:
                        self.connected = True
                        await self.save()

                    self.disconnect = False

                    return
                except TimeoutError:
                    self.send_notice("Connection timed out.")
                except irc.client.ServerConnectionError:
                    self.send_notice(
                        "Unexpected connection error, issue was logged.")
                    logging.exception("Failed to connect")
                except Exception as e:
                    self.send_notice(f"Failed to connect: {str(e)}")
                    logging.exception("Failed to connect")

            if not self.disconnect:
                self.send_notice(
                    f"Tried all servers, waiting {backoff} seconds before trying again."
                )
                await asyncio.sleep(backoff)

                if backoff < 60:
                    backoff += 5

        self.send_notice("Connection aborted.")

    def on_disconnect(self, conn, event) -> None:
        self.conn.disconnect()
        self.conn = None

        if self.connected and not self.disconnect:
            self.send_notice("Disconnected, reconnecting...")

            async def later():
                await asyncio.sleep(10)
                await self.connect()

            asyncio.ensure_future(later())
        else:
            self.send_notice("Disconnected.")

    @ircroom_event()
    def on_pass(self, conn, event) -> None:
        logging.warning(
            f"IRC room event '{event.type}' fell through, target was from command."
        )
        source = self.source_text(conn, event)
        args = " ".join(event.arguments)
        source = self.source_text(conn, event)
        target = str(event.target)
        self.send_notice_html(f"<b>{source} {event.type} {target}</b> {args}")

    @ircroom_event()
    def on_pass_if(self, conn, event) -> None:
        self.send_notice(" ".join(event.arguments))

    @ircroom_event()
    def on_pass_or_ignore(self, conn, event) -> None:
        pass

    @ircroom_event(target_arg=0)
    def on_pass0(self, conn, event) -> None:
        logging.warning(
            f"IRC room event '{event.type}' fell through, target was '{event.arguments[0]}'."
        )
        self.send_notice(" ".join(event.arguments))

    @ircroom_event(target_arg=1)
    def on_pass1(self, conn, event) -> None:
        logging.warning(
            f"IRC room event '{event.type}' fell through, target was '{event.arguments[1]}'."
        )
        self.send_notice(" ".join(event.arguments))

    def on_server_message(self, conn, event) -> None:
        self.send_notice(" ".join(event.arguments))

    def on_umodeis(self, conn, event) -> None:
        self.send_notice(f"Your user mode is: {event.arguments[0]}")

    def on_umode(self, conn, event) -> None:
        self.send_notice(
            f"User mode changed for {event.target}: {event.arguments[0]}")

    def source_text(self, conn, event) -> str:
        source = None

        if event.source is not None:
            source = str(event.source.nick)

            if event.source.user is not None and event.source.host is not None:
                source += f" ({event.source.user}@{event.source.host})"
        else:
            source = conn.server

        return source

    @ircroom_event()
    def on_privnotice(self, conn, event) -> None:
        # show unhandled notices in server room
        source = self.source_text(conn, event)
        self.send_notice_html(
            f"Notice from <b>{source}:</b> {event.arguments[0]}")

    @ircroom_event()
    def on_ctcp(self, conn, event) -> None:
        # show unhandled ctcps in server room
        source = self.source_text(conn, event)
        self.send_notice_html(
            f"<b>{source}</b> requested <b>CTCP {event.arguments[0]}</b> which we ignored"
        )

    def on_welcome(self, conn, event) -> None:
        self.on_server_message(conn, event)

        async def later():
            await asyncio.sleep(2)

            if self.autocmd is not None:
                self.send_notice(
                    "Sending autocmd and waiting a bit before joining channels..."
                )
                self.conn.send_raw(self.autocmd)
                await asyncio.sleep(4)

            channels = []
            keys = []

            for room in self.rooms.values():
                if type(room) is ChannelRoom:
                    channels.append(room.name)
                    keys.append(room.key if room.key else "")

            if len(channels) > 0:
                self.send_notice(f"Joining channels {', '.join(channels)}")
                self.conn.join(",".join(channels), ",".join(keys))

        asyncio.ensure_future(later())

    @ircroom_event()
    def on_privmsg(self, conn, event) -> bool:
        # slightly backwards
        target = event.source.nick.lower()

        if target not in self.rooms:

            async def later():
                # reuse query command to create a room
                await self.cmd_query(
                    Namespace(nick=event.source.nick, message=[]))

                # push the message
                room = self.rooms[target]
                room.on_privmsg(conn, event)

            asyncio.ensure_future(later())
        else:
            room = self.rooms[target]
            if not room.in_room(self.user_id):
                asyncio.ensure_future(
                    self.serv.api.post_room_invite(self.rooms[target].id,
                                                   self.user_id))

    @ircroom_event()
    def on_join(self, conn, event) -> None:
        target = event.target.lower()

        logging.debug(
            f"Handling JOIN to {target} by {event.source.nick} (we are {self.conn.real_nickname})"
        )

        # create a ChannelRoom in response to JOIN
        if event.source.nick == self.conn.real_nickname and target not in self.rooms:
            logging.debug(
                "Pre-flight check for JOIN ok, going to create it...")
            self.rooms[target] = ChannelRoom.create(self, event.target)

            # pass this event through
            self.rooms[target].on_join(conn, event)

    def on_join_update_host(self, conn, event) -> None:
        # update for split long
        if event.source.nick == self.conn.real_nickname and self.real_host != event.source.host:
            self.real_host = event.source.host
            logging.debug(f"Self host updated to '{self.real_host}'")

    def on_quit(self, conn, event) -> None:
        irc_user_id = self.serv.irc_user_id(self.name, event.source.nick)

        # leave channels
        for room in self.rooms.values():
            if type(room) is ChannelRoom:
                if room.in_room(irc_user_id):
                    asyncio.ensure_future(
                        self.serv.api.post_room_leave(room.id, irc_user_id))

    def on_nick(self, conn, event) -> None:
        async def later():
            old_irc_user_id = self.serv.irc_user_id(self.name,
                                                    event.source.nick)
            new_irc_user_id = await self.serv.ensure_irc_user_id(
                self.name, event.target)

            # special case where only cases change, ensure will update displayname
            if old_irc_user_id == new_irc_user_id:
                return

            # leave and join channels
            for room in self.rooms.values():
                if type(room) is ChannelRoom:
                    if room.in_room(old_irc_user_id):
                        # notify mx user about the change
                        room.send_notice("{} is changing nick to {}".format(
                            event.source.nick, event.target))
                        await self.serv.api.post_room_leave(
                            room.id, old_irc_user_id)
                        await self.serv.api.post_room_invite(
                            room.id, new_irc_user_id)
                        await self.serv.api.post_room_join(
                            room.id, new_irc_user_id)

        asyncio.ensure_future(later())

    def on_nicknameinuse(self, conn, event) -> None:
        newnick = event.arguments[0] + "_"
        self.conn.nick(newnick)
        self.send_notice(
            f"Nickname {event.arguments[0]} is in use, trying {newnick}")

    def on_invite(self, conn, event) -> None:
        self.send_notice_html("<b>{}</b> has invited you to <b>{}</b>".format(
            event.source.nick, event.arguments[0]))

    @ircroom_event()
    def on_kill(self, conn, event) -> None:
        if event.target == conn.real_nickname:
            source = self.source_text(conn, event)
            self.send_notice_html(
                f"Killed by <b>{source}</b>: {event.arguments[0]}")

            # do not reconnect after KILL
            self.connected = False

    def on_error(self, conn, event) -> None:
        self.send_notice_html(f"<b>ERROR</b>: {event.target}")
Beispiel #10
0
    def init(self):
        self.commands = CommandManager()

        cmd = CommandParser(prog="NETWORKS",
                            description="list available networks")
        self.commands.register(cmd, self.cmd_networks)

        cmd = CommandParser(prog="SERVERS",
                            description="list servers for a network")
        cmd.add_argument("network", help="network name (see NETWORKS)")
        self.commands.register(cmd, self.cmd_servers)

        cmd = CommandParser(prog="OPEN",
                            description="open network for connecting")
        cmd.add_argument("name", help="network name (see NETWORKS)")
        cmd.add_argument("--new",
                         action="store_true",
                         help="force open a new network connection")
        self.commands.register(cmd, self.cmd_open)

        cmd = CommandParser(
            prog="STATUS",
            description="show bridge status",
            epilog="Note: admins see all users but only their own rooms",
        )
        self.commands.register(cmd, self.cmd_status)

        cmd = CommandParser(
            prog="QUIT",
            description="disconnect from all networks",
            epilog=
            ("For quickly leaving all networks and removing configurations in a single command.\n"
             "\n"
             "Additionally this will close current DM session with the bridge.\n"
             ),
        )
        self.commands.register(cmd, self.cmd_quit)

        if self.serv.is_admin(self.user_id):
            cmd = CommandParser(prog="MASKS", description="list allow masks")
            self.commands.register(cmd, self.cmd_masks)

            cmd = CommandParser(
                prog="ADDMASK",
                description="add new allow mask",
                epilog=
                ("For anyone else than the owner to use this bridge they need to be allowed to talk with the bridge bot.\n"
                 "This is accomplished by adding an allow mask that determines their permission level when using the bridge.\n"
                 "\n"
                 "Only admins can manage networks, normal users can just connect.\n"
                 ),
            )
            cmd.add_argument(
                "mask",
                help="Matrix ID mask (eg: @friend:contoso.com or *:contoso.com)"
            )
            cmd.add_argument("--admin",
                             help="Admin level access",
                             action="store_true")
            self.commands.register(cmd, self.cmd_addmask)

            cmd = CommandParser(
                prog="DELMASK",
                description="delete allow mask",
                epilog=
                ("Note: Removing a mask only prevents starting a new DM with the bridge bot. Use FORGET for ending existing"
                 " sessions."),
            )
            cmd.add_argument(
                "mask",
                help="Matrix ID mask (eg: @friend:contoso.com or *:contoso.com)"
            )
            self.commands.register(cmd, self.cmd_delmask)

            cmd = CommandParser(prog="ADDNETWORK",
                                description="add new network")
            cmd.add_argument("name", help="network name")
            self.commands.register(cmd, self.cmd_addnetwork)

            cmd = CommandParser(prog="DELNETWORK",
                                description="delete network")
            cmd.add_argument("name", help="network name")
            self.commands.register(cmd, self.cmd_delnetwork)

            cmd = CommandParser(prog="ADDSERVER",
                                description="add server to a network")
            cmd.add_argument("network", help="network name")
            cmd.add_argument("address", help="server address")
            cmd.add_argument("port",
                             nargs="?",
                             type=int,
                             help="server port",
                             default=6667)
            cmd.add_argument("--tls",
                             action="store_true",
                             help="use TLS encryption",
                             default=False)
            cmd.add_argument(
                "--tls-insecure",
                action="store_true",
                help=
                "ignore TLS verification errors (hostname, self-signed, expired)",
                default=False,
            )
            cmd.add_argument("--proxy",
                             help="use a SOCKS proxy (socks5://...)",
                             default=None)
            self.commands.register(cmd, self.cmd_addserver)

            cmd = CommandParser(prog="DELSERVER",
                                description="delete server from a network")
            cmd.add_argument("network", help="network name")
            cmd.add_argument("address", help="server address")
            cmd.add_argument("port",
                             nargs="?",
                             type=int,
                             help="server port",
                             default=6667)
            self.commands.register(cmd, self.cmd_delserver)

            cmd = CommandParser(
                prog="FORGET",
                description=
                "remove all connections and configuration of a user",
                epilog=
                ("Kills all connections of this user, removes all user set configuration and makes the bridge leave all rooms"
                 " where this user is in.\n"
                 "If the user still has an allow mask they can DM the bridge again to reconfigure and reconnect.\n"
                 "\n"
                 "This is meant as a way to kick users after removing an allow mask or resetting a user after losing access to"
                 " existing account/rooms for any reason.\n"),
            )
            cmd.add_argument("user",
                             help="Matrix ID (eg: @ex-friend:contoso.com)")
            self.commands.register(cmd, self.cmd_forget)

            cmd = CommandParser(prog="DISPLAYNAME",
                                description="change bridge displayname")
            cmd.add_argument("displayname", help="new bridge displayname")
            self.commands.register(cmd, self.cmd_displayname)

            cmd = CommandParser(prog="AVATAR",
                                description="change bridge avatar")
            cmd.add_argument("url", help="new avatar URL (mxc:// format)")
            self.commands.register(cmd, self.cmd_avatar)

            cmd = CommandParser(
                prog="IDENT",
                description="configure ident replies",
                epilog=
                "Note: MXID here is case sensitive, see subcommand help with IDENTCFG SET -h",
            )
            subcmd = cmd.add_subparsers(help="commands", dest="cmd")
            subcmd.add_parser("list", help="list custom idents (default)")
            cmd_set = subcmd.add_parser("set", help="set custom ident")
            cmd_set.add_argument("mxid", help="mxid of the user")
            cmd_set.add_argument("ident", help="custom ident for the user")
            cmd_remove = subcmd.add_parser("remove",
                                           help="remove custom ident")
            cmd_remove.add_argument("mxid", help="mxid of the user")
            self.commands.register(cmd, self.cmd_ident)

            cmd = CommandParser(
                prog="SYNC",
                description="set default IRC member sync mode",
                epilog="Note: Users can override this per room.",
            )
            group = cmd.add_mutually_exclusive_group()
            group.add_argument(
                "--lazy",
                help="set lazy sync, members are added when they talk",
                action="store_true")
            group.add_argument(
                "--half",
                help=
                "set half sync, members are added when they join or talk (default)",
                action="store_true")
            group.add_argument(
                "--full",
                help="set full sync, members are fully synchronized",
                action="store_true")
            self.commands.register(cmd, self.cmd_sync)

            cmd = CommandParser(
                prog="MAXLINES",
                description=
                "set default maximum number of lines per message until truncation or pastebin",
                epilog="Note: Users can override this per room.",
            )
            cmd.add_argument("lines",
                             type=int,
                             nargs="?",
                             help="Number of lines")
            self.commands.register(cmd, self.cmd_maxlines)

            cmd = CommandParser(
                prog="PASTEBIN",
                description=
                "enable or disable automatic pastebin of long messages by default",
                epilog="Note: Users can override this per room.",
            )
            cmd.add_argument("--enable",
                             dest="enabled",
                             action="store_true",
                             help="Enable pastebin")
            cmd.add_argument(
                "--disable",
                dest="enabled",
                action="store_false",
                help="Disable pastebin (messages will be truncated)")
            cmd.set_defaults(enabled=None)
            self.commands.register(cmd, self.cmd_pastebin)

            cmd = CommandParser(prog="MEDIAURL",
                                description="configure media URL for links")
            cmd.add_argument("url", nargs="?", help="new URL override")
            cmd.add_argument(
                "--remove",
                help="remove URL override (will retry auto-detection)",
                action="store_true")
            self.commands.register(cmd, self.cmd_media_url)

            cmd = CommandParser(prog="VERSION",
                                description="show bridge version")
            self.commands.register(cmd, self.cmd_version)

        self.mx_register("m.room.message", self.on_mx_message)
Beispiel #11
0
class ControlRoom(Room):
    commands: CommandManager

    def init(self):
        self.commands = CommandManager()

        cmd = CommandParser(prog="NETWORKS",
                            description="list available networks")
        self.commands.register(cmd, self.cmd_networks)

        cmd = CommandParser(prog="SERVERS",
                            description="list servers for a network")
        cmd.add_argument("network", help="network name (see NETWORKS)")
        self.commands.register(cmd, self.cmd_servers)

        cmd = CommandParser(prog="OPEN",
                            description="open network for connecting")
        cmd.add_argument("name", help="network name (see NETWORKS)")
        cmd.add_argument("--new",
                         action="store_true",
                         help="force open a new network connection")
        self.commands.register(cmd, self.cmd_open)

        cmd = CommandParser(
            prog="STATUS",
            description="show bridge status",
            epilog="Note: admins see all users but only their own rooms",
        )
        self.commands.register(cmd, self.cmd_status)

        cmd = CommandParser(
            prog="QUIT",
            description="disconnect from all networks",
            epilog=
            ("For quickly leaving all networks and removing configurations in a single command.\n"
             "\n"
             "Additionally this will close current DM session with the bridge.\n"
             ),
        )
        self.commands.register(cmd, self.cmd_quit)

        if self.serv.is_admin(self.user_id):
            cmd = CommandParser(prog="MASKS", description="list allow masks")
            self.commands.register(cmd, self.cmd_masks)

            cmd = CommandParser(
                prog="ADDMASK",
                description="add new allow mask",
                epilog=
                ("For anyone else than the owner to use this bridge they need to be allowed to talk with the bridge bot.\n"
                 "This is accomplished by adding an allow mask that determines their permission level when using the bridge.\n"
                 "\n"
                 "Only admins can manage networks, normal users can just connect.\n"
                 ),
            )
            cmd.add_argument(
                "mask",
                help="Matrix ID mask (eg: @friend:contoso.com or *:contoso.com)"
            )
            cmd.add_argument("--admin",
                             help="Admin level access",
                             action="store_true")
            self.commands.register(cmd, self.cmd_addmask)

            cmd = CommandParser(
                prog="DELMASK",
                description="delete allow mask",
                epilog=
                ("Note: Removing a mask only prevents starting a new DM with the bridge bot. Use FORGET for ending existing"
                 " sessions."),
            )
            cmd.add_argument(
                "mask",
                help="Matrix ID mask (eg: @friend:contoso.com or *:contoso.com)"
            )
            self.commands.register(cmd, self.cmd_delmask)

            cmd = CommandParser(prog="ADDNETWORK",
                                description="add new network")
            cmd.add_argument("name", help="network name")
            self.commands.register(cmd, self.cmd_addnetwork)

            cmd = CommandParser(prog="DELNETWORK",
                                description="delete network")
            cmd.add_argument("name", help="network name")
            self.commands.register(cmd, self.cmd_delnetwork)

            cmd = CommandParser(prog="ADDSERVER",
                                description="add server to a network")
            cmd.add_argument("network", help="network name")
            cmd.add_argument("address", help="server address")
            cmd.add_argument("port",
                             nargs="?",
                             type=int,
                             help="server port",
                             default=6667)
            cmd.add_argument("--tls",
                             action="store_true",
                             help="use TLS encryption",
                             default=False)
            cmd.add_argument(
                "--tls-insecure",
                action="store_true",
                help=
                "ignore TLS verification errors (hostname, self-signed, expired)",
                default=False,
            )
            cmd.add_argument("--proxy",
                             help="use a SOCKS proxy (socks5://...)",
                             default=None)
            self.commands.register(cmd, self.cmd_addserver)

            cmd = CommandParser(prog="DELSERVER",
                                description="delete server from a network")
            cmd.add_argument("network", help="network name")
            cmd.add_argument("address", help="server address")
            cmd.add_argument("port",
                             nargs="?",
                             type=int,
                             help="server port",
                             default=6667)
            self.commands.register(cmd, self.cmd_delserver)

            cmd = CommandParser(
                prog="FORGET",
                description=
                "remove all connections and configuration of a user",
                epilog=
                ("Kills all connections of this user, removes all user set configuration and makes the bridge leave all rooms"
                 " where this user is in.\n"
                 "If the user still has an allow mask they can DM the bridge again to reconfigure and reconnect.\n"
                 "\n"
                 "This is meant as a way to kick users after removing an allow mask or resetting a user after losing access to"
                 " existing account/rooms for any reason.\n"),
            )
            cmd.add_argument("user",
                             help="Matrix ID (eg: @ex-friend:contoso.com)")
            self.commands.register(cmd, self.cmd_forget)

            cmd = CommandParser(prog="DISPLAYNAME",
                                description="change bridge displayname")
            cmd.add_argument("displayname", help="new bridge displayname")
            self.commands.register(cmd, self.cmd_displayname)

            cmd = CommandParser(prog="AVATAR",
                                description="change bridge avatar")
            cmd.add_argument("url", help="new avatar URL (mxc:// format)")
            self.commands.register(cmd, self.cmd_avatar)

            cmd = CommandParser(
                prog="IDENT",
                description="configure ident replies",
                epilog=
                "Note: MXID here is case sensitive, see subcommand help with IDENTCFG SET -h",
            )
            subcmd = cmd.add_subparsers(help="commands", dest="cmd")
            subcmd.add_parser("list", help="list custom idents (default)")
            cmd_set = subcmd.add_parser("set", help="set custom ident")
            cmd_set.add_argument("mxid", help="mxid of the user")
            cmd_set.add_argument("ident", help="custom ident for the user")
            cmd_remove = subcmd.add_parser("remove",
                                           help="remove custom ident")
            cmd_remove.add_argument("mxid", help="mxid of the user")
            self.commands.register(cmd, self.cmd_ident)

            cmd = CommandParser(
                prog="SYNC",
                description="set default IRC member sync mode",
                epilog="Note: Users can override this per room.",
            )
            group = cmd.add_mutually_exclusive_group()
            group.add_argument(
                "--lazy",
                help="set lazy sync, members are added when they talk",
                action="store_true")
            group.add_argument(
                "--half",
                help=
                "set half sync, members are added when they join or talk (default)",
                action="store_true")
            group.add_argument(
                "--full",
                help="set full sync, members are fully synchronized",
                action="store_true")
            self.commands.register(cmd, self.cmd_sync)

            cmd = CommandParser(
                prog="MAXLINES",
                description=
                "set default maximum number of lines per message until truncation or pastebin",
                epilog="Note: Users can override this per room.",
            )
            cmd.add_argument("lines",
                             type=int,
                             nargs="?",
                             help="Number of lines")
            self.commands.register(cmd, self.cmd_maxlines)

            cmd = CommandParser(
                prog="PASTEBIN",
                description=
                "enable or disable automatic pastebin of long messages by default",
                epilog="Note: Users can override this per room.",
            )
            cmd.add_argument("--enable",
                             dest="enabled",
                             action="store_true",
                             help="Enable pastebin")
            cmd.add_argument(
                "--disable",
                dest="enabled",
                action="store_false",
                help="Disable pastebin (messages will be truncated)")
            cmd.set_defaults(enabled=None)
            self.commands.register(cmd, self.cmd_pastebin)

            cmd = CommandParser(prog="MEDIAURL",
                                description="configure media URL for links")
            cmd.add_argument("url", nargs="?", help="new URL override")
            cmd.add_argument(
                "--remove",
                help="remove URL override (will retry auto-detection)",
                action="store_true")
            self.commands.register(cmd, self.cmd_media_url)

            cmd = CommandParser(prog="VERSION",
                                description="show bridge version")
            self.commands.register(cmd, self.cmd_version)

        self.mx_register("m.room.message", self.on_mx_message)

    def is_valid(self) -> bool:
        if self.user_id is None:
            return False

        if len(self.members) != 2:
            return False

        return True

    async def show_help(self):
        self.send_notice_html(
            f"<b>Howdy, stranger!</b> You have been granted access to the IRC bridge of <b>{self.serv.server_name}</b>."
        )

        try:
            return await self.commands.trigger("HELP")
        except CommandParserError as e:
            return self.send_notice(str(e))

    async def on_mx_message(self, event) -> bool:
        if str(event.content.msgtype
               ) != "m.text" or event.sender == self.serv.user_id:
            return

        # ignore edits
        if event.content.get_edit():
            return

        try:
            if event.content.formatted_body:
                lines = str(await
                            self.parser.parse(event.content.formatted_body
                                              )).split("\n")
            else:
                lines = event.content.body.split("\n")

            command = lines.pop(0)
            tail = "\n".join(lines) if len(lines) > 0 else None

            await self.commands.trigger(command, tail)
        except CommandParserError as e:
            self.send_notice(str(e))

    def networks(self):
        networks = {}

        for network, config in self.serv.config["networks"].items():
            config["name"] = network
            networks[network.lower()] = config

        return networks

    async def cmd_masks(self, args):
        msg = "Configured masks:\n"

        for mask, value in self.serv.config["allow"].items():
            msg += "\t{} -> {}\n".format(mask, value)

        self.send_notice(msg)

    async def cmd_addmask(self, args):
        masks = self.serv.config["allow"]

        if args.mask in masks:
            return self.send_notice("Mask already exists")

        masks[args.mask] = "admin" if args.admin else "user"
        await self.serv.save()

        self.send_notice("Mask added.")

    async def cmd_delmask(self, args):
        masks = self.serv.config["allow"]

        if args.mask not in masks:
            return self.send_notice("Mask does not exist")

        del masks[args.mask]
        await self.serv.save()

        self.send_notice("Mask removed.")

    async def cmd_networks(self, args):
        networks = self.serv.config["networks"]

        self.send_notice("Configured networks:")

        for network, data in networks.items():
            self.send_notice(f"\t{network} ({len(data['servers'])} servers)")

    async def cmd_addnetwork(self, args):
        networks = self.networks()

        if args.name.lower() in networks:
            return self.send_notice("Network already exists")

        self.serv.config["networks"][args.name] = {"servers": []}
        await self.serv.save()

        self.send_notice("Network added.")

    async def cmd_delnetwork(self, args):
        networks = self.networks()

        if args.name.lower() not in networks:
            return self.send_notice("Network does not exist")

        # FIXME: check if anyone is currently connected

        # FIXME: if no one is currently connected, leave from all network related rooms

        del self.serv.config["networks"][args.name]
        await self.serv.save()

        return self.send_notice("Network removed.")

    async def cmd_servers(self, args):
        networks = self.networks()

        if args.network.lower() not in networks:
            return self.send_notice("Network does not exist")

        network = networks[args.network.lower()]

        self.send_notice(f"Configured servers for {network['name']}:")

        for server in network["servers"]:
            with_tls = ""
            if server["tls"]:
                if "tls_insecure" in server and server["tls_insecure"]:
                    with_tls = "with insecure TLS"
                else:
                    with_tls = "with TLS"
            proxy = (f" through {server['proxy']}"
                     if "proxy" in server and server["proxy"] is not None
                     and len(server["proxy"]) > 0 else "")
            self.send_notice(
                f"\t{server['address']}:{server['port']} {with_tls}{proxy}")

    async def cmd_addserver(self, args):
        networks = self.networks()

        if args.network.lower() not in networks:
            return self.send_notice("Network does not exist")

        network = networks[args.network.lower()]
        address = args.address.lower()

        for server in network["servers"]:
            if server["address"] == address and server["port"] == args.port:
                return self.send_notice("This server already exists.")

        self.serv.config["networks"][network["name"]]["servers"].append({
            "address":
            address,
            "port":
            args.port,
            "tls":
            args.tls,
            "tls_insecure":
            args.tls_insecure,
            "proxy":
            args.proxy,
        })
        await self.serv.save()

        self.send_notice("Server added.")

    async def cmd_delserver(self, args):
        networks = self.networks()

        if args.network.lower() not in networks:
            return self.send_notice("Network does not exist")

        network = networks[args.network.lower()]
        address = args.address.lower()

        to_pop = -1
        for i, server in enumerate(network["servers"]):
            if server["address"] == address and server["port"] == args.port:
                to_pop = i
                break

        if to_pop == -1:
            return self.send_notice("No such server.")

        self.serv.config["networks"][network["name"]]["servers"].pop(to_pop)
        await self.serv.save()

        self.send_notice("Server deleted.")

    async def cmd_status(self, args):
        users = set()

        if self.serv.is_admin(self.user_id):
            for room in self.serv.find_rooms():
                users.add(room.user_id)

            users = list(users)
            users.sort()
        else:
            users.add(self.user_id)

        self.send_notice_html(f"I have {len(users)} known users:")
        for user_id in users:
            ncontrol = len(self.serv.find_rooms("ControlRoom", user_id))

            self.send_notice_html(
                f"{indent(1)}{user_id} ({ncontrol} open control rooms):")

            for network in self.serv.find_rooms("NetworkRoom", user_id):
                connected = "not connected"
                channels = "not in channels"
                privates = "not in PMs"
                plumbs = "not in plumbs"

                if network.conn and network.conn.connected:
                    user = network.real_user if network.real_user[
                        0] != "?" else "?"
                    host = network.real_host if network.real_host[
                        0] != "?" else "?"
                    connected = f"connected as {network.conn.real_nickname}!{user}@{host}"

                nchannels = 0
                nprivates = 0
                nplumbs = 0

                for room in network.rooms.values():
                    if type(room).__name__ == "PrivateRoom":
                        nprivates += 1
                    if type(room).__name__ == "ChannelRoom":
                        nchannels += 1
                    if type(room).__name__ == "PlumbedRoom":
                        nplumbs += 1

                if nprivates > 0:
                    privates = f"in {nprivates} PMs"

                if nchannels > 0:
                    channels = f"in {nchannels} channels"

                if nplumbs > 0:
                    plumbs = f"in {nplumbs} plumbs"

                self.send_notice_html(
                    f"{indent(2)}{network.name}, {connected}, {channels}, {privates}, {plumbs}"
                )

                if self.user_id == user_id:
                    for room in network.rooms.values():
                        join = ""
                        if not room.in_room(user_id):
                            join = " (you have not joined this room)"
                            # ensure the user invite is valid
                            await self.az.intent.invite_user(
                                room.id, self.user_id)
                        self.send_notice_html(
                            f'{indent(3)}<a href="https://matrix.to/#/{escape(room.id)}">{escape(room.name)}</a>{join}'
                        )

    async def cmd_forget(self, args):
        if args.user == self.user_id:
            return self.send_notice("I can't forget you, silly!")

        rooms = self.serv.find_rooms(None, args.user)

        if len(rooms) == 0:
            return self.send_notice(
                "No such user. See STATUS for list of users.")

        # disconnect each network room in first pass
        for room in rooms:
            if type(room) == NetworkRoom and room.conn and room.conn.connected:
                self.send_notice(
                    f"Disconnecting {args.user} from {room.name}...")
                await room.cmd_disconnect(Namespace())

        self.send_notice(
            f"Leaving all {len(rooms)} rooms {args.user} was in...")

        # then just forget everything
        for room in rooms:
            self.serv.unregister_room(room.id)

            try:
                await self.az.intent.leave_room(room.id)
            except MatrixRequestError:
                pass
            try:
                await self.az.intent.forget_room(room.id)
            except MatrixRequestError:
                pass

        self.send_notice(f"Done, I have forgotten about {args.user}")

    async def cmd_displayname(self, args):
        try:
            await self.az.intent.set_displayname(args.displayname)
        except MatrixRequestError as e:
            self.send_notice(f"Failed to set displayname: {str(e)}")

    async def cmd_avatar(self, args):
        try:
            await self.az.intent.set_avatar_url(args.url)
        except MatrixRequestError as e:
            self.send_notice(f"Failed to set avatar: {str(e)}")

    async def cmd_ident(self, args):
        idents = self.serv.config["idents"]

        if args.cmd == "list" or args.cmd is None:
            self.send_notice("Configured custom idents:")
            for mxid, ident in idents.items():
                self.send_notice(f"\t{mxid} -> {ident}")
        elif args.cmd == "set":
            if not re.match(r"^[a-z][-a-z0-9]*$", args.ident):
                self.send_notice(f"Invalid ident string: {args.ident}")
                self.send_notice(
                    "Must be lowercase, start with a letter, can contain dashes, letters and numbers."
                )
            else:
                idents[args.mxid] = args.ident
                self.send_notice(
                    f"Set custom ident for {args.mxid} to {args.ident}")
                await self.serv.save()
        elif args.cmd == "remove":
            if args.mxid in idents:
                del idents[args.mxid]
                self.send_notice(f"Removed custom ident for {args.mxid}")
                await self.serv.save()
            else:
                self.send_notice(f"No custom ident for {args.mxid}")

    async def cmd_sync(self, args):
        if args.lazy:
            self.serv.config["member_sync"] = "lazy"
            await self.serv.save()
        elif args.half:
            self.serv.config["member_sync"] = "half"
            await self.serv.save()
        elif args.full:
            self.serv.config["member_sync"] = "full"
            await self.serv.save()

        self.send_notice(
            f"Member sync is set to {self.serv.config['member_sync']}")

    async def cmd_media_url(self, args):
        if args.remove:
            self.serv.config["media_url"] = None
            await self.serv.save()
            self.serv.endpoint = await self.serv.detect_public_endpoint()
        elif args.url:
            parsed = urlparse(args.url)
            if parsed.scheme in [
                    "http", "https"
            ] and not parsed.params and not parsed.query and not parsed.fragment:
                self.serv.config["media_url"] = args.url
                await self.serv.save()
                self.serv.endpoint = args.url
            else:
                self.send_notice(f"Invalid media URL format: {args.url}")
                return

        self.send_notice(
            f"Media URL override is set to {self.serv.config['media_url']}")
        self.send_notice(f"Current active media URL: {self.serv.endpoint}")

    async def cmd_maxlines(self, args):
        if args.lines is not None:
            self.serv.config["max_lines"] = args.lines
            await self.serv.save()

        self.send_notice(
            f"Max lines default is {self.serv.config['max_lines']}")

    async def cmd_pastebin(self, args):
        if args.enabled is not None:
            self.serv.config["use_pastebin"] = args.enabled
            await self.serv.save()

        self.send_notice(
            f"Pastebin is {'enabled' if self.serv.config['use_pastebin'] else 'disabled'} by default"
        )

    async def cmd_open(self, args):
        networks = self.networks()
        name = args.name.lower()

        if name not in networks:
            return self.send_notice("Network does not exist")

        network = networks[name]

        found = 0
        for room in self.serv.find_rooms(NetworkRoom, self.user_id):
            if room.name == network["name"]:
                found += 1

                if not args.new:
                    if self.user_id not in room.members:
                        self.send_notice(
                            f"Inviting back to {room.name} ({room.id})")
                        await self.az.intent.invite_user(room.id, self.user_id)
                    else:
                        self.send_notice(
                            f"You are already in {room.name} ({room.id})")

        # if we found at least one network room, no need to create unless forced
        if found > 0 and not args.new:
            return

        name = network[
            "name"] if found == 0 else f"{network['name']} {found + 1}"

        self.send_notice(f"You have been invited to {name}")
        await NetworkRoom.create(self.serv, network["name"], self.user_id,
                                 name)

    async def cmd_quit(self, args):
        rooms = self.serv.find_rooms(None, self.user_id)

        # disconnect each network room in first pass
        for room in rooms:
            if type(room) == NetworkRoom and room.conn and room.conn.connected:
                self.send_notice(f"Disconnecting from {room.name}...")
                await room.cmd_disconnect(Namespace())

        self.send_notice("Closing all channels and private messages...")

        # then just forget everything
        for room in rooms:
            if room.id == self.id:
                continue

            self.serv.unregister_room(room.id)

            try:
                await self.az.intent.leave_room(room.id)
            except MatrixRequestError:
                pass
            try:
                await self.az.intent.forget_room(room.id)
            except MatrixRequestError:
                pass

        self.send_notice("Goodbye!")
        await asyncio.sleep(1)
        raise RoomInvalidError("Leaving")

    async def cmd_version(self, args):
        self.send_notice(f"heisenbridge v{__version__}")
Beispiel #12
0
class ControlRoom(Room):
    commands: CommandManager

    def init(self):
        self.commands = CommandManager()

        cmd = CommandParser(prog="NETWORKS", description="List networks")
        self.commands.register(cmd, self.cmd_networks)

        cmd = CommandParser(prog="SERVERS", description="List servers")
        cmd.add_argument("network", help="network name")
        self.commands.register(cmd, self.cmd_servers)

        cmd = CommandParser(prog="OPEN",
                            description="Open network room to connect")
        cmd.add_argument("name", help="network name")
        self.commands.register(cmd, self.cmd_open)

        if self.serv.is_admin(self.user_id):
            cmd = CommandParser(prog="MASKS", description="List allow masks")
            self.commands.register(cmd, self.cmd_masks)

            cmd = CommandParser(prog="ADDMASK", description="Add allow mask")
            cmd.add_argument(
                "mask",
                help="Matrix ID mask (eg: @friend:contoso.com or *:contoso.com)"
            )
            cmd.add_argument("--admin",
                             help="Admin level access",
                             action="store_true")
            self.commands.register(cmd, self.cmd_addmask)

            cmd = CommandParser(prog="DELMASK",
                                description="Remove allow mask")
            cmd.add_argument(
                "mask",
                help="Matrix ID mask (eg: @friend:contoso.com or *:contoso.com)"
            )
            self.commands.register(cmd, self.cmd_delmask)

            cmd = CommandParser(prog="ADDNETWORK", description="Add network")
            cmd.add_argument("name", help="network name")
            self.commands.register(cmd, self.cmd_addnetwork)

            cmd = CommandParser(prog="DELNETWORK",
                                description="Delete network")
            cmd.add_argument("name", help="network name")
            self.commands.register(cmd, self.cmd_delnetwork)

            cmd = CommandParser(prog="ADDSERVER",
                                description="Add server to network")
            cmd.add_argument("network", help="network name")
            cmd.add_argument("address", help="server address")
            cmd.add_argument("port",
                             nargs="?",
                             type=int,
                             help="server port",
                             default=6667)
            cmd.add_argument("--tls",
                             action="store_true",
                             help="use TLS encryption",
                             default=False)
            self.commands.register(cmd, self.cmd_addserver)

            cmd = CommandParser(prog="DELSERVER",
                                description="Delete server from network")
            cmd.add_argument("network", help="network name")
            cmd.add_argument("address", help="server address")
            cmd.add_argument("port",
                             nargs="?",
                             type=int,
                             help="server port",
                             default=6667)
            self.commands.register(cmd, self.cmd_delserver)

        self.mx_register("m.room.message", self.on_mx_message)

    def is_valid(self) -> bool:
        if self.user_id is None:
            return False

        if len(self.members) != 2:
            return False

        return True

    async def show_help(self):
        self.send_notice_html(
            f"<b>Howdy, stranger!</b> You have been granted access to the IRC bridge of <b>{self.serv.server_name}</b>."
        )

        try:
            return await self.commands.trigger("HELP")
        except CommandParserError as e:
            return self.send_notice(str(e))

    async def on_mx_message(self, event) -> None:
        if event["content"]["msgtype"] != "m.text" or event[
                "user_id"] == self.serv.user_id:
            return True

        try:
            return await self.commands.trigger(event["content"]["body"])
        except CommandParserError as e:
            return self.send_notice(str(e))

    def networks(self):
        networks = {}

        for network, config in self.serv.config["networks"].items():
            config["name"] = network
            networks[network.lower()] = config

        return networks

    async def cmd_masks(self, args):
        msg = "Configured masks:\n"

        for mask, value in self.serv.config["allow"].items():
            msg += "\t{} -> {}\n".format(mask, value)

        self.send_notice(msg)

    async def cmd_addmask(self, args):
        masks = self.serv.config["allow"]

        if args.mask in masks:
            return self.send_notice("Mask already exists")

        masks[args.mask] = "admin" if args.admin else "user"
        await self.serv.save()

        self.send_notice("Mask added.")

    async def cmd_delmask(self, args):
        masks = self.serv.config["allow"]

        if args.mask not in masks:
            return self.send_notice("Mask does not exist")

        del masks[args.mask]
        await self.serv.save()

        self.send_notice("Mask removed.")

    async def cmd_networks(self, args):
        networks = self.serv.config["networks"]

        self.send_notice("Configured networks:")

        for network, data in networks.items():
            self.send_notice(f"\t{network} ({len(data['servers'])} servers)")

    async def cmd_addnetwork(self, args):
        networks = self.networks()

        if args.name.lower() in networks:
            return self.send_notice("Network already exists")

        self.serv.config["networks"][args.name] = {"servers": []}
        await self.serv.save()

        self.send_notice("Network added.")

    async def cmd_delnetwork(self, args):
        networks = self.networks()

        if args.name.lower() not in networks:
            return self.send_notice("Network does not exist")

        # FIXME: check if anyone is currently connected

        # FIXME: if no one is currently connected, leave from all network related rooms

        del self.serv.config["networks"][args.name]
        await self.serv.save()

        return self.send_notice("Network removed.")

    async def cmd_servers(self, args):
        networks = self.networks()

        if args.network.lower() not in networks:
            return self.send_notice("Network does not exist")

        network = networks[args.network.lower()]

        self.send_notice(f"Configured servers for {network['name']}:")

        for server in network["servers"]:
            self.send_notice(
                f"\t{server['address']}:{server['port']} {'with TLS' if server['tls'] else ''}"
            )

    async def cmd_addserver(self, args):
        networks = self.networks()

        if args.network.lower() not in networks:
            return self.send_notice("Network does not exist")

        network = networks[args.network.lower()]
        address = args.address.lower()

        for server in network["servers"]:
            if server["address"] == address and server["port"] == args.port:
                return self.send_notice("This server already exists.")

        self.serv.config["networks"][network["name"]]["servers"].append({
            "address":
            address,
            "port":
            args.port,
            "tls":
            args.tls
        })
        await self.serv.save()

        self.send_notice("Server added.")

    async def cmd_delserver(self, args):
        networks = self.networks()

        if args.network.lower() not in networks:
            return self.send_notice("Network does not exist")

        network = networks[args.network.lower()]
        address = args.address.lower()

        to_pop = -1
        for i, server in enumerate(network["servers"]):
            if server["address"] == address and server["port"] == args.port:
                to_pop = i
                break

        if to_pop == -1:
            return self.send_notice("No such server.")

        self.serv.config["networks"][network["name"]]["servers"].pop(to_pop)
        await self.serv.save()

        self.send_notice("Server deleted.")

    async def cmd_open(self, args):
        networks = self.networks()
        name = args.name.lower()

        if name not in networks:
            return self.send_notice("Network does not exist")

        network = networks[name]

        for room in self.serv.find_rooms(NetworkRoom, self.user_id):
            if room.name == network["name"]:
                if self.user_id not in room.members:
                    self.send_notice(f"Inviting back to {room.name}")
                    await self.serv.api.post_room_invite(room.id, self.user_id)
                else:
                    self.send_notice(f"You are already in {room.name}")
                return

        self.send_notice(f"You have been invited to {network['name']}")
        await NetworkRoom.create(self.serv, network["name"], self.user_id)
Beispiel #13
0
    def init(self):
        self.commands = CommandManager()

        cmd = CommandParser(prog="NETWORKS", description="List networks")
        self.commands.register(cmd, self.cmd_networks)

        cmd = CommandParser(prog="SERVERS", description="List servers")
        cmd.add_argument("network", help="network name")
        self.commands.register(cmd, self.cmd_servers)

        cmd = CommandParser(prog="OPEN",
                            description="Open network room to connect")
        cmd.add_argument("name", help="network name")
        self.commands.register(cmd, self.cmd_open)

        if self.serv.is_admin(self.user_id):
            cmd = CommandParser(prog="MASKS", description="List allow masks")
            self.commands.register(cmd, self.cmd_masks)

            cmd = CommandParser(prog="ADDMASK", description="Add allow mask")
            cmd.add_argument(
                "mask",
                help="Matrix ID mask (eg: @friend:contoso.com or *:contoso.com)"
            )
            cmd.add_argument("--admin",
                             help="Admin level access",
                             action="store_true")
            self.commands.register(cmd, self.cmd_addmask)

            cmd = CommandParser(prog="DELMASK",
                                description="Remove allow mask")
            cmd.add_argument(
                "mask",
                help="Matrix ID mask (eg: @friend:contoso.com or *:contoso.com)"
            )
            self.commands.register(cmd, self.cmd_delmask)

            cmd = CommandParser(prog="ADDNETWORK", description="Add network")
            cmd.add_argument("name", help="network name")
            self.commands.register(cmd, self.cmd_addnetwork)

            cmd = CommandParser(prog="DELNETWORK",
                                description="Delete network")
            cmd.add_argument("name", help="network name")
            self.commands.register(cmd, self.cmd_delnetwork)

            cmd = CommandParser(prog="ADDSERVER",
                                description="Add server to network")
            cmd.add_argument("network", help="network name")
            cmd.add_argument("address", help="server address")
            cmd.add_argument("port",
                             nargs="?",
                             type=int,
                             help="server port",
                             default=6667)
            cmd.add_argument("--tls",
                             action="store_true",
                             help="use TLS encryption",
                             default=False)
            self.commands.register(cmd, self.cmd_addserver)

            cmd = CommandParser(prog="DELSERVER",
                                description="Delete server from network")
            cmd.add_argument("network", help="network name")
            cmd.add_argument("address", help="server address")
            cmd.add_argument("port",
                             nargs="?",
                             type=int,
                             help="server port",
                             default=6667)
            self.commands.register(cmd, self.cmd_delserver)

        self.mx_register("m.room.message", self.on_mx_message)