`Manager.RemoveUser` deletes from `m.instances` without holding `m.mutex`:
delete(m.instances, pluginConf.ID)
Every other access to `m.instances` is synchronized — `Instance` reads it
under `m.mutex.RLock()`, and the writes in `InitializeForUserID` /
`initializeSingleUserPlugin` happen under `m.mutex.Lock()`. The adjacent
`inst.Disable()` call in this same loop is even wrapped in Lock/Unlock, so
only the map delete is left unguarded.
`RemoveUser` is registered as the `OnUserDeleted` callback (router.go), fired
when an admin deletes a user, while any authenticated request to the plugin
API (`GET /plugin`, `/plugin/:id/*`) concurrently reads `m.instances` via
`Instance`. A `RLock` reader is not protected against a writer that never
takes the mutex, so this races and triggers Go's runtime-fatal
"concurrent map read and map write", crashing the whole server (a fatal that
gin.Recovery cannot recover). Take the write lock around the delete, matching
every other access.
Add a final safety net in redirectToChannel.SendMessage that refuses a
message when ApplicationID == 0, so it can never be stored as an orphaned
message (application_id = 0, not shown, not deletable). Requested in review.
Cover the previously untested error branches around internal-application
back-fill (create/update failures) so patch coverage no longer regresses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a plugin gains the Messenger capability after it was first
initialized for a user, its plugin conf already exists without an
associated internal application (ApplicationID == 0). Messages sent by
the plugin were then stored with application_id = 0, orphaning them:
they disappeared on reload and could not be deleted (#653).
Back-fill the internal application during initialization when a
Messenger plugin has none yet, mirroring the creation already done for
plugins that support Messenger from the start.
Fixes#653
Co-Authored-By: Claude <noreply@anthropic.com>
If a plugin was built for a different gotify version, then plugin.Open
may throw an unrecoverable error. This log statement should help
debugging which plugin is causing the error.
See #510