diff --git a/artworks/artstation/artstation.go b/artworks/artstation/artstation.go index fd4de33..33d816e 100644 --- a/artworks/artstation/artstation.go +++ b/artworks/artstation/artstation.go @@ -3,7 +3,6 @@ package artstation import ( "encoding/json" "fmt" - "github.com/VTGare/boe-tea-go/artworks/embed" "net/http" "regexp" "strconv" @@ -41,7 +40,6 @@ type ArtstationResponse struct { HideAsAdult bool `json:"hide_as_adult,omitempty"` VisibleOnArtstation bool `json:"visible_on_artstation,omitempty"` - artworkID string AIGenerated bool CreatedAt time.Time `json:"created_at,omitempty"` } @@ -73,32 +71,40 @@ type Category struct { } func New() artworks.Provider { + r := regexp.MustCompile(`(?i)https:\/\/(?:www\.)?artstation\.com\/artwork\/([\w\-]+)`) + return &Artstation{ - regex: regexp.MustCompile(`(?i)https://(?:www\.)?artstation\.com/artwork/([\w\-]+)`), + regex: r, } } func (as *Artstation) Find(id string) (artworks.Artwork, error) { - return artworks.NewError(as, func() (artworks.Artwork, error) { - reqURL := fmt.Sprintf("https://www.artstation.com/projects/%v.json", id) - resp, err := http.Get(reqURL) - if err != nil { - return nil, err - } + artwork, err := as._find(id) + if err != nil { + return nil, artworks.NewError(as, err) + } - defer resp.Body.Close() + return artwork, nil +} - res := &ArtstationResponse{} - err = json.NewDecoder(resp.Body).Decode(res) - if err != nil { - return nil, err - } +func (as *Artstation) _find(id string) (artworks.Artwork, error) { + reqURL := fmt.Sprintf("https://www.artstation.com/projects/%v.json", id) + resp, err := http.Get(reqURL) + if err != nil { + return nil, err + } + + defer resp.Body.Close() - res.artworkID = id - res.AIGenerated = artworks.IsAIGenerated(res.Tags...) + res := &ArtstationResponse{} + err = json.NewDecoder(resp.Body).Decode(res) + if err != nil { + return nil, err + } - return res, nil - }) + res.AIGenerated = artworks.IsAIGenerated(res.Tags...) + + return res, nil } func (as *Artstation) Match(url string) (string, bool) { @@ -129,8 +135,13 @@ func (artwork *ArtstationResponse) StoreArtwork() *store.Artwork { } func (artwork *ArtstationResponse) MessageSends(footer string, tagsEnabled bool) ([]*discordgo.MessageSend, error) { - if len(artwork.Assets) == 0 { - eb := embeds.NewBuilder() + var ( + length = len(artwork.Assets) + pages = make([]*discordgo.MessageSend, 0, length) + eb = embeds.NewBuilder() + ) + + if length == 0 { eb.Title("❎ An error has occured.") eb.Description("Artwork has been deleted or the ID does not exist.") eb.Footer(footer, "") @@ -140,32 +151,51 @@ func (artwork *ArtstationResponse) MessageSends(footer string, tagsEnabled bool) }, nil } - eb := &embed.Embed{ - Title: artwork.Title, - Username: artwork.User.Name, - FieldName1: "Likes", - FieldValue1: strconv.Itoa(artwork.LikesCount), - FieldName2: "Views", - FieldValue2: []string{strconv.Itoa(artwork.ViewsCount)}, - URL: artwork.Permalink, - Timestamp: artwork.CreatedAt, - Footer: footer, - AIGenerated: artwork.AIGenerated, + if length > 1 { + eb.Title(fmt.Sprintf("%v by %v | Page %v / %v", artwork.Title, artwork.User.Name, 1, length)) + } else { + eb.Title(fmt.Sprintf("%v by %v", artwork.Title, artwork.User.Name)) } if tagsEnabled { - eb.Tags = bluemonday.StrictPolicy().Sanitize(artwork.Description) + desc := bluemonday.StrictPolicy().Sanitize(artwork.Description) + eb.Description(artworks.EscapeMarkdown(desc)) } - for _, image := range artwork.Assets { - eb.Images = append(eb.Images, image.ImageURL) + eb.URL(artwork.URL()). + AddField("Likes", strconv.Itoa(artwork.LikesCount), true). + AddField("Views", strconv.Itoa(artwork.ViewsCount), true). + Timestamp(artwork.CreatedAt) + + if footer != "" { + eb.Footer(footer, "") } - return eb.ToEmbed(), nil -} + if artwork.AIGenerated { + eb.AddField("⚠️ Disclaimer", "This artwork is AI-generated.") + } + + eb.Image(artwork.Assets[0].ImageURL) + pages = append(pages, &discordgo.MessageSend{Embeds: []*discordgo.MessageEmbed{eb.Finalize()}}) + if length > 1 { + for ind, image := range artwork.Assets[1:] { + eb := embeds.NewBuilder() + + eb.Title(fmt.Sprintf("%v by %v | Page %v / %v", artwork.Title, artwork.User.Name, ind+2, length)). + Image(image.ImageURL). + URL(artwork.URL()). + Timestamp(artwork.CreatedAt) + + if footer != "" { + eb.Footer(footer, "") + } + + eb.AddField("Likes", strconv.Itoa(artwork.LikesCount), true) + pages = append(pages, &discordgo.MessageSend{Embeds: []*discordgo.MessageEmbed{eb.Finalize()}}) + } + } -func (artwork *ArtstationResponse) ArtworkID() string { - return artwork.artworkID + return pages, nil } func (artwork *ArtstationResponse) URL() string { diff --git a/artworks/artstation/artstation_suite_test.go b/artworks/artstation/artstation_suite_test.go index 62006cb..7e3d46c 100644 --- a/artworks/artstation/artstation_suite_test.go +++ b/artworks/artstation/artstation_suite_test.go @@ -16,9 +16,9 @@ func TestArtstation(t *testing.T) { var _ = DescribeTable( "Match Artstation URL", func(url string, expectedID string, expectedResult bool) { - provider := artstation.New() + as := artstation.New() - id, ok := provider.Match(url) + id, ok := as.Match(url) Expect(id).To(BeEquivalentTo(expectedID)) Expect(ok).To(BeEquivalentTo(expectedResult)) }, diff --git a/artworks/artworks.go b/artworks/artworks.go index 1d5a47b..c5273af 100644 --- a/artworks/artworks.go +++ b/artworks/artworks.go @@ -1,9 +1,13 @@ package artworks import ( + "errors" + "fmt" + "regexp" + "strings" + "github.com/VTGare/boe-tea-go/store" "github.com/bwmarrin/discordgo" - "strings" ) type Provider interface { @@ -15,12 +19,49 @@ type Provider interface { type Artwork interface { StoreArtwork() *store.Artwork MessageSends(footer string, tags bool) ([]*discordgo.MessageSend, error) - ArtworkID() string URL() string Len() int } -func IsAIGenerated(content ...string) bool { +type Error struct { + provider string + cause error +} + +func (e *Error) Error() string { + return fmt.Sprintf("provider %v returned an error: %v", e.provider, e.cause.Error()) +} + +func (e *Error) Unwrap() error { + return e.cause +} + +func NewError(p Provider, err error) error { + return &Error{ + provider: fmt.Sprintf("%T", p), + cause: err, + } +} + +// Common errors +var ( + ErrArtworkNotFound = errors.New("artwork not found") + ErrRateLimited = errors.New("provider rate limited") +) + +func EscapeMarkdown(content string) string { + contents := strings.Split(content, "\n") + regex := regexp.MustCompile("^#{1,3}") + + for i, line := range contents { + if regex.MatchString(line) { + contents[i] = "\\" + line + } + } + return strings.Join(contents, "\n") +} + +func IsAIGenerated(contents ...string) bool { aiTags := []string{ "aiart", "aigenerated", @@ -32,7 +73,7 @@ func IsAIGenerated(content ...string) bool { "stablediffusion", } - for _, tag := range content { + for _, tag := range contents { for _, test := range aiTags { if strings.EqualFold(tag, test) { return true diff --git a/artworks/deviant/deviant.go b/artworks/deviant/deviant.go index 1923202..87f027d 100644 --- a/artworks/deviant/deviant.go +++ b/artworks/deviant/deviant.go @@ -2,6 +2,7 @@ package deviant import ( "encoding/json" + "fmt" "net/http" "net/url" "regexp" @@ -10,10 +11,10 @@ import ( "time" "github.com/VTGare/boe-tea-go/artworks" - "github.com/VTGare/boe-tea-go/artworks/embed" "github.com/VTGare/boe-tea-go/internal/arrays" "github.com/VTGare/boe-tea-go/messages" "github.com/VTGare/boe-tea-go/store" + "github.com/VTGare/embeds" "github.com/bwmarrin/discordgo" ) @@ -32,7 +33,6 @@ type Artwork struct { Comments int AIGenerated bool CreatedAt time.Time - artworkID string url string } @@ -65,46 +65,52 @@ type deviantEmbed struct { func New() artworks.Provider { return &DeviantArt{ - regex: regexp.MustCompile(`(?i)https://(?:www\.)?deviantart\.com/\w+/art/([\w\-]+)`), + regex: regexp.MustCompile(`(?i)https:\/\/(?:www\.)?deviantart\.com\/[\w]+\/art\/([\w\-]+)`), } } func (d *DeviantArt) Find(id string) (artworks.Artwork, error) { - return artworks.NewError(d, func() (artworks.Artwork, error) { - reqURL := "https://backend.deviantart.com/oembed?url=" + url.QueryEscape("deviantart.com/art/"+id) - resp, err := http.Get(reqURL) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - var res deviantEmbed - err = json.NewDecoder(resp.Body).Decode(&res) - if err != nil { - return nil, err - } - - artwork := &Artwork{ - Title: res.Title, - Author: &Author{ - Name: res.AuthorName, - URL: res.AuthorURL, - }, - ImageURL: res.URL, - ThumbnailURL: res.ThumbnailURL, - Tags: strings.Split(res.Tags, ", "), - Views: res.Community.Statistics.Attributes.Views, - Favorites: res.Community.Statistics.Attributes.Favorites, - Comments: res.Community.Statistics.Attributes.Comments, - CreatedAt: res.Pubdate, - artworkID: id, - url: res.AuthorURL + "/art/" + id, - } - - artwork.AIGenerated = artworks.IsAIGenerated(artwork.Tags...) - - return artwork, nil - }) + artwork, err := d._find(id) + if err != nil { + return nil, artworks.NewError(d, err) + } + + return artwork, nil +} + +func (d *DeviantArt) _find(id string) (artworks.Artwork, error) { + reqURL := "https://backend.deviantart.com/oembed?url=" + url.QueryEscape("deviantart.com/art/"+id) + resp, err := http.Get(reqURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var res deviantEmbed + err = json.NewDecoder(resp.Body).Decode(&res) + if err != nil { + return nil, err + } + + artwork := &Artwork{ + Title: res.Title, + Author: &Author{ + Name: res.AuthorName, + URL: res.AuthorURL, + }, + ImageURL: res.URL, + ThumbnailURL: res.ThumbnailURL, + Tags: strings.Split(res.Tags, ", "), + Views: res.Community.Statistics.Attributes.Views, + Favorites: res.Community.Statistics.Attributes.Favorites, + Comments: res.Community.Statistics.Attributes.Comments, + CreatedAt: res.Pubdate, + url: res.AuthorURL + "/art/" + id, + } + + artwork.AIGenerated = artworks.IsAIGenerated(artwork.Tags...) + + return artwork, nil } func (d *DeviantArt) Match(s string) (string, bool) { @@ -121,28 +127,36 @@ func (d *DeviantArt) Enabled(g *store.Guild) bool { } func (a *Artwork) MessageSends(footer string, tagsEnabled bool) ([]*discordgo.MessageSend, error) { - eb := &embed.Embed{ - Title: a.Title, - Username: a.Author.Name, - FieldName1: "Views", - FieldValue1: strconv.Itoa(a.Views), - FieldName2: "Favorites", - FieldValue2: []string{strconv.Itoa(a.Favorites)}, - Images: []string{a.ImageURL}, - URL: a.url, - Timestamp: a.CreatedAt, - Footer: footer, - AIGenerated: a.AIGenerated, - } + eb := embeds.NewBuilder() + + eb.Title(fmt.Sprintf("%v by %v", a.Title, a.Author.Name)). + Image(a.ImageURL). + URL(a.url). + Timestamp(a.CreatedAt). + AddField("Views", strconv.Itoa(a.Views), true). + AddField("Favorites", strconv.Itoa(a.Favorites), true) if tagsEnabled && len(a.Tags) > 0 { tags := arrays.Map(a.Tags, func(s string) string { - return messages.NamedLink(s, "https://www.deviantart.com/tag/"+s) + return messages.NamedLink( + s, "https://www.deviantart.com/tag/"+s, + ) }) - eb.Tags = strings.Join(tags, " • ") + + eb.Description("**Tags:**\n" + strings.Join(tags, " • ")) + } + + if footer != "" { + eb.Footer(footer, "") + } + + if a.AIGenerated { + eb.AddField("⚠️ Disclaimer", "This artwork is AI-generated.") } - return eb.ToEmbed(), nil + return []*discordgo.MessageSend{ + {Embeds: []*discordgo.MessageEmbed{eb.Finalize()}}, + }, nil } func (a *Artwork) StoreArtwork() *store.Artwork { @@ -154,10 +168,6 @@ func (a *Artwork) StoreArtwork() *store.Artwork { } } -func (a *Artwork) ArtworkID() string { - return a.artworkID -} - func (a *Artwork) URL() string { return a.url } diff --git a/artworks/embed/embed.go b/artworks/embed/embed.go deleted file mode 100644 index e3cb8f5..0000000 --- a/artworks/embed/embed.go +++ /dev/null @@ -1,93 +0,0 @@ -package embed - -import ( - "fmt" - "strings" - "time" - - "github.com/VTGare/boe-tea-go/internal/dgoutils" - "github.com/VTGare/embeds" - "github.com/bwmarrin/discordgo" -) - -type Embed struct { - Title string - Username string - Description string - Tags string - FieldName1 string - FieldValue1 string - FieldName2 string - FieldValue2 []string - Images []string - Files []*discordgo.File - URL string - Timestamp time.Time - Footer string - AIGenerated bool -} - -func (e *Embed) ToEmbed() []*discordgo.MessageSend { - var ( - length = dgoutils.Ternary(len(e.Files) > 0, 1, len(e.Images)) - pages = make([]*discordgo.MessageSend, 0, length) - ) - - for i := 0; i < length; i++ { - eb := embeds.NewBuilder() - - eb.Title(EscapeMarkdown( - dgoutils.Ternary(length == 1, - fmt.Sprintf("%v by %v", e.Title, e.Username), - fmt.Sprintf("%v by %v | Page %v / %v", e.Title, e.Username, i+1, length), - ), - )) - - if len(e.Images) != 0 { - eb.Image(e.Images[i]) - } - - eb.URL(e.URL) - eb.Timestamp(e.Timestamp) - - if i == 0 { - if e.Description != "" { - eb.Description(EscapeMarkdown(e.Description)) - } - - if e.Tags != "" { - eb.AddField("Tags", e.Tags) - } - } - - if i < len(e.FieldValue2) { - eb.AddField(e.FieldName1, e.FieldValue1, true) - eb.AddField(e.FieldName2, e.FieldValue2[i], true) - } - - if i == 0 && e.AIGenerated { - eb.AddField("⚠️ Disclaimer", "This artwork is AI-generated.") - } - - if e.Footer != "" { - eb.Footer(e.Footer, "") - } - - pages = append(pages, &discordgo.MessageSend{ - Embeds: []*discordgo.MessageEmbed{eb.Finalize()}, - Files: e.Files, - }) - } - - return pages -} - -func EscapeMarkdown(content string) string { - markdown := []string{"-", "_", "#", "*", "`", ">"} - - for _, m := range markdown { - content = strings.ReplaceAll(content, m, "\\"+m) - } - - return content -} diff --git a/artworks/error.go b/artworks/error.go deleted file mode 100644 index 0bd559d..0000000 --- a/artworks/error.go +++ /dev/null @@ -1,38 +0,0 @@ -package artworks - -import ( - "errors" - "fmt" -) - -// Common errors -var ( - ErrArtworkNotFound = errors.New("artwork not found") - ErrRateLimited = errors.New("provider rate limited") -) - -type Error struct { - provider string - cause error -} - -func (e *Error) Error() string { - return fmt.Sprintf("provider %v returned an error: %v", e.provider, e.cause.Error()) -} - -func (e *Error) Unwrap() error { - return e.cause -} - -func NewError(p Provider, find func() (Artwork, error)) (Artwork, error) { - artwork, err := find() - - if err != nil { - return nil, &Error{ - provider: fmt.Sprintf("%T", p), - cause: err, - } - } - - return artwork, nil -} diff --git a/artworks/pixiv/pixiv.go b/artworks/pixiv/pixiv.go index 44354c5..acfa4e3 100644 --- a/artworks/pixiv/pixiv.go +++ b/artworks/pixiv/pixiv.go @@ -8,19 +8,21 @@ import ( "time" "github.com/VTGare/boe-tea-go/artworks" - "github.com/VTGare/boe-tea-go/artworks/embed" "github.com/VTGare/boe-tea-go/internal/arrays" - "github.com/VTGare/boe-tea-go/internal/dgoutils" "github.com/VTGare/boe-tea-go/messages" "github.com/VTGare/boe-tea-go/store" + "github.com/VTGare/embeds" "github.com/bwmarrin/discordgo" "github.com/everpcpc/pixiv" ) +var regex = regexp.MustCompile( + `(?i)http(?:s)?:\/\/(?:www\.)?pixiv\.net\/(?:en\/)?(?:artworks\/|member_illust\.php\?)(?:mode=medium\&)?(?:illust_id=)?([0-9]+)`, +) + type Pixiv struct { app *pixiv.AppPixivAPI proxyHost string - regex *regexp.Regexp } type Artwork struct { @@ -45,15 +47,12 @@ type Image struct { Original string } -func LoadAuth(authToken, refreshToken string) error { +func New(proxyHost, authToken, refreshToken string) (artworks.Provider, error) { _, err := pixiv.LoadAuth(authToken, refreshToken, time.Now()) if err != nil { - return err + return nil, err } - return nil -} -func New(proxyHost string) artworks.Provider { if proxyHost == "" { proxyHost = "https://boetea.dev" } @@ -61,12 +60,11 @@ func New(proxyHost string) artworks.Provider { return &Pixiv{ app: pixiv.NewApp(), proxyHost: proxyHost, - regex: regexp.MustCompile(`(?i)https?://(?:www\.)?pixiv\.net/(?:en/)?(?:artworks/|member_illust\.php\?)(?:mode=medium&)?(?:illust_id=)?([0-9]+)`), - } + }, nil } func (p *Pixiv) Match(s string) (string, bool) { - res := p.regex.FindStringSubmatch(s) + res := regex.FindStringSubmatch(s) if res == nil { return "", false } @@ -75,83 +73,103 @@ func (p *Pixiv) Match(s string) (string, bool) { } func (p *Pixiv) Find(id string) (artworks.Artwork, error) { - return artworks.NewError(p, func() (artworks.Artwork, error) { - i, err := strconv.ParseUint(id, 10, 64) - if err != nil { - return nil, err - } + artwork, err := p._find(id) + if err != nil { + return nil, artworks.NewError(p, err) + } - illust, err := p.app.IllustDetail(i) - if err != nil { - return nil, err - } + return artwork, nil +} - if illust.ID == 0 { - return nil, artworks.ErrArtworkNotFound - } +func (p *Pixiv) _find(id string) (artworks.Artwork, error) { + i, err := strconv.ParseUint(id, 10, 64) + if err != nil { + return nil, err + } - tags := make([]string, 0) - nsfw := false - for _, tag := range illust.Tags { - if tag.Name == "R-18" { - nsfw = true - } + illust, err := p.app.IllustDetail(i) + if err != nil { + return nil, err + } - tags = dgoutils.Ternary(tag.TranslatedName != "", - append(tags, tag.TranslatedName), - append(tags, tag.Name), - ) - } + if illust.ID == 0 { + return nil, artworks.ErrArtworkNotFound + } - images := make([]*Image, 0, illust.PageCount) - if page := illust.MetaSinglePage; page != nil { - if page.OriginalImageURL != "" { - img := &Image{ - Original: page.OriginalImageURL, - Preview: illust.Images.Medium, - } + author := "" + if illust.User != nil { + author = illust.User.Name + } else { + author = "Unknown" + } - images = append(images, img) - } + tags := make([]string, 0) + nsfw := false + for _, tag := range illust.Tags { + if tag.Name == "R-18" { + nsfw = true } - for _, page := range illust.MetaPages { + if tag.TranslatedName != "" { + tags = append(tags, tag.TranslatedName) + } else { + tags = append(tags, tag.Name) + } + } + + images := make([]*Image, 0, illust.PageCount) + if page := illust.MetaSinglePage; page != nil { + if page.OriginalImageURL != "" { img := &Image{ - Original: page.Images.Original, - Preview: page.Images.Large, + Original: page.OriginalImageURL, + Preview: illust.Images.Medium, } images = append(images, img) } + } - errImages := []string{ - "limit_sanity_level_360.png", - "limit_unknown_360.png", + for _, page := range illust.MetaPages { + img := &Image{ + Original: page.Images.Original, + Preview: page.Images.Large, } - for _, img := range errImages { - if images[0].Original == fmt.Sprintf("https://s.pximg.net/common/images/%s", img) { - return nil, artworks.ErrRateLimited - } + images = append(images, img) + } + + artwork := &Artwork{ + ID: id, + url: "https://www.pixiv.net/en/artworks/" + id, + Title: illust.Title, + Author: author, + Tags: tags, + Images: images, + NSFW: nsfw, + Type: illust.Type, + Pages: illust.PageCount, + Likes: illust.TotalBookmarks, + CreatedAt: illust.CreateDate, + + proxy: p.proxyHost, + } + + errImages := []string{ + "limit_sanity_level_360.png", + "limit_unknown_360.png", + } + + for _, img := range errImages { + if artwork.Images[0].Original == fmt.Sprintf("https://s.pximg.net/common/images/%s", img) { + return nil, artworks.ErrRateLimited } + } - return &Artwork{ - ID: id, - url: "https://www.pixiv.net/en/artworks/" + id, - Title: illust.Title, - Author: dgoutils.Ternary(illust.User != nil, illust.User.Name, "Unknown"), - Tags: tags, - Images: images, - NSFW: nsfw, - Type: illust.Type, - Pages: illust.PageCount, - Likes: illust.TotalBookmarks, - CreatedAt: illust.CreateDate, - AIGenerated: illust.IllustAIType == pixiv.IllustAITypeAIGenerated, - - proxy: p.proxyHost, - }, nil - }) + if illust.IllustAIType == pixiv.IllustAITypeAIGenerated { + artwork.AIGenerated = true + } + + return artwork, nil } func (p *Pixiv) Enabled(g *store.Guild) bool { @@ -168,35 +186,61 @@ func (a *Artwork) StoreArtwork() *store.Artwork { } func (a *Artwork) MessageSends(footer string, tagsEnabled bool) ([]*discordgo.MessageSend, error) { - eb := &embed.Embed{ - Title: a.Title, - Username: a.Author, - FieldName1: "Likes", - FieldValue1: strconv.Itoa(a.Likes), - FieldName2: "Original quality", - URL: a.url, - Timestamp: a.CreatedAt, - Footer: footer, - AIGenerated: a.AIGenerated, + var ( + length = len(a.Images) + pages = make([]*discordgo.MessageSend, 0, length) + eb = embeds.NewBuilder() + ) + + if length > 1 { + eb.Title(fmt.Sprintf("%v by %v | Page %v / %v", a.Title, a.Author, 1, length)) + } else { + eb.Title(fmt.Sprintf("%v by %v", a.Title, a.Author)) } if tagsEnabled && len(a.Tags) > 0 { tags := arrays.Map(a.Tags, func(s string) string { return fmt.Sprintf("[%v](https://pixiv.net/en/tags/%v/artworks)", s, s) }) - eb.Tags = strings.Join(tags, " • ") + + eb.Description(fmt.Sprintf("**Tags**\n%v", strings.Join(tags, " • "))) } - for _, image := range a.Images { - eb.Images = append(eb.Images, image.previewProxy(a.proxy)) - eb.FieldValue2 = append(eb.FieldValue2, messages.ClickHere(image.originalProxy(a.proxy))) + eb.URL(a.url). + AddField("Likes", strconv.Itoa(a.Likes), true). + AddField("Original quality", messages.ClickHere(a.Images[0].originalProxy(a.proxy)), true). + Timestamp(a.CreatedAt) + + if footer != "" { + eb.Footer(footer, "") } - return eb.ToEmbed(), nil -} + if a.AIGenerated { + eb.AddField("⚠️ Disclaimer", "This artwork is AI-generated.") + } + + eb.Image(a.Images[0].previewProxy(a.proxy)) + pages = append(pages, &discordgo.MessageSend{Embeds: []*discordgo.MessageEmbed{eb.Finalize()}}) + if length > 1 { + for ind, image := range a.Images[1:] { + eb := embeds.NewBuilder() + + eb.Title(fmt.Sprintf("%v by %v | Page %v / %v", a.Title, a.Author, ind+2, length)) + eb.Image(image.previewProxy(a.proxy)) + eb.URL(a.url).Timestamp(a.CreatedAt) + + if footer != "" { + eb.Footer(footer, "") + } + + eb.AddField("Likes", strconv.Itoa(a.Likes), true) + eb.AddField("Original quality", messages.ClickHere(image.originalProxy(a.proxy)), true) + + pages = append(pages, &discordgo.MessageSend{Embeds: []*discordgo.MessageEmbed{eb.Finalize()}}) + } + } -func (a *Artwork) ArtworkID() string { - return a.ID + return pages, nil } func (a *Artwork) URL() string { diff --git a/artworks/pixiv/pixiv_suite_test.go b/artworks/pixiv/pixiv_suite_test.go index 9c13139..f83c033 100644 --- a/artworks/pixiv/pixiv_suite_test.go +++ b/artworks/pixiv/pixiv_suite_test.go @@ -16,7 +16,7 @@ func TestPixiv(t *testing.T) { var _ = DescribeTable( "Match Pixiv URL", func(url string, expectedID string, expectedResult bool) { - provider := pixiv.New("") + provider := pixiv.Pixiv{} id, ok := provider.Match(url) Expect(id).To(BeEquivalentTo(expectedID)) diff --git a/artworks/twitter/fxtwitter.go b/artworks/twitter/fxtwitter.go index e6d477f..06745c0 100644 --- a/artworks/twitter/fxtwitter.go +++ b/artworks/twitter/fxtwitter.go @@ -12,10 +12,11 @@ import ( "github.com/VTGare/boe-tea-go/internal/arrays" ) +var nonAlphanumericRegex = regexp.MustCompile(`[^\p{L}\p{N} -]+`) + type fxTwitter struct { twitterMatcher - client *http.Client - nonAlphanumericRegex *regexp.Regexp + client *http.Client } type fxTwitterResponse struct { @@ -53,13 +54,10 @@ type fxTwitterResponse struct { } `json:"tweet,omitempty"` } -func newFxTwitter(re *regexp.Regexp) artworks.Provider { +func newFxTwitter() artworks.Provider { return &fxTwitter{ - client: &http.Client{}, - nonAlphanumericRegex: regexp.MustCompile(`[^\p{L}\p{N} -]+`), - twitterMatcher: twitterMatcher{ - regex: re, - }, + twitterMatcher: twitterMatcher{}, + client: &http.Client{}, } } @@ -129,7 +127,7 @@ func (fxt *fxTwitter) Find(id string) (artworks.Artwork, error) { } artwork.AIGenerated = artworks.IsAIGenerated(arrays.Map(strings.Fields(artwork.Content), func(s string) string { - return fxt.nonAlphanumericRegex.ReplaceAllString(s, "") + return nonAlphanumericRegex.ReplaceAllString(s, "") })...) return artwork, nil diff --git a/artworks/twitter/matcher.go b/artworks/twitter/matcher.go index 60e8e4f..cef5453 100644 --- a/artworks/twitter/matcher.go +++ b/artworks/twitter/matcher.go @@ -2,24 +2,21 @@ package twitter import ( "net/url" - "regexp" "strconv" "strings" "github.com/VTGare/boe-tea-go/store" ) -type twitterMatcher struct { - regex *regexp.Regexp -} +type twitterMatcher struct{} -func (t twitterMatcher) Match(s string) (string, bool) { +func (twitterMatcher) Match(s string) (string, bool) { u, err := url.ParseRequestURI(s) if err != nil { return "", false } - if ok := t.regex.MatchString(u.Host); !ok { + if !strings.Contains(u.Host, "twitter.com") && u.Host != "x.com" { return "", false } diff --git a/artworks/twitter/twitter.go b/artworks/twitter/twitter.go index 8979ea7..cb6f84f 100644 --- a/artworks/twitter/twitter.go +++ b/artworks/twitter/twitter.go @@ -4,11 +4,9 @@ import ( "bytes" "errors" "fmt" - "github.com/VTGare/boe-tea-go/artworks/embed" "io" "net/http" "net/url" - "regexp" "strconv" "strings" "time" @@ -53,40 +51,33 @@ type Video struct { } func New() artworks.Provider { - re := regexp.MustCompile(`^(?:mobile\.)?(?:(?:fix(?:up|v))?x|(?:[fv]x)?twitter)\.com$`) - return &Twitter{ - providers: []artworks.Provider{newFxTwitter(re)}, - twitterMatcher: twitterMatcher{ - regex: re, - }, + providers: []artworks.Provider{newFxTwitter()}, } } func (t *Twitter) Find(id string) (artworks.Artwork, error) { - return artworks.NewError(t, func() (artworks.Artwork, error) { - var ( - artwork artworks.Artwork - errs []error - ) - - for _, provider := range t.providers { - var err error - artwork, err = provider.Find(id) - if errors.Is(err, ErrTweetNotFound) || errors.Is(err, ErrPrivateAccount) { - return nil, err - } - - if err != nil { - errs = append(errs, err) - continue - } + var ( + artwork artworks.Artwork + errs []error + ) + + for _, provider := range t.providers { + var err error + artwork, err = provider.Find(id) + if errors.Is(err, ErrTweetNotFound) || errors.Is(err, ErrPrivateAccount) { + return nil, artworks.NewError(t, err) + } - return artwork, nil + if err != nil { + errs = append(errs, err) + continue } - return &Artwork{}, errors.Join(errs...) - }) + return artwork, nil + } + + return &Artwork{}, artworks.NewError(t, errors.Join(errs...)) } func (a *Artwork) StoreArtwork() *store.Artwork { @@ -106,8 +97,8 @@ func (a *Artwork) StoreArtwork() *store.Artwork { // MessageSends transforms an artwork to discordgo embeds. func (a *Artwork) MessageSends(footer string, _ bool) ([]*discordgo.MessageSend, error) { + eb := embeds.NewBuilder() if a.FullName == "" && a.Len() == 0 { - eb := embeds.NewBuilder() eb.Title("❎ Tweet doesn't exist.") eb.Description("The tweet is NSFW or doesn't exist.\n\nUnsafe tweets can't be embedded due to API changes.") eb.Footer(footer, "") @@ -117,41 +108,80 @@ func (a *Artwork) MessageSends(footer string, _ bool) ([]*discordgo.MessageSend, }, nil } - eb := &embed.Embed{ - Title: a.FullName, - Username: a.Username, - Description: a.Content, - FieldName1: "Likes", - FieldValue1: strconv.Itoa(a.Likes), - FieldName2: "Retweets", - FieldValue2: []string{strconv.Itoa(a.Retweets)}, - URL: a.Permalink, - Timestamp: a.Timestamp, - AIGenerated: a.AIGenerated, + eb.URL(a.Permalink).Description(artworks.EscapeMarkdown(a.Content)).Timestamp(a.Timestamp) + + if a.Retweets > 0 { + eb.AddField("Retweets", strconv.Itoa(a.Retweets), true) + } + + if a.Likes > 0 { + eb.AddField("Likes", strconv.Itoa(a.Likes), true) + } + + if footer != "" { + eb.Footer(footer, "") + } + + if a.AIGenerated { + eb.AddField("⚠️ Disclaimer", "This artwork is AI-generated.") } if len(a.Videos) > 0 { return a.videoEmbed(eb) } - for _, image := range a.Photos { - eb.Images = append(eb.Images, image) + length := len(a.Photos) + tweets := make([]*discordgo.MessageSend, 0, length) + if length > 1 { + eb.Title(fmt.Sprintf("%v (%v) | Page %v / %v", a.FullName, a.Username, 1, length)) + } else { + eb.Title(fmt.Sprintf("%v (%v)", a.FullName, a.Username)) + } + + if length > 0 { + eb.Image(a.Photos[0]) + } + + tweets = append(tweets, &discordgo.MessageSend{ + Embeds: []*discordgo.MessageEmbed{eb.Finalize()}, + }) + + if len(a.Photos) > 1 { + for ind, photo := range a.Photos[1:] { + eb := embeds.NewBuilder() + + eb.Title(fmt.Sprintf("%v (%v) | Page %v / %v", a.FullName, a.Username, ind+2, length)).URL(a.Permalink) + eb.Image(photo).Timestamp(a.Timestamp) + + if footer != "" { + eb.Footer(footer, "") + } + + tweets = append(tweets, &discordgo.MessageSend{Embeds: []*discordgo.MessageEmbed{eb.Finalize()}}) + } } - return eb.ToEmbed(), nil + return tweets, nil } -func (a *Artwork) videoEmbed(eb *embed.Embed) ([]*discordgo.MessageSend, error) { +func (a *Artwork) videoEmbed(eb *embeds.Builder) ([]*discordgo.MessageSend, error) { + files := make([]*discordgo.File, 0, len(a.Videos)) for _, video := range a.Videos { file, err := downloadVideo(video.URL) if err != nil { return nil, err } - eb.Files = append(eb.Files, file) + files = append(files, file) } - return eb.ToEmbed(), nil + eb.Title(fmt.Sprintf("%v (%v)", a.FullName, a.Username)) + msg := &discordgo.MessageSend{ + Embeds: []*discordgo.MessageEmbed{eb.Finalize()}, + Files: files, + } + + return []*discordgo.MessageSend{msg}, nil } func downloadVideo(fileURL string) (*discordgo.File, error) { @@ -180,10 +210,6 @@ func downloadVideo(fileURL string) (*discordgo.File, error) { }, nil } -func (a *Artwork) ArtworkID() string { - return a.ID -} - func (a *Artwork) URL() string { return a.Permalink } diff --git a/artworks/twitter/twitter_suite_test.go b/artworks/twitter/twitter_suite_test.go index 232b6d4..5293e4b 100644 --- a/artworks/twitter/twitter_suite_test.go +++ b/artworks/twitter/twitter_suite_test.go @@ -24,8 +24,7 @@ var _ = DescribeTable( }, Entry("Valid artwork", "https://twitter.com/watsonameliaEN/status/1371674594675937282", "1371674594675937282", true), Entry("Query params", "https://twitter.com/watsonameliaEN/status/1371674594675937282?param=1", "1371674594675937282", true), - Entry("Mobile Twitter URL", "https://mobile.twitter.com/watsonameliaEN/status/1371674594675937282", "1371674594675937282", true), - Entry("Mobile X URL", "https://mobile.x.com/watsonameliaEN/status/1371674594675937282", "1371674594675937282", true), + Entry("Mobile URL", "https://mobile.twitter.com/watsonameliaEN/status/1371674594675937282", "1371674594675937282", true), Entry("No username", "https://twitter.com/i/status/1371674594675937282", "1371674594675937282", true), Entry("iweb URL", "https://twitter.com/i/web/status/1371674594675937282", "1371674594675937282", true), Entry("With photo suffix", "https://twitter.com/i/web/status/1371674594675937282/photo/1", "1371674594675937282", true), @@ -33,9 +32,6 @@ var _ = DescribeTable( Entry("ID with letters", "https://twitter.com/i/web/status/1371674594675937282f", "", false), Entry("Different domain", "https://google.com/i/status/123456", "", false), Entry("Invalid URL", "efe", "", false), - Entry("fxtwitter link", "https://fxtwitter.com/i/status/1234", "1234", true), Entry("vxtwitter link", "https://vxtwitter.com/i/status/1234", "1234", true), Entry("X link", "https://x.com/i/status/1234", "1234", true), - Entry("fixupx link", "https://fixupx.com/i/status/1234", "1234", true), - Entry("fixvx link", "https://fixvx.com/i/status/1234", "1234", true), )