├── .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 | 2 | 3 | DEV Community: go 4 | The latest articles tagged 'go' on DEV Community. 5 | https://dev.to/t/go 6 | 7 | en 8 | 9 | Climbing Stairs LeetCode 70 10 | digitebs 11 | Sun, 19 Mar 2023 07:04:42 +0000 12 | https://dev.to/digitebs/climbing-stairs-leetcode-70-4n1j 13 | https://dev.to/digitebs/climbing-stairs-leetcode-70-4n1j 14 |

15 | leetcode 16 | go 17 | programming 18 | algorithms 19 |
20 | 21 | My Favorite Free Courses to Learn Golang in 2023 22 | javinpaul 23 | Sat, 18 Mar 2023 07:22:06 +0000 24 | https://dev.to/javinpaul/my-favorite-free-courses-to-learn-golang-in-2023-3mh6 25 | https://dev.to/javinpaul/my-favorite-free-courses-to-learn-golang-in-2023-3mh6 26 |

Disclosure: This post includes affiliate links; I may receive compensation if you purchase products or services from the different links provided in this article.

best Free Courses to Learn Golang

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.

best course to learn Golang

5 Best Free Golang Programming courses for Beginners in 2023

Without any further delay, here I will show you the top courses to learn Go.

1. GetGoing: Introduction to Golang

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

Best free Golang tutorials and courses

Key highlights of this course

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:

  • All the basic concepts to get you started with Golang.
  • Creating an application programming interface.
  • Hosting an application in a cloud environment (Heroku cloud).
  • How to establish a connection with a database, & Backend development with Go.

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.

2. Getting Started with Go on Coursera

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

best Coursera Courses to learn Golang

Highlights of the program

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.

  • Brief understanding of the concept of arrays, slices & maps.
  • You will also learn how to gain & modify information from external files with the help of Go.

3. Functions, Methods, and Interfaces in 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

best Coursera Courses to learn Golang

Highlights of the course

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.

  • Object-oriented programming in Go.
  • How to create classes & use different properties of a class.
  • Interface for abstraction

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.

4. Learn Go Programming - Golang Tutorial for Beginners

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.

Key highlights of the course

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:

  • The fundamentals of the language its usage & origin.
  • How to establish your practice environment.
  • About data types, arrays, slices.
  • Loop statements with practical implementation rather than theoretical.
  • What are channels & Goroutines?

This one is the complete package course but, it is a paid one. This is for people who want to master Golang.

5. Go: The Complete Developer's Guide (Golang) [Paid Couse]

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.

The course's highlights

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:

  • Create tremendously contemporaneous applications using Go functions.
  • Understand the distinctions between the most prevalent data structures.
  • How to use advanced functions innovatively.

Here is the link to the course- Go: The Complete Developer's Guide (Golang)

Best online course to learn Golang

Why should you learn Golang? Is there any job benefits?

Yes, there are several job benefits for learning Golang in 2023. Some of them are:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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

27 | programming 28 | coding 29 | go 30 | development 31 |
32 |
33 |
-------------------------------------------------------------------------------- /internal/fetcher/testdata/feed2.xml: -------------------------------------------------------------------------------- 1 | This XML file does not appear to have any style information associated with it. The document tree is shown below. 2 | 3 | 4 | Go Time: Golang, Software Engineering 5 | All rights reserved 6 | https://changelog.com/gotime 7 | 8 | 9 | en-us 10 | Your source for diverse discussions from around the Go community. This show records LIVE every Tuesday at 3pm US Eastern. Join the Golang community and chat with us during the show in the #gotimefm channel of Gophers slack. Panelists include Mat Ryer, Jon Calhoun, Natalie Pistunovich, Johnny Boursiquot, Angelica Hill, Kris Brandow, and Ian Lopshire. We discuss cloud infrastructure, distributed systems, microservices, Kubernetes, Docker… oh and also Go! Some people search for GoTime or GoTimeFM and can’t find the show, so now the strings GoTime and GoTimeFM are in our description too. 11 | Changelog Media 12 | Your source for diverse discussions from around the Go community. This show records LIVE every Tuesday at 3pm US Eastern. Join the Golang community and chat with us during the show in the #gotimefm channel of Gophers slack. Panelists include Mat Ryer, Jon Calhoun, Natalie Pistunovich, Johnny Boursiquot, Angelica Hill, Kris Brandow, and Ian Lopshire. We discuss cloud infrastructure, distributed systems, microservices, Kubernetes, Docker… oh and also Go! Some people search for GoTime or GoTimeFM and can’t find the show, so now the strings GoTime and GoTimeFM are in our description too. 13 | no 14 | 15 | go, golang, open source, software, development, devops, architecture, docker, kubernetes 16 | 17 | 18 | 19 | 20 | Support our work by joining Changelog++ 21 | Mat Ryer 22 | Angelica Hill 23 | Kris Brandow 24 | Ian Lopshire 25 | 26 | The bits of Go we avoid (and why) 27 | https://changelog.com/gotime/269 28 | changelog.com/2/2025 29 | Thu, 16 Mar 2023 16:30:00 +0000 30 | 31 | The panel discuss the parts of Go they never use. Do they avoid them because of pain in the past? Were they overused? Did they always end up getting refactoring out? Is there a preferred alternative? 32 | 33 | The panel discuss the parts of Go they never use. Do they avoid them because of pain in the past? Were they overused? Did they always end up getting refactoring out? Is there a preferred alternative?

