Index: /trunk/downstream.go
===================================================================
--- /trunk/downstream.go	(revision 664)
+++ /trunk/downstream.go	(revision 665)
@@ -519,5 +519,5 @@
 	dc.SendMessage(msg)
 
-	if id == "" || !dc.messageSupportsHistory(msg) {
+	if id == "" || !dc.messageSupportsBacklog(msg) {
 		return
 	}
@@ -530,5 +530,5 @@
 // isn't enabled.
 func (dc *downstreamConn) advanceMessageWithID(msg *irc.Message, id string) {
-	if id == "" || !dc.messageSupportsHistory(msg) {
+	if id == "" || !dc.messageSupportsBacklog(msg) {
 		return
 	}
@@ -572,6 +572,11 @@
 // marshalMessage re-formats a message coming from an upstream connection so
 // that it's suitable for being sent on this downstream connection. Only
-// messages that may appear in logs are supported, except MODE.
+// messages that may appear in logs are supported, except MODE messages which
+// may only appear in single-upstream mode.
 func (dc *downstreamConn) marshalMessage(msg *irc.Message, net *network) *irc.Message {
+	if dc.network != nil {
+		return msg
+	}
+
 	msg = msg.Copy()
 	msg.Prefix = dc.marshalUserPrefix(net, msg.Prefix)
@@ -984,4 +989,10 @@
 		}
 	}
+
+	if dc.srv.LogPath != "" && dc.network != nil {
+		dc.setSupportedCap("draft/event-playback", "")
+	} else {
+		dc.unsetSupportedCap("draft/event-playback")
+	}
 }
 
@@ -1298,11 +1309,10 @@
 }
 
-// messageSupportsHistory checks whether the provided message can be sent as
+// messageSupportsBacklog checks whether the provided message can be sent as
 // part of an history batch.
