func TestRoomQuietToggleBroadcasts(t *testing.T) { u := message.NewUser(message.SimpleId("foo")) u.Config = message.UserConfig{ Quiet: true, } ch := NewRoom() defer ch.Close() _, err := ch.Join(u) if err != nil { t.Fatal(err) } // Drain the initial Join message <-ch.broadcast u.ToggleQuietMode() expectedMsg := message.NewAnnounceMsg("Ignored") ch.HandleMsg(expectedMsg) msg := <-u.ConsumeChan() if _, ok := msg.(*message.AnnounceMsg); !ok { t.Errorf("Got: `%T`; Expected: `%T`", msg, expectedMsg) } u.ToggleQuietMode() ch.HandleMsg(message.NewAnnounceMsg("Ignored")) ch.HandleMsg(message.NewSystemMsg("hello", u)) msg = <-u.ConsumeChan() if _, ok := msg.(*message.AnnounceMsg); ok { t.Errorf("Got unexpected `%T`", msg) } }
func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) { u := message.NewUser(message.SimpleId("foo")) u.Config = message.UserConfig{ Quiet: true, } ch := NewRoom() defer ch.Close() _, err := ch.Join(u) if err != nil { t.Fatal(err) } // Drain the initial Join message <-ch.broadcast go func() { for msg := range u.ConsumeChan() { if _, ok := msg.(*message.AnnounceMsg); ok { t.Errorf("Got unexpected `%T`", msg) } } }() // Call with an AnnounceMsg and all the other types // and assert we received only non-announce messages ch.HandleMsg(message.NewAnnounceMsg("Ignored")) // Assert we still get all other types of messages ch.HandleMsg(message.NewEmoteMsg("hello", u)) ch.HandleMsg(message.NewSystemMsg("hello", u)) ch.HandleMsg(message.NewPrivateMsg("hello", u, u)) ch.HandleMsg(message.NewPublicMsg("hello", u)) }
func TestRoomJoin(t *testing.T) { var expected, actual []byte s := &MockScreen{} u := message.NewUser(message.SimpleId("foo")) ch := NewRoom() go ch.Serve() defer ch.Close() _, err := ch.Join(u) if err != nil { t.Fatal(err) } u.HandleMsg(<-u.ConsumeChan(), s) expected = []byte(" * foo joined. (Connected: 1)" + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) } ch.Send(message.NewSystemMsg("hello", u)) u.HandleMsg(<-u.ConsumeChan(), s) expected = []byte("-> hello" + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) } ch.Send(message.ParseInput("/me says hello.", u)) u.HandleMsg(<-u.ConsumeChan(), s) expected = []byte("** foo says hello." + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) } }
// HandleMsg reacts to a message, will block until done. func (r *Room) HandleMsg(m message.Message) { switch m := m.(type) { case *message.CommandMsg: cmd := *m err := r.commands.Run(r, cmd) if err != nil { m := message.NewSystemMsg(fmt.Sprintf("Err: %s", err), cmd.From()) go r.HandleMsg(m) } case message.MessageTo: user := m.To() user.Send(m) default: fromMsg, skip := m.(message.MessageFrom) var skipUser *message.User if skip { skipUser = fromMsg.From() } r.history.Add(m) r.members.Each(func(u identified) { user := u.(*Member).User if skip && skipUser == user { // Skip return } if _, ok := m.(*message.AnnounceMsg); ok { if user.Config.Quiet { // Skip return } } user.Send(m) }) } }
// InitCommands injects default commands into a Commands registry. func InitCommands(c *Commands) { c.Add(Command{ Prefix: "/help", Handler: func(room *Room, msg message.CommandMsg) error { op := room.IsOp(msg.From()) room.Send(message.NewSystemMsg(room.commands.Help(op), msg.From())) return nil }, }) c.Add(Command{ Prefix: "/me", Handler: func(room *Room, msg message.CommandMsg) error { me := strings.TrimLeft(msg.Body(), "/me") if me == "" { me = "is at a loss for words." } else { me = me[1:] } room.Send(message.NewEmoteMsg(me, msg.From())) return nil }, }) c.Add(Command{ Prefix: "/exit", Help: "Exit the chat.", Handler: func(room *Room, msg message.CommandMsg) error { msg.From().Close() return nil }, }) c.Alias("/exit", "/quit") c.Add(Command{ Prefix: "/nick", PrefixHelp: "NAME", Help: "Rename yourself.", Handler: func(room *Room, msg message.CommandMsg) error { args := msg.Args() if len(args) != 1 { return ErrMissingArg } u := msg.From() member, ok := room.MemberById(u.Id()) if !ok { return errors.New("failed to find member") } oldId := member.Id() member.SetId(SanitizeName(args[0])) err := room.Rename(oldId, member) if err != nil { member.SetId(oldId) return err } return nil }, }) c.Add(Command{ Prefix: "/names", Help: "List users who are connected.", Handler: func(room *Room, msg message.CommandMsg) error { // TODO: colorize names := room.NamesPrefix("") body := fmt.Sprintf("%d connected: %s", len(names), strings.Join(names, ", ")) room.Send(message.NewSystemMsg(body, msg.From())) return nil }, }) c.Alias("/names", "/list") c.Add(Command{ Prefix: "/theme", PrefixHelp: "[mono|colors]", Help: "Set your color theme.", Handler: func(room *Room, msg message.CommandMsg) error { user := msg.From() args := msg.Args() if len(args) == 0 { theme := "plain" if user.Config.Theme != nil { theme = user.Config.Theme.Id() } body := fmt.Sprintf("Current theme: %s", theme) room.Send(message.NewSystemMsg(body, user)) return nil } id := args[0] for _, t := range message.Themes { if t.Id() == id { user.Config.Theme = &t body := fmt.Sprintf("Set theme: %s", id) room.Send(message.NewSystemMsg(body, user)) return nil } } return errors.New("theme not found") }, }) c.Add(Command{ Prefix: "/quiet", Help: "Silence room announcements.", Handler: func(room *Room, msg message.CommandMsg) error { u := msg.From() u.ToggleQuietMode() var body string if u.Config.Quiet { body = "Quiet mode is toggled ON" } else { body = "Quiet mode is toggled OFF" } room.Send(message.NewSystemMsg(body, u)) return nil }, }) c.Add(Command{ Prefix: "/slap", PrefixHelp: "NAME", Handler: func(room *Room, msg message.CommandMsg) error { var me string args := msg.Args() if len(args) == 0 { me = "slaps themselves around a bit with a large trout." } else { me = fmt.Sprintf("slaps %s around a bit with a large trout.", strings.Join(args, " ")) } room.Send(message.NewEmoteMsg(me, msg.From())) return nil }, }) }
// Connect a specific Terminal to this host and its room. func (h *Host) Connect(term *sshd.Terminal) { id := NewIdentity(term.Conn) user := message.NewUserScreen(id, term) user.Config.Theme = &h.theme go func() { // Close term once user is closed. user.Wait() term.Close() }() defer user.Close() // Send MOTD if h.motd != "" { user.Send(message.NewAnnounceMsg(h.motd)) } member, err := h.Join(user) if err != nil { // Try again... id.SetName(fmt.Sprintf("Guest%d", h.count)) member, err = h.Join(user) } if err != nil { logger.Errorf("Failed to join: %s", err) return } // Successfully joined. term.SetPrompt(GetPrompt(user)) term.AutoCompleteCallback = h.AutoCompleteFunction(user) user.SetHighlight(user.Name()) h.count++ // Should the user be op'd on join? member.Op = h.isOp(term.Conn) ratelimit := rateio.NewSimpleLimiter(3, time.Second*3) for { line, err := term.ReadLine() if err == io.EOF { // Closed break } else if err != nil { logger.Errorf("Terminal reading error: %s", err) break } err = ratelimit.Count(1) if err != nil { user.Send(message.NewSystemMsg("Message rejected: Rate limiting is in effect.", user)) continue } if len(line) > maxInputLength { user.Send(message.NewSystemMsg("Message rejected: Input too long.", user)) continue } if line == "" { // Silently ignore empty lines. continue } m := message.ParseInput(line, user) // FIXME: Any reason to use h.room.Send(m) instead? h.HandleMsg(m) cmd := m.Command() if cmd == "/nick" || cmd == "/theme" { // Hijack /nick command to update terminal synchronously. Wouldn't // work if we use h.room.Send(m) above. // // FIXME: This is hacky, how do we improve the API to allow for // this? Chat module shouldn't know about terminals. term.SetPrompt(GetPrompt(user)) user.SetHighlight(user.Name()) } } err = h.Leave(user) if err != nil { logger.Errorf("Failed to leave: %s", err) return } }
// InitCommands adds host-specific commands to a Commands container. These will // override any existing commands. func (h *Host) InitCommands(c *chat.Commands) { c.Add(chat.Command{ Prefix: "/msg", PrefixHelp: "USER MESSAGE", Help: "Send MESSAGE to USER.", Handler: func(room *chat.Room, msg message.CommandMsg) error { args := msg.Args() switch len(args) { case 0: return errors.New("must specify user") case 1: return errors.New("must specify message") } target, ok := h.GetUser(args[0]) if !ok { return errors.New("user not found") } m := message.NewPrivateMsg(strings.Join(args[1:], " "), msg.From(), target) room.Send(m) return nil }, }) c.Add(chat.Command{ Prefix: "/reply", PrefixHelp: "MESSAGE", Help: "Reply with MESSAGE to the previous private message.", Handler: func(room *chat.Room, msg message.CommandMsg) error { args := msg.Args() switch len(args) { case 0: return errors.New("must specify message") } target := msg.From().ReplyTo() if target == nil { return errors.New("no message to reply to") } m := message.NewPrivateMsg(strings.Join(args, " "), msg.From(), target) room.Send(m) return nil }, }) c.Add(chat.Command{ Prefix: "/whois", PrefixHelp: "USER", Help: "Information about USER.", Handler: func(room *chat.Room, msg message.CommandMsg) error { args := msg.Args() if len(args) == 0 { return errors.New("must specify user") } target, ok := h.GetUser(args[0]) if !ok { return errors.New("user not found") } id := target.Identifier.(*Identity) room.Send(message.NewSystemMsg(id.Whois(), msg.From())) return nil }, }) // Hidden commands c.Add(chat.Command{ Prefix: "/version", Handler: func(room *chat.Room, msg message.CommandMsg) error { room.Send(message.NewSystemMsg(buildCommit, msg.From())) return nil }, }) timeStarted := time.Now() c.Add(chat.Command{ Prefix: "/uptime", Handler: func(room *chat.Room, msg message.CommandMsg) error { room.Send(message.NewSystemMsg(time.Now().Sub(timeStarted).String(), msg.From())) return nil }, }) // Op commands c.Add(chat.Command{ Op: true, Prefix: "/kick", PrefixHelp: "USER", Help: "Kick USER from the server.", Handler: func(room *chat.Room, msg message.CommandMsg) error { if !room.IsOp(msg.From()) { return errors.New("must be op") } args := msg.Args() if len(args) == 0 { return errors.New("must specify user") } target, ok := h.GetUser(args[0]) if !ok { return errors.New("user not found") } body := fmt.Sprintf("%s was kicked by %s.", target.Name(), msg.From().Name()) room.Send(message.NewAnnounceMsg(body)) target.Close() return nil }, }) c.Add(chat.Command{ Op: true, Prefix: "/ban", PrefixHelp: "USER [DURATION]", Help: "Ban USER from the server.", Handler: func(room *chat.Room, msg message.CommandMsg) error { // TODO: Would be nice to specify what to ban. Key? Ip? etc. if !room.IsOp(msg.From()) { return errors.New("must be op") } args := msg.Args() if len(args) == 0 { return errors.New("must specify user") } target, ok := h.GetUser(args[0]) if !ok { return errors.New("user not found") } var until time.Duration = 0 if len(args) > 1 { until, _ = time.ParseDuration(args[1]) } id := target.Identifier.(*Identity) h.auth.Ban(id.PublicKey(), until) h.auth.BanAddr(id.RemoteAddr(), until) body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name()) room.Send(message.NewAnnounceMsg(body)) target.Close() logger.Debugf("Banned: \n-> %s", id.Whois()) return nil }, }) c.Add(chat.Command{ Op: true, Prefix: "/motd", PrefixHelp: "MESSAGE", Help: "Set the MESSAGE of the day.", Handler: func(room *chat.Room, msg message.CommandMsg) error { if !room.IsOp(msg.From()) { return errors.New("must be op") } motd := "" args := msg.Args() if len(args) > 0 { motd = strings.Join(args, " ") } h.motd = motd body := fmt.Sprintf("New message of the day set by %s:", msg.From().Name()) room.Send(message.NewAnnounceMsg(body)) if motd != "" { room.Send(message.NewAnnounceMsg(motd)) } return nil }, }) c.Add(chat.Command{ Op: true, Prefix: "/op", PrefixHelp: "USER [DURATION]", Help: "Set USER as admin.", Handler: func(room *chat.Room, msg message.CommandMsg) error { if !room.IsOp(msg.From()) { return errors.New("must be op") } args := msg.Args() if len(args) == 0 { return errors.New("must specify user") } var until time.Duration = 0 if len(args) > 1 { until, _ = time.ParseDuration(args[1]) } member, ok := room.MemberById(args[0]) if !ok { return errors.New("user not found") } member.Op = true id := member.Identifier.(*Identity) h.auth.Op(id.PublicKey(), until) body := fmt.Sprintf("Made op by %s.", msg.From().Name()) room.Send(message.NewSystemMsg(body, member.User)) return nil }, }) }