Collapse and log join, part and quit, closes #27, log nick and topic changes, move state into irc package

This commit is contained in:
Ken-Håvard Lieng 2020-06-03 03:04:38 +02:00
parent edd4d6eadb
commit ead3b37cf9
37 changed files with 1980 additions and 969 deletions

View file

@ -278,19 +278,36 @@ func (s *BoltStore) RemoveOpenDM(user *storage.User, server, nick string) error
})
}
func (s *BoltStore) logMessage(tx *bolt.Tx, message *storage.Message) error {
b, err := tx.Bucket(bucketMessages).CreateBucketIfNotExists([]byte(message.Server + ":" + message.To))
if err != nil {
return err
}
data, err := message.Marshal(nil)
if err != nil {
return err
}
return b.Put([]byte(message.ID), data)
}
func (s *BoltStore) LogMessage(message *storage.Message) error {
return s.db.Batch(func(tx *bolt.Tx) error {
b, err := tx.Bucket(bucketMessages).CreateBucketIfNotExists([]byte(message.Server + ":" + message.To))
if err != nil {
return err
return s.logMessage(tx, message)
})
}
func (s *BoltStore) LogMessages(messages []*storage.Message) error {
return s.db.Batch(func(tx *bolt.Tx) error {
for _, message := range messages {
err := s.logMessage(tx, message)
if err != nil {
return err
}
}
data, err := message.Marshal(nil)
if err != nil {
return err
}
return b.Put([]byte(message.ID), data)
return nil
})
}

View file

@ -1,206 +0,0 @@
package storage
import (
"strings"
"sync"
)
type ChannelStore struct {
users map[string]map[string][]*ChannelStoreUser
userLock sync.Mutex
topic map[string]map[string]string
topicLock sync.Mutex
}
const userModePrefixes = "~&@%+"
const userModeChars = "qaohv"
type ChannelStoreUser struct {
nick string
modes string
prefix string
}
func NewChannelStoreUser(nick string) *ChannelStoreUser {
user := &ChannelStoreUser{nick: nick}
if i := strings.IndexAny(nick, userModePrefixes); i == 0 {
i = strings.Index(userModePrefixes, string(nick[0]))
user.modes = string(userModeChars[i])
user.nick = nick[1:]
user.updatePrefix()
}
return user
}
func (c *ChannelStoreUser) String() string {
return c.prefix + c.nick
}
func (c *ChannelStoreUser) addModes(modes string) {
for _, mode := range modes {
if strings.Contains(c.modes, string(mode)) {
continue
}
c.modes += string(mode)
}
c.updatePrefix()
}
func (c *ChannelStoreUser) removeModes(modes string) {
for _, mode := range modes {
c.modes = strings.Replace(c.modes, string(mode), "", 1)
}
c.updatePrefix()
}
func (c *ChannelStoreUser) updatePrefix() {
for i, mode := range userModeChars {
if strings.Contains(c.modes, string(mode)) {
c.prefix = string(userModePrefixes[i])
return
}
}
c.prefix = ""
}
func NewChannelStore() *ChannelStore {
return &ChannelStore{
users: make(map[string]map[string][]*ChannelStoreUser),
topic: make(map[string]map[string]string),
}
}
func (c *ChannelStore) GetUsers(server, channel string) []string {
c.userLock.Lock()
users := make([]string, len(c.users[server][channel]))
for i, user := range c.users[server][channel] {
users[i] = user.String()
}
c.userLock.Unlock()
return users
}
func (c *ChannelStore) SetUsers(users []string, server, channel string) {
c.userLock.Lock()
if _, ok := c.users[server]; !ok {
c.users[server] = make(map[string][]*ChannelStoreUser)
}
c.users[server][channel] = make([]*ChannelStoreUser, len(users))
for i, nick := range users {
c.users[server][channel][i] = NewChannelStoreUser(nick)
}
c.userLock.Unlock()
}
func (c *ChannelStore) AddUser(user, server, channel string) {
c.userLock.Lock()
if _, ok := c.users[server]; !ok {
c.users[server] = make(map[string][]*ChannelStoreUser)
}
if users, ok := c.users[server][channel]; ok {
for _, u := range users {
if u.nick == user {
c.userLock.Unlock()
return
}
}
c.users[server][channel] = append(users, NewChannelStoreUser(user))
} else {
c.users[server][channel] = []*ChannelStoreUser{NewChannelStoreUser(user)}
}
c.userLock.Unlock()
}
func (c *ChannelStore) RemoveUser(user, server, channel string) {
c.userLock.Lock()
c.removeUser(user, server, channel)
c.userLock.Unlock()
}
func (c *ChannelStore) RemoveUserAll(user, server string) {
c.userLock.Lock()
for channel := range c.users[server] {
c.removeUser(user, server, channel)
}
c.userLock.Unlock()
}
func (c *ChannelStore) RenameUser(oldNick, newNick, server string) {
c.userLock.Lock()
c.renameAll(server, oldNick, newNick)
c.userLock.Unlock()
}
func (c *ChannelStore) SetMode(server, channel, user, add, remove string) {
c.userLock.Lock()
for _, u := range c.users[server][channel] {
if u.nick == user {
u.addModes(add)
u.removeModes(remove)
c.userLock.Unlock()
return
}
}
c.userLock.Unlock()
}
func (c *ChannelStore) GetTopic(server, channel string) string {
c.topicLock.Lock()
topic := c.topic[server][channel]
c.topicLock.Unlock()
return topic
}
func (c *ChannelStore) SetTopic(topic, server, channel string) {
c.topicLock.Lock()
if _, ok := c.topic[server]; !ok {
c.topic[server] = make(map[string]string)
}
c.topic[server][channel] = topic
c.topicLock.Unlock()
}
func (c *ChannelStore) rename(server, channel, oldNick, newNick string) {
for _, user := range c.users[server][channel] {
if user.nick == oldNick {
user.nick = newNick
return
}
}
}
func (c *ChannelStore) renameAll(server, oldNick, newNick string) {
for channel := range c.users[server] {
c.rename(server, channel, oldNick, newNick)
}
}
func (c *ChannelStore) removeUser(user, server, channel string) {
for i, u := range c.users[server][channel] {
if u.nick == user {
users := c.users[server][channel]
c.users[server][channel] = append(users[:i], users[i+1:]...)
return
}
}
}

View file

@ -1,89 +0,0 @@
package storage
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetSetUsers(t *testing.T) {
channelStore := NewChannelStore()
users := []string{"a", "b"}
channelStore.SetUsers(users, "srv", "#chan")
assert.Equal(t, users, channelStore.GetUsers("srv", "#chan"))
channelStore.SetUsers(users, "srv", "#chan")
assert.Equal(t, users, channelStore.GetUsers("srv", "#chan"))
}
func TestAddRemoveUser(t *testing.T) {
channelStore := NewChannelStore()
channelStore.AddUser("user", "srv", "#chan")
channelStore.AddUser("user", "srv", "#chan")
assert.Len(t, channelStore.GetUsers("srv", "#chan"), 1)
channelStore.AddUser("user2", "srv", "#chan")
assert.Equal(t, []string{"user", "user2"}, channelStore.GetUsers("srv", "#chan"))
channelStore.RemoveUser("user", "srv", "#chan")
assert.Equal(t, []string{"user2"}, channelStore.GetUsers("srv", "#chan"))
}
func TestRemoveUserAll(t *testing.T) {
channelStore := NewChannelStore()
channelStore.AddUser("user", "srv", "#chan1")
channelStore.AddUser("user", "srv", "#chan2")
channelStore.RemoveUserAll("user", "srv")
assert.Empty(t, channelStore.GetUsers("srv", "#chan1"))
assert.Empty(t, channelStore.GetUsers("srv", "#chan2"))
}
func TestRenameUser(t *testing.T) {
channelStore := NewChannelStore()
channelStore.AddUser("user", "srv", "#chan1")
channelStore.AddUser("user", "srv", "#chan2")
channelStore.RenameUser("user", "new", "srv")
assert.Equal(t, []string{"new"}, channelStore.GetUsers("srv", "#chan1"))
assert.Equal(t, []string{"new"}, channelStore.GetUsers("srv", "#chan2"))
channelStore.AddUser("@gotop", "srv", "#chan3")
channelStore.RenameUser("gotop", "stillgotit", "srv")
assert.Equal(t, []string{"@stillgotit"}, channelStore.GetUsers("srv", "#chan3"))
}
func TestMode(t *testing.T) {
channelStore := NewChannelStore()
channelStore.AddUser("+user", "srv", "#chan")
channelStore.SetMode("srv", "#chan", "user", "o", "v")
assert.Equal(t, []string{"@user"}, channelStore.GetUsers("srv", "#chan"))
channelStore.SetMode("srv", "#chan", "user", "v", "")
assert.Equal(t, []string{"@user"}, channelStore.GetUsers("srv", "#chan"))
channelStore.SetMode("srv", "#chan", "user", "", "o")
assert.Equal(t, []string{"+user"}, channelStore.GetUsers("srv", "#chan"))
channelStore.SetMode("srv", "#chan", "user", "q", "")
assert.Equal(t, []string{"~user"}, channelStore.GetUsers("srv", "#chan"))
}
func TestTopic(t *testing.T) {
channelStore := NewChannelStore()
assert.Equal(t, "", channelStore.GetTopic("srv", "#chan"))
channelStore.SetTopic("the topic", "srv", "#chan")
assert.Equal(t, "the topic", channelStore.GetTopic("srv", "#chan"))
}
func TestChannelUserMode(t *testing.T) {
user := NewChannelStoreUser("&test")
assert.Equal(t, "test", user.nick)
assert.Equal(t, "a", string(user.modes[0]))
assert.Equal(t, "&test", user.String())
user.removeModes("a")
assert.Equal(t, "test", user.String())
user.addModes("o")
assert.Equal(t, "@test", user.String())
user.addModes("q")
assert.Equal(t, "~test", user.String())
user.addModes("v")
assert.Equal(t, "~test", user.String())
user.removeModes("qo")
assert.Equal(t, "+test", user.String())
user.removeModes("v")
assert.Equal(t, "test", user.String())
}

View file

@ -7,7 +7,12 @@ import (
"github.com/khlieng/dispatch/pkg/session"
)
var Path directory
var (
Path directory
GetMessageStore MessageStoreCreator
GetMessageSearchProvider MessageSearchProviderCreator
)
func Initialize(root, dataRoot, configRoot string) {
if root != DefaultDirectory() {
@ -52,13 +57,18 @@ type SessionStore interface {
type MessageStore interface {
LogMessage(message *Message) error
LogMessages(messages []*Message) error
GetMessages(server, channel string, count int, fromID string) ([]Message, bool, error)
GetMessagesByID(server, channel string, ids []string) ([]Message, error)
Close()
}
type MessageStoreCreator func(*User) (MessageStore, error)
type MessageSearchProvider interface {
SearchMessages(server, channel, q string) ([]string, error)
Index(id string, message *Message) error
Close()
}
type MessageSearchProviderCreator func(*User) (MessageSearchProvider, error)

View file

@ -25,7 +25,6 @@ struct Server {
struct Channel {
Server string
Name string
Topic string
}
struct Message {
@ -33,4 +32,11 @@ struct Message {
From string
Content string
Time int64
Events []Event
}
struct Event {
Type string
Params []string
Time int64
}

View file

@ -791,21 +791,6 @@ func (d *Channel) Size() (s uint64) {
}
s += l
}
{
l := uint64(len(d.Topic))
{
t := l
for t >= 0x80 {
t >>= 7
s++
}
s++
}
s += l
}
return
}
func (d *Channel) Marshal(buf []byte) ([]byte, error) {
@ -857,25 +842,6 @@ func (d *Channel) Marshal(buf []byte) ([]byte, error) {
copy(buf[i+0:], d.Name)
i += l
}
{
l := uint64(len(d.Topic))
{
t := uint64(l)
for t >= 0x80 {
buf[i+0] = byte(t) | 0x80
t >>= 7
i++
}
buf[i+0] = byte(t)
i++
}
copy(buf[i+0:], d.Topic)
i += l
}
return buf[:i+0], nil
}
@ -922,26 +888,6 @@ func (d *Channel) Unmarshal(buf []byte) (uint64, error) {
d.Name = string(buf[i+0 : i+0+l])
i += l
}
{
l := uint64(0)
{
bs := uint8(7)
t := uint64(buf[i+0] & 0x7F)
for buf[i+0]&0x80 == 0x80 {
i++
t |= uint64(buf[i+0]&0x7F) << bs
bs += 7
}
i++
l = t
}
d.Topic = string(buf[i+0 : i+0+l])
i += l
}
return i + 0, nil
}
@ -992,6 +938,29 @@ func (d *Message) Size() (s uint64) {
}
s += l
}
{
l := uint64(len(d.Events))
{
t := l
for t >= 0x80 {
t >>= 7
s++
}
s++
}
for k0 := range d.Events {
{
s += d.Events[k0].Size()
}
}
}
s += 8
return
}
@ -1068,6 +1037,34 @@ func (d *Message) Marshal(buf []byte) ([]byte, error) {
*(*int64)(unsafe.Pointer(&buf[i+0])) = d.Time
}
{
l := uint64(len(d.Events))
{
t := uint64(l)
for t >= 0x80 {
buf[i+8] = byte(t) | 0x80
t >>= 7
i++
}
buf[i+8] = byte(t)
i++
}
for k0 := range d.Events {
{
nbuf, err := d.Events[k0].Marshal(buf[i+8:])
if err != nil {
return nil, err
}
i += uint64(len(nbuf))
}
}
}
return buf[:i+8], nil
}
@ -1138,6 +1135,251 @@ func (d *Message) Unmarshal(buf []byte) (uint64, error) {
d.Time = *(*int64)(unsafe.Pointer(&buf[i+0]))
}
{
l := uint64(0)
{
bs := uint8(7)
t := uint64(buf[i+8] & 0x7F)
for buf[i+8]&0x80 == 0x80 {
i++
t |= uint64(buf[i+8]&0x7F) << bs
bs += 7
}
i++
l = t
}
if uint64(cap(d.Events)) >= l {
d.Events = d.Events[:l]
} else {
d.Events = make([]Event, l)
}
for k0 := range d.Events {
{
ni, err := d.Events[k0].Unmarshal(buf[i+8:])
if err != nil {
return 0, err
}
i += ni
}
}
}
return i + 8, nil
}
func (d *Event) Size() (s uint64) {
{
l := uint64(len(d.Type))
{
t := l
for t >= 0x80 {
t >>= 7
s++
}
s++
}
s += l
}
{
l := uint64(len(d.Params))
{
t := l
for t >= 0x80 {
t >>= 7
s++
}
s++
}
for k0 := range d.Params {
{
l := uint64(len(d.Params[k0]))
{
t := l
for t >= 0x80 {
t >>= 7
s++
}
s++
}
s += l
}
}
}
s += 8
return
}
func (d *Event) Marshal(buf []byte) ([]byte, error) {
size := d.Size()
{
if uint64(cap(buf)) >= size {
buf = buf[:size]
} else {
buf = make([]byte, size)
}
}
i := uint64(0)
{
l := uint64(len(d.Type))
{
t := uint64(l)
for t >= 0x80 {
buf[i+0] = byte(t) | 0x80
t >>= 7
i++
}
buf[i+0] = byte(t)
i++
}
copy(buf[i+0:], d.Type)
i += l
}
{
l := uint64(len(d.Params))
{
t := uint64(l)
for t >= 0x80 {
buf[i+0] = byte(t) | 0x80
t >>= 7
i++
}
buf[i+0] = byte(t)
i++
}
for k0 := range d.Params {
{
l := uint64(len(d.Params[k0]))
{
t := uint64(l)
for t >= 0x80 {
buf[i+0] = byte(t) | 0x80
t >>= 7
i++
}
buf[i+0] = byte(t)
i++
}
copy(buf[i+0:], d.Params[k0])
i += l
}
}
}
{
*(*int64)(unsafe.Pointer(&buf[i+0])) = d.Time
}
return buf[:i+8], nil
}
func (d *Event) Unmarshal(buf []byte) (uint64, error) {
i := uint64(0)
{
l := uint64(0)
{
bs := uint8(7)
t := uint64(buf[i+0] & 0x7F)
for buf[i+0]&0x80 == 0x80 {
i++
t |= uint64(buf[i+0]&0x7F) << bs
bs += 7
}
i++
l = t
}
d.Type = string(buf[i+0 : i+0+l])
i += l
}
{
l := uint64(0)
{
bs := uint8(7)
t := uint64(buf[i+0] & 0x7F)
for buf[i+0]&0x80 == 0x80 {
i++
t |= uint64(buf[i+0]&0x7F) << bs
bs += 7
}
i++
l = t
}
if uint64(cap(d.Params)) >= l {
d.Params = d.Params[:l]
} else {
d.Params = make([]string, l)
}
for k0 := range d.Params {
{
l := uint64(0)
{
bs := uint8(7)
t := uint64(buf[i+0] & 0x7F)
for buf[i+0]&0x80 == 0x80 {
i++
t |= uint64(buf[i+0]&0x7F) << bs
bs += 7
}
i++
l = t
}
d.Params[k0] = string(buf[i+0 : i+0+l])
i += l
}
}
}
{
d.Time = *(*int64)(unsafe.Pointer(&buf[i+0]))
}
return i + 8, nil
}