Discuss on Changelog News

Changelog++ members support our work, get closer to the metal, and make the ads disappear. Join today!

Sponsors:

  • FastlyOur bandwidth partner. Fastly powers fast, secure, and scalable digital experiences. Move beyond your content delivery network to their powerful edge cloud platform. Learn more at fastly.com
  • Fly.ioThe home of Changelog.com — Deploy your apps and databases close to your users. In minutes you can run your Ruby, Go, Node, Deno, Python, or Elixir app (and databases!) all over the world. No ops required. Learn more at fly.io/changelog and check out the speedrun in their docs.

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

]]> 34 |
35 | full 36 | 269 37 | 38 | 1:00:24 39 | no 40 | go, golang, open source, software, development, devops, architecture, docker, kubernetes 41 | with Mat, Jon & Carl 42 | The panel discuss the parts of Go they never use. Do they avoid them because of pain in the past? Were they overused? Did they always end up getting refactoring out? Is there a preferred alternative? 43 | Changelog Media 44 | Changelog Media 45 | 46 | 47 | 48 |
49 | 50 | This will blow your docs off 51 | https://changelog.com/gotime/268 52 | changelog.com/2/1980 53 | Fri, 10 Mar 2023 16:00:00 +0000 54 | 55 | In a world where most documentation sucks, large language models write better than humans, and people won’t be bothered to type full sentences with actual punctuation. 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! 56 | 57 | In a world where most documentation sucks, large language models write better than humans, and people won’t be bothered to type full sentences with actual punctuation.

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!

Discuss on Changelog News

Changelog++ members save 2 minutes on this episode because they made the ads disappear. Join today!

Sponsors:

  • FastlyOur bandwidth partner. Fastly powers fast, secure, and scalable digital experiences. Move beyond your content delivery network to their powerful edge cloud platform. Learn more at fastly.com
  • Fly.ioThe home of Changelog.com — Deploy your apps and databases close to your users. In minutes you can run your Ruby, Go, Node, Deno, Python, or Elixir app (and databases!) all over the world. No ops required. Learn more at fly.io/changelog and check out the speedrun in their docs.
  • Changelog++ – You love our content and you want to take it to the next level by showing your support. We’ll take you closer to the metal with extended episodes, make the ads disappear, and increment your audio quality with higher bitrate mp3s. Let’s do this!

Featuring:

