Passed
Push — master ( 4347c5...495634 )
by Stanislav
01:28
created

app/client/client.go   F

Size/Duplication

Total Lines 387
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
cc 69
dl 0
loc 387
rs 2.88
c 0
b 0
f 0

13 Methods

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