From 4728c41fd4fe8185f9b626481cf70078d8348f13 Mon Sep 17 00:00:00 2001 From: Lowikian Date: Wed, 28 May 2025 08:28:18 +0200 Subject: [PATCH 01/18] small changes & cleanup --- src/http_calls.go | 21 ++++++++++++--------- src/main.go | 10 ++++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/http_calls.go b/src/http_calls.go index aa53411..f1bac0e 100644 --- a/src/http_calls.go +++ b/src/http_calls.go @@ -10,6 +10,7 @@ import ( "context" "encoding/base64" "encoding/json" + "fmt" "io" "net/http" "path/filepath" @@ -78,7 +79,8 @@ func proxyAndTransform(c *gin.Context) { } // parse it into username, password and proxy auth authParts := strings.Split(string(decodedAuth), ":") - suppliedProxyAuth := authParts[1] // if you want to check the password, set it to 2 + suppliedProxyAuth := authParts[1] // to change the password position you need to change this here. 1 is after the username, 2 is after the proxy auth + logging.Debug("Parsed Proxy Authorization header: ", authParts) if len(authParts) != 3 || suppliedProxyAuth != PROXY_AUTH { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return @@ -86,8 +88,9 @@ func proxyAndTransform(c *gin.Context) { c.Request.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(authParts[0]+":"+authParts[2]))) } else { - + // this gets run if the proxy auth function not enabled if auth == "" { + logging.Debug("No Authorization header found, using anonymous user") requestUsername = "anonymous" } @@ -172,11 +175,10 @@ func proxyAndTransform(c *gin.Context) { defer reader.Close() default: + logging.Debug("Server sent uncompressed response") reader = resp.Body } - //Accept-Encoding:[gzip, compress, deflate, br] - // Read response body respBody, err := io.ReadAll(reader) @@ -184,6 +186,7 @@ func proxyAndTransform(c *gin.Context) { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Failed to read response body"}) return } + // check for rate limit if resp.StatusCode == 501 { c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "Rate limit exceeded"}) @@ -248,7 +251,7 @@ func proxyAndTransform(c *gin.Context) { for _, pool := range pools { // Store in DB - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), globalTimeout) defer cancel() Database.UpdatePool(ctx, &pool) } @@ -263,7 +266,7 @@ func proxyAndTransform(c *gin.Context) { } // Store in DB - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), globalTimeout) defer cancel() Database.UpdatePool(ctx, &pool) @@ -416,11 +419,11 @@ func copyHeaders(src http.Header, dst http.Header) { } func ProcessPost(c *gin.Context, post *Post) { - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), globalTimeout) defer cancel() Database.CheckAndInsertPost(ctx, post) - // Rewrite url to go through proxy + // Rewrite url to go through the proxy post.File.URL = makeProxyLink(post.File.URL) post.Preview.URL = makeProxyLink(post.Preview.URL) post.Sample.URL = makeProxyLink(post.Sample.URL) @@ -434,7 +437,7 @@ func setUseragent(username string, req *http.Request) { return } - useragent = useragentBase + " (Request made on behalf of " + username + ")" + useragent = fmt.Sprintf("%s (Request made on behalf of %s)", useragentBase, username) req.Header.Set("User-Agent", useragent) } diff --git a/src/main.go b/src/main.go index 05f1285..4a3b999 100644 --- a/src/main.go +++ b/src/main.go @@ -23,9 +23,10 @@ var ( Database DB useragentBase = "e6-cache (https://github.com/bugmaschine/e6-cache)" port = ":8080" - Key []byte // gets randomly generated every launch, and used for signing the urls. - maxCacheAge = 1 * time.Hour // idk what's a good value, but 1 hours seems enough - Signer *signer.Signer // feel free to sugest a better name + Key []byte // gets randomly generated every launch, and used for signing the urls. + maxCacheAge = 1 * time.Hour // idk what's a good value, but 1 hours seems enough + Signer *signer.Signer // feel free to sugest a better name + globalTimeout = 5 * time.Second // global timeout for requests to e6, if it takes longer than this, we assume the request failed. // env stuff @@ -108,7 +109,7 @@ func main() { // setup s3 logging.Info("Connecting to S3...") - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), globalTimeout) defer cancel() s3Svc, err := NewS3Service(ctx, S3_REGION, S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET_NAME) if err != nil { @@ -125,6 +126,7 @@ func main() { } router.ForwardedByClientIP = true + router.Use(gin.Recovery()) // Routes implementing caching From 39e745e3d488a67711cbcfd27871bcd3c62da7db Mon Sep 17 00:00:00 2001 From: Lowikian Date: Wed, 28 May 2025 09:20:53 +0200 Subject: [PATCH 02/18] use openapi file to parse, for easy route imports --- src/go.mod | 9 + src/go.sum | 18 + src/main.go | 67 +- src/openapi.yaml | 11428 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 11483 insertions(+), 39 deletions(-) create mode 100644 src/openapi.yaml diff --git a/src/go.mod b/src/go.mod index 26b1c71..8a22778 100644 --- a/src/go.mod +++ b/src/go.mod @@ -9,6 +9,7 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.17.67 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.76 github.com/aws/aws-sdk-go-v2/service/s3 v1.79.4 + github.com/getkin/kin-openapi v0.132.0 github.com/gin-gonic/gin v1.10.1 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 @@ -35,17 +36,25 @@ require ( github.com/cloudwego/iasm v0.2.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.8.0 // indirect diff --git a/src/go.sum b/src/go.sum index 11bfdba..a1730ed 100644 --- a/src/go.sum +++ b/src/go.sum @@ -51,10 +51,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= +github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -70,6 +76,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -80,6 +88,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -87,8 +97,16 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/src/main.go b/src/main.go index 4a3b999..c1df5bd 100644 --- a/src/main.go +++ b/src/main.go @@ -4,12 +4,17 @@ import ( "bugmaschine/e6-cache/logging" "context" _ "embed" + "fmt" + "log" "os" + "regexp" "strconv" "time" "bugmaschine/e6-cache/signer" + "github.com/getkin/kin-openapi/openapi3" + "github.com/gin-gonic/gin" "github.com/joho/godotenv" ) @@ -49,6 +54,9 @@ var ( PROXY_URL string baseURL string PROXY_AUTH string + + //go:embed openapi.yaml + openApiRoutes []byte // embedded OpenAPI routes, used to dynamically register the routes in the gin router. ) func loadEnv() { @@ -125,45 +133,26 @@ func main() { gin.SetMode(gin.ReleaseMode) } - router.ForwardedByClientIP = true - router.Use(gin.Recovery()) - - // Routes implementing caching - - router.GET("/posts.json", proxyAndTransform) - router.GET("/posts/:Post_ID.json", proxyAndTransform) - router.GET("/pools.json", proxyAndTransform) - router.GET("/pools/:Pool_ID", proxyAndTransform) - router.GET("/comments.json", proxyAndTransform) // this for reason just returns posts????????? - - // Routes that could be maybe saved - - router.GET("/notes.json", proxyAndTransform) - router.GET("/wiki_pages.json", proxyAndTransform) - router.GET("/post_flags.json", proxyAndTransform) - - // Routes just needing proxying and have make no sense being cached - - router.POST("/favorites.json", proxyAndTransform) - router.POST("/uploads.json", proxyAndTransform) - router.POST("/post_flags.json", proxyAndTransform) - router.DELETE("/favorites/:Post_ID", proxyAndTransform) - router.POST("/notes.json", proxyAndTransform) - router.PUT("/notes/*path", proxyAndTransform) - router.DELETE("/notes/:Note_ID", proxyAndTransform) - router.POST("/posts/:Post_ID/votes.json", proxyAndTransform) - router.GET("/forum_topics.json", proxyAndTransform) - router.GET("/forum_posts.json", proxyAndTransform) - router.POST("/comments.json", proxyAndTransform) - router.PUT("/comments/:Comment_ID", proxyAndTransform) - router.DELETE("/comments/:Comment_ID", proxyAndTransform) - router.POST("/comments/:Comment_ID/votes.json", proxyAndTransform) - router.PUT("/users/:User_ID.json", proxyAndTransform) - router.GET("/users/:User_ID.json", proxyAndTransform) - router.GET("/tags/*path", proxyAndTransform) - - // potentially just forward to the baseUrl - router.PATCH("/posts/:Post_ID", proxyAndTransform) + // load e621 routes + loader := openapi3.NewLoader() + doc, err := loader.LoadFromData(openApiRoutes) + if err != nil { + log.Fatalf("Failed to load OpenAPI: %v (make sure to run 'go embed')", err) + } + + // create a regex to convert OpenAPI path parameters {id} to gins format :id + re := regexp.MustCompile(`\{(.+?)\}`) + + for _, path := range doc.Paths.InMatchingOrder() { + pathItem := doc.Paths.Find(path) + // convert OpenAPI parameter syntax to Gin parameter syntax, with the problem being that gin has problems supporting that + convertedPath := re.ReplaceAllString(path, ":$1") + for method := range pathItem.Operations() { + router.Handle(method, convertedPath, proxyAndTransform) + } + } + + logging.Info(fmt.Sprintf("Registered %d routes from OpenAPI spec", len(doc.Paths.InMatchingOrder()))) // Proxy files from S3, if not save them. router.GET("/proxy/:File_ID", proxyFile) diff --git a/src/openapi.yaml b/src/openapi.yaml new file mode 100644 index 0000000..8afb089 --- /dev/null +++ b/src/openapi.yaml @@ -0,0 +1,11428 @@ +openapi: "3.1.0" +info: + title: "E621" + description: | + OpenAPI definition for E621's API. You can find the source [here](https://github.com/DonovanDMC/E621OpenAPI)
+ This document is intended to compliment E621's existing [API Documentation](https://e621.net/help/api).
+ Note if E621's api is under attack and/or cloudflare protections are enabled, the "Try it out" buttons here will not work.
+ If they are not working, you can check this [Unofficial Status Page](https://status.e621.ws). + license: + name: "MIT" + url: "https://opensource.org/licenses/MIT" + + contact: + name: Donovan_DMC + url: https://e621.net/forum_topics/46279 + version: "d69c34e" + +servers: + - url: https://e621.net + description: E621 (production) + - url: http://localhost:3000 + description: E621 (Development) + +x-constants: + - &modaction-actions + - artist_page_rename + - artist_page_lock + - artist_page_unlock + - artist_user_linked + - artist_user_unlinked + - avoid_posting_create + - avoid_posting_update + - avoid_posting_delete + - avoid_posting_undelete + - avoid_posting_destroy + - blip_delete + - blip_hide + - blip_unhide + - blip_update + - comment_delete + - comment_hide + - comment_unhide + - comment_update + - forum_category_create + - forum_category_delete + - forum_category_update + - forum_post_delete + - forum_post_hide + - forum_post_unhide + - forum_post_update + - forum_topic_delete + - forum_topic_hide + - forum_topic_unhide + - forum_topic_lock + - forum_topic_unlock + - forum_topic_stick + - forum_topic_unstick + - forum_topic_update + - help_create + - help_delete + - help_update + - ip_ban_create + - ip_ban_delete + - mascot_create + - mascot_update + - mascot_delete + - pool_delete + - report_reason_create + - report_reason_delete + - report_reason_update + - set_update + - set_delete + - set_change_visibility + - tag_alias_create + - tag_alias_update + - tag_implication_create + - tag_implication_update + - ticket_claim + - ticket_unclaim + - ticket_update + - upload_whitelist_create + - upload_whitelist_update + - upload_whitelist_delete + - user_blacklist_changed + - user_text_change + - user_upload_limit_change + - user_flags_change + - user_level_change + - user_name_change + - user_delete + - user_ban + - user_ban_update + - user_unban + - user_feedback_create + - user_feedback_update + - user_feedback_delete + - user_feedback_undelete + - user_feedback_destroy + - wiki_page_rename + - wiki_page_delete + - wiki_page_lock + - wiki_page_unlock + - mass_update + - nuke_tag + - takedown_delete + - takedown_process + # legacy + - created_positive_record + - created_neutral_record + - created_negative_record + - created_flag_reason + - edited_flag_reason + - deleted_flag_reason + - post_move_favorites + - post_delete + - post_undelete + - post_destroy + - post_rating_lock + - post_unapprove + - post_replacement_accept + - post_replacement_reject + - post_replacement_delete + - &post-event-actions + - deleted + - undeleted + - approved + - unapproved + - flag_created + - flag_removed + - favorites_moved + - favorites_received + - rating_locked + - rating_unlocked + - status_locked + - status_unlocked + - note_locked + - note_unlocked + - comment_locked + - comment_unlocked + - replacement_accepted + - replacement_rejected + - replacement_promoted + - replacement_deleted + - expunged + - changed_bg_color + - &ratings + - s + - q + - e + - &tag-categories + - 0 # general + - 1 # artist + - 3 # copyright + - 4 # character + - 5 # species + - 6 # invalid + - 7 # meta + - 8 # lore + - &feedback-categories + - negative + - neutral + - positive + - &tag-request-statuses + - active + - deleted + - processing + - queued + - retired + - error + - pending + - &warning-types + - warning + - record + - ban + - &pool-categories + - collection + - series + - &ticket-types + - blip + - comment + - dmail + - forum + - pool + - post + - set + - user + - wiki + - &ticket-statuses + - pending + - partial + - approved + +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + parameters: + id: + name: search[id] + in: query + description: Search for a specific id. + schema: + type: number + limit: + name: limit + in: query + description: The maximum number of results to return. Between 0 and 320. + schema: + type: number + minimum: 0 + maximum: 500 + page: + name: page + in: query + description: The page number of results to get. Between 1 and 750. + schema: + type: number + minimum: 1 + maximum: 750 + ip_addr: + name: search[ip_addr] + in: query + description: Must be Admin+ to use. See [postgres' documentation](https://www.postgresql.org/docs/9.3/functions-net.html) for information on how this is parsed. Specifically, "is contained within or equals" (`<<=`). + schema: + type: string + schemas: + Artist: + type: object + required: + - id + - name + - updated_at + - is_active + - other_names + - group_name + - linked_user_id + - created_at + - creator_id + - is_locked + - notes + - urls + properties: + id: + type: number + name: + type: string + updated_at: + type: string + format: date-time + is_active: + type: boolean + other_names: + type: array + items: + type: string + group_name: + type: string + linked_user_id: + type: ["null", "number"] + created_at: + type: string + format: date-time + creator_id: + type: number + is_locked: + type: boolean + notes: + type: ["null", "string"] + ArtistURL: + type: object + required: + - id + - artist_id + - url + - normalized_url + - created_at + - updated_at + - is_active + properties: + id: + type: number + artist_id: + type: number + url: + type: string + format: uri + normalized_url: + type: string + format: uri + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + is_active: + type: boolean + ArtistVersion: + type: object + required: + - id + - artist_id + - name + - updater_id + - created_at + - updated_at + - is_active + - other_names + - notes_changed + - urls + properties: + id: + type: number + artist_id: + type: number + name: + type: string + updater_id: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + is_active: + type: boolean + other_names: + type: array + items: + type: string + notes_changed: + type: boolean + urls: + type: array + items: + type: string + format: uri + AvoidPosting: + type: object + required: + - id + - creator_id + - updater_id + - artist_id + - details + - is_active + - created_at + - updated_at + properties: + id: + type: number + creator_id: + type: number + updater_id: + type: number + artist_id: + type: number + staff_notes: + type: string + description: Only visible to Janitor+ + details: + type: string + is_active: + type: boolean + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + AvoidPostingVersion: + type: object + required: + - id + - updater_id + - avoid_posting_id + - details + - is_active + - updated_at + properties: + id: + type: number + updater_id: + type: number + avoid_posting_id: + type: number + details: + type: string + staff_notes: + type: string + description: Only visible to Janitor+ + is_active: + type: boolean + updated_at: + type: string + format: date-time + Ban: + type: object + required: + - id + - user_id + - reason + - expires_at + - banner_id + - created_at + - updated_at + properties: + id: + type: number + user_id: + type: number + reason: + type: string + expires_at: + type: ["string", "null"] + format: date-time + banner_id: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + Blip: + type: object + required: + - id + - creator_id + - body + - response_to + - created_at + - updated_at + - is_hidden + - warning_type + - warning_user_id + - updater_id + - creator_name + properties: + id: + type: number + creator_id: + type: number + body: + type: string + response_to: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + is_hidden: + type: boolean + warning_type: + type: string + enum: *warning-types + warning_user_id: + type: number + updater_id: + type: number + creator_name: + type: string + BulkRelatedTag: + allOf: + - $ref: "#/components/schemas/RelatedTag" + - type: object + required: + - count + properties: + count: + type: number + BulkUpdateRequest: + type: object + required: + - id + - creator_id + - forum_topic_id + - script + - status + - created_at + - updated_at + - approver_id + - forum_post_id + - title + properties: + id: + type: number + creator_id: + type: number + forum_topic_id: + type: ["number", "null"] + script: + type: string + examples: + - "alias a -> b" + status: + type: string + enum: + - approved + - rejected + - pending + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + approver_id: + type: ["number", "null"] + forum_post_id: + type: ["number", "null"] + title: + type: string + Comment: + type: object + required: + - id + - created_at + - post_id + - creator_id + - body + - score + - updated_at + - updater_id + - do_not_bump_post + - is_hidden + - is_sticky + - warning_type + - warning_user_id + - creator_name + - updater_name + properties: + id: + type: number + created_at: + type: string + format: date-time + post_id: + type: number + creator_id: + type: number + body: + type: string + score: + type: number + updated_at: + type: string + format: date-time + updater_id: + type: number + do_not_bump_post: + type: boolean + deprecated: true + is_hidden: + type: boolean + is_sticky: + type: boolean + warning_type: + type: ["string", "null"] + enum: *warning-types + warning_user_id: + type: ["number", "null"] + creator_name: + type: string + updater_name: + type: string + CurrentUser: + allOf: + - $ref: "#/components/schemas/User" + - type: object + required: + - blacklist_users + - description_collapsed_initially + - hide_comments + - show_hidden_comments + - show_post_statistics + - receive_email_notifications + - enable_keyboard_navigation + - enable_privacy_mode + - style_usernames + - enable_auto_complete + - disabled_cropped_thumbnails + - enable_safe_mode + - disable_responsive_mode + - no_flagging + - disable_user_dmails + - enable_compact_uploader + - replacements_beta + - updated_at + - email + - last_logged_in_at + - last_forum_read_at + - recent_tags + - comment_threshold + - default_image_size + - favorite_tags + - blacklisted_tags + - time_zone + - per_page + - custom_style + - favorite_count + - api_regen_multiplier + - api_burst_limit + - remaining_api_limit + - statement_timeout + - favorite_limit + - tag_query_limit + - has_mail + properties: + blacklist_users: + type: boolean + description_collapsed_initially: + type: boolean + hide_comments: + type: boolean + show_hidden_comments: + type: boolean + show_post_statistics: + type: boolean + receive_email_notifications: + type: boolean + enable_keyboard_navigation: + type: boolean + enable_privacy_mode: + type: boolean + style_usernames: + type: boolean + enable_auto_complete: + type: boolean + can_approve_posts: + type: boolean + disabled_cropped_thumbnails: + type: boolean + enable_safe_mode: + type: boolean + disable_responsive_mode: + type: boolean + no_flagging: + type: boolean + disable_user_dmails: + type: boolean + enable_compact_uploader: + type: boolean + replacements_beta: + type: boolean + updated_at: + type: string + format: date-time + email: + type: string + last_logged_in_at: + type: string + format: date-time + last_forum_read_at: + type: string + format: date-time + recent_tags: + type: string + comment_threshold: + type: number + default_image_sizedefault_image_size: + type: string + enum: + - large + - fit + - fitv + - original + favorite_tags: + type: string + blacklisted_tags: + type: string + time_zone: + type: string + per_page: + type: number + custom_style: + type: string + favorite_count: + type: number + api_regen_multiplier: + type: number + api_burst_limit: + type: number + remaining_api_limit: + type: number + statement_timeout: + type: number + favorite_limit: + type: number + tag_query_limit: + type: number + has_mail: + type: boolean + DMail: + type: object + required: + - id + - owner_id + - from_id + - to_id + - title + - body + - is_read + - is_deleted + - created_at + - updated_at + properties: + id: + type: number + owner_id: + type: number + from_id: + type: number + to_id: + type: number + title: + type: string + body: + type: string + is_read: + type: boolean + is_deleted: + type: boolean + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + DeferredPost: + type: object + required: + - id + - flags + - tags + - rating + - file_ext + - width + - height + - size + - created_at + - uploader + - uploader_id + - score + - fav_count + - is_favorited + - pools + - md5 + - preview_url + - large_url + - file_url + - preview_width + - preview_height + properties: + id: + type: number + flags: + type: string + tags: + type: string + rating: + type: string + enum: *ratings + file_ext: + type: string + width: + type: number + height: + type: number + size: + type: number + created_at: + type: string + format: date-time + uploader: + type: string + uploader_id: + type: number + score: + type: number + fav_count: + type: number + is_favorited: + type: boolean + pools: + type: array + items: + type: number + md5: + type: string + preview_url: + type: ["string", "null"] + large_url: + type: ["string", "null"] + file_url: + type: ["string", "null"] + preview_width: + type: number + preview_height: + type: number + DTextResponse: + type: object + required: + - html + - posts + properties: + html: + type: string + posts: + type: object + patternProperties: + "^0-9+$": + $ref: "#/components/schemas/DeferredPost" + EmailBlacklist: + type: object + required: + - id + - created_at + - updated_at + - domain + - creator_id + - reason + properties: + id: + type: number + created_at: + type: string + updated_at: + type: string + domain: + type: string + creator_id: + type: string + reason: + type: string + ForumPost: + type: object + required: + - id + - topic_id + - creator_id + - updater_id + - body + - is_hidden + - created_at + - updated_at + - warning_type + - warning_user_id + properties: + id: + type: number + topic: + type: number + creator_id: + type: number + updater_id: + type: number + body: + type: string + is_hidden: + type: boolean + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + warning_type: + type: ["string", "null"] + enum: *warning-types + warning_user_id: + type: ["number", "null"] + ForumPostVote: + type: object + required: + - id + - forum_post_id + - creator_id + - score + - created_at + - updated_at + - creator_name + properties: + id: + type: number + forum_post_id: + type: number + creator_id: + type: number + score: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + creator_name: + type: string + ForumTopic: + type: object + required: + - id + - creator_id + - updater_id + - title + - response_count + - is_sticky + - is_locked + - is_hidden + - created_at + - updated_at + - category_id + properties: + id: + type: number + creator_id: + type: number + updater_id: + type: number + title: + type: string + response_count: + type: number + is_sticky: + type: boolean + is_locked: + type: boolean + is_hidden: + type: boolean + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + category_id: + type: number + FullCurrentUser: + type: object + allOf: + - $ref: "#/components/schemas/CurrentUser" + - $ref: "#/components/schemas/FullUser" + FullUser: + allOf: + - $ref: "#/components/schemas/User" + - type: object + required: + - wiki_page_version_count + - artist_version_count + - pool_version_count + - forum_post_count + - comment_count + - flag_count + - favorite_count + - positive_feedback_count + - neutral_feedback_count + - negative_feedback_count + - upload_limit + - profile_about + - profile_artinfo + properties: + wiki_page_version_count: + type: number + artist_version_count: + type: number + pool_version_count: + type: number + forum_post_count: + type: number + comment_count: + type: number + flag_count: + type: number + favorite_count: + type: number + positive_feedback_count: + type: number + neutral_feedback_count: + type: number + negative_feedback_count: + type: number + upload_limit: + type: number + profile_about: + type: string + profile_artinfo: + type: string + Help: + type: object + required: + - id + - name + - title + - wiki_page + - related + - created_at + - updated_at + properties: + id: + type: number + name: + type: string + title: + type: string + wiki_page: + type: string + related: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + IPBan: + type: object + description: Due to a global filter, the ip_addr is not present no matter your user level. + required: + - id + - creator_id + - reason + - created_at + - updated_at + properties: + id: + type: number + creator_id: + type: number + reason: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + IQDBPost: + type: object + required: + - id + - created_at + - updated_at + - up_score + - down_score + - score + - source + - md5 + - rating + - is_note_locked + - is_rating_locked + - is_status_locked + - is_pending + - is_flagged + - is_deleted + - uploader_id + - approver_id + - last_noted_at + - last_comment_bumped_at + - fav_count + - tag_string + - tag_count + - tag_count_general + - tag_count_artist + - tag_count_character + - tag_count_copyright + - file_ext + - file_size + - image_width + - image_height + - parent_id + - has_children + - last_commented_at + - has_active_children + - bit_flags + - tag_count_meta + - locked_tags + - tag_count_species + - tag_count_invalid + - description + - comment_count + - change_seq + - tag_count_lore + - bg_color + - generated_samples + - duration + - is_comment_disabled + - is_comment_locked + - has_large + - has_visible_children + - children_ids + - pool_ids + - is_favorited + properties: + id: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + up_score: + type: number + down_score: + type: number + score: + type: number + source: + type: string + md5: + type: string + rating: + type: string + enum: *ratings + is_note_locked: + type: boolean + is_rating_locked: + type: boolean + is_status_locked: + type: boolean + is_pending: + type: boolean + is_flagged: + type: boolean + is_deleted: + type: boolean + uploader_id: + type: number + approver_id: + type: number + last_noted_at: + type: ["null", "string"] + format: date-time + last_comment_bumped_at: + type: ["null", "string"] + format: date-time + fav_count: + type: number + tag_string: + type: string + tag_count: + type: number + tag_count_general: + type: number + tag_count_artist: + type: number + tag_count_character: + type: number + tag_count_copyright: + type: number + file_ext: + type: string + file_size: + type: number + image_width: + type: number + image_height: + type: number + parent_id: + type: ["null", "number"] + has_children: + type: boolean + last_commented_at: + type: ["null", "string"] + format: date-time + has_active_children: + type: boolean + bit_flags: + type: number + tag_count_meta: + type: number + locked_tags: + type: ["null", "string"] + tag_count_species: + type: number + tag_count_invalid: + type: number + description: + type: string + comment_count: + type: number + change_seq: + type: number + tag_count_lore: + type: number + bg_color: + type: ["null", "string"] + generated_samples: + type: ["null", "array"] + items: + type: string + enum: + - 720p + - 480p + - original + duration: + type: ["null", "string"] + is_comment_disabled: + type: boolean + is_comment_locked: + type: boolean + has_large: + type: boolean + has_visible_children: + type: boolean + children_ids: + type: ["null", "string"] + pool_ids: + type: array + items: + type: number + is_favorited: + type: boolean + file_url: + type: string + large_file_url: + type: string + preview_file_url: + type: string + IQDBResponse: + type: object + required: + - hash + - post_id + - score + - post + properties: + hash: + type: string + post_id: + type: number + score: + type: number + post: + type: object + required: + - posts + properties: + posts: + $ref: "#/components/schemas/IQDBPost" + Mascot: + type: object + required: + - id + - creator_id + - display_name + - md5 + - file_ext + - background_color + - artist_url + - artist_name + - active + - created_at + - updated_at + - available_on + - url_path + properties: + id: + type: number + creator_id: + type: number + display_name: + type: string + md5: + type: string + file_ext: + type: string + background_color: + type: string + artist_url: + type: string + format: uri + artist_name: + type: string + active: + type: boolean + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + available_on: + type: array + items: + type: string + url_path: + type: string + format: uri + ModAction: + type: object + required: + - id + - creator_id + - created_at + - updated_at + - action + properties: + id: + type: number + creator_id: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + action: + type: string + enum: *modaction-actions + NewsUpdate: + type: object + required: + - id + - message + - creator_id + - updater_id + - created_at + - updated_at + properties: + id: + type: number + message: + type: string + creator_id: + type: number + updater_id: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + Note: + type: object + required: + - id + - created_at + - updated_at + - creator_id + - x + - y + - width + - height + - version + - is_active + - post_id + - body + - creator_name + properties: + id: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + creator_id: + type: number + x: + type: number + y: + type: number + width: + type: number + height: + type: number + version: + type: number + is_active: + type: boolean + post_id: + type: number + body: + type: string + creator_name: + type: string + NoteVersion: + type: object + required: + - id + - created_at + - updated_at + - x + - y + - width + - height + - body + - version + - is_active + - note_id + - post_id + - updater_id + properties: + id: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + x: + type: number + y: + type: number + width: + type: number + height: + type: number + body: + type: string + version: + type: number + is_active: + type: boolean + note_id: + type: number + post_id: + type: number + updater_id: + type: number + Pool: + type: object + required: + - id + - name + - created_at + - updated_at + - creator_id + - description + - is_active + - category + - post_ids + - creator_name + - post_count + properties: + id: + type: number + name: + type: string + updated_at: + type: string + format: date-time + creator_id: + type: number + description: + type: string + is_active: + type: boolean + category: + type: string + enum: *pool-categories + post_ids: + type: array + items: + type: number + created_at: + type: string + format: date-time + creator_name: + type: string + post_count: + type: number + PoolVersion: + type: object + required: + - id + - pool_id + - post_ids + - updater_id + - created_at + - updated_at + - name + - name_changed + - description + - description_changed + - is_active + - is_locked + - category + - version + - added_post_ids + - removed_post_ids + properties: + id: + type: number + pool_id: + type: number + post_ids: + type: array + items: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + updater_id: + type: number + name: + type: string + name_changed: + type: boolean + description: + type: string + description_changed: + type: boolean + is_active: + type: boolean + is_locked: + type: boolean + category: + type: string + enum: *pool-categories + version: + type: number + added_post_ids: + type: array + items: + type: number + removed_post_ids: + type: array + items: + type: number + Post: + type: object + required: + - id + - created_at + - updated_at + - file + - preview + - sample + - score + - tags + - locked_tags + - change_seq + - flags + - rating + - fav_count + - sources + - pools + - relationships + - approver_id + - uploader_id + - description + - comment_count + - is_favorited + - has_notes + - duration + properties: + id: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + file: + type: object + required: + - width + - height + - ext + - size + - md5 + - url + properties: + width: + type: number + height: + type: number + ext: + type: string + size: + type: number + md5: + type: string + url: + type: ["string", "null"] + preview: + type: object + required: + - width + - height + - url + properties: + width: + type: number + height: + type: number + url: + type: ["string", "null"] + sample: + type: object + required: + - has + - height + - width + - url + - alternates + properties: + has: + type: boolean + height: + type: ["number", "null"] + width: + type: ["number", "null"] + url: + type: ["string", "null"] + alternates: + type: object + properties: + 480p: + $ref: "#/components/schemas/PostSampleAlternate" + 720p: + $ref: "#/components/schemas/PostSampleAlternate" + original: + $ref: "#/components/schemas/PostSampleAlternate" + score: + type: object + required: + - up + - down + - total + properties: + up: + type: number + down: + type: number + total: + type: number + tags: + type: object + required: + - general + - artist + - copyright + - character + - species + - invalid + - meta + - lore + properties: + general: + type: array + items: + type: string + artist: + type: array + items: + type: string + copyright: + type: array + items: + type: string + character: + type: array + items: + type: string + species: + type: array + items: + type: string + invalid: + type: array + items: + type: string + meta: + type: array + items: + type: string + lore: + type: array + items: + type: string + locked_tags: + type: ["array", "null"] + items: + type: string + change_seq: + type: number + flags: + type: object + required: + - pending + - flagged + - note_locked + - status_locked + - rating_locked + - deleted + properties: + pending: + type: boolean + flagged: + type: boolean + note_locked: + type: boolean + status_locked: + type: boolean + rating_locked: + type: boolean + deleted: + type: boolean + rating: + type: string + enum: *ratings + fav_count: + type: number + sources: + type: array + items: + type: string + pools: + type: array + items: + type: number + relationships: + type: object + required: + - parent_id + - has_children + - has_active_children + - children + properties: + parent_id: + type: ["number", "null"] + has_children: + type: boolean + has_active_children: + type: boolean + children: + type: array + items: + type: number + approver_id: + type: ["number", "null"] + uploader_id: + type: number + description: + type: string + comment_count: + type: number + is_favorited: + type: boolean + has_notes: + type: boolean + duration: + type: ["number", "null"] + PostApproval: + type: object + required: + - id + - user_id + - post_id + - created_at + - updated_at + properties: + id: + type: number + user_id: + type: number + post_id: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + PostDisapproval: + type: object + required: + - id + - user_id + - post_id + - reason + - message + - created_at + - updated_at + properties: + id: + type: number + user_id: + type: number + post_id: + type: number + reason: + type: string + enum: + - borderline_quality + - borderline_relevancy + - other + message: + type: ["string", "null"] + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + PostEvent: + type: object + required: + - id + - creator_id + - post_id + - action + - created_at + properties: + id: + type: number + creator_id: + type: ["number", "null"] + post_id: + type: number + action: + type: string + enum: *post-event-actions + created_at: + type: string + format: date-time + PostFlag: + type: object + required: + - id + - created_at + - post_id + - reason + - creator_id + - is_resolved + - updated_at + - is_deletion + - type + properties: + id: + type: number + created_at: + type: string + format: date-time + post_id: + type: number + reason: + type: string + creator_id: + type: ["number", "null"] + is_resolved: + type: boolean + updated_at: + type: string + format: date-time + is_deletion: + type: boolean + type: + type: string + enum: + - flag + - deletion + PostReplacement: + type: object + required: + - id + - created_at + - updated_at + - post_id + - creator_id + - approver_id + - file_ext + - file_size + - image_height + - image_width + - md5 + - source + - file_name + - status + - reason + properties: + id: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + post_id: + type: number + creator_id: + type: number + approver_id: + type: ["number", "null"] + file_ext: + type: string + file_size: + type: number + image_height: + type: number + image_width: + type: number + md5: + type: string + source: + type: string + file_name: + type: string + status: + type: string + enum: + - prompted + - approved + - rejected + - pending + reason: + type: string + PostSampleAlternate: + type: object + required: + - type + - height + - width + - urls + properties: + type: + type: string + enum: + - video + height: + type: number + width: + type: number + urls: + type: array + items: + anyOf: + - type: ["string", "null"] + format: "uri" + - type: ["string", "null"] + format: "uri" + PostSet: + type: object + required: + - id + - created_at + - updated_at + - creator_id + - is_public + - name + - shortname + - description + - transfer_on_delete + - post_ids + - post_count + properties: + id: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + creator_id: + type: number + is_public: + type: boolean + name: + type: string + shortname: + type: string + description: + type: string + post_count: + type: number + transfer_on_delete: + type: boolean + post_ids: + type: array + items: + type: number + PostVersion: + type: object + required: + - id + - post_id + - tags + - updater_id + - updated_at + - rating + - parent_id + - source + - description + - reason + - locked_tags + - added_tags + - removed_tags + - added_locked_tags + - removed_locked_tags + - rating_changed + - parent_changed + - source_changed + - description_changed + - version + - obsolete_added_tags + - obsolete_removed_tags + - unchanged_tags + - updater_name + properties: + id: + type: number + post_id: + type: number + tags: + type: string + updater_id: + type: number + updated_at: + type: string + format: date-time + rating: + type: string + enum: *ratings + parent_id: + type: ["number", "null"] + source: + type: string + description: + type: string + reason: + type: ["string", "null"] + locked_tags: + type: ["string", "null"] + added_tags: + type: array + items: + type: string + removed_tags: + type: array + items: + type: string + added_locked_tags: + type: array + items: + type: string + removed_locked_tags: + type: array + items: + type: string + rating_changed: + type: boolean + parent_changed: + type: boolean + source_changed: + type: boolean + description_changed: + type: boolean + version: + type: number + obsolete_added_tags: + type: string + obsolete_removed_tags: + type: string + unchanged_tags: + type: string + updater_name: + type: string + RelatedTag: + type: object + required: + - name + - category_id + properties: + name: + type: string + category_id: + type: number + enum: *tag-categories + Tag: + type: object + required: + - id + - name + - post_count + - related_tags + - related_tags_updated_at + - category + - is_locked + - created_at + - updated_at + properties: + id: + type: number + name: + type: string + post_count: + type: number + related_tags: + type: array + items: + type: string + related_tags_updated_at: + type: ["string", "null"] + format: date-time + category: + type: number + enum: *tag-categories + is_locked: + type: boolean + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + TagAlias: + type: object + required: + - id + - antecedent_name + - reason + - creator_id + - created_at + - forum_post_id + - updated_at + - forum_topic_id + - consequent_name + - status + - post_count + - approver_id + properties: + id: + type: number + antecedent_name: + type: string + reason: + type: string + creator_id: + type: number + created_at: + type: ["string", "null"] + format: date-time + forum_post_id: + type: ["number", "null"] + updated_at: + type: ["string", "null"] + format: date-time + forum_topic_id: + type: ["number", "null"] + consequent_name: + type: string + status: + type: string + description: | + Note: The "error" status will be proceeded by an error, ex: "error: Validation failed: A tag alias for tag_name already exists" + enum: *tag-request-statuses + post_count: + type: number + approver_id: + type: ["number", "null"] + TagImplication: + type: object + required: + - id + - reason + - creator_id + - created_at + - forum_post_id + - antecedent_name + - consequent_name + - status + - forum_topic_id + - updated_at + - descendant_names + - approver_id + properties: + id: + type: number + reason: + type: string + creator_id: + type: number + created_at: + type: string + format: date-time + forum_post_id: + type: ["number", "null"] + antecedent_name: + type: string + consequent_name: + type: string + status: + type: string + description: | + Note: The "error" status will be proceeded by an error, ex: "error: Validation failed: A tag alias for tag_name already exists" + enum: *tag-request-statuses + forum_topic_id: + type: ["number", "null"] + updated_at: + type: string + format: date-time + descendant_names: + type: array + items: + type: string + approver_id: + type: ["number", "null"] + TagPreview: + type: object + required: + - a + - type + - tagTypeA + properties: + a: + type: string + description: The name if type=tag, else the antecedent. + b: + type: string + description: The consequent, only present if type=alias or type=implication. + type: + type: string + enum: + - tag + - implication + - alias + tagTypeA: + type: ["number", "null"] + enum: *tag-categories + tagTypeB: + type: ["number", "null"] + enum: *tag-categories + TagTypeVersion: + type: object + required: + - id + - created_at + - updated_at + - old_type + - new_type + - is_locked + - tag_id + - creator_id + properties: + id: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + old_type: + type: number + enum: *tag-categories + new_type: + type: number + enum: *tag-categories + is_locked: + type: boolean + tag_id: + type: number + creator_id: + type: number + Takedown: + type: object + required: + - id + - status + - approver_id + - reason_hidden + - created_at + - updated_at + - post_count + properties: + id: + type: number + status: + type: string + enum: + - approved + - denied + - partial + - pending + approver_id: + type: ["number", "null"] + reason_hidden: + type: boolean + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + post_count: + type: number + Ticket: + type: object + required: + - id + - creator_id + - reason + - disp_id + - qtype + - status + - created_at + - updated_at + - response + - handler_id + - report_reason + - accused_id + properties: + id: + type: number + creator_id: + type: number + reason: + type: string + disp_id: + type: number + qtype: + type: string + enum: *ticket-types + status: + type: string + enum: *ticket-statuses + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + response: + type: string + handler_id: + type: ["number", "null"] + claimant_id: + type: ["number", "null"] + description: Only visible to Moderator+. + report_reason: + type: ["string", "null"] + accused_id: + type: ["number", "null"] + UploadWhitelist: + type: object + required: + - id + - created_at + - updated_at + - pattern + - note + - hidden + - allowed + - reason + properties: + id: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + pattern: + type: string + note: + type: string + hidden: + type: boolean + allowed: + type: boolean + reason: + type: string + Upload: + type: object + required: + - id + - source + - rating + - uploader_id + - tag_string + - status + - backtrace + - post_id + - md5_confirmation + - created_at + - updated_at + - parent_id + - md5 + - file_ext + - file_size + - image_width + - image_height + - description + - uploader_name + properties: + id: + type: number + source: + type: string + rating: + type: string + enum: *ratings + uploader_id: + type: number + tag_string: + type: string + status: + type: string + description: | + Note: The "error" status will be proceeded by an error, ex: "error: RuntimeError - No file or source URL provided" + enum: + - completed + - duplicate + - error + - processing + - pending + backtrace: + type: ["string", "null"] + post_id: + type: ["number", "null"] + md5_confirmation: + type: "null" + deprecated: true + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + parent_id: + type: ["null", "number"] + md5: + type: ["string", "null"] + file_ext: + type: ["string", "number"] + file_size: + type: ["null", "number"] + image_width: + type: ["null", "number"] + image_height: + type: ["null", "number"] + description: + type: string + uploader_name: + type: string + User: + type: object + required: + - id + - created_at + - name + - level + - base_upload_limit + - post_upload_count + - post_update_count + - note_update_count + - is_banned + - can_approve_posts + - can_upload_free + - level_string + - avatar_id + properties: + id: + type: number + created_at: + type: string + format: date-time + name: + type: string + level: + type: number + base_upload_limit: + type: number + post_upload_count: + type: number + post_update_count: + type: number + note_update_count: + type: number + is_banned: + type: boolean + can_approve_posts: + type: boolean + can_upload_free: + type: boolean + level_string: + type: string + avatar_id: + type: ["number", "null"] + UserFeedback: + type: object + required: + - id + - user_id + - creator_id + - category + - body + - created_at + - updated_at + - updater_id + - is_deleted + properties: + id: + type: number + user_id: + type: number + creator_id: + type: number + category: + type: string + enum: *feedback-categories + body: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + updater_id: + type: number + is_deleted: + type: boolean + UserNameChangeRequest: + type: object + required: + - id + - approver_id + - user_id + - original_name + - desired_name + - created_at + - updated_at + - status + properties: + id: + type: number + approver_id: + type: number + user_id: + type: number + original_name: + type: string + desired_name: + type: string + change_reason: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + status: + type: string + enum: + - approved + WikiPage: + type: object + required: + - id + - created_at + - updated_at + - title + - body + - creator_id + - is_locked + - updater_id + - is_deleted + - other_names + - parent + - creator_name + - category_id + properties: + id: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + title: + type: string + body: + type: string + creator_id: + type: number + is_locked: + type: boolean + updater_id: + type: number + is_deleted: + type: boolean + other_names: + type: array + items: + type: string + parent: + type: ["string", "null"] + creator_name: + type: string + category_id: + type: number + enum: *tag-categories + WikiPageVersion: + type: object + required: + - id + - created_at + - updated_at + - title + - body + - updater_id + - wiki_page_id + - is_locked + - other_names + - is_deleted + - reason + - parent + properties: + id: + type: number + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + title: + type: string + body: + type: string + updater_id: + type: number + wiki_page_id: + type: number + is_locked: + type: boolean + other_names: + type: array + items: + type: string + is_deleted: + type: boolean + reason: + type: ["string", "null"] + parent: + type: ["string", "null"] + responses: + NotFound: + description: Not Found + content: + application/json: + schema: + type: object + required: + - success + - reason + properties: + success: + type: boolean + enum: + - false + reason: + type: string + enum: + - "not found" + AccessDenied: + description: Access Denied + content: + application/json: + schema: + type: object + required: + - success + - reason + properties: + success: + type: boolean + enum: + - false + reason: + type: string + enum: + - "Access Denied" + ExpectedError: + description: Invalid Input Data + content: + application/json: + schema: + type: object + required: + - errors + examples: + - { "errors": { "key": [ "the error" ] } } + properties: + errors: + type: object + patternProperties: + "^[a-z]+$": + type: array + items: + type: string + MessageError: + description: Error + content: + application/json: + schema: + type: object + required: + - success + - message + properties: + success: + type: boolean + enum: + - false + message: + type: string + code: + type: ["string", "null"] + requestBodies: + warning: + content: + application/json: + schema: + type: object + required: + - record_type + properties: + record_type: + type: string + enum: + - unmark + - ban + - record + - warning + +paths: + # Artists + /artists.json: + get: + summary: Search Artists + operationId: searchArtists + tags: + - Artists + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - updated_at + - name + - post_count + - name: search[name] + in: query + schema: + type: string + - name: search[group_name] + in: query + schema: + type: string + - name: search[any_other_name_like] + in: query + schema: + type: string + - name: search[any_name_matches] + in: query + schema: + type: string + - name: search[any_name_or_url_matches] + in: query + schema: + type: string + - name: search[url_matches] + in: query + schema: + type: string + - name: search[creator_name] + in: query + schema: + type: string + - name: search[creator_id] + in: query + schema: + type: string + - name: search[has_tag] + in: query + schema: + type: string + - name: search[is_linked] + in: query + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + allOf: + - $ref: "#/components/schemas/Artist" + - type: object + required: + - domains + - urls + properties: + domains: + type: array + example: [["e621.net", 1]] + items: + type: array + items: + anyOf: + - type: string + - type: number + urls: + type: array + items: + $ref: "#/components/schemas/ArtistURL" + post: + summary: Create Artist + operationId: createArtist + tags: + - Artists + security: + - basicAuth: [] + description: | + `other_names` & `urls` are silently truncated to 25 entries. + `notes` is silently truncated to the wiki page limit (250,000). + Individual `other_names` are silently truncated to 100 characters. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - artist[name] + properties: + artist[name]: + type: string + artist[other_names]: + type: array + items: + type: string + artist[other_names_string]: + type: string + artist[url_string]: + type: string + artist[notes]: + type: string + artist[group_name]: + type: string + artist[linked_user_id]: + type: ["number", "null"] + description: Only usable for Janitor+ + artist[is_locked]: + type: boolean + description: Only usable for Janitor+ + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Artist" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /artists/{idOrName}.json: + get: + summary: Get Artist + operationId: getArtist + tags: + - Artists + parameters: + - name: idOrName + in: path + required: true + description: The ID or name of the artist to get. + schema: + type: ["number", "string"] + responses: + 200: + description: Success + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Artist" + - type: object + required: + - domains + - urls + properties: + domains: + type: array + items: + type: array + example: [["e621.net", 1]] + items: + anyOf: + - type: string + - type: number + urls: + type: array + items: + $ref: "#/components/schemas/ArtistURL" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Artist + operationId: editArtist + tags: + - Artists + security: + - basicAuth: [] + description: | + If an artist is locked, you must be Janitor+ to edit them. + `other_names` & `urls` are silently truncated to 25 entries. + `notes` is silently truncated to the wiki page limit (250,000). + Individual `other_names` are silently truncated to 100 characters. + If an artist is on the avoid posting list, you must have the bd staff user flag to edit name, other_names, or group_name. + parameters: + - name: idOrName + in: path + required: true + description: The ID or name of the artist to edit. + schema: + type: ["number", "string"] + requestBody: + content: + application/c-www-form-urlencoded: + schema: + type: object + properties: + artist[name]: + type: string + description: Only usable for Janitor+ + artist[other_names]: + type: array + items: + type: string + artist[other_names_string]: + type: string + artist[url_string]: + type: string + artist[notes]: + type: string + artist[group_name]: + type: string + artist[linked_user_id]: + type: ["number", "null"] + description: Only usable for Janitor+ + artist[is_locked]: + type: boolean + description: Only usable for Janitor+ + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete Artist + operationId: deleteArtist + tags: + - Artists + security: + - basicAuth: [] + description: | + You must be an Admin+ to delete an artist. + parameters: + - name: idOrName + in: path + required: true + description: The ID or name of the artist to edit. + schema: + type: ["number", "string"] + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /artists/{idOrName}/revert.json: + put: + summary: Revert Artist + operationId: revertArtist + tags: + - Artists + security: + - basicAuth: [] + description: | + If an artist is locked, you must be Janitor+ to revert them. + parameters: + - name: idOrName + in: path + required: true + description: The ID or name of the artist to revert. + schema: + type: ["number", "string"] + - name: version_id + in: query + required: true + description: The version ID to revert to. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + # Artist Versions + /artist_versions.json: + get: + summary: Search Artist Versions + operationId: searchArtistVersions + tags: + - Artist Versions + description: When no results are found, an object with an `artist_versions` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/ip_addr" + - name: search[order] + in: query + description: The order of the results. + schema: + type: string + enum: + - id_asc + - id_desc + - name + - name: search[name] + in: query + description: The name of the artist. + schema: + type: string + - name: search[artist_id] + in: query + description: The id of the artist. + schema: + type: string + - name: search[updater_name] + in: query + description: The name of the user that updated the artist. + schema: + type: string + - name: search[updater_id] + in: query + description: The id of the user that updated the artist. + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/ArtistVersion" + - type: object + description: No Results + required: + - artist_versions + properties: + artist_versions: + type: array + maxItems: 0 + # Artist URLs + /artist_urls.json: + get: + summary: Search Artist URLs + operationId: searchArtistUrls + tags: + - Artist URLs + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + description: The order of the results. + schema: + type: string + enum: + - id + - id_asc + - id_desc + - artist_id + - artist_id_asc + - artist_id_desc + - url + - url_asc + - url_desc + - normalized_url + - normalized_url_asc + - normalized_url_desc + - created_at + - created_at_asc + - created_at_desc + - updated_at + - updated_at_asc + - updated_at_desc + - name: search[artist_name] + in: query + description: The name of the artist. + schema: + type: string + - name: search[artist_id] + in: query + description: The id of the artist. + schema: + type: string + - name: search[is_active] + in: query + description: If the artist url is active. + schema: + type: boolean + - name: search[url] + in: query + description: The url. + schema: + type: string + - name: search[normalized_url] + in: query + description: The normalized url. (http:, trailing `/`) + schema: + type: string + - name: search[artist] + in: query + description: Legacy nested search for artist. Supports the same parameters as /artists.json. + deprecated: true + schema: + type: object + - name: search[url_matches] + in: query + description: Legacy name for `search[url]`. + deprecated: true + schema: + type: string + - name: search[normalized_url_matches] + in: query + description: Legacy name for `search[normalized_url]`. + deprecated: true + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + allOf: + - $ref: "#/components/schemas/ArtistURL" + - type: object + required: + - artist + properties: + artist: + $ref: "#/components/schemas/Artist" + # Avoid Posting Entries + /avoid_postings.json: + get: + summary: Search Avoid Posting Entries + operationId: searchAvoidPostings + tags: + - Avoid Posting Entries + description: When no results are found, an object with an `avoid_postings` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/ip_addr" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - name: search[creator_name] + in: query + schema: + type: string + - name: search[creator_id] + in: query + schema: + type: string + - name: search[any_name_matches] + in: query + description: Any name matching. + schema: + type: string + - name: search[artist_name] + in: query + description: The artist name of the avoid posting entry. + schema: + type: string + - name: search[artist_id] + in: query + description: The artist id for the avoid posting entry. + schema: + type: string + - name: search[any_other_name_matches] + in: query + description: Any other name matching. + schema: + type: string + - name: search[details] + in: query + description: The details of the avoid posting entry. + schema: + type: string + - name: search[staff_notes] + in: query + description: The staff notes on the avoid posting entry. Must be Janitor+ to use. + schema: + type: string + - name: search[is_active] + in: query + description: If the avoid posting entry is active. + schema: + type: boolean + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/AvoidPosting" + - type: object + description: No Results + required: + - avoid_postings + properties: + avoid_postings: + type: array + maxItems: 0 + post: + summary: Create Avoid Posting Entry + operationId: createAvoidPosting + tags: + - Avoid Posting Entries + security: + - basicAuth: [] + description: Must have the bd staff user flag. When no results are found, an object with an `avoid_posting_versions` key is returned. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + avoid_posting[details]: + type: string + avoid_posting[staff_notes]: + type: string + avoid_posting[is_active]: + type: boolean + avoid_posting[artist_attributes][id]: + type: number + avoid_posting[artist_attributes][name]: + type: string + description: If provided and the artist does not exist, an artist will be created. + avoid_posting[artist_attributes][other_names_string]: + type: string + avoid_posting[artist_attributes][other_names]: + type: array + items: + type: string + avoid_posting[artist_attributes][group_name]: + type: string + avoid_posting[artist_attributes][linked_user_id]: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/AvoidPosting" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /avoid_postings/{idOrArtistName}.json: + get: + summary: Get Avoid Posting Entry + operationId: getAvoidPosting + tags: + - Avoid Posting Entries + parameters: + - name: idOrArtistName + in: path + required: true + description: The ID of the avoid posting entry, or the name of the artist. + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/AvoidPosting" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Avoid Posting Entry + operationId: editAvoidPosting + tags: + - Avoid Posting Entries + security: + - basicAuth: [] + description: Must have the bd staff user flag. + parameters: + - name: idOrArtistName + in: path + required: true + description: The ID of the avoid posting entry, or the name of the artist. + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + avoid_posting[details]: + type: string + avoid_posting[staff_notes]: + type: string + avoid_posting[is_active]: + type: boolean + avoid_posting[artist_attributes][name]: + type: string + avoid_posting[artist_attributes][other_names_string]: + type: string + avoid_posting[artist_attributes][other_names]: + type: array + items: + type: string + avoid_posting[artist_attributes][group_name]: + type: string + avoid_posting[artist_attributes][linked_user_id]: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/AvoidPosting" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Destroy Avoid Posting Entry + operationId: destroyAvoidPosting + tags: + - Avoid Posting Entries + security: + - basicAuth: [] + description: Must have the bd staff user flag. + parameters: + - name: idOrArtistName + in: path + required: true + description: The ID of the avoid posting entry, or the name of the artist. + schema: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /avoid_postings/{idOrArtistName}/delete.json: + put: + summary: Delete Avoid Posting Entry + operationId: deleteAvoidPosting + tags: + - Avoid Posting Entries + security: + - basicAuth: [] + description: Must have the bd staff user flag. + parameters: + - name: idOrArtistName + in: path + required: true + description: The ID of the avoid posting entry, or the name of the artist. + schema: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /avoid_postings/{idOrArtistName}/undelete.json: + put: + summary: Undelete Avoid Posting Entry + operationId: undeleteAvoidPosting + tags: + - Avoid Posting Entries + security: + - basicAuth: [] + description: Must have the bd staff user flag. + parameters: + - name: idOrArtistName + in: path + required: true + description: The ID of the avoid posting entry, or the name of the artist. + schema: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # Avoid Posting Versions + /avoid_posting_versions.json: + get: + summary: Search Avoid Posting Versions + operationId: searchAvoidPostingVersions + tags: + - Avoid Posting Versions + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/ip_addr" + - name: search[order] + in: query + description: The order of the results. + schema: + type: string + enum: + - id_asc + - id_desc + - name: search[updater_name] + in: query + description: The name of the updater of the avoid posting entry. + schema: + type: string + - name: search[updater_id] + in: query + description: The ID of the updater of the avoid posting entry. + schema: + type: string + - name: search[any_name_matches] + in: query + description: Any name matching. + schema: + type: string + - name: search[artist_name] + in: query + description: The artist name of the avoid posting entry. + schema: + type: string + - name: search[artist_id] + in: query + description: The artist id for the avoid posting entry. + schema: + type: string + - name: search[any_other_name_matches] + in: query + description: Any other name matching. + schema: + type: string + - name: search[group_name] + in: query + description: Any other name matching. + schema: + type: string + - name: search[is_active] + in: query + description: If the avoid posting entry is active. + schema: + type: boolean + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/AvoidPostingVersion" + - type: object + description: No Results + required: + - avoid_posting_versions + properties: + avoid_posting_versions: + type: array + maxItems: 0 + # Bans + /bans.json: + get: + summary: Search Bans + operationId: searchBans + tags: + - Bans + description: When no results are found, an object with an `bans` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + description: The order of the results. + schema: + type: string + enum: + - id_asc + - id_desc + - expires_at_desc + - name: search[banner_id] + in: query + description: The ID of the banner. + schema: + type: string + - name: search[banner_name] + in: query + description: The name of banner. + schema: + type: string + - name: search[user_id] + in: query + description: The ID of the banned user. + schema: + type: string + - name: search[user_name] + in: query + description: The name of the banned user. + schema: + type: string + - name: search[reason_matches] + in: query + description: The reason of the ban. + schema: + type: string + - name: search[expired] + in: query + description: If the ban is expired. + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/Ban" + - type: object + description: No Results + required: + - bans + properties: + bans: + type: array + maxItems: 0 + /bans/{id}.json: + get: + summary: Get Ban + operationId: getBan + tags: + - Bans + parameters: + - name: id + in: path + description: The ID of the ban to get. + required: true + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Ban" + 404: + $ref: "#/components/responses/NotFound" + # Blips + /blips.json: + get: + summary: Search Blips + operationId: searchBlips + tags: + - Blips + description: When no results are found, an object with an `blips` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/ip_addr" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - updated_at + - updated_at_desc + - name: search[creator_id] + in: query + schema: + type: number + - name: search[creator_name] + in: query + schema: + type: string + - name: search[body_matches] + in: query + schema: + type: string + - name: search[response_to] + in: query + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/Blip" + - type: object + description: No Results + required: + - blips + properties: + blips: + type: array + maxItems: 0 + post: + summary: Create Blip + operationId: createBlip + tags: + - Blips + security: + - basicAuth: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - blip[body] + properties: + blip[body]: + type: string + blip[response_to]: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Blip" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /blips/{id}.json: + get: + summary: Get Blip + operationId: getBlip + tags: + - Blips + parameters: + - name: id + in: path + description: The ID of the blip to get. + required: true + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Blip" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Blip + operationId: editBlip + tags: + - Blips + security: + - basicAuth: [] + description: Unless Admin+, blips cannot be edited after 5 minutes. Marked blips cannot be edited. + parameters: + - name: id + in: path + description: The ID of the blip. + required: true + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + blip[body]: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Blip" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete Blip + operationId: deleteBlip + tags: + - Blips + security: + - basicAuth: [] + description: | + You must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the blip. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /blips/{id}/hide.json: + post: + summary: Hide Blip + operationId: hideBlip + tags: + - Blips + security: + - basicAuth: [] + description: | + You must be the creator or Moderator+. + parameters: + - name: id + in: path + required: true + description: The ID of the blip. + schema: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Blip" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /blips/{id}/unhide.json: + post: + summary: Unhide Blip + operationId: unhideBlip + tags: + - Blips + security: + - basicAuth: [] + description: | + You must be Moderator+. + parameters: + - name: id + in: path + required: true + description: The ID of the blip. + schema: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Blip" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /blips/{id}/warning.json: + post: + summary: Mark Blip + operationId: markBlip + tags: + - Blips + security: + - basicAuth: [] + description: You must be Moderator+. + parameters: + - name: id + in: path + required: true + description: The ID of the blip. + schema: + type: number + requestBody: + $ref: "#/components/requestBodies/warning" + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/DTextResponse" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # Bulk Update Requests + /bulk_update_requests.json: + get: + summary: Search Bulk Update Requests + operationId: searchBulkUpdateRequests + tags: + - Bulk Update Requests + description: When no results are found, an object with an `bulk_update_requests` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - status_desc + - updated_at_desc + - updated_at_asc + - name: search[user_id] + in: query + schema: + type: number + - name: search[user_name] + in: query + schema: + type: string + - name: search[approver_id] + in: query + schema: + type: number + - name: search[approver_name] + in: query + schema: + type: string + - name: search[forum_topic_id] + in: query + schema: + type: number + - name: search[forum_post_id] + in: query + schema: + type: number + - name: search[status] + in: query + schema: + type: string + description: Multiple can be specified via comma separating. + enum: + - approved + - rejected + - pending + - name: search[title_matches] + in: query + schema: + type: string + - name: search[script_matches] + in: query + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/BulkUpdateRequest" + - type: object + description: No Results + required: + - bulk_update_requests + properties: + bulk_update_requests: + type: array + maxItems: 0 + post: + summary: Create Bulk Update Request + operationId: createBulkUpdateRequest + tags: + - Bulk Update Requests + security: + - basicAuth: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - bulk_update_request[script] + - bulk_update_request[title] + - bulk_update_request[reason] + properties: + bulk_update_request[script]: + type: string + bulk_update_request[title]: + type: string + bulk_update_request[reason]: + type: string + bulk_update_request[forum_topic_id]: + type: number + bulk_update_request[skip_forum]: + type: boolean + description: Only usable for Admin+ + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/BulkUpdateRequest" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /bulk_update_request/{id}.json: + get: + summary: Get Bulk Update Request + operationId: getBulkUpdateRequest + tags: + - Bulk Update Requests + parameters: + - name: id + in: path + required: true + description: The ID of the bulk update request. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/BulkUpdateRequest" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Bulk Update Request + operationId: editBulkUpdateRequest + tags: + - Bulk Update Requests + security: + - basicAuth: [] + description: You must be the creator of the BUR, or Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the bulk update request. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + bulk_update_request[script]: + type: string + bulk_update_request[forum_topic_id]: + description: You must be Admin+. + type: string + bulk_update_request[forum_post_id]: + description: You must be Admin+. + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Reject Bulk Update Request + operationId: rejectBulkUpdateRequest + tags: + - Bulk Update Requests + security: + - basicAuth: [] + description: You must be the creator of the BUR, or Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the bulk update request. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /bulk_update_request/{id}/approve.json: + post: + summary: Approve Bulk Update Request + operationId: approveBulkUpdateRequest + tags: + - Bulk Update Requests + description: Must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the bulk update request. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # Comments + /comments.json: + get: + summary: Search Comments + operationId: searchComments + tags: + - Comments + description: For searching comments, group_by=comment must be set. When no results are found, an object with an `comments` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/ip_addr" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - status + - status_desc + - updated_at_desc + - in: query + name: group_by + schema: + type: string + enum: + - comment + - post + - in: query + name: search[body_matches] + schema: + type: string + - in: query + name: search[post_id] + description: Accepts a comma separated list. + schema: + type: number + - in: query + name: search[post_tags_match] + schema: + type: string + - in: query + name: search[post_note_updater_name] + schema: + type: string + - in: query + name: search[post_note_updater_id] + schema: + type: number + - in: query + name: search[creator_name] + schema: + type: string + - in: query + name: search[creator_id] + schema: + type: number + - in: query + name: search[is_sticky] + schema: + type: boolean + - in: query + name: search[is_hidden] + description: Only usable by Moderator+ + schema: + type: boolean + - in: query + name: search[do_not_bump_post] + schema: + type: boolean + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/Comment" + - type: object + description: No Results + required: + - comments + properties: + comments: + type: array + maxItems: 0 + post: + summary: Create Comment + operationId: createComment + tags: + - Comments + security: + - basicAuth: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - comment[body] + - comment[post_id] + properties: + comment[body]: + type: string + comment[post_id]: + type: number + comment[do_not_bump_post]: + type: boolean + comment[is_sticky]: + type: boolean + description: Only usable for Janitor+ + comment[is_hidden]: + type: boolean + description: Only usable for Moderator+ + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Comment" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /comments/{id}.json: + get: + summary: Get Comment + operationId: getComment + tags: + - Comments + description: If the comment is hidden, you must be the creator or Moderator+ to see it. + parameters: + - name: id + in: path + required: true + description: The ID of the comment. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Comment" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Comment + operationId: editComment + tags: + - Comments + security: + - basicAuth: [] + description: | + You must be the creator of the comment, or Admin+ to edit. Marked comments cannot be edited. + parameters: + - name: id + in: path + required: true + description: The ID of the comment. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + comment[body]: + type: string + comment[is_sticky]: + type: boolean + description: Only usable for Janitor+ + comment[is_hidden]: + type: boolean + description: Only usable for Moderator+ + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete Comment + operationId: deleteComment + tags: + - Comments + security: + - basicAuth: [] + description: | + You must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the comment. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /comments/{id}/hide.json: + post: + summary: Hide Comment + operationId: hideComment + tags: + - Comments + security: + - basicAuth: [] + description: | + You must be the creator or Moderator+. + parameters: + - name: id + in: path + required: true + description: The ID of the comment. + schema: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Comment" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /comments/{id}/unhide.json: + post: + summary: Unhide Comment + operationId: unhideComment + tags: + - Comments + security: + - basicAuth: [] + description: | + You must be Moderator+. + parameters: + - name: id + in: path + required: true + description: The ID of the comment. + schema: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Comment" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /comments/{id}/warning.json: + post: + summary: Mark Comment + operationId: markComment + tags: + - Comments + security: + - basicAuth: [] + description: You must be Moderator+. + parameters: + - name: id + in: path + required: true + description: The ID of the comment. + schema: + type: number + requestBody: + $ref: "#/components/requestBodies/warning" + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/DTextResponse" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # Comment Votes + /comments/{id}/votes.json: + post: + summary: Create Comment Vote + operationId: createCommentVote + tags: + - Comment Votes + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the comment. + schema: + type: string + - name: score + in: query + required: true + schema: + type: number + enum: + - -1 + - 1 + - name: no_unvote + in: query + schema: + type: boolean + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - score + - our_score + properties: + score: + type: number + our_score: + type: number + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete Comment Vote + operationId: deleteCommentVote + tags: + - Comment Votes + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the comment. + schema: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /comment_votes/lock.json: + post: + summary: Lock Comment Votes + operationId: lockCommentVotes + tags: + - Comment Votes + security: + - basicAuth: [] + description: | + You must be Moderator+. Errors if ids is not provided. + parameters: + - name: ids + in: query + required: true + description: The IDs of the comment votes, comma separated. + schema: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /comment_votes/delete.json: + post: + summary: Delete Comment Vote + operationId: deleteCommentVotes + tags: + - Comment Votes + security: + - basicAuth: [] + description: | + You must be Admin+. Errors if ids is not provided. + parameters: + - name: ids + in: query + required: true + description: The IDs of the comment votes, comma separated. + schema: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + # DMails + /dmails.json: + get: + summary: Search DMails + operationId: searchDMails + tags: + - DMails + description: When no results are found, an object with a `dmails` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - in: query + name: search[title_matches] + schema: + type: string + - in: query + name: search[message_matches] + schema: + type: string + - in: query + name: search[to_name] + schema: + type: string + - in: query + name: search[to_id] + schema: + type: number + - in: query + name: search[from_name] + schema: + type: string + - in: query + name: search[from_id] + schema: + type: number + - in: query + name: search[is_read] + schema: + type: boolean + - in: query + name: search[is_deleted] + schema: + type: boolean + - in: query + name: search[read] + schema: + type: boolean + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/DMail" + - type: object + description: No Results + required: + - dmails + properties: + dmails: + type: array + maxItems: 0 + /dmails/{id}.json: + get: + summary: Get DMail + operationId: getDMail + tags: + - DMails + description: Fetching a dmail will not mark it as read. + parameters: + - name: id + in: path + required: true + description: The ID of the dmail. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/DMail" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + delete: + summary: Delete DMail + operationId: deleteDMail + tags: + - DMails + security: + - basicAuth: [] + description: Deleting simply hides your copy of the dmail. + parameters: + - name: id + in: path + required: true + description: The ID of the dmail. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /dmails/{id}/mark_as_read.json: + put: + summary: Mark DMail As Read + operationId: markDMailAsRead + tags: + - DMails + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the dmail. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /dmails/{id}/mark_as_unread.json: + put: + summary: Mark DMail As Unread + operationId: markDMailAsUnread + tags: + - DMails + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the dmail. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /dmails/mark_all_as_unread.json: + put: + summary: Mark All DMails As Unread + operationId: markAllDMailsAsUnread + tags: + - DMails + security: + - basicAuth: [] + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # DText Preview + /dtext_preview.json: + post: + summary: Preview DText + operationId: previewDText + tags: + - DText + description: Note while this route does not require auth, without auth it requires a CSRF token. For that reason it has been marked as requiring auth. + security: + - basicAuth: [] + requestBody: + content: + application/json: + schema: + type: object + required: + - body + properties: + body: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/DTextResponse" + # Email Blacklists + /email_blacklists.json: + get: + summary: Search Email Blacklists + operationId: searchEmailBlacklists + tags: + - Email Blacklists + description: You must be Admin+. When no results are found, an object with an `email_blacklists` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - reason + - domain + - in: query + name: search[domain] + schema: + type: string + - in: query + name: search[reason] + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/EmailBlacklist" + - type: object + description: No Results + required: + - email_blacklists + properties: + email_blacklists: + type: array + maxItems: 0 + post: + summary: Create Email Blacklist + operationId: createEmailBlacklist + tags: + - Email Blacklists + description: You must be Admin+. + security: + - basicAuth: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - email_blacklist[domain] + - email_blacklist[reason] + properties: + email_blacklist[domain]: + type: string + email_blacklist[reason]: + type: string + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/EmailBlacklist" + 422: + $ref: "#/components/responses/ExpectedError" + /email_blacklists/{id}.json: + delete: + summary: Delete Email Blacklist + operationId: deleteEmailBlacklist + tags: + - Email Blacklists + security: + - basicAuth: [] + description: You must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the email blacklist. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # Favorites + /favorites.json: + get: + summary: List Favorites + operationId: listFavorites + tags: + - Favorites + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - in: query + name: user_id + description: You must be the user or Moderator+ if the user has their favorites hidden. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - posts + properties: + posts: + type: array + items: + $ref: "#/components/schemas/Post" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + post: + summary: Add Favorite + operationId: addFavorite + tags: + - Favorites + security: + - basicAuth: [] + requestBody: + content: + application/json: + schema: + type: object + required: + - post_id + properties: + post_ud: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + type: object + required: + - post + properties: + post: + $ref: "#/components/schemas/Post" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /favorites/{id}.json: + delete: + summary: Remove Favorite + operationId: removeFavorite + tags: + - Favorites + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + # Forum Post + /forum_posts.json: + get: + summary: Search Forum Posts + operationId: searchForumPosts + tags: + - Forum Posts + description: When no results are found, an object with an `forum_posts` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - in: query + name: search[creator_id] + schema: + type: number + - in: query + name: search[creator_name] + schema: + type: string + - in: query + name: search[topic_id] + schema: + type: number + - in: query + name: search[topic_title_matches] + schema: + type: string + - in: query + name: search[body_matches] + schema: + type: string + - in: query + name: search[topic_category_id] + schema: + type: number + - in: query + name: search[is_hidden] + schema: + type: boolean + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/ForumPost" + - type: object + description: No Results + required: + - forum_posts + properties: + forum_posts: + type: array + maxItems: 0 + post: + summary: Create Forum Post + operationId: createForumPost + tags: + - Forum Posts + security: + - basicAuth: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - forum_post[body] + - forum_post[topic_id] + properties: + forum_post[body]: + type: string + forum_post[topic_id]: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/ForumPost" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /forum_posts/{id}.json: + get: + summary: Get Forum Post + operationId: getForumPost + tags: + - Forum Posts + description: If the forum post is hidden, you must be the creator or Moderator+ to see it. + parameters: + - name: id + in: path + required: true + description: The ID of the forum post. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/ForumPost" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Forum Post + operationId: editForumPost + tags: + - Forum Posts + security: + - basicAuth: [] + description: You must be the creator of the forum post, or Admin+ to edit. Marked forum posts cannot be edited. + parameters: + - name: id + in: path + required: true + description: The ID of the forum post. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + forum_post[body]: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete Forum Post + operationId: deleteForumPost + tags: + - Forum Posts + security: + - basicAuth: [] + description: You must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the forum post. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /forum_posts/{id}/hide.json: + post: + summary: Hide Forum Post + operationId: hideForumPost + tags: + - Forum Posts + security: + - basicAuth: [] + description: You must be the creator or Moderator+. + parameters: + - name: id + in: path + required: true + description: The ID of the forum post. + schema: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/ForumPost" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /forum_posts/{id}/unhide.json: + post: + summary: Unhide Forum Post + operationId: unhideForumPost + tags: + - Forum Posts + security: + - basicAuth: [] + description: You must be Moderator+. + parameters: + - name: id + in: path + required: true + description: The ID of the forum post. + schema: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/ForumPost" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /forum_posts/{id}/warning.json: + post: + summary: Mark Forum Post + operationId: markForumPost + tags: + - Forum Posts + security: + - basicAuth: [] + description: | + You must be Moderator+. + parameters: + - name: id + in: path + required: true + description: The ID of the forum post. + schema: + type: number + requestBody: + $ref: "#/components/requestBodies/warning" + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/DTextResponse" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # Forum Post Votes + /forum_posts/{id}/votes.json: + post: + summary: Create Forum Post Vote + operationId: createForumPostVote + tags: + - Forum Post Votes + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the forum post. + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - forum_post_vote[score] + properties: + forum_post_vote[score]: + type: number + enum: + - -1 + - 0 + - 1 + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/ForumPostVote" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete Forum Post Vote + operationId: deleteForumPostVote + tags: + - Forum Post Votes + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the forum post. + schema: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + # Forum Topics + /forum_topics.json: + get: + summary: Search Forum Topics + operationId: searchForumTopics + tags: + - Forum Topics + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - sticky + - in: query + name: search[title] + schema: + type: string + - in: query + name: search[title_matches] + schema: + type: string + - in: query + name: search[category_id] + schema: + type: number + - in: query + name: search[is_sticky] + schema: + type: boolean + - in: query + name: search[is_locked] + schema: + type: boolean + - in: query + name: search[is_hidden] + schema: + type: boolean + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ForumTopic" + post: + summary: Create Forum Topic + operationId: createForumTopic + tags: + - Forum Topics + security: + - basicAuth: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - forum_topic[title] + - forum_topic[category_id] + properties: + forum_topic[title]: + type: string + forum_topic[category_id]: + type: number + forum_topic[original_post_attributes][id]: + type: number + description: Forum post ID. Mutually exclusive with body, one must be provided. + forum_topic[original_post_attributes][body]: + type: string + description: First forum post body. Mutually exclusive with id, one must be provided. + forum_topic[is_sticky]: + type: boolean + description: You must be Moderator+. + forum_topic[is_locked]: + type: boolean + description: You must be Moderator+. + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/ForumTopic" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /forum_topics/{id}.json: + get: + summary: Get Forum Forum Topic + operationId: getForumTopic + tags: + - Forum Topics + description: If the forum topic is hidden, you must be the creator or Moderator+ to see it. + parameters: + - name: id + in: path + required: true + description: The ID of the forum topic. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/ForumTopic" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Forum Topic + operationId: editForumTopic + tags: + - Forum Topics + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the forum topic. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + forum_topic[title]: + type: string + forum_topic[category_id]: + type: number + forum_topic[original_post_attributes][id]: + type: number + description: Forum post ID. Silently ignored + forum_topic[original_post_attributes][body]: + type: string + description: First forum post body. + forum_topic[is_sticky]: + type: boolean + description: You must be Moderator+. + forum_topic[is_locked]: + type: boolean + description: You must be Moderator+. + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete Forum Topic + operationId: deleteForumTopic + tags: + - Forum Topics + security: + - basicAuth: [] + description: You must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the forum topic. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /forum_topics/{id}/hide.json: + post: + summary: Hide Forum Topic + operationId: hideForumTopic + tags: + - Forum Topics + security: + - basicAuth: [] + description: You must be the creator or Moderator+. + parameters: + - name: id + in: path + required: true + description: The ID of the forum topic. + schema: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/ForumTopic" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /forum_topics/{id}/unhide.json: + post: + summary: Unhide Forum Topic + operationId: unhideForumTopic + tags: + - Forum Topics + security: + - basicAuth: [] + description: You must be Moderator+. + parameters: + - name: id + in: path + required: true + description: The ID of the forum topic. + schema: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/ForumTopic" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /forum_topics/{id}/subscribe.json: + post: + summary: Subscribe To Forum Topic + operationId: subscribeForumTopic + tags: + - Forum Topics + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the forum topic. + schema: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/ForumTopic" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /forum_topics/{id}/unsubscribe.json: + post: + summary: Unsubscribe From Forum Topic + operationId: unsubscribeForumTopic + tags: + - Forum Topics + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the forum topic. + schema: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/ForumTopic" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /forum_topics/mark_all_as_read.json: + post: + summary: Mark All Forum Topics As Read + operationId: markAllForumTopicsAsRead + tags: + - Forum Topics + security: + - basicAuth: [] + requestBody: + content: + application/json: + schema: + type: object + required: + - category_id + properties: + category_id: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + # Help Pages + /help.json: + get: + summary: List Help Pages + operationId: listHelpPages + description: Will error if no help pages exist. + tags: + - Help Pages + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Help" + post: + summary: Create Help Page + operationId: createHelpPage + tags: + - Help Pages + security: + - basicAuth: [] + description: You must be Admin+. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - help_page[name] + - help_page[wiki_page] + properties: + help_page[name]: + type: string + help_page[wiki_page]: + type: string + help_page[related]: + type: string + description: Separate with a comma followed by a space. + help_page[title]: + type: string + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Help" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /help/{id}.json: + get: + summary: Get Help Page + operationId: getHelpPage + tags: + - Help Pages + parameters: + - name: id + in: path + required: true + description: The ID or name of the help page. + schema: + type: ["string", "number"] + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Help" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Help Page + operationId: editHelpPage + tags: + - Help Pages + security: + - basicAuth: [] + description: You must be Admin+ + parameters: + - name: id + in: path + required: true + description: The ID of the help page. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + help_page[name]: + type: string + help_page[wiki_page]: + type: string + help_page[related]: + type: string + description: Separate with a comma followed by a space. + help_page[title]: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete Help Page + operationId: deleteHelpPage + tags: + - Help Pages + security: + - basicAuth: [] + description: You must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the help page. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # IP Bans + /ip_bans.json: + get: + summary: List IP Bans + operationId: listIPBans + tags: + - IP Bans + security: + - basicAuth: [] + description: You must be Admin+. When no results are found, an object with an `ip_bans` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/ip_addr" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - in: query + name: search[banner_id] + schema: + type: number + - in: query + name: search[banner_name] + schema: + type: string + - in: query + name: search[reason] + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/IPBan" + - type: object + description: No Results + required: + - ip_bans + properties: + ip_bans: + type: array + maxItems: 0 + post: + summary: Create IP Ban + operationId: createIPBan + tags: + - IP Bans + security: + - basicAuth: [] + description: You must be Admin+. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - ip_ban[ip_addr] + - ip_ban[reason] + properties: + ip_ban[ip_addr]: + type: string + ip_ban[reason]: + type: string + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/IPBan" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /ip_bans/{id}.json: + delete: + summary: Delete IP Ban + operationId: deleteIPBan + tags: + - IP Bans + security: + - basicAuth: [] + description: You must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the ip ban. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # IQDB + /iqdb_queries.json: + get: + summary: Query IQDB + operationId: queryIQDBGet + tags: + - IQDB + parameters: + - name: search[score_cutoff] + in: query + schema: + type: number + - name: search[url] + in: query + schema: + type: string + - name: search[post_id] + in: query + schema: + type: number + - name: search[hash] + in: query + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - posts + properties: + posts: + type: array + items: + $ref: "#/components/schemas/IQDBResponse" + 403: + $ref: "#/components/responses/AccessDenied" + post: + summary: Query IQDB + operationId: queryIQDPost + tags: + - IQDB + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + search[file]: + type: string + format: binary + search[score_cutoff]: + type: number + search[url]: + type: string + search[post_id]: + type: string + search[hash]: + type: string + application/json: + schema: + type: object + properties: + score_cutoff: + type: number + url: + type: string + post_id: + type: string + hash: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - posts + properties: + posts: + type: array + items: + $ref: "#/components/schemas/IQDBResponse" + 403: + $ref: "#/components/responses/AccessDenied" + # Mascots + /mascots.json: + get: + summary: Search Mascots + operationId: searchMascots + tags: + - Mascots + description: When no results are found, an object with an `mascots` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/Mascot" + - type: object + description: No Results + required: + - mascots + properties: + mascots: + type: array + maxItems: 0 + post: + summary: Create Mascot + operationId: createMascot + tags: + - Mascots + security: + - basicAuth: [] + description: You must be Admin+. + requestBody: + content: + multipart/form-data: + schema: + type: object + required: + - mascot[mascot_file] + - mascot[display_name] + - mascot[background_color] + - mascot[artist_url] + - mascot[artist_name] + properties: + mascot[mascot_file]: + type: string + format: binary + mascot[display_name]: + type: string + mascot[background_color]: + type: string + mascot[artist_url]: + type: string + mascot[artist_name]: + type: string + mascot[available_on_string]: + type: string + description: Comma separated site names. + mascot[active]: + type: boolean + mascot[hide_anonymous]: + type: boolean + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Mascot" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /mascots/{id}.json: + patch: + summary: Edit Mascot + operationId: editMascot + tags: + - Mascots + security: + - basicAuth: [] + description: You must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the mascot. + schema: + type: number + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + mascot[mascot_file]: + type: string + format: binary + mascot[display_name]: + type: string + mascot[background_color]: + type: string + mascot[artist_url]: + type: string + mascot[artist_name]: + type: string + mascot[available_on_string]: + type: string + description: Comma separated site names. + mascot[active]: + type: boolean + mascot[hide_anonymous]: + type: boolean + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete Mascot + operationId: deleteMascot + tags: + - Mascots + security: + - basicAuth: [] + description: You must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the mascot. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # Mod Actions + /mod_actions.json: + get: + summary: Search Mod Actions + operationId: searchModActions + tags: + - Mod Actions + description: | + Note that some mod actions are not in use anymore. Their entries exist for historical purposes only.
+ The current legacy actions are as follows:
+ * `created_positive_record`, `created_neutral_record`, `created_negative_record` + * `created_flag_reason`, `edited_flag_reason`, `deleted_flag_reason` + * `post_move_favorites`, `post_delete`, `post_undelete`, `post_destroy`, `post_rating_lock`, `post_unapprove` + * `post_replacement_accept`, `post_replacement_reject`, `post_replacement_delete` + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - in: query + name: search[creator_id] + schema: + type: number + - in: query + name: search[creator_name] + schema: + type: string + - in: query + name: search[action] + schema: + type: string + enum: *modaction-actions + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ModAction" + # News Updates + /news_updates.json: + get: + summary: List News Updates + operationId: listNewsUpdates + tags: + - News Updates + description: When no results are found, an object with an `news_updates` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/NewsUpdate" + - type: object + description: No Results + required: + - news_updates + properties: + news_updates: + type: array + maxItems: 0 + post: + summary: Create News Update + operationId: createNewsUpdate + tags: + - News Updates + security: + - basicAuth: [] + description: You must be Admin+. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - news_update[message] + properties: + news_update[message]: + type: string + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/NewsUpdate" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /news_updates/{id}.json: + patch: + summary: Edit News Update + operationId: editNewsUpdate + tags: + - News Updates + security: + - basicAuth: [] + description: You must be Admin+ + parameters: + - name: id + in: path + required: true + description: The ID of the news update. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - news_update[message] + properties: + news_update[message]: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete News Update + operationId: deleteNewsUpdate + tags: + - News Updates + security: + - basicAuth: [] + description: You must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the news update. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # Notes + /notes.json: + get: + summary: Search Notes + operationId: searchNotes + tags: + - Notes + description: When no results are found, an object with an `notes` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - in: query + name: search[body_matches] + schema: + type: string + - in: query + name: search[is_active] + schema: + type: boolean + - in: query + name: search[post_id] + schema: + type: number + - in: query + name: search[post_tags_match] + schema: + type: string + - in: query + name: search[post_note_updater_id] + schema: + type: number + - in: query + name: search[post_note_updater_name] + schema: + type: string + - in: query + name: search[creator_id] + schema: + type: number + - in: query + name: search[creator_name] + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/Note" + - type: object + description: No Results + required: + - notes + properties: + notes: + type: array + maxItems: 0 + post: + summary: Create Note + operationId: createNote + tags: + - Notes + security: + - basicAuth: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - note[post_id] + - note[x] + - note[y] + - note[width] + - note[height] + - note[body] + properties: + note[post_id]: + type: number + note[x]: + type: number + note[y]: + type: number + note[width]: + type: number + note[height]: + type: number + note[body]: + type: string + note[html_id]: + type: string + description: Passthrough, used in frontend. + responses: + 201: + description: Success + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Note" + - type: object + required: + - html_id + properties: + html_id: + type: string + description: Passthrough, used in frontend. + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /notes/{id}.json: + get: + summary: Get Note + operationId: getNote + tags: + - Notes + parameters: + - name: id + in: path + required: true + description: The ID of the note. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Note" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Note + operationId: editNote + tags: + - Notes + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the note. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + note[x]: + type: number + note[y]: + type: number + note[width]: + type: number + note[height]: + type: number + note[body]: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete Note + operationId: deleteNote + tags: + - Notes + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the note. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /notes/{id}/revert.json: + post: + summary: Revert Note + operationId: revertNote + tags: + - Notes + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the note. + schema: + type: number + - name: version_id + in: query + required: true + description: The version ID to revert to. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + # Note Versions + /note_versions.json: + get: + summary: Search Note Versions + operationId: searchNoteVersions + tags: + - Note Versions + description: When no results are found, an object with an `note_versions` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/ip_addr" + - name: search[order] + in: query + description: The order of the results. + schema: + type: string + enum: + - id_asc + - id_desc + - in: query + name: search[updater_id] + schema: + type: number + - in: query + name: search[updater_name] + schema: + type: string + - in: query + name: search[post_id] + schema: + type: number + - in: query + name: search[note_id] + schema: + type: number + - in: query + name: search[is_active] + schema: + type: boolean + - in: query + name: search[body_matches] + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/NoteVersion" + - type: object + description: No Results + required: + - note_versions + properties: + note_versions: + type: array + maxItems: 0 + # Pools + /pools.json: + get: + summary: Search Pools + operationId: searchPools + tags: + - Pools + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - name + - created_at + - post_count + - in: query + name: search[name_matches] + schema: + type: string + - in: query + name: search[description_matches] + schema: + type: string + - in: query + name: search[creator_id] + schema: + type: number + - in: query + name: search[creator_name] + schema: + type: string + - in: query + name: search[category] + schema: + type: string + enum: *pool-categories + - in: query + name: search[is_active] + schema: + type: boolean + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Pool" + post: + summary: Create Pool + operationId: createPool + tags: + - Pools + security: + - basicAuth: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - pool[name] + properties: + pool[name]: + type: string + pool[description]: + type: string + pool[category]: + type: string + enum: *pool-categories + ipool[s_active]: + type: boolean + pool[post_ids_string]: + type: string + description: Space separated list of post IDs. Mutually exclusive with post_ids. + pool[post_ids]: + type: array + description: Array of post IDs. Mutually exclusive with post_ids_string. + items: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Pool" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /pools/{id}.json: + get: + summary: Get Pool + operationId: getPool + tags: + - Pools + parameters: + - name: id + in: path + required: true + description: The ID of the pool. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Pool" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Pool + operationId: editPool + tags: + - Pools + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the pool. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + pool[name]: + type: string + pool[description]: + type: string + pool[is_active]: + type: boolean + pool[category]: + type: string + enum: *pool-categories + description: If the pool has more than 30 posts, you must be Janitor+. + pool[post_ids_string]: + type: string + description: Space separated list of post IDs. Mutually exclusive with post_ids. + pool[post_ids]: + type: array + description: Array of post IDs. Mutually exclusive with post_ids_string. + items: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete Pool + operationId: deletePool + tags: + - Pools + description: You must be Janitor+. + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the pool. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /pools/{id}/revert.json: + put: + summary: Revert Pool + operationId: revertPool + tags: + - Pools + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the pool. + schema: + type: number + - name: version_id + in: query + required: true + description: The version ID to revert to. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + # Pool Element + /pool_element.js: + post: + summary: Add Post To Pool + operationId: addPostToPool + tags: + - Pools + security: + - basicAuth: [] + description: Note that the extension is JS, this route will not return JSON. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - post_id + properties: + pool_id: + type: number + description: Mutually exclusive with pool_name. + pool_name: + type: string + description: Mutually exclusive with pool_id. + post_id: + type: number + responses: + 200: + description: Success + /pool_element.json: + delete: + summary: Remove Post From Pool + operationId: removePostFromPool + tags: + - Pools + security: + - basicAuth: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - pool_id + - post_id + properties: + pool_id: + type: number + post_id: + type: number + responses: + 204: + description: Success + # Pool Versions + /pool_versions.json: + get: + summary: Search Pool Versions + operationId: searchPoolVersions + tags: + - Pool Versions + description: When no results are found, an object with an `pool_versions` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/ip_addr" + - name: search[order] + in: query + description: The order of the results. + schema: + type: string + enum: + - id_asc + - id_desc + - in: query + name: search[updater_id] + schema: + type: number + - in: query + name: search[updater_name] + schema: + type: string + - in: query + name: search[pool_id] + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/PoolVersion" + - type: object + description: No Results + required: + - pool_versions + properties: + pool_versions: + type: array + maxItems: 0 + # Popular + /popular.json: + get: + summary: List Most Upvoted Posts + operationId: listPopular + tags: + - Popular + parameters: + - $ref: "#/components/parameters/limit" + - name: date + in: query + description: The date to list popular uploads for. Only The day, month, and year are considered. + schema: + type: string + format: date + - name: scale + in: query + description: The scale of the results, in relation to `date`. + schema: + type: string + enum: + - month + - week + - day + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - posts + properties: + posts: + type: array + items: + $ref: "#/components/schemas/Post" + # Posts + /posts.json: + get: + summary: Search Posts + operationId: searchPosts + tags: + - Posts + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - in: query + name: tags + schema: + type: string + - in: query + name: md5 + schema: + type: string + - in: query + name: random + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - posts + properties: + posts: + type: array + items: + $ref: "#/components/schemas/Post" + /posts/{id}.json: + get: + summary: Get Post + operationId: getPost + tags: + - Posts + parameters: + - name: id + in: path + required: true + description: The ID of the post. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - post + properties: + post: + $ref: "#/components/schemas/Post" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Post + operationId: editPost + tags: + - Posts + security: + - basicAuth: [] + description: Most errors are silently swallowed. + parameters: + - name: id + in: path + required: true + description: The ID of the post. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + post[tag_string]: + type: string + description: Replaces all tags on the post. + post[old_tag_string]: + type: string + description: The tag string before your edits, used to reconcile conflicts. + post[tag_string_diff]: + type: string + description: Tags with a minus are removed, else they are added. Mutually exclusive with tag_string. + post[source_diff]: + type: string + description: Sources with a minus are removed, else they are added. It is not possible to add inactive sources through this. Mutually exclusive with source. + post[source]: + type: string + description: Replaces all sources on the post. + post[old_source]: + type: string + description: The sources before your edits, used to reconcile conflicts. + post[parent_id]: + type: number + post[old_parent_id]: + type: number + post[description]: + type: string + post[old_description]: + type: string + post[rating]: + type: string + enum: *ratings + post[old_rating]: + type: string + enum: *ratings + post[edit_reason]: + type: string + post[is_rating_locked]: + type: boolean + description: You must be Privileged+. + post[is_note_locked]: + type: boolean + description: You must be Janitor+. + post[bg_colo]r: + type: string + description: You must be Janitor+. + post[is_comment_locked]: + type: boolean + description: You must be Admin+. + post[is_status_locked]: + type: boolean + description: You must be Admin+. + post[locked_tags]: + type: string + description: You must be Admin+. + post[hide_from_anonymous]: + type: boolean + description: You must be Admin+. + post[hide_from_search_engines]: + type: boolean + description: You must be Admin+. + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /posts/{id}/update_iqdb.json: + get: + summary: Update Post IQDB + operationId: updatePostIqdb + tags: + - Posts + security: + - basicAuth: [] + description: You must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the post. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - post + properties: + post: + $ref: "#/components/schemas/Post" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /posts/{id}/mark_as_translated.json: + post: + summary: Mark Post As Translated + operationId: markPostAsTranslated + tags: + - Posts + security: + - basicAuth: [] + description: Will error if no body is provided. + parameters: + - name: id + in: path + required: true + description: The ID of the post. + schema: + type: number + requestBody: + content: + application/json: + schema: + type: object + properties: + translation_check: + type: boolean + partially_translated: + type: boolean + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - post + properties: + post: + $ref: "#/components/schemas/Post" + + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /posts/{id}/copy_notes.json: + put: + summary: Copy Notes To Post + operationId: copyNotesToPost + tags: + - Posts + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the post. + schema: + type: number + requestBody: + content: + application/json: + schema: + type: object + required: + - other_post_id + properties: + other_post_id: + type: number + responses: + 204: + description: Success + 400: + $ref: "#/components/responses/MessageError" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /posts/{id}/revert.json: + post: + summary: Revert Post + operationId: revertPost + tags: + - Posts + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the post. + schema: + type: number + - name: version_id + in: query + required: true + description: The version ID to revert to. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /posts/{id}/show_seq.json: + get: + summary: Get Post In Sequence + operationId: getPostInSequence + tags: + - Posts + parameters: + - name: id + in: path + required: true + description: The ID of the post. + schema: + type: number + - name: seq + in: query + description: The direction to move in the sequence. + schema: + type: string + enum: + - next + - prev + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - post + properties: + post: + $ref: "#/components/schemas/Post" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /posts/{id}/flag.json: + delete: + summary: Unflag Post + operationId: unflagPost + tags: + - Posts + description: You must have the "Approve Posts" permission. + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the post. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + approval: + type: string + description: Approves the post if set to "approve". + + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /posts/random.json: + get: + summary: Get Random Post + operationId: getRandomPost + tags: + - Posts + parameters: + - name: tags + in: query + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - post + properties: + post: + $ref: "#/components/schemas/Post" + 404: + $ref: "#/components/responses/NotFound" + # Post Sets + /post_sets.json: + get: + summary: Search Post Sets + operationId: searchPostSets + tags: + - Post Sets + description: When no results are found, an object with a `post_sets` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - name + - shortname + - post_count + - postcount + - created_at + - updated_at + - update + - in: query + name: search[creator_id] + schema: + type: number + - in: query + name: search[creator_name] + schema: + type: string + - in: query + name: search[name] + schema: + type: string + - in: query + name: search[shortname] + schema: + type: string + - in: query + name: search[is_public] + description: You must be Moderator+. + schema: + type: boolean + - in: query + name: search[post_id] + schema: + type: number + - in: query + name: search[maintainer_id] + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/PostSet" + - type: object + description: No Results + required: + - post_sets + properties: + post_sets: + type: array + maxItems: 0 + post: + summary: Create Post Set + operationId: createPostSet + tags: + - Post Sets + security: + - basicAuth: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - post_set[name] + - post_set[shortname] + properties: + post_set[name]: + type: string + post_set[shortname]: + type: string + post_set[description]: + type: string + post_set[is_public]: + type: boolean + post_set[ransfer_on_delete]: + type: boolean + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/PostSet" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /post_sets/{id}.json: + get: + summary: Get Post Set + operationId: getPostSet + tags: + - Post Sets + description: You must be Moderator+ if the set is not public. + parameters: + - name: id + in: path + required: true + description: The ID of the post set. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/PostSet" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Post Set + operationId: editPostSet + tags: + - Post Sets + security: + - basicAuth: [] + description: You must be the owner of the set, or Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the post sets. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + post_set[name]: + type: string + post_set[shortname]: + type: string + post_set[description]: + type: string + post_set[is_public]: + type: boolean + post_set[transfer_on_delete]: + type: boolean + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete Post Set + operationId: deletePostSet + tags: + - Post Sets + security: + - basicAuth: [] + description: You must be the owner of the set, or Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the post set. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /post_sets/{id}/update_posts.json: + post: + summary: Update Post Set Posts + operationId: updatePostSetPosts + tags: + - Post Sets + security: + - basicAuth: [] + description: You must be the owner of the set, a maintainer (if public), or Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the post set. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - post_set[post_ids_string] + properties: + post_set[post_ids_string]: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/PostSet" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /post_sets/{id}/add_posts.json: + post: + summary: Add Posts To Post Set + operationId: addPostsToPostSet + tags: + - Post Sets + security: + - basicAuth: [] + description: You must be the owner of the set, a maintainer (if public), or Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the post set. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - post_ids + properties: + post_ids: + type: array + description: post_ids[]=1&post_ids[]=2 + items: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/PostSet" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /post_sets/{id}/remove_posts.json: + post: + summary: Remove Posts From Post Set + operationId: removePostsFromPostSet + tags: + - Post Sets + security: + - basicAuth: [] + description: You must be the owner of the set, a maintainer (if public), or Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the post set. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - post_ids + properties: + post_ids: + type: array + description: post_ids[]=1&post_ids[]=2 + items: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/PostSet" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /post_sets/for_select.json: + get: + summary: List Post Sets For Select + operationId: listPostSetsForSelect + tags: + - Post Sets + security: + - basicAuth: [] + description: You must be the owner of the set, a maintainer (if public), or Admin+. + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + properties: + Owned: + type: array + items: + anyOf: + - type: string + - type: number + Maintained: + type: array + items: + anyOf: + - type: string + - type: number + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + # Post Approvals + /post_approvals.json: + get: + summary: Search Post Approvals + operationId: searchPostApprovals + tags: + - Post Approvals + description: When no results are found, an object with an `post_approvals` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - name: search[post_tags_match] + in: query + schema: + type: string + - name: search[user_id] + in: query + schema: + type: number + - name: search[user_name] + in: query + schema: + type: string + - name: search[post_id] + in: query + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/PostApproval" + - type: object + description: No Results + required: + - post_aprovals + properties: + post_approvals: + type: array + maxItems: 0 + # Post Events + /post_events.json: + get: + summary: Search Post Events + operationId: searchPostEvents + tags: + - Post Events + security: + - basicAuth: [] + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - in: query + name: search[post_id] + schema: + type: number + - in: query + name: search[creator_id] + schema: + type: number + - in: query + name: search[creator_name] + schema: + type: string + - in: query + name: search[action] + schema: + type: string + enum: *post-event-actions + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - post_events + properties: + post_events: + type: array + items: + $ref: "#/components/schemas/PostEvent" + # Post Flags + /post_flags.json: + get: + summary: Search Post Flags + operationId: searchPostFlags + tags: + - Post Flags + description: When no results are found, an object with an `post_flags` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/ip_addr" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - in: query + name: search[reason_matches] + schema: + type: string + - in: query + name: search[creator_id] + schema: + type: number + - in: query + name: search[creator_name] + schema: + type: string + - in: query + name: search[post_id] + schema: + type: number + - in: query + name: search[post_tags_match] + schema: + type: string + - in: query + name: search[type] + schema: + type: string + - in: query + name: search[is_resolved] + schema: + type: boolean + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/PostFlag" + - type: object + description: No Results + required: + - post_flags + properties: + post_flags: + type: array + maxItems: 0 + post: + summary: Create Post Flag + operationId: createPostFlag + tags: + - Post Flags + security: + - basicAuth: [] + description: Will error if post_id is not provided. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - post_flag[post_id] + - post_flag[reason_name] + properties: + post_flag[post_id]: + type: number + post_flag[reason_name]: + type: string + enum: + - uploading_guidelines + - young_human + - dnp_artist + - pay_content + - trace + - previously_deleted + - real_porn + - corrupt + - inferior + post_flag[parent_id]: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/PostFlag" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /post_flags/{id}.json: + get: + summary: Get Post Flag + operationId: getPostFlag + tags: + - Post Flags + parameters: + - name: id + in: path + required: true + description: The ID of the post flag. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/PostFlag" + 404: + $ref: "#/components/responses/NotFound" + # Post Replacements + /post_replacements.json: + get: + summary: Search Post Replacements + operationId: searchPostReplacements + tags: + - Post Replacements + description: When no results are found, an object with an `post_replacements` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - in: query + name: search[file_ext] + schema: + type: string + - in: query + name: search[md5] + schema: + type: string + - in: query + name: search[status] + schema: + type: string + - in: query + name: search[creator_id] + schema: + type: number + - in: query + name: search[creator_name] + schema: + type: string + - in: query + name: search[approver_id] + schema: + type: number + - in: query + name: search[approver_name] + schema: + type: string + - in: query + name: search[rejector_id] + schema: + type: number + - in: query + name: search[rejector_name] + schema: + type: string + - in: query + name: search[uploader_name_on_approve] + schema: + type: string + - in: query + name: search[uploader_id_on_approve] + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/PostReplacement" + - type: object + description: No Results + required: + - post_replacements + properties: + post_replacements: + type: array + maxItems: 0 + post: + summary: Create Post Replacement + operationId: createPostReplacement + tags: + - Post Replacements + security: + - basicAuth: [] + requestBody: + content: + multipart/form-data: + schema: + type: object + required: + - post_replacement[reason] + properties: + post_replacement[replacement_file]: + type: string + format: binary + description: Mutually exclusive with replacement_url. + post_replacement[replacement_url]: + type: string + description: Mutually exclusive with replacement_file. + post_replacement[reason]: + type: string + post_replacement[source]: + type: string + post_replacement[as_pending]: + type: boolean + description: You must be Janitor+. + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - success + - location + properties: + success: + type: boolean + enum: + - true + location: + type: string + example: "/posts/1" + 403: + $ref: "#/components/responses/AccessDenied" + 412: + $ref: "#/components/responses/MessageError" + 422: + $ref: "#/components/responses/ExpectedError" + /post_replacements/{id}.json: + delete: + summary: Delete Post Replacement + operationId: deletePostReplacement + tags: + - Post Replacements + description: You must be Admin+. + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the post replacement. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /post_replacements/{id}/approve.json: + put: + summary: Approve Post Replacement + operationId: approvePostReplacement + tags: + - Post Replacements + security: + - basicAuth: [] + description: You must have the "Approve Posts" permission. + parameters: + - name: id + in: path + required: true + description: The ID of the post replacement. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /post_replacements/{id}/reject.json: + put: + summary: Reject Post Replacement + operationId: rejectPostReplacement + tags: + - Post Replacements + security: + - basicAuth: [] + description: You must have the "Approve Posts" permission. + parameters: + - name: id + in: path + required: true + description: The ID of the post replacement. + schema: + type: number + requestBody: + content: + application/json: + schema: + type: object + properties: + reason: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /post_replacements/{id}/promote.json: + post: + summary: Promote Post Replacement + operationId: promotePostReplacement + tags: + - Post Replacements + security: + - basicAuth: [] + description: You must have the "Approve Posts" permission. + parameters: + - name: id + in: path + required: true + description: The ID of the post replacement. + schema: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + type: object + required: + - post + properties: + post: + $ref: "#/components/schemas/Post" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /post_replacements/{id}/toggle_penalize.json: + put: + summary: Toggle Post Replacement Penalty + operationId: togglePostReplacementPenalty + tags: + - Post Replacements + security: + - basicAuth: [] + description: You must have the "Approve Posts" permission. + parameters: + - name: id + in: path + required: true + description: The ID of the post replacement. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + # Post Versions + /post_versions.json: + get: + summary: Search Post Versions + operationId: searchPostVersions + tags: + - Post Versions + description: Errors if no results are found. + parameters: + - $ref: "#/components/parameters/limit" + - name: page + in: query + description: The page number of results to get. Between 1 and 750. Note that for post versions specifically, you can only go through the 10,000 most recent results with page numbers. + schema: + type: number + minimum: 1 + maximum: 750 + - $ref: "#/components/parameters/id" + - in: query + name: search[updater_name] + schema: + type: string + - in: query + name: search[updater_id] + schema: + type: number + - in: query + name: search[post_id] + schema: + type: number + - in: query + name: search[start_id] + schema: + type: number + - in: query + name: search[rating] + schema: + type: string + enum: *ratings + - in: query + name: search[rating_changed] + schema: + type: string + enum: + - e + - q + - s + - any + - in: query + name: search[parent_id] + schema: + type: number + - in: query + name: search[parent_id_changed] + schema: + type: boolean + - in: query + name: search[tags] + schema: + type: string + - in: query + name: search[tags_removed] + schema: + type: string + - in: query + name: search[tags_added] + schema: + type: string + - in: query + name: search[locked_tags] + schema: + type: string + - in: query + name: search[locked_tags_removed] + schema: + type: string + - in: query + name: search[locked_tags_added] + schema: + type: string + - in: query + name: search[reason] + schema: + type: string + - in: query + name: search[description] + schema: + type: string + - in: query + name: search[description_changed] + schema: + type: boolean + - in: query + name: search[source_changed] + schema: + type: boolean + - in: query + name: search[uploads] + schema: + type: string + enum: + - included + - excluded + - only + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/PostVersion" + # Post Votes + /posts/{id}/votes.json: + post: + summary: Create Post Vote + operationId: createPostVote + tags: + - Post Votes + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the post. + schema: + type: string + - name: score + in: query + required: true + schema: + type: number + enum: + - -1 + - 1 + - name: no_unvote + in: query + schema: + type: boolean + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - score + - up + - down + - our_score + properties: + score: + type: number + up: + type: number + down: + type: number + our_score: + type: number + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete Post Vote + operationId: deletePostVote + tags: + - Post Votes + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the post. + schema: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /post_votes/lock.json: + post: + summary: Lock Post Vote + operationId: lockPostVote + tags: + - Post Votes + security: + - basicAuth: [] + description: You must be Moderator+. Errors if ids is not provided. + parameters: + - name: ids + in: query + required: true + description: The IDs of the post votes, comma separated. + schema: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /post_votes/delete.json: + post: + summary: Delete Post Vote + operationId: deletePostVotes + tags: + - Post Votes + security: + - basicAuth: [] + description: You must be Admin+. Errors if ids is not provided. + parameters: + - name: ids + in: query + required: true + description: The IDs of the post votes, comma separated. + schema: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + # Tags + /tags.json: + get: + summary: Search Tags + operationId: searchTags + tags: + - Tags + description: When no results are found, an object with an `tags` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - name + - date + - count + - similarity + - in: query + name: search[fuzzy_name_matches] + schema: + type: string + - in: query + name: search[name_matches] + schema: + type: string + - in: query + name: search[name] + schema: + type: string + - in: query + name: search[category] + schema: + type: number + description: Allows multiple, comma separated. + enum: *tag-categories + - in: query + name: search[hide_empty] + schema: + type: boolean + - in: query + name: search[has_wiki] + schema: + type: boolean + - in: query + name: search[has_artist] + schema: + type: boolean + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/Tag" + - type: object + description: No Results + required: + - tags + properties: + tags: + type: array + maxItems: 0 + /tags/{id}.json: + get: + summary: Get Tag + operationId: getTag + tags: + - Tags + parameters: + - name: id + in: path + required: true + description: The ID or name of the tag. + schema: + type: ["number", "string"] + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Tag" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Tag + operationId: editTag + tags: + - Tags + security: + - basicAuth: [] + description: Must be Admin+ if the tag is locked or post count is >100. + parameters: + - name: id + in: path + required: true + description: The ID of the tag. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + tag[category]: + type: number + enum: *tag-categories + tag[is_locked]: + description: Must be Admin+. + type: boolean + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /tags/{id}/correction.json: + post: + summary: Correct Tag + operationId: correctTag + tags: + - Tags + security: + - basicAuth: [] + description: You must be Janitor+. `commit=Fix` must be set. + parameters: + - name: id + in: path + required: true + description: The ID of the tag. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - commit + properties: + commit: + type: string + description: If not set, nothing will happen. + responses: + 302: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /tags/preview.json: + get: + summary: Preview Tags + operationId: previewTags + tags: + - Tags + description: Note while this route does not require auth, without auth it requires a CSRF token. For that reason it has been marked as requiring auth. + security: + - basicAuth: [] + parameters: + - name: tags + in: query + required: true + description: The tags to preview, space separated. + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/TagPreview" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # Tag Type Versions + /tag_type_versions.json: + get: + summary: Search Tag Versions + operationId: searchTagVersions + tags: + - Tag Versions + description: When no results are found, an object with an `tag_type_versions` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[tag] + in: query + schema: + type: string + - name: search[user_id] + in: query + schema: + type: string + - name: search[user_name] + in: query + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/TagTypeVersion" + - type: object + description: No Results + required: + - tag_type_versions + properties: + tag_type_versions: + type: array + maxItems: 0 + # Tag Aliases + /tag_aliases.json: + get: + summary: Search Tag Aliases + operationId: searchTagAliases + tags: + - Tag Aliases + description: When no results are found, an object with an `tag_aliases` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - created_at + - updated_at + - name + - tag_count + - in: query + name: search[name_matches] + schema: + type: string + - in: query + name: search[antecedent_name] + schema: + type: string + - in: query + name: search[consequent_name] + schema: + type: string + - in: query + name: search[status] + schema: + type: string + enum: *tag-request-statuses + - in: query + name: search[antecedent_tag_category] + schema: + type: number + enum: *tag-categories + - in: query + name: search[consequent_tag_category] + schema: + type: number + enum: *tag-categories + - in: query + name: search[creator_id] + schema: + type: number + - in: query + name: search[creator_name] + schema: + type: string + - in: query + name: search[approver_id] + schema: + type: number + - in: query + name: search[approver_name] + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/TagAlias" + - type: object + description: No Results + required: + - tag_aliases + properties: + tag_aliases: + type: array + maxItems: 0 + /tag_aliases/{id}.json: + get: + summary: Get Tag Alias + operationId: getTagAlias + tags: + - Tag Aliases + parameters: + - name: id + in: path + required: true + description: The ID of the tag alias. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/TagAlias" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Tag Alias + operationId: editTagAlias + tags: + - Tag Aliases + security: + - basicAuth: [] + description: You must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the tag alias. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + tag_alias[antecedent_name]: + type: string + tag_alias[consequent_name]: + type: string + tag_alias[forum_topic_id]: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Reject Tag Alias + operationId: rejectTagAlias + tags: + - Tag Aliases + description: You must be the creator of the request (if pending), or Admin+. + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the tag alias. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /tag_aliases/{id}/approve.json: + post: + summary: Approve Tag Alias + operationId: approveTagAlias + tags: + - Tag Aliases + security: + - basicAuth: [] + description: You must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the tag alias. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /tag_alias_requests.json: + post: + summary: Create Tag Alias + operationId: createTagAlias + tags: + - Tag Aliases + security: + - basicAuth: [] + description: Errors will result in a 406 with no information. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - tag_alias[antecedent_name] + - tag_alias[consequent_name] + - tag_alias[reason] + properties: + tag_alias[antecedent_name]: + type: string + tag_alias[consequent_name]: + type: string + tag_alias[reason]: + type: string + tag_alias[skip_forum]: + type: boolean + description: Must be Admin+. + responses: + 302: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 406: + description: Failure + # Tag Implications + /tag_implications.json: + get: + summary: Search Tag Implications + operationId: searchTagImplications + tags: + - Tag Implications + description: When no results are found, an object with an `tag_implications` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - created_at + - updated_at + - name + - tag_count + - in: query + name: search[name_matches] + schema: + type: string + - in: query + name: search[antecedent_name] + schema: + type: string + - in: query + name: search[consequent_name] + schema: + type: string + - in: query + name: search[status] + schema: + type: string + enum: *tag-request-statuses + - in: query + name: search[antecedent_tag_category] + schema: + type: number + enum: *tag-categories + - in: query + name: search[consequent_tag_category] + schema: + type: number + enum: *tag-categories + - in: query + name: search[creator_id] + schema: + type: number + - in: query + name: search[creator_name] + schema: + type: string + - in: query + name: search[approver_id] + schema: + type: number + - in: query + name: search[approver_name] + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/TagImplication" + - type: object + description: No Results + required: + - tag_implications + properties: + tag_implications: + type: array + maxItems: 0 + /tag_implications/{id}.json: + get: + summary: Get Tag Implication + operationId: getTagImplication + tags: + - Tag Implications + parameters: + - name: id + in: path + required: true + description: The ID of the tag implication. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/TagImplication" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Tag Implication + operationId: editTagImplication + tags: + - Tag Implications + security: + - basicAuth: [] + description: You must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the tag implication. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + tag_implication[antecedent_name]: + type: string + tag_implication[consequent_name]: + type: string + tag_implication[forum_topic_id]: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Reject Tag Implication + operationId: rejectTagImplication + tags: + - Tag Implications + description: You must be the creator of the request (if pending), or Admin+. + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the tag implication. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /tag_implications/{id}/approve.json: + post: + summary: Approve Tag Implication + operationId: approveTagImplication + tags: + - Tag Implications + security: + - basicAuth: [] + description: You must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the tag implication. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /tag_implication_requests.json: + post: + summary: Create Tag Implication + operationId: createTagImplication + tags: + - Tag Implications + security: + - basicAuth: [] + description: Errors will result in a 406 with no information. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - tag_implication[antecedent_name] + - tag_implication[consequent_name] + - tag_implication[reason] + properties: + tag_implication[antecedent_name]: + type: string + tag_implication[consequent_name]: + type: string + tag_implication[reason]: + type: string + tag_implication[skip_forum]: + type: boolean + description: Must be Admin+. + responses: + 302: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 406: + description: Failure + # Bulk Related Tags + /related_tag/bulk.json: + post: + summary: List Bulk Related Tags + operationId: listBulkRelatedTags + tags: + - Related Tags + security: + - basicAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + query: + type: string + category_id: + type: number + enum: *tag-categories + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + examples: + - {"male":[{"name":"male","count":0,"category_id":0}]} + patternProperties: + "^.+$": + type: array + items: + $ref: "#/components/schemas/BulkRelatedTag" + 403: + $ref: "#/components/responses/AccessDenied" + # Takedowns + /takedowns.json: + get: + summary: Search Takedowns + operationId: searchTakedowns + tags: + - Takedowns + description: When no results are found, an object with an `takedowns` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/ip_addr" + - in: query + name: search[order] + schema: + type: string + description: Must Admin+ to use. + enum: + - id_asc + - id_desc + - status + - post_count + - in: query + name: search[status] + schema: + type: string + - in: query + name: search[source] + description: Must be Moderator+ to use. + schema: + type: string + - in: query + name: search[reason] + description: Must be Moderator+ to use. + schema: + type: string + - in: query + name: search[creator_id] + description: Must be Moderator+ to use. + schema: + type: number + - in: query + name: search[creator_name] + description: Must be Moderator+ to use. + schema: + type: string + - in: query + name: search[reason_hidden] + description: Must be Moderator+ to use. + schema: + type: boolean + - in: query + name: search[instructions] + description: Must be Moderator+ to use. + schema: + type: string + - in: query + description: Must be Moderator+ to use. + name: search[post_id] + schema: + type: number + - in: query + description: Must be Moderator+ to use. + name: search[notes] + schema: + type: string + - in: query + description: Must be Admin+ to use. + name: search[email] + schema: + type: string + - in: query + name: search[vericode] + description: Must be Admin+ to use. + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/Takedown" + - type: object + description: No Results + required: + - takedowns + properties: + takedowns: + type: array + maxItems: 0 + post: + summary: Create Takedown + operationId: createTakedown + tags: + - Takedowns + security: + - basicAuth: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - takedown[email] + - takedown[reason] + properties: + takedown[email]: + type: string + takedown[source]: + type: string + takedown[instructions]: + type: string + takedown[reason]: + type: string + takedown[post_ids]: + type: array + description: takedown[post_ids][]=1&takedown[post_ids][]=2 + items: + type: number + takedown[reason_hidden]: + type: boolean + takedown[notes]: + type: string + description: Must have the bd staff user flag to use. + takedown[del_post_ids]: + type: array + description: | + Must have the bd staff user flag to use. + takedown[del_post_ids][]=1&takedown[del_post_ids][]=2 + items: + type: number + takedown[status]: + type: string + description: Must have the bd staff user flag to use. + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Takedown" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /takedowns/{id}.json: + get: + summary: Get Takedown + operationId: getTakedown + tags: + - Takedowns + parameters: + - name: id + in: path + required: true + description: The ID of the takedown. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Takedown" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Takedown + operationId: editTakedown + tags: + - Takedowns + security: + - basicAuth: [] + description: You must have the bd staff user flag. + parameters: + - name: id + in: path + required: true + description: The ID of the takedown. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + takedown[notes]: + type: string + takedown[reason_hidden]: + type: boolean + takedown_posts: + type: string + process_takedown: + type: boolean + description: If not truthy, the takedown will be denied. + delete_reason: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete Takedown + operationId: deleteTakedown + tags: + - Takedowns + security: + - basicAuth: [] + description: You must have the bd staff user flag. + parameters: + - name: id + in: path + required: true + description: The ID of the takedown. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /takedowns/{id}/add_by_ids.json: + post: + summary: Add Posts To Takedown By IDs + operationId: addPostsToTakedownByIds + tags: + - Takedowns + security: + - basicAuth: [] + description: You must have the bd staff user flag. + parameters: + - name: id + in: path + required: true + description: The ID of the takedown. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - post_ids + properties: + post_ids: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - added_count + - added_post_ids + properties: + added_count: + type: number + added_post_ids: + type: array + items: + type: number + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /takedowns/{id}/add_by_tags.json: + post: + summary: Add Posts To Takedown By Tags + operationId: addPostsToTakedownByTags + tags: + - Takedowns + security: + - basicAuth: [] + description: You must have the bd staff user flag. + parameters: + - name: id + in: path + required: true + description: The ID of the takedown. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - post_tags + properties: + post_tags: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - added_count + - added_post_ids + properties: + added_count: + type: number + added_post_ids: + type: array + items: + type: number + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /takedowns/{id}/count_matching_posts.json: + post: + summary: Count Matching Posts + operationId: countMatchingPosts + tags: + - Takedowns + security: + - basicAuth: [] + description: You must have the bd staff user flag. + parameters: + - name: id + in: path + required: true + description: The ID of the takedown. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - post_tags + properties: + post_tags: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - matched_post_count + properties: + matched_post_count: + type: number + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /takedowns/{id}/remove_by_ids.json: + post: + summary: Remove Posts From Takedown By IDs + operationId: removePostsFromTakedownByIds + tags: + - Takedowns + security: + - basicAuth: [] + description: You must have the bd staff user flag. + parameters: + - name: id + in: path + required: true + description: The ID of the takedown. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - post_ids + properties: + post_ids: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # Tickets + /tickets.json: + get: + summary: Search Tickets + operationId: searchTickets + tags: + - Tickets + description: You must be Janitor+ to see tickets you did not create. When no results are found, an object with an `tickets` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - in: query + name: search[creator_name] + description: You must be Moderator+. + schema: + type: string + - in: query + name: search[creator_id] + description: You must be Moderator+ unless providing your own id. + schema: + type: number + - in: query + name: search[claimant_name] + description: You must be Moderator+. + schema: + type: string + - in: query + name: search[claimant_id] + description: You must be Moderator+. + schema: + type: number + - in: query + name: search[accused_name] + description: You must be Moderator+. + schema: + type: string + - in: query + name: search[accused_id] + description: You must be Moderator+. + schema: + type: number + - in: query + name: search[qtype] + schema: + type: string + enum: *ticket-types + - in: query + name: search[reason] + description: You must be Moderator+. + schema: + type: string + - in: query + name: search[status] + schema: + type: string + enum: + - pending + - partial + - approved + - pending_claimed + - pending_unclaimed + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/Ticket" + - type: object + description: No Results + required: + - tickets + properties: + tickets: + type: array + maxItems: 0 + 403: + $ref: "#/components/responses/AccessDenied" + /tickets/{id}.json: + get: + summary: Get Ticket + operationId: getTicket + tags: + - Tickets + security: + - basicAuth: [] + description: You must be Janitor+ to see tickets you did not create. + parameters: + - name: id + in: path + required: true + description: The ID of the ticket. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Ticket" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Ticket + operationId: editTicket + tags: + - Tickets + description: You must be Moderator+. + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the ticket. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - ticket[response] + properties: + ticket[status]: + type: string + enum: + - partial + - approved + ticket[response]: + type: string + ticket[record_type]: + type: string + enum: *warning-types + ticket[send_update_dmail]: + type: boolean + description: An update dmail will always be sent when the status is changed. + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /tickets/{id}/claim.json: + post: + summary: Claim Ticket + operationId: claimTicket + tags: + - Tickets + description: You must be Moderator+. Errors are quietly swallowed and shown as notices in html. + parameters: + - name: id + in: path + required: true + description: The ID of the ticket. + schema: + type: number + security: + - basicAuth: [] + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Ticket" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /tickets/{id}/unclaim.json: + post: + summary: Unclaim Ticket + operationId: unclaimTicket + tags: + - Tickets + description: You must be Moderator+. Errors are quietly swallowed and shown as notices in html. + parameters: + - name: id + in: path + required: true + description: The ID of the ticket. + schema: + type: number + security: + - basicAuth: [] + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Ticket" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # Uploads + /uploads.json: + get: + summary: Search Uploads + operationId: searchUploads + tags: + - Uploads + description: You must be Janitor+. When no results are found, an object with an `uploads` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - in: query + name: search[uploader_id] + schema: + type: number + - in: query + name: search[uploader_name] + schema: + type: string + - in: query + name: search[source] + schema: + type: string + - in: query + name: search[source_matches] + schema: + type: string + - in: query + name: search[rating] + schema: + type: string + enum: *ratings + - in: query + name: search[parent_id] + schema: + type: number + - in: query + name: search[post_id] + schema: + type: number + - in: query + name: search[has_post] + schema: + type: boolean + - in: query + name: search[post_tags_match] + schema: + type: string + - in: query + name: search[status] + schema: + type: string + description: | + Note: The "error" status will be proceeded by an error, ex: "error: RuntimeError - No file or source URL provided" + enum: + - completed + - processing + - pending + - error + - in: query + name: search[backtrace] + schema: + type: string + - in: query + name: search[tag_string] + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/Upload" + - type: object + description: No Results + required: + - uploads + properties: + uploads: + type: array + maxItems: 0 + post: + summary: Upload Post + operationId: uploadPost + tags: + - Uploads + - Posts + security: + - basicAuth: [] + requestBody: + content: + multipart/form-data: + schema: + type: object + required: + - upload[tag_string] + - upload[rating] + properties: + upload[file]: + type: string + format: binary + description: Mutually exclusive with direct_url. + upload[direct_url]: + type: string + description: Mutually exclusive with file. + upload[source]: + type: string + upload[tag_string]: + type: string + upload[rating]: + type: string + enum: *ratings + upload[parent_id]: + type: number + upload[description]: + type: string + upload[as_pending]: + type: boolean + description: Must have the "Unrestricted Uploads" permission. + upload[locked_rating]: + type: boolean + description: Must be Privileged+ to use. + upload[locked_tags]: + type: string + description: Must be Admin+ to use. + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - success + - location + - post_id + properties: + success: + type: boolean + enum: + - true + location: + type: string + post_id: + type: number + 403: + $ref: "#/components/responses/AccessDenied" + 412: + $ref: "#/components/responses/MessageError" + # Upload Whitelists + /upload_whitelists.json: + get: + summary: Search Upload Whitelists + operationId: searchUploadWhitelists + tags: + - Upload Whitelists + description: When no results are found, an object with an `upload_whitelists` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - pattern + - updated_at + - created_at + - in: query + name: search[pattern] + schema: + type: string + - in: query + name: search[note] + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/UploadWhitelist" + - type: object + description: No Results + required: + - upload_whitelists + properties: + upload_whitelists: + type: array + maxItems: 0 + post: + summary: Create Upload Whitelist + operationId: createUploadWhitelist + tags: + - Upload Whitelists + security: + - basicAuth: [] + description: You must be Admin+. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - upload_whitelist[allowed] + - upload_whitelist[pattern] + properties: + upload_whitelist[allowed]: + type: string + upload_whitelist[pattern]: + type: string + upload_whitelist[reason]: + type: string + upload_whitelist[note]: + type: string + upload_whitelist[hidden]: + type: boolean + /upload_whitelists/{id}.json: + patch: + summary: Edit Upload Whitelist + operationId: editUploadWhitelist + tags: + - Upload Whitelists + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the upload whitelist entry. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + upload_whitelist[allowed]: + type: string + upload_whitelist[pattern]: + type: string + upload_whitelist[reason]: + type: string + upload_whitelist[note]: + type: string + upload_whitelist[hidden]: + type: boolean + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete Upload Whitelist + operationId: deleteUploadWhitelist + tags: + - Upload Whitelists + security: + - basicAuth: [] + description: You must be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the upload whitelist. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /upload_whitelists/{id}/is_allowed.json: + get: + summary: Check If URL Is Allowed + operationId: checkIfUrlIsAllowed + tags: + - Upload Whitelists + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the upload whitelist. + schema: + type: number + - name: url + in: query + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + type: object + required: + - url + - domain + - is_allowed + - reason + properties: + url: + type: string + domain: + type: string + is_allowed: + type: boolean + reason: + type: string + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + # Users + /users.json: + get: + summary: Search Users + operationId: searchUsers + tags: + - Users + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/ip_addr" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - name + - post_upload_count + - note_count + - post_update_count + - in: query + name: search[name_matches] + schema: + type: string + - in: query + name: search[about_me] + schema: + type: string + - in: query + name: search[avatar_id] + schema: + type: number + - in: query + name: search[level] + schema: + type: number + - in: query + name: search[min_level] + schema: + type: number + - in: query + name: search[max_level] + schema: + type: number + - in: query + name: search[can_upload_free] + schema: + type: boolean + - in: query + name: search[can_approve_posts] + schema: + type: boolean + - in: query + name: search[email_matches] + description: You must be Admin+. + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + anyOf: + - $ref: "#/components/schemas/User" + - $ref: "#/components/schemas/FullUser" + description: Extra properties included for the current user. + /users/{id}.json: + get: + summary: Get User + operationId: getUser + tags: + - Users + parameters: + - name: id + in: path + required: true + description: The ID of the user. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - $ref: "#/components/schemas/User" + - $ref: "#/components/schemas/FullCurrentUser" + description: Extra properties included for the current user. + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Current User + operationId: editCurrentUser + tags: + - Users + parameters: + - name: id + in: path + required: true + description: The ID of the user. The actual value is ignored, but something must be supplied. + schema: + type: number + security: + - basicAuth: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + user[comment_threshold]: + type: number + user[default_image_size]: + type: string + enum: + - large + - fit + - fitv + - original + user[favorite_tags]: + type: string + user[blacklisted_tags]: + type: string + user[time_zone]: + type: string + description: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + user[per_page]: + type: number + user[custom_style]: + type: string + user[description_collapsed_initially]: + type: boolean + user[hide_comments]: + type: boolean + user[receive_email_notifications]: + type: boolean + user[enable_keyboard_navigation]: + type: boolean + user[enable_privacy_mode]: + type: boolean + user[disable_user_dmails]: + type: boolean + user[blacklist_users]: + type: boolean + user[show_post_statistics]: + type: boolean + user[style_usernames]: + type: boolean + user[show_hidden_comments]: + type: boolean + user[enable_autocomplete]: + type: boolean + user[disable_cropped_thumbnails]: + type: boolean + user[enable_safe_mode]: + type: boolean + user[disable_responsive_mode]: + type: boolean + user[dmail_filter_attributes][id]: + type: number + user[dmail_filter_attributes][words]: + type: string + user[profile_about]: + type: string + user[profile_artinfo]: + type: string + user[avatar_id]: + type: number + user[enable_compact_uploader]: + type: boolean + description: You must have uploaded at least 10 posts. + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /users/upload_limit.json: + get: + summary: Get Current User + operationId: getCurrentUser + tags: + - Users + security: + - basicAuth: [] + description: | + This is a crude but effective way to get the currently authenticated user without scraping HTML. + Note that this route does not include some properties included in the show action. + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/CurrentUser" + 403: + $ref: "#/components/responses/AccessDenied" + /maintenance/user/count_fixes.json: + post: + summary: Fix User Counts + operationId: fixUserCounts + tags: + - Users + security: + - basicAuth: [] + responses: + 302: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + /maintenance/user/dmail_filter.json: + patch: + summary: Update User Dmail Filter + operationId: updateUserDmailFilter + tags: + - DMails + parameters: + - name: dmail_id + in: query + required: true + description: Due to the odd way this route works, a dmail is REQUIRED to edit your dmail filter. You must be the owner of the dmail. + schema: + type: number + security: + - basicAuth: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - dmail_filter[words] + properties: + dmail_filter[words]: + type: string + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + # User Feedbacks + /user_feedbacks.json: + get: + summary: Search User Feedbacks + operationId: searchUserFeedbacks + tags: + - User Feedbacks + description: When no results are found, an object with an `user_feedbacks` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - in: query + name: search[deleted] + description: You must be Moderator+. + schema: + type: string + enum: + - included + - excluded + - only + - in: query + name: search[body_matches] + schema: + type: string + - in: query + name: search[user_id] + schema: + type: number + - in: query + name: search[user_name] + schema: + type: string + - in: query + name: search[creator_id] + schema: + type: number + - in: query + name: search[creator_name] + schema: + type: string + - in: query + name: search[category] + schema: + type: string + enum: *feedback-categories + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/UserFeedback" + - type: object + description: No Results + required: + - user_feedbacks + properties: + user_feedbacks: + type: array + maxItems: 0 + post: + summary: Create User Feedback + operationId: createUserFeedback + tags: + - User Feedbacks + security: + - basicAuth: [] + description: You must be Moderator+. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - user_feedback[body] + - user_feedback[category] + properties: + user_feedback[user_id]: + type: number + user_feedback[user_name]: + type: string + user_feedback[body]: + type: string + user_feedback[category]: + type: string + enum: *feedback-categories + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/UserFeedback" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /user_feedbacks/{id}.json: + get: + summary: Get User Feedback + operationId: getUserFeedback + tags: + - User Feedbacks + description: You must be Moderator+ if the feedback is deleted. + parameters: + - name: id + in: path + required: true + description: The ID of the feedback. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/UserFeedback" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit User Feedback + operationId: editUserFeedback + tags: + - User Feedbacks + security: + - basicAuth: [] + description: You must be Moderator+. + parameters: + - name: id + in: path + required: true + description: The ID of the feedback. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + user_feedback[body]: + type: string + user_feedback[category]: + type: string + enum: *feedback-categories + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Destroy User Feedback + operationId: destroyUserFeedback + tags: + - User Feedbacks + security: + - basicAuth: [] + description: You must be Admin+, or the creator and Moderator+. + parameters: + - name: id + in: path + required: true + description: The ID of the feedback. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /user_feedbacks/{id}/delete.json: + put: + summary: Delete User Feedback + operationId: deleteUserFeedback + tags: + - User Feedbacks + security: + - basicAuth: [] + description: You must be Moderator+. + parameters: + - name: id + in: path + required: true + description: The ID of the feedback. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /user_feedbacks/{id}/undelete.json: + put: + summary: Undelete User Feedback + operationId: undeleteUserFeedback + tags: + - User Feedbacks + security: + - basicAuth: [] + description: You must be Moderator+. + parameters: + - name: id + in: path + required: true + description: The ID of the feedback. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # User Name Change Requests + /user_name_change_requests.json: + get: + summary: Search User Name Change Requests + operationId: searchUserNameChangeRequests + tags: + - User Name Change Requests + security: + - basicAuth: [] + description: You must be Moderator+. When no results are found, an object with an `user_name_change_requests` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - in: query + name: search[current_id] + schema: + type: number + - in: query + name: search[current_name] + schema: + type: string + - in: query + name: search[original_name] + schema: + type: string + - in: query + name: search[desired_name] + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/UserNameChangeRequest" + - type: object + description: No Results + required: + - user_name_change_requests + properties: + user_name_change_requests: + type: array + maxItems: 0 + post: + summary: Create User Name Change Request + operationId: createUserNameChangeRequest + tags: + - User Name Change Requests + security: + - basicAuth: [] + description: You must be Moderator+. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - user_name_change_request[desired_name] + properties: + user_name_change_request[desired_name]: + type: string + user_name_change_request[change_reason]: + type: string + responses: + 302: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /user_name_change_requests/{id}.json: + get: + summary: Get User Name Change Request + operationId: getUserNameChangeRequest + tags: + - User Name Change Requests + security: + - basicAuth: [] + description: You must be the creator of the request or Moderator+. + parameters: + - name: id + in: path + required: true + description: The ID of the name change request. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/UserNameChangeRequest" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # Wiki Pages + /wiki_pages.json: + get: + summary: Search Wiki Pages + operationId: searchWikiPages + tags: + - Wiki Pages + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - title + - post_count + - in: query + name: search[title] + schema: + type: string + - in: query + name: search[title_matches] + schema: + type: string + - in: query + name: search[body_matches] + schema: + type: string + - in: query + name: search[other_names_match] + schema: + type: string + - in: query + name: search[creator_id] + schema: + type: number + - in: query + name: search[creator_name] + schema: + type: string + - in: query + name: search[parent] + schema: + type: string + - in: query + name: search[other_names_present] + schema: + type: boolean + - in: query + name: search[is_locked] + schema: + type: boolean + - in: query + name: search[is_deleted] + schema: + type: boolean + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/WikiPage" + post: + summary: Create Wiki Page + operationId: createWikiPage + tags: + - Wiki Pages + security: + - basicAuth: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - wiki_page[title] + - wiki_page[body] + properties: + wiki_page[title]: + type: string + wiki_page[body]: + type: string + wiki_page[edit_reason]: + type: string + wiki_page[parent]: + type: string + description: Must be Privileged+ to use. + wiki_page[is_locked]: + type: boolean + description: Must be Janitor+ to use. + wiki_page[is_deleted]: + type: boolean + description: Must be Janitor+ to use. + wiki_page[skip_secondary_validations]: + type: boolean + description: Must be Janitor+ to use. + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/WikiPage" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" + /wiki_pages/{id}.json: + get: + summary: Get Wiki Page + operationId: getWikiPage + tags: + - Wiki Pages + parameters: + - name: id + in: path + required: true + description: The ID or name of the wiki page. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/WikiPage" + 404: + $ref: "#/components/responses/NotFound" + patch: + summary: Edit Wiki Page + operationId: editWikiPage + tags: + - Wiki Pages + security: + - basicAuth: [] + description: You must be Janitor+ if the wiki page is locked. + parameters: + - name: id + in: path + required: true + description: The ID of the wiki page. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + wiki_page[body]: + type: string + wiki_page[edit_reason]: + type: string + wiki_page[parent]: + type: string + description: Must be Privileged+ to use. + wiki_page[title]: + type: string + description: Must be Janitor+ to use. + wiki_page[is_locked]: + type: boolean + description: Must be Janitor+ to use. + wiki_page[is_deleted]: + type: boolean + description: Must be Janitor+ to use. + wiki_page[skip_secondary_validations]: + type: boolean + description: Must be Janitor+ to use. + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + delete: + summary: Delete Wiki Page + operationId: deleteWikiPage + tags: + - Wiki Pages + security: + - basicAuth: [] + description: You must be Admin+ + parameters: + - name: id + in: path + required: true + description: The ID of the wiki page. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /wiki_page/{id}/revert.json: + put: + summary: Revert Wiki Page + operationId: revertWikiPage + tags: + - Wiki Pages + security: + - basicAuth: [] + parameters: + - name: id + in: path + required: true + description: The ID of the wiki page. + schema: + type: number + - name: version_id + in: query + required: true + description: The version ID to revert to. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + # Wiki Page Versions + /wiki_page_versions.json: + get: + summary: Search Wiki Page Versions + operationId: searchWikiPageVersions + tags: + - Wiki Page Versions + description: When no results are found, an object with an `wiki_page_versions` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/ip_addr" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - in: query + name: search[updater_id] + schema: + type: number + - in: query + name: search[updater_name] + schema: + type: string + - in: query + name: search[wiki_page_id] + schema: + type: number + - in: query + name: search[title] + schema: + type: string + - in: query + name: search[body] + schema: + type: string + - in: query + name: search[is_locked] + schema: + type: boolean + - in: query + name: search[is_deleted] + schema: + type: boolean + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/WikiPageVersion" + - type: object + description: No Results + required: + - wiki_page_versions + properties: + wiki_page_versions: + type: array + maxItems: 0 + /wiki_page_versions/{id}.json: + get: + summary: Get Wiki Page Version + operationId: getWikiPageVersion + tags: + - Wiki Page Versions + parameters: + - name: id + in: path + required: true + description: The ID of the wiki page version. + schema: + type: number + responses: + 200: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/WikiPageVersion" + 404: + $ref: "#/components/responses/NotFound" + # Admin Users + /admin/users/alt_list.json: + get: + summary: Get Alt List + operationId: getAltList + tags: + - Admin Users + security: + - basicAuth: [] + description: You must be Admin+. + parameters: + - name: page + in: query + description: The page number of results to get. Between 1 and 9999. + schema: + type: number + minimum: 1 + maximum: 9999 + responses: + 200: + description: Success + content: + application/json: + schema: + type: array + items: + oneOf: + - type: number + description: ID of concerned user. + - type: array + items: + type: number + description: ID of suspected alt. + examples: + - [[1,[2]],[2,[1]]] + /admins/users/{id}.json: + patch: + summary: Admin Edit User + operationId: adminEditUser + tags: + - Admin Users + security: + - basicAuth: [] + description: You must be Admin+. If editing an Admin+, you must be Owner+. + parameters: + - name: id + in: path + required: true + description: The ID of the user. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + user[verified]: + type: boolean + description: Must have the bd staff user flag to use. + user[level]: + type: number + description: Must have the bd staff user flag to promote to Admin+. + user[name]: + type: string + user[profile_about]: + type: string + user[profile_artinfo]: + type: string + user[base_upload_limit]: + type: number + user[enable_privacy_mode]: + type: boolean + user[email]: + type: string + description: Must have the bd staff user flag to use. + user[can_approve_posts]: + type: boolean + user[can_upload_free]: + type: boolean + user[no_flagging]: + type: boolean + user[replacements_beta]: + type: boolean + responses: + 204: + description: Success + 400: + $ref: "#/components/responses/MessageError" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + # Moderator Posts + /moderator/post/posts/{id}/delete.json: + post: + summary: Delete Post + operationId: deletePost + tags: + - Posts + security: + - basicAuth: [] + description: You must have the "Approve Posts" permission. `commit=Delete` must be set. + parameters: + - name: id + in: path + required: true + description: The ID of the post. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + reason: + type: string + description: If the post does not have an active flag, this is required. + move_favorites: + type: boolean + description: Move favorites to parent. + copy_sources: + type: boolean + description: Copy sources to parent. + copy_tags: + type: boolean + description: Copy tags to parent. + commit: + type: string + description: If not set, nothing will happen. + enum: + - Delete + + responses: + 302: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /moderator/post/posts/{id}/undelete.json: + post: + summary: Undelete Post + operationId: undeletePost + tags: + - Posts + security: + - basicAuth: [] + description: You must have the "Approve Posts" permission. + parameters: + - name: id + in: path + required: true + description: The ID of the post. + schema: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + type: object + required: + - post + properties: + post: + $ref: "#/components/schemas/Post" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /moderator/post/posts/{id}/regenerate_thumbnails.json: + post: + summary: Regenerate Post Thumbnails + operationId: regeneratePostThumbnails + tags: + - Posts + security: + - basicAuth: [] + description: You must be Janitor+. + parameters: + - name: id + in: path + required: true + description: The ID of the post. + schema: + type: number + responses: + 201: + description: Success + content: + application/json: + schema: + type: object + required: + - post + properties: + post: + $ref: "#/components/schemas/Post" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /moderator/post/posts/{id}/regenerate_videos.json: + post: + summary: Regenerate Post Videos + operationId: regeneratePostVideos + tags: + - Posts + security: + - basicAuth: [] + description: You must be Janitor+. + parameters: + - name: id + in: path + required: true + description: The ID of the post. + schema: + type: number + responses: + 204: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /moderator/post/posts/{id}/expunge.json: + post: + summary: Expunge Post + operationId: expungePost + tags: + - Posts + security: + - basicAuth: [] + description: You must have the "Approve Posts" permission and be Admin+. + parameters: + - name: id + in: path + required: true + description: The ID of the post. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + reason: + type: string + responses: + 201: + description: Success + content: + application/json: + schema: + type: object + required: + - post + properties: + post: + $ref: "#/components/schemas/Post" + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + 422: + $ref: "#/components/responses/ExpectedError" + /moderator/post/posts/{id}/move_favorites.json: + post: + summary: Move Post Favorites + operationId: movePostFavorites + tags: + - Posts + security: + - basicAuth: [] + description: You must have the "Approve Posts" permission. `commit=Submit`` must be set. + parameters: + - name: id + in: path + required: true + description: The ID of the post. + schema: + type: number + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - commit + properties: + commit: + type: string + description: If not set, nothing will happen. + enum: + - Submit + responses: + 302: + description: Success + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + /moderator/post/approval.json: + post: + summary: Approve Post + operationId: approvePost + tags: + - Posts + - Post Approvals + security: + - basicAuth: [] + description: You must have the "Approve Posts" permission. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - post_id + properties: + post_id: + type: number + responses: + 201: + description: Success + 204: + description: Failure + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + delete: + summary: Unapprove Post + operationId: unapprovePost + tags: + - Posts + - Post Approvals + security: + - basicAuth: [] + description: You must have the "Approve Posts" permission. The response does not differ for success or failure. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - post_id + properties: + post_id: + type: number + responses: + 204: + description: Success/Failure + 403: + $ref: "#/components/responses/AccessDenied" + 404: + $ref: "#/components/responses/NotFound" + # Post Disapprovals + /moderator/post/disapprovals.json: + get: + summary: Search Post Disapprovals + operationId: searchPostDisapprovals + tags: + - Post Disapprovals + security: + - basicAuth: [] + description: You must have the "Approve Posts" permission When no results are found, an object with a `post_disapprovals` key is returned. + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/id" + - name: search[order] + in: query + schema: + type: string + enum: + - id_asc + - id_desc + - post_id + - post_id_desc + - in: query + name: search[creator_id] + schema: + type: number + - in: query + name: search[creator_name] + schema: + type: string + - in: query + name: search[post_id] + schema: + type: number + - in: query + name: search[message] + schema: + type: string + - in: query + name: search[post_tags_match] + schema: + type: string + - in: query + name: search[reason] + schema: + type: string + - in: query + name: search[has_message] + schema: + type: boolean + responses: + 200: + description: Success + content: + application/json: + schema: + anyOf: + - type: array + items: + $ref: "#/components/schemas/PostDisapproval" + - type: object + description: No Results + required: + - post_disapprovals + properties: + post_disapprovals: + type: array + maxItems: 0 + post: + summary: Create Post Disapproval + operationId: createPostDisapproval + tags: + - Post Disapprovals + security: + - basicAuth: [] + description: You must have the "Approve Posts" permission. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - post_disapproval[post_id] + - post_disapproval[reason] + properties: + post_disapproval[post_id]: + type: number + post_disapproval[reason]: + type: string + enum: + - borderline_quality + - borderline_relevancy + - other + post_disapproval[message]: + type: string + responses: + 201: + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/PostDisapproval" + 403: + $ref: "#/components/responses/AccessDenied" + 422: + $ref: "#/components/responses/ExpectedError" From 8df62596d1462fed5454cc7224e6f23eb6e551e9 Mon Sep 17 00:00:00 2001 From: Lowikian Date: Wed, 28 May 2025 10:07:50 +0200 Subject: [PATCH 03/18] fix gin crashing due to the format --- src/main.go | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/main.go b/src/main.go index c1df5bd..64047cf 100644 --- a/src/main.go +++ b/src/main.go @@ -8,7 +8,9 @@ import ( "log" "os" "regexp" + "slices" "strconv" + "strings" "time" "bugmaschine/e6-cache/signer" @@ -87,9 +89,9 @@ func loadEnv() { PROXY_AUTH = os.Getenv("PROXY_AUTH") if PROXY_AUTH != "" { - logging.Debug("Proxy auth is enabled with key: ", PROXY_AUTH) + logging.Info("Proxy auth is enabled with key: ", PROXY_AUTH) } else { - logging.Debug("Proxy auth is disabled") + logging.Info("Proxy auth is disabled") } } @@ -140,14 +142,39 @@ func main() { log.Fatalf("Failed to load OpenAPI: %v (make sure to run 'go embed')", err) } - // create a regex to convert OpenAPI path parameters {id} to gins format :id + // below imports the OpenAPI spec into the router, and does some processing to prevent errors + + // create a regex to convert OpenAPI path parameters {id} to Gin's format :id re := regexp.MustCompile(`\{(.+?)\}`) + // regex to remove a file extension from the end like .json, .png, etc. + extRegex := regexp.MustCompile(`\.[a-zA-Z0-9]+$`) + + var registeredRoutes []string + for _, path := range doc.Paths.InMatchingOrder() { pathItem := doc.Paths.Find(path) - // convert OpenAPI parameter syntax to Gin parameter syntax, with the problem being that gin has problems supporting that + if pathItem == nil { + logging.Warn("Path item not found for path: ", path) + continue + } + logging.Debug("Processing path: ", path) + + // convert OpenAPI parameter syntax to Gin parameter syntax convertedPath := re.ReplaceAllString(path, ":$1") + + // remove leading slash and extension + convertedPath = strings.TrimPrefix(convertedPath, "/") + convertedPath = extRegex.ReplaceAllString(convertedPath, "") + + if slices.Contains(registeredRoutes, convertedPath) { + logging.Warn("Duplicate route detected: ", convertedPath) + continue + } + + registeredRoutes = append(registeredRoutes, convertedPath) for method := range pathItem.Operations() { + logging.Debug("Adding route: ", method, " ", convertedPath) router.Handle(method, convertedPath, proxyAndTransform) } } From bea14f1c6f3a7a520037e0c9e79119ebbb64eb8b Mon Sep 17 00:00:00 2001 From: Lowikian Date: Wed, 28 May 2025 10:27:53 +0200 Subject: [PATCH 04/18] Add openapi updater --- update_openapi.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 update_openapi.sh diff --git a/update_openapi.sh b/update_openapi.sh new file mode 100644 index 0000000..9d678c4 --- /dev/null +++ b/update_openapi.sh @@ -0,0 +1,3 @@ +rm ./src/openapi.yaml + +curl -o ./src/openapi.yaml https://raw.githubusercontent.com/DonovanDMC/E621OpenAPI/master/openapi.yaml From f28dd38727f687c8e219736ea08491443e068f1b Mon Sep 17 00:00:00 2001 From: Lowikian Date: Wed, 28 May 2025 10:28:06 +0200 Subject: [PATCH 05/18] Update readme and add development.md --- README.md | 27 ++++++++++----------------- development.md | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 17 deletions(-) create mode 100644 development.md diff --git a/README.md b/README.md index a04e13d..5ef29d3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # e6-cache -**e6-cache** is a locally hosted caching proxy for e621 (or anything that's api compatible), designed to passively archive and cache content you browse. +**e6-cache** is a locally hosted caching proxy for e621 (or anything that's api compatible), designed to passively archive and cache content as you browse. ## Why? @@ -9,7 +9,7 @@ Because: * You don’t remember that *one* post with the perfect lighting and suspiciously specific tags * **You want your own archive. Your own CDN. Your own sin-server.** * You have too much storage space -* Your Internet connection is too slow (the only legitimate use case) +* Your Internet connection is too slow * You worry about e621 going down, or posts being removed. ## What it does @@ -24,7 +24,7 @@ Because: * **Transparent Proxy**: Redirects all requests through e6-cache, then to your chosen instance. * **Passive Caching**: Automatically caches every post you view. * **Local Storage**: Stores metadata in a local PostgreSQL database and media files in your own S3-compatible bucket. -* **Customizable**: Easily configurable to work with any e621-compatible instance. +* **Fast**: Streams the images / videos directly to your client. * **Self-Hosted**: Runs on your own server, giving you full control over your data. * **Authentication**: Supports authentication for secure access, even when exposed to the world. @@ -77,7 +77,7 @@ For The Wolf's Stash, it just reports the host not being supported. Feel free to open a PR to add documentation for other clients. -## Speed Comparison (Speed depends on your internet, and database speed) +## Speed Comparison ### Image 1: - **Normal e621 image load:** 2.859s @@ -94,21 +94,9 @@ Feel free to open a PR to add documentation for other clients. * Proxy Mode (it act's like a proxy and redirects all e621 requests to e6-cache) * Firefox Extension (to make it easier to use by replacing all e621 links with e6-cache links) -* Offline Mode (to use the cache without internet connection, like a local e621) +* Offline API Mode (to use the cache without internet connection, like a local e621) * Website (basically a mirror of e621, but with the cache enabled) -## Architecture - -* Every API request you make goes through `e6-cache` -* We pull the data from the API and store it in our database. And then we serve it to you -* If there are images, we create special proxy links -* If the user or a client accesses the proxy link, we check if the image is already cached, and if not, we download it and store it in the S3 bucket - -## Ethical & Legal Notice - -This project **does not** scrape, spider, or hammer the API. It only caches what *you* manually request. -You, as the operator, are fully responsible for your usage. This just hands you a shovel. What you dig up is on you. - ## Contributing Feel free to open an issue or a pull request. I don't have any specific guidelines for contributing, just be nice and respectful. @@ -116,3 +104,8 @@ Feel free to open an issue or a pull request. I don't have any specific guidelin ## Why? Idk. I just wanted to play with Go, Docker and wanted to make something (relatively) useful. It's also my first published project on GitHub. + +## Ethical & Legal Notice + +This project **does not** scrape, spider, or hammer the API. It only caches what *you* manually request. +You, as the operator, are fully responsible for your usage. This just hands you a shovel. What you dig up is on you. \ No newline at end of file diff --git a/development.md b/development.md new file mode 100644 index 0000000..06fd04e --- /dev/null +++ b/development.md @@ -0,0 +1,23 @@ + +## Development +`e6-cache` works by forwarding the users request, which means that it should never require any client changes, and should work on anything that abides by the offical api. + +## Request Forwarding Process +The core concept of the implementation looks like this: + +1. Receive an api request +2. Forward the request to the target e6-based service (While checking for the Proxy Auth for example) +3. Capture the response +4. Modify the response and save it in the DB (URIs dont change in the DB) +5. Return the modified response to the client + +## File Proxying Process +File Proxying works like this: + +1. Check the Signature and decode the base64 encrypted url +2. Check in S3 if the file exists +3. If not, then request it and save it while forwarding it to the client. If it exist than stream it to the client. + +## OpenAPI Updates +The `update_openapi.sh` script: +- Updates the openai.yaml file \ No newline at end of file From 592f9cfff7c100e4a8cc6017f3532644c7ac9d4e Mon Sep 17 00:00:00 2001 From: Lowikian Date: Wed, 28 May 2025 16:35:31 +0200 Subject: [PATCH 06/18] improved the openapi parsing, as posts.json was broken --- src/http_calls.go | 2 +- src/main.go | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/http_calls.go b/src/http_calls.go index f1bac0e..b12d01a 100644 --- a/src/http_calls.go +++ b/src/http_calls.go @@ -278,7 +278,7 @@ func proxyAndTransform(c *gin.Context) { } func proxyFile(c *gin.Context) { - fileID := c.Param("File_ID") + fileID := c.Param("fileId") sig := c.Query("sig") if sig == "" { diff --git a/src/main.go b/src/main.go index 64047cf..2cb0348 100644 --- a/src/main.go +++ b/src/main.go @@ -163,9 +163,24 @@ func main() { // convert OpenAPI parameter syntax to Gin parameter syntax convertedPath := re.ReplaceAllString(path, ":$1") - // remove leading slash and extension + // get the parameter for later use + matches := re.FindStringSubmatch(path) + var param string + if len(matches) > 1 { + param = fmt.Sprintf(":%s", matches[1]) // this is $1 + } + + // remove leading slash convertedPath = strings.TrimPrefix(convertedPath, "/") - convertedPath = extRegex.ReplaceAllString(convertedPath, "") + + // only remove the extension if it's not a parameter, otherwise it will break the route + // this is here to preserve cases like /posts.json + if strings.HasSuffix(extRegex.ReplaceAllString(convertedPath, ""), param) && param != "" { + convertedPath = extRegex.ReplaceAllString(convertedPath, "") + logging.Debug("Removed extension from path: ", convertedPath) + } else { + logging.Debug("Preserving extension in path: ", convertedPath, " because it's a parameter or doesn't have an extension") + } if slices.Contains(registeredRoutes, convertedPath) { logging.Warn("Duplicate route detected: ", convertedPath) @@ -182,7 +197,7 @@ func main() { logging.Info(fmt.Sprintf("Registered %d routes from OpenAPI spec", len(doc.Paths.InMatchingOrder()))) // Proxy files from S3, if not save them. - router.GET("/proxy/:File_ID", proxyFile) + router.GET("/proxy/:fileId", proxyFile) router.GET("/", func(c *gin.Context) { c.String(200, "e6-cache is running. Use this as the instance in your preffered client.\n"+ From 7582572154f5c5b6ab72cb3b4e456554b19d069e Mon Sep 17 00:00:00 2001 From: Lowikian Date: Wed, 28 May 2025 16:58:28 +0200 Subject: [PATCH 07/18] update typos --- development.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/development.md b/development.md index 06fd04e..e357e39 100644 --- a/development.md +++ b/development.md @@ -16,8 +16,8 @@ File Proxying works like this: 1. Check the Signature and decode the base64 encrypted url 2. Check in S3 if the file exists -3. If not, then request it and save it while forwarding it to the client. If it exist than stream it to the client. +3. If not, then request it and save it while forwarding it to the client. If it exist than stream it to the client from S3. ## OpenAPI Updates The `update_openapi.sh` script: -- Updates the openai.yaml file \ No newline at end of file +- Updates the openai.yaml file from another repo \ No newline at end of file From efa28e8120f0b15f54890f9047ddc6367080eb58 Mon Sep 17 00:00:00 2001 From: Lowikian Date: Wed, 28 May 2025 17:00:27 +0200 Subject: [PATCH 08/18] update concurency on s3 --- src/s3.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/s3.go b/src/s3.go index 7ed18ec..2ffa8a7 100644 --- a/src/s3.go +++ b/src/s3.go @@ -49,6 +49,7 @@ func NewS3Service(ctx context.Context, region, endpoint, accessKey, secretKey, b uploader := manager.NewUploader(s3Client, func(u *manager.Uploader) { u.PartSize = 10 * 1024 * 1024 // 10MB Parts + u.Concurrency = 999999 // give me all the power! (i mean, threads) }) downloader := manager.NewDownloader(s3Client, func(d *manager.Downloader) { From d83f9591078eeb357fea4f37ce04415d162b3f53 Mon Sep 17 00:00:00 2001 From: Lowikian Date: Wed, 28 May 2025 18:52:44 +0200 Subject: [PATCH 09/18] made loading additional openapi definitions easier in the future --- src/main.go | 42 ++++++++++++++----------- src/{openapi.yaml => openapi/e621.yaml} | 0 update_openapi.sh | 6 ++-- 3 files changed, 27 insertions(+), 21 deletions(-) rename src/{openapi.yaml => openapi/e621.yaml} (100%) diff --git a/src/main.go b/src/main.go index 2cb0348..d6edd51 100644 --- a/src/main.go +++ b/src/main.go @@ -57,8 +57,8 @@ var ( baseURL string PROXY_AUTH string - //go:embed openapi.yaml - openApiRoutes []byte // embedded OpenAPI routes, used to dynamically register the routes in the gin router. + //go:embed "openapi/e621.yaml" + e621OpenApiRoutes []byte // embedded OpenAPI routes, used to dynamically register the routes in the gin router. ) func loadEnv() { @@ -135,15 +135,31 @@ func main() { gin.SetMode(gin.ReleaseMode) } - // load e621 routes + // register e621 routes + parseOpenAPIRoutes(e621OpenApiRoutes, router) + + // Proxy files from S3, if not save them. + router.GET("/proxy/:fileId", proxyFile) + + router.GET("/", func(c *gin.Context) { + c.String(200, "e6-cache is running. Use this as the instance in your preffered client.\n"+ + "Make sure to set the base URL in your client to: "+PROXY_URL+"\n"+ + "Server is caching following url: "+baseURL) + }) + + logging.Info("Started router at ", port) + router.Run(port) +} + +func parseOpenAPIRoutes(openapifile []byte, router *gin.Engine) { + + // load all routes from the file loader := openapi3.NewLoader() - doc, err := loader.LoadFromData(openApiRoutes) + doc, err := loader.LoadFromData(openapifile) if err != nil { - log.Fatalf("Failed to load OpenAPI: %v (make sure to run 'go embed')", err) + log.Fatalf("Failed to load OpenAPI: %v", err) } - // below imports the OpenAPI spec into the router, and does some processing to prevent errors - // create a regex to convert OpenAPI path parameters {id} to Gin's format :id re := regexp.MustCompile(`\{(.+?)\}`) @@ -196,16 +212,4 @@ func main() { logging.Info(fmt.Sprintf("Registered %d routes from OpenAPI spec", len(doc.Paths.InMatchingOrder()))) - // Proxy files from S3, if not save them. - router.GET("/proxy/:fileId", proxyFile) - - router.GET("/", func(c *gin.Context) { - c.String(200, "e6-cache is running. Use this as the instance in your preffered client.\n"+ - "Make sure to set the base URL in your client to: "+PROXY_URL+"\n"+ - "Server is caching following url: "+baseURL) - }) - - logging.Info("Started router at ", port) - router.Run(port) - } diff --git a/src/openapi.yaml b/src/openapi/e621.yaml similarity index 100% rename from src/openapi.yaml rename to src/openapi/e621.yaml diff --git a/update_openapi.sh b/update_openapi.sh index 9d678c4..cd9d704 100644 --- a/update_openapi.sh +++ b/update_openapi.sh @@ -1,3 +1,5 @@ -rm ./src/openapi.yaml +echo "Deleting old routes" +rm ./src/openapi/e621.yaml -curl -o ./src/openapi.yaml https://raw.githubusercontent.com/DonovanDMC/E621OpenAPI/master/openapi.yaml +echo "Updating e621 routes" +curl -o ./src/openapi/e621.yaml https://raw.githubusercontent.com/DonovanDMC/E621OpenAPI/master/openapi.yaml From 046747aabfe8eddd66c317bb45470cc6827e60cc Mon Sep 17 00:00:00 2001 From: Lowikian Date: Thu, 29 May 2025 13:51:39 +0200 Subject: [PATCH 10/18] reword the readme slightly --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5ef29d3..9d25b06 100644 --- a/README.md +++ b/README.md @@ -64,13 +64,13 @@ After the container is running, you can access the API at `http://localhost:8080 For most users i recommend using [e1547](https://github.com/clragon/e1547) as it has built-in support for custom instances. -### e1547 Setup +### e1547 > [!IMPORTANT] > You need to have an public URL for the API to work with e1547, as it requires https. https://github.com/user-attachments/assets/d2304e64-0c08-4065-bd55-aaa24d13727e -### The Wolf's Stash Setup +### The Wolf's Stash For The Wolf's Stash, it just reports the host not being supported. ### Other Clients From 731a063d6dc034dd7713a2fdc53ca947259391b6 Mon Sep 17 00:00:00 2001 From: Lowikian Date: Thu, 29 May 2025 18:26:01 +0200 Subject: [PATCH 11/18] Add copyright notice from e621 OpenAPI specification --- THIRD_PARTY.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 THIRD_PARTY.md diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md new file mode 100644 index 0000000..9d4df6c --- /dev/null +++ b/THIRD_PARTY.md @@ -0,0 +1,5 @@ +# Third Party Libraries / Assets + +This project wouldn't be possible without the following third party libraries and assets: + +- e621 OpenAPI Specification from [https://github.com/DonovanDMC/E621OpenAPI](https://github.com/DonovanDMC/E621OpenAPI) (License: MIT) \ No newline at end of file From cafeabca9186be8b67fe7302f81b08813dc8d76c Mon Sep 17 00:00:00 2001 From: Lowikian Date: Fri, 30 May 2025 00:06:24 +0200 Subject: [PATCH 12/18] small comment update for clarity --- docker-compose.yml | 2 +- src/.env.example | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ed29f13..97b4571 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,7 +41,7 @@ services: # Proxy settings PROXY_URL: http://localhost:8080 # Set this to the Server IP / URL, as otherwise the proxy will not work. E6_BASE: https://e621.net - PROXY_AUTH: "" # Leave empty to disable proxy auth append like this to your username "Username:YourPassword" + PROXY_AUTH: "" # Leave empty to disable proxy auth. If you want to use it, append like this to your username "Username:YourProxyPassword" ports: - "8080:8080" # Point this to an Reverse Proxy and set the Proxy Url acordingly. diff --git a/src/.env.example b/src/.env.example index 7954167..58c5cc5 100644 --- a/src/.env.example +++ b/src/.env.example @@ -17,4 +17,4 @@ S3_REGION=us-east-1 # Proxy settings PROXY_URL=http://localhost:8080 E6_BASE=https://e621.net -PROXY_AUTH="" # Leave empty to disable proxy auth append like this to your username "Username:YourPassword" \ No newline at end of file +PROXY_AUTH="" # Leave empty to disable proxy auth. If you want to use it, append like this to your username "Username:YourProxyPassword" \ No newline at end of file From 6eb0d90f46998d5a1ba3af6888671cc71c6c9bd6 Mon Sep 17 00:00:00 2001 From: Lowikian Date: Sun, 1 Jun 2025 14:55:05 +0200 Subject: [PATCH 13/18] base64 is not encryption --- development.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/development.md b/development.md index e357e39..3964319 100644 --- a/development.md +++ b/development.md @@ -14,7 +14,7 @@ The core concept of the implementation looks like this: ## File Proxying Process File Proxying works like this: -1. Check the Signature and decode the base64 encrypted url +1. Check the Signature and decode the base64 encoded url 2. Check in S3 if the file exists 3. If not, then request it and save it while forwarding it to the client. If it exist than stream it to the client from S3. From d2c7bfe8f14bc6777b23f3a71d9cea72cd29527f Mon Sep 17 00:00:00 2001 From: Lowikian Date: Sun, 1 Jun 2025 18:46:41 +0200 Subject: [PATCH 14/18] add go test on push --- .github/workflows/go-test.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/go-test.yml diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml new file mode 100644 index 0000000..f8a6bc9 --- /dev/null +++ b/.github/workflows/go-test.yml @@ -0,0 +1,27 @@ +name: Upload Go test results +# https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-go +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + go-version: [ '1.24.3' ] + + steps: + - uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + - name: Install dependencies + run: go get . + - name: Test with Go + run: go test -json > TestResults-${{ matrix.go-version }}.json + - name: Upload Go test results + uses: actions/upload-artifact@v4 + with: + name: Go-results-${{ matrix.go-version }} + path: TestResults-${{ matrix.go-version }}.json From 7905d60e958699c2cd20b20345fcc831da0337c9 Mon Sep 17 00:00:00 2001 From: Lowikian Date: Sun, 1 Jun 2025 18:49:06 +0200 Subject: [PATCH 15/18] try fixing go test --- .github/workflows/go-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index f8a6bc9..563c3b4 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -16,6 +16,8 @@ jobs: uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} + - name: Change in to src + run: cd src - name: Install dependencies run: go get . - name: Test with Go From ef8b4dc40dccf2a1436f0a98c8719562ac6ded70 Mon Sep 17 00:00:00 2001 From: Lowikian Date: Sun, 1 Jun 2025 18:53:25 +0200 Subject: [PATCH 16/18] trying to fix the testing again --- .github/workflows/go-test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index 563c3b4..f89fd89 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -16,14 +16,14 @@ jobs: uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - - name: Change in to src - run: cd src - name: Install dependencies run: go get . + working-directory: ./src - name: Test with Go run: go test -json > TestResults-${{ matrix.go-version }}.json + working-directory: ./src - name: Upload Go test results uses: actions/upload-artifact@v4 with: name: Go-results-${{ matrix.go-version }} - path: TestResults-${{ matrix.go-version }}.json + path: src/TestResults-${{ matrix.go-version }}.json From f091c3f36c51e23b85fd2a4ba09855dd6a952a0b Mon Sep 17 00:00:00 2001 From: Lowikian Date: Sun, 1 Jun 2025 19:04:41 +0200 Subject: [PATCH 17/18] fix formatting --- src/db.go | 21 ++++++++++----------- src/http_calls.go | 31 +++++++++++++++---------------- src/main.go | 24 ++++++++++++------------ 3 files changed, 37 insertions(+), 39 deletions(-) diff --git a/src/db.go b/src/db.go index 222fc6c..fed58f6 100644 --- a/src/db.go +++ b/src/db.go @@ -5,7 +5,6 @@ import ( "context" "database/sql" "fmt" - "log" "strings" "time" @@ -80,23 +79,23 @@ func (d *DB) CreatePost(ctx context.Context, p *Post) error { ) if err != nil { - logging.Error("Error inserting post: ", err) + logging.Error("Error inserting post: %v", err) } return err } func (d *DB) CheckAndInsertPost(ctx context.Context, p *Post) error { - logging.Info("Checking if post exists: ", p.ID) + logging.Info("Checking if post exists: %v", p.ID) const existsQuery = `SELECT 1 FROM posts WHERE id = $1` row := d.db.QueryRowContext(ctx, existsQuery, p.ID) var dummy int err := row.Scan(&dummy) switch { case err == sql.ErrNoRows: - logging.Info("Post does not exist, inserting: ", p.ID) + logging.Error("Post does not exist, inserting: %v", p.ID) return d.CreatePost(ctx, p) case err != nil: - log.Println("Error checking post existence: ", err) + logging.Error("Error checking post existence: %v", err) return err default: // Row exists, nothing to do @@ -143,7 +142,7 @@ func (d *DB) SaveComments(comments []Comment) error { c.UpdaterName, ) if err != nil { - logging.Error(err.Error()) + logging.Error("%v", err.Error()) return err } } @@ -227,7 +226,7 @@ func (d *DB) UpdatePost(ctx context.Context, p *Post) error { p.ApproverID, p.UploaderID, p.Description, p.CommentCount, p.IsFavorited, ) if err != nil { - logging.Error("Error updating post: ", err) + logging.Error("Error updating post: %v", err) } return err } @@ -236,7 +235,7 @@ func (d *DB) UpdatePost(ctx context.Context, p *Post) error { func (d *DB) DeletePost(ctx context.Context, id int64) error { _, err := d.db.ExecContext(ctx, `DELETE FROM posts WHERE id = $1`, id) if err != nil { - logging.Error("Error deleting post: ", err) + logging.Error("Error deleting post: %v", err) } return err } @@ -271,20 +270,20 @@ func (d *DB) UpdatePool(ctx context.Context, p *Pool) error { p.Description, p.IsActive, p.Category, p.PostCount, ) if err != nil { - logging.Error("error upserting pool: ", err) + logging.Error("error upserting pool: %v", err) return err } _, err = tx.ExecContext(ctx, `DELETE FROM pool_posts WHERE pool_id = $1`, p.ID) if err != nil { - logging.Error("error clearing pool_posts: ", err) + logging.Error("error clearing pool_posts: %v", err) return err } for _, postID := range p.PostIDs { _, err = tx.ExecContext(ctx, `INSERT INTO pool_posts (pool_id, post_id) VALUES ($1, $2)`, p.ID, postID) if err != nil { - logging.Error("error inserting pool_post: ", err) + logging.Error("error inserting pool_post: %v", err) return err } } diff --git a/src/http_calls.go b/src/http_calls.go index b12d01a..1fa1934 100644 --- a/src/http_calls.go +++ b/src/http_calls.go @@ -47,15 +47,14 @@ func makeProxyLink(original string) string { // if the route changes, we need to update this proxiedURL := PROXY_URL + "/proxy/" + encodedUrl + "?sig=" + sig - - logging.Info("Creating proxy url for file: ", original, " | ID: ", match[1], " | Proxied URL: ", proxiedURL) + logging.Info("Creating proxy url for file: %v | ID: %v | Proxied URL: %v", original, match[1], proxiedURL) return proxiedURL } func proxyAndTransform(c *gin.Context) { - logging.Debug("Headers: ", c.Request.Header) + logging.Debug("Headers: %v", c.Request.Header) auth := c.Request.Header.Get("Authorization") @@ -73,14 +72,14 @@ func proxyAndTransform(c *gin.Context) { auth = strings.TrimPrefix(auth, "Basic ") decodedAuth, err := base64.StdEncoding.DecodeString(auth) if err != nil { - logging.Error("Error decoding auth header: ", err) + logging.Error("Error decoding auth header: %v", err) c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } // parse it into username, password and proxy auth authParts := strings.Split(string(decodedAuth), ":") suppliedProxyAuth := authParts[1] // to change the password position you need to change this here. 1 is after the username, 2 is after the proxy auth - logging.Debug("Parsed Proxy Authorization header: ", authParts) + logging.Debug("Parsed Proxy Authorization header: %v", authParts) if len(authParts) != 3 || suppliedProxyAuth != PROXY_AUTH { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return @@ -127,8 +126,8 @@ func proxyAndTransform(c *gin.Context) { // useragent stuff setUseragent(requestUsername, req) - logging.Debug("Host: ", c.Request.Host) - logging.Debug("Proxied Headers: ", req.Header) + logging.Debug("Host: %v", c.Request.Host) + logging.Debug("Proxied Headers: %v", req.Header) // Perform request resp, err := http.DefaultClient.Do(req) @@ -193,7 +192,7 @@ func proxyAndTransform(c *gin.Context) { return } - logging.Debug("Response Body: ", string(respBody)) + logging.Debug("Response Body: %v", string(respBody)) switch { case strings.HasSuffix(c.Request.URL.Path, "/comments.json") && c.Query("search[post_id]") != "": // specific post comments are returned differently @@ -211,14 +210,14 @@ func proxyAndTransform(c *gin.Context) { return } - logging.Info("Saving ", len(comments), " comments") + logging.Info("Saving %v comments", len(comments)) Database.SaveComments(comments) respBody, _ = json.Marshal(comments) case strings.HasSuffix(c.Request.URL.Path, "/posts.json") || strings.HasSuffix(c.Request.URL.Path, "/comments.json"): // comments and posts seem to be the same thing var posts PostsResponse if err := json.Unmarshal(respBody, &posts); err != nil { - logging.Debug("Response Body: ", string(respBody)) + logging.Debug("Response Body: %v", string(respBody)) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid response format", "ok": false}) return } @@ -233,7 +232,7 @@ func proxyAndTransform(c *gin.Context) { if err := json.Unmarshal(respBody, &post); err != nil { - logging.Debug("Response Body: ", string(respBody)) + logging.Debug("Response Body: %v", string(respBody)) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid response format", "ok": false}) return } @@ -308,11 +307,11 @@ func proxyFile(c *gin.Context) { c.Header("Expires", time.Now().Add(time.Duration(maxCacheAge)*time.Second).Format(http.TimeFormat)) if fileExists && err == nil { - logging.Info("File exists in S3, downloading: ", string(url)) + logging.Info("File exists in S3, downloading: %v", string(url)) body, err := S3.StreamFromS3(c, string(CleanFileID)) if err != nil { - logging.Error("Error downloading from S3: ", err) + logging.Error("Error downloading from S3: %v", err) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Failed to download from S3", "ok": false}) return } @@ -386,12 +385,12 @@ func proxyFile(c *gin.Context) { // upload to S3 in the background, while the user is downloading the file go func() { - logging.Info("Uploading to S3: ", string(CleanFileID)) + logging.Info("Uploading to S3: %v", string(CleanFileID)) err := S3.UploadToS3(c, r1, string(CleanFileID)) if err != nil { - logging.Error("Failed to upload to S3", err) + logging.Error("Failed to upload to S3: %v", err) } - logging.Info("Upload to S3 complete: ", string(CleanFileID)) + logging.Info("Upload to S3 complete: %v", string(CleanFileID)) }() // Stream live to user (I hope it's actually streaming) diff --git a/src/main.go b/src/main.go index d6edd51..e4e04e6 100644 --- a/src/main.go +++ b/src/main.go @@ -89,7 +89,7 @@ func loadEnv() { PROXY_AUTH = os.Getenv("PROXY_AUTH") if PROXY_AUTH != "" { - logging.Info("Proxy auth is enabled with key: ", PROXY_AUTH) + logging.Info("Proxy auth is enabled with key: %v", PROXY_AUTH) } else { logging.Info("Proxy auth is disabled") } @@ -105,13 +105,13 @@ func main() { // generate signing key Key = signer.GenerateSecretKey() Signer = signer.NewSigner(Key) - logging.Debug("Generated key: ", Key) + logging.Debug("Generated key: %v", Key) // setup db logging.Info("Connecting to DB...") d, err := newDB(DB_HOST, DB_NAME, DB_USER, DB_PASS, DB_PORT) if err != nil { - logging.Info("Failed to connect to DB (is it up?): ", err) + logging.Info("Failed to connect to DB (is it up?): %v", err) return } Database = d @@ -123,7 +123,7 @@ func main() { defer cancel() s3Svc, err := NewS3Service(ctx, S3_REGION, S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET_NAME) if err != nil { - logging.Fatal("Failed to connect to S3: ", err) + logging.Fatal("Failed to connect to S3: %v", err) } S3 = *s3Svc logging.Info("Connected to S3!") @@ -147,7 +147,7 @@ func main() { "Server is caching following url: "+baseURL) }) - logging.Info("Started router at ", port) + logging.Info("Started router at %v", port) router.Run(port) } @@ -171,10 +171,10 @@ func parseOpenAPIRoutes(openapifile []byte, router *gin.Engine) { for _, path := range doc.Paths.InMatchingOrder() { pathItem := doc.Paths.Find(path) if pathItem == nil { - logging.Warn("Path item not found for path: ", path) + logging.Warn("Path item not found for path: %v", path) continue } - logging.Debug("Processing path: ", path) + logging.Debug("Processing path: %v", path) // convert OpenAPI parameter syntax to Gin parameter syntax convertedPath := re.ReplaceAllString(path, ":$1") @@ -193,23 +193,23 @@ func parseOpenAPIRoutes(openapifile []byte, router *gin.Engine) { // this is here to preserve cases like /posts.json if strings.HasSuffix(extRegex.ReplaceAllString(convertedPath, ""), param) && param != "" { convertedPath = extRegex.ReplaceAllString(convertedPath, "") - logging.Debug("Removed extension from path: ", convertedPath) + logging.Debug("Removed extension from path: %v", convertedPath) } else { - logging.Debug("Preserving extension in path: ", convertedPath, " because it's a parameter or doesn't have an extension") + logging.Debug("Preserving extension in path: %v because it's a parameter or doesn't have an extension", convertedPath) } if slices.Contains(registeredRoutes, convertedPath) { - logging.Warn("Duplicate route detected: ", convertedPath) + logging.Warn("Duplicate route detected: %v", convertedPath) continue } registeredRoutes = append(registeredRoutes, convertedPath) for method := range pathItem.Operations() { - logging.Debug("Adding route: ", method, " ", convertedPath) + logging.Debug("Adding route: %v %v", method, convertedPath) router.Handle(method, convertedPath, proxyAndTransform) } } - logging.Info(fmt.Sprintf("Registered %d routes from OpenAPI spec", len(doc.Paths.InMatchingOrder()))) + logging.Info("Registered %d routes from OpenAPI spec", len(doc.Paths.InMatchingOrder())) } From 6c5ed018cc90a738e284adcf570bb962b524af82 Mon Sep 17 00:00:00 2001 From: lowikian <38097264+bugmaschine@users.noreply.github.com> Date: Sun, 1 Jun 2025 19:07:01 +0200 Subject: [PATCH 18/18] Potential fix for code scanning alert no. 3: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: lowikian <38097264+bugmaschine@users.noreply.github.com> --- .github/workflows/go-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index f89fd89..b77d9d5 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -1,6 +1,8 @@ name: Upload Go test results # https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-go on: [push] +permissions: + contents: read jobs: build: