Log direct messages and keep track of open direct message tabs

This commit is contained in:
Ken-Håvard Lieng 2020-05-06 04:19:55 +02:00
parent e97bb519ed
commit 8305dd561d
17 changed files with 655 additions and 304 deletions

File diff suppressed because one or more lines are too long

View File

@ -29,6 +29,13 @@ function loadState({ store }, env) {
});
}
if (env.openDMs) {
store.dispatch({
type: 'PRIVATE_CHATS',
privateChats: env.openDMs
});
}
if (env.users) {
store.dispatch({
type: socketActions.USERS,

View File

@ -1,7 +1,7 @@
import Cookie from 'js-cookie';
import debounce from 'lodash/debounce';
import { getSelectedTab } from 'state/tab';
import { isChannel, stringifyTab } from 'utils';
import { stringifyTab } from 'utils';
import { observe } from 'utils/observe';
const saveTab = debounce(
@ -11,7 +11,7 @@ const saveTab = debounce(
export default function storage({ store }) {
observe(store, getSelectedTab, tab => {
if (isChannel(tab) || (tab.server && !tab.name)) {
if (tab.server) {
saveTab(tab);
}
});

View File

@ -26,6 +26,7 @@ export const CLOSE_MODAL = 'CLOSE_MODAL';
export const CLOSE_PRIVATE_CHAT = 'CLOSE_PRIVATE_CHAT';
export const OPEN_PRIVATE_CHAT = 'OPEN_PRIVATE_CHAT';
export const PRIVATE_CHATS = 'PRIVATE_CHATS';
export const SEARCH_MESSAGES = 'SEARCH_MESSAGES';
export const TOGGLE_SEARCH = 'TOGGLE_SEARCH';

View File

@ -30,6 +30,16 @@ export default createReducer(
}
},
[actions.PRIVATE_CHATS](state, { privateChats }) {
privateChats.forEach(({ server, name }) => {
if (!state[server]) {
state[server] = [];
}
state[server].push(name);
});
},
[actions.socket.PM](state, action) {
if (action.from.indexOf('.') === -1) {
open(state, action.server, action.from);
@ -46,7 +56,11 @@ export function openPrivateChat(server, nick) {
return {
type: actions.OPEN_PRIVATE_CHAT,
server,
nick
nick,
socket: {
type: 'open_dm',
data: { server, name: nick }
}
};
}
@ -55,7 +69,11 @@ export function closePrivateChat(server, nick) {
dispatch({
type: actions.CLOSE_PRIVATE_CHAT,
server,
nick
nick,
socket: {
type: 'close_dm',
data: { server, name: nick }
}
});
dispatch(updateSelection());
};

View File

@ -31,6 +31,7 @@ type indexData struct {
Defaults *config.Defaults
Servers []Server
Channels []*storage.Channel
OpenDMs []storage.Tab
HexIP bool
Version dispatchVersion
@ -43,7 +44,7 @@ type indexData struct {
Messages *Messages
}
func (d *Dispatch) getIndexData(r *http.Request, path string, state *State) *indexData {
func (d *Dispatch) getIndexData(r *http.Request, state *State) *indexData {
cfg := d.Config()
data := indexData{
@ -98,35 +99,37 @@ func (d *Dispatch) getIndexData(r *http.Request, path string, state *State) *ind
}
data.Channels = channels
server, channel := getTabFromPath(path)
if isInChannel(channels, server, channel) {
data.addUsersAndMessages(server, channel, state)
return &data
openDMs, err := state.user.GetOpenDMs()
if err != nil {
return nil
}
data.OpenDMs = openDMs
server, channel = parseTabCookie(r, path)
if isInChannel(channels, server, channel) {
data.addUsersAndMessages(server, channel, state)
tab, err := tabFromRequest(r)
if err == nil && hasTab(channels, openDMs, tab.Server, tab.Name) {
data.addUsersAndMessages(tab.Server, tab.Name, state)
}
return &data
}
func (d *indexData) addUsersAndMessages(server, channel string, state *State) {
users := channelStore.GetUsers(server, channel)
func (d *indexData) addUsersAndMessages(server, name string, state *State) {
if isChannel(name) {
users := channelStore.GetUsers(server, name)
if len(users) > 0 {
d.Users = &Userlist{
Server: server,
Channel: channel,
Channel: name,
Users: users,
}
}
}
messages, hasMore, err := state.user.GetLastMessages(server, channel, 50)
messages, hasMore, err := state.user.GetLastMessages(server, name, 50)
if err == nil && len(messages) > 0 {
m := Messages{
Server: server,
To: channel,
To: name,
Messages: messages,
}
@ -138,10 +141,16 @@ func (d *indexData) addUsersAndMessages(server, channel string, state *State) {
}
}
func isInChannel(channels []*storage.Channel, server, channel string) bool {
if channel != "" {
func hasTab(channels []*storage.Channel, openDMs []storage.Tab, server, name string) bool {
if name != "" {
for _, ch := range channels {
if server == ch.Server && channel == ch.Name {
if server == ch.Server && name == ch.Name {
return true
}
}
for _, tab := range openDMs {
if server == tab.Server && name == tab.Name {
return true
}
}
@ -149,30 +158,52 @@ func isInChannel(channels []*storage.Channel, server, channel string) bool {
return false
}
func getTabFromPath(rawPath string) (string, string) {
path := strings.Split(strings.Trim(rawPath, "/"), "/")
if len(path) >= 2 {
name, err := url.PathUnescape(path[len(path)-1])
if err == nil && isChannel(name) {
return path[len(path)-2], name
}
}
return "", ""
func tabFromRequest(r *http.Request) (Tab, error) {
tab := Tab{}
var path string
if strings.HasPrefix(r.URL.Path, "/ws") {
path = r.URL.EscapedPath()[3:]
} else {
referer, err := url.Parse(r.Referer())
if err != nil {
return tab, err
}
path = referer.EscapedPath()
}
func parseTabCookie(r *http.Request, path string) (string, string) {
if path == "/" {
cookie, err := r.Cookie("tab")
if err == nil {
v, err := url.PathUnescape(cookie.Value)
if err == nil {
tab := strings.SplitN(v, ";", 2)
if err != nil {
return tab, err
}
if len(tab) == 2 && isChannel(tab[1]) {
return tab[0], tab[1]
v, err := url.PathUnescape(cookie.Value)
if err != nil {
return tab, err
}
parts := strings.SplitN(v, ";", 2)
if len(parts) == 2 {
tab.Server = parts[0]
tab.Name = parts[1]
}
} else {
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) > 0 && len(parts) < 3 {
if len(parts) == 2 {
name, err := url.PathUnescape(parts[1])
if err != nil {
return tab, err
}
tab.Name = name
}
tab.Server = parts[0]
}
}
}
}
return "", ""
return tab, nil
}

View File

@ -104,6 +104,29 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchServer(in *jlexer.Lexer, out
}
in.Delim(']')
}
case "openDMs":
if in.IsNull() {
in.Skip()
out.OpenDMs = nil
} else {
in.Delim('[')
if out.OpenDMs == nil {
if !in.IsDelim(']') {
out.OpenDMs = make([]storage.Tab, 0, 2)
} else {
out.OpenDMs = []storage.Tab{}
}
} else {
out.OpenDMs = (out.OpenDMs)[:0]
}
for !in.IsDelim(']') {
var v3 storage.Tab
easyjson7e607aefDecodeGithubComKhliengDispatchStorage1(in, &v3)
out.OpenDMs = append(out.OpenDMs, v3)
in.WantComma()
}
in.Delim(']')
}
case "hexIP":
out.HexIP = bool(in.Bool())
case "version":
@ -176,11 +199,11 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchServer(out *jwriter.Writer, i
}
{
out.RawByte('[')
for v3, v4 := range in.Servers {
if v3 > 0 {
for v4, v5 := range in.Servers {
if v4 > 0 {
out.RawByte(',')
}
out.Raw((v4).MarshalJSON())
out.Raw((v5).MarshalJSON())
}
out.RawByte(']')
}
@ -195,19 +218,38 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchServer(out *jwriter.Writer, i
}
{
out.RawByte('[')
for v5, v6 := range in.Channels {
if v5 > 0 {
for v6, v7 := range in.Channels {
if v6 > 0 {
out.RawByte(',')
}
if v6 == nil {
if v7 == nil {
out.RawString("null")
} else {
easyjson7e607aefEncodeGithubComKhliengDispatchStorage(out, *v6)
easyjson7e607aefEncodeGithubComKhliengDispatchStorage(out, *v7)
}
}
out.RawByte(']')
}
}
if len(in.OpenDMs) != 0 {
const prefix string = ",\"openDMs\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
{
out.RawByte('[')
for v8, v9 := range in.OpenDMs {
if v8 > 0 {
out.RawByte(',')
}
easyjson7e607aefEncodeGithubComKhliengDispatchStorage1(out, v9)
}
out.RawByte(']')
}
}
if in.HexIP {
const prefix string = ",\"hexIP\":"
if first {
@ -284,6 +326,61 @@ func (v *indexData) UnmarshalJSON(data []byte) error {
func (v *indexData) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson7e607aefDecodeGithubComKhliengDispatchServer(l, v)
}
func easyjson7e607aefDecodeGithubComKhliengDispatchStorage1(in *jlexer.Lexer, out *storage.Tab) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeFieldName(false)
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "server":
out.Server = string(in.String())
case "name":
out.Name = string(in.String())
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjson7e607aefEncodeGithubComKhliengDispatchStorage1(out *jwriter.Writer, in storage.Tab) {
out.RawByte('{')
first := true
_ = first
if in.Server != "" {
const prefix string = ",\"server\":"
first = false
out.RawString(prefix[1:])
out.String(string(in.Server))
}
if in.Name != "" {
const prefix string = ",\"name\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Name))
}
out.RawByte('}')
}
func easyjson7e607aefDecodeGithubComKhliengDispatchStorage(in *jlexer.Lexer, out *storage.Channel) {
isTopLevel := in.IsStart()
if in.IsNull() {
@ -392,9 +489,9 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchConfig(in *jlexer.Lexer, out
out.Channels = (out.Channels)[:0]
}
for !in.IsDelim(']') {
var v7 string
v7 = string(in.String())
out.Channels = append(out.Channels, v7)
var v10 string
v10 = string(in.String())
out.Channels = append(out.Channels, v10)
in.WantComma()
}
in.Delim(']')
@ -457,11 +554,11 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchConfig(out *jwriter.Writer, i
}
{
out.RawByte('[')
for v8, v9 := range in.Channels {
if v8 > 0 {
for v11, v12 := range in.Channels {
if v11 > 0 {
out.RawByte(',')
}
out.String(string(v9))
out.String(string(v12))
}
out.RawByte(']')
}
@ -640,9 +737,9 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchServer2(in *jlexer.Lexer, out
out.Channels = (out.Channels)[:0]
}
for !in.IsDelim(']') {
var v10 string
v10 = string(in.String())
out.Channels = append(out.Channels, v10)
var v13 string
v13 = string(in.String())
out.Channels = append(out.Channels, v13)
in.WantComma()
}
in.Delim(']')
@ -705,11 +802,11 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchServer2(out *jwriter.Writer,
}
{
out.RawByte('[')
for v11, v12 := range in.Channels {
if v11 > 0 {
for v14, v15 := range in.Channels {
if v14 > 0 {
out.RawByte(',')
}
out.String(string(v12))
out.String(string(v15))
}
out.RawByte(']')
}

View File

@ -1,47 +1,60 @@
package server
import (
"net/http"
"net/url"
"testing"
"github.com/khlieng/dispatch/storage"
"github.com/stretchr/testify/assert"
)
func TestGetTabFromPath(t *testing.T) {
cases := []struct {
input string
expectedServer string
expectedChannel string
input *http.Request
expectedTab Tab
}{
{
"/chat.freenode.net/%23r%2Fstuff%2F/",
"chat.freenode.net",
"#r/stuff/",
&http.Request{
URL: &url.URL{Path: "/init"},
Header: http.Header{"Referer": []string{"/chat.freenode.net/%23r%2Fstuff%2F"}},
},
Tab{storage.Tab{Server: "chat.freenode.net", Name: "#r/stuff/"}},
}, {
"/chat.freenode.net/%23r%2Fstuff%2F",
"chat.freenode.net",
"#r/stuff/",
&http.Request{
URL: &url.URL{Path: "/init"},
Header: http.Header{"Referer": []string{"/chat.freenode.net/%23r%2Fstuff"}},
},
Tab{storage.Tab{Server: "chat.freenode.net", Name: "#r/stuff"}},
}, {
"/chat.freenode.net/%23r%2Fstuff",
"chat.freenode.net",
"#r/stuff",
&http.Request{
URL: &url.URL{Path: "/init"},
Header: http.Header{"Referer": []string{"/chat.freenode.net/%23stuff"}},
},
Tab{storage.Tab{Server: "chat.freenode.net", Name: "#stuff"}},
}, {
"/chat.freenode.net/%23stuff",
"chat.freenode.net",
"#stuff",
&http.Request{
URL: &url.URL{Path: "/init"},
Header: http.Header{"Referer": []string{"/chat.freenode.net/stuff"}},
},
Tab{storage.Tab{Server: "chat.freenode.net", Name: "stuff"}},
}, {
"/chat.freenode.net/%23stuff/cake",
"",
"",
&http.Request{
URL: &url.URL{Path: "/init"},
Header: http.Header{"Referer": []string{"/data/chat.freenode.net/%23apples"}},
},
Tab{},
}, {
"/data/chat.freenode.net/%23apples",
"chat.freenode.net",
"#apples",
&http.Request{
URL: &url.URL{Path: "/ws/chat.freenode.net"},
},
Tab{storage.Tab{Server: "chat.freenode.net"}},
},
}
for _, tc := range cases {
server, channel := getTabFromPath(tc.input)
assert.Equal(t, tc.expectedServer, server)
assert.Equal(t, tc.expectedChannel, channel)
tab, err := tabFromRequest(tc.input)
assert.Nil(t, err)
assert.Equal(t, tc.expectedTab, tab)
}
}

View File

@ -169,17 +169,21 @@ func (i *ircHandler) message(msg *irc.Message) {
From: msg.Nick,
Content: msg.LastParam(),
}
target := msg.Params[0]
if msg.Params[0] == i.client.GetNick() {
if target == i.client.GetNick() {
i.state.sendJSON("pm", message)
i.state.user.AddOpenDM(i.client.Host, message.From)
target = message.From
} else {
message.To = msg.Params[0]
message.To = target
i.state.sendJSON("message", message)
}
if msg.Params[0] != "*" {
if target != "*" {
go i.state.user.LogMessage(message.ID,
i.client.Host, msg.Nick, msg.Params[0], msg.LastParam())
i.client.Host, msg.Nick, target, msg.LastParam())
}
}

View File

@ -221,3 +221,7 @@ type ChannelForward struct {
Old string
New string
}
type Tab struct {
storage.Tab
}

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,6 @@ package server
import (
"log"
"net/http"
"net/url"
"strings"
"sync"
@ -163,14 +162,8 @@ func (d *Dispatch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
if r.URL.Path == "/init" {
referer, err := url.Parse(r.Header.Get("Referer"))
if err != nil {
fail(w, http.StatusInternalServerError)
return
}
state := d.handleAuth(w, r, true, true)
data := d.getIndexData(r, referer.EscapedPath(), state)
data := d.getIndexData(r, state)
writeJSON(w, r, data)
} else if strings.HasPrefix(r.URL.Path, "/ws") {

View File

@ -68,17 +68,15 @@ func (h *wsHandler) init(r *http.Request) {
h.state.numIRC(), "IRC connections |",
h.state.numWS(), "WebSocket connections")
tab, err := tabFromRequest(r)
channels, err := h.state.user.GetChannels()
if err != nil {
log.Println(err)
}
path := r.URL.EscapedPath()
pathServer, pathChannel := getTabFromPath(path)
cookieServer, cookieChannel := parseTabCookie(r, path[3:])
for _, channel := range channels {
if (channel.Server == pathServer && channel.Name == pathChannel) ||
(channel.Server == cookieServer && channel.Name == cookieChannel) {
if channel.Server == tab.Server && channel.Name == tab.Name {
// Userlist and messages for this channel gets embedded in the index page
continue
}
@ -91,6 +89,19 @@ func (h *wsHandler) init(r *http.Request) {
h.state.sendLastMessages(channel.Server, channel.Name, 50)
}
openDMs, err := h.state.user.GetOpenDMs()
if err != nil {
log.Println(err)
}
for _, openDM := range openDMs {
if openDM.Server == tab.Server && openDM.Name == tab.Name {
continue
}
h.state.sendLastMessages(openDM.Server, openDM.Name, 50)
}
}
func (h *wsHandler) connect(b []byte) {
@ -306,6 +317,21 @@ func (h *wsHandler) channelSearch(b []byte) {
}
}
func (h *wsHandler) openDM(b []byte) {
var data Tab
data.UnmarshalJSON(b)
h.state.sendLastMessages(data.Server, data.Name, 50)
h.state.user.AddOpenDM(data.Server, data.Name)
}
func (h *wsHandler) closeDM(b []byte) {
var data Tab
data.UnmarshalJSON(b)
h.state.user.RemoveOpenDM(data.Server, data.Name)
}
func (h *wsHandler) initHandlers() {
h.handlers = map[string]func([]byte){
"connect": h.connect,
@ -327,6 +353,8 @@ func (h *wsHandler) initHandlers() {
"set_server_name": h.setServerName,
"settings_set": h.setSettings,
"channel_search": h.channelSearch,
"open_dm": h.openDM,
"close_dm": h.closeDM,
}
}

View File

@ -15,6 +15,7 @@ var (
bucketUsers = []byte("Users")
bucketServers = []byte("Servers")
bucketChannels = []byte("Channels")
bucketOpenDMs = []byte("OpenDMs")
bucketMessages = []byte("Messages")
bucketSessions = []byte("Sessions")
)
@ -34,6 +35,7 @@ func New(path string) (*BoltStore, error) {
tx.CreateBucketIfNotExists(bucketUsers)
tx.CreateBucketIfNotExists(bucketServers)
tx.CreateBucketIfNotExists(bucketChannels)
tx.CreateBucketIfNotExists(bucketOpenDMs)
tx.CreateBucketIfNotExists(bucketMessages)
tx.CreateBucketIfNotExists(bucketSessions)
return nil
@ -168,6 +170,13 @@ func (s *BoltStore) RemoveServer(user *storage.User, address string) error {
b.Delete(k)
}
b = tx.Bucket(bucketOpenDMs)
c = b.Cursor()
for k, _ := c.Seek(serverID); bytes.HasPrefix(k, serverID); k, _ = c.Next() {
b.Delete(k)
}
return nil
})
}
@ -246,6 +255,42 @@ func (s *BoltStore) RemoveChannel(user *storage.User, server, channel string) er
})
}
func (s *BoltStore) GetOpenDMs(user *storage.User) ([]storage.Tab, error) {
var openDMs []storage.Tab
s.db.View(func(tx *bolt.Tx) error {
c := tx.Bucket(bucketOpenDMs).Cursor()
for k, _ := c.Seek(user.IDBytes); bytes.HasPrefix(k, user.IDBytes); k, _ = c.Next() {
tab := bytes.Split(k[8:], []byte{0})
openDMs = append(openDMs, storage.Tab{
Server: string(tab[0]),
Name: string(tab[1]),
})
}
return nil
})
return openDMs, nil
}
func (s *BoltStore) AddOpenDM(user *storage.User, server, nick string) error {
return s.db.Batch(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketOpenDMs)
return b.Put(channelID(user, server, nick), nil)
})
}
func (s *BoltStore) RemoveOpenDM(user *storage.User, server, nick string) error {
return s.db.Batch(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketOpenDMs)
return b.Delete(channelID(user, server, nick))
})
}
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))

View File

@ -38,6 +38,10 @@ type Store interface {
GetChannels(user *User) ([]*Channel, error)
AddChannel(user *User, channel *Channel) error
RemoveChannel(user *User, server, channel string) error
GetOpenDMs(user *User) ([]Tab, error)
AddOpenDM(user *User, server, nick string) error
RemoveOpenDM(user *User, server, nick string) error
}
type SessionStore interface {

View File

@ -190,6 +190,23 @@ func (u *User) RemoveChannel(server, channel string) error {
return u.store.RemoveChannel(u, server, channel)
}
type Tab struct {
Server string
Name string
}
func (u *User) GetOpenDMs() ([]Tab, error) {
return u.store.GetOpenDMs(u)
}
func (u *User) AddOpenDM(server, nick string) error {
return u.store.AddOpenDM(u, server, nick)
}
func (u *User) RemoveOpenDM(server, nick string) error {
return u.store.RemoveOpenDM(u, server, nick)
}
type Message struct {
ID string `json:"-" bleve:"-"`
Server string `json:"-" bleve:"server"`

View File

@ -80,6 +80,16 @@ func TestUser(t *testing.T) {
channels, err = user.GetChannels()
assert.Len(t, channels, 0)
user.AddOpenDM(srv.Host, "cake")
openDMs, err := user.GetOpenDMs()
assert.Nil(t, err)
assert.Len(t, openDMs, 1)
err = user.RemoveOpenDM(srv.Host, "cake")
assert.Nil(t, err)
openDMs, err = user.GetOpenDMs()
assert.Nil(t, err)
assert.Len(t, openDMs, 0)
settings := user.GetClientSettings()
assert.NotNil(t, settings)
assert.Equal(t, storage.DefaultClientSettings(), settings)