Show Notes:

  • a.k.a. “Doc, Stock and Two Smoking Barrels”
  • a.k.a. “Doc-a Schoen”
  • a.k.a. “Like Doc-work”
  • a.k.a. “Pull Your Docs Up”
  • a.k.a. “What’s Up, Docs?”
  • a.k.a. “A Doc-work Orange”

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

]]> 58 |
59 | full 60 | 268 61 | 62 | 1:14:59 63 | no 64 | go, golang, open source, software, development, devops, architecture, docker, kubernetes 65 | with Mark Bates & Cory LaNou 66 | In a world where most documentation sucks, large language models write better than humans, and people won’t be bothered to type full sentences with actual punctuation. 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! 67 | Changelog Media 68 | Changelog Media 69 | 70 | 71 | 72 |
73 |
74 |
-------------------------------------------------------------------------------- /internal/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Item struct { 8 | Title string 9 | Categories []string 10 | Link string 11 | Date time.Time 12 | Summary string 13 | SourceName string 14 | } 15 | 16 | type Source struct { 17 | ID int64 18 | Name string 19 | FeedURL string 20 | Priority int 21 | CreatedAt time.Time 22 | } 23 | 24 | type Article struct { 25 | ID int64 26 | SourceID int64 27 | Title string 28 | Link string 29 | Summary string 30 | PublishedAt time.Time 31 | PostedAt time.Time 32 | CreatedAt time.Time 33 | } 34 | -------------------------------------------------------------------------------- /internal/notifier/notifier.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "regexp" 10 | "strings" 11 | "time" 12 | 13 | "github.com/go-shiori/go-readability" 14 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 15 | 16 | "github.com/defer-panic/news-feed-bot/internal/botkit/markup" 17 | "github.com/defer-panic/news-feed-bot/internal/model" 18 | ) 19 | 20 | type ArticleProvider interface { 21 | AllNotPosted(ctx context.Context, since time.Time, limit uint64) ([]model.Article, error) 22 | MarkAsPosted(ctx context.Context, article model.Article) error 23 | } 24 | 25 | type Summarizer interface { 26 | Summarize(text string) (string, error) 27 | } 28 | 29 | type Notifier struct { 30 | articles ArticleProvider 31 | summarizer Summarizer 32 | bot *tgbotapi.BotAPI 33 | sendInterval time.Duration 34 | lookupTimeWindow time.Duration 35 | channelID int64 36 | } 37 | 38 | func New( 39 | articleProvider ArticleProvider, 40 | summarizer Summarizer, 41 | bot *tgbotapi.BotAPI, 42 | sendInterval time.Duration, 43 | lookupTimeWindow time.Duration, 44 | channelID int64, 45 | ) *Notifier { 46 | return &Notifier{ 47 | articles: articleProvider, 48 | summarizer: summarizer, 49 | bot: bot, 50 | sendInterval: sendInterval, 51 | lookupTimeWindow: lookupTimeWindow, 52 | channelID: channelID, 53 | } 54 | } 55 | 56 | func (n *Notifier) Start(ctx context.Context) error { 57 | ticker := time.NewTicker(n.sendInterval) 58 | defer ticker.Stop() 59 | 60 | if err := n.SelectAndSendArticle(ctx); err != nil { 61 | return err 62 | } 63 | 64 | for { 65 | select { 66 | case <-ticker.C: 67 | if err := n.SelectAndSendArticle(ctx); err != nil { 68 | return err 69 | } 70 | case <-ctx.Done(): 71 | return ctx.Err() 72 | } 73 | } 74 | } 75 | 76 | func (n *Notifier) SelectAndSendArticle(ctx context.Context) error { 77 | topOneArticles, err := n.articles.AllNotPosted(ctx, time.Now().Add(-n.lookupTimeWindow), 1) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | if len(topOneArticles) == 0 { 83 | return nil 84 | } 85 | 86 | article := topOneArticles[0] 87 | 88 | summary, err := n.extractSummary(article) 89 | if err != nil { 90 | log.Printf("[ERROR] failed to extract summary: %v", err) 91 | } 92 | 93 | if err := n.sendArticle(article, summary); err != nil { 94 | return err 95 | } 96 | 97 | return n.articles.MarkAsPosted(ctx, article) 98 | } 99 | 100 | var redundantNewLines = regexp.MustCompile(`\n{3,}`) 101 | 102 | func (n *Notifier) extractSummary(article model.Article) (string, error) { 103 | var r io.Reader 104 | 105 | if article.Summary != "" { 106 | r = strings.NewReader(article.Summary) 107 | } else { 108 | resp, err := http.Get(article.Link) 109 | if err != nil { 110 | return "", err 111 | } 112 | defer resp.Body.Close() 113 | 114 | r = resp.Body 115 | } 116 | 117 | doc, err := readability.FromReader(r, nil) 118 | if err != nil { 119 | return "", err 120 | } 121 | 122 | summary, err := n.summarizer.Summarize(cleanupText(doc.TextContent)) 123 | if err != nil { 124 | return "", err 125 | } 126 | 127 | return "\n\n" + summary, nil 128 | } 129 | 130 | func cleanupText(text string) string { 131 | return redundantNewLines.ReplaceAllString(text, "\n") 132 | } 133 | 134 | func (n *Notifier) sendArticle(article model.Article, summary string) error { 135 | const msgFormat = "*%s*%s\n\n%s" 136 | 137 | msg := tgbotapi.NewMessage(n.channelID, fmt.Sprintf( 138 | msgFormat, 139 | markup.EscapeForMarkdown(article.Title), 140 | markup.EscapeForMarkdown(summary), 141 | markup.EscapeForMarkdown(article.Link), 142 | )) 143 | msg.ParseMode = "MarkdownV2" 144 | 145 | _, err := n.bot.Send(msg) 146 | if err != nil { 147 | return err 148 | } 149 | 150 | return nil 151 | } 152 | -------------------------------------------------------------------------------- /internal/source/rss.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/SlyMarbo/rss" 8 | "github.com/samber/lo" 9 | 10 | "github.com/defer-panic/news-feed-bot/internal/model" 11 | ) 12 | 13 | type RSSSource struct { 14 | URL string 15 | SourceID int64 16 | SourceName string 17 | } 18 | 19 | func NewRSSSourceFromModel(m model.Source) RSSSource { 20 | return RSSSource{ 21 | URL: m.FeedURL, 22 | SourceID: m.ID, 23 | SourceName: m.Name, 24 | } 25 | } 26 | 27 | func (s RSSSource) Fetch(ctx context.Context) ([]model.Item, error) { 28 | feed, err := s.loadFeed(ctx, s.URL) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return lo.Map(feed.Items, func(item *rss.Item, _ int) model.Item { 34 | return model.Item{ 35 | Title: item.Title, 36 | Categories: item.Categories, 37 | Link: item.Link, 38 | Date: item.Date, 39 | SourceName: s.SourceName, 40 | Summary: strings.TrimSpace(item.Summary), 41 | } 42 | }), nil 43 | } 44 | 45 | func (s RSSSource) ID() int64 { 46 | return s.SourceID 47 | } 48 | 49 | func (s RSSSource) Name() string { 50 | return s.SourceName 51 | } 52 | 53 | func (s RSSSource) loadFeed(ctx context.Context, url string) (*rss.Feed, error) { 54 | var ( 55 | feedCh = make(chan *rss.Feed) 56 | errCh = make(chan error) 57 | ) 58 | 59 | go func() { 60 | feed, err := rss.Fetch(url) 61 | if err != nil { 62 | errCh <- err 63 | return 64 | } 65 | feedCh <- feed 66 | }() 67 | 68 | select { 69 | case <-ctx.Done(): 70 | return nil, ctx.Err() 71 | case err := <-errCh: 72 | return nil, err 73 | case feed := <-feedCh: 74 | return feed, nil 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /internal/source/rss_test.go: -------------------------------------------------------------------------------- 1 | package source_test 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | 14 | "github.com/defer-panic/news-feed-bot/internal/model" 15 | "github.com/defer-panic/news-feed-bot/internal/source" 16 | ) 17 | 18 | //go:embed testdata/feed.xml 19 | var feed []byte 20 | 21 | func TestRSSSource_Fetch(t *testing.T) { 22 | var ( 23 | ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 24 | w.Header().Add("Content-Type", "application/xml; charset=utf-8") 25 | _, _ = w.Write(feed) 26 | })) 27 | src = &source.RSSSource{ 28 | URL: ts.URL, 29 | SourceName: "dev.to", 30 | } 31 | expected = []model.Item{ 32 | { 33 | Title: "Climbing Stairs LeetCode 70", 34 | Categories: []string{"leetcode", "go", "programming", "algorithms"}, 35 | Link: "https://dev.to/digitebs/climbing-stairs-leetcode-70-4n1j", 36 | Date: parseDate(t, "Sun, 19 Mar 2023 07:04:42 +0000"), 37 | SourceName: "dev.to", 38 | }, 39 | { 40 | Title: "Find the Duplicate Number", 41 | Categories: []string{"go", "algorithms", "programming", "leetcode"}, 42 | Link: "https://dev.to/digitebs/find-the-duplicate-number-1l0", 43 | Date: parseDate(t, "Sat, 18 Mar 2023 23:34:32 +0000"), 44 | SourceName: "dev.to", 45 | }, 46 | { 47 | Title: "My Favorite Free Courses to Learn Golang in 2023", 48 | Categories: []string{"programming", "coding", "go", "development"}, 49 | Link: "https://dev.to/javinpaul/my-favorite-free-courses-to-learn-golang-in-2023-3mh6", 50 | Date: parseDate(t, "Sat, 18 Mar 2023 07:22:06 +0000"), 51 | SourceName: "dev.to", 52 | }, 53 | } 54 | ) 55 | 56 | items, err := src.Fetch(context.Background()) 57 | require.NoError(t, err) 58 | assert.Equal(t, expected, items) 59 | } 60 | 61 | func parseDate(t *testing.T, dateStr string) time.Time { 62 | date, err := time.Parse("Mon, 2 Jan 2006 15:04:05 -0700", dateStr) 63 | require.NoError(t, err) 64 | 65 | return date 66 | } 67 | -------------------------------------------------------------------------------- /internal/source/testdata/feed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | DEV Community: go 4 | The latest articles tagged 'go' on DEV Community. 5 | https://dev.to/t/go 6 | 7 | en 8 | 9 | Climbing Stairs LeetCode 70 10 | digitebs 11 | Sun, 19 Mar 2023 07:04:42 +0000 12 | https://dev.to/digitebs/climbing-stairs-leetcode-70-4n1j 13 | https://dev.to/digitebs/climbing-stairs-leetcode-70-4n1j 14 |

