diff --git a/client/src/components/files.vue b/client/src/components/files.vue
index bfae9cf..cc4c7b9 100644
--- a/client/src/components/files.vue
+++ b/client/src/components/files.vue
@@ -131,6 +131,26 @@
max-height: 50vh;
overflow-y: scroll;
overflow-x: hidden;
+ scrollbar-width: thin;
+ scrollbar-color: $background-tertiary transparent;
+
+ &::-webkit-scrollbar {
+ width: 8px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background-color: transparent;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background-color: $background-tertiary;
+ border: 2px solid $background-primary;
+ border-radius: 4px;
+ }
+
+ &::-webkit-scrollbar-thumb:hover {
+ background-color: $background-floating;
+ }
}
.transfers > p {
diff --git a/client/src/components/settings.vue b/client/src/components/settings.vue
index e6c991b..7b02b83 100644
--- a/client/src/components/settings.vue
+++ b/client/src/components/settings.vue
@@ -44,6 +44,20 @@
+
{{ $t('setting.broadcast_title') }}
@@ -366,6 +380,22 @@
return this.$accessor.settings.keyboard_layout
}
+ get file_transfer() {
+ return this.$accessor.settings.file_transfer
+ }
+
+ set file_transfer(value: boolean) {
+ this.$accessor.settings.setGlobalFileTransferStatus({ admin: value, unpriv: false })
+ }
+
+ get unpriv_file_transfer() {
+ return this.$accessor.settings.unpriv_file_transfer
+ }
+
+ set unpriv_file_transfer(value: boolean) {
+ this.$accessor.settings.setGlobalFileTransferStatus({ admin: this.file_transfer, unpriv: value })
+ }
+
get broadcast_is_active() {
return this.$accessor.settings.broadcast_is_active
}
diff --git a/client/src/components/side.vue b/client/src/components/side.vue
index 2122e78..282065a 100644
--- a/client/src/components/side.vue
+++ b/client/src/components/side.vue
@@ -6,7 +6,7 @@
{{ $t('side.chat') }}
-
+
{{ $t('side.files') }}
@@ -94,6 +94,20 @@
},
})
export default class extends Vue {
+
+ constructor() {
+ super()
+ if (this.tab === 'files' && (!this.$accessor.settings.file_transfer ||
+ !this.$accessor.user.admin && this.$accessor.settings.unpriv_file_transfer)) {
+ this.change('chat')
+ }
+ }
+
+ get filetransferAllowed() {
+ return this.$accessor.user.admin && this.$accessor.settings.file_transfer ||
+ this.$accessor.settings.unpriv_file_transfer
+ }
+
get tab() {
return this.$accessor.client.tab
}
diff --git a/client/src/neko/events.ts b/client/src/neko/events.ts
index d12046a..d73c185 100644
--- a/client/src/neko/events.ts
+++ b/client/src/neko/events.ts
@@ -39,10 +39,7 @@ export const EVENT = {
EMOTE: 'chat/emote',
},
FILETRANSFER: {
- ENABLE: 'filetransfer/enable',
- DISABLE: 'filetransfer/disable',
- UNPRIVENABLE: 'filetransfer/unprivenable',
- UNPRIVDISABLE: 'filetransfer/unprivdisable',
+ STATUS: 'filetransfer/status',
LIST: 'filetransfer/list',
REFRESH: 'filetransfer/refresh'
},
@@ -102,10 +99,7 @@ export type SignalEvents =
export type ChatEvents = typeof EVENT.CHAT.MESSAGE | typeof EVENT.CHAT.EMOTE
export type FileTransferEvents =
- | typeof EVENT.FILETRANSFER.ENABLE
- | typeof EVENT.FILETRANSFER.DISABLE
- | typeof EVENT.FILETRANSFER.UNPRIVENABLE
- | typeof EVENT.FILETRANSFER.UNPRIVDISABLE
+ | typeof EVENT.FILETRANSFER.STATUS
| typeof EVENT.FILETRANSFER.LIST
| typeof EVENT.FILETRANSFER.REFRESH
diff --git a/client/src/neko/index.ts b/client/src/neko/index.ts
index 15033db..dc4b5ee 100644
--- a/client/src/neko/index.ts
+++ b/client/src/neko/index.ts
@@ -25,6 +25,7 @@ import {
SystemInitPayload,
AdminLockResource,
FileTransferListPayload,
+ FileTransferStatusPayload,
} from './messages'
interface NekoEvents extends BaseEvents {}
@@ -361,8 +362,12 @@ export class NekoClient extends BaseClient implements EventEmitter {
}
/////////////////////////////
- // Chat Events
+ // Filetransfer Events
/////////////////////////////
+ protected [EVENT.FILETRANSFER.STATUS]({ admin, unpriv }: FileTransferStatusPayload) {
+ this.$accessor.settings.setLocalFileTransferStatus({ admin, unpriv })
+ }
+
protected [EVENT.FILETRANSFER.LIST]({ cwd, files }: FileTransferListPayload) {
this.$accessor.files.setCwd(cwd)
this.$accessor.files.setFileList(files)
diff --git a/client/src/neko/messages.ts b/client/src/neko/messages.ts
index feb6073..b71d73d 100644
--- a/client/src/neko/messages.ts
+++ b/client/src/neko/messages.ts
@@ -44,6 +44,7 @@ export type WebSocketPayloads =
| ChatPayload
| ChatSendPayload
| EmojiSendPayload
+ | FileTransferStatusPayload
| ScreenResolutionPayload
| ScreenConfigurationsPayload
| AdminPayload
@@ -198,8 +199,17 @@ export interface EmojiSendPayload {
emote: string
}
-// file transfer
-export interface FileTransferMessage extends WebSocketMessage, FileTransferListPayload {
+// file transfer enabled
+export interface FileTransferStatusMessage extends WebSocketMessage, FileTransferStatusPayload {
+ event: typeof EVENT.FILETRANSFER.STATUS
+}
+export interface FileTransferStatusPayload {
+ admin: boolean,
+ unpriv: boolean
+}
+
+// file transfer list
+export interface FileTransferListMessage extends WebSocketMessage, FileTransferListPayload {
event: FileTransferEvents
}
export interface FileTransferListPayload {
diff --git a/client/src/store/files.ts b/client/src/store/files.ts
index 5eb2c1f..c4122af 100644
--- a/client/src/store/files.ts
+++ b/client/src/store/files.ts
@@ -52,6 +52,15 @@ export const actions = actionTree(
accessor.files._removeTransfer(transfer)
},
+ cancelAllTransfers(store) {
+ for (const t of accessor.files.transfers) {
+ if (t.status !== 'completed') {
+ t.abortController?.abort()
+ }
+ accessor.files.removeTransfer(t)
+ }
+ },
+
refresh(store) {
if (!accessor.connected) {
return
diff --git a/client/src/store/settings.ts b/client/src/store/settings.ts
index 3d99fad..f68a468 100644
--- a/client/src/store/settings.ts
+++ b/client/src/store/settings.ts
@@ -20,6 +20,9 @@ export const state = () => {
keyboard_layouts_list: {} as KeyboardLayouts,
+ file_transfer: false,
+ unpriv_file_transfer: false,
+
broadcast_is_active: false,
broadcast_url: '',
}
@@ -58,6 +61,14 @@ export const mutations = mutationTree(state, {
set('keyboard_layout', value)
},
+ setFileTransfer(state, value: boolean) {
+ state.file_transfer = value
+ },
+
+ setUnprivFileTransfer(state, value: boolean) {
+ state.unpriv_file_transfer = value
+ },
+
setKeyboardLayoutsList(state, value: KeyboardLayouts) {
state.keyboard_layouts_list = value
},
@@ -79,6 +90,23 @@ export const actions = actionTree(
}
},
+ setLocalFileTransferStatus({ getters }, { admin, unpriv }) {
+ accessor.settings.setFileTransfer(admin)
+ accessor.settings.setUnprivFileTransfer(unpriv)
+
+ if (!admin || !accessor.user.admin && !unpriv) {
+ accessor.files.cancelAllTransfers()
+ }
+
+ if (accessor.client.tab === 'files' && !unpriv) {
+ accessor.client.setTab('chat')
+ }
+ },
+
+ setGlobalFileTransferStatus({ getters}, { admin, unpriv }) {
+ $client.sendMessage(EVENT.FILETRANSFER.STATUS, { admin, unpriv })
+ },
+
broadcastStatus({ getters }, { url, isActive }) {
accessor.settings.setBroadcastStatus({ url, isActive })
},
diff --git a/server/internal/types/event/events.go b/server/internal/types/event/events.go
index 8c5e12f..4b6b047 100644
--- a/server/internal/types/event/events.go
+++ b/server/internal/types/event/events.go
@@ -35,12 +35,9 @@ const (
)
const (
- FILETRANSFER_ENABLE = "filetransfer/enable"
- FILETRANSFER_DISABLE = "filetransfer/disable"
- FILETRANSFER_UNPRIVENABLE = "filetransfer/unprivenable"
- FILETRANSFER_UNPRIVDISABLE = "filetransfer/unprivdisable"
- FILETRANSFER_LIST = "filetransfer/list"
- FILETRANSFER_REFRESH = "filetransfer/refresh"
+ FILETRANSFER_STATUS = "filetransfer/status"
+ FILETRANSFER_LIST = "filetransfer/list"
+ FILETRANSFER_REFRESH = "filetransfer/refresh"
)
const (
diff --git a/server/internal/types/message/messages.go b/server/internal/types/message/messages.go
index 2364360..27a83d6 100644
--- a/server/internal/types/message/messages.go
+++ b/server/internal/types/message/messages.go
@@ -108,7 +108,12 @@ type EmoteSend struct {
type FileTransferTarget struct {
Event string `json:"event"`
- ID string `json:"id"`
+}
+
+type FileTransferStatus struct {
+ Event string `json:"event"`
+ Admin bool `json:"admin"`
+ Unpriv bool `json:"unpriv"`
}
type FileList struct {
diff --git a/server/internal/websocket/handler/files.go b/server/internal/websocket/handler/files.go
index cda7ec8..fb1d190 100644
--- a/server/internal/websocket/handler/files.go
+++ b/server/internal/websocket/handler/files.go
@@ -8,6 +8,36 @@ import (
"m1k1o/neko/internal/utils"
)
+func (h *MessageHandler) setFileTransferStatus(session types.Session, payload *message.FileTransferStatus) error {
+ if !session.Admin() {
+ return errors.New(session.Member().Name + " tried to toggle file transfer but they're not admin")
+ }
+ h.state.SetFileTransferState(payload.Admin, payload.Unpriv)
+ err := h.sessions.Broadcast(message.FileTransferStatus{
+ Event: event.FILETRANSFER_STATUS,
+ Admin: payload.Admin,
+ Unpriv: payload.Admin && payload.Unpriv,
+ }, nil)
+ if err != nil {
+ return err
+ }
+
+ files, err := utils.ListFiles(h.state.FileTransferPath())
+ if err != nil {
+ return err
+ }
+ msg := message.FileList{
+ Event: event.FILETRANSFER_LIST,
+ Cwd: h.state.FileTransferPath(),
+ Files: *files,
+ }
+ if payload.Unpriv {
+ return h.sessions.Broadcast(msg, nil)
+ } else {
+ return h.sessions.AdminBroadcast(msg, nil)
+ }
+}
+
func (h *MessageHandler) refresh(session types.Session) error {
if !(h.state.FileTransferEnabled() && session.Admin() || h.state.UnprivFileTransferEnabled()) {
return errors.New(session.Member().Name + " tried to refresh file list when they can't")
diff --git a/server/internal/websocket/handler/handler.go b/server/internal/websocket/handler/handler.go
index 004a4b4..985f027 100644
--- a/server/internal/websocket/handler/handler.go
+++ b/server/internal/websocket/handler/handler.go
@@ -127,6 +127,12 @@ func (h *MessageHandler) Message(id string, raw []byte) error {
}), "%s failed", header.Event)
// File Transfer Events
+ case event.FILETRANSFER_STATUS:
+ payload := &message.FileTransferStatus{}
+ return errors.Wrapf(
+ utils.Unmarshal(payload, raw, func() error {
+ return h.setFileTransferStatus(session, payload)
+ }), "%s failed", header.Event)
case event.FILETRANSFER_REFRESH:
return errors.Wrapf(h.refresh(session), "%s failed", header.Event)
diff --git a/server/internal/websocket/state/state.go b/server/internal/websocket/state/state.go
index 1c2719c..3eba130 100644
--- a/server/internal/websocket/state/state.go
+++ b/server/internal/websocket/state/state.go
@@ -78,6 +78,11 @@ func (s *State) UnprivFileTransferEnabled() bool {
return s.fileTransferUnprivEnabled
}
+func (s *State) SetFileTransferState(admin bool, unpriv bool) {
+ s.fileTransferEnabled = admin
+ s.fileTransferUnprivEnabled = unpriv
+}
+
func (s *State) FileTransferPath() string {
return s.fileTransferPath
}
diff --git a/server/internal/websocket/websocket.go b/server/internal/websocket/websocket.go
index f461765..022067a 100644
--- a/server/internal/websocket/websocket.go
+++ b/server/internal/websocket/websocket.go
@@ -135,7 +135,19 @@ func (ws *WebSocketHandler) Start() {
}
// send file list if necessary
- if session.Admin() && ws.conf.FileTransfer || ws.conf.FileTransfer && ws.conf.UnprivFileTransfer {
+ if session.Admin() && ws.state.FileTransferEnabled() ||
+ ws.state.FileTransferEnabled() && ws.state.UnprivFileTransferEnabled() {
+ err := session.Send(
+ message.FileTransferStatus{
+ Event: event.FILETRANSFER_STATUS,
+ Admin: ws.state.FileTransferEnabled(),
+ Unpriv: ws.state.UnprivFileTransferEnabled(),
+ })
+ if err != nil {
+ ws.logger.Warn().Err(err).Msgf("file transfer status event has failed")
+ return
+ }
+
files, err := utils.ListFiles(ws.conf.FileTransferPath)
if err == nil {
if err := session.Send(
@@ -214,27 +226,25 @@ func (ws *WebSocketHandler) Start() {
})
// watch for file changes
- if ws.conf.FileTransfer {
- watcher, err := fsnotify.NewWatcher()
- if err != nil {
- ws.logger.Err(err).Msg("unable to start file transfer dir watcher")
- return
- }
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ ws.logger.Err(err).Msg("unable to start file transfer dir watcher")
+ return
+ }
- go func() {
- for {
- select {
- case <-watcher.Events:
- ws.sendFileTransferUpdate()
- case err := <-watcher.Errors:
- ws.logger.Err(err).Msg("error in file transfer dir watcher")
- }
+ go func() {
+ for {
+ select {
+ case <-watcher.Events:
+ ws.sendFileTransferUpdate()
+ case err := <-watcher.Errors:
+ ws.logger.Err(err).Msg("error in file transfer dir watcher")
}
- }()
-
- if err := watcher.Add(ws.conf.FileTransferPath); err != nil {
- ws.logger.Err(err).Msg("unable to add file transfer path to watcher")
}
+ }()
+
+ if err := watcher.Add(ws.conf.FileTransferPath); err != nil {
+ ws.logger.Err(err).Msg("unable to add file transfer path to watcher")
}
}
@@ -364,11 +374,11 @@ func (ws *WebSocketHandler) IsAdmin(password string) (bool, error) {
}
func (ws *WebSocketHandler) CanTransferFiles(password string) (bool, error) {
- if !ws.conf.FileTransfer {
+ if !ws.state.FileTransferEnabled() {
return false, nil
}
- if !ws.conf.UnprivFileTransfer {
+ if !ws.state.UnprivFileTransferEnabled() {
return ws.IsAdmin(password)
}
@@ -380,6 +390,10 @@ func (ws *WebSocketHandler) MakeFilePath(filename string) string {
}
func (ws *WebSocketHandler) sendFileTransferUpdate() {
+ if !ws.state.FileTransferEnabled() {
+ return
+ }
+
files, err := utils.ListFiles(ws.conf.FileTransferPath)
if err != nil {
ws.logger.Err(err).Msg("unable to ls file transfer path")
@@ -393,7 +407,7 @@ func (ws *WebSocketHandler) sendFileTransferUpdate() {
}
var broadcastErr error
- if ws.conf.UnprivFileTransfer {
+ if ws.state.UnprivFileTransferEnabled() {
broadcastErr = ws.sessions.Broadcast(message, nil)
} else {
broadcastErr = ws.sessions.AdminBroadcast(message, nil)