Skip to content

Commit

Permalink
feat: implement app update checker and UI notification
Browse files Browse the repository at this point in the history
  • Loading branch information
abhinavxd committed Feb 24, 2025
1 parent e8f3f24 commit fcbd16f
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 5 deletions.
2 changes: 1 addition & 1 deletion cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ func initEmailInbox(inboxRecord imodels.Inbox, store inbox.MessageStore) (inbox.
return nil, fmt.Errorf("initializing `%s` inbox: `%s` error : %w", inboxRecord.Channel, inboxRecord.Name, err)
}

log.Printf("`%s` inbox successfully initialized. %d SMTP servers. %d IMAP clients.", inboxRecord.Name, len(config.SMTP), len(config.IMAP))
log.Printf("`%s` inbox successfully initialized", inboxRecord.Name)

return inbox, nil
}
Expand Down
11 changes: 11 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import (
"log"
"os"
"os/signal"
"sync"
"sync/atomic"
"syscall"
"time"

"github.com/abhinavxd/libredesk/internal/ai"
auth_ "github.com/abhinavxd/libredesk/internal/auth"
Expand Down Expand Up @@ -83,6 +85,10 @@ type App struct {
ai *ai.Manager
search *search.Manager
notifier *notifier.Service

// Global state that stores data on an available app update.
update *AppUpdate
sync.Mutex
}

func main() {
Expand Down Expand Up @@ -242,6 +248,11 @@ func main() {
}
}()

// Start the app update checker.
if ko.Bool("app.check_updates") {
go checkUpdates(versionString, time.Hour*24, app)
}

// Wait for shutdown signal.
<-ctx.Done()
colorlog.Red("Shutting down HTTP server...")
Expand Down
10 changes: 9 additions & 1 deletion cmd/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,15 @@ func handleGetGeneralSettings(r *fastglue.Request) error {
if err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(out)
// Unmarshal to add the app.update to the settings.
var settings map[string]interface{}
if err := json.Unmarshal(out, &settings); err != nil {
app.lo.Error("error unmarshalling settings", "err", err)
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, "Error fetching settings", nil))
}
// Add the app.update to the settings, adding `app` prefix to the key to match the settings structure in db.
settings["app.update"] = app.update
return r.SendEnvelope(settings)
}