15 | leetcode 16 | go 17 | programming 18 | algorithms 19 |
20 | 21 | Find the Duplicate Number 22 | digitebs 23 | Sat, 18 Mar 2023 23:34:32 +0000 24 | https://dev.to/digitebs/find-the-duplicate-number-1l0 25 | https://dev.to/digitebs/find-the-duplicate-number-1l0 26 |

Find the Duplicate Number LeetCode 287 - YouTube

Floyd's Cycle Detection💡LeetCode Solutions: https://www.youtube.com/playlist?list=PLcmfkNx9uDgcawxhxLc0pQdLY3qHJ51GU📱Phone Case: https://amzn.to/3J42lFT⌨️K...

favicon youtube.com
27 | go 28 | algorithms 29 | programming 30 | leetcode 31 |
32 | 33 | My Favorite Free Courses to Learn Golang in 2023 34 | javinpaul 35 | Sat, 18 Mar 2023 07:22:06 +0000 36 | https://dev.to/javinpaul/my-favorite-free-courses-to-learn-golang-in-2023-3mh6 37 | https://dev.to/javinpaul/my-favorite-free-courses-to-learn-golang-in-2023-3mh6 38 |

Disclosure: This post includes affiliate links; I may receive compensation if you purchase products or services from the different links provided in this article.

