main: add support for logging out

This commit is contained in:
Tulir Asokan 2024-12-03 23:36:03 +02:00
parent 74439be24c
commit 529ffda4ed
12 changed files with 130 additions and 5 deletions

View file

@ -148,9 +148,9 @@ func main() {
URL: "/", URL: "/",
}) })
gmx.Client.EventHandler = hicli.JSONEventHandler(func(command *hicli.JSONCommand) { gmx.SubscribeEvents(nil, func(command *hicli.JSONCommand) {
app.EmitEvent("hicli_event", command) app.EmitEvent("hicli_event", command)
}).HandleEvent })
err = app.Run() err = app.Run()
if err != nil { if err != nil {

View file

@ -178,6 +178,7 @@ func (gmx *Gomuks) StartClient() {
[]byte("meow"), []byte("meow"),
hicli.JSONEventHandler(gmx.OnEvent).HandleEvent, hicli.JSONEventHandler(gmx.OnEvent).HandleEvent,
) )
gmx.Client.LogoutFunc = gmx.Logout
httpClient := gmx.Client.Client.Client httpClient := gmx.Client.Client.Client
httpClient.Transport.(*http.Transport).ForceAttemptHTTP2 = false httpClient.Transport.(*http.Transport).ForceAttemptHTTP2 = false
if !gmx.Config.Matrix.DisableHTTP2 { if !gmx.Config.Matrix.DisableHTTP2 {
@ -246,7 +247,9 @@ func (gmx *Gomuks) SubscribeEvents(closeForRestart WebsocketCloseFunc, cb func(c
gmx.nextListenerID++ gmx.nextListenerID++
id := gmx.nextListenerID id := gmx.nextListenerID
gmx.eventListeners[id] = cb gmx.eventListeners[id] = cb
if closeForRestart != nil {
gmx.websocketClosers[id] = closeForRestart gmx.websocketClosers[id] = closeForRestart
}
return func() { return func() {
gmx.eventListenersLock.Lock() gmx.eventListenersLock.Lock()
defer gmx.eventListenersLock.Unlock() defer gmx.eventListenersLock.Unlock()

64
pkg/gomuks/logout.go Normal file
View file

@ -0,0 +1,64 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package gomuks
import (
"context"
"errors"
"os"
"path/filepath"
"github.com/rs/zerolog"
"maunium.net/go/mautrix"
)
func (gmx *Gomuks) Logout(ctx context.Context) error {
log := zerolog.Ctx(ctx)
log.Info().Msg("Stopping client and logging out")
gmx.Client.Stop()
_, err := gmx.Client.Client.Logout(ctx)
if err != nil && !errors.Is(err, mautrix.MUnknownToken) {
log.Warn().Err(err).Msg("Failed to log out")
return err
}
log.Info().Msg("Logout complete, removing data")
err = os.RemoveAll(gmx.CacheDir)
if err != nil {
log.Err(err).Str("cache_dir", gmx.CacheDir).Msg("Failed to remove cache dir")
}
if gmx.DataDir == gmx.ConfigDir {
err = os.Remove(filepath.Join(gmx.DataDir, "gomuks.db"))
if err != nil && !errors.Is(err, os.ErrNotExist) {
log.Err(err).Str("data_dir", gmx.DataDir).Msg("Failed to remove database")
}
_ = os.Remove(filepath.Join(gmx.DataDir, "gomuks.db-shm"))
_ = os.Remove(filepath.Join(gmx.DataDir, "gomuks.db-wal"))
} else {
err = os.RemoveAll(gmx.DataDir)
if err != nil {
log.Err(err).Str("data_dir", gmx.DataDir).Msg("Failed to remove data dir")
}
}
log.Info().Msg("Re-initializing directories")
gmx.InitDirectories()
log.Info().Msg("Restarting client")
gmx.StartClient()
gmx.Client.EventHandler(gmx.Client.State())
gmx.Client.EventHandler(gmx.Client.SyncStatus.Load())
log.Info().Msg("Client restarted")
return nil
}

View file

@ -32,6 +32,7 @@ import (
type HiClient struct { type HiClient struct {
DB *database.Database DB *database.Database
CryptoDB *dbutil.Database
Account *database.Account Account *database.Account
Client *mautrix.Client Client *mautrix.Client
Crypto *crypto.OlmMachine Crypto *crypto.OlmMachine
@ -50,6 +51,7 @@ type HiClient struct {
lastSync time.Time lastSync time.Time
EventHandler func(evt any) EventHandler func(evt any)
LogoutFunc func(context.Context) error
firstSyncReceived bool firstSyncReceived bool
syncingID int syncingID int
@ -90,6 +92,9 @@ func New(rawDB, cryptoDB *dbutil.Database, log zerolog.Logger, pickleKey []byte,
EventHandler: evtHandler, EventHandler: evtHandler,
} }
if cryptoDB != rawDB {
c.CryptoDB = cryptoDB
}
c.SyncStatus.Store(syncWaiting) c.SyncStatus.Store(syncWaiting)
c.ClientStore = &database.ClientStateStore{Database: db} c.ClientStore = &database.ClientStateStore{Database: db}
c.Client = &mautrix.Client{ c.Client = &mautrix.Client{
@ -267,4 +272,10 @@ func (h *HiClient) Stop() {
if err != nil { if err != nil {
h.Log.Err(err).Msg("Failed to close database cleanly") h.Log.Err(err).Msg("Failed to close database cleanly")
} }
if h.CryptoDB != nil {
err = h.CryptoDB.Close()
if err != nil {
h.Log.Err(err).Msg("Failed to close crypto database cleanly")
}
}
} }

View file

@ -114,6 +114,11 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
return unmarshalAndCall(req.Data, func(params *resolveAliasParams) (*mautrix.RespAliasResolve, error) { return unmarshalAndCall(req.Data, func(params *resolveAliasParams) (*mautrix.RespAliasResolve, error) {
return h.Client.ResolveAlias(ctx, params.Alias) return h.Client.ResolveAlias(ctx, params.Alias)
}) })
case "logout":
if h.LogoutFunc == nil {
return nil, errors.New("logout not supported")
}
return true, h.LogoutFunc(ctx)
case "login": case "login":
return unmarshalAndCall(req.Data, func(params *loginParams) (bool, error) { return unmarshalAndCall(req.Data, func(params *loginParams) (bool, error) {
return true, h.LoginPassword(ctx, params.HomeserverURL, params.Username, params.Password) return true, h.LoginPassword(ctx, params.HomeserverURL, params.Username, params.Password)

View file

@ -293,4 +293,10 @@ export default class Client {
room.paginating = false room.paginating = false
} }
} }
async logout() {
await this.rpc.logout()
localStorage.clear()
this.store.clear()
}
} }

View file

@ -129,6 +129,10 @@ export default abstract class RPCClient {
}, this.cancelRequest.bind(this, request_id)) }, this.cancelRequest.bind(this, request_id))
} }
logout(): Promise<boolean> {
return this.request("logout", {})
}
sendMessage(params: SendMessageParams): Promise<RawDBEvent> { sendMessage(params: SendMessageParams): Promise<RawDBEvent> {
return this.request("send_message", params) return this.request("send_message", params)
} }

View file

@ -386,4 +386,17 @@ export class StateStore {
} }
return { deletedEvents, deletedState } as const return { deletedEvents, deletedState } as const
} }
clear() {
this.rooms.clear()
this.roomList.emit([])
this.accountData.clear()
this.currentRoomListFilter = ""
this.#frequentlyUsedEmoji = null
this.#emojiPackKeys = null
this.#watchedRoomEmojiPacks = null
this.#personalEmojiPack = null
this.serverPreferenceCache = {}
this.activeRoomID = undefined
}
} }