// handleUpdateGeneralSettings updates general settings.
Expand Down
98 changes: 98 additions & 0 deletions cmd/updates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright Kailash Nadh (https://github.com/knadh/listmonk)
// SPDX-License-Identifier: AGPL-3.0
// Adapted from listmonk for Libredesk.

package main

import (
"encoding/json"
"io"
"net/http"
"regexp"
"time"

"golang.org/x/mod/semver"
)

const updateCheckURL = "https://updates.libredesk.io/updates.json"

type AppUpdate struct {
Update struct {
ReleaseVersion string `json:"release_version"`
ReleaseDate string `json:"release_date"`
URL string `json:"url"`
Description string `json:"description"`

// This is computed and set locally based on the local version.
IsNew bool `json:"is_new"`
} `json:"update"`
Messages []struct {
Date string `json:"date"`
Title string `json:"title"`
Description string `json:"description"`
URL string `json:"url"`
Priority string `json:"priority"`
} `json:"messages"`
}

var reSemver = regexp.MustCompile(`-(.*)`)

// checkUpdates is a blocking function that checks for updates to the app
// at the given intervals. On detecting a new update (new semver), it
// sets the global update status that renders a prompt on the UI.
func checkUpdates(curVersion string, interval time.Duration, app *App) {
// Strip -* suffix.
curVersion = reSemver.ReplaceAllString(curVersion, "")

fnCheck := func() {
resp, err := http.Get(updateCheckURL)
if err != nil {
app.lo.Error("error checking for app updates", "err", err)
return
}

if resp.StatusCode != 200 {
app.lo.Error("non-ok status code checking for app updates", "status", resp.StatusCode)
return
}

b, err := io.ReadAll(resp.Body)
if err != nil {
app.lo.Error("error reading response body", "err", err)
return
}
resp.Body.Close()

var out AppUpdate
if err := json.Unmarshal(b, &out); err != nil {
app.lo.Error("error unmarshalling response body", "err", err)
return
}

// There is an update. Set it on the global app state.
if semver.IsValid(out.Update.ReleaseVersion) {
v := reSemver.ReplaceAllString(out.Update.ReleaseVersion, "")
if semver.Compare(v, curVersion) > 0 {
out.Update.IsNew = true
app.lo.Info("new update available", "version", out.Update.ReleaseVersion)
}
}

app.Lock()
app.update = &out
app.Unlock()
}

// Give a 15 minute buffer after app start in case the admin wants to disable
// update checks entirely and not make a request to upstream.
time.Sleep(time.Minute * 15)
fnCheck()

// Thereafter, check every $interval.
ticker := time.NewTicker(interval)
defer ticker.Stop()

for range ticker.C {
fnCheck()
}
}
3 changes: 2 additions & 1 deletion config.sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
[app]
log_level = "debug"
env = "dev"
check_updates = true

# HTTP server.
[app.server]
Expand Down Expand Up @@ -45,7 +46,7 @@ max_lifetime = "300s"

# Redis.
[redis]
# If using docker compose, use the service name as the host. e.g. redis
# If using docker compose, use the service name as the host. e.g. redis:6379
address = "127.0.0.1:6379"
password = ""
db = 0
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
@delete-view="deleteView"
>
<div class="flex flex-col h-screen">
<AppUpdate />
<PageHeader />
<RouterView class="flex-grow" />
</div>
Expand Down Expand Up @@ -77,6 +78,7 @@ import { useMacroStore } from '@/stores/macro'
import { useTagStore } from '@/stores/tag'
import PageHeader from './components/layout/PageHeader.vue'
import ViewForm from '@/features/view/ViewForm.vue'
import AppUpdate from '@/components/update/AppUpdate.vue'
import api from '@/api'
import { toast as sooner } from 'vue-sonner'
import Sidebar from '@/components/sidebar/Sidebar.vue'
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/components/update/AppUpdate.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<template>
<div
v-if="appSettingsStore.settings['app.update']?.update?.is_new"
class="p-2 mb-2 border-b bg-secondary text-secondary-foreground"
>
A new update is available:
{{ appSettingsStore.settings['app.update'].update.release_version }} ({{
appSettingsStore.settings['app.update'].update.release_date
}})
<a
:href="appSettingsStore.settings['app.update'].update.url"
target="_blank"
nofollow
noreferrer
class="underline ml-2"
>
View details
</a>
</div>
</template>

<script setup>
import { useAppSettingsStore } from '@/stores/appSettings'
const appSettingsStore = useAppSettingsStore()
</script>
7 changes: 6 additions & 1 deletion frontend/src/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createI18n } from 'vue-i18n'
import { useAppSettingsStore } from './stores/appSettings'
import router from './router'
import mitt from 'mitt'
import api from './api'
Expand Down Expand Up @@ -38,12 +39,16 @@ async function initApp () {
const i18n = createI18n(i18nConfig)
const app = createApp(Root)
const pinia = createPinia()
app.use(pinia)

// Store app settings in Pinia
const settingsStore = useAppSettingsStore()
settingsStore.setSettings(settings)

// Add emitter to global properties.
app.config.globalProperties.emitter = emitter

app.use(router)
app.use(pinia)
app.use(i18n)
app.mount('#app')
}
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/stores/appSettings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineStore } from 'pinia'

export const useAppSettingsStore = defineStore('settings', {
state: () => ({
settings: {}
}),
actions: {
setSettings (newSettings) {
this.settings = newSettings
}
}
})
2 changes: 1 addition & 1 deletion internal/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ func ChangeSystemUserPassword(ctx context.Context, db *sqlx.DB) error {
if err := updateSystemUserPassword(db, hashedPassword); err != nil {
return fmt.Errorf("error updating system user password: %v", err)
}
fmt.Println("password updated successfully.")
fmt.Println("password updated successfully. Login with email 'System' and the new password.")
return nil
}

Expand Down

0 comments on commit fcbd16f

Please sign in to comment.