best Free Courses to Learn Golang

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.

best course to learn Golang

5 Best Free Golang Programming courses for Beginners in 2023

Without any further delay, here I will show you the top courses to learn Go.

1. GetGoing: Introduction to Golang

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

Best free Golang tutorials and courses

Key highlights of this course

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:

  • All the basic concepts to get you started with Golang.
  • Creating an application programming interface.
  • Hosting an application in a cloud environment (Heroku cloud).
  • How to establish a connection with a database, & Backend development with Go.

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.

2. Getting Started with Go on Coursera

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

best Coursera Courses to learn Golang

Highlights of the program

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.

  • Brief understanding of the concept of arrays, slices & maps.
  • You will also learn how to gain & modify information from external files with the help of Go.

3. Functions, Methods, and Interfaces in 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

best Coursera Courses to learn Golang

Highlights of the course

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.

  • Object-oriented programming in Go.
  • How to create classes & use different properties of a class.
  • Interface for abstraction

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.

4. Learn Go Programming - Golang Tutorial for Beginners

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.

Key highlights of the course

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:

  • The fundamentals of the language its usage & origin.
  • How to establish your practice environment.
  • About data types, arrays, slices.
  • Loop statements with practical implementation rather than theoretical.
  • What are channels & Goroutines?

This one is the complete package course but, it is a paid one. This is for people who want to master Golang.

