diff --git a/README.md b/README.md index d51e32bd..28d34780 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ See [`frontend/README.md`](frontend/README.md) for information on working with S ## Style guide * For Go source, follow the standard Go [style guide](https://github.com/golang/go/wiki/CodeReviewComments). +* variables and functions exposed to Go templates should be in camelCase, like Go variables * All exported functions and variables should have a documentation comment explaining their functionality, as per go style guides. * Unexported functions are preferred to have a documentation comment explaining it, unless it is sufficiently self explanatory or simple. * Git commits should be descriptive. Put further explanation in the comment of the commit. @@ -55,18 +56,19 @@ See [`frontend/README.md`](frontend/README.md) for information on working with S ## Roadmap ### Near future -All features that are to be realised for the near future are found in the issues tab with the milestone "Next Release" +* Make the plugin support actually useful +* Improve moderation tools, using path routing instead of the action variable +* Add support for sticky and locked threads +* Add support for more filetypes (zip,) +* Improve API support for existing chan browing phone apps ### Lower priority -* Improve moderation tools heavily +* Better image fingerpringing and banning system (as opposed to a hash) * Rework any legacy structs that uses comma separated fields to use a slice instead. -* Readd sqlite support * RSS feeds from boards/specific threads/specific usernames+tripcodes (such as newsanon) * Pinning a post within a thread even if its not the OP, to prevent its deletion in a cyclical thread. ### Later down the line -* Improve API support for existing chan browing phone apps -* Better image fingerpringing and banning system (as opposed to a hash) ### Possible experimental features: * Allow users to be mini-moderators within threads they posted themselves, to prevent spammers/derailers. \ No newline at end of file diff --git a/cmd/gochan/deleteposts.go b/cmd/gochan/deleteposts.go index 5abf699a..55e2bfa4 100644 --- a/cmd/gochan/deleteposts.go +++ b/cmd/gochan/deleteposts.go @@ -2,7 +2,9 @@ package main import ( "database/sql" + "errors" "fmt" + "io/fs" "net/http" "os" "path" @@ -14,8 +16,15 @@ import ( "github.com/gochan-org/gochan/pkg/gcutil" "github.com/gochan-org/gochan/pkg/manage" "github.com/gochan-org/gochan/pkg/serverutil" + "github.com/rs/zerolog" ) +type upload struct { + postID int + filename string + boardDir string +} + func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.Request) { // Delete a post or thread errEv := gcutil.LogError(nil). @@ -85,61 +94,9 @@ func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.R } if fileOnly { - upload, err := post.GetUpload() - if err != nil { - errEv.Err(err).Caller(). - Int("postid", post.ID). - Msg("Unable to get file upload info") - serverutil.ServeError(writer, "Error getting file uplaod info: "+err.Error(), - wantsJSON, map[string]interface{}{"postid": post.ID}) + if deletePostUpload(post, board, writer, request, errEv) { return } - documentRoot := config.GetSystemCriticalConfig().DocumentRoot - if upload != nil && upload.Filename != "deleted" { - filePath := path.Join(documentRoot, board.Dir, "src", upload.Filename) - if err = os.Remove(filePath); err != nil { - errEv.Err(err).Caller(). - Int("postid", post.ID). - Str("filename", upload.Filename). - Msg("Unable to delete file") - serverutil.ServeError(writer, "Unable to delete file: "+err.Error(), - wantsJSON, map[string]interface{}{"postid": post.ID}) - return - } - // delete the file's thumbnail - thumbPath := path.Join(documentRoot, board.Dir, "thumb", upload.ThumbnailPath("thumb")) - if err = os.Remove(thumbPath); err != nil { - errEv.Err(err).Caller(). - Int("postid", post.ID). - Str("thumbnail", upload.ThumbnailPath("thumb")). - Msg("Unable to delete thumbnail") - serverutil.ServeError(writer, "Unable to delete thumbnail: "+err.Error(), - wantsJSON, map[string]interface{}{"postid": post.ID}) - return - } - // delete the catalog thumbnail - if post.IsTopPost { - thumbPath := path.Join(documentRoot, board.Dir, "thumb", upload.ThumbnailPath("catalog")) - if err = os.Remove(thumbPath); err != nil { - errEv.Err(err).Caller(). - Int("postid", post.ID). - Str("catalogThumb", upload.ThumbnailPath("catalog")). - Msg("Unable to delete catalog thumbnail") - serverutil.ServeError(writer, "Unable to delete catalog thumbnail: "+err.Error(), - wantsJSON, map[string]interface{}{"postid": post.ID}) - return - } - } - if err = post.UnlinkUploads(true); err != nil { - errEv.Err(err).Caller(). - Str("requestType", "deleteFile"). - Int("postid", post.ID). - Msg("Error unlinking post uploads") - serverutil.ServeError(writer, "Unable to unlink post uploads"+err.Error(), - wantsJSON, map[string]interface{}{"postid": post.ID}) - return - } - } if err = building.BuildBoardPages(board); err != nil { errEv.Err(err).Caller().Send() serverutil.ServeError(writer, "Unable to build board pages for /"+board.Dir+"/: "+err.Error(), wantsJSON, map[string]interface{}{ @@ -172,6 +129,53 @@ func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.R return } } else { + if post.IsTopPost { + rows, err := gcsql.QuerySQL( + `SELECT filename FROM DBPREFIXfiles + LEFT JOIN ( + SELECT id FROM DBPREFIXposts WHERE thread_id = ? + ) p + ON p.id = post_id + WHERE post_id = p.id AND filename != 'deleted'`, + post.ThreadID) + if err != nil { + errEv.Err(err).Caller(). + Str("requestType", "deleteThread"). + Int("postid", post.ID). + Int("threadID", post.ThreadID). + Msg("Unable to get list of filenames in thread") + serverutil.ServeError(writer, "Unable to get list of filenames in thread", wantsJSON, map[string]interface{}{ + "postid": post.ID, + }) + return + } + defer rows.Close() + var uploads []upload + for rows.Next() { + var filename string + if err = rows.Scan(&filename); err != nil { + errEv.Err(err).Caller(). + Str("requestType", "deleteThread"). + Int("postid", post.ID). + Int("threadID", post.ThreadID). + Msg("Unable to get list of filenames in thread") + serverutil.ServeError(writer, "Unable to get list of filenames in thread", wantsJSON, map[string]interface{}{ + "postid": post.ID, + }) + return + } + uploads = append(uploads, upload{ + filename: filename, + boardDir: board.Dir, + }) + } + // done as a goroutine to avoid delays if the thread has a lot of files + // the downside is of course that if something goes wrong, deletion errors + // won't be seen in the browser + go deleteUploads(uploads) + } else if deletePostUpload(post, board, writer, request, errEv) { + return + } // delete the post if err = post.Delete(); err != nil { errEv.Err(err).Caller(). @@ -189,8 +193,6 @@ func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.R os.Remove(threadIndexPath + ".json") } else { building.BuildBoardPages(board) - // _board, _ := gcsql.GetBoardFromID(post.BoardID) - // building.BuildBoardPages(&_board) } building.BuildBoards(false, boardid) } @@ -218,3 +220,90 @@ func deletePosts(checkedPosts []int, writer http.ResponseWriter, request *http.R } } } + +func deleteUploads(uploads []upload) { + documentRoot := config.GetSystemCriticalConfig().DocumentRoot + var filePath, thumbPath, catalogThumbPath string + var err error + for _, upload := range uploads { + filePath = path.Join(documentRoot, upload.boardDir, "src", upload.filename) + if err = os.Remove(filePath); err != nil && !errors.Is(err, os.ErrNotExist) { + gcutil.LogError(err).Caller(). + Str("filePath", filePath). + Int("postid", upload.postID).Send() + } + thumbPath = path.Join(documentRoot, upload.boardDir, "thumb", gcutil.GetThumbnailPath("reply", upload.filename)) + if err = os.Remove(thumbPath); err != nil && !errors.Is(err, os.ErrNotExist) { + gcutil.LogError(err).Caller(). + Str("thumbPath", thumbPath). + Int("postid", upload.postID).Send() + } + catalogThumbPath = path.Join(documentRoot, upload.boardDir, "thumb", gcutil.GetThumbnailPath("catalog", upload.filename)) + if err = os.Remove(catalogThumbPath); err != nil && !errors.Is(err, os.ErrNotExist) { + gcutil.LogError(err).Caller(). + Str("catalogThumbPath", catalogThumbPath). + Int("postid", upload.postID).Send() + } + } +} + +func deletePostUpload(post *gcsql.Post, board *gcsql.Board, writer http.ResponseWriter, request *http.Request, errEv *zerolog.Event) bool { + documentRoot := config.GetSystemCriticalConfig().DocumentRoot + upload, err := post.GetUpload() + wantsJSON := serverutil.IsRequestingJSON(request) + if err != nil { + errEv.Err(err).Caller(). + Int("postid", post.ID). + Msg("Unable to get file upload info") + serverutil.ServeError(writer, "Error getting file uplaod info: "+err.Error(), + wantsJSON, map[string]interface{}{"postid": post.ID}) + return true + } + if upload != nil && upload.Filename != "deleted" { + filePath := path.Join(documentRoot, board.Dir, "src", upload.Filename) + if err = os.Remove(filePath); err != nil && !errors.Is(err, fs.ErrNotExist) { + errEv.Err(err).Caller(). + Int("postid", post.ID). + Str("filename", upload.Filename). + Msg("Unable to delete file") + serverutil.ServeError(writer, "Unable to delete file: "+err.Error(), + wantsJSON, map[string]interface{}{"postid": post.ID}) + return true + } + // delete the file's thumbnail + thumbPath := path.Join(documentRoot, board.Dir, "thumb", upload.ThumbnailPath("thumb")) + if err = os.Remove(thumbPath); err != nil && !errors.Is(err, fs.ErrNotExist) { + errEv.Err(err).Caller(). + Int("postid", post.ID). + Str("thumbnail", upload.ThumbnailPath("thumb")). + Msg("Unable to delete thumbnail") + serverutil.ServeError(writer, "Unable to delete thumbnail: "+err.Error(), + wantsJSON, map[string]interface{}{"postid": post.ID}) + return true + } + // delete the catalog thumbnail + if post.IsTopPost { + thumbPath := path.Join(documentRoot, board.Dir, "thumb", upload.ThumbnailPath("catalog")) + if err = os.Remove(thumbPath); err != nil && !errors.Is(err, fs.ErrNotExist) { + errEv.Err(err).Caller(). + Int("postid", post.ID). + Str("catalogThumb", upload.ThumbnailPath("catalog")). + Msg("Unable to delete catalog thumbnail") + serverutil.ServeError(writer, "Unable to delete catalog thumbnail: "+err.Error(), + wantsJSON, map[string]interface{}{"postid": post.ID}) + return true + } + } + // remove the upload from the database + if err = post.UnlinkUploads(true); err != nil { + errEv.Err(err).Caller(). + Str("requestType", "deleteFile"). + Int("postid", post.ID). + Msg("Error unlinking post uploads") + serverutil.ServeError(writer, "Unable to unlink post uploads"+err.Error(), + wantsJSON, map[string]interface{}{"postid": post.ID}) + return true + } + } + return false +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 88d3514b..a3606829 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "gochan.js", - "version": "3.3.0", + "version": "3.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gochan.js", - "version": "3.3.0", + "version": "3.4.0", "license": "BSD-2-Clause", "dependencies": { "jquery": "^3.5.1", @@ -4922,9 +4922,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001367", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001367.tgz", - "integrity": "sha512-XDgbeOHfifWV3GEES2B8rtsrADx4Jf+juKX2SICJcaUhjYBO3bR96kvEIHa15VU6ohtOhBZuPGGYGbXMRn0NCw==", + "version": "1.0.30001441", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz", + "integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg==", "dev": true, "funding": [ { @@ -13729,9 +13729,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001367", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001367.tgz", - "integrity": "sha512-XDgbeOHfifWV3GEES2B8rtsrADx4Jf+juKX2SICJcaUhjYBO3bR96kvEIHa15VU6ohtOhBZuPGGYGbXMRn0NCw==", + "version": "1.0.30001441", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz", + "integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg==", "dev": true }, "chalk": { diff --git a/frontend/package.json b/frontend/package.json index dbaab23b..007aadd3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "gochan.js", - "version": "3.3.0", + "version": "3.4.0", "description": "", "type": "module", "source": "js/gochan.js", diff --git a/html/error/404.html b/html/error/404.html index ee77d01e..0c4b7fda 100755 --- a/html/error/404.html +++ b/html/error/404.html @@ -7,6 +7,6 @@

