Passed
Push — master ( 373907...3fcd0c )
by Stanislav
01:37
created

client.TogglClient.SumEntriesGroup   C

Complexity

Conditions 10

Size

Total Lines 26
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
dl 0
loc 26
rs 5.9999
c 0
b 0
f 0
eloc 18
nop 1

How to fix   Complexity   

Complexity

Complex classes like client.TogglClient.SumEntriesGroup often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
package client
2
3
import (
4
	"fmt"
5
	"github.com/popstas/go-toggl"
6
	"github.com/popstas/planfix-go/planfix"
7
	"github.com/viasite/planfix-toggl-server/app/config"
8
	"log"
9
	"math"
10
	"net/smtp"
11
	"regexp"
12
	"strconv"
13
	"time"
14
	"sort"
15
)
16
17
// данные не меняются при этой опции
18
var analiticDataCached PlanfixAnaliticData
19
20
// TogglClient - Клиент, общающийся с Toggl и Планфиксом
21
type TogglClient struct {
22
	Session    toggl.Session
23
	Config     *config.Config
24
	PlanfixAPI planfix.API
25
	Logger     *log.Logger
26
}
27
28
// PlanfixEntryData - Данные, подмешивающиеся к toggl.DetailedTimeEntry
29
type PlanfixEntryData struct {
30
	Sent       bool `json:"sent"`
31
	TaskID     int  `json:"task_id"`
32
	GroupCount int  `json:"group_count"`
33
}
34
35
// TogglPlanfixEntry - toggl.DetailedTimeEntry дополнительными данными о задаче в Планфиксе
36
type TogglPlanfixEntry struct {
37
	toggl.DetailedTimeEntry
38
	Planfix PlanfixEntryData `json:"planfix"`
39
}
40
41
// TogglPlanfixEntryGroup - группа toggl-записей, объединенных одной задачей Планфикса
42
type TogglPlanfixEntryGroup struct {
43
	Entries         []TogglPlanfixEntry
44
	Description     string
45
	Project         string
46
	ProjectHexColor string
47
	Duration        int64
48
}
49
50
// PlanfixAnaliticData - данные аналитики, которая будет проставляться в Планфикс
51
type PlanfixAnaliticData struct {
52
	ID          int
53
	TypeID      int
54
	TypeValueID int
55
	CountID     int
56
	CommentID   int
57
	DateID      int
58
	UsersID     int
59
}
60
61
// RunSender - запускалка цикла отправки накопившихся toggl-записей
62
func (c TogglClient) RunSender() {
63
	if c.Config.SendInterval <= 0 {
64
		c.Logger.Println("[INFO] No send interval, sending disabled")
65
		return
66
	}
67
68
	time.Sleep(1 * time.Second) // wait for server start
69
	for {
70
		c.SendToPlanfix()
71
		time.Sleep(time.Duration(c.Config.SendInterval) * time.Minute)
72
	}
73
}
74
75
// RunTagCleaner - запускалка цикла очистки запущенных toggl-записей от тега sent
76
func (c TogglClient) RunTagCleaner() {
77
	time.Sleep(1 * time.Second) // wait for server start
78
	for {
79
		entry, err := c.Session.GetCurrentTimeEntry()
80
		if err != nil {
81
			c.Logger.Println("[ERROR] failed to get current toggl entry")
82
			continue
83
		}
84
85
		// delete sent tag
86
		for _, tag := range entry.Tags {
87
			if tag == c.Config.TogglSentTag {
88
				c.Logger.Printf("[INFO] removed %s tag from current toggl entry", c.Config.TogglSentTag)
89
				c.Session.AddRemoveTag(entry.ID, c.Config.TogglSentTag, false)
90
			}
91
		}
92
93
		time.Sleep(1 * time.Minute)
94
	}
95
}
96
97
// SendToPlanfix получает записи из Toggl и отправляет в Планфикс
98
// * нужна, чтобы сохранился c.PlanfixAPI.Sid при авторизации
99
func (c *TogglClient) SendToPlanfix() (sumEntries []TogglPlanfixEntry, err error) {
100
	c.Logger.Println("[INFO] send to planfix")
101
	pendingEntries, err := c.GetPendingEntries()
102
	if err != nil {
103
		return []TogglPlanfixEntry{}, err
104
	}
105
	c.Logger.Printf("[INFO] found %d pending entries", len(pendingEntries))
106
	grouped := c.GroupEntriesByTask(pendingEntries)
107
	for taskID, entries := range grouped {
108
		err := c.sendEntries(taskID, entries)
109
		if err != nil {
110
			c.Logger.Printf("[ERROR] entries of task #%d failed to send", taskID)
111
		} else {
112
			c.Logger.Printf("[INFO] entries sent to https://%s.planfix.ru/task/%d", c.Config.PlanfixAccount, taskID)
113
		}
114
	}
115
	return c.SumEntriesGroup(grouped), nil
116
}
117
118
// GroupEntriesByTask объединяет плоский список toggl-записей в map c ключом - ID задачи в Планфиксе
119
func (c TogglClient) GroupEntriesByTask(entries []TogglPlanfixEntry) (grouped map[int][]TogglPlanfixEntry) {
120
	grouped = make(map[int][]TogglPlanfixEntry)
121
	if len(entries) == 0 {
122
		return grouped
123
	}
124
	for _, entry := range entries {
125
		grouped[entry.Planfix.TaskID] = append(grouped[entry.Planfix.TaskID], entry)
126
	}
127
	return grouped
128
}
129
130
// SumEntriesGroup объединяет несколько toggl-записей в одну с просуммированным временем
131
// Входной map формируется через GroupEntriesByTask
132
// Ключ массива - ID задачи в Планфиксе
133
func (c TogglClient) SumEntriesGroup(grouped map[int][]TogglPlanfixEntry) (summed []TogglPlanfixEntry) {
134
	g := make(map[int]TogglPlanfixEntry)
135
	for taskID, entries := range grouped {
136
		for _, entry := range entries {
137
			if ge, ok := g[taskID]; ok {
138
				ge.Duration += entry.Duration
139
				ge.Planfix.GroupCount++
140
				g[entry.Planfix.TaskID] = ge
141
			} else {
142
				g[entry.Planfix.TaskID] = entry
143
			}
144
		}
145
	}
146
147
	keys := make([]int, 0, len(g))
148
	for k := range g {
149
		keys = append(keys, k)
150
	}
151
	sort.Ints(keys)
152
153
	summed = make([]TogglPlanfixEntry, 0, len(g))
154
	for _, taskID := range keys {
155
		summed = append(summed, g[taskID])
156
	}
157
158
	return summed
159
}
160
161
// GetTogglUserID возвращает ID юзера в Toggl
162
func (c TogglClient) GetTogglUserID() int {
163
	account, err := c.Session.GetAccount()
164
	if err != nil {
165
		c.Logger.Fatalf("[ERROR] Failed to get Toggl UserID, check TogglAPIToken, %s", err.Error())
166
	}
167
	return account.Data.ID
168
}
169
170
// GetPlanfixUserID возвращает ID юзера в Планфиксе
171
func (c TogglClient) GetPlanfixUserID() int {
172
	var user planfix.XMLResponseUserGet
173
	user, err := c.PlanfixAPI.UserGet(0)
174
	if err != nil {
175
		c.Logger.Fatalf("[ERROR] Failed to get Planfix UserID, check PlanfixAPIKey, PlanfixAPIUrl, PlanfixUserName, PlanfixUserPassword, %s", err.Error())
176
	}
177
	return user.User.ID
178
}
179
180
// GetEntries получает []toggl.DetailedTimeEntry и превращает их в []TogglPlanfixEntry с подмешенными данными Планфикса
181
func (c TogglClient) GetEntries(togglWorkspaceID int, since, until string) (entries []TogglPlanfixEntry, err error) {
182
	report, err := c.Session.GetDetailedReport(togglWorkspaceID, since, until, 1)
183
	if err != nil {
184
		c.Logger.Printf("[ERROR] Toggl: %s", err)
185
	}
186
187
	for _, entry := range report.Data {
188
189
		pfe := TogglPlanfixEntry{
190
			DetailedTimeEntry: entry,
191
			Planfix: PlanfixEntryData{
192
				Sent:       false,
193
				TaskID:     0,
194
				GroupCount: 1,
195
			},
196
		}
197
198
		for _, tag := range entry.Tags {
199
			// only digit == planfix.task_id
200
			regex := regexp.MustCompile(`^\d+$`)
201
			if regex.MatchString(tag) {
202
				pfe.Planfix.TaskID, _ = strconv.Atoi(tag)
203
			}
204
205
			// sent tag
206
			if tag == c.Config.TogglSentTag {
207
				pfe.Planfix.Sent = true
208
			}
209
		}
210
211
		entries = append(entries, pfe)
212
	}
213
214
	return entries, nil
215
}
216
217
// filter - хэлпер, фильтрующий toggl-записи
218
func filter(input []TogglPlanfixEntry, f func(entry TogglPlanfixEntry) bool) (output []TogglPlanfixEntry) {
219
	for _, v := range input {
220
		if f(v) {
221
			output = append(output, v)
222
		}
223
	}
224
	return output
225
}
226
227
// GetPendingEntries возвращает toggl-записи, которые должны быть отправлены в Планфикс
228
func (c TogglClient) GetPendingEntries() ([]TogglPlanfixEntry, error) {
229
	entries, err := c.GetEntries(
230
		c.Config.TogglWorkspaceID,
231
		time.Now().AddDate(0, 0, -30).Format("2006-01-02"),
232
		time.Now().AddDate(0, 0, 1).Format("2006-01-02"),
233
	)
234
	if err != nil {
235
		return []TogglPlanfixEntry{}, err
236
	}
237
	entries = filter(entries, func(entry TogglPlanfixEntry) bool { return entry.Planfix.TaskID != 0 })
238
	entries = filter(entries, func(entry TogglPlanfixEntry) bool { return !entry.Planfix.Sent })
239
	entries = filter(entries, func(entry TogglPlanfixEntry) bool { return entry.Uid == c.Config.TogglUserID })
240
	return entries, nil
241
}
242
243
// sendEntries отправляет toggl-записи в Планфикс и помечает их в Toggl тегом sent
244
func (c TogglClient) sendEntries(planfixTaskID int, entries []TogglPlanfixEntry) error {
245
	// будет точно просуммировано в одну
246
	sumEntry := c.SumEntriesGroup(map[int][]TogglPlanfixEntry{
247
		planfixTaskID: entries,
248
	})[0]
249
250
	date := sumEntry.Start.Format("2006-01-02")
251
	mins := int(math.Floor(float64(sumEntry.Duration)/60000 + .5))
252
	entryString := fmt.Sprintf(
253
		"[%s] %s (%d)",
254
		sumEntry.Project,
255
		sumEntry.Description,
256
		mins,
257
	)
258
	comment := fmt.Sprintf("toggl: %s", entryString)
259
260
	c.Logger.Printf("[DEBUG] sending %s", entryString)
261
	if c.Config.DryRun {
262
		c.Logger.Println("[DEBUG] dry-run")
263
		return nil
264
	}
265
266
	// send to planfix
267
	var err error
268
	if c.Config.PlanfixUserID != 0 {
269
		err = c.sendWithPlanfixAPI(planfixTaskID, date, mins, comment)
270
	} else {
271
		err = c.sendWithSMTP(planfixTaskID, date, mins)
272
	}
273
	if err != nil {
274
		c.Logger.Printf("[ERROR] %v", err)
275
		return err
276
	}
277
278
	return c.markAsSent(entries)
279
}
280
281
// markAsSent отмечает toggl-записи тегом sent
282
func (c TogglClient) markAsSent(entries []TogglPlanfixEntry) error {
283
	for _, entry := range entries {
284
		entryString := fmt.Sprintf(
285
			"[%s] %s (%d)",
286
			entry.Project,
287
			entry.Description,
288
			int(math.Floor(float64(entry.Duration)/60000+.5)),
289
		)
290
		c.Logger.Printf("[DEBUG] marking %s in toggl", entryString)
291
		if _, err := c.Session.AddRemoveTag(entry.ID, c.Config.TogglSentTag, true); err != nil {
292
			c.Logger.Println(err)
293
			return err
294
		}
295
	}
296
	return nil
297
}
298
299
// sendWithSMTP отправляет toggl-запись через SMTP
300
func (c TogglClient) sendWithSMTP(planfixTaskID int, date string, mins int) error {
301
	auth := smtp.PlainAuth("", c.Config.SMTPLogin, c.Config.SMTPPassword, c.Config.SMTPHost)
302
	taskEmail := fmt.Sprintf("task+%d@%s.planfix.ru", planfixTaskID, c.Config.PlanfixAccount)
303
	testEmail := c.Config.SMTPEmailFrom
304
	//test2Email := "[email protected]"
305
	to := []string{taskEmail, testEmail}
306
	body := fmt.Sprintf(
307
		"Content-Type: text/plain; charset=\"utf-8\"\r\n"+
308
			"From: %s\r\n"+
309
			"To: %s\r\n"+
310
			"Subject: @toggl @nonotify\r\n"+
311
			"\r\n"+
312
			"Вид работы: %s\r\n"+
313
			"time: %d\r\n"+
314
			"Автор: %s\r\n"+
315
			"Дата: %s\r\n",
316
		c.Config.SMTPEmailFrom,
317
		taskEmail,
318
		c.Config.PlanfixAnaliticTypeValue,
319
		mins,
320
		c.Config.PlanfixAuthorName,
321
		date,
322
	)
323
	msg := []byte(body)
324
	return smtp.SendMail(fmt.Sprintf("%s:%d", c.Config.SMTPHost, c.Config.SMTPPort), auth, c.Config.SMTPEmailFrom, to, msg)
325
}
326
327
// sendWithPlanfixAPI отправляет toggl-запись через Планфикс API
328
func (c TogglClient) sendWithPlanfixAPI(planfixTaskID int, date string, mins int, comment string) error {
329
	analiticData, err := c.getAnaliticData(
330
		c.Config.PlanfixAnaliticName,
331
		c.Config.PlanfixAnaliticTypeName,
332
		c.Config.PlanfixAnaliticCountName,
333
		c.Config.PlanfixAnaliticCommentName,
334
		c.Config.PlanfixAnaliticDateName,
335
		c.Config.PlanfixAnaliticUsersName,
336
	)
337
	if err != nil {
338
		return err
339
	}
340
	userIDs := struct {
341
		ID []int `xml:"id"`
342
	}{[]int{c.Config.PlanfixUserID}}
343
344
	_, err = c.PlanfixAPI.ActionAdd(planfix.XMLRequestActionAdd{
345
		TaskGeneral: planfixTaskID,
346
		Description: "",
347
		Analitics: []planfix.XMLRequestActionAnalitic{
348
			{
349
				ID: analiticData.ID,
350
				// аналитика должна содержать поля: вид работы, кол-во, дата, коммент, юзеры
351
				ItemData: []planfix.XMLRequestAnaliticField{
352
					{FieldID: analiticData.TypeID, Value: analiticData.TypeValueID}, // name
353
					{FieldID: analiticData.CountID, Value: mins},                    // count, минут
354
					{FieldID: analiticData.CommentID, Value: comment},               // comment
355
					{FieldID: analiticData.DateID, Value: date},                     // date
356
					{FieldID: analiticData.UsersID, Value: userIDs},                 // user
357
				},
358
			},
359
		},
360
	})
361
	return err
362
}
363
364
// getAnaliticData получает ID аналитики и ее полей из названий аналитики и полей
365
func (c TogglClient) getAnaliticData(name, typeName, countName, commentName, dateName, usersName string) (PlanfixAnaliticData, error) {
366
	if analiticDataCached.ID != 0 { // only on first call
367
		return analiticDataCached, nil
368
	}
369
370
	// получение аналитики
371
	analitic, err := c.PlanfixAPI.GetAnaliticByName(name)
372
	if err != nil {
373
		return PlanfixAnaliticData{}, err
374
	}
375
376
	// получение полей аналитики
377
	analiticOptions, err := c.PlanfixAPI.AnaliticGetOptions(analitic.ID)
378
	if err != nil {
379
		return PlanfixAnaliticData{}, err
380
	}
381
382
	analiticData := PlanfixAnaliticData{
383
		ID: analitic.ID,
384
	}
385
386
	// получение ID полей по их названиям
387
	for _, field := range analiticOptions.Analitic.Fields {
388
		switch field.Name {
389
		case typeName:
390
			analiticData.TypeID = field.ID
391
			// получение ID записи справочника
392
			record, err := c.PlanfixAPI.GetHandbookRecordByName(field.HandbookID, c.Config.PlanfixAnaliticTypeValue)
393
			if err != nil {
394
				return analiticData, err
395
			}
396
			analiticData.TypeValueID = record.Key
397
		case countName:
398
			analiticData.CountID = field.ID
399
		case commentName:
400
			analiticData.CommentID = field.ID
401
		case dateName:
402
			analiticData.DateID = field.ID
403
		case usersName:
404
			analiticData.UsersID = field.ID
405
		}
406
	}
407
408
	analiticDataCached = analiticData
409
	return analiticData, nil
410
}
411