package main import ( "fmt" "strings" "time" "github.com/mattermost/mattermost/server/public/model" "git.nakama.town/fmartingr/mattermost-plugin-shelfmark/server/shelfmark" ) // runDownloadJob is the background job that processes pending download tasks. // It is scheduled via cluster.Schedule to run every 5 seconds. func (p *Plugin) runDownloadJob() { config := p.getConfiguration() if err := config.IsValid(); err != nil { // Plugin not configured; nothing to do. return } tasks, err := p.taskStore.ListActiveTasks() if err != nil { p.API.LogError("Failed to list active download tasks", "error", err.Error()) return } if len(tasks) == 0 { return } for _, task := range tasks { p.processTask(task) } } // processTask handles a single download task based on its current status. func (p *Plugin) processTask(task *DownloadTask) { // Check if task is too old and should be considered stale. if time.Since(task.CreatedAt) > taskMaxAge { p.API.LogWarn("Download task expired", "task_id", task.ID, "title", task.BookTitle) p.failTask(task, "Task timed out after "+taskMaxAge.String(), T(task.RequesterLocale, MsgDownloadTimeout, task.BookTitle)) return } switch task.Status { case TaskStatusPending: p.processTaskPending(task) case TaskStatusQueued, TaskStatusDownloading: p.processTaskQueued(task) case TaskStatusComplete: p.processTaskComplete(task) } } // processTaskPending finds releases for the book and queues the best one for download. func (p *Plugin) processTaskPending(task *DownloadTask) { p.API.LogDebug("Processing pending task", "task_id", task.ID, "title", task.BookTitle) releases, err := p.shelfmarkClient.GetReleases(task.BookProvider, task.BookProviderID, task.Language) if err != nil { p.API.LogError("Failed to get releases", "task_id", task.ID, "error", err.Error()) p.failTask(task, "Failed to find downloadable releases: "+err.Error(), T(task.RequesterLocale, MsgNoReleases, task.BookTitle)) return } // Log partial errors from Shelfmark sources if any. if len(releases.Errors) > 0 { p.API.LogWarn("Shelfmark reported partial errors for releases", "task_id", task.ID, "errors", strings.Join(releases.Errors, "; ")) } // Extract the localized title from the releases book metadata if available. if task.Language != "" { localizedTitle := releases.Book.LocalizedTitle(task.Language) if localizedTitle != task.BookTitle { p.API.LogDebug("Found localized title", "task_id", task.ID, "language", task.Language, "localized_title", localizedTitle) task.LocalizedTitle = localizedTitle } } // Use the user-selected release if provided (from RHS panel), otherwise pick the first. var release *shelfmark.Release if task.SelectedRelease != nil { release = task.SelectedRelease } else { if len(releases.Releases) == 0 { p.API.LogWarn("No releases found for book", "task_id", task.ID, "title", task.BookTitle) p.failTask(task, "No downloadable releases found", T(task.RequesterLocale, MsgNoReleasesFound, task.BookTitle)) return } release = &releases.Releases[0] } // Check if this release is already in the Shelfmark queue before trying to queue it. // This avoids a 500 error from Shelfmark when the release is already queued or complete. statusMap, err := p.shelfmarkClient.GetStatus() if err != nil { p.API.LogWarn("Failed to check Shelfmark status before queueing", "task_id", task.ID, "error", err.Error()) // Continue to try queueing anyway. } else if existingStatus, found := shelfmark.GetTaskStatus(statusMap, release.SourceID); found { p.API.LogInfo("Release already exists in Shelfmark queue", "task_id", task.ID, "shelfmark_task_id", release.SourceID, "status", existingStatus) task.ShelfmarkTaskID = release.SourceID switch existingStatus { case "complete": task.Status = TaskStatusComplete if saveErr := p.taskStore.SaveTask(task); saveErr != nil { p.API.LogError("Failed to save task", "task_id", task.ID, "error", saveErr.Error()) } return case "error", "cancelled": // Previous attempt failed; fall through to re-queue below. default: // Already in progress (queued, downloading, locating, resolving). task.Status = TaskStatusQueued if saveErr := p.taskStore.SaveTask(task); saveErr != nil { p.API.LogError("Failed to save task", "task_id", task.ID, "error", saveErr.Error()) } return } } // Queue the download on Shelfmark. queueResp, err := p.shelfmarkClient.QueueDownload(release) if err != nil { p.API.LogError("Failed to queue download", "task_id", task.ID, "error", err.Error()) p.failTask(task, "Failed to queue download: "+err.Error(), T(task.RequesterLocale, MsgDownloadStartFailed, task.BookTitle)) return } if queueResp.Error != "" { p.API.LogError("Shelfmark returned error when queueing download", "task_id", task.ID, "error", queueResp.Error) p.failTask(task, "Shelfmark error: "+queueResp.Error, T(task.RequesterLocale, MsgShelfmarkError, task.BookTitle)) return } task.ShelfmarkTaskID = release.SourceID task.Status = TaskStatusQueued if err := p.taskStore.SaveTask(task); err != nil { p.API.LogError("Failed to save task", "task_id", task.ID, "error", err.Error()) return } p.API.LogInfo("Download queued on Shelfmark", "task_id", task.ID, "shelfmark_task_id", task.ShelfmarkTaskID, "title", task.BookTitle) } // processTaskQueued checks the Shelfmark download status. func (p *Plugin) processTaskQueued(task *DownloadTask) { statusMap, err := p.shelfmarkClient.GetStatus() if err != nil { p.API.LogWarn("Failed to get Shelfmark status", "task_id", task.ID, "error", err.Error()) return // Will retry on next poll. } status, found := shelfmark.GetTaskStatus(statusMap, task.ShelfmarkTaskID) if !found { // Task not found in status; it may have already been processed and removed. if time.Since(task.UpdatedAt) > 5*time.Minute { p.API.LogWarn("Queued task not found in Shelfmark status, marking as failed", "task_id", task.ID) p.failTask(task, "Task disappeared from Shelfmark queue", T(task.RequesterLocale, MsgTrackingLost, task.EffectiveTitle())) } return } switch status { case "complete": p.API.LogInfo("Download complete on Shelfmark", "task_id", task.ID, "title", task.BookTitle) task.Status = TaskStatusComplete if err := p.taskStore.SaveTask(task); err != nil { p.API.LogError("Failed to save task", "task_id", task.ID, "error", err.Error()) } case "error", "cancelled": p.API.LogWarn("Download failed on Shelfmark", "task_id", task.ID, "status", status) p.failTask(task, fmt.Sprintf("Shelfmark download %s", status), T(task.RequesterLocale, MsgDownloadStatus, task.BookTitle, status)) default: // Still in progress (queued, downloading, locating, resolving, etc.). task.Status = TaskStatusDownloading if err := p.taskStore.SaveTask(task); err != nil { p.API.LogError("Failed to save task", "task_id", task.ID, "error", err.Error()) } } } // processTaskComplete downloads the file from Shelfmark, creates the book post // with the cover image, and replies with the book file. func (p *Plugin) processTaskComplete(task *DownloadTask) { p.API.LogInfo("Downloading file from Shelfmark", "task_id", task.ID, "title", task.BookTitle) // Download the book file from Shelfmark. fileData, filename, err := p.shelfmarkClient.DownloadFile(task.ShelfmarkTaskID) if err != nil { p.API.LogError("Failed to download file from Shelfmark", "task_id", task.ID, "error", err.Error()) // If the file is not ready yet, retry later. if time.Since(task.UpdatedAt) < 2*time.Minute { return } p.failTask(task, "Failed to download file: "+err.Error(), T(task.RequesterLocale, MsgDownloadFailed, task.EffectiveTitle())) return } // Download the cover image if available. var coverFileIDs []string if task.BookCoverURL != "" { coverData, coverFilename, coverErr := p.shelfmarkClient.DownloadCover(task.BookCoverURL) if coverErr != nil { p.API.LogWarn("Failed to download cover image", "task_id", task.ID, "error", coverErr.Error()) } else { coverInfo, appErr := p.API.UploadFile(coverData, task.ChannelID, coverFilename) if appErr != nil { p.API.LogWarn("Failed to upload cover image", "task_id", task.ID, "error", appErr.Error()) } else { coverFileIDs = append(coverFileIDs, coverInfo.Id) } } } // Render the post message using the (possibly localized) title. postMessage, err := p.renderPostMessage(task) if err != nil { p.API.LogError("Failed to render post message", "task_id", task.ID, "error", err.Error()) postMessage = "### " + task.EffectiveTitle() } // Create the book announcement post with the cover image. bookPost := &model.Post{ UserId: p.botUserID, ChannelId: task.ChannelID, Message: postMessage, FileIds: coverFileIDs, } createdPost, appErr := p.API.CreatePost(bookPost) if appErr != nil { p.API.LogError("Failed to create book post", "task_id", task.ID, "error", appErr.Error()) p.failTask(task, "Failed to create post: "+appErr.Error(), T(task.RequesterLocale, MsgPostFailed, task.EffectiveTitle())) return } task.PostID = createdPost.Id // Upload the book file to Mattermost. fileInfo, appErr := p.API.UploadFile(fileData, task.ChannelID, filename) if appErr != nil { p.API.LogError("Failed to upload file to Mattermost", "task_id", task.ID, "error", appErr.Error()) p.failTask(task, "Failed to upload file: "+appErr.Error(), T(task.RequesterLocale, MsgUploadFailed, task.EffectiveTitle())) return } // Create a reply to the book post with the file attached. replyPost := &model.Post{ UserId: p.botUserID, ChannelId: task.ChannelID, RootId: createdPost.Id, Message: "", FileIds: []string{fileInfo.Id}, } if _, appErr := p.API.CreatePost(replyPost); appErr != nil { p.API.LogError("Failed to create file reply post", "task_id", task.ID, "error", appErr.Error()) p.failTask(task, "Failed to create reply post: "+appErr.Error(), T(task.RequesterLocale, MsgAttachFailed, task.EffectiveTitle())) return } task.Status = TaskStatusUploaded if err := p.taskStore.SaveTask(task); err != nil { p.API.LogError("Failed to save task", "task_id", task.ID, "error", err.Error()) return } p.API.LogInfo("Book posted successfully", "task_id", task.ID, "title", task.BookTitle, "filename", filename, "post_id", createdPost.Id) } // failTask marks a task as failed, persists it, and notifies the requester. func (p *Plugin) failTask(task *DownloadTask, errMsg, userMsg string) { task.Status = TaskStatusFailed task.ErrorMessage = errMsg if err := p.taskStore.SaveTask(task); err != nil { p.API.LogError("Failed to save task", "task_id", task.ID, "error", err.Error()) } if userMsg != "" { p.notifyRequester(task, userMsg) } } // notifyRequester sends a DM to the user who requested the book // about errors or status updates. Uses a DM channel with the bot. func (p *Plugin) notifyRequester(task *DownloadTask, message string) { if task.RequestedBy == "" { return } // Create a DM channel between the bot and the requester. dmChannel, appErr := p.API.GetDirectChannel(p.botUserID, task.RequestedBy) if appErr != nil { p.API.LogWarn("Failed to get DM channel for requester notification", "task_id", task.ID, "error", appErr.Error()) return } post := &model.Post{ UserId: p.botUserID, ChannelId: dmChannel.Id, Message: message, } if _, appErr := p.API.CreatePost(post); appErr != nil { p.API.LogError("Failed to send requester notification", "task_id", task.ID, "error", appErr.Error()) } }