404: File not found

The requested file could not be found on this server.

-
Site powered by Gochan v3.3.0 +
Site powered by Gochan v3.4.0 \ No newline at end of file diff --git a/html/error/500.html b/html/error/500.html index a61a8207..7ccc671f 100755 --- a/html/error/500.html +++ b/html/error/500.html @@ -7,6 +7,6 @@

Error 500: Internal Server error

The server encountered an error while trying to serve the page, and we apologize for the inconvenience. The system administrator will try to fix things as soon they get around to it, whenever that is. Hopefully soon.

-
Site powered by Gochan v3.3.0 +
Site powered by Gochan v3.4.0 \ No newline at end of file diff --git a/html/error/502.html b/html/error/502.html index f74b6079..0717570a 100644 --- a/html/error/502.html +++ b/html/error/502.html @@ -7,6 +7,6 @@

Error 502: Bad gateway

The server encountered an error while trying to serve the page, and we apologize for the inconvenience. The system administrator will try to fix things as soon they get around to it, whenever that is. Hopefully soon.

-
Site powered by Gochan v3.3.0 +
Site powered by Gochan v3.4.0 \ No newline at end of file diff --git a/pkg/posting/uploads.go b/pkg/posting/uploads.go index 75a0ffdd..dd8164ff 100644 --- a/pkg/posting/uploads.go +++ b/pkg/posting/uploads.go @@ -102,7 +102,7 @@ func AttachUploadFromRequest(request *http.Request, writer http.ResponseWriter, infoEv.Str("post", "withVideo"). Str("filename", handler.Filename). Str("referer", request.Referer()).Send() - if post.IsTopPost { + if post.ThreadID == 0 { if err := createVideoThumbnail(filePath, thumbPath, boardConfig.ThumbWidth); err != nil { errEv.Err(err).Caller(). Str("filePath", filePath). @@ -158,7 +158,7 @@ func AttachUploadFromRequest(request *http.Request, writer http.ResponseWriter, } } thumbType := "reply" - if post.IsTopPost { + if post.ThreadID == 0 { thumbType = "op" } upload.ThumbnailWidth, upload.ThumbnailHeight = getThumbnailSize( @@ -188,7 +188,7 @@ func AttachUploadFromRequest(request *http.Request, writer http.ResponseWriter, upload.Width = img.Bounds().Max.X upload.Height = img.Bounds().Max.Y thumbType := "reply" - if post.IsTopPost { + if post.ThreadID == 0 { thumbType = "op" } upload.ThumbnailWidth, upload.ThumbnailHeight = getThumbnailSize( @@ -219,7 +219,7 @@ func AttachUploadFromRequest(request *http.Request, writer http.ResponseWriter, if shouldThumb { var thumbnail image.Image var catalogThumbnail image.Image - if post.IsTopPost { + if post.ThreadID == 0 { // If this is a new thread, generate thumbnail and catalog thumbnail thumbnail = createImageThumbnail(img, postBoard.Dir, "op") catalogThumbnail = createImageThumbnail(img, postBoard.Dir, "catalog") @@ -251,7 +251,7 @@ func AttachUploadFromRequest(request *http.Request, writer http.ResponseWriter, serverutil.ServeErrorPage(writer, "Couldn't create thumbnail: "+err.Error()) return nil, true } - if post.IsTopPost { + if post.ThreadID == 0 { // Generate catalog thumbnail catalogThumbnail := createImageThumbnail(img, postBoard.Dir, "catalog") if err = imaging.Save(catalogThumbnail, catalogThumbPath); err != nil { diff --git a/version b/version index 0fa4ae48..fbcbf738 100644 --- a/version +++ b/version @@ -1 +1 @@ -3.3.0 \ No newline at end of file +3.4.0 \ No newline at end of file