-func (dc *downstreamConn) messageSupportsHistory(msg *irc.Message) bool {
+func (dc *downstreamConn) messageSupportsBacklog(msg *irc.Message) bool {
 	// Don't replay all messages, because that would mess up client
 	// state. For instance we just sent the list of users, sending
 	// PART messages for one of these users would be incorrect.
-	// TODO: add support for draft/event-playback
 	switch msg.Command {
 	case "PRIVMSG", "NOTICE":
@@ -1329,8 +1339,4 @@
 	dc.SendBatch("chathistory", []string{dc.marshalEntity(net, target)}, nil, func(batchRef irc.TagValue) {
 		for _, msg := range history {
-			if !dc.messageSupportsHistory(msg) {
-				continue
-			}
-
 			if ch != nil && ch.Detached {
 				if net.detachedMessageNeedsRelay(ch, msg) {
@@ -2327,19 +2333,21 @@
 		}
 
+		eventPlayback := dc.caps["draft/event-playback"]
+
 		var history []*irc.Message
 		switch subcommand {
 		case "BEFORE":
-			history, err = store.LoadBeforeTime(network, entity, bounds[0], time.Time{}, limit)
+			history, err = store.LoadBeforeTime(network, entity, bounds[0], time.Time{}, limit, eventPlayback)
 		case "AFTER":
-			history, err = store.LoadAfterTime(network, entity, bounds[0], time.Now(), limit)
+			history, err = store.LoadAfterTime(network, entity, bounds[0], time.Now(), limit, eventPlayback)
 		case "BETWEEN":
 			if bounds[0].Before(bounds[1]) {
-				history, err = store.LoadAfterTime(network, entity, bounds[0], bounds[1], limit)
+				history, err = store.LoadAfterTime(network, entity, bounds[0], bounds[1], limit, eventPlayback)
 			} else {
-				history, err = store.LoadBeforeTime(network, entity, bounds[0], bounds[1], limit)
+				history, err = store.LoadBeforeTime(network, entity, bounds[0], bounds[1], limit, eventPlayback)
 			}
 		case "TARGETS":
 			// TODO: support TARGETS in multi-upstream mode
-			targets, err := store.ListTargets(network, bounds[0], bounds[1], limit)
+			targets, err := store.ListTargets(network, bounds[0], bounds[1], limit, eventPlayback)
 			if err != nil {
 				dc.logger.Printf("failed fetching targets for chathistory: %v", err)
Index: /trunk/msgstore.go
===================================================================
--- /trunk/msgstore.go	(revision 664)
+++ /trunk/msgstore.go	(revision 665)
@@ -18,4 +18,6 @@
 	// used in history queries.
 	LastMsgID(network *network, entity string, t time.Time) (string, error)
+	// LoadLatestID queries the latest non-event messages for the given network,
+	// entity and date, up to a count of limit messages, sorted from oldest to newest.
 	LoadLatestID(network *network, entity, id string, limit int) ([]*irc.Message, error)
 	Append(network *network, entity string, msg *irc.Message) (id string, err error)
@@ -35,13 +37,16 @@
 	// It returns up to limit targets, starting from start and ending on end,
 	// both excluded. end may be before or after start.
-	ListTargets(network *network, start, end time.Time, limit int) ([]chatHistoryTarget, error)
+	// If events is false, only PRIVMSG/NOTICE messages are considered.
+	ListTargets(network *network, start, end time.Time, limit int, events bool) ([]chatHistoryTarget, error)
 	// LoadBeforeTime loads up to limit messages before start down to end. The
 	// returned messages must be between and excluding the provided bounds.
 	// end is before start.
-	LoadBeforeTime(network *network, entity string, start, end time.Time, limit int) ([]*irc.Message, error)
+	// If events is false, only PRIVMSG/NOTICE messages are considered.
+	LoadBeforeTime(network *network, entity string, start, end time.Time, limit int, events bool) ([]*irc.Message, error)
 	// LoadBeforeTime loads up to limit messages after start up to end. The
 	// returned messages must be between and excluding the provided bounds.
 	// end is after start.
-	LoadAfterTime(network *network, entity string, start, end time.Time, limit int) ([]*irc.Message, error)
+	// If events is false, only PRIVMSG/NOTICE messages are considered.
+	LoadAfterTime(network *network, entity string, start, end time.Time, limit int, events bool) ([]*irc.Message, error)
 }
 
Index: /trunk/msgstore_fs.go
===================================================================
--- /trunk/msgstore_fs.go	(revision 664)
+++ /trunk/msgstore_fs.go	(revision 665)
@@ -250,5 +250,5 @@
 }
 
-func parseMessage(line, entity string, ref time.Time) (*irc.Message, time.Time, error) {
+func parseMessage(line, entity string, ref time.Time, events bool) (*irc.Message, time.Time, error) {
 	var hour, minute, second int
 	_, err := fmt.Sscanf(line, "[%02d:%02d:%02d] ", &hour, &minute, &second)
@@ -258,28 +258,119 @@
 	line = line[11:]
 
-	var cmd, sender, text string
-	if strings.HasPrefix(line, "<") {
-		cmd = "PRIVMSG"
-		parts := strings.SplitN(line[1:], "> ", 2)
+	var cmd string
+	var prefix *irc.Prefix
+	var params []string
+	if events && strings.HasPrefix(line, "*** ") {
+		parts := strings.SplitN(line[4:], " ", 2)
 		if len(parts) != 2 {
 			return nil, time.Time{}, nil
 		}
-		sender, text = parts[0], parts[1]
-	} else if strings.HasPrefix(line, "-") {
-		cmd = "NOTICE"
-		parts := strings.SplitN(line[1:], "- ", 2)
-		if len(parts) != 2 {
+		switch parts[0] {
+		case "Joins:", "Parts:", "Quits:":
+			args := strings.SplitN(parts[1], " ", 3)
+			if len(args) < 2 {
+				return nil, time.Time{}, nil
+			}
+			nick := args[0]
+			mask := strings.TrimSuffix(strings.TrimPrefix(args[1], "("), ")")
+			maskParts := strings.SplitN(mask, "@", 2)
+			if len(maskParts) != 2 {
+				return nil, time.Time{}, nil
+			}
+			prefix = &irc.Prefix{
+				Name: nick,
+				User: maskParts[0],
+				Host: maskParts[1],
+			}
+			var reason string
+			if len(args) > 2 {
+				reason = strings.TrimSuffix(strings.TrimPrefix(args[2], "("), ")")
+			}
+			switch parts[0] {
+			case "Joins:":
+				cmd = "JOIN"
+				params = []string{entity}
+			case "Parts:":
+				cmd = "PART"
+				if reason != "" {
+					params = []string{entity, reason}
+				} else {
+					params = []string{entity}
+				}
+			case "Quits:":
+				cmd = "QUIT"
+				if reason != "" {
+					params = []string{reason}
+				}
+			}
+		default:
+			nick := parts[0]
+			rem := parts[1]
+			if r := strings.TrimPrefix(rem, "is now known as "); r != rem {
+				cmd = "NICK"
+				prefix = &irc.Prefix{
+					Name: nick,
+				}
+				params = []string{r}
+			} else if r := strings.TrimPrefix(rem, "was kicked by "); r != rem {
+				args := strings.SplitN(r, " ", 2)
+				if len(args) != 2 {
+					return nil, time.Time{}, nil
+				}
+				cmd = "KICK"
+				prefix = &irc.Prefix{
+					Name: args[0],
+				}
+				reason := strings.TrimSuffix(strings.TrimPrefix(args[1], "("), ")")
+				params = []string{entity, nick}
+				if reason != "" {
+					params = append(params, reason)
+				}
+			} else if r := strings.TrimPrefix(rem, "changes topic to "); r != rem {
+				cmd = "TOPIC"
+				prefix = &irc.Prefix{
+					Name: nick,
+				}
+				topic := strings.TrimSuffix(strings.TrimPrefix(r, "'"), "'")
+				params = []string{entity, topic}
+			} else if r := strings.TrimPrefix(rem, "sets mode: "); r != rem {
+				cmd = "MODE"
+				prefix = &irc.Prefix{
+					Name: nick,
+				}
+				params = append([]string{entity}, strings.Split(r, " ")...)
+			} else {
+				return nil, time.Time{}, nil
+			}
+		}
+	} else {
+		var sender, text string
+		if strings.HasPrefix(line, "<") {
+			cmd = "PRIVMSG"
+			parts := strings.SplitN(line[1:], "> ", 2)
+			if len(parts) != 2 {
+				return nil, time.Time{}, nil
+			}
+			sender, text = parts[0], parts[1]
+		} else if strings.HasPrefix(line, "-") {
+			cmd = "NOTICE"
+			parts := strings.SplitN(line[1:], "- ", 2)
+			if len(parts) != 2 {
+				return nil, time.Time{}, nil
+			}
+			sender, text = parts[0], parts[1]
+		} else if strings.HasPrefix(line, "* ") {
+			cmd = "PRIVMSG"
+			parts := strings.SplitN(line[2:], " ", 2)
+			if len(parts) != 2 {
+				return nil, time.Time{}, nil
+			}
+			sender, text = parts[0], "\x01ACTION "+parts[1]+"\x01"
+		} else {
 			return nil, time.Time{}, nil
 		}
-		sender, text = parts[0], parts[1]
-	} else if strings.HasPrefix(line, "* ") {
-		cmd = "PRIVMSG"
-		parts := strings.SplitN(line[2:], " ", 2)
-		if len(parts) != 2 {
-			return nil, time.Time{}, nil
-		}
-		sender, text = parts[0], "\x01ACTION "+parts[1]+"\x01"
-	} else {
-		return nil, time.Time{}, nil
+
+		prefix = &irc.Prefix{Name: sender}
+		params = []string{entity, text}
 	}
 
@@ -291,12 +382,12 @@
 			"time": irc.TagValue(t.UTC().Format(serverTimeLayout)),
 		},
-		Prefix:  &irc.Prefix{Name: sender},
+		Prefix:  prefix,
 		Command: cmd,
-		Params:  []string{entity, text},
+		Params:  params,
 	}
 	return msg, t, nil
 }
 
-func (ms *fsMessageStore) parseMessagesBefore(network *network, entity string, ref time.Time, end time.Time, limit int, afterOffset int64) ([]*irc.Message, error) {
+func (ms *fsMessageStore) parseMessagesBefore(network *network, entity string, ref time.Time, end time.Time, events bool, limit int, afterOffset int64) ([]*irc.Message, error) {
 	path := ms.logPath(network, entity, ref)
 	f, err := os.Open(path)
@@ -322,5 +413,5 @@
 
 	for sc.Scan() {
-		msg, t, err := parseMessage(sc.Text(), entity, ref)
+		msg, t, err := parseMessage(sc.Text(), entity, ref, events)
 		if err != nil {
 			return nil, err
@@ -354,5 +445,5 @@
 }
 
-func (ms *fsMessageStore) parseMessagesAfter(network *network, entity string, ref time.Time, end time.Time, limit int) ([]*irc.Message, error) {
+func (ms *fsMessageStore) parseMessagesAfter(network *network, entity string, ref time.Time, end time.Time, events bool, limit int) ([]*irc.Message, error) {
 	path := ms.logPath(network, entity, ref)
 	f, err := os.Open(path)
@@ -368,5 +459,5 @@
 	sc := bufio.NewScanner(f)
 	for sc.Scan() && len(history) < limit {
-		msg, t, err := parseMessage(sc.Text(), entity, ref)
+		msg, t, err := parseMessage(sc.Text(), entity, ref, events)
 		if err != nil {
 			return nil, err
@@ -386,5 +477,5 @@
 }
 
-func (ms *fsMessageStore) LoadBeforeTime(network *network, entity string, start time.Time, end time.Time, limit int) ([]*irc.Message, error) {
+func (ms *fsMessageStore) LoadBeforeTime(network *network, entity string, start time.Time, end time.Time, limit int, events bool) ([]*irc.Message, error) {
 	start = start.In(time.Local)
 	end = end.In(time.Local)
@@ -393,5 +484,5 @@
 	tries := 0
 	for remaining > 0 && tries < fsMessageStoreMaxTries && end.Before(start) {
-		buf, err := ms.parseMessagesBefore(network, entity, start, end, remaining, -1)
+		buf, err := ms.parseMessagesBefore(network, entity, start, end, events, remaining, -1)
 		if err != nil {
 			return nil, err
@@ -411,5 +502,5 @@
 }
 
-func (ms *fsMessageStore) LoadAfterTime(network *network, entity string, start time.Time, end time.Time, limit int) ([]*irc.Message, error) {
+func (ms *fsMessageStore) LoadAfterTime(network *network, entity string, start time.Time, end time.Time, limit int, events bool) ([]*irc.Message, error) {
 	start = start.In(time.Local)
 	end = end.In(time.Local)
@@ -418,5 +509,5 @@
 	tries := 0
 	for remaining > 0 && tries < fsMessageStoreMaxTries && start.Before(end) {
-		buf, err := ms.parseMessagesAfter(network, entity, start, end, remaining)
+		buf, err := ms.parseMessagesAfter(network, entity, start, end, events, remaining)
 		if err != nil {
 			return nil, err
@@ -461,5 +552,5 @@
 		}
 
-		buf, err := ms.parseMessagesBefore(network, entity, t, time.Time{}, remaining, offset)
+		buf, err := ms.parseMessagesBefore(network, entity, t, time.Time{}, false, remaining, offset)
 		if err != nil {
 			return nil, err
@@ -479,5 +570,5 @@
 }
 
-func (ms *fsMessageStore) ListTargets(network *network, start, end time.Time, limit int) ([]chatHistoryTarget, error) {
+func (ms *fsMessageStore) ListTargets(network *network, start, end time.Time, limit int, events bool) ([]chatHistoryTarget, error) {
 	start = start.In(time.Local)
 	end = end.In(time.Local)
Index: /trunk/msgstore_memory.go
===================================================================
--- /trunk/msgstore_memory.go	(revision 664)
+++ /trunk/msgstore_memory.go	(revision 665)
@@ -75,4 +75,10 @@
 
 func (ms *memoryMessageStore) Append(network *network, entity string, msg *irc.Message) (string, error) {
+	switch msg.Command {
+	case "PRIVMSG", "NOTICE":
+	default:
+		return "", nil
+	}
+
 	k := ringBufferKey{networkID: network.ID, entity: entity}
 	rb, ok := ms.buffers[k]
