blob: 93fa7025e6415fa2ad00e887e40775d21da354a5 [file] [log] [blame]
avm999639bbb3a42020-12-29 03:29:44 +01001package main
2
3import (
4 "bufio"
5 "encoding/json"
6 "fmt"
7 "io"
8 "io/ioutil"
9 "log"
10 "net/http"
11 "os"
12 "strings"
13 "time"
14)
15
16const blockFileName = "blocked-users.txt"
17const projectId = "191707"
18const checkAttempts = 5
19const checkWaitTime = 2 * time.Second
20const baseApiURL = "https://api.crowdin.com/api/v2/"
21const i18nCreditsFile = "../../src/json/i18n-credits.json"
22
23// Contributors who have sent translations before the Crowdin instance
24// was set up:
25var additionalContributors = []Contributor{
26 Contributor{
27 Name: "Alexander Simkin",
28 Languages: []Language{
29 Language{
30 Id: "ru",
31 Name: "Russian",
32 }},
33 }}
34
35type User struct {
36 Id string `json:"id"`
37 Username string `json:"username"`
38 FullName string `json:"fullName"`
39 AvatarUrl string `json:"avatarUrl"`
40}
41
42type Language struct {
43 Id string `json:"id"`
44 Name string `json:"name"`
45}
46
47type TopUser struct {
48 User User `json:"user"`
49 Languages []Language `json:"languages"`
50 Translated int `json:"translated"`
51 Target int `json:"target"`
52 Approved int `json:"approved"`
53 Voted int `json:"voted"`
54 PositiveVotes int `json:"positiveVotes"`
55 NegativeVotes int `json:"negativeVotes"`
56 Winning int `json:"winning"`
57}
58
59type DateRange struct {
60 from string `json:"from"`
61 to string `json:"to"`
62}
63
64type Report struct {
65 Name string `json:"name"`
66 Url string `json:"url"`
67 Unit string `json:"unit"`
68 DateRange DateRange `json:"dateRange"`
69 Language string `json:"language"`
70 TopUsers []TopUser `json:"data"`
71}
72
73type Contributor struct {
74 Name string `json:"name"`
75 Languages []Language `json:"languages"`
76}
77
78func isBlocked(blockedUsers []string, user string) bool {
79 for _, u := range blockedUsers {
80 if user == u {
81 return true
82 }
83 }
84 return false
85}
86
87func getBlockedUsers() []string {
88 blockFile, err := os.Open(blockFileName)
89 if err != nil {
90 log.Fatalf("Couldn't open blockfile, error: %v", err)
91 }
92 defer blockFile.Close()
93
94 blocked := make([]string, 0)
95
96 scanner := bufio.NewScanner(blockFile)
97 for scanner.Scan() {
98 line := strings.TrimSpace(scanner.Text())
99 if len(line) == 0 || line[0] == '#' {
100 continue
101 }
102 blocked = append(blocked, line)
103 }
104
105 return blocked
106}
107
108func getJSONFromResponseBody(body *io.ReadCloser) (map[string]interface{}, error) {
109 var responseJSON map[string]interface{}
110
111 responseRawBody, err := ioutil.ReadAll(*body)
112 if err != nil {
113 return responseJSON, err
114 }
115 if err := json.Unmarshal(responseRawBody, &responseJSON); err != nil {
116 return responseJSON, err
117 }
118
119 return responseJSON, nil
120}
121
122func apiCall(path string, method string, body string) (*http.Response, error) {
123 token, isTokenSet := os.LookupEnv("GTRANSLATE_CROWDIN_API_KEY")
124 if !isTokenSet {
125 return nil, fmt.Errorf("Environmental variable GTRANSLATE_CROWDIN_API_KEY is not set.")
126 }
127
128 if body == "" {
129 body = "{}"
130 }
131
132 client := &http.Client{
133 Timeout: 10 * time.Second,
134 }
135
136 url := baseApiURL + path
137 bodyReader := strings.NewReader(body)
138 request, err := http.NewRequest(method, url, bodyReader)
139 if err != nil {
140 return nil, err
141 }
142
143 request.Header.Add("Authorization", "Bearer "+token)
144 request.Header.Add("Content-Type", "application/json")
145
146 return client.Do(request)
147}
148
149func generateReport(projectId string) (string, error) {
150 response, err := apiCall("projects/"+projectId+"/reports", "POST", `
151 {
152 "name": "top-members",
153 "schema": {
154 "unit": "words",
155 "format": "json"
156 }
157 }
158 `)
159 if err != nil {
160 return "", fmt.Errorf("Error while requesting top users report: %v", err)
161 }
162
163 if response.StatusCode != 201 {
164 return "", fmt.Errorf("Error while requesting top users report (status code %d)", response.StatusCode)
165 }
166
167 responseJSON, err := getJSONFromResponseBody(&response.Body)
168 if err != nil {
169 return "", err
170 }
171
172 data := responseJSON["data"].(map[string]interface{})
173 return data["identifier"].(string), nil
174}
175
176func isReportGenerated(projectId string, reportId string) (bool, error) {
177 response, err := apiCall("projects/"+projectId+"/reports/"+reportId, "GET", "{}")
178 if err != nil {
179 return false, fmt.Errorf("Error while checking report generation: %v", err)
180 }
181
182 if response.StatusCode != 200 {
183 return false, fmt.Errorf("Error while checking report generation (status code %d)", response.StatusCode)
184 }
185
186 responseJSON, err := getJSONFromResponseBody(&response.Body)
187 if err != nil {
188 return false, err
189 }
190
191 data := responseJSON["data"].(map[string]interface{})
192 return data["status"].(string) == "finished", nil
193}
194
195func getReportUrl(projectId string, reportId string) (string, error) {
196 response, err := apiCall("projects/"+projectId+"/reports/"+reportId+"/download", "GET", "{}")
197 if err != nil {
198 return "", fmt.Errorf("Error while retrieving top users report download URL: %v", err)
199 }
200
201 if response.StatusCode != 200 {
202 return "", fmt.Errorf("Error while retrieving top users report download URL (status code %d)", response.StatusCode)
203 }
204
205 responseJSON, err := getJSONFromResponseBody(&response.Body)
206 if err != nil {
207 return "", err
208 }
209
210 data := responseJSON["data"].(map[string]interface{})
211 return data["url"].(string), nil
212}
213
214func getReport(projectId string, reportId string) (Report, error) {
215 reportUrl, err := getReportUrl(projectId, reportId)
216 if err != nil {
217 return Report{}, err
218 }
219
220 response, err := http.Get(reportUrl)
221 if err != nil {
222 return Report{}, fmt.Errorf("An error occurred while downloading the report: %v", err)
223 }
224
225 var report Report
226
227 responseRawBody, err := ioutil.ReadAll(response.Body)
228 if err != nil {
229 return Report{}, err
230 }
231 if err := json.Unmarshal(responseRawBody, &report); err != nil {
232 return Report{}, err
233 }
234
235 return report, nil
236}
237
238func getContributorsFromReport(report Report) []Contributor {
239 blockedUsers := getBlockedUsers()
240
241 contributors := make([]Contributor, 0)
242 for _, c := range additionalContributors {
243 contributors = append(contributors, c)
244 }
245 for _, u := range report.TopUsers {
246 if u.Translated <= 0 || isBlocked(blockedUsers, u.User.Username) {
247 continue
248 }
249 contributors = append(contributors, Contributor{
250 Name: u.User.FullName,
251 Languages: u.Languages,
252 })
253 }
254 return contributors
255}
256
257func GetContributors(projectId string) ([]Contributor, error) {
258 id, err := generateReport(projectId)
259 if err != nil {
260 return nil, err
261 }
262
263 log.Printf("Top users report requested successfully (assigned id: %v)", id)
264
265 reportGenerated := false
266 for i := 0; i < checkAttempts; i++ {
267 currReportGenerated, err := isReportGenerated(projectId, id)
268 if err != nil {
269 log.Printf("[Try %d] Couldn't check whether the top users report has been generated, error: %v", i+1, err)
270 } else if currReportGenerated {
271 log.Printf("[Try %d] The top users report has been generated.", i+1)
272 reportGenerated = true
273 break
274 } else {
275 log.Printf("[Try %d] Top users report hasn't still been generated.", i+1)
276 }
277 time.Sleep(checkWaitTime)
278 }
279
280 if !reportGenerated {
281 return nil, fmt.Errorf("After %d checks, the top users report hasn't still been generated. Aborting.", checkAttempts)
282 }
283
284 report, err := getReport(projectId, id)
285 if err != nil {
286 return nil, fmt.Errorf("Couldn't retrieve top users report, error: %v", err)
287 }
288
289 return getContributorsFromReport(report), nil
290}
291
292func main() {
293 log.SetPrefix("[generate-i18n-credits] ")
294 log.SetFlags(0)
295
296 log.Println("Starting to generate i18n credits")
297
298 contributors, err := GetContributors(projectId)
299 if err != nil {
300 log.Fatalf("%v", err)
301 }
302
303 creditsFile, err := os.Create(i18nCreditsFile)
304 if err != nil {
305 log.Fatalf("Couldn't create i18n credits file, error: %v", err)
306 }
307 defer creditsFile.Close()
308
309 JSONBytes, err := json.MarshalIndent(contributors, "", " ")
310 if err != nil {
311 log.Fatalf("Couldn't marshal Contributors interface, error: %v", err)
312 }
313
314 if _, err := creditsFile.Write(JSONBytes); err != nil {
315 log.Fatalf("Couldn't write to i18n credits file, error: %v", err)
316 }
317
318 log.Println("Done!")
319}