Commit 7f1ab1b73d

7f1ab1b73d1e7a0d8a730354bbc3136c5584fea3

parent: 8b839f9406

Unsigned

lost+skunk <skunky@ebloid.ru> · 2024-07-05 21:46 UTC

Реализация конфига

Layout: unified · split

app/config.go +50 −19
@@ -1,26 +1,31 @@
1package app 1package app
2 2
3import ( 3import (
4 "encoding/json"
5 "errors"
4 "os" 6 "os"
5) 7)
6 8
7type cache_config struct { 9type cache_config struct {
8 Enabled bool 10 Enabled bool
9 Path string 11 Path string
10 Max_size, Lifetime int64 12 MaxSize int64 `json:"max-size"`
13 Lifetime int64
11} 14}
12 15
13type config struct { 16type config struct {
14 cfg string 17 cfg string
15 Listen, Base_uri string 18 Listen string
16 Cache cache_config 19 BasePath string `json:"base-path"`
17 Proxy, Nsfw bool 20 Cache cache_config
21 Proxy, Nsfw bool
22 WixmpProxy string `json:"wixmp-proxy"`
18} 23}
19 24
20var CFG = config{ 25var CFG = config{
21 cfg: "config.json", 26 cfg: "config.json",
22 Listen: "127.0.0.1:3003", 27 Listen: "127.0.0.1:3003",
23 Base_uri: "/", 28 BasePath: "/",
24 Cache: cache_config{ 29 Cache: cache_config{
25 Enabled: true, 30 Enabled: true,
26 Path: "cache", 31 Path: "cache",
@@ -29,20 +34,46 @@ var CFG = config{
29 Nsfw: true, 34 Nsfw: true,
30} 35}
31 36
32func execcfg() { 37func ExecuteConfig() {
38 try := func(err error, exitcode int) {
39 if err != nil {
40 println(err.Error())
41 os.Exit(exitcode)
42 }
43 }
44
33 a := os.Args 45 a := os.Args
34 for num, val := range a { 46 if l := len(a); l > 1 {
35 switch val { 47 switch a[1] {
36 case "-conf": 48 case "-c", "--config":
37 CFG.cfg = a[num] 49 if l >= 3 {
38 case "-help": 50 CFG.cfg = a[2]
39 println(`SkunkyArt v 1.3 [refactoring] 51 } else {
52 try(errors.New("Not enought arguments"), 1)
53 }
54 case "-h", "--help":
55 try(errors.New(`SkunkyArt v1.3 [refactoring]
40Usage: 56Usage:
41 - -conf - path to config 57 - [-c|--config] - path to config
42 - -help this message 58 - [-h|--help] - returns this message
43Example: 59Example:
44 ./skunkyart -conf config.json 60 ./skunkyart -c config.json
45Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3`) 61Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3`), 0)
62 default:
63 try(errors.New("Unreconginzed argument: "+a[1]), 1)
64 }
65 if CFG.cfg != "" {
66 f, err := os.ReadFile(CFG.cfg)
67 try(err, 1)
68
69 try(json.Unmarshal(f, &CFG), 1)
70 if CFG.Cache.Enabled && !CFG.Proxy {
71 try(errors.New("Incompatible settings detected: cannot use caching media content without proxy"), 1)
72 }
73
74 if CFG.Cache.MaxSize != 0 || CFG.Cache.Lifetime != 0 {
75 go InitCacheSystem()
76 }
46 } 77 }
47 } 78 }
48} 79}
app/router.go +21 −9
@@ -9,11 +9,14 @@ import (
9 "strings" 9 "strings"
10) 10)
11 11
12const addr string = "0.0.0.0:3003"
13
14// роутер
15func Router() { 12func Router() {
16 parsepath := func(path string) map[int]string { 13 parsepath := func(path string) map[int]string {
14 if l := len(CFG.BasePath); len(path) > l {
15 path = path[l-1:]
16 } else {
17 path = "/"
18 }
19
17 parsedpath := make(map[int]string) 20 parsedpath := make(map[int]string)
18 for x := 0; true; x++ { 21 for x := 0; true; x++ {
19 slash := strings.Index(path, "/") + 1 22 slash := strings.Index(path, "/") + 1
@@ -52,16 +55,20 @@ func Router() {
52 var skunky skunkyart 55 var skunky skunkyart
53 skunky.Args = r.URL.Query() 56 skunky.Args = r.URL.Query()
54 skunky.Writer = w 57 skunky.Writer = w
58 skunky.BasePath = CFG.BasePath
55 59
56 arg := skunky.Args.Get 60 arg := skunky.Args.Get
57 skunky.Query = u.QueryEscape(arg("q")) 61 skunky.QueryRaw = arg("q")
62 skunky.Query = u.QueryEscape(skunky.QueryRaw)
63
58 if t := arg("type"); len(t) > 0 { 64 if t := arg("type"); len(t) > 0 {
59 skunky.Type = rune(t[0]) 65 skunky.Type = rune(t[0])
60 } 66 }
61 p, _ := strconv.Atoi(arg("p")) 67 p, _ := strconv.Atoi(arg("p"))
62 skunky.Page = p 68 skunky.Page = p
69
63 if arg("atom") == "true" { 70 if arg("atom") == "true" {
64 skunky.atom = true 71 skunky.Atom = true
65 } 72 }
66 73
67 // пути 74 // пути
@@ -69,7 +76,7 @@ func Router() {
69 default: 76 default:
70 skunky.ReturnHTTPError(404) 77 skunky.ReturnHTTPError(404)
71 case "": 78 case "":
72 open_n_send("html/index.htm") 79 skunky.ExecuteTemplate("html/index.htm", &CFG.BasePath)
73 case "post": 80 case "post":
74 skunky.Deviation(path[2], path[3]) 81 skunky.Deviation(path[2], path[3])
75 case "search": 82 case "search":
@@ -80,9 +87,14 @@ func Router() {
80 skunky.GRUser() 87 skunky.GRUser()
81 88
82 case "media": 89 case "media":
83 skunky.Emojitar(path[2]) 90 switch path[2] {
91 case "file":
92 skunky.DownloadAndSendMedia(path[3], next(path, 4))
93 case "emojitar":
94 skunky.Emojitar(path[3])
95 }
84 case "about": 96 case "about":
85 open_n_send("html/about.htm") 97 skunky.About()
86 case "gui": 98 case "gui":
87 w.Header().Add("content-type", "text/css") 99 w.Header().Add("content-type", "text/css")
88 open_n_send(next(path, 2)) 100 open_n_send(next(path, 2))
@@ -90,7 +102,7 @@ func Router() {
90 } 102 }
91 103
92 http.HandleFunc("/", handle) 104 http.HandleFunc("/", handle)
93 http.ListenAndServe(addr, nil) 105 http.ListenAndServe(CFG.Listen, nil)
94} 106}
95 107
96func err(e error) { 108func err(e error) {
app/util.go +186 −71
@@ -1,11 +1,17 @@
1package app 1package app
2 2
3import ( 3import (
4 "encoding/base64"
4 "encoding/json" 5 "encoding/json"
6 "io"
5 "net/http" 7 "net/http"
8 u "net/url"
9 "os"
6 "strconv" 10 "strconv"
7 "strings" 11 "strings"
12 "syscall"
8 "text/template" 13 "text/template"
14 "time"
9 15
10 "git.macaw.me/skunky/devianter" 16 "git.macaw.me/skunky/devianter"
11) 17)
@@ -19,11 +25,15 @@ func (s skunkyart) ExecuteTemplate(file string, data any) {
19 wr(s.Writer, buf.String()) 25 wr(s.Writer, buf.String())
20} 26}
21 27
22func (s skunkyart) UrlBuilder(strs ...string) string { 28func UrlBuilder(strs ...string) string {
23 var str strings.Builder 29 var str strings.Builder
24 for _, x := range strs { 30 l := len(strs)
31 str.WriteString(CFG.BasePath)
32 for n, x := range strs {
25 str.WriteString(x) 33 str.WriteString(x)
26 str.WriteString("/") 34 if n+1 < l && !(strs[n+1][0] == '?' || strs[n+1][0] == '&') && !(x[0] == '?' || x[0] == '&') {
35 str.WriteString("/")
36 }
27 } 37 }
28 return str.String() 38 return str.String()
29} 39}
@@ -31,10 +41,10 @@ func (s skunkyart) UrlBuilder(strs ...string) string {
31func (s skunkyart) ReturnHTTPError(status int) { 41func (s skunkyart) ReturnHTTPError(status int) {
32 s.Writer.WriteHeader(status) 42 s.Writer.WriteHeader(status)
33 43
34 // пострйока с помощью strings.Builder, потому что такой метод быстрее обычного сложения
35 var msg strings.Builder 44 var msg strings.Builder
36 msg.WriteString(`<html><link rel="stylesheet" href="/gui/css/skunky.css" />`) 45 msg.WriteString(`<html><link rel="stylesheet" href="`)
37 msg.WriteString("<h1>") 46 msg.WriteString(UrlBuilder("gui", "css", "skunky.css"))
47 msg.WriteString(`" /><h1>`)
38 msg.WriteString(strconv.Itoa(status)) 48 msg.WriteString(strconv.Itoa(status))
39 msg.WriteString(" - ") 49 msg.WriteString(" - ")
40 msg.WriteString(http.StatusText(status)) 50 msg.WriteString(http.StatusText(status))
@@ -43,6 +53,15 @@ func (s skunkyart) ReturnHTTPError(status int) {
43 wr(s.Writer, msg.String()) 53 wr(s.Writer, msg.String())
44} 54}
45 55
56func (s skunkyart) ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
57 if len(url) > 32 && url[27:32] != "stash" {
58 url = url[27:]
59 toart := strings.Index(url, "/art/")
60 output = UrlBuilder("post", url[:toart], url[toart+5:])
61 }
62 return
63}
64
46type text struct { 65type text struct {
47 TXT string 66 TXT string
48 from int 67 from int
@@ -200,80 +219,78 @@ func (s skunkyart) NavBase(c dlist) string {
200 219
201func (s skunkyart) DeviationList(devs []devianter.Deviation, content ...dlist) string { 220func (s skunkyart) DeviationList(devs []devianter.Deviation, content ...dlist) string {
202 var list strings.Builder 221 var list strings.Builder
203 if !s.atom { 222 if s.Atom && s.Page > 1 {
204 list.WriteString(`<div class="content">`) 223 s.ReturnHTTPError(400)
205 } else { 224 return ""
206 list.WriteString(`<?xml version="1.0" encoding="UTF-8"?><feed xmlns:media="http://search.yahoo.com/mrss" xmlns="http://www.w3.org/2005/Atom">`) 225 } else if s.Atom {
226 list.WriteString(`<?xml version="1.0" encoding="UTF-8"?><feed xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">`)
207 list.WriteString(`<title>SkunkyArt</title>`) 227 list.WriteString(`<title>SkunkyArt</title>`)
208 // list.WriteString(`<link rel="alternate" href="HOMEPAGE_URL"/><link href="FEED_URL" rel="self"/>`) 228 // list.WriteString(`<link rel="alternate" href="HOMEPAGE_URL"/><link href="FEED_URL" rel="self"/>`)
229 } else {
230 list.WriteString(`<div class="content">`)
209 } 231 }
210 for _, data := range devs { 232 for _, data := range devs {
211 url := devianter.UrlFromMedia(data.Media) 233 if !(data.NSFW && !CFG.Nsfw) {
212 if s.atom { 234 url := s.ParseMedia(data.Media)
213 id := strconv.Itoa(data.ID) 235 if s.Atom {
214 list.WriteString(`<entry><author><name>`) 236 id := strconv.Itoa(data.ID)
215 list.WriteString(data.Author.Username) 237 list.WriteString(`<entry><author><name>`)
216 list.WriteString(`</name></author><title>`) 238 list.WriteString(data.Author.Username)
217 list.WriteString(data.Title) 239 list.WriteString(`</name></author><title>`)
218 list.WriteString(`</title><link rel="alternate" type="text/html" href="`) 240 list.WriteString(data.Title)
219 list.WriteString(CFG.Base_uri) 241 list.WriteString(`</title><link rel="alternate" type="text/html" href="`)
220 list.WriteString("/post/") 242 list.WriteString(UrlBuilder("post", data.Author.Username, "atom-"+id))
221 list.WriteString(data.Author.Username) 243 list.WriteString(`"/><id>`)
222 list.WriteString("/atom-") 244 list.WriteString(id)
223 list.WriteString(id) 245 list.WriteString(`</id><published>`)
224 list.WriteString(`"/><id>`) 246 list.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
225 list.WriteString(id) 247 list.WriteString(`</published>`)
226 list.WriteString(`</id><published>`) 248 list.WriteString(`<media:group><media:title>`)
227 list.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700")) 249 list.WriteString(data.Title)
228 list.WriteString(`</published>`) 250 list.WriteString(`</media:title><media:thumbinal url="`)
229 list.WriteString(`<media:group><media:title>`)
230 list.WriteString(data.Title)
231 list.WriteString(`</media:title><media:thumbinal url="`)
232 list.WriteString(url)
233 list.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`)
234 list.WriteString(data.Url)
235 list.WriteString(`"><img src="`)
236 list.WriteString(url)
237 list.WriteString(`"/></a><p>`)
238 list.WriteString(ParseDescription(data.TextContent))
239 list.WriteString(`</p></div></content></entry>`)
240 } else {
241 list.WriteString(`<div class="block">`)
242 if url != "" {
243 list.WriteString(`<a title="open/download" href="`)
244 list.WriteString(url) 251 list.WriteString(url)
252 list.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`)
253 list.WriteString(data.Url)
245 list.WriteString(`"><img src="`) 254 list.WriteString(`"><img src="`)
246 list.WriteString(url) 255 list.WriteString(url)
247 list.WriteString(`" width="15%"></a>`) 256 list.WriteString(`"/></a><p>`)
257 list.WriteString(ParseDescription(data.TextContent))
258 list.WriteString(`</p></div></content></entry>`)
248 } else { 259 } else {
249 list.WriteString(`<h1>[ TEXT ]</h1>`) 260 list.WriteString(`<div class="block">`)
250 } 261 if url != "" {
251 list.WriteString(`<br><a href="`) 262 list.WriteString(`<a title="open/download" href="`)
252 list.WriteString("/post/") 263 list.WriteString(url)
253 list.WriteString(data.Author.Username) 264 list.WriteString(`"><img src="`)
254 list.WriteString("/") 265 list.WriteString(url)
255 list.WriteString(data.Url[27:][strings.Index(data.Url[27:], "/art/")+5:]) 266 list.WriteString(`" width="15%"></a>`)
256 list.WriteString(`">`) 267 } else {
257 list.WriteString(data.Author.Username) 268 list.WriteString(`<h1>[ TEXT ]</h1>`)
258 list.WriteString(" - ") 269 }
259 list.WriteString(data.Title) 270 list.WriteString(`<br><a href="`)
271 list.WriteString(s.ConvertDeviantArtUrlToSkunkyArt(data.Url))
272 list.WriteString(`">`)
273 list.WriteString(data.Author.Username)
274 list.WriteString(" - ")
275 list.WriteString(data.Title)
276
277 // шильдики нсфв, аи и ежедневного поста
278 if data.NSFW {
279 list.WriteString(` [<span class="nsfw">NSFW</span>]`)
280 }
281 if data.AI {
282 list.WriteString(" [🤖]")
283 }
284 if data.DD {
285 list.WriteString(` [<span class="dd">DD</span>]`)
286 }
260 287
261 // шильдики нсфв, аи и ежедневного поста 288 list.WriteString("</a></div>")
262 if data.NSFW {
263 list.WriteString(` [<span class="nsfw">NSFW</span>]`)
264 }
265 if data.AI {
266 list.WriteString(" [🤖]")
267 }
268 if data.DD {
269 list.WriteString(` [<span class="dd">DD</span>]`)
270 } 289 }
271
272 list.WriteString("</a></div>")
273 } 290 }
274 } 291 }
275 292
276 if s.atom { 293 if s.Atom {
277 list.WriteString("</feed>") 294 list.WriteString("</feed>")
278 s.Writer.Write([]byte(list.String())) 295 s.Writer.Write([]byte(list.String()))
279 return "" 296 return ""
@@ -301,10 +318,10 @@ func (s skunkyart) ParseComments(c devianter.Comments) string {
301 } 318 }
302 cmmts.WriteString(`"><p id="`) 319 cmmts.WriteString(`"><p id="`)
303 cmmts.WriteString(strconv.Itoa(x.ID)) 320 cmmts.WriteString(strconv.Itoa(x.ID))
304 cmmts.WriteString(`"><img src="/media/`) 321 cmmts.WriteString(`"><img src="`)
305 cmmts.WriteString(x.User.Username) 322 cmmts.WriteString(UrlBuilder("media", "emojitar", x.User.Username, "?type=a"))
306 cmmts.WriteString(`?type=a" width="30px" height="30px"><a href="/group_user?type=about&q=`) 323 cmmts.WriteString(`" width="30px" height="30px"><a href="`)
307 cmmts.WriteString(x.User.Username) 324 cmmts.WriteString(UrlBuilder("group_user", "?q=", x.User.Username, "&type=a"))
308 cmmts.WriteString(`"><b`) 325 cmmts.WriteString(`"><b`)
309 cmmts.WriteString(` class="`) 326 cmmts.WriteString(` class="`)
310 if x.User.Banned { 327 if x.User.Banned {
@@ -346,3 +363,101 @@ func (s skunkyart) ParseComments(c devianter.Comments) string {
346 cmmts.WriteString("</details>") 363 cmmts.WriteString("</details>")
347 return cmmts.String() 364 return cmmts.String()
348} 365}
366
367func (s skunkyart) ParseMedia(media devianter.Media) string {
368 url := devianter.UrlFromMedia(media)
369 if len(url) != 0 {
370 url = url[21:]
371 dot := strings.Index(url, ".")
372
373 return UrlBuilder("media", "file", url[:dot], "/", url[dot+10:])
374 }
375 return ""
376}
377
378func (s skunkyart) DownloadAndSendMedia(subdomain, path string) {
379 var url strings.Builder
380 url.WriteString("https://images-wixmp-")
381 url.WriteString(subdomain)
382 url.WriteString(".wixmp.com/")
383 url.WriteString(path)
384 url.WriteString("?token=")
385 url.WriteString(s.Args.Get("token"))
386
387 download := func() (body []byte, status int, headers http.Header) {
388 cli := &http.Client{}
389 if CFG.WixmpProxy != "" {
390 u, e := u.Parse(CFG.WixmpProxy)
391 err(e)
392 cli.Transport = &http.Transport{Proxy: http.ProxyURL(u)}
393 }
394
395 req, e := http.NewRequest("GET", url.String(), nil)
396 err(e)
397 req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0.0")
398
399 resp, e := cli.Do(req)
400 err(e)
401 defer resp.Body.Close()
402
403 b, e := io.ReadAll(resp.Body)
404 err(e)
405 return b, resp.StatusCode, resp.Header
406 }
407
408 if CFG.Cache.Enabled {
409 os.Mkdir(CFG.Cache.Path, 0700)
410 fname := CFG.Cache.Path + "/" + base64.StdEncoding.EncodeToString([]byte(subdomain+path))
411 file, e := os.Open(fname)
412
413 if e != nil {
414 b, status, headers := download()
415 if status == 200 && headers["Content-Type"][0][:5] == "image" {
416 err(os.WriteFile(fname, b, 0700))
417 s.Writer.Write(b)
418 }
419 } else {
420 file, e := io.ReadAll(file)
421 err(e)
422 s.Writer.Write(file)
423 }
424 } else if CFG.Proxy {
425 b, _, _ := download()
426 s.Writer.Write(b)
427 } else {
428 s.Writer.Write([]byte("Sorry, butt proxy on this instance disabled."))
429 }
430}
431
432func InitCacheSystem() {
433 c := &CFG.Cache
434 for {
435 dir, e := os.Open(c.Path)
436 err(e)
437 stat, e := dir.Stat()
438 err(e)
439
440 dirnames, e := dir.Readdirnames(-1)
441 err(e)
442 for _, a := range dirnames {
443 a = c.Path + "/" + a
444 if c.Lifetime != 0 {
445 now := time.Now().UnixMilli()
446
447 f, _ := os.Stat(a)
448 stat := f.Sys().(*syscall.Stat_t)
449 time := time.Unix(stat.Ctim.Unix()).UnixMilli()
450
451 if time+c.Lifetime <= now {
452 os.RemoveAll(a)
453 }
454 }
455 if c.MaxSize != 0 && stat.Size() > c.MaxSize {
456 os.RemoveAll(a)
457 }
458 }
459
460 dir.Close()
461 time.Sleep(time.Second)
462 }
463}
app/wraper.go +58 −33
@@ -15,13 +15,27 @@ import (
15var wr = io.WriteString 15var wr = io.WriteString
16 16
17type skunkyart struct { 17type skunkyart struct {
18 Writer http.ResponseWriter 18 Writer http.ResponseWriter
19 Args url.Values 19 Args url.Values
20 Type rune 20 BasePath string
21 Query string 21 Type rune
22 Page int 22 Query, QueryRaw string
23 atom bool 23 Page int
24 Templates struct { 24 Atom bool
25 Templates struct {
26 About struct {
27 Proxy bool
28 Nsfw bool
29 }
30
31 SomeList string
32 Deviation struct {
33 Post devianter.Post
34 StringTime string
35 Tags string
36 Comments string
37 }
38
25 GroupUser struct { 39 GroupUser struct {
26 GR devianter.GRuser 40 GR devianter.GRuser
27 Admins string 41 Admins string
@@ -51,6 +65,19 @@ type skunkyart struct {
51 } 65 }
52} 66}
53 67
68// var Templates struct {
69// Index string
70// About string
71//
72// GRuser string
73// Deviation string
74// List string
75// Search string
76// }
77
78// //go:embed ../html/*
79// var Templates embed.FS
80
54func (s skunkyart) GRUser() { 81func (s skunkyart) GRUser() {
55 if len(s.Query) < 1 { 82 if len(s.Query) < 1 {
56 s.ReturnHTTPError(400) 83 s.ReturnHTTPError(400)
@@ -65,7 +92,7 @@ func (s skunkyart) GRUser() {
65 switch s.Type { 92 switch s.Type {
66 case 'a': 93 case 'a':
67 g := group.GR 94 g := group.GR
68 s.atom = false 95 s.Atom = false
69 for _, x := range g.Gruser.Page.Modules { 96 for _, x := range g.Gruser.Page.Modules {
70 switch x.Name { 97 switch x.Name {
71 case "about", "group_about": 98 case "about", "group_about":
@@ -110,17 +137,15 @@ func (s skunkyart) GRUser() {
110 137
111 case "cover_deviation": 138 case "cover_deviation":
112 group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation 139 group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation
113 group.About.BG = devianter.UrlFromMedia(group.About.BGMeta.Media) 140 group.About.BGMeta.Url = s.ConvertDeviantArtUrlToSkunkyArt(group.About.BGMeta.Url)
141 group.About.BG = s.ParseMedia(group.About.BGMeta.Media)
114 case "group_admins": 142 case "group_admins":
115 var htm strings.Builder 143 var htm strings.Builder
116 for _, z := range x.ModuleData.GroupAdmins.Results { 144 for _, z := range x.ModuleData.GroupAdmins.Results {
117 htm.WriteString(`<p><img src="`) 145 htm.WriteString(`<p><img src="`)
118 htm.WriteString("/media/") 146 htm.WriteString(UrlBuilder("media", "emojitar", z.User.Username, "?type=a"))
119 htm.WriteString(z.User.Username)
120 htm.WriteString("?type=a")
121 htm.WriteString(`"><a href="`) 147 htm.WriteString(`"><a href="`)
122 htm.WriteString("/group_user?type=about&q=") 148 htm.WriteString(UrlBuilder("group_user", "?type=about&q=", z.User.Username))
123 htm.WriteString(z.User.Username)
124 htm.WriteString(`">`) 149 htm.WriteString(`">`)
125 htm.WriteString(z.User.Username) 150 htm.WriteString(z.User.Username)
126 htm.WriteString(`</a></p>`) 151 htm.WriteString(`</a></p>`)
@@ -175,24 +200,18 @@ func (s skunkyart) GRUser() {
175 s.ReturnHTTPError(400) 200 s.ReturnHTTPError(400)
176 } 201 }
177 202
178 if !s.atom { 203 if !s.Atom {
179 s.ExecuteTemplate("html/gruser.htm", &s) 204 s.ExecuteTemplate("html/gruser.htm", &s)
180 } 205 }
181} 206}
182 207
183// посты 208// посты
184func (s skunkyart) Deviation(author, postname string) { 209func (s skunkyart) Deviation(author, postname string) {
185 // поиск ID 210 id_search := regexp.MustCompile("[0-9]+").FindAllString(postname, -1)
186 re := regexp.MustCompile("[0-9]+").FindAllString(postname, -1) 211 if len(id_search) >= 1 {
187 if len(re) >= 1 { 212 post := &s.Templates.Deviation
188 var post struct {
189 Post devianter.Post
190 StringTime string
191 Tags string
192 Comments string
193 }
194 213
195 id := re[len(re)-1] 214 id := id_search[len(id_search)-1]
196 post.Post = devianter.DeviationFunc(id, author) 215 post.Post = devianter.DeviationFunc(id, author)
197 216
198 post.Post.Description = ParseDescription(post.Post.Deviation.TextContent) 217 post.Post.Description = ParseDescription(post.Post.Deviation.TextContent)
@@ -202,9 +221,9 @@ func (s skunkyart) Deviation(author, postname string) {
202 // хештэги 221 // хештэги
203 for _, x := range post.Post.Deviation.Extended.Tags { 222 for _, x := range post.Post.Deviation.Extended.Tags {
204 var tag strings.Builder 223 var tag strings.Builder
205 tag.WriteString(` <a href="/search?q=`) 224 tag.WriteString(` <a href="`)
206 tag.WriteString(x.Name) 225 tag.WriteString(UrlBuilder("search", "?q=", x.Name, "&type=tag"))
207 tag.WriteString(`&type=tag">#`) 226 tag.WriteString(`">#`)
208 tag.WriteString(x.Name) 227 tag.WriteString(x.Name)
209 tag.WriteString("</a>") 228 tag.WriteString("</a>")
210 229
@@ -213,7 +232,7 @@ func (s skunkyart) Deviation(author, postname string) {
213 232
214 post.Comments = s.ParseComments(devianter.CommentsFunc(id, post.Post.Comments.Cursor, s.Page, 1)) 233 post.Comments = s.ParseComments(devianter.CommentsFunc(id, post.Post.Comments.Cursor, s.Page, 1))
215 234
216 s.ExecuteTemplate("html/deviantion.htm", &post) 235 s.ExecuteTemplate("html/deviantion.htm", &s)
217 } else { 236 } else {
218 s.ReturnHTTPError(400) 237 s.ReturnHTTPError(400)
219 } 238 }
@@ -221,17 +240,17 @@ func (s skunkyart) Deviation(author, postname string) {
221 240
222func (s skunkyart) DD() { 241func (s skunkyart) DD() {
223 dd := devianter.DailyDeviationsFunc(s.Page) 242 dd := devianter.DailyDeviationsFunc(s.Page)
224 ddparsed := s.DeviationList(dd.Deviations, dlist{ 243 s.Templates.SomeList = s.DeviationList(dd.Deviations, dlist{
225 Pages: 0, 244 Pages: 0,
226 More: dd.HasMore, 245 More: dd.HasMore,
227 }) 246 })
228 if !s.atom { 247 if !s.Atom {
229 s.ExecuteTemplate("html/list.htm", &ddparsed) 248 s.ExecuteTemplate("html/list.htm", &s)
230 } 249 }
231} 250}
232 251
233func (s skunkyart) Search() { 252func (s skunkyart) Search() {
234 s.atom = false 253 s.Atom = false
235 var e error 254 var e error
236 ss := &s.Templates.Search 255 ss := &s.Templates.Search
237 switch s.Type { 256 switch s.Type {
@@ -263,3 +282,9 @@ func (s skunkyart) Emojitar(name string) {
263 s.ReturnHTTPError(400) 282 s.ReturnHTTPError(400)
264 } 283 }
265} 284}
285
286func (s skunkyart) About() {
287 s.Templates.About.Nsfw = CFG.Nsfw
288 s.Templates.About.Proxy = CFG.Proxy
289 s.ExecuteTemplate("html/about.htm", &s)
290}
atom.xml deleted −43
@@ -1,43 +0,0 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<feed xmlns:yt="http://www.youtube.com/xml/schemas/2015" xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US">
3 <link rel="self" href="https://inv.clovius.club/feed/channel/UCuxpxCCevIlF-k-K5YU8XPA"/>
4 <id>yt:channel:UCuxpxCCevIlF-k-K5YU8XPA</id>
5 <yt:channelId>UCuxpxCCevIlF-k-K5YU8XPA</yt:channelId>
6 <icon>https://yt3.googleusercontent.com/ytc/AIdro_n7bKr9iusclFjvYw3U8UR74iTCoMcdGTKI3h55SOG8mSsY=s900-c-k-c0x00ffffff-no-rj</icon>
7 <title>Test</title>
8 <link rel="alternate" href="https://skunky.ebloid.ru"/>
9 <author>
10 <name>lost+skunk</name>
11 <uri>https://skunky.ebloid.ru</uri>
12 </author>
13 <image>
14 <url>https://ebloid.ru/_matrix/media/v3/download/ebloid.ru/enfAtfdjzCQMRNBgLXmiURfJ?allow_redirect=true</url>
15 <title>lost+skunk</title>
16 <link rel="self" href="https://skunky.ebloid.ru"/>
17 </image>
18 <entry>
19 <title>Test</title>
20 <link rel="alternate" href="https://2ip.ua"/>
21 <author>
22 <name>lost+skunk</name>
23 <uri>https://skunky.ebloid.ru</uri>
24 </author>
25 <content type="xhtml">
26 <div xmlns="http://www.w3.org/1999/xhtml">
27 <a href="https://2ip.ua">
28 <img src="https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/526e735c-e3da-4542-a339-72ccdf827a00/dh3mfuq-5bd6472c-68f4-428d-bb90-8d461fc21410.jpg/v1/fill/w_1920,h_1080/image.gif?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNhNWYwZDQxNWVhMGQyNmUwIiwiaXNzIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCIsIm9iaiI6W1t7ImhlaWdodCI6Ijw9MTA4MCIsInBhdGgiOiJcL2ZcLzUyNmU3MzVjLWUzZGEtNDU0Mi1hMzM5LTcyY2NkZjgyN2EwMFwvZGgzbWZ1cS01YmQ2NDcyYy02OGY0LTQyOGQtYmI5MC04ZDQ2MWZjMjE0MTAuanBnIiwid2lkdGgiOiI8PTE5MjAifV1dLCJhdWQiOlsidXJuOnNlcnZpY2U6aW1hZ2Uub3BlcmF0aW9ucyJdfQ.S_1DGdd29F5l6Y46Q_bumtGlEEUf_WawZsmF8kBygBs"/>
29 </a>
30 <p>test</p>
31 </div>
32 </content>
33 <published>2024-07-04T00:37:55+00:00</published>
34 <media:group>
35 <media:title>GM Just Announced Their Shutting Down Production in America and Firing Their Workers</media:title>
36 <media:thumbnail url="https://ebloid.ru/_matrix/media/v3/download/ebloid.ru/enfAtfdjzCQMRNBgLXmiURfJ?allow_redirect=true" width="320" height="180"/>
37 <media:description>test</media:description>
38 </media:group>
39 <media:community>
40 <media:statistics views="91247"/>
41 </media:community>
42 </entry>
43</feed>
config.example.json +4 −3
@@ -1,12 +1,13 @@
1{ 1{
2 "listen": "0.0.0.0:3003", 2 "listen": "0.0.0.0:3003",
3 "base_uri": null, 3 "base-path": "/skunky/",
4 "cache": { 4 "cache": {
5 "enabled": true, 5 "enabled": true,
6 "path": "cache", 6 "path": "cache",
7 "lifetime": null, 7 "lifetime": null,
8 "max_size": 10000 8 "max-size": 100000
9 }, 9 },
10 "proxy": false, 10 "proxy": true,
11 "wixmp-proxy": "http://127.0.0.1:8080",
11 "nsfw": false 12 "nsfw": false
12}\ No newline at end of file 13}\ No newline at end of file
css/skunky.css +4 −1
@@ -28,9 +28,12 @@ form input, button, select {
28 border: 0px; 28 border: 0px;
29 border-radius: 1px; 29 border-radius: 1px;
30} 30}
31.nsfw, .true { 31.nsfw, .about-false {
32 color: red; 32 color: red;
33} 33}
34.about-true {
35 color: green;
36}
34.author { 37.author {
35 color: seagreen; 38 color: seagreen;
36} 39}
html/about.htm +9 −3
@@ -2,12 +2,12 @@
2<html> 2<html>
3 <head> 3 <head>
4 <title>SkunkyArt</title> 4 <title>SkunkyArt</title>
5 <link rel="stylesheet" href="gui/css/skunky.css"> 5 <link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
6 </head> 6 </head>
7 <main> 7 <main>
8 <header> 8 <header>
9 <h1><a href="/">HOME</a> | <a href="/dd">DD</a></h1> 9 <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a></h1>
10 <form method="get" action="/search"> 10 <form method="get" action="{{.BasePath}}search">
11 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false"> 11 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
12 <select name="type"> 12 <select name="type">
13 <option value="all">All</option> 13 <option value="all">All</option>
@@ -19,5 +19,11 @@
19 <p> 19 <p>
20 SkunkyArt is an alternative frontend for deviantart.com, written in Go. 20 SkunkyArt is an alternative frontend for deviantart.com, written in Go.
21 </p> 21 </p>
22 <h2>Instance settings:</h2>
23 <ul>
24 <li><b>NSFW</b>: <span class="about-{{.Templates.About.Nsfw}}">{{if .Templates.About.Nsfw}}YES{{else}}NO{{end}}</span></li>
25 <li><b>Proxyfing</b>: <span class="about-{{.Templates.About.Proxy}}">{{if .Templates.About.Proxy}}YES{{else}}NO{{end}}</span></li>
26 </ul>
27 <p>Copyright <a href="https://go.kde.org/matrix/#/@softpigeones:ebloid.ru" target="_blank">lost+skunk</a>, X11. <a href="https://git.macaw.me/skunky/skunkyart/src/tag/v1.3" target="_blank">SkunkyArt v1.3</a></p>
22 </main> 28 </main>
23</html>\ No newline at end of file 29</html>\ No newline at end of file
html/deviantion.htm +19 −19
@@ -1,13 +1,13 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html> 2<html>
3 <head> 3 <head>
4 <title>SkunkyArt | {{.Post.Deviation.Author.Username}} - {{.Post.Deviation.Title}}</title> 4 <title>SkunkyArt | {{.Templates.Deviation.Post.Deviation.Author.Username}} - {{.Templates.Deviation.Post.Deviation.Title}}</title>
5 <link rel="stylesheet" href="/gui/css/skunky.css"> 5 <link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
6 </head> 6 </head>
7 <main> 7 <main>
8 <header> 8 <header>
9 <h1><a href="/">HOME</a> | <a href="/dd">DD</a></h1> 9 <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a></h1>
10 <form method="get" action="/search"> 10 <form method="get" action="{{.BasePath}}search">
11 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false"> 11 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
12 <select name="type"> 12 <select name="type">
13 <option value="all">All</option> 13 <option value="all">All</option>
@@ -17,29 +17,29 @@
17 </form> 17 </form>
18 </header> 18 </header>
19 <figure> 19 <figure>
20 <img src="/media/{{.Post.Deviation.Author.Username}}?type=a" width="30px"> 20 <img src="{{.BasePath}}media/emojitar/{{.Templates.Deviation.Post.Deviation.Author.Username}}?type=a" width="30px">
21 <span><strong><a href="/group_user?type=about&q={{.Post.Deviation.Author.Username}}">{{.Post.Deviation.Author.Username}}</a></strong> — {{if (.Post.Deviation.DD)}} 21 <span><strong><a href="{{.BasePath}}group_user?type=about&q={{.Templates.Deviation.Post.Deviation.Author.Username}}">{{.Templates.Deviation.Post.Deviation.Author.Username}}</a></strong> — {{if (.Templates.Deviation.Post.Deviation.DD)}}
22 <span class="dd" title="Daily Deviation!"><b>{{.Post.Deviation.Title}}</b></span> 22 <span class="dd" title="Daily Deviation!"><b>{{.Templates.Deviation.Post.Deviation.Title}}</b></span>
23 {{else}}{{.Post.Deviation.Title}}{{end}} 23 {{else}}{{.Templates.Deviation.Post.Deviation.Title}}{{end}}
24 {{if (ne .Post.Deviation.License "none")}}<mark title="License">{{.Post.Deviation.License}}</mark>{{end}} {{if (.Post.Deviation.AI)}}[🤖]{{end}} 24 {{if (ne .Templates.Deviation.Post.Deviation.License "none")}}<mark title="License">{{.Templates.Deviation.Post.Deviation.License}}</mark>{{end}} {{if (.Templates.Deviation.Post.Deviation.AI)}}[🤖]{{end}}
25 {{if (.Post.Deviation.NSFW)}}[<span class="nsfw">NSFW</span>]{{end}} 25 {{if (.Templates.Deviation.Post.Deviation.NSFW)}}[<span class="nsfw">NSFW</span>]{{end}}
26 </span> 26 </span>
27 <br> 27 <br>
28 {{if (ne .Post.IMG "")}} 28 {{if (ne .Templates.Deviation.Post.IMG "")}}
29 <a href="{{.Post.IMG}}" title="open/download image"><img src="{{.Post.IMG}}" width="50%"></a> 29 <a href="{{.Templates.Deviation.Post.IMG}}" title="open/download image"><img src="{{.Templates.Deviation.Post.IMG}}" width="50%"></a>
30 <br> 30 <br>
31 {{end}} 31 {{end}}
32 {{if (ne .Tags "")}} 32 {{if (ne .Templates.Deviation.Tags "")}}
33 {{.Tags}}<br> 33 {{.Templates.Deviation.Tags}}<br>
34 {{end}} 34 {{end}}
35 <span>Published: <strong>{{.StringTime}}</strong>; Views: <strong>{{.Post.Deviation.Stats.Views}}</strong>; Favourites: <strong>{{.Post.Deviation.Stats.Favourites}}</strong>; Downloads: <strong>{{.Post.Deviation.Stats.Downloads}}</strong></span> 35 <span>Published: <strong>{{.Templates.Deviation.StringTime}}</strong>; Views: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Views}}</strong>; Favourites: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Favourites}}</strong>; Downloads: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Downloads}}</strong></span>
36 {{if (ne .Post.Description "")}} 36 {{if (ne .Templates.Deviation.Post.Description "")}}
37 <figcaption> 37 <figcaption>
38 {{.Post.Description}} 38 {{.Templates.Deviation.Post.Description}}
39 </figcaption> 39 </figcaption>
40 {{end}} 40 {{end}}
41 {{if (ne .Comments "")}} 41 {{if (ne .Templates.Deviation.Comments "")}}
42 {{.Comments}} 42 {{.Templates.Deviation.Comments}}
43 {{end}} 43 {{end}}
44 </figure> 44 </figure>
45 </main> 45 </main>
html/gruser.htm +4 −4
@@ -8,14 +8,14 @@
8 gallery of {{.Templates.GroupUser.GR.Owner.Username}} 8 gallery of {{.Templates.GroupUser.GR.Owner.Username}}
9 {{end}} 9 {{end}}
10 </title> 10 </title>
11 <link rel="stylesheet" href="/gui/css/skunky.css"> 11 <link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
12 </head> 12 </head>
13 <main> 13 <main>
14 <header> 14 <header>
15 <h1><a href="/">HOME</a> | <a href="/dd">DD</a> 15 <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a>
16 | <a href="?q={{.Templates.GroupUser.GR.Owner.Username}}&type={{if eq .Type 'a'}}gallery">Gallery{{else}}about">About{{end}}</a> 16 | <a href="?q={{.Templates.GroupUser.GR.Owner.Username}}&type={{if eq .Type 'a'}}gallery">Gallery{{else}}about">About{{end}}</a>
17 | <a href="?q={{.Templates.GroupUser.GR.Owner.Username}}&type=gallery&atom=true">RSS</a></h1> 17 | <a href="?q={{.Templates.GroupUser.GR.Owner.Username}}&type=gallery&atom=true">RSS</a></h1>
18 <form method="get" action="/search"> 18 <form method="get" action="{{.BasePath}}search">
19 <input type="gallery" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false"> 19 <input type="gallery" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
20 <input type="hidden" name="usr" value="{{.Templates.GroupUser.GR.Owner.Username}}"> 20 <input type="hidden" name="usr" value="{{.Templates.GroupUser.GR.Owner.Username}}">
21 <select name="type"> 21 <select name="type">
@@ -32,7 +32,7 @@
32 {{.Templates.GroupUser.About.BGMeta.Author.Username}} - {{end}}{{.Templates.GroupUser.About.BGMeta.Title}}" src="{{.Templates.GroupUser.About.BG}}"></a> 32 {{.Templates.GroupUser.About.BGMeta.Author.Username}} - {{end}}{{.Templates.GroupUser.About.BGMeta.Title}}" src="{{.Templates.GroupUser.About.BG}}"></a>
33 {{end}} 33 {{end}}
34 34
35 <img src="/media/{{.Templates.GroupUser.GR.Owner.Username}}?type=a" width="30px" height="30px"> <b>{{.Templates.GroupUser.GR.Owner.Username}}</b> 35 <img src="{{.BasePath}}media/emojitar/{{.Templates.GroupUser.GR.Owner.Username}}?type=a" width="30px" height="30px"> <b>{{.Templates.GroupUser.GR.Owner.Username}}</b>
36 {{if (eq .Templates.GroupUser.About.A.Gender "male")}} 36 {{if (eq .Templates.GroupUser.About.A.Gender "male")}}
37 ♂️ 37 ♂️
38 {{end}} 38 {{end}}
html/index.htm +3 −3
@@ -2,11 +2,11 @@
2<html> 2<html>
3 <head> 3 <head>
4 <title>SkunkyArt</title> 4 <title>SkunkyArt</title>
5 <link rel="stylesheet" href="gui/css/skunky.css" 5 <link rel="stylesheet" href="{{.}}gui/css/skunky.css"/>
6 </head> 6 </head>
7 <main> 7 <main>
8 <center> 8 <center>
9 <form method="get" action="search"> 9 <form method="get" action="{{.}}search">
10 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false"> 10 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
11 <select name="type"> 11 <select name="type">
12 <option value="all">All</option> 12 <option value="all">All</option>
@@ -14,7 +14,7 @@
14 </select> 14 </select>
15 <button type="submit">Search!</button> 15 <button type="submit">Search!</button>
16 </form> 16 </form>
17 <h1><a href="dd">Daily Deviations</a> | <a href="about">About</a> | <a href="https://git.macaw.me/skunky/SkunkyArt" target="_blank">Source Code</a></h1> 17 <h1><a href="{{.}}dd">Daily Deviations</a> | <a href="{{.}}about">About</a> | <a href="https://git.macaw.me/skunky/SkunkyArt" target="_blank">Source Code</a></h1>
18 </center> 18 </center>
19 </main> 19 </main>
20</html>\ No newline at end of file 20</html>\ No newline at end of file
html/list.htm +4 −4
@@ -2,12 +2,12 @@
2<html> 2<html>
3 <head> 3 <head>
4 <title>SkunkyArt</title> 4 <title>SkunkyArt</title>
5 <link rel="stylesheet" href="gui/css/skunky.css"> 5 <link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
6 </head> 6 </head>
7 <main> 7 <main>
8 <header> 8 <header>
9 <h1><a href="/">HOME</a> | <a href="/dd">DD</a> | <a href="?atom=true">RSS</a></h1> 9 <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a> | <a href="?atom=true">RSS</a></h1>
10 <form method="get" action="/search"> 10 <form method="get" action="{{.BasePath}}search">
11 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false"> 11 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
12 <select name="type"> 12 <select name="type">
13 <option value="all">All</option> 13 <option value="all">All</option>
@@ -16,6 +16,6 @@
16 <button type="submit">Search!</button> 16 <button type="submit">Search!</button>
17 </form> 17 </form>
18 </header> 18 </header>
19 {{.}} 19 {{.Templates.SomeList}}
20 </main> 20 </main>
21</html>\ No newline at end of file 21</html>\ No newline at end of file
html/search.htm +3 −3
@@ -1,12 +1,12 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html> 2<html>
3 <head> 3 <head>
4 <title>SkunkyArt | Search "{{.Query}}"</title> 4 <title>SkunkyArt | Search "{{.QueryRaw}}"</title>
5 <link rel="stylesheet" href="/gui/css/skunky.css"> 5 <link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
6 </head> 6 </head>
7 <main> 7 <main>
8 <header> 8 <header>
9 <h1><a href="/">HOME</a> | <a href="/dd">DD</a></h1> 9 <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a></h1>
10 <form method="get" action="search"> 10 <form method="get" action="search">
11 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false"> 11 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
12 <select name="type"> 12 <select name="type">
main.go +1
@@ -7,6 +7,7 @@ import (
7) 7)
8 8
9func main() { 9func main() {
10 app.ExecuteConfig()
10 err := devianter.UpdateCSRF() 11 err := devianter.UpdateCSRF()
11 if err != nil { 12 if err != nil {
12 println(err.Error()) 13 println(err.Error())