#!/usr/bin/env python # coding: utf-8 """ Classes for Chatwisted servers """ from twisted.protocols.basic import LineReceiver from twisted.internet.protocol import Factory from twisted.python.failure import Failure from twisted.internet import reactor from chatwisted.parser import CommandParser from chatwisted.plugins import PLUGINS class GroupPlugin(object): """ Plugin base call to handle client groups and implement interactions between members in one group. Inherit from this class to write your own server plugin. :CVars: name : string name of the plugin. Only clients, who have a ClientPlugin with the same name can join groups of this class msg : dict some messages that can be modified in child classes. max_members : int or None if given, maximal number of clients that can connect to groups of this class at the same time """ name = "chat" msgs = {"welcome":"Welcome in group %s", "closed":"Sorry, but '%s' is a closed group"} max_members = None def __init__(self, factory, name, admin, *args): """ :Parameters: factory : ServerFactory name : string name of this group (should be unique) admin : ChatServerProtocol the client who owns this group (who opened it) """ self.factory = factory self.group_name = name self.cmd_prefix = name+":" if name != "main" else "" self.admin = admin self.factory.parser.get_cmds_from_obj(self, cmd_prefix=name+":") #: can every player join this group? self.opened = True #: list of all clients in this group self.members = {} def cmd_join(self, client): """ Add `client` to the group if its opened. """ err = "" if self.max_members and len(self.members) >= self.max_members: err = "Sorry, group %s is full" % (self.name) elif not self.opened: err = ("Sorry, %s is a closed group. Please contact the group" + " admin %s" % (self.name, self.admin.name)) elif client.name in self.members: err = "You are already in this group!" if err: self.send_cmd(client, "error", err) self.send_cmd(client, "join_failed", "") else: self.members[client.name] = client client.groups[self.group_name] = self self.send_cmd(client, "write", self.msgs["welcome"] % self.group_name) print "MEMBERS:", self.members, client, client.name self.list_update() def cmd_leave(self, client): """ Remove `client` from the group. """ print "SERVER: LEAVE" if not client.name in self.members: self.send_cmd(client, "error", "You are not in this group!") else: del self.members[client.name] if client == self.admin: self.factory.remove_group(self) else: self.list_update() def send_cmd(self, client, cmd, arg_string): """ Sends the command `cmd` to `client` with `arg_string` as parameters. :Parameters: client : ChatServerProtocol the client to send the command to cmd : string the command to send arg_string : string arguments as string. (The receiver has to encode them itself.) """ if client == "all": for client in self.members.itervalues(): self.send_cmd(client, cmd, arg_string) else: if arg_string: arg_string = " " + arg_string client.sendLine("/%s%s%s" % (self.cmd_prefix, cmd, arg_string)) def cmd_say(self, speaker, words): """ broadcasts a message to all clients that `speaker` says `words` """ self.send_cmd("all", "say", "%s %s" % (speaker.name, words)) def cmd_me(self, actor, words): """ broadcasts a message to all clients that `actor` does `words` """ self.send_cmd("all", "me", "%s %s" % (actor.name, words)) def cmd_write(self, dummy, words): """ writes `words` directly into the chat :param dummy: not used client object (for command parser convention) """ self.send_cmd("all", "write", words) def cmd_list(self, client): """ answers to a client's list request """ print "members>", self.members self.send_cmd(client, "list", " ".join(self.members.keys())) def list_update(self): """ call after changing the member list, ie. on join, leave and rename """ self.send_cmd("all", "list", " ".join(self.members.keys())) def cmd_info(self, client): """ tells the client type and status of this group """ can_join = (self.opened and ( not self.max_members or self.max_members > len(self.members))) client.sendLine("/info %s" % " ".join([self.group_name, self.name, str(int(can_join))])) PLUGINS["chat"]["server"] = GroupPlugin class ChatServerProtocol(LineReceiver): """ Server Protocol Represents a client on the server, holds its data (name) and gives the user interface (ie. encodes the commands) """ factory = None def connectionMade(self): """ Called when connected ask the new client for his name by sending the prompt """ self.name = None self.groups = {} def connectionLost(self, reason=Failure): """ Called when disconnected logout at factory """ self.factory.logout(self) def lineReceived(self, line): """ Called when a line was received If the client just connected, take the first `line` as name. If not, pass `line` to factory to parse it. """ line = line.rstrip() if not self.name: self.factory.register(self, line) else: self.factory.exec_cmd(self, line) class ChatServerFactory(Factory): """ Server Factory Creates a new Protocol object for every connecting client and manages all clients and their interaction """ protocol = ChatServerProtocol clients = {} client_no = 0 groups = {} def __init__(self): """ ChatServerFactory constructor Create a new CommandParser and scan this class for command methods. Then, create the "main"-Group and parse it too. """ self.parser = CommandParser() self.parser.get_cmds_from_obj(self) self.groups["main"] = GroupPlugin(self, "main", None) self.parser.default_func = self.groups["main"].cmd_say self.parser.get_cmds_from_obj(self.groups["main"]) #{ General helper methods def send_cmd(self, client, cmd, arg_string): """ Sends the command `cmd` to `client` with `arg_string` as parameters. :Parameters: client : ChatServerProtocol the client to send the command to cmd : string the command to send arg_string : string arguments as string. (The receiver has to encode them itself.) """ if client == "all": for client in self.clients.itervalues(): self.send_cmd(client, cmd, arg_string) else: client.sendLine("/%s %s" % (cmd, arg_string)) def exec_cmd(self, client, line): """ Parse and execute a received command string (\ `line`\ ) """ self.parser.parse(line, client) #{ General default commands def register(self, client, name): """ Called from a client (ChatServerProtocol) to register itself as new client searches for a valid name and add `client` to the client dict :Parameters: client : ChatServerProtocol reference to the client that wants to register name : string preferred name of the new client :return: name of the client (may be different to `name` when there is allready a client with that name :rtype: string """ if name in self.clients: i = 2 while name+str(i) in self.clients: i += 1 name += str(i) self.clients[name] = client self.client_no += 1 client.name = name self.groups["main"].cmd_join(client) self.update_lists() self.cmd_group_list(client) self.cmd_plugin_list(client) def logout(self, client): """ Called from a client (ChatServerProtocol) to log out removes `client` from the client dict :Parameters: client : ChatServerProtocol reference to the client that wants to log out """ print "LogOut" for group in self.groups.itervalues(): if client.name in group.members: print "leaving" group.cmd_leave(client) else: print client, "not in", group.members del self.clients[client.name] self.update_lists() def rename(self, client, new_name): """ Called from a client (ChatServerProtocol) to rename itself validates `new_name` and returns the new name :Parameters: client : ChatServerProtocol reference to the client that wants to get a new name new_name : string preferred new name :return: name of the client (may be different to `new_name` when there is already a client with that name :rtype: string """ if new_name in self.clients: i = 2 while new_name+str(i) in self.clients: i += 1 new_name += str(i) self.clients[new_name] = client for group in client.groups: del group.members[client.name] group.members[new_name] = client del self.clients[client.name] self.update_lists() return new_name def update_lists(self): """ update every groups memberlist """ for group in self.groups.itervalues(): group.list_update() #{ Default chat commands def cmd_create(self, client, plugin_name, group_name, *args): """ Create a new group :Parameters: client : ChatServerProtocol server protocol representing the client who wants to create a new group plugin_name : string name of the plugin(-class) to build the new group with group_name : string name of the new group (must be unique) more arguments are passed to the plugin constructor """ if plugin_name not in PLUGINS: self.send_cmd(client, "error", "Plugin not found: %s" % plugin_name) elif "server" not in PLUGINS[plugin_name]: self.send_cmd(client, "error", "Found plugin, but no server part: %s" % plugin_name) elif group_name in self.groups: self.send_cmd(client, "error", "Groupname already in use: %s" % group_name) else: self.groups[group_name] = PLUGINS[plugin_name]["server"](self, group_name, client, *args) self.cmd_group_list("all") self.send_cmd(client, "group_created", "%s %s" % (plugin_name, group_name)) #{ plugin handling def remove_group(self, group_name): """ unregister and remove a group """ if group_name in self.groups: del self.groups[group_name] self.parser.unregister(group_name + ":") def cmd_group_list(self, client): """ Send a list of all groups currently known on this server to `client` """ self.send_cmd(client, "group_list", " ".join(self.groups.keys())) def cmd_plugin_list(self, client): """ Send a list of all installed plugins to `client` """ server_plugins = filter(lambda name: "server" in PLUGINS[name], PLUGINS.keys()) self.send_cmd(client, "plugin_list", " ".join(server_plugins)) def start_standalone_server(): """ Start a standalone server without gui/client """ port = raw_input("Please enter a port: [5555]") or 5555 reactor.listenTCP(int(port), ChatServerFactory())