diff --git a/client/src/components/files.vue b/client/src/components/files.vue index bfae9cf3..cc4c7b98 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 e6c991bd..7b02b83f 100644 --- a/client/src/components/settings.vue +++ b/client/src/components/settings.vue @@ -44,6 +44,20 @@ +
  • + File transfer + +
  • +
  • + Non-admin file transfer + +
  • {{ $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 2122e789..282065a9 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 d12046a0..d73c1854 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 15033db3..dc4b5eed 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 feb60738..b71d73d3 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 5eb2c1f7..c4122aff 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 3d99fad3..f68a4681 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 8c5e12ff..4b6b047c 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 2364360f..27a83d6d 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 cda7ec83..fb1d1907 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 004a4b46..985f0278 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 1c2719cc..3eba130d 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 f4617658..022067a2 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)