├── .gitignore
├── .golangci.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── cmd
└── main.go
├── docker-compose.dev.yml
├── docker-compose.local.yml
├── go.mod
├── go.sum
└── internal
├── bot
├── bot.go
├── middleware
│ └── admins_only.go
├── view_cmd_addsource.go
├── view_cmd_deletesource.go
├── view_cmd_getsource.go
├── view_cmd_listsources.go
└── view_cmd_setpriority.go
├── botkit
├── args.go
├── bot.go
└── markup
│ └── markdown.go
├── config
└── config.go
├── fetcher
├── fetcher.go
├── fetcher_test.go
├── mocks
│ ├── mock_article_storage.go
│ ├── mock_source.go
│ └── mock_sources_provider.go
└── testdata
│ ├── feed1.xml
│ └── feed2.xml
├── model
└── model.go
├── notifier
└── notifier.go
├── source
├── rss.go
├── rss_test.go
└── testdata
│ └── feed.xml
├── storage
├── article.go
├── migrations
│ ├── 20230318200359_create_sources.sql
│ ├── 20230318200520_create_articles.sql
│ └── 20230320140458_create_articles_summary.sql
└── source.go
└── summary
└── openai.go
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/go
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=go
3 |
4 | ### Go ###
5 | # If you prefer the allow list template instead of the deny list, see community template:
6 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
7 | #
8 | # Binaries for programs and plugins
9 | *.exe
10 | *.exe~
11 | *.dll
12 | *.so
13 | *.dylib
14 |
15 | # Test binary, built with `go test -c`
16 | *.test
17 |
18 | # Output of the go coverage tool, specifically when used with LiteIDE
19 | *.out
20 |
21 | # Dependency directories (remove the comment below to include it)
22 | # vendor/
23 |
24 | # Go workspace file
25 | go.work
26 |
27 | # End of https://www.toptal.com/developers/gitignore/api/go
28 |
29 | /config.local.hcl
30 | /bin/
31 | .env
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | run:
2 | timeout: 3m
3 | gofmt:
4 | simplify: true
5 | revive:
6 | rules:
7 | - name: line-length-limit
8 | arguments: 120
9 | skip-dirs:
10 | - ".*mocks.*"
11 | - vendor
12 | - ".*gen.*"
13 | gofumports:
14 | local-prefixes: "github.com/defer-panic/news-feed-bot"
15 |
16 | output:
17 | sort-results: true
18 |
19 | issues:
20 | exclude-use-default: true
21 | fix: false
22 | exclude-rules:
23 | - linters:
24 | - revive
25 | text: 'should not use basic type string as key in context.WithValue'
26 |
27 | - linters:
28 | - revive
29 | text: "don't use an underscore in package name"
30 |
31 | - linters:
32 | - staticcheck
33 | text: 'SA1029:' # the same as revive's rule about string as a key in context.WithValue
34 |
35 | - linters:
36 | - revive
37 | text: "comment on exported var"
38 |
39 | linters:
40 | enable:
41 | - revive
42 | - gofmt
43 | - gosimple
44 | - misspell
45 | - goimports
46 | - godot
47 | - cyclop
48 | - gocognit
49 | - gocritic
50 | - prealloc
51 | - wsl
52 | - goconst
53 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.20-alpine AS builder
2 |
3 | WORKDIR /app
4 |
5 | COPY go.mod go.sum ./
6 |
7 | RUN go mod download
8 |
9 | COPY internal ./internal
10 | COPY cmd ./cmd
11 |
12 | RUN go build -o /app/news-feed-bot ./cmd/
13 |
14 | EXPOSE 8080
15 |
16 | CMD ["/app/news-feed-bot"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright © 2023 defer panic Team
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PROJECT_DIR = $(shell pwd)
2 | PROJECT_BIN = $(PROJECT_DIR)/bin
3 |
4 | MOQ = $(PROJECT_BIN)/moq
5 | MOQ_VERSION = v0.3.1
6 |
7 | GOLANGCI_LINT = $(PROJECT_BIN)/golangci-lint
8 | GOLANGCI_LINT_VERSION = v1.52.0
9 |
10 |
11 | # === Mocks generator ===
12 |
13 | .PHONY: .install-moq
14 | .install-moq:
15 | @echo "Installing moq..."
16 | @mkdir -p $(PROJECT_BIN)
17 | [ -f $(MOQ) ] || GOBIN=$(PROJECT_BIN) go install github.com/matryer/moq@$(MOQ_VERSION)
18 |
19 |
20 | # === Linter ===
21 | .PHONY: .install-linter
22 | .install-linter:
23 | ### INSTALL GOLANGCI-LINT ###
24 | [ -f $(GOLANGCI_LINT) ] || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(PROJECT_BIN) $(GOLANCI_LINT_VERSION)
25 |
26 | .PHONY: lint
27 | lint: .install-linter
28 | ### RUN GOLANGCI-LINT ###
29 | $(GOLANGCI_LINT) run ./... --config=./.golangci.yml
30 |
31 | .PHONY: lint-fast
32 | lint-fast: .install-linter
33 | $(GOLANGCI_LINT) run ./... --fast --config=./.golangci.yml
34 |
35 |
36 | # === Install environment ===
37 | .PHONY: install-env
38 | install-env: .install-moq .install-linter
39 |
40 |
41 | # === Tests ===
42 | .PHONY: test
43 | test:
44 | go test ./...
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # News Feed Bot
2 |
3 | Bot for Telegram that gets and posts news to a channel.
4 |
5 | # Features
6 |
7 | - Fetching articles from RSS feeds
8 | - Article summaries powered by GPT-3.5
9 | - Admin commands for managing sources
10 |
11 | # Configuration
12 |
13 | ## Environment variables
14 |
15 | - `NFB_TELEGRAM_BOT_TOKEN` — token for Telegram Bot API
16 | - `NFB_TELEGRAM_CHANNEL_ID` — ID of the channel to post to, can be obtained via [@JsonDumpBot](https://t.me/JsonDumpBot)
17 | - `NFB_DATABASE_DSN` — PostgreSQL connection string
18 | - `NFB_FETCH_INTERVAL` — the interval of checking for new articles, default `10m`
19 | - `NFB_NOTIFICATION_INTERVAL` — the interval of delivering new articles to Telegram channel, default `1m`
20 | - `NFB_FILTER_KEYWORDS` — comma separated list of words to skip articles containing these words
21 | - `NFB_OPENAI_KEY` — token for OpenAI API
22 | - `NFB_OPENAI_PROMPT` — prompt for GPT-3.5 Turbo to generate summary
23 |
24 | ## HCL
25 |
26 | News Feed Bot can be configured with HCL config file. The service is looking for config file in following locations:
27 |
28 | - `./config.hcl`
29 | - `./config.local.hcl`
30 | - `$HOME/.config/news-feed-bot/config.hcl`
31 |
32 | The names of parameters are the same except that there is no prefix and names are in lower case instead of upper case.
33 |
34 | # Nice to have features (backlog)
35 |
36 | - [ ] More types of resources — not only RSS
37 | - [x] Summary for the article
38 | - [ ] Dynamic source priority (based on 👍 and 👎 reactions) — currently blocked by Telegram Bot API
39 | - [ ] Article types: text, video, audio
40 | - [ ] De-duplication — filter articles with the same title and author
41 | - [ ] Low quality articles filter — need research
42 | - Ban by author?
43 | - Check article length — not working with audio/video posts, but it will be fixed after article type implementation
44 |
--------------------------------------------------------------------------------
/cmd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "log"
7 | "net/http"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 |
12 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
13 | "github.com/jmoiron/sqlx"
14 | _ "github.com/lib/pq"
15 |
16 | "github.com/defer-panic/news-feed-bot/internal/bot"
17 | "github.com/defer-panic/news-feed-bot/internal/bot/middleware"
18 | "github.com/defer-panic/news-feed-bot/internal/botkit"
19 | "github.com/defer-panic/news-feed-bot/internal/config"
20 | "github.com/defer-panic/news-feed-bot/internal/fetcher"
21 | "github.com/defer-panic/news-feed-bot/internal/notifier"
22 | "github.com/defer-panic/news-feed-bot/internal/storage"
23 | "github.com/defer-panic/news-feed-bot/internal/summary"
24 | )
25 |
26 | func main() {
27 | botAPI, err := tgbotapi.NewBotAPI(config.Get().TelegramBotToken)
28 | if err != nil {
29 | log.Printf("[ERROR] failed to create botAPI: %v", err)
30 | return
31 | }
32 |
33 | db, err := sqlx.Connect("postgres", config.Get().DatabaseDSN)
34 | if err != nil {
35 | log.Printf("[ERROR] failed to connect to db: %v", err)
36 | return
37 | }
38 | defer db.Close()
39 |
40 | var (
41 | articleStorage = storage.NewArticleStorage(db)
42 | sourceStorage = storage.NewSourceStorage(db)
43 | fetcher = fetcher.New(
44 | articleStorage,
45 | sourceStorage,
46 | config.Get().FetchInterval,
47 | config.Get().FilterKeywords,
48 | )
49 | summarizer = summary.NewOpenAISummarizer(
50 | config.Get().OpenAIKey,
51 | config.Get().OpenAIModel,
52 | config.Get().OpenAIPrompt,
53 | )
54 | notifier = notifier.New(
55 | articleStorage,
56 | summarizer,
57 | botAPI,
58 | config.Get().NotificationInterval,
59 | 2*config.Get().FetchInterval,
60 | config.Get().TelegramChannelID,
61 | )
62 | )
63 |
64 | newsBot := botkit.New(botAPI)
65 | newsBot.RegisterCmdView(
66 | "addsource",
67 | middleware.AdminsOnly(
68 | config.Get().TelegramChannelID,
69 | bot.ViewCmdAddSource(sourceStorage),
70 | ),
71 | )
72 | newsBot.RegisterCmdView(
73 | "setpriority",
74 | middleware.AdminsOnly(
75 | config.Get().TelegramChannelID,
76 | bot.ViewCmdSetPriority(sourceStorage),
77 | ),
78 | )
79 | newsBot.RegisterCmdView(
80 | "getsource",
81 | middleware.AdminsOnly(
82 | config.Get().TelegramChannelID,
83 | bot.ViewCmdGetSource(sourceStorage),
84 | ),
85 | )
86 | newsBot.RegisterCmdView(
87 | "listsources",
88 | middleware.AdminsOnly(
89 | config.Get().TelegramChannelID,
90 | bot.ViewCmdListSource(sourceStorage),
91 | ),
92 | )
93 | newsBot.RegisterCmdView(
94 | "deletesource",
95 | middleware.AdminsOnly(
96 | config.Get().TelegramChannelID,
97 | bot.ViewCmdDeleteSource(sourceStorage),
98 | ),
99 | )
100 |
101 | mux := http.NewServeMux()
102 | mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
103 | w.WriteHeader(http.StatusOK)
104 | })
105 |
106 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
107 | defer cancel()
108 |
109 | go func(ctx context.Context) {
110 | if err := fetcher.Start(ctx); err != nil {
111 | if !errors.Is(err, context.Canceled) {
112 | log.Printf("[ERROR] failed to run fetcher: %v", err)
113 | return
114 | }
115 |
116 | log.Printf("[INFO] fetcher stopped")
117 | }
118 | }(ctx)
119 |
120 | go func(ctx context.Context) {
121 | if err := notifier.Start(ctx); err != nil {
122 | if !errors.Is(err, context.Canceled) {
123 | log.Printf("[ERROR] failed to run notifier: %v", err)
124 | return
125 | }
126 |
127 | log.Printf("[INFO] notifier stopped")
128 | }
129 | }(ctx)
130 |
131 | go func(ctx context.Context) {
132 | if err := http.ListenAndServe("9.0.0.0:8080", mux); err != nil {
133 | if !errors.Is(err, context.Canceled) {
134 | log.Printf("[ERROR] failed to run http server: %v", err)
135 | return
136 | }
137 |
138 | log.Printf("[INFO] http server stopped")
139 | }
140 | }(ctx)
141 |
142 | if err := newsBot.Run(ctx); err != nil {
143 | log.Printf("[ERROR] failed to run botkit: %v", err)
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/docker-compose.dev.yml:
--------------------------------------------------------------------------------
1 | version: '3.1'
2 |
3 | services:
4 |
5 | db:
6 | image: postgres:15
7 | restart: always
8 | environment:
9 | POSTGRES_USER: postgres
10 | POSTGRES_PASSWORD: postgres
11 | POSTGRES_DB: news_feed_bot
12 | PGDATA: /var/lib/postgresql/data/
13 | ports:
14 | - "5432:5432"
15 | volumes:
16 | - db:/var/lib/postgresql/data/
17 |
18 | volumes:
19 | db:
--------------------------------------------------------------------------------
/docker-compose.local.yml:
--------------------------------------------------------------------------------
1 | version: '3.1'
2 |
3 | services:
4 |
5 | db:
6 | image: postgres:15
7 | restart: always
8 | environment:
9 | POSTGRES_USER: postgres
10 | POSTGRES_PASSWORD: postgres
11 | POSTGRES_DB: news_feed_bot
12 | PGDATA: /var/lib/postgresql/data/
13 | ports:
14 | - "5432:5432"
15 | volumes:
16 | - db:/var/lib/postgresql/data/
17 |
18 | bot:
19 | build:
20 | context: .
21 | restart: on-failure
22 | environment:
23 | NFB_DATABASE_DSN: ${NFB_DATABASE_DSN:-postgres://postgres:postgres@db:5432/news_feed_bot?sslmode=disable}
24 | NFB_TELEGRAM_BOT_TOKEN: ${NFB_TELEGRAM_BOT_TOKEN}
25 | NFB_TELEGRAM_CHANNEL_ID: ${NFB_TELEGRAM_CHANNEL_ID}
26 | NFB_FETCH_INTERVAL: ${NFB_FETCH_INTERVAL}
27 | NFB_NOTIFICATION_INTERVAL: ${NFB_NOTIFICATION_INTERVAL}
28 | NFB_FILTER_KEYWORDS: ${NFB_FILTER_KEYWORDS}
29 | NFB_OPENAI_KEY: ${NFB_OPENAI_KEY}
30 | ports:
31 | - "8080:8080"
32 | depends_on:
33 | - db
34 |
35 | volumes:
36 | db:
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/defer-panic/news-feed-bot
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/SlyMarbo/rss v1.0.5
7 | github.com/cristalhq/aconfig v0.18.3
8 | github.com/cristalhq/aconfig/aconfighcl v0.17.1
9 | github.com/go-shiori/go-readability v0.0.0-20220215145315-dd6828d2f09b
10 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
11 | github.com/jmoiron/sqlx v1.3.5
12 | github.com/lib/pq v1.10.7
13 | github.com/samber/lo v1.37.0
14 | github.com/sashabaranov/go-openai v1.5.4
15 | github.com/stretchr/testify v1.8.1
16 | github.com/tomakado/containers v0.0.0-20230201144544-f093171e88cf
17 | )
18 |
19 | require (
20 | github.com/andybalholm/cascadia v1.3.1 // indirect
21 | github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 // indirect
22 | github.com/davecgh/go-spew v1.1.1 // indirect
23 | github.com/go-shiori/dom v0.0.0-20210627111528-4e4722cd0d65 // indirect
24 | github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
25 | github.com/hashicorp/hcl v1.0.0 // indirect
26 | github.com/pmezard/go-difflib v1.0.0 // indirect
27 | github.com/sirupsen/logrus v1.9.0 // indirect
28 | golang.org/x/exp v0.0.0-20230131160201-f062dba9d201 // indirect
29 | golang.org/x/net v0.8.0 // indirect
30 | golang.org/x/sys v0.6.0 // indirect
31 | golang.org/x/text v0.8.0 // indirect
32 | gopkg.in/yaml.v3 v3.0.1 // indirect
33 | )
34 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
3 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
4 | github.com/SlyMarbo/rss v1.0.5 h1:DPcZ4aOXXHJ5yNLXY1q/57frIixMmAvTtLxDE3fsMEI=
5 | github.com/SlyMarbo/rss v1.0.5/go.mod h1:w6Bhn1BZs91q4OlEnJVZEUNRJmlbFmV7BkAlgCN8ofM=
6 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
7 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
8 | github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
9 | github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
10 | github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
11 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
12 | github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ=
13 | github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg=
14 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
15 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
16 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
17 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
18 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
19 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
20 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
21 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
22 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
23 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
24 | github.com/cristalhq/aconfig v0.17.0/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E=
25 | github.com/cristalhq/aconfig v0.18.3 h1:Or12LIWIF+2mQpcGWA2PQnNc55+WiHFAqRjYh/pQNtM=
26 | github.com/cristalhq/aconfig v0.18.3/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E=
27 | github.com/cristalhq/aconfig/aconfighcl v0.17.1 h1:/hJvmmoP3akvjY8qARiXgN30m/WXeBvpQdQ8v4DE8W8=
28 | github.com/cristalhq/aconfig/aconfighcl v0.17.1/go.mod h1:oOPqMmdUjVW6pKKO1zkOwQKHSUD0rlu8F5do+pylOQc=
29 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
30 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
31 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
32 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
33 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
34 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
35 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
36 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
37 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
38 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
39 | github.com/go-shiori/dom v0.0.0-20210627111528-4e4722cd0d65 h1:zx4B0AiwqKDQq+AgqxWeHwbbLJQeidq20hgfP+aMNWI=
40 | github.com/go-shiori/dom v0.0.0-20210627111528-4e4722cd0d65/go.mod h1:NPO1+buE6TYOWhUI98/hXLHHJhunIpXRuvDN4xjkCoE=
41 | github.com/go-shiori/go-readability v0.0.0-20220215145315-dd6828d2f09b h1:yrGomo5CP7IvXwSwKbDeaJkhwa4BxfgOO/s1V7iOQm4=
42 | github.com/go-shiori/go-readability v0.0.0-20220215145315-dd6828d2f09b/go.mod h1:LTRGsNyO3/Y6u3ERbz17OiXy2qO1Y+/8QjXpg2ViyEY=
43 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
44 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
45 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
46 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
47 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
48 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
49 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
50 | github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
51 | github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
52 | github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
53 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
54 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
55 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
56 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
57 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
58 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
59 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
60 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
61 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
62 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
63 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
64 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
65 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
66 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
67 | github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
68 | github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
69 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
70 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
71 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
72 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
73 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
74 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
75 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
76 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
77 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
78 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
79 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
80 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
81 | github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
82 | github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
83 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
84 | github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
85 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
86 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
87 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
88 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
89 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
90 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
91 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
92 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
93 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
94 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
95 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
96 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
97 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
98 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
99 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
100 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
101 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
102 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
103 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
104 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
105 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
106 | github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw=
107 | github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA=
108 | github.com/sashabaranov/go-openai v1.5.4 h1:I2K7JMIx/EC/mwT2fbypBzJ3OtwKNxaFg4jf3KOvXuc=
109 | github.com/sashabaranov/go-openai v1.5.4/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
110 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
111 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
112 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
113 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
114 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
115 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
116 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
117 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
118 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
119 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
120 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
121 | github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
122 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
123 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
124 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
125 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
126 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
127 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
128 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
129 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
130 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
131 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
132 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
133 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
134 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
135 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
136 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
137 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
138 | github.com/tomakado/containers v0.0.0-20230201144544-f093171e88cf h1:LNyOcxD0dWNuX4rnnvlq8MkunX3IBcBrcZuzr+LDdF8=
139 | github.com/tomakado/containers v0.0.0-20230201144544-f093171e88cf/go.mod h1:3DYOr7A5HvtNc9NpPh/JZvr1zsOuFcCHtM39egxC6m0=
140 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
141 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
142 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
143 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
144 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
145 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
146 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
147 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
148 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
149 | golang.org/x/exp v0.0.0-20230131160201-f062dba9d201 h1:BEABXpNXLEz0WxtA+6CQIz2xkg80e+1zrhWyMcq8VzE=
150 | golang.org/x/exp v0.0.0-20230131160201-f062dba9d201/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
151 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
152 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
153 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
154 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
155 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
156 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
157 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
158 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
159 | golang.org/x/net v0.0.0-20210505214959-0714010a04ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
160 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
161 | golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
162 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
163 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
164 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
165 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
166 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
167 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
168 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
169 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
170 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
171 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
172 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
173 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
174 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
175 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
176 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
177 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
178 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
179 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
180 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
181 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
182 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
183 | golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
184 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
185 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
186 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
187 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
188 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
189 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
190 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
191 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
192 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
193 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
194 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
195 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
196 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
197 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
198 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
199 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
200 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
201 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
202 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
203 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
204 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
205 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
206 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
207 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
208 |
--------------------------------------------------------------------------------
/internal/bot/bot.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | const parseModeMarkdownV2 = "MarkdownV2"
4 |
--------------------------------------------------------------------------------
/internal/bot/middleware/admins_only.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "context"
5 |
6 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
7 |
8 | "github.com/defer-panic/news-feed-bot/internal/botkit"
9 | )
10 |
11 | func AdminsOnly(channelID int64, next botkit.ViewFunc) botkit.ViewFunc {
12 | return func(ctx context.Context, bot *tgbotapi.BotAPI, update tgbotapi.Update) error {
13 | admins, err := bot.GetChatAdministrators(
14 | tgbotapi.ChatAdministratorsConfig{
15 | ChatConfig: tgbotapi.ChatConfig{
16 | ChatID: channelID,
17 | },
18 | },
19 | )
20 |
21 | if err != nil {
22 | return err
23 | }
24 |
25 | for _, admin := range admins {
26 | if admin.User.ID == update.SentFrom().ID {
27 | return next(ctx, bot, update)
28 | }
29 | }
30 |
31 | if _, err := bot.Send(tgbotapi.NewMessage(
32 | update.FromChat().ID,
33 | "У вас нет прав на выполнение этой команды.",
34 | )); err != nil {
35 | return err
36 | }
37 |
38 | return nil
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/internal/bot/view_cmd_addsource.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
8 |
9 | "github.com/defer-panic/news-feed-bot/internal/botkit"
10 | "github.com/defer-panic/news-feed-bot/internal/model"
11 | )
12 |
13 | type SourceStorage interface {
14 | Add(ctx context.Context, source model.Source) (int64, error)
15 | }
16 |
17 | func ViewCmdAddSource(storage SourceStorage) botkit.ViewFunc {
18 | type addSourceArgs struct {
19 | Name string `json:"name"`
20 | URL string `json:"url"`
21 | Priority int `json:"priority"`
22 | }
23 |
24 | return func(ctx context.Context, bot *tgbotapi.BotAPI, update tgbotapi.Update) error {
25 | args, err := botkit.ParseJSON[addSourceArgs](update.Message.CommandArguments())
26 | if err != nil {
27 | return err
28 | }
29 |
30 | source := model.Source{
31 | Name: args.Name,
32 | FeedURL: args.URL,
33 | Priority: args.Priority,
34 | }
35 |
36 | sourceID, err := storage.Add(ctx, source)
37 | if err != nil {
38 | // TODO: send error message
39 | return err
40 | }
41 |
42 | var (
43 | msgText = fmt.Sprintf(
44 | "Источник добавлен с ID: `%d`\\. Используйте этот ID для обновления источника или удаления\\.",
45 | sourceID,
46 | )
47 | reply = tgbotapi.NewMessage(update.Message.Chat.ID, msgText)
48 | )
49 |
50 | reply.ParseMode = parseModeMarkdownV2
51 |
52 | if _, err := bot.Send(reply); err != nil {
53 | return err
54 | }
55 |
56 | return nil
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/internal/bot/view_cmd_deletesource.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "context"
5 | "strconv"
6 |
7 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
8 |
9 | "github.com/defer-panic/news-feed-bot/internal/botkit"
10 | )
11 |
12 | type SourceDeleter interface {
13 | Delete(ctx context.Context, sourceID int64) error
14 | }
15 |
16 | func ViewCmdDeleteSource(deleter SourceDeleter) botkit.ViewFunc {
17 | return func(ctx context.Context, bot *tgbotapi.BotAPI, update tgbotapi.Update) error {
18 | idStr := update.Message.CommandArguments()
19 |
20 | id, err := strconv.ParseInt(idStr, 10, 64)
21 | if err != nil {
22 | return err
23 | }
24 |
25 | if err := deleter.Delete(ctx, id); err != nil {
26 | return nil
27 | }
28 |
29 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Источник успешно удален")
30 | if _, err := bot.Send(msg); err != nil {
31 | return err
32 | }
33 |
34 | return nil
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/internal/bot/view_cmd_getsource.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strconv"
7 |
8 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
9 |
10 | "github.com/defer-panic/news-feed-bot/internal/botkit"
11 | "github.com/defer-panic/news-feed-bot/internal/botkit/markup"
12 | "github.com/defer-panic/news-feed-bot/internal/model"
13 | )
14 |
15 | type SourceProvider interface {
16 | SourceByID(ctx context.Context, id int64) (*model.Source, error)
17 | }
18 |
19 | func ViewCmdGetSource(provider SourceProvider) botkit.ViewFunc {
20 | return func(ctx context.Context, bot *tgbotapi.BotAPI, update tgbotapi.Update) error {
21 | idStr := update.Message.CommandArguments()
22 |
23 | id, err := strconv.ParseInt(idStr, 10, 64)
24 | if err != nil {
25 | return err
26 | }
27 |
28 | source, err := provider.SourceByID(ctx, id)
29 | if err != nil {
30 | return err
31 | }
32 |
33 | reply := tgbotapi.NewMessage(update.Message.Chat.ID, formatSource(*source))
34 | reply.ParseMode = parseModeMarkdownV2
35 |
36 | if _, err := bot.Send(reply); err != nil {
37 | return err
38 | }
39 |
40 | return nil
41 | }
42 | }
43 |
44 | func formatSource(source model.Source) string {
45 | return fmt.Sprintf(
46 | "🌐 *%s*\nID: `%d`\nURL фида: %s\nПриоритет: %d",
47 | markup.EscapeForMarkdown(source.Name),
48 | source.ID,
49 | markup.EscapeForMarkdown(source.FeedURL),
50 | source.Priority,
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/internal/bot/view_cmd_listsources.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "sort"
7 | "strings"
8 |
9 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
10 | "github.com/samber/lo"
11 |
12 | "github.com/defer-panic/news-feed-bot/internal/botkit"
13 | "github.com/defer-panic/news-feed-bot/internal/model"
14 | )
15 |
16 | type SourceLister interface {
17 | Sources(ctx context.Context) ([]model.Source, error)
18 | }
19 |
20 | func ViewCmdListSource(lister SourceLister) botkit.ViewFunc {
21 | return func(ctx context.Context, bot *tgbotapi.BotAPI, update tgbotapi.Update) error {
22 | sources, err := lister.Sources(ctx)
23 | if err != nil {
24 | return err
25 | }
26 |
27 | sort.SliceStable(sources, func(i, j int) bool {
28 | return sources[i].Priority > sources[j].Priority
29 | })
30 |
31 | var (
32 | sourceInfos = lo.Map(sources, func(source model.Source, _ int) string { return formatSource(source) })
33 | msgText = fmt.Sprintf(
34 | "Список источников \\(всего %d\\):\n\n%s",
35 | len(sources),
36 | strings.Join(sourceInfos, "\n\n"),
37 | )
38 | )
39 |
40 | reply := tgbotapi.NewMessage(update.Message.Chat.ID, msgText)
41 | reply.ParseMode = parseModeMarkdownV2
42 |
43 | if _, err := bot.Send(reply); err != nil {
44 | return err
45 | }
46 |
47 | return nil
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/internal/bot/view_cmd_setpriority.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "context"
5 |
6 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
7 |
8 | "github.com/defer-panic/news-feed-bot/internal/botkit"
9 | )
10 |
11 | type PrioritySetter interface {
12 | SetPriority(ctx context.Context, sourceID int64, priority int) error
13 | }
14 |
15 | func ViewCmdSetPriority(prioritySetter PrioritySetter) botkit.ViewFunc {
16 | type setPriorityArgs struct {
17 | SourceID int64 `json:"source_id"`
18 | Priority int `json:"priority"`
19 | }
20 |
21 | return func(ctx context.Context, bot *tgbotapi.BotAPI, update tgbotapi.Update) error {
22 | args, err := botkit.ParseJSON[setPriorityArgs](update.Message.CommandArguments())
23 | if err != nil {
24 | return err
25 | }
26 |
27 | if err := prioritySetter.SetPriority(ctx, args.SourceID, args.Priority); err != nil {
28 | return err
29 | }
30 |
31 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Приоритет успешно обновлен")
32 |
33 | if _, err := bot.Send(msg); err != nil {
34 | return err
35 | }
36 |
37 | return nil
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/internal/botkit/args.go:
--------------------------------------------------------------------------------
1 | package botkit
2 |
3 | import (
4 | "encoding/json"
5 | )
6 |
7 | func ParseJSON[T any](src string) (T, error) {
8 | var args T
9 |
10 | if err := json.Unmarshal([]byte(src), &args); err != nil {
11 | return *(new(T)), err
12 | }
13 |
14 | return args, nil
15 | }
16 |
--------------------------------------------------------------------------------
/internal/botkit/bot.go:
--------------------------------------------------------------------------------
1 | package botkit
2 |
3 | import (
4 | "context"
5 | "log"
6 | "runtime/debug"
7 | "time"
8 |
9 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
10 | )
11 |
12 | type Bot struct {
13 | api *tgbotapi.BotAPI
14 | cmdViews map[string]ViewFunc
15 | }
16 |
17 | func New(api *tgbotapi.BotAPI) *Bot {
18 | return &Bot{api: api}
19 | }
20 |
21 | func (b *Bot) RegisterCmdView(cmd string, view ViewFunc) {
22 | if b.cmdViews == nil {
23 | b.cmdViews = make(map[string]ViewFunc)
24 | }
25 |
26 | b.cmdViews[cmd] = view
27 | }
28 |
29 | func (b *Bot) Run(ctx context.Context) error {
30 | u := tgbotapi.NewUpdate(0)
31 | u.Timeout = 60
32 |
33 | updates := b.api.GetUpdatesChan(u)
34 |
35 | for {
36 | select {
37 | case update := <-updates:
38 | updateCtx, updateCancel := context.WithTimeout(context.Background(), 5*time.Minute)
39 | b.handleUpdate(updateCtx, update)
40 | updateCancel()
41 | case <-ctx.Done():
42 | return ctx.Err()
43 | }
44 | }
45 | }
46 |
47 | func (b *Bot) handleUpdate(ctx context.Context, update tgbotapi.Update) {
48 | defer func() {
49 | if p := recover(); p != nil {
50 | log.Printf("[ERROR] panic recovered: %v\n%s", p, string(debug.Stack()))
51 | }
52 | }()
53 |
54 | if (update.Message == nil || !update.Message.IsCommand()) && update.CallbackQuery == nil {
55 | return
56 | }
57 |
58 | var view ViewFunc
59 |
60 | if !update.Message.IsCommand() {
61 | return
62 | }
63 |
64 | cmd := update.Message.Command()
65 |
66 | cmdView, ok := b.cmdViews[cmd]
67 | if !ok {
68 | return
69 | }
70 |
71 | view = cmdView
72 |
73 | if err := view(ctx, b.api, update); err != nil {
74 | log.Printf("[ERROR] failed to execute view: %v", err)
75 |
76 | if _, err := b.api.Send(tgbotapi.NewMessage(update.Message.Chat.ID, "Internal error")); err != nil {
77 | log.Printf("[ERROR] failed to send error message: %v", err)
78 | }
79 | }
80 | }
81 |
82 | type ViewFunc func(ctx context.Context, bot *tgbotapi.BotAPI, update tgbotapi.Update) error
83 |
--------------------------------------------------------------------------------
/internal/botkit/markup/markdown.go:
--------------------------------------------------------------------------------
1 | package markup
2 |
3 | import "strings"
4 |
5 | var (
6 | replacer = strings.NewReplacer(
7 | "-",
8 | "\\-",
9 | "_",
10 | "\\_",
11 | "*",
12 | "\\*",
13 | "[",
14 | "\\[",
15 | "]",
16 | "\\]",
17 | "(",
18 | "\\(",
19 | ")",
20 | "\\)",
21 | "~",
22 | "\\~",
23 | "`",
24 | "\\`",
25 | ">",
26 | "\\>",
27 | "#",
28 | "\\#",
29 | "+",
30 | "\\+",
31 | "=",
32 | "\\=",
33 | "|",
34 | "\\|",
35 | "{",
36 | "\\{",
37 | "}",
38 | "\\}",
39 | ".",
40 | "\\.",
41 | "!",
42 | "\\!",
43 | )
44 | )
45 |
46 | func EscapeForMarkdown(src string) string {
47 | return replacer.Replace(src)
48 | }
49 |
--------------------------------------------------------------------------------
/internal/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "log"
5 | "sync"
6 | "time"
7 |
8 | "github.com/cristalhq/aconfig"
9 | "github.com/cristalhq/aconfig/aconfighcl"
10 | )
11 |
12 | type Config struct {
13 | TelegramBotToken string `hcl:"telegram_bot_token" env:"TELEGRAM_BOT_TOKEN" required:"true"`
14 | TelegramChannelID int64 `hcl:"telegram_channel_id" env:"TELEGRAM_CHANNEL_ID" required:"true"`
15 | DatabaseDSN string `hcl:"database_dsn" env:"DATABASE_DSN" default:"postgres://postgres:postgres@localhost:5432/news_feed_bot?sslmode=disable"`
16 | FetchInterval time.Duration `hcl:"fetch_interval" env:"FETCH_INTERVAL" default:"10m"`
17 | NotificationInterval time.Duration `hcl:"notification_interval" env:"NOTIFICATION_INTERVAL" default:"1m"`
18 | FilterKeywords []string `hcl:"filter_keywords" env:"FILTER_KEYWORDS"`
19 | OpenAIKey string `hcl:"openai_key" env:"OPENAI_KEY"`
20 | OpenAIPrompt string `hcl:"openai_prompt" env:"OPENAI_PROMPT"`
21 | OpenAIModel string `hcl:"openai_model" env:"OPENAI_MODEL" default:"gpt-3.5-turbo"`
22 | }
23 |
24 | var (
25 | cfg Config
26 | once sync.Once
27 | )
28 |
29 | func Get() Config {
30 | once.Do(func() {
31 | loader := aconfig.LoaderFor(&cfg, aconfig.Config{
32 | EnvPrefix: "NFB",
33 | Files: []string{"./config.hcl", "./config.local.hcl", "$HOME/.config/news-feed-bot/config.hcl"},
34 | FileDecoders: map[string]aconfig.FileDecoder{
35 | ".hcl": aconfighcl.New(),
36 | },
37 | })
38 |
39 | if err := loader.Load(); err != nil {
40 | log.Printf("[ERROR] failed to load config: %v", err)
41 | }
42 | })
43 |
44 | return cfg
45 | }
46 |
--------------------------------------------------------------------------------
/internal/fetcher/fetcher.go:
--------------------------------------------------------------------------------
1 | package fetcher
2 |
3 | import (
4 | "context"
5 | "log"
6 | "strings"
7 | "sync"
8 | "time"
9 |
10 | "github.com/tomakado/containers/set"
11 |
12 | "github.com/defer-panic/news-feed-bot/internal/model"
13 | src "github.com/defer-panic/news-feed-bot/internal/source"
14 | )
15 |
16 | //go:generate moq --out=mocks/mock_article_storage.go --pkg=mocks . ArticleStorage
17 | type ArticleStorage interface {
18 | Store(ctx context.Context, article model.Article) error
19 | }
20 |
21 | //go:generate moq --out=mocks/mock_sources_provider.go --pkg=mocks . SourcesProvider
22 | type SourcesProvider interface {
23 | Sources(ctx context.Context) ([]model.Source, error)
24 | }
25 |
26 | //go:generate moq --out=mocks/mock_source.go --pkg=mocks . Source
27 | type Source interface {
28 | ID() int64
29 | Name() string
30 | Fetch(ctx context.Context) ([]model.Item, error)
31 | }
32 |
33 | type Fetcher struct {
34 | articles ArticleStorage
35 | sources SourcesProvider
36 |
37 | fetchInterval time.Duration
38 | filterKeywords []string
39 | }
40 |
41 | func New(
42 | articleStorage ArticleStorage,
43 | sourcesProvider SourcesProvider,
44 | fetchInterval time.Duration,
45 | filterKeywords []string,
46 | ) *Fetcher {
47 | return &Fetcher{
48 | articles: articleStorage,
49 | sources: sourcesProvider,
50 | fetchInterval: fetchInterval,
51 | filterKeywords: filterKeywords,
52 | }
53 | }
54 |
55 | func (f *Fetcher) Start(ctx context.Context) error {
56 | ticker := time.NewTicker(f.fetchInterval)
57 | defer ticker.Stop()
58 |
59 | if err := f.Fetch(ctx); err != nil {
60 | return err
61 | }
62 |
63 | for {
64 | select {
65 | case <-ctx.Done():
66 | return ctx.Err()
67 | case <-ticker.C:
68 | if err := f.Fetch(ctx); err != nil {
69 | return err
70 | }
71 | }
72 | }
73 | }
74 |
75 | func (f *Fetcher) Fetch(ctx context.Context) error {
76 | sources, err := f.sources.Sources(ctx)
77 | if err != nil {
78 | return err
79 | }
80 |
81 | var wg sync.WaitGroup
82 |
83 | for _, source := range sources {
84 | wg.Add(1)
85 |
86 | go func(source Source) {
87 | defer wg.Done()
88 |
89 | items, err := source.Fetch(ctx)
90 | if err != nil {
91 | log.Printf("[ERROR] failed to fetch items from source %q: %v", source.Name(), err)
92 | return
93 | }
94 |
95 | if err := f.processItems(ctx, source, items); err != nil {
96 | log.Printf("[ERROR] failed to process items from source %q: %v", source.Name(), err)
97 | return
98 | }
99 | }(src.NewRSSSourceFromModel(source))
100 | }
101 |
102 | wg.Wait()
103 |
104 | return nil
105 | }
106 |
107 | func (f *Fetcher) processItems(ctx context.Context, source Source, items []model.Item) error {
108 | for _, item := range items {
109 | item.Date = item.Date.UTC()
110 |
111 | if f.itemShouldBeSkipped(item) {
112 | log.Printf("[INFO] item %q (%s) from source %q should be skipped", item.Title, item.Link, source.Name())
113 | continue
114 | }
115 |
116 | if err := f.articles.Store(ctx, model.Article{
117 | SourceID: source.ID(),
118 | Title: item.Title,
119 | Link: item.Link,
120 | Summary: item.Summary,
121 | PublishedAt: item.Date,
122 | }); err != nil {
123 | return err
124 | }
125 | }
126 |
127 | return nil
128 | }
129 |
130 | func (f *Fetcher) itemShouldBeSkipped(item model.Item) bool {
131 | categoriesSet := set.New(item.Categories...)
132 |
133 | for _, keyword := range f.filterKeywords {
134 | if categoriesSet.Contains(keyword) || strings.Contains(strings.ToLower(item.Title), keyword) {
135 | return true
136 | }
137 | }
138 |
139 | return false
140 | }
141 |
--------------------------------------------------------------------------------
/internal/fetcher/fetcher_test.go:
--------------------------------------------------------------------------------
1 | package fetcher_test
2 |
3 | import (
4 | "context"
5 | _ "embed"
6 | "net/http"
7 | "net/http/httptest"
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 | "github.com/stretchr/testify/require"
12 |
13 | "github.com/defer-panic/news-feed-bot/internal/fetcher"
14 | "github.com/defer-panic/news-feed-bot/internal/fetcher/mocks"
15 | "github.com/defer-panic/news-feed-bot/internal/model"
16 | )
17 |
18 | //go:embed testdata/feed1.xml
19 | var feed1 []byte
20 |
21 | //go:embed testdata/feed2.xml
22 | var feed2 []byte
23 |
24 | func TestFetcher_Fetch(t *testing.T) {
25 | var (
26 | source1Server = setupFeedSever(feed1)
27 | source2Server = setupFeedSever(feed2)
28 | sourcesProvider = &mocks.SourcesProviderMock{
29 | SourcesFunc: func(ctx context.Context) ([]model.Source, error) {
30 | return []model.Source{
31 | {
32 | ID: 1,
33 | Name: "dev.to",
34 | FeedURL: source1Server.URL,
35 | Priority: 10,
36 | },
37 | {
38 | ID: 2,
39 | Name: "Go Time Podcast",
40 | FeedURL: source2Server.URL,
41 | Priority: 100,
42 | },
43 | }, nil
44 | },
45 | }
46 | )
47 |
48 | t.Run("should fetch articles from all sources", func(t *testing.T) {
49 | var (
50 | articles = make(map[string]model.Article)
51 | articleStorage = &mocks.ArticleStorageMock{
52 | StoreFunc: func(ctx context.Context, article model.Article) error {
53 | articles[article.Link] = article
54 | return nil
55 | },
56 | }
57 | fetcher = fetcher.New(articleStorage, sourcesProvider, 0, nil)
58 | )
59 |
60 | require.NoError(t, fetcher.Fetch(context.Background()))
61 | assert.Len(t, articles, 4)
62 | })
63 |
64 | t.Run("should filter articles by keywords", func(t *testing.T) {
65 | var (
66 | articles = make(map[string]model.Article)
67 | articleStorage = &mocks.ArticleStorageMock{
68 | StoreFunc: func(ctx context.Context, article model.Article) error {
69 | articles[article.Link] = article
70 | return nil
71 | },
72 | }
73 | filterKeywords = []string{"leetcode"}
74 | fetcher = fetcher.New(articleStorage, sourcesProvider, 0, filterKeywords)
75 | )
76 |
77 | require.NoError(t, fetcher.Fetch(context.Background()))
78 | assert.Len(t, articles, 3)
79 | })
80 | }
81 |
82 | func setupFeedSever(feed []byte) *httptest.Server {
83 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
84 | w.Header().Add("Content-Type", "application/xml; charset=utf-8")
85 | _, _ = w.Write(feed)
86 | }))
87 | }
88 |
--------------------------------------------------------------------------------
/internal/fetcher/mocks/mock_article_storage.go:
--------------------------------------------------------------------------------
1 | // Code generated by moq; DO NOT EDIT.
2 | // github.com/matryer/moq
3 |
4 | package mocks
5 |
6 | import (
7 | "context"
8 | "github.com/defer-panic/news-feed-bot/internal/fetcher"
9 | "github.com/defer-panic/news-feed-bot/internal/model"
10 | "sync"
11 | )
12 |
13 | // Ensure, that ArticleStorageMock does implement fetcher.ArticleStorage.
14 | // If this is not the case, regenerate this file with moq.
15 | var _ fetcher.ArticleStorage = &ArticleStorageMock{}
16 |
17 | // ArticleStorageMock is a mock implementation of fetcher.ArticleStorage.
18 | //
19 | // func TestSomethingThatUsesArticleStorage(t *testing.T) {
20 | //
21 | // // make and configure a mocked fetcher.ArticleStorage
22 | // mockedArticleStorage := &ArticleStorageMock{
23 | // StoreFunc: func(ctx context.Context, article model.Article) error {
24 | // panic("mock out the Store method")
25 | // },
26 | // }
27 | //
28 | // // use mockedArticleStorage in code that requires fetcher.ArticleStorage
29 | // // and then make assertions.
30 | //
31 | // }
32 | type ArticleStorageMock struct {
33 | // StoreFunc mocks the Store method.
34 | StoreFunc func(ctx context.Context, article model.Article) error
35 |
36 | // calls tracks calls to the methods.
37 | calls struct {
38 | // Store holds details about calls to the Store method.
39 | Store []struct {
40 | // Ctx is the ctx argument value.
41 | Ctx context.Context
42 | // Article is the article argument value.
43 | Article model.Article
44 | }
45 | }
46 | lockStore sync.RWMutex
47 | }
48 |
49 | // Store calls StoreFunc.
50 | func (mock *ArticleStorageMock) Store(ctx context.Context, article model.Article) error {
51 | if mock.StoreFunc == nil {
52 | panic("ArticleStorageMock.StoreFunc: method is nil but ArticleStorage.Store was just called")
53 | }
54 | callInfo := struct {
55 | Ctx context.Context
56 | Article model.Article
57 | }{
58 | Ctx: ctx,
59 | Article: article,
60 | }
61 | mock.lockStore.Lock()
62 | mock.calls.Store = append(mock.calls.Store, callInfo)
63 | mock.lockStore.Unlock()
64 | return mock.StoreFunc(ctx, article)
65 | }
66 |
67 | // StoreCalls gets all the calls that were made to Store.
68 | // Check the length with:
69 | //
70 | // len(mockedArticleStorage.StoreCalls())
71 | func (mock *ArticleStorageMock) StoreCalls() []struct {
72 | Ctx context.Context
73 | Article model.Article
74 | } {
75 | var calls []struct {
76 | Ctx context.Context
77 | Article model.Article
78 | }
79 | mock.lockStore.RLock()
80 | calls = mock.calls.Store
81 | mock.lockStore.RUnlock()
82 | return calls
83 | }
84 |
--------------------------------------------------------------------------------
/internal/fetcher/mocks/mock_source.go:
--------------------------------------------------------------------------------
1 | // Code generated by moq; DO NOT EDIT.
2 | // github.com/matryer/moq
3 |
4 | package mocks
5 |
6 | import (
7 | "context"
8 | "github.com/defer-panic/news-feed-bot/internal/fetcher"
9 | "github.com/defer-panic/news-feed-bot/internal/model"
10 | "sync"
11 | )
12 |
13 | // Ensure, that SourceMock does implement fetcher.Source.
14 | // If this is not the case, regenerate this file with moq.
15 | var _ fetcher.Source = &SourceMock{}
16 |
17 | // SourceMock is a mock implementation of fetcher.Source.
18 | //
19 | // func TestSomethingThatUsesSource(t *testing.T) {
20 | //
21 | // // make and configure a mocked fetcher.Source
22 | // mockedSource := &SourceMock{
23 | // FetchFunc: func(ctx context.Context) ([]model.Item, error) {
24 | // panic("mock out the Fetch method")
25 | // },
26 | // IDFunc: func() int64 {
27 | // panic("mock out the ID method")
28 | // },
29 | // NameFunc: func() string {
30 | // panic("mock out the Name method")
31 | // },
32 | // }
33 | //
34 | // // use mockedSource in code that requires fetcher.Source
35 | // // and then make assertions.
36 | //
37 | // }
38 | type SourceMock struct {
39 | // FetchFunc mocks the Fetch method.
40 | FetchFunc func(ctx context.Context) ([]model.Item, error)
41 |
42 | // IDFunc mocks the ID method.
43 | IDFunc func() int64
44 |
45 | // NameFunc mocks the Name method.
46 | NameFunc func() string
47 |
48 | // calls tracks calls to the methods.
49 | calls struct {
50 | // Fetch holds details about calls to the Fetch method.
51 | Fetch []struct {
52 | // Ctx is the ctx argument value.
53 | Ctx context.Context
54 | }
55 | // ID holds details about calls to the ID method.
56 | ID []struct {
57 | }
58 | // Name holds details about calls to the Name method.
59 | Name []struct {
60 | }
61 | }
62 | lockFetch sync.RWMutex
63 | lockID sync.RWMutex
64 | lockName sync.RWMutex
65 | }
66 |
67 | // Fetch calls FetchFunc.
68 | func (mock *SourceMock) Fetch(ctx context.Context) ([]model.Item, error) {
69 | if mock.FetchFunc == nil {
70 | panic("SourceMock.FetchFunc: method is nil but Source.Fetch was just called")
71 | }
72 | callInfo := struct {
73 | Ctx context.Context
74 | }{
75 | Ctx: ctx,
76 | }
77 | mock.lockFetch.Lock()
78 | mock.calls.Fetch = append(mock.calls.Fetch, callInfo)
79 | mock.lockFetch.Unlock()
80 | return mock.FetchFunc(ctx)
81 | }
82 |
83 | // FetchCalls gets all the calls that were made to Fetch.
84 | // Check the length with:
85 | //
86 | // len(mockedSource.FetchCalls())
87 | func (mock *SourceMock) FetchCalls() []struct {
88 | Ctx context.Context
89 | } {
90 | var calls []struct {
91 | Ctx context.Context
92 | }
93 | mock.lockFetch.RLock()
94 | calls = mock.calls.Fetch
95 | mock.lockFetch.RUnlock()
96 | return calls
97 | }
98 |
99 | // ID calls IDFunc.
100 | func (mock *SourceMock) ID() int64 {
101 | if mock.IDFunc == nil {
102 | panic("SourceMock.IDFunc: method is nil but Source.ID was just called")
103 | }
104 | callInfo := struct {
105 | }{}
106 | mock.lockID.Lock()
107 | mock.calls.ID = append(mock.calls.ID, callInfo)
108 | mock.lockID.Unlock()
109 | return mock.IDFunc()
110 | }
111 |
112 | // IDCalls gets all the calls that were made to ID.
113 | // Check the length with:
114 | //
115 | // len(mockedSource.IDCalls())
116 | func (mock *SourceMock) IDCalls() []struct {
117 | } {
118 | var calls []struct {
119 | }
120 | mock.lockID.RLock()
121 | calls = mock.calls.ID
122 | mock.lockID.RUnlock()
123 | return calls
124 | }
125 |
126 | // Name calls NameFunc.
127 | func (mock *SourceMock) Name() string {
128 | if mock.NameFunc == nil {
129 | panic("SourceMock.NameFunc: method is nil but Source.Name was just called")
130 | }
131 | callInfo := struct {
132 | }{}
133 | mock.lockName.Lock()
134 | mock.calls.Name = append(mock.calls.Name, callInfo)
135 | mock.lockName.Unlock()
136 | return mock.NameFunc()
137 | }
138 |
139 | // NameCalls gets all the calls that were made to Name.
140 | // Check the length with:
141 | //
142 | // len(mockedSource.NameCalls())
143 | func (mock *SourceMock) NameCalls() []struct {
144 | } {
145 | var calls []struct {
146 | }
147 | mock.lockName.RLock()
148 | calls = mock.calls.Name
149 | mock.lockName.RUnlock()
150 | return calls
151 | }
152 |
--------------------------------------------------------------------------------
/internal/fetcher/mocks/mock_sources_provider.go:
--------------------------------------------------------------------------------
1 | // Code generated by moq; DO NOT EDIT.
2 | // github.com/matryer/moq
3 |
4 | package mocks
5 |
6 | import (
7 | "context"
8 | "github.com/defer-panic/news-feed-bot/internal/fetcher"
9 | "github.com/defer-panic/news-feed-bot/internal/model"
10 | "sync"
11 | )
12 |
13 | // Ensure, that SourcesProviderMock does implement fetcher.SourcesProvider.
14 | // If this is not the case, regenerate this file with moq.
15 | var _ fetcher.SourcesProvider = &SourcesProviderMock{}
16 |
17 | // SourcesProviderMock is a mock implementation of fetcher.SourcesProvider.
18 | //
19 | // func TestSomethingThatUsesSourcesProvider(t *testing.T) {
20 | //
21 | // // make and configure a mocked fetcher.SourcesProvider
22 | // mockedSourcesProvider := &SourcesProviderMock{
23 | // SourcesFunc: func(ctx context.Context) ([]model.Source, error) {
24 | // panic("mock out the Sources method")
25 | // },
26 | // }
27 | //
28 | // // use mockedSourcesProvider in code that requires fetcher.SourcesProvider
29 | // // and then make assertions.
30 | //
31 | // }
32 | type SourcesProviderMock struct {
33 | // SourcesFunc mocks the Sources method.
34 | SourcesFunc func(ctx context.Context) ([]model.Source, error)
35 |
36 | // calls tracks calls to the methods.
37 | calls struct {
38 | // Sources holds details about calls to the Sources method.
39 | Sources []struct {
40 | // Ctx is the ctx argument value.
41 | Ctx context.Context
42 | }
43 | }
44 | lockSources sync.RWMutex
45 | }
46 |
47 | // Sources calls SourcesFunc.
48 | func (mock *SourcesProviderMock) Sources(ctx context.Context) ([]model.Source, error) {
49 | if mock.SourcesFunc == nil {
50 | panic("SourcesProviderMock.SourcesFunc: method is nil but SourcesProvider.Sources was just called")
51 | }
52 | callInfo := struct {
53 | Ctx context.Context
54 | }{
55 | Ctx: ctx,
56 | }
57 | mock.lockSources.Lock()
58 | mock.calls.Sources = append(mock.calls.Sources, callInfo)
59 | mock.lockSources.Unlock()
60 | return mock.SourcesFunc(ctx)
61 | }
62 |
63 | // SourcesCalls gets all the calls that were made to Sources.
64 | // Check the length with:
65 | //
66 | // len(mockedSourcesProvider.SourcesCalls())
67 | func (mock *SourcesProviderMock) SourcesCalls() []struct {
68 | Ctx context.Context
69 | } {
70 | var calls []struct {
71 | Ctx context.Context
72 | }
73 | mock.lockSources.RLock()
74 | calls = mock.calls.Sources
75 | mock.lockSources.RUnlock()
76 | return calls
77 | }
78 |
--------------------------------------------------------------------------------
/internal/fetcher/testdata/feed1.xml:
--------------------------------------------------------------------------------
1 | Disclosure: This post includes affiliate links; I may receive compensation if you purchase products or services from the different links provided in this article. Hello folks, if you want to learn the Go programming language or Golang in 2023, one of the darling language of emerging tech companies like ByteDance (the company behind TikTok) and one that has come from Google to improve developer productivity and looking for the best resources like books, tutorials, and online courses then you have come to the right place. Earlier, I have shared the best Golang courses, best Golang projects from Udemy, Pluralsight, and Coursera but a lot of you asked for free Golang online courses to learn Go programming language so that you can start learning this in-demand programming language without any cost barrier. I heard that and I looked for the best free courses I can find on the internet to learn Golang and this article is the result of that. In this article I have shared the best free online courses to learn Golang from sites like freeCodecamp, YouTube, Udemy, and Coursera. If you don't know, Yes, both Udemy and Coursera also have free online tutorials and courses and you can join them to learn useful skills like Golang. Coming back to Golang and the power of the Go programming language, what if I tell to you that in the significant number of experiment, there is a programming language that beats Python? A language that excels Java, which is often recognized as being much faster than Python. That it can even make the software run quicker if that is all you want it to do. Golang is the answer to all of these questions. Google has created this rationally built programming language. Although it is comparable to C in terms of data consumption and storage, it differs in terms of syntactic type. It also incorporates best practices from the modern programming language which makes it ideal for backend and server-side development If you are wondering where is Golang used in real world then let me tell you that Go or Golang programming language is used in various domains such as web development, network programming, system programming, cloud-native development, artificial intelligence and machine learning, and more. Since Golang is a highly performant and scalable language its also suitable for building large-scale distributed systems and microservices. Some of the popular companies that use Go include Google, Uber, Dropbox, Docker, and Netflix, to name a few. Its simplicity, efficiency, and ease of use have made it a preferred choice among developers for building high-performance applications. By the way, if you can spend few bucks like $10 then I also suggest you to checkout Go: The Complete Developer's Guide (Golang) course on Udemy, it's a great course to start with Go programming in 2023. Without any further delay, here I will show you the top courses to learn Go. The "GetGoing: Introduction to Golang" is a free, beginner-level course on Udemy that aims to provide a comprehensive introduction to the Go programming language to beginners. The course covers topics such as data types, control structures, functions, arrays, slices, maps, and pointers. It also teaches how to work with packages, create and use interfaces, and use Goroutines and channels for concurrent programming. This free Udemy course is designed for developers who have some experience with programming but are new to Go. It is structured in a way that is easy to follow, with clear explanations and practical examples. The course includes a mix of video lectures, quizzes, and coding exercises that allow learners to practice what they have learned. Some of the benefits of taking this course include learning a popular and fast-growing programming language, developing skills that are in high demand in the job market, and gaining an understanding of how to build efficient and scalable applications. Overall, the "GetGoing: Introduction to Golang" course on Udemy can be a great starting point for those who want to learn Go and expand their programming skills. Link to the course- GetGoing: Introduction to Golang The total time of all lectures is roughly 3 hours and 30 minutes; however, you can go at your leisure. It's great for software lovers and total beginners who wish to learn more about programming. Here are things you will learn in this free Golang tutorial: Overall a great free tutorial and online course to learn Golang programming language from scratch in 2023. All you need is a free Udemy account to join this course. This is another free online Golang course which is available on Coursera with an average rating of 4.6 & more than 43000 learners have enrolled in it. This is an intermediate-level course suggesting that having basic knowledge about the fundamentals of Go will be a plus point. Build a solid foundation of Go, an open-source language created by Google & improved by a large number of volunteers. This session is for those who have prior programming expertise in languages like C, Java, etc. It explains the principles of this language. Input parameters, methods, interfaces, & creating code that integrates RFCs and JSON are all taught. Most significantly, you'll get the opportunity to develop Go programs & get comments from your colleagues. Here is the link to join the course- Getting Started with Go This course requires basic knowledge of programming languages like you should be familiar with loops, data types, etc. All the lectures are completely online which you can access any time after enrolling in the course. What you will learn from this course? This one is another great course available on the Coursera platform, with an average 4.6 learners rating & over 16000 students enrolled in this program. It is offered by the UCI Division of continuing education. In this program, you will discover routines, protocols, & interfaces as you extend your understanding of the Go programming language. The execution of routines, function types, object orientation in Go, approaches, and class generation are among the subjects addressed in the lectures. You will learn all these topics by implementing them in a software program so that you will also have a hands-on project experience or you can say a real-time problem-solving encounter. Here is the link to join the course- Functions, Methods, and Interfaces in Go Intermediate level course -- basic knowledge of Go will be appreciated. No need to worry about timings, you can learn at your schedule. Theory-related documents are also provided with the lectures so, that. You don't have to wander from one webpage to another reading the concepts. What you will learn- functions -- what are they, how to call a function. By the way, If you are planning to join multiple Coursera courses or specializations then consider taking a** Coursera Plus subscription,** which provides you unlimited access to their most popular courses, specialization, professional certificate, and guided projects. It costs around $399/year but it's completely worth of your money as you get unlimited certificates. YouTube is another excellent resource for learning Golang. This course is offered on the platform's FreeCodeCamp channel. When it comes to learning a certain skill or chore, such as how to tie a knot, prepare a specific meal or program in Java or another language, YouTube is the best site to learn all of these things for free. The FreeCodeCamp.org channel has a variety of complete playlists from which one can learn java programming, python, android development, Golang, and even much more from basic to advanced levels in a couple of hours. The tutors there are experts in their field & work in big companies in the software development department. A complete step-by-step instructional course that will teach you Go programming. Here are things you will learn in this course: This one is the complete package course but, it is a paid one. This is for people who want to master Golang. This course is offered by the Udemy platform. With over 4.6 rating on the platform & helping over 87000 learners across the globe. This is one of the best programs available on the site which aids you in understanding the Go programming in brief. You will go over the fundamentals swiftly before diving through some of the language's relatively complex capabilities in the lessons. Don't be misled by other programs that teach you simply loop statements. It's the only program on Udemy that will train you to leverage Go's parallelism framework to its full potential. Golang was supposed to be intuitive to acquire yet complicated to comprehend. You'll soon understand the language's peculiarities & eccentricities thanks to various tasks, tests, and projects in this course. Go is similar to any other programming language in that it requires you to create code to master it. Top organizations like Volkswagen, Netflix, and others, according to Udemy, recommend this course to their staff. This program is 9+ hours long, but you don't have to rush through it. Take as much time as you need to understand each topic. Here are things you will learn in this course: Here is the link to the course- Go: The Complete Developer's Guide (Golang) Yes, there are several job benefits for learning Golang in 2023. Some of them are: High Demand Good Pay Career Growth Easy to Learn Strong Community Support That's all about the best free online courses to learn Golang in 2023. There is no doubt that Golang is worth learning. Even it has a promising future as in upcoming years it will attract more & more developers. If you want to create applications with a parallelism concept then Go is what you will need. Other Free Programming Resource articles you may like to explore Thanks for reading this article so far. If you find these best free Golang programming courses from Udemy and Coursera useful, please share them with your friends and colleagues. If you have any questions, feedback, or other fee courses to add to this list, please feel free to suggest. P. S. - If you want to learn Golang programming and development and need a hands-on, project-based resource then the Building Modern Web Applications with Go (Golang) course is a great course to start with. It's not free but quite affordable, and you can buy it for just $10 on Udemy sales. More than 8K Golang developers have already benefited from it 5 Best Free Golang Programming courses for Beginners in 2023
1. GetGoing: Introduction to Golang
Key highlights of this course
2. Getting Started with Go on Coursera
Highlights of the program
You will explore all the benefits of learning Go & the instructor will aid you in setting up your practice environment to create programs with Go.
3. Functions, Methods, and Interfaces in Go
Highlights of the course
4. Learn Go Programming - Golang Tutorial for Beginners
Key highlights of the course
The duration of this course is around 7 hours you can either go all out & learn everything in one sitting or for better understanding of the concepts it is recommended to learn & revise what you learn in more than one sitting. You can access the lectures on any device you want & at any time.
5. Go: The Complete Developer's Guide (Golang) [Paid Couse]
The course's highlights
Why should you learn Golang? Is there any job benefits?
Golang is a popular programming language for developing high-performance systems, microservices, and network applications. With the rise of cloud computing and distributed systems, the demand for Golang developers has increased rapidly.
As the demand for Golang developers has increased, so has the salary. According to various job sites, Golang developers earn an average salary of $120,000 to $140,000 per year, which is higher than the average for other programming languages.
Golang is a relatively new language, which means that there are many opportunities for career growth. As more and more companies adopt Golang, the demand for experienced Golang developers will only increase.
Golang has a simple syntax and is easy to learn. If you have experience with C, C++, or Java, you can learn Golang quickly.
Golang has a strong community of developers who are always willing to help and share their knowledge. This makes it easy to get started with Golang and find solutions to problems you may encounter while developing applications.
Changelog++ members support our work, get closer to the metal, and make the ads disappear. Join today!
Sponsors:
Featuring:
Show Notes:
Something missing or broken? PRs welcome!
Timestamps:
(00:00) - It's Go Time!
(00:58) - ICE BREAKERS
(12:00) - Mat avoids new()
(17:20) - Jon avoids full slice expressions
(20:53) - Carl avoids bare returns
(24:22) - A linter up in your kitchen
(26:59) - Mat tries not to panic
(30:52) - Jon avoids labels
(35:1346) - Templates, globals, init
(38:40) - Prank Time (+ more bad ideas)
(42:57) - Jon tells a story
(44:20) - Useless uses of generics
(47:41) - Jon doesn't use internal packages
(51:17) - It's time for Unpopular Opinions!
(51:43) - Mat's unpop
(57:56) - Gotta Go!
(59:04) - Next time on Go Time
Two men… against all odds… join an award-worthy podcast… hosted by a coin-operated, singing code monkey (?)… to convince the developer world they’re doing it ALL wrong.
Grab your code-generator and heat up that cold cup of coffee on your desk. Because this episode of Go Time is about to blow your docs off!
Changelog++ members save 2 minutes on this episode because they made the ads disappear. Join today!
Sponsors:
Featuring:
Show Notes:
Something missing or broken? PRs welcome!
Timestamps:
(00:00) - In a world...
(01:29) - It's Go Time!
(02:27) - Welcoming our guests
(12:06) - Usage docs
(20:35) - Maintaining docs
(29:10) - Sponsor: Changelog++
(30:05) - Don't believe the Hype
(36:11) - Document-driven development
(40:55) - The bigger takeaways
(44:53) - It's time for Unpopular Opinions!
(45:53) - Mark's unpop[0]
(51:51) - Cory's unpop
(58:18) - Mark's unpop[1]
(1:02:23) - Mark's unpop[2]
(1:03:23) - Johnny's unpop
(1:12:05) - Time to Go!
(1:13:27) - Next time on Go Time
Disclosure: This post includes affiliate links; I may receive compensation if you purchase products or services from the different links provided in this article.
Hello folks, if you want to learn the Go programming language or Golang in 2023, one of the darling language of emerging tech companies like ByteDance (the company behind TikTok) and one that has come from Google to improve developer productivity and looking for the best resources like books, tutorials, and online courses then you have come to the right place.
Earlier, I have shared the best Golang courses, best Golang projects from Udemy, Pluralsight, and Coursera but a lot of you asked for free Golang online courses to learn Go programming language so that you can start learning this in-demand programming language without any cost barrier.
I heard that and I looked for the best free courses I can find on the internet to learn Golang and this article is the result of that.
In this article I have shared the best free online courses to learn Golang from sites like freeCodecamp, YouTube, Udemy, and Coursera.
If you don't know, Yes, both Udemy and Coursera also have free online tutorials and courses and you can join them to learn useful skills like Golang.
Coming back to Golang and the power of the Go programming language, what if I tell to you that in the significant number of experiment, there is a programming language that beats Python? A language that excels Java, which is often recognized as being much faster than Python. That it can even make the software run quicker if that is all you want it to do.
Golang is the answer to all of these questions.
Google has created this rationally built programming language. Although it is comparable to C in terms of data consumption and storage, it differs in terms of syntactic type.
It also incorporates best practices from the modern programming language which makes it ideal for backend and server-side development
If you are wondering where is Golang used in real world then let me tell you that Go or Golang programming language is used in various domains such as web development, network programming, system programming, cloud-native development, artificial intelligence and machine learning, and more.
Since Golang is a highly performant and scalable language its also suitable for building large-scale distributed systems and microservices.
Some of the popular companies that use Go include Google, Uber, Dropbox, Docker, and Netflix, to name a few. Its simplicity, efficiency, and ease of use have made it a preferred choice among developers for building high-performance applications.
By the way, if you can spend few bucks like $10 then I also suggest you to checkout Go: The Complete Developer's Guide (Golang) course on Udemy, it's a great course to start with Go programming in 2023.
Without any further delay, here I will show you the top courses to learn Go.
The "GetGoing: Introduction to Golang" is a free, beginner-level course on Udemy that aims to provide a comprehensive introduction to the Go programming language to beginners.
The course covers topics such as data types, control structures, functions, arrays, slices, maps, and pointers. It also teaches how to work with packages, create and use interfaces, and use Goroutines and channels for concurrent programming.
This free Udemy course is designed for developers who have some experience with programming but are new to Go. It is structured in a way that is easy to follow, with clear explanations and practical examples. The course includes a mix of video lectures, quizzes, and coding exercises that allow learners to practice what they have learned.
Some of the benefits of taking this course include learning a popular and fast-growing programming language, developing skills that are in high demand in the job market, and gaining an understanding of how to build efficient and scalable applications.
Overall, the "GetGoing: Introduction to Golang" course on Udemy can be a great starting point for those who want to learn Go and expand their programming skills.
Link to the course- GetGoing: Introduction to Golang
The total time of all lectures is roughly 3 hours and 30 minutes; however, you can go at your leisure. It's great for software lovers and total beginners who wish to learn more about programming.
Here are things you will learn in this free Golang tutorial:
Overall a great free tutorial and online course to learn Golang programming language from scratch in 2023. All you need is a free Udemy account to join this course.
This is another free online Golang course which is available on Coursera with an average rating of 4.6 & more than 43000 learners have enrolled in it. This is an intermediate-level course suggesting that having basic knowledge about the fundamentals of Go will be a plus point.
Build a solid foundation of Go, an open-source language created by Google & improved by a large number of volunteers. This session is for those who have prior programming expertise in languages like C, Java, etc.
It explains the principles of this language. Input parameters, methods, interfaces, & creating code that integrates RFCs and JSON are all taught. Most significantly, you'll get the opportunity to develop Go programs & get comments from your colleagues.
Here is the link to join the course- Getting Started with Go
This course requires basic knowledge of programming languages like you should be familiar with loops, data types, etc.
All the lectures are completely online which you can access any time after enrolling in the course.
What you will learn from this course?
You will explore all the benefits of learning Go & the instructor will aid you in setting up your practice environment to create programs with Go.
This one is another great course available on the Coursera platform, with an average 4.6 learners rating & over 16000 students enrolled in this program. It is offered by the UCI Division of continuing education.
In this program, you will discover routines, protocols, & interfaces as you extend your understanding of the Go programming language.
The execution of routines, function types, object orientation in Go, approaches, and class generation are among the subjects addressed in the lectures. You will learn all these topics by implementing them in a software program so that you will also have a hands-on project experience or you can say a real-time problem-solving encounter.
Here is the link to join the course- Functions, Methods, and Interfaces in Go
Intermediate level course -- basic knowledge of Go will be appreciated.
No need to worry about timings, you can learn at your schedule. Theory-related documents are also provided with the lectures so, that. You don't have to wander from one webpage to another reading the concepts.
What you will learn- functions -- what are they, how to call a function.
By the way, If you are planning to join multiple Coursera courses or specializations then consider taking a** Coursera Plus subscription,** which provides you unlimited access to their most popular courses, specialization, professional certificate, and guided projects. It costs around $399/year but it's completely worth of your money as you get unlimited certificates.
YouTube is another excellent resource for learning Golang. This course is offered on the platform's FreeCodeCamp channel.
When it comes to learning a certain skill or chore, such as how to tie a knot, prepare a specific meal or program in Java or another language, YouTube is the best site to learn all of these things for free.
The FreeCodeCamp.org channel has a variety of complete playlists from which one can learn java programming, python, android development, Golang, and even much more from basic to advanced levels in a couple of hours. The tutors there are experts in their field & work in big companies in the software development department.
A complete step-by-step instructional course that will teach you Go programming.
The duration of this course is around 7 hours you can either go all out & learn everything in one sitting or for better understanding of the concepts it is recommended to learn & revise what you learn in more than one sitting. You can access the lectures on any device you want & at any time.
Here are things you will learn in this course:
This one is the complete package course but, it is a paid one. This is for people who want to master Golang.
This course is offered by the Udemy platform. With over 4.6 rating on the platform & helping over 87000 learners across the globe. This is one of the best programs available on the site which aids you in understanding the Go programming in brief.
You will go over the fundamentals swiftly before diving through some of the language's relatively complex capabilities in the lessons. Don't be misled by other programs that teach you simply loop statements. It's the only program on Udemy that will train you to leverage Go's parallelism framework to its full potential.
Golang was supposed to be intuitive to acquire yet complicated to comprehend. You'll soon understand the language's peculiarities & eccentricities thanks to various tasks, tests, and projects in this course. Go is similar to any other programming language in that it requires you to create code to master it.
Top organizations like Volkswagen, Netflix, and others, according to Udemy, recommend this course to their staff. This program is 9+ hours long, but you don't have to rush through it. Take as much time as you need to understand each topic.
Here are things you will learn in this course:
Here is the link to the course- Go: The Complete Developer's Guide (Golang)
Yes, there are several job benefits for learning Golang in 2023. Some of them are:
High Demand
Golang is a popular programming language for developing high-performance systems, microservices, and network applications. With the rise of cloud computing and distributed systems, the demand for Golang developers has increased rapidly.
Good Pay
As the demand for Golang developers has increased, so has the salary. According to various job sites, Golang developers earn an average salary of $120,000 to $140,000 per year, which is higher than the average for other programming languages.
Career Growth
Golang is a relatively new language, which means that there are many opportunities for career growth. As more and more companies adopt Golang, the demand for experienced Golang developers will only increase.
Easy to Learn
Golang has a simple syntax and is easy to learn. If you have experience with C, C++, or Java, you can learn Golang quickly.
Strong Community Support
Golang has a strong community of developers who are always willing to help and share their knowledge. This makes it easy to get started with Golang and find solutions to problems you may encounter while developing applications.
That's all about the best free online courses to learn Golang in 2023. There is no doubt that Golang is worth learning. Even it has a promising future as in upcoming years it will attract more & more developers. If you want to create applications with a parallelism concept then Go is what you will need.
Other Free Programming Resource articles you may like to explore
Thanks for reading this article so far. If you find these best free Golang programming courses from Udemy and Coursera useful, please share them with your friends and colleagues. If you have any questions, feedback, or other fee courses to add to this list, please feel free to suggest.
P. S. - If you want to learn Golang programming and development and need a hands-on, project-based resource then the Building Modern Web Applications with Go (Golang) course is a great course to start with. It's not free but quite affordable, and you can buy it for just $10 on Udemy sales. More than 8K Golang developers have already benefited from it