-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathblog.go
175 lines (158 loc) · 5.02 KB
/
blog.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package main
import (
"context"
"fmt"
"github.com/andersfylling/disgord"
"github.com/meooow25/cfspy/bot"
"github.com/meooow25/cfspy/fetch"
)
// Length limits for short preview of blog/comments. Much lower than Discord message limits.
const (
msgLimit = 200
msgSlack = 100
)
// Installs the blog watcher feature. The bot watches for Codeforces blog and comment links and
// responds with an embed containing info about the blog or comment.
func installBlogAndCommentFeature(bot *bot.Bot) {
bot.Client.Logger().Info("Setting up CF blog and comment feature")
bot.OnMessageCreate(maybeHandleBlogURL)
}
func maybeHandleBlogURL(ctx *bot.Context, evt *disgord.MessageCreate) {
go func() {
blogURLMatches := fetch.ParseBlogURLs(evt.Message.Content)
if len(blogURLMatches) == 0 {
return
}
first := blogURLMatches[0]
if first.CommentID != "" {
handleCommentURL(ctx, first.URL, first.CommentID)
} else {
handleBlogURL(ctx, first.URL)
}
}()
}
// Fetches the blog page and responds on the Discord channel with some basic info on the blog.
func handleBlogURL(ctx *bot.Context, blogURL string) {
ctx.Logger.Info("Processing blog URL: ", blogURL)
blogInfo, err := fetch.Blog(context.Background(), blogURL)
if err != nil {
err = fmt.Errorf("Error fetching blog from %v: %w", blogURL, err)
ctx.Logger.Error(err)
respondWithError(ctx, err)
return
}
short, full := makeBlogEmbeds(blogInfo)
var page *bot.Page
if full != nil {
page = bot.NewPageWithExpansion("", short, "", full)
} else {
page = bot.NewPage("", short)
}
if err = respondWithOnePagePreview(ctx, page); err != nil {
ctx.Logger.Error(fmt.Errorf("Error sending blog info: %w", err))
}
}
func makeBlogEmbeds(b *fetch.BlogInfo) (short *disgord.Embed, full *disgord.Embed) {
embed := &disgord.Embed{
Title: b.Title,
URL: b.URL,
Author: &disgord.EmbedAuthor{
Name: b.AuthorHandle + "'s blog",
},
Description: b.Content,
Thumbnail: &disgord.EmbedThumbnail{
URL: b.AuthorAvatar,
},
Timestamp: disgord.Time{
Time: b.CreationTime,
},
Footer: &disgord.EmbedFooter{
Text: fmt.Sprintf("Score %+d", b.Rating),
},
Color: b.AuthorColor,
}
if len(b.Images) > 0 {
embed.Image = &disgord.EmbedImage{URL: b.Images[0]}
}
return makeShortAndFullEmbeds(embed)
}
// Fetches the comment from the blog page, converts it to markdown and responds on the Discord
// channel.
func handleCommentURL(ctx *bot.Context, commentURL, commentID string) {
ctx.Logger.Info("Processing comment URL: ", commentURL)
revisionCount, infoGetter, err := fetch.Comment(context.Background(), commentURL, commentID)
if err != nil {
err = fmt.Errorf("Error fetching comment from %v: %w", commentURL, err)
ctx.Logger.Error(err)
respondWithError(ctx, err)
return
}
getPage := func(revision int) *bot.Page {
commentInfo, err := infoGetter(revision)
if err != nil {
err := fmt.Errorf("Error fetching revision %v of comment %v: %w", revision, commentURL, err)
ctx.Logger.Error(err)
return bot.NewPage("", ctx.MakeErrorEmbed(err.Error()))
}
short, full := makeCommentEmbeds(commentInfo)
if full != nil {
return bot.NewPageWithExpansion("", short, "", full)
}
return bot.NewPage("", short)
}
if err = respondWithMultiPagePreview(ctx, getPage, revisionCount); err != nil {
ctx.Logger.Error(fmt.Errorf("Error sending comment preview: %w", err))
}
}
func makeCommentEmbeds(c *fetch.CommentInfo) (short *disgord.Embed, full *disgord.Embed) {
revisionStr := ""
if c.RevisionCount > 1 {
revisionStr = fmt.Sprintf(
" • Revision %v/%v", c.Revision, c.RevisionCount)
}
embed := &disgord.Embed{
Title: c.BlogTitle,
URL: c.URL,
Author: &disgord.EmbedAuthor{
Name: "Comment by " + c.AuthorHandle,
},
Description: c.Content,
Thumbnail: &disgord.EmbedThumbnail{
URL: c.AuthorAvatar,
},
Timestamp: disgord.Time{
Time: c.CreationTime,
},
Footer: &disgord.EmbedFooter{
Text: fmt.Sprintf("Score %+d%s", c.Rating, revisionStr),
},
Color: c.AuthorColor,
}
if len(c.Images) > 0 {
embed.Image = &disgord.EmbedImage{URL: c.Images[0]}
}
return makeShortAndFullEmbeds(embed)
}
func makeShortAndFullEmbeds(embed *disgord.Embed) (short *disgord.Embed, full *disgord.Embed) {
full = embed
short = disgord.DeepCopy(full).(*disgord.Embed)
short.Description = truncate(full.Description)
// If the content is short enough, no need for full.
// If the content is too long, full cannot be shown.
if full.Description == short.Description || bot.EmbedDescriptionTooLong(full) {
full = nil
}
return
}
// Returns the string unchanged if the length is within msgLimit+msgSlack, otherwise returns it
// truncated to msgLimit chars. The motivation for the slack is that the poster would probably want
// to display the full content anyway if it is a bit over the limit.
func truncate(s string) string {
if len(s) <= msgLimit+msgSlack {
return s
}
// Cutting off everything beyond limit doesn't care about markdown formatting and can leave
// unclosed markup.
// TODO: Maybe use a markdown parser to properly handle these.
return s[:msgLimit] + "…"
}