-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathtelegram.go
346 lines (289 loc) · 9.72 KB
/
telegram.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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
// This is the function that is called by google functions,
// the structure of the code must be like specified in the
// docs https://cloud.google.com/functions/docs/writing#directory-structure
package telegram
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net/http"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"text/template"
"time"
"github.com/Arturomtz8/github-inspector/pkg/github"
"github.com/GoogleCloudPlatform/functions-framework-go/functions"
)
const (
searchCommand string = "/search"
trendingCommand string = "/trend"
telegramApiBaseUrl string = "https://api.telegram.org/bot"
telegramApiSendMessage string = "/sendMessage"
telegramTokenEnv string = "GITHUB_BOT_TOKEN"
defaulRepoLen int = 4
repoExpr string = `^\/search\s(([A-Za-z0-9\-\_]+))\s*.*`
langExpr string = `^\/search\s.*\s+lang:([\w]*)`
authorExpr string = `^\/search\s.*\s+author:(([A-Za-z0-9\-\_]+))`
langParam string = "lang"
authorParam string = "author"
)
const templ = `
{{.FullName}}: {{.Description}}
Author: {{.Owner.Login}}
⭐: {{.StargazersCount}}
{{.HtmlURL}}
`
// Chat struct stores the id of the chat in question.
type Chat struct {
Id int `json:"id"`
Title string `json:"title"`
Username string `json:"username"`
Type string `json:"type"`
}
// Message struct store Chat and text data.
type Message struct {
Text string `json:"text"`
Chat Chat `json:"chat"`
}
// trigger deploy
// Update event.
type Update struct {
UpdateId int `json:"update_id"`
Message Message `json:"message"`
}
// Register an HTTP function with the Functions Framework
func init() {
functions.HTTP("HandleTelegramWebhook", HandleTelegramWebhook)
}
// HandleTelegramWebhook is the web hook that has to have the handler signature.
// Listen for incoming web requests from Telegram events and
// responds back with the treding repositories on GitHub.
func HandleTelegramWebhook(w http.ResponseWriter, r *http.Request) {
var update, err = parseTelegramRequest(r)
if err != nil {
log.Printf("error parsing update, %s", err.Error())
return
}
log.Printf("incoming request from chat with username %s", update.Message.Chat.Username)
// Handle multiple commands.
switch {
// Handle /search command to return a single repository data of interest.
case strings.HasPrefix(update.Message.Text, searchCommand):
// Get params from text message.
repo, lang, author, err := ExtractParams(update.Message.Text)
if err != nil {
sendTextToTelegramChat(update.Message.Chat.Id, err.Error())
fmt.Fprintf(w, "invalid input %s with error %v", update.Message.Text, err)
return
}
// Get repository based off received params.
repository, err := github.GetRepository(github.RepoURL, repo, lang, author)
if err != nil {
sendTextToTelegramChat(update.Message.Chat.Id, err.Error())
fmt.Fprintf(w, "invalid input %s with error %v", update.Message.Text, err)
return
}
// Parse Repo into text template.
repoText, err := parseRepoToTemplate(repository)
// Send responsa back to chat.
_, err = sendTextToTelegramChat(update.Message.Chat.Id, repoText)
if err != nil {
sendTextToTelegramChat(update.Message.Chat.Id, err.Error())
fmt.Fprintf(w, "invalid input %s with error %v", update.Message.Text, err)
return
}
// Handle /trend command to return a list of treding repositories on GitHub.
case strings.HasPrefix(update.Message.Text, trendingCommand):
sanitizedString, err := sanitize(update.Message.Text, trendingCommand)
if err != nil {
sendTextToTelegramChat(update.Message.Chat.Id, err.Error())
fmt.Fprintf(w, "invalid input %s with error %v", update.Message.Text, err)
return
}
log.Printf("sanitized string: %s", sanitizedString)
repos, err := github.GetTrendingRepos(github.TimeToday, sanitizedString)
if err != nil {
sendTextToTelegramChat(update.Message.Chat.Id, err.Error())
fmt.Fprintf(w, "an error has ocurred, %s!", err)
return
}
responseFunc, err := formatReposContentAndSend(repos, update.Message.Chat.Id)
if err != nil {
sendTextToTelegramChat(update.Message.Chat.Id, err.Error())
fmt.Printf("got error %v from parsing repos", err)
return
}
log.Printf("successfully distributed to chat id %d, response from loop: %s", update.Message.Chat.Id, responseFunc)
return
default:
log.Printf("invalid command: %s", update.Message.Text)
return
}
}
// parseTelegramRequest decodes and incoming request from the Telegram hook,
// and returns an Update pointer.
func parseTelegramRequest(r *http.Request) (*Update, error) {
var update Update
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
log.Printf("could not decode incoming update %s", err.Error())
return nil, err
}
return &update, nil
}
// returns the term that wants to be searched or
// an string that specifies the expected input
func sanitize(s, botCommand string) (string, error) {
var lenBotCommand int = len(botCommand)
if len(s) >= lenBotCommand {
if s[:lenBotCommand] == botCommand {
s = s[lenBotCommand:]
s = strings.TrimSpace(s)
log.Printf("type of value entered: %T", s)
}
} else {
return "", fmt.Errorf("invalid command: %s", s)
}
return s, nil
}
// Formats the content of the repos and uses internally sendTextToTelegramChat function
// for sending the formatted content to the respective chat
func formatReposContentAndSend(repos *github.TrendingSearchResult, chatId int) (string, error) {
reposContent := make([]string, 0)
// suffle the repos
rand.Seed(time.Now().UnixNano())
rand.Shuffle(len(repos.Items), func(i, j int) { repos.Items[i], repos.Items[j] = repos.Items[j], repos.Items[i] })
for index, repo := range repos.Items {
if index <= defaulRepoLen {
var report = template.Must(template.New("trendinglist").Parse(templ))
buf := &bytes.Buffer{}
if err := report.Execute(buf, repo); err != nil {
sendTextToTelegramChat(chatId, err.Error())
return "", err
}
s := buf.String()
reposContent = append(reposContent, s)
}
}
if len(reposContent) == 0 {
return "", errors.New("there are not trending repos yet for today, try again later")
}
log.Println("template created and proceeding to send repos to chat")
log.Println("repos count to be sent", len(reposContent))
text := strings.Join(reposContent, "\n-------------\n")
_, err := sendTextToTelegramChat(chatId, text)
if err != nil {
log.Printf("error occurred publishing event %v", err)
return "", err
}
return "all repos sent to chat", nil
}
// sendTextToTelegramChat sends the response from the GitHub back to the chat,
// given a chat id and the text from GitHub.
func sendTextToTelegramChat(chatId int, text string) (string, error) {
fmt.Printf("sending %s to chat_id: %d", text, chatId)
var telegramApi string = "https://api.telegram.org/bot" + os.Getenv("GITHUB_BOT_TOKEN") + "/sendMessage"
response, err := http.PostForm(
telegramApi,
url.Values{
"chat_id": {strconv.Itoa(chatId)},
"text": {text},
})
if err != nil {
log.Printf("error when posting text to the chat: %s", err.Error())
return "", err
}
defer response.Body.Close()
var bodyBytes, errRead = ioutil.ReadAll(response.Body)
if errRead != nil {
log.Printf("error parsing telegram answer %s", errRead.Error())
return "", err
}
bodyString := string(bodyBytes)
log.Printf("body of telegram response: %s", bodyString)
return bodyString, nil
}
// ExtractParams parse the command sent by the user and returns
// the name of the repository of interest (mandatory),
// the programming languague (if provided),
// the author of the repository (if provided).
// The structure of the command is the shown below:
// /search <repository> lang:<lang> author:<author>
// e.g.
// /search dblab lang:go author:danvergara
func ExtractParams(s string) (string, string, string, error) {
s = strings.TrimSpace(s)
repo, err := extractRepo(s)
if err != nil {
return "", "", "", err
}
lang, err := extractOptionalParam(s, langParam)
if err != nil {
return "", "", "", err
}
author, err := extractOptionalParam(s, authorParam)
if err != nil {
return "", "", "", err
}
return repo, lang, author, nil
}
// extractRepo returns the name of the repository,
// this is a mandatory parameter.
// This function will error out if the repos is not found.
func extractRepo(s string) (string, error) {
repoRegexp, err := regexp.Compile(repoExpr)
if err != nil {
return "", err
}
matches := repoRegexp.FindStringSubmatch(s)
if len(matches) >= 2 {
return matches[1], nil
}
return "", fmt.Errorf("repo not found in %s", s)
}
// extractOptionalParam returns the value of the param in question.
// This function will not error out if the value is not found,
// since this kind of params is not mandatory.
func extractOptionalParam(s, param string) (string, error) {
var matches []string
switch param {
case langParam:
langRegexp, err := regexp.Compile(langExpr)
if err != nil {
return "", err
}
matches = langRegexp.FindStringSubmatch(s)
if len(matches) >= 2 {
return matches[1], nil
}
case authorParam:
authorRegexp, err := regexp.Compile(authorExpr)
if err != nil {
return "", err
}
matches = authorRegexp.FindStringSubmatch(s)
if len(matches) >= 2 {
return matches[1], nil
}
default:
return "", fmt.Errorf("%s option not supported", param)
}
// optional parameters.
return "", nil
}
// parseRepoToTemplate returns a text parsed based on the template constant,
// to display the repository nicely to the user in the Telegram chat.
func parseRepoToTemplate(repo *github.RepoTrending) (string, error) {
var report = template.Must(template.New("getrepo").Parse(templ))
buf := &bytes.Buffer{}
if err := report.Execute(buf, repo); err != nil {
return "", err
}
return buf.String(), nil
}