commit 5fc45b446c3a74e2944e95ed3252bc7ed3bc93e6 Author: Nulo Date: Wed Mar 1 15:39:46 2023 -0300 ya diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..8bd147e --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,8 @@ +version: "3" + +tasks: + ko: + cmds: + - ko build . + env: + KO_DOCKER_REPO: gitea.nulo.in/nulo/zulip-checkin-cyborg diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dd7a3aa --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module gitea.nulo.in/Nulo/zulip-checkin-cyborg + +go 1.20 + +require golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0661dd6 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI= +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= diff --git a/main.go b/main.go new file mode 100644 index 0000000..d8872be --- /dev/null +++ b/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "log" + "os" + "time" + + "gitea.nulo.in/Nulo/zulip-checkin-cyborg/zulip" + "golang.org/x/exp/slices" +) + +var bot zulip.Bot +var stream string +var message string + +func main() { + bot.Url = os.Getenv("ZULIP_URL") + if len(bot.Url) == 0 { + log.Fatalln("Falta ZULIP_URL") + } + bot.Email = os.Getenv("ZULIP_BOT_EMAIL") + if len(bot.Email) == 0 { + log.Fatalln("Falta ZULIP_BOT_EMAIL") + } + bot.Key = os.Getenv("ZULIP_BOT_KEY") + if len(bot.Key) == 0 { + log.Fatalln("Falta ZULIP_BOT_KEY") + } + stream = os.Getenv("ZULIP_STREAM") + if len(stream) == 0 { + stream = "check-in bot test" + } + message = os.Getenv("ZULIP_MESSAGE") + if len(message) == 0 { + message = "prueba de mensaje" + } + + cycle() +} + +func cycle() { + now := time.Now() + lastWeek := now.Add(-time.Hour * 24 * time.Duration(now.Weekday()-time.Monday)) + err := createIfNotExists(lastWeek.Format("semana 2006-01-02")) + if err != nil { + log.Fatalln(err) + } + + time.Sleep(time.Hour * 5) + cycle() +} + +func createIfNotExists(topicName string) (err error) { + streamId, err := bot.GetStreamId(stream) + if err != nil { + log.Fatalln(err) + } + topics, err := bot.GetStreamTopics(streamId) + if err != nil { + log.Fatalln(err) + } + names := mapSlice(topics, func(t zulip.GetStreamTopicsResStream) string { + return t.Name + }) + + if slices.Contains(names, topicName) { + return + } + + err = bot.SendMessage(stream, topicName, message) + if err != nil { + return + } + return +} + +// https://gosamples.dev/generics-map-function/ +func mapSlice[T any, M any](a []T, f func(T) M) []M { + n := make([]M, len(a)) + for i, e := range a { + n[i] = f(e) + } + return n +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..fb48fe0 --- /dev/null +++ b/readme.md @@ -0,0 +1,3 @@ +# Zulip Check-in Cyborg + +Manda un mensaje por semana a un hilo para hacer check-ins. Para uso en [Sutty](https://sutty.coop.ar). diff --git a/zulip/zulip.go b/zulip/zulip.go new file mode 100644 index 0000000..02b5da6 --- /dev/null +++ b/zulip/zulip.go @@ -0,0 +1,131 @@ +package zulip + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "strings" +) + +type Bot struct { + Url string + Email string + Key string + HTTPClient http.Client +} + +func (b *Bot) url() *url.URL { + u, err := url.Parse(b.Url) + if err != nil { + log.Fatalln(u) + } + return u +} +func (b *Bot) authHeaders(h http.Header) { + h.Add("authorization", "basic "+base64.StdEncoding.EncodeToString([]byte(b.Email+":"+b.Key))) +} + +func (b *Bot) SendMessage(stream, topic, message string) error { + u := b.url() + u.Path = "/api/v1/messages" + + values := url.Values{ + "type": []string{"stream"}, + "to": []string{stream}, + "topic": []string{topic}, + "content": []string{message}, + } + req, err := http.NewRequest("POST", u.String(), strings.NewReader(values.Encode())) + if err != nil { + log.Fatalln(err) + } + req.Header.Add("content-type", "application/x-www-form-urlencoded") + b.authHeaders(req.Header) + + res, err := b.HTTPClient.Do(req) + if err != nil { + log.Println("Error enviando mensaje", err) + return err + } + if res.StatusCode != http.StatusOK { + log.Printf("Zulip tiró un %d", res.StatusCode) + } + byt, err := io.ReadAll(res.Body) + if err != nil { + log.Println("Error enviando mensaje", err) + return err + } + log.Printf("Response: %s", string(byt)) + return nil +} + +type getStreamIdRes struct { + SteamId int `json:"stream_id"` +} + +func (b *Bot) GetStreamId(name string) (int, error) { + u := b.url() + u.Path = "/api/v1/get_stream_id" + + values := url.Values{"stream": []string{name}} + u.RawQuery = values.Encode() + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + log.Fatalln(err) + } + b.authHeaders(req.Header) + + res, err := b.HTTPClient.Do(req) + if err != nil { + return 0, err + } + if res.StatusCode != http.StatusOK { + return 0, errors.New(fmt.Sprintf("Zulip tiró un %d", res.StatusCode)) + } + + r := getStreamIdRes{} + err = json.NewDecoder(res.Body).Decode(&r) + if err != nil { + return 0, err + } + return r.SteamId, nil +} + +type getStreamTopicsRes struct { + Topics []GetStreamTopicsResStream `json:"topics"` +} +type GetStreamTopicsResStream struct { + Name string `json:"name"` +} + +func (b *Bot) GetStreamTopics(streamId int) ([]GetStreamTopicsResStream, error) { + var r getStreamTopicsRes + + u := b.url() + u.Path = fmt.Sprintf("/api/v1/users/me/%d/topics", streamId) + + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + log.Fatalln(err) + } + b.authHeaders(req.Header) + + res, err := b.HTTPClient.Do(req) + if err != nil { + return r.Topics, err + } + if res.StatusCode != http.StatusOK { + return r.Topics, errors.New(fmt.Sprintf("Zulip tiró un %d", res.StatusCode)) + } + + err = json.NewDecoder(res.Body).Decode(&r) + if err != nil { + return r.Topics, err + } + return r.Topics, nil +}