View file

@ -82,7 +82,7 @@ const BeeperLogin = ({ domain, client }: BeeperLoginProps) => {
onChange={onChangeCode} onChange={onChangeCode}
/>} />}
<button <button
className="beeper-login-button" className="beeper-login-button primary-color-button"
type="submit" type="submit"
>{requestID ? "Submit Code" : "Request Code"}</button> >{requestID ? "Submit Code" : "Request Code"}</button>
{error && <div className="error"> {error && <div className="error">

View file

@ -44,7 +44,7 @@ export const VerificationScreen = ({ client, clientState }: LoginScreenProps) =>
value={recoveryKey} value={recoveryKey}
onChange={evt => setRecoveryKey(evt.target.value)} onChange={evt => setRecoveryKey(evt.target.value)}
/> />
<button className="mx-login-button" type="submit">Verify</button> <button className="mx-login-button primary-color-button" type="submit">Verify</button>
</form> </form>
{error && <div className="error"> {error && <div className="error">
{error} {error}

View file

@ -72,4 +72,14 @@ div.settings-view {
} }
} }
} }
button.logout {
margin-top: 1rem;
padding: .5rem 1rem;
&:hover, &:focus {
background-color: var(--error-color);
color: var(--inverted-text-color);
}
}
} }

View file

@ -259,6 +259,14 @@ const SettingsView = ({ room }: SettingsViewProps) => {
} }
} }
}, [client, room]) }, [client, room])
const onClickLogout = useCallback(() => {
if (window.confirm("Really log out and delete all local data?")) {
client.logout().then(
() => console.info("Successfully logged out"),
err => window.alert(`Failed to log out: ${err}`),
)
}
}, [client])
usePreferences(client.store, room) usePreferences(client.store, room)
const globalServer = client.store.serverPreferenceCache const globalServer = client.store.serverPreferenceCache
const globalLocal = client.store.localPreferenceCache const globalLocal = client.store.localPreferenceCache
@ -293,6 +301,7 @@ const SettingsView = ({ room }: SettingsViewProps) => {
</table> </table>
<CustomCSSInput setPref={setPref} room={room} /> <CustomCSSInput setPref={setPref} room={room} />
<AppliedSettingsView room={room} /> <AppliedSettingsView room={room} />
<button className="logout" onClick={onClickLogout}>Logout</button>
</> </>
} }