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