From f4b454deae3c048c64e7fb2dae55e2305a1dd9ba Mon Sep 17 00:00:00 2001 From: walnuts1018 Date: Fri, 22 Sep 2023 13:45:35 +0900 Subject: [PATCH] add --- config/config.go | 2 + docker-compose.yaml | 2 +- domain/slack.go | 1 + domain/wakatime.go | 3 +- emoji.json | 3 ++ handler/handler.go | 3 -- infra/slack/slack.go | 4 ++ infra/wakatime/wakatime.go | 80 ++++++++++++++++++++++++++++++++++++-- main.go | 30 +++++++++++++- usecase/emoji.go | 45 +++++++++++++++++++++ usecase/languages.go | 19 +++++++-- usecase/usecase.go | 22 ++++++++++- 12 files changed, 198 insertions(+), 16 deletions(-) create mode 100644 emoji.json create mode 100644 usecase/emoji.go diff --git a/config/config.go b/config/config.go index df0f506..f3580e0 100644 --- a/config/config.go +++ b/config/config.go @@ -24,6 +24,7 @@ type Config_t struct { SlackAccessToken string `env:"SLACK_ACCESS_TOKEN"` ServerPort string + ServerURL string } var Config = Config_t{} @@ -32,6 +33,7 @@ func LoadConfig() error { serverport := flag.String("port", "8080", "server port") flag.Parse() Config.ServerPort = *serverport + Config.ServerURL = fmt.Sprintf("http://localhost:%v/", Config.ServerPort) err := godotenv.Load(".env") if err != nil { diff --git a/docker-compose.yaml b/docker-compose.yaml index 7202852..09094c2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,7 +2,7 @@ version: '3' services: postgres: image: postgres:16 - container_name: psql + container_name: wakatime-to-slack-psql ports: - "5432:5432" volumes: diff --git a/domain/slack.go b/domain/slack.go index 3390ed7..65fc71a 100644 --- a/domain/slack.go +++ b/domain/slack.go @@ -1,4 +1,5 @@ package domain type SlackClient interface { + SetUserCustomStatus(emoji string) error } diff --git a/domain/wakatime.go b/domain/wakatime.go index 12e84c0..df6fb2c 100644 --- a/domain/wakatime.go +++ b/domain/wakatime.go @@ -17,7 +17,8 @@ type WakatimeClient interface { Auth(state string) string Callback(ctx context.Context, code string) (OAuth2Token, error) SetToken(ctx context.Context, tokenStore TokenStore) error - Languages(ctx context.Context) ([]Language, error) + ListLanguages(ctx context.Context) ([]Language, error) + NowLanguage(ctx context.Context) (string, error) } type Language struct { diff --git a/emoji.json b/emoji.json new file mode 100644 index 0000000..b3e3c95 --- /dev/null +++ b/emoji.json @@ -0,0 +1,3 @@ +{ + "Go":"gopher" +} diff --git a/handler/handler.go b/handler/handler.go index deeeb03..fcee230 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -26,9 +26,6 @@ func NewHandler(usecase *usecase.Usecase) (*gin.Engine, error) { r.GET("/signin", signIn) r.GET("/callback", callback) - r.GET("/languages", func(ctx *gin.Context) { - uc.Languages(ctx) - }) return r, nil } diff --git a/infra/slack/slack.go b/infra/slack/slack.go index 4913e66..f8d090d 100644 --- a/infra/slack/slack.go +++ b/infra/slack/slack.go @@ -2,6 +2,7 @@ package slack import ( "fmt" + "strings" "github.com/slack-go/slack" "github.com/walnuts1018/wakatime-to-slack-profile/config" @@ -18,6 +19,9 @@ func NewClient() *client { } func (c *client) SetUserCustomStatus(emoji string) error { + if !(strings.HasPrefix(emoji, ":") && strings.HasSuffix(emoji, ":")) { + emoji = ":" + emoji + ":" + } err := c.slackClient.SetUserCustomStatus("", emoji, 0) if err != nil { return fmt.Errorf("error setting status: %w", err) diff --git a/infra/wakatime/wakatime.go b/infra/wakatime/wakatime.go index 78bd03e..b1c42d1 100644 --- a/infra/wakatime/wakatime.go +++ b/infra/wakatime/wakatime.go @@ -5,7 +5,11 @@ import ( "encoding/json" "fmt" "io" + "log/slog" + "math" "net/http" + "net/url" + "time" "github.com/walnuts1018/wakatime-to-slack-profile/config" "github.com/walnuts1018/wakatime-to-slack-profile/domain" @@ -21,6 +25,7 @@ const ( var ( scopes = []string{ "read_stats", + "read_logged_time", } ) @@ -35,13 +40,14 @@ func NewOauth2Client() domain.WakatimeClient { ClientID: config.Config.WakatimeAppID, ClientSecret: config.Config.WakatimeAppSecret, Endpoint: oauth2.Endpoint{AuthURL: AuthEndpoint, TokenURL: TokenEndpoint}, + RedirectURL: config.Config.ServerURL + "callback", Scopes: scopes, }, } } func (c *client) Auth(state string) string { - url := c.cfg.AuthCodeURL(state, oauth2.AccessTypeOffline) + url := c.cfg.AuthCodeURL(state) return url } @@ -84,7 +90,13 @@ func (c *client) SetToken(ctx context.Context, tokenStore domain.TokenStore) err return nil } -func (c *client) Languages(ctx context.Context) ([]domain.Language, error) { +type listLanguageResponce struct { + Data []domain.Language `json:"data"` + Total int `json:"total"` + TotalPages int `json:"total_pages"` +} + +func (c *client) ListLanguages(ctx context.Context) ([]domain.Language, error) { if c.wclient == nil { return nil, fmt.Errorf("client is not set") } @@ -103,11 +115,71 @@ func (c *client) Languages(ctx context.Context) ([]domain.Language, error) { return nil, fmt.Errorf("failed to read response body: %w", err) } - var languages []domain.Language + var languages listLanguageResponce err = json.Unmarshal(raw, &languages) if err != nil { return nil, fmt.Errorf("failed to unmarshal response body: %w", err) } - return languages, nil + return languages.Data, nil +} + +type nowLanguageResponce struct { + Data []struct { + Duration float64 `json:"duration"` + Language string `json:"language"` + Project string `json:"project"` + Time float64 `json:"time"` + } `json:"data"` + Start time.Time `json:"start"` + End time.Time `json:"end"` + Timezone string `json:"timezone"` +} + +func (c *client) NowLanguage(ctx context.Context) (string, error) { + if c.wclient == nil { + return "", fmt.Errorf("client is not set") + } + + endpoint, err := url.Parse("https://wakatime.com/api/v1/users/current/durations") + if err != nil { + return "", fmt.Errorf("failed to parse url: %w", err) + } + query := endpoint.Query() + query.Set("date", timeJST.Now().Format("2006-01-02")) + query.Set("slice_by", "language") + endpoint.RawQuery = query.Encode() + + resp, err := c.wclient.Get(endpoint.String()) + if err != nil { + return "", fmt.Errorf("failed to get languages: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to get languages: %v", resp.Status) + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response body: %w", err) + } + + var languages nowLanguageResponce + err = json.Unmarshal(raw, &languages) + if err != nil { + return "", fmt.Errorf("failed to unmarshal response body: %w", err) + } + + if len(languages.Data) == 0 { + slog.Warn("no language") + return "", nil + } + + l := languages.Data[len(languages.Data)-1] + t := time.Unix(int64(math.Floor(l.Time)), 0) + if t.Before(timeJST.Now().Add(-10 * time.Minute)) { + slog.Warn("last language is too old", "lastLanguage", l.Language, "lastTime", t) + return "", nil + } + return l.Language, nil } diff --git a/main.go b/main.go index c71f59f..23de9cc 100644 --- a/main.go +++ b/main.go @@ -5,10 +5,12 @@ import ( "fmt" "log/slog" "os" + "time" "github.com/walnuts1018/wakatime-to-slack-profile/config" "github.com/walnuts1018/wakatime-to-slack-profile/handler" "github.com/walnuts1018/wakatime-to-slack-profile/infra/psql" + "github.com/walnuts1018/wakatime-to-slack-profile/infra/slack" "github.com/walnuts1018/wakatime-to-slack-profile/infra/wakatime" "github.com/walnuts1018/wakatime-to-slack-profile/usecase" ) @@ -32,12 +34,38 @@ func main() { wakatimeClient := wakatime.NewOauth2Client() - usecase := usecase.NewUsecase(wakatimeClient, psqClient) + slackClient := slack.NewClient() + + usecase := usecase.NewUsecase(wakatimeClient, psqClient, slackClient) err = usecase.SetToken(ctx) if err != nil { slog.Warn("failed to set token", "error", err) } + go func() { + err := usecase.SetLanguage(ctx) + if err != nil { + slog.Error("Failed to set language", "error", err) + return + } + + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + err := usecase.SetLanguage(ctx) + if err != nil { + slog.Error("Failed to set language", "error", err) + return + } + } + } + }() + handler, err := handler.NewHandler(usecase) if err != nil { slog.Error("Error loading handler: %v", "error", err) diff --git a/usecase/emoji.go b/usecase/emoji.go new file mode 100644 index 0000000..b82412e --- /dev/null +++ b/usecase/emoji.go @@ -0,0 +1,45 @@ +package usecase + +import ( + "fmt" + "log/slog" + "strings" +) + +func (u *Usecase) SetUserCustomStatus(language string) error { + if language == "" { + err := u.slackClient.SetUserCustomStatus("sloth") + if err == nil { + slog.Info("set user custom status", "emoji", "🦥") + return nil + } + } + + override, ok := u.emojiOverides[language] + if ok { + err := u.slackClient.SetUserCustomStatus(override) + if err == nil { + slog.Info("set user custom status", "emoji", override) + return nil + } + } + + err := u.slackClient.SetUserCustomStatus(language) + if err == nil { + slog.Info("set user custom status", "emoji", language) + return nil + } + err = u.slackClient.SetUserCustomStatus(strings.ToLower(language)) + if err == nil { + slog.Info("set user custom status", "emoji", language) + return nil + } + + err = u.slackClient.SetUserCustomStatus("question") + if err == nil { + slog.Info("set user custom status", "emoji", "❓") + return nil + } + + return fmt.Errorf("failed to find emoji: %v", language) +} diff --git a/usecase/languages.go b/usecase/languages.go index 22f1273..3999e98 100644 --- a/usecase/languages.go +++ b/usecase/languages.go @@ -5,11 +5,22 @@ import ( "fmt" ) -func (u *Usecase) Languages(ctx context.Context) error { - langs, err := u.wakatimeClient.Languages(ctx) +func (u *Usecase) SetLanguage(ctx context.Context) error { + language, err := u.wakatimeClient.NowLanguage(ctx) if err != nil { - return err + return fmt.Errorf("failed to get now language: %w", err) } - fmt.Printf("%#v\n", langs) + + if u.lastLanguage != nil { + if *u.lastLanguage == language { + return nil + } + } + + err = u.SetUserCustomStatus(language) + if err != nil { + return fmt.Errorf("failed to set user custom status: %w", err) + } + u.lastLanguage = &language return nil } diff --git a/usecase/usecase.go b/usecase/usecase.go index 9ba5e88..8c9c147 100644 --- a/usecase/usecase.go +++ b/usecase/usecase.go @@ -1,15 +1,33 @@ package usecase -import "github.com/walnuts1018/wakatime-to-slack-profile/domain" +import ( + "encoding/json" + "log/slog" + "os" + + "github.com/walnuts1018/wakatime-to-slack-profile/domain" +) type Usecase struct { wakatimeClient domain.WakatimeClient tokenStore domain.TokenStore + slackClient domain.SlackClient + emojiOverides map[string]string + lastLanguage *string } -func NewUsecase(wakatimeClient domain.WakatimeClient, tokenStore domain.TokenStore) *Usecase { +func NewUsecase(wakatimeClient domain.WakatimeClient, tokenStore domain.TokenStore, slackClient domain.SlackClient) *Usecase { + var emojis map[string]string + b, err := os.ReadFile("emoji.json") + if err != nil { + slog.Warn("emoji.json is not found") + } else { + json.Unmarshal(b, &emojis) + } return &Usecase{ wakatimeClient: wakatimeClient, tokenStore: tokenStore, + slackClient: slackClient, + emojiOverides: emojis, } }