View file

@ -5,6 +5,8 @@ import (
"os"
"sync"
"time"
"github.com/kjk/betterguid"
)
type User struct {
@ -15,6 +17,7 @@ type User struct {
store Store
messageLog MessageStore
messageIndex MessageSearchProvider
lastMessages map[string]map[string]*Message
clientSettings *ClientSettings
lastIP []byte
certificate *tls.Certificate
@ -25,6 +28,7 @@ func NewUser(store Store) (*User, error) {
user := &User{
store: store,
clientSettings: DefaultClientSettings(),
lastMessages: map[string]map[string]*Message{},
}
err := store.SaveUser(user)
@ -32,11 +36,19 @@ func NewUser(store Store) (*User, error) {
return nil, err
}
user.messageLog, err = GetMessageStore(user)
if err != nil {
return nil, err
}
user.messageIndex, err = GetMessageSearchProvider(user)
if err != nil {
return nil, err
}
err = os.MkdirAll(Path.User(user.Username), 0700)
if err != nil {
return nil, err
}
err = os.Mkdir(Path.Downloads(user.Username), 0700)
if err != nil {
return nil, err
@ -53,20 +65,35 @@ func LoadUsers(store Store) ([]*User, error) {
for _, user := range users {
user.store = store
user.messageLog, err = GetMessageStore(user)
if err != nil {
return nil, err
}
user.messageIndex, err = GetMessageSearchProvider(user)
if err != nil {
return nil, err
}
user.lastMessages = map[string]map[string]*Message{}
user.loadCertificate()
channels, err := user.GetChannels()
if err != nil {
return nil, err
}
for _, channel := range channels {
messages, _, err := user.GetLastMessages(channel.Server, channel.Name, 1)
if err == nil && len(messages) == 1 {
user.lastMessages[channel.Server] = map[string]*Message{
channel.Name: &messages[0],
}
}
}
}
return users, nil
}
func (u *User) SetMessageStore(store MessageStore) {
u.messageLog = store
}
func (u *User) SetMessageSearchProvider(search MessageSearchProvider) {
u.messageIndex = search
}
func (u *User) Remove() {
u.store.DeleteUser(u)
if u.messageLog != nil {
@ -178,7 +205,6 @@ func (u *User) SetServerName(name, address string) error {
return u.AddServer(server)
}
// TODO: Remove topic from disk schema
type Channel struct {
Server string
Name string
@ -215,33 +241,128 @@ func (u *User) RemoveOpenDM(server, nick string) error {
}
type Message struct {
ID string `json:"-" bleve:"-"`
Server string `json:"-" bleve:"server"`
From string `bleve:"-"`
To string `json:"-" bleve:"to"`
Content string `bleve:"content"`
Time int64 `bleve:"-"`
ID string `json:"-" bleve:"-"`
Server string `json:"-" bleve:"server"`
From string `bleve:"-"`
To string `json:"-" bleve:"to"`
Content string `bleve:"content"`
Time int64 `bleve:"-"`
Events []Event `bleve:"-"`
}
func (m Message) Type() string {
return "message"
}
func (u *User) LogMessage(id, server, from, to, content string) error {
message := &Message{
ID: id,
Server: server,
From: from,
To: to,
Content: content,
Time: time.Now().Unix(),
func (u *User) LogMessage(msg *Message) error {
if msg.Time == 0 {
msg.Time = time.Now().Unix()
}
err := u.messageLog.LogMessage(message)
if msg.ID == "" {
msg.ID = betterguid.New()
}
if msg.To == "" {
msg.To = msg.From
}
u.setLastMessage(msg.Server, msg.To, msg)
err := u.messageLog.LogMessage(msg)
if err != nil {
return err
}
return u.messageIndex.Index(id, message)
return u.messageIndex.Index(msg.ID, msg)
}
type Event struct {
Type string
Params []string
Time int64
}
func (u *User) LogEvent(server, name string, params []string, channels ...string) error {
now := time.Now().Unix()
event := Event{
Type: name,
Params: params,
Time: now,
}
for _, channel := range channels {
lastMessage := u.getLastMessage(server, channel)
if lastMessage != nil && shouldCollapse(lastMessage, event) {
lastMessage.Events = append(lastMessage.Events, event)
u.setLastMessage(server, channel, lastMessage)
err := u.messageLog.LogMessage(lastMessage)
if err != nil {
return err
}
} else {
msg := &Message{
ID: betterguid.New(),
Server: server,
To: channel,
Time: now,
Events: []Event{event},
}
u.setLastMessage(server, channel, msg)
err := u.messageLog.LogMessage(msg)
if err != nil {
return err
}
}
}
return nil
}
var collapsed = []string{"join", "part", "quit"}
func shouldCollapse(msg *Message, event Event) bool {
matches := 0
if len(msg.Events) > 0 {
for _, collapseType := range collapsed {
if msg.Events[0].Type == collapseType {
matches++
}
if event.Type == collapseType {
matches++
}
}
}
return matches == 2
}
func (u *User) getLastMessage(server, channel string) *Message {
u.lock.Lock()
defer u.lock.Unlock()
if _, ok := u.lastMessages[server]; !ok {
return nil
}
last := u.lastMessages[server][channel]
if last != nil {
msg := *last
return &msg
}
return nil
}
func (u *User) setLastMessage(server, channel string, msg *Message) {
u.lock.Lock()
if _, ok := u.lastMessages[server]; !ok {
u.lastMessages[server] = map[string]*Message{}
}
u.lastMessages[server][channel] = msg
u.lock.Unlock()
}
func (u *User) GetMessages(server, channel string, count int, fromID string) ([]Message, bool, error) {

View file

@ -24,6 +24,13 @@ func TestUser(t *testing.T) {
db, err := boltdb.New(storage.Path.Database())
assert.Nil(t, err)
storage.GetMessageStore = func(_ *storage.User) (storage.MessageStore, error) {
return db, nil
}
storage.GetMessageSearchProvider = func(_ *storage.User) (storage.MessageSearchProvider, error) {
return nil, nil
}
user, err := storage.NewUser(db)
assert.Nil(t, err)
@ -124,17 +131,18 @@ func TestMessages(t *testing.T) {
db, err := boltdb.New(storage.Path.Database())
assert.Nil(t, err)
storage.GetMessageStore = func(_ *storage.User) (storage.MessageStore, error) {
return db, nil
}
storage.GetMessageSearchProvider = func(user *storage.User) (storage.MessageSearchProvider, error) {
return bleve.New(storage.Path.Index(user.Username))
}
user, err := storage.NewUser(db)
assert.Nil(t, err)
os.MkdirAll(storage.Path.User(user.Username), 0700)
search, err := bleve.New(storage.Path.Index(user.Username))
assert.Nil(t, err)
user.SetMessageStore(db)
user.SetMessageSearchProvider(search)
messages, hasMore, err := user.GetMessages("irc.freenode.net", "#go-nuts", 10, "6")
assert.Nil(t, err)
assert.False(t, hasMore)
@ -153,7 +161,13 @@ func TestMessages(t *testing.T) {
for i := 0; i < 5; i++ {
id := betterguid.New()
ids = append(ids, id)
err = user.LogMessage(id, "irc.freenode.net", "nick", "#go-nuts", "message"+strconv.Itoa(i))
err = user.LogMessage(&storage.Message{
ID: id,
Server: "irc.freenode.net",
From: "nick",
To: "#go-nuts",
Content: "message" + strconv.Itoa(i),
})
assert.Nil(t, err)
}
@ -196,5 +210,42 @@ func TestMessages(t *testing.T) {
assert.Nil(t, err)
assert.True(t, len(messages) > 0)
user.LogEvent("irc.freenode.net", "join", []string{"bob"}, "#go-nuts")
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 1)
assert.Zero(t, messages[0].Content)
assert.Nil(t, err)
assert.True(t, hasMore)
assert.Len(t, messages[0].Events, 1)
assert.Equal(t, "join", messages[0].Events[0].Type)
assert.NotZero(t, messages[0].Events[0].Time)
user.LogEvent("irc.freenode.net", "part", []string{"bob"}, "#go-nuts")
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 1)
assert.Zero(t, messages[0].Content)
assert.Nil(t, err)
assert.True(t, hasMore)
assert.Len(t, messages[0].Events, 2)
assert.Equal(t, "part", messages[0].Events[1].Type)
assert.NotZero(t, messages[0].Events[0].Time)
user.LogEvent("irc.freenode.net", "nick", []string{"bob", "rob"}, "#go-nuts")
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 1)
assert.Zero(t, messages[0].Content)
assert.Nil(t, err)
assert.True(t, hasMore)
assert.Len(t, messages[0].Events, 1)
assert.Equal(t, "nick", messages[0].Events[0].Type)
assert.NotZero(t, messages[0].Events[0].Time)
user.LogEvent("irc.freenode.net", "quit", []string{"rob", "bored"}, "#go-nuts")
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 1)
assert.Zero(t, messages[0].Content)
assert.Nil(t, err)
assert.True(t, hasMore)
assert.Len(t, messages[0].Events, 1)
assert.Equal(t, "quit", messages[0].Events[0].Type)
assert.Equal(t, []string{"rob", "bored"}, messages[0].Events[0].Params)
assert.NotZero(t, messages[0].Events[0].Time)
db.Close()
}