5. Go: The Complete Developer's Guide (Golang) [Paid Couse]

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.

The course's highlights

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:

  • Create tremendously contemporaneous applications using Go functions.
  • Understand the distinctions between the most prevalent data structures.
  • How to use advanced functions innovatively.

Here is the link to the course- Go: The Complete Developer's Guide (Golang)

Best online course to learn Golang

Why should you learn Golang? Is there any job benefits?

Yes, there are several job benefits for learning Golang in 2023. Some of them are:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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

39 | programming 40 | coding 41 | go 42 | development 43 |
44 |
45 |
-------------------------------------------------------------------------------- /internal/storage/article.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "time" 7 | 8 | "github.com/jmoiron/sqlx" 9 | "github.com/samber/lo" 10 | 11 | "github.com/defer-panic/news-feed-bot/internal/model" 12 | ) 13 | 14 | type ArticlePostgresStorage struct { 15 | db *sqlx.DB 16 | } 17 | 18 | func NewArticleStorage(db *sqlx.DB) *ArticlePostgresStorage { 19 | return &ArticlePostgresStorage{db: db} 20 | } 21 | 22 | func (s *ArticlePostgresStorage) Store(ctx context.Context, article model.Article) error { 23 | conn, err := s.db.Connx(ctx) 24 | if err != nil { 25 | return err 26 | } 27 | defer conn.Close() 28 | 29 | if _, err := conn.ExecContext( 30 | ctx, 31 | `INSERT INTO articles (source_id, title, link, summary, published_at) 32 | VALUES ($1, $2, $3, $4, $5) 33 | ON CONFLICT DO NOTHING;`, 34 | article.SourceID, 35 | article.Title, 36 | article.Link, 37 | article.Summary, 38 | article.PublishedAt, 39 | ); err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func (s *ArticlePostgresStorage) AllNotPosted(ctx context.Context, since time.Time, limit uint64) ([]model.Article, error) { 47 | conn, err := s.db.Connx(ctx) 48 | if err != nil { 49 | return nil, err 50 | } 51 | defer conn.Close() 52 | 53 | var articles []dbArticleWithPriority 54 | 55 | if err := conn.SelectContext( 56 | ctx, 57 | &articles, 58 | `SELECT 59 | a.id AS a_id, 60 | s.priority AS s_priority, 61 | s.id AS s_id, 62 | a.title AS a_title, 63 | a.link AS a_link, 64 | a.summary AS a_summary, 65 | a.published_at AS a_published_at, 66 | a.posted_at AS a_posted_at, 67 | a.created_at AS a_created_at 68 | FROM articles a JOIN sources s ON s.id = a.source_id 69 | WHERE a.posted_at IS NULL 70 | AND a.published_at >= $1::timestamp 71 | ORDER BY a.created_at DESC, s_priority DESC LIMIT $2;`, 72 | since.UTC().Format(time.RFC3339), 73 | limit, 74 | ); err != nil { 75 | return nil, err 76 | } 77 | 78 | return lo.Map(articles, func(article dbArticleWithPriority, _ int) model.Article { 79 | return model.Article{ 80 | ID: article.ID, 81 | SourceID: article.SourceID, 82 | Title: article.Title, 83 | Link: article.Link, 84 | Summary: article.Summary.String, 85 | PublishedAt: article.PublishedAt, 86 | CreatedAt: article.CreatedAt, 87 | } 88 | }), nil 89 | } 90 | 91 | func (s *ArticlePostgresStorage) MarkAsPosted(ctx context.Context, article model.Article) error { 92 | conn, err := s.db.Connx(ctx) 93 | if err != nil { 94 | return err 95 | } 96 | defer conn.Close() 97 | 98 | if _, err := conn.ExecContext( 99 | ctx, 100 | `UPDATE articles SET posted_at = $1::timestamp WHERE id = $2;`, 101 | time.Now().UTC().Format(time.RFC3339), 102 | article.ID, 103 | ); err != nil { 104 | return err 105 | } 106 | 107 | return nil 108 | } 109 | 110 | type dbArticleWithPriority struct { 111 | ID int64 `db:"a_id"` 112 | SourcePriority int64 `db:"s_priority"` 113 | SourceID int64 `db:"s_id"` 114 | Title string `db:"a_title"` 115 | Link string `db:"a_link"` 116 | Summary sql.NullString `db:"a_summary"` 117 | PublishedAt time.Time `db:"a_published_at"` 118 | PostedAt sql.NullTime `db:"a_posted_at"` 119 | CreatedAt time.Time `db:"a_created_at"` 120 | } 121 | -------------------------------------------------------------------------------- /internal/storage/migrations/20230318200359_create_sources.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- +goose StatementBegin 3 | CREATE TABLE sources 4 | ( 5 | id SERIAL PRIMARY KEY, 6 | name VARCHAR(255) NOT NULL, 7 | feed_url VARCHAR(255) NOT NULL, 8 | priority INT NOT NULL, 9 | created_at TIMESTAMP NOT NULL DEFAULT NOW() 10 | ); 11 | -- +goose StatementEnd 12 | 13 | -- +goose Down 14 | -- +goose StatementBegin 15 | DROP TABLE IF EXISTS sources; 16 | -- +goose StatementEnd 17 | -------------------------------------------------------------------------------- /internal/storage/migrations/20230318200520_create_articles.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- +goose StatementBegin 3 | CREATE TABLE articles 4 | ( 5 | id BIGSERIAL PRIMARY KEY, 6 | source_id BIGINT NOT NULL, 7 | title VARCHAR(255) NOT NULL, 8 | link TEXT NOT NULL UNIQUE, 9 | published_at TIMESTAMP NOT NULL, 10 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 11 | posted_at TIMESTAMP, 12 | CONSTRAINT fk_articles_source_id 13 | FOREIGN KEY (source_id) 14 | REFERENCES sources (id) 15 | ON DELETE CASCADE 16 | ); 17 | -- +goose StatementEnd 18 | 19 | -- +goose Down 20 | -- +goose StatementBegin 21 | DROP TABLE IF EXISTS articles; 22 | -- +goose StatementEnd 23 | -------------------------------------------------------------------------------- /internal/storage/migrations/20230320140458_create_articles_summary.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- +goose StatementBegin 3 | ALTER TABLE articles ADD COLUMN summary TEXT; 4 | -- +goose StatementEnd 5 | 6 | -- +goose Down 7 | -- +goose StatementBegin 8 | ALTER TABLE articles DROP COLUMN summary; 9 | -- +goose StatementEnd 10 | -------------------------------------------------------------------------------- /internal/storage/source.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/jmoiron/sqlx" 8 | "github.com/samber/lo" 9 | 10 | "github.com/defer-panic/news-feed-bot/internal/model" 11 | ) 12 | 13 | type SourcePostgresStorage struct { 14 | db *sqlx.DB 15 | } 16 | 17 | func NewSourceStorage(db *sqlx.DB) *SourcePostgresStorage { 18 | return &SourcePostgresStorage{db: db} 19 | } 20 | 21 | func (s *SourcePostgresStorage) Sources(ctx context.Context) ([]model.Source, error) { 22 | conn, err := s.db.Connx(ctx) 23 | if err != nil { 24 | return nil, err 25 | } 26 | defer conn.Close() 27 | 28 | var sources []dbSource 29 | if err := conn.SelectContext(ctx, &sources, `SELECT * FROM sources`); err != nil { 30 | return nil, err 31 | } 32 | 33 | return lo.Map(sources, func(source dbSource, _ int) model.Source { return model.Source(source) }), nil 34 | } 35 | 36 | func (s *SourcePostgresStorage) SourceByID(ctx context.Context, id int64) (*model.Source, error) { 37 | conn, err := s.db.Connx(ctx) 38 | if err != nil { 39 | return nil, err 40 | } 41 | defer conn.Close() 42 | 43 | var source dbSource 44 | if err := conn.GetContext(ctx, &source, `SELECT * FROM sources WHERE id = $1`, id); err != nil { 45 | return nil, err 46 | } 47 | 48 | return (*model.Source)(&source), nil 49 | } 50 | 51 | func (s *SourcePostgresStorage) Add(ctx context.Context, source model.Source) (int64, error) { 52 | conn, err := s.db.Connx(ctx) 53 | if err != nil { 54 | return 0, err 55 | } 56 | defer conn.Close() 57 | 58 | var id int64 59 | 60 | row := conn.QueryRowxContext( 61 | ctx, 62 | `INSERT INTO sources (name, feed_url, priority) 63 | VALUES ($1, $2, $3) RETURNING id;`, 64 | source.Name, source.FeedURL, source.Priority, 65 | ) 66 | 67 | if err := row.Err(); err != nil { 68 | return 0, err 69 | } 70 | 71 | if err := row.Scan(&id); err != nil { 72 | return 0, err 73 | } 74 | 75 | return id, nil 76 | } 77 | 78 | func (s *SourcePostgresStorage) SetPriority(ctx context.Context, id int64, priority int) error { 79 | conn, err := s.db.Connx(ctx) 80 | if err != nil { 81 | return err 82 | } 83 | defer conn.Close() 84 | 85 | _, err = conn.ExecContext(ctx, `UPDATE sources SET priority = $1 WHERE id = $2`, priority, id) 86 | 87 | return err 88 | } 89 | 90 | func (s *SourcePostgresStorage) Delete(ctx context.Context, id int64) error { 91 | conn, err := s.db.Connx(ctx) 92 | if err != nil { 93 | return err 94 | } 95 | defer conn.Close() 96 | 97 | if _, err := conn.ExecContext(ctx, `DELETE FROM sources WHERE id = $1`, id); err != nil { 98 | return err 99 | } 100 | 101 | return nil 102 | } 103 | 104 | type dbSource struct { 105 | ID int64 `db:"id"` 106 | Name string `db:"name"` 107 | FeedURL string `db:"feed_url"` 108 | Priority int `db:"priority"` 109 | CreatedAt time.Time `db:"created_at"` 110 | } 111 | -------------------------------------------------------------------------------- /internal/summary/openai.go: -------------------------------------------------------------------------------- 1 | package summary 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/sashabaranov/go-openai" 13 | ) 14 | 15 | type OpenAISummarizer struct { 16 | client *openai.Client 17 | prompt string 18 | model string 19 | enabled bool 20 | mu sync.Mutex 21 | } 22 | 23 | func NewOpenAISummarizer(apiKey, model, prompt string) *OpenAISummarizer { 24 | s := &OpenAISummarizer{ 25 | client: openai.NewClient(apiKey), 26 | prompt: prompt, 27 | model: model, 28 | } 29 | 30 | log.Printf("openai summarizer is enabled: %v", apiKey != "") 31 | 32 | if apiKey != "" { 33 | s.enabled = true 34 | } 35 | 36 | return s 37 | } 38 | 39 | func (s *OpenAISummarizer) Summarize(text string) (string, error) { 40 | s.mu.Lock() 41 | defer s.mu.Unlock() 42 | 43 | if !s.enabled { 44 | return "", fmt.Errorf("openai summarizer is disabled") 45 | } 46 | 47 | request := openai.ChatCompletionRequest{ 48 | Model: s.model, 49 | Messages: []openai.ChatCompletionMessage{ 50 | { 51 | Role: openai.ChatMessageRoleSystem, 52 | Content: s.prompt, 53 | }, 54 | { 55 | Role: openai.ChatMessageRoleUser, 56 | Content: text, 57 | }, 58 | }, 59 | MaxTokens: 1024, 60 | Temperature: 1, 61 | TopP: 1, 62 | } 63 | 64 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) 65 | defer cancel() 66 | 67 | resp, err := s.client.CreateChatCompletion(ctx, request) 68 | if err != nil { 69 | return "", err 70 | } 71 | 72 | if len(resp.Choices) == 0 { 73 | return "", errors.New("no choices in openai response") 74 | } 75 | 76 | rawSummary := strings.TrimSpace(resp.Choices[0].Message.Content) 77 | if strings.HasSuffix(rawSummary, ".") { 78 | return rawSummary, nil 79 | } 80 | 81 | // cut all after the last ".": 82 | sentences := strings.Split(rawSummary, ".") 83 | 84 | return strings.Join(sentences[:len(sentences)-1], ".") + ".", nil 85 | } 86 | --------------------------------------------------------------------------------