unknown 3 týždňov pred
rodič
commit
df07750142
100 zmenil súbory, kde vykonal 7426 pridanie a 0 odobranie
  1. 4 0
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/dockerfile
  2. 3 0
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/go.mod
  3. 22 0
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/main.go
  4. 75 0
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/main_test.go
  5. 67 0
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/readme.md
  6. 1 0
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/project/file.txt
  7. BIN
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/project/gopher.png
  8. 0 0
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/a_lorem/dolor.txt
  9. BIN
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/a_lorem/gopher.png
  10. BIN
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/a_lorem/ipsum/gopher.png
  11. 1 0
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/css/body.css
  12. 0 0
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/empty.txt
  13. 4 0
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/html/index.html
  14. 1 0
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/js/site.js
  15. 0 0
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/z_lorem/dolor.txt
  16. BIN
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/z_lorem/gopher.png
  17. BIN
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/z_lorem/ipsum/gopher.png
  18. 0 0
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/zline/empty.txt
  19. 0 0
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/zline/lorem/dolor.txt
  20. BIN
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/zline/lorem/gopher.png
  21. BIN
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/zline/lorem/ipsum/gopher.png
  22. 0 0
      courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/zzfile.txt
  23. BIN
      courses/golang_web/golang_web_services_2024-04-26/1/Practical_Go__Real_world_advice.pdf
  24. 25 0
      courses/golang_web/golang_web_services_2024-04-26/1/basics/array.go
  25. 34 0
      courses/golang_web/golang_web_services_2024-04-26/1/basics/const.go
  26. 64 0
      courses/golang_web/golang_web_services_2024-04-26/1/basics/control.go
  27. 66 0
      courses/golang_web/golang_web_services_2024-04-26/1/basics/loop.go
  28. 35 0
      courses/golang_web/golang_web_services_2024-04-26/1/basics/map.go
  29. 20 0
      courses/golang_web/golang_web_services_2024-04-26/1/basics/pointers.go
  30. 40 0
      courses/golang_web/golang_web_services_2024-04-26/1/basics/slice_1.go
  31. 43 0
      courses/golang_web/golang_web_services_2024-04-26/1/basics/slice_2.go
  32. 63 0
      courses/golang_web/golang_web_services_2024-04-26/1/basics/strings.go
  33. 16 0
      courses/golang_web/golang_web_services_2024-04-26/1/basics/types.go
  34. 46 0
      courses/golang_web/golang_web_services_2024-04-26/1/basics/vars_1.go
  35. 43 0
      courses/golang_web/golang_web_services_2024-04-26/1/basics/vars_2.go
  36. 16 0
      courses/golang_web/golang_web_services_2024-04-26/1/functions/defer.go
  37. 39 0
      courses/golang_web/golang_web_services_2024-04-26/1/functions/firstclass.go
  38. 58 0
      courses/golang_web/golang_web_services_2024-04-26/1/functions/functions.go
  39. 27 0
      courses/golang_web/golang_web_services_2024-04-26/1/functions/recover.go
  40. 34 0
      courses/golang_web/golang_web_services_2024-04-26/1/interfaces/basic.go
  41. 97 0
      courses/golang_web/golang_web_services_2024-04-26/1/interfaces/cast.go
  42. 58 0
      courses/golang_web/golang_web_services_2024-04-26/1/interfaces/embed.go
  43. 30 0
      courses/golang_web/golang_web_services_2024-04-26/1/interfaces/empty_1.go
  44. 58 0
      courses/golang_web/golang_web_services_2024-04-26/1/interfaces/empty_2.go
  45. 84 0
      courses/golang_web/golang_web_services_2024-04-26/1/interfaces/many.go
  46. 53 0
      courses/golang_web/golang_web_services_2024-04-26/1/readings_1.md
  47. 64 0
      courses/golang_web/golang_web_services_2024-04-26/1/structs/methods.go
  48. 38 0
      courses/golang_web/golang_web_services_2024-04-26/1/structs/structs.go
  49. 8 0
      courses/golang_web/golang_web_services_2024-04-26/1/uniq/basic/data.txt
  50. 4 0
      courses/golang_web/golang_web_services_2024-04-26/1/uniq/basic/data_bad.txt
  51. 5 0
      courses/golang_web/golang_web_services_2024-04-26/1/uniq/basic/data_map.txt
  52. 23 0
      courses/golang_web/golang_web_services_2024-04-26/1/uniq/basic/main.go
  53. 8 0
      courses/golang_web/golang_web_services_2024-04-26/1/uniq/with_tests/data.txt
  54. 4 0
      courses/golang_web/golang_web_services_2024-04-26/1/uniq/with_tests/data_bad.txt
  55. 5 0
      courses/golang_web/golang_web_services_2024-04-26/1/uniq/with_tests/data_map.txt
  56. 33 0
      courses/golang_web/golang_web_services_2024-04-26/1/uniq/with_tests/main.go
  57. 46 0
      courses/golang_web/golang_web_services_2024-04-26/1/uniq/with_tests/main_test.go
  58. 13 0
      courses/golang_web/golang_web_services_2024-04-26/1/visibility/dir.txt
  59. 16 0
      courses/golang_web/golang_web_services_2024-04-26/1/visibility/main.go
  60. 21 0
      courses/golang_web/golang_web_services_2024-04-26/1/visibility/person/func.go
  61. 16 0
      courses/golang_web/golang_web_services_2024-04-26/1/visibility/person/person.go
  62. 2 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/Makefile
  63. 14 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/go.mod
  64. 6 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/go.sum
  65. 3 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/.gitignore
  66. 6 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/.travis.yml
  67. 21 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/LICENSE.txt
  68. 121 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/README.md
  69. 967 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/bot.go
  70. 1267 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/configs.go
  71. 5 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/go.mod
  72. 2 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/go.sum
  73. 749 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/helpers.go
  74. 27 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/log.go
  75. 315 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/passport.go
  76. 819 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/types.go
  77. 45 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/readme.md
  78. 31 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/taskbot.go
  79. 395 0
      courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/taskbot_test.go
  80. 12 0
      courses/golang_web/golang_web_services_2024-04-26/10/oauth/go.mod
  81. 23 0
      courses/golang_web/golang_web_services_2024-04-26/10/oauth/go.sum
  82. 105 0
      courses/golang_web/golang_web_services_2024-04-26/10/oauth/main.go
  83. 26 0
      courses/golang_web/golang_web_services_2024-04-26/10/oauth/notes.txt
  84. 13 0
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/Makefile
  85. 45 0
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/_mysql/db_init.sql
  86. 98 0
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/crypt_token.go
  87. 18 0
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/docker-compose.yaml
  88. BIN
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/favicon.ico
  89. 19 0
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/go.mod
  90. 33 0
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/go.sum
  91. 152 0
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/handlers.go
  92. 54 0
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/hash_token.go
  93. 14 0
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/index.go
  94. 56 0
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/jwt_token.go
  95. 79 0
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/main.go
  96. 14 0
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/note.txt
  97. 91 0
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/photos.go
  98. 62 0
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/session_common.go
  99. 87 0
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/session_db.go
  100. 99 0
      courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/session_jwt.go

+ 4 - 0
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/dockerfile

@@ -0,0 +1,4 @@
+# docker build -t golang_hw1_tree .
+FROM golang:1.21
+COPY . .
+RUN go test -v

+ 3 - 0
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/go.mod

@@ -0,0 +1,3 @@
+module hw
+
+go 1.17

+ 22 - 0
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/main.go

@@ -0,0 +1,22 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+func main() {
+	out := os.Stdout
+	if !(len(os.Args) == 2 || len(os.Args) == 3) {
+		panic("usage go run main.go . [-f]")
+	}
+	path := os.Args[1]
+	printFiles := len(os.Args) == 3 && os.Args[2] == "-f"
+	err := dirTree(out, path, printFiles)
+	if err != nil {
+		panic(err.Error())
+	}
+}

+ 75 - 0
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/main_test.go

@@ -0,0 +1,75 @@
+package main
+
+import (
+	"bytes"
+	"testing"
+)
+
+const testFullResult = `├───project
+│	├───file.txt (19b)
+│	└───gopher.png (70372b)
+├───static
+│	├───a_lorem
+│	│	├───dolor.txt (empty)
+│	│	├───gopher.png (70372b)
+│	│	└───ipsum
+│	│		└───gopher.png (70372b)
+│	├───css
+│	│	└───body.css (28b)
+│	├───empty.txt (empty)
+│	├───html
+│	│	└───index.html (57b)
+│	├───js
+│	│	└───site.js (10b)
+│	└───z_lorem
+│		├───dolor.txt (empty)
+│		├───gopher.png (70372b)
+│		└───ipsum
+│			└───gopher.png (70372b)
+├───zline
+│	├───empty.txt (empty)
+│	└───lorem
+│		├───dolor.txt (empty)
+│		├───gopher.png (70372b)
+│		└───ipsum
+│			└───gopher.png (70372b)
+└───zzfile.txt (empty)
+`
+
+func TestTreeFull(t *testing.T) {
+	out := new(bytes.Buffer)
+	err := dirTree(out, "testdata", true)
+	if err != nil {
+		t.Errorf("test for OK Failed - error")
+	}
+	result := out.String()
+	if result != testFullResult {
+		t.Errorf("test for OK Failed - results not match\nGot:\n%v\nExpected:\n%v", result, testFullResult)
+	}
+}
+
+const testDirResult = `├───project
+├───static
+│	├───a_lorem
+│	│	└───ipsum
+│	├───css
+│	├───html
+│	├───js
+│	└───z_lorem
+│		└───ipsum
+└───zline
+	└───lorem
+		└───ipsum
+`
+
+func TestTreeDir(t *testing.T) {
+	out := new(bytes.Buffer)
+	err := dirTree(out, "testdata", false)
+	if err != nil {
+		t.Errorf("test for OK Failed - error")
+	}
+	result := out.String()
+	if result != testDirResult {
+		t.Errorf("test for OK Failed - results not match\nGot:\n%v\nExpected:\n%v", result, testDirResult)
+	}
+}

+ 67 - 0
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/readme.md

@@ -0,0 +1,67 @@
+Утилита tree.
+
+Выводит дерево каталогов и файлов (если указана опция -f).
+
+Необходимо реализовать функцию `dirTree` внутри `main.go`. Начать можно с https://golang.org/pkg/os/#Open и дальше смотреть какие методы есть у результата.
+
+Код писать в файле main.go
+
+Запускать тесты через `go test -v` находясь в папке c заданием. После запуска вы должны увидеть такой результат:
+
+```
+$ go test -v
+=== RUN   TestTreeFull
+--- PASS: TestTreeFull (0.00s)
+=== RUN   TestTreeDir
+--- PASS: TestTreeDir (0.00s)
+PASS
+ok      coursera/homework/tree     0.127s
+```
+
+```
+go run main.go . -f
+├───main.go (1881b)
+├───main_test.go (1318b)
+└───testdata
+	├───project
+	│	├───file.txt (19b)
+	│	└───gopher.png (70372b)
+	├───static
+	│	├───css
+	│	│	└───body.css (28b)
+	│	├───html
+	│	│	└───index.html (57b)
+	│	└───js
+	│		└───site.js (10b)
+	├───zline
+	│	└───empty.txt (empty)
+	└───zzfile.txt (empty)
+go run main.go .
+└───testdata
+	├───project
+	├───static
+	│	├───css
+	│	├───html
+	│	└───js
+	└───zline
+```
+
+Замечания:
+* Перенос строки - unix-style ( \n )
+* Отступы - символ графики + символ табуляции ( \t )
+* Для расчета символа графики в отступах подумайте про последний элемент и префикс предыдущих уровней. Там довольно простое условие. Хорошо помогает проговорить вслух то что вы видите на экране.
+* Если вы пользуетесь windows - помните, что там и в linux разделители директорий различаются - используйте лучше `string(os.PathSeparator)`
+* Рекурсивный алгоритм проще всего. Но можно реализовать и не-рекурсивно
+* Вы можете реализовать любые нужные вам функции, вы не ограничены в единственной dirTree. Если вам нужно больше аргументов - создайте другую функцию и вызывайте её рекурсивно. dirTree в этом случае может быть только входной точкой.
+* Символы графики лучше копируйте не из текста задания ( который вы читаете сейчас ), а из исходного кода теста ( main_test.go )
+* Результаты ( список папок-файлов ) должны быть отсортированы по алфавиту. Т.е. у вас должен быть код который отсортирует уровень. Смотрите для этого пакет sort. Это самая частая причина непрохождения тестов. Тесты запускаются в среде linux. В задании есть докер-файл для тестов ровно в тех же условиях, он сразу выявит все проблемы.
+* У вас может быть соблазн использовать глобальные переменные, но вариант с рекурсией проще получается без них, а в не-рекурсивном варианте они вообще не нужны
+* сигнатуру функции dirTree ( количество параметров ) менять нельзя, тесты на сервере не пройдут
+* если вы столкнётесь с несовместимостью os.File и bytes.Buffer - смотрите видео "Написание тестов для программы уникализации", uniq/wint_tests в коде в уроку, а так же ссылку на хабр ниже
+* На MacOS может быть проблема с системным файлом `.DS_Store` - его можно просто игнорировать
+
+Материалы в помощь:
+* https://habrahabr.ru/post/306914/ - пакет io
+* https://golang.org/pkg/sort/
+* https://golang.org/pkg/io/
+* https://golang.org/pkg/io/ioutil/

+ 1 - 0
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/project/file.txt

@@ -0,0 +1 @@
+some text data here

BIN
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/project/gopher.png


+ 0 - 0
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/a_lorem/dolor.txt


BIN
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/a_lorem/gopher.png


BIN
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/a_lorem/ipsum/gopher.png


+ 1 - 0
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/css/body.css

@@ -0,0 +1 @@
+body {background-color:red;}

+ 0 - 0
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/empty.txt


+ 4 - 0
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/html/index.html

@@ -0,0 +1,4 @@
+<!doctype html>
+<html>
+	<body>Hello World</body>
+</html>

+ 1 - 0
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/js/site.js

@@ -0,0 +1 @@
+var a = 3;

+ 0 - 0
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/z_lorem/dolor.txt


BIN
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/z_lorem/gopher.png


BIN
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/static/z_lorem/ipsum/gopher.png


+ 0 - 0
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/zline/empty.txt


+ 0 - 0
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/zline/lorem/dolor.txt


BIN
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/zline/lorem/gopher.png


BIN
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/zline/lorem/ipsum/gopher.png


+ 0 - 0
courses/golang_web/golang_web_services_2024-04-26/1/99_hw/tree/testdata/zzfile.txt


BIN
courses/golang_web/golang_web_services_2024-04-26/1/Practical_Go__Real_world_advice.pdf


+ 25 - 0
courses/golang_web/golang_web_services_2024-04-26/1/basics/array.go

@@ -0,0 +1,25 @@
+package main
+
+import "fmt"
+
+func main() {
+	// размер массива является частью его типа
+
+	// инициализация значениями по-умолчанию
+	var a1 [3]int // [0,0,0]
+	fmt.Println("a1 short", a1)
+	fmt.Printf("a1 short %v\n", a1)
+	fmt.Printf("a1 full %#v\n", a1)
+
+	const size = 2
+	var a2 [2 * size]bool // [false,false,false,false]
+	fmt.Println("a2", a2)
+
+	// определение размера при объявлении
+	a3 := [...]int{1, 2, 3}
+	fmt.Println("a2", a3)
+
+	// проверка при компиляции или при выполнении
+	// invalid array index 4 (out of bounds for 3-element array)
+	// a3[idx] = 12
+}

+ 34 - 0
courses/golang_web/golang_web_services_2024-04-26/1/basics/const.go

@@ -0,0 +1,34 @@
+package main
+
+import "fmt"
+
+const pi = 3.141
+const (
+	hello = "Привет"
+	e     = 2.718
+)
+const (
+	zero = iota
+	_    // пустая переменная, пропуск iota
+	two
+	three // = 3
+)
+const (
+	_         = iota             // пропускаем первое значение
+	KB uint64 = 1 << (10 * iota) // 1 << (10 * 1) = 1024
+	MB                           // 1 << (10 * 2) = 1048576
+)
+const (
+	// нетипизированная константа
+	year = 2017
+	// типизированная константа
+	yearTyped int = 2017
+)
+
+func main() {
+	var month int32 = 13
+	fmt.Println(month + year)
+
+	// month + yearTyped (mismatched types int32 and int)
+	// fmt.Println( month + yearTyped )
+}

+ 64 - 0
courses/golang_web/golang_web_services_2024-04-26/1/basics/control.go

@@ -0,0 +1,64 @@
+package main
+
+import "fmt"
+
+func main() {
+	// простое условие
+	boolVal := true
+	if boolVal {
+		fmt.Println("boolVal is true")
+	}
+
+	mapVal := map[string]string{"name": "rvasily"}
+	// условие с блоком инициализации
+	if keyValue, keyExist := mapVal["name"]; keyExist {
+		fmt.Println("name =", keyValue)
+	}
+	// получаем только признак сущестования ключа
+	if _, keyExist := mapVal["name"]; keyExist {
+		fmt.Println("key 'name' exist")
+	}
+
+	cond := 1
+	// множественные if else
+	if cond == 1 {
+		fmt.Println("cond is 1")
+	} else if cond == 2 {
+		fmt.Println("cond is 2")
+	}
+
+	// switch по 1 переменной
+	strVal := "name"
+	switch strVal {
+	case "name":
+		fallthrough
+	case "test", "lastName":
+		// some work
+	default:
+		// some work
+	}
+
+	// switch как замена многим ifelse
+	var val1, val2 = 2, 2
+	switch {
+	case val1 > 1 || val2 < 11:
+		fmt.Println("first block")
+	case val2 > 10:
+		fmt.Println("second block")
+	}
+
+	// выход из цикла, находясь внутри switch
+Loop:
+	for key, val := range mapVal {
+		println("switch in loop", key, val)
+		switch {
+		case key == "lastName":
+			break
+			println("dont pront this")
+		case key == "firstName" && val == "Vasily":
+			println("switch - break loop here")
+			break Loop
+		}
+	} // конец for
+
+}

+ 66 - 0
courses/golang_web/golang_web_services_2024-04-26/1/basics/loop.go

@@ -0,0 +1,66 @@
+package main
+
+import "fmt"
+
+func main() {
+	// цикл без условия, while(true) OR for(;;;)
+	for {
+		fmt.Println("loop iteration")
+		break
+	}
+
+	// цикл без условия, while(isRun)
+	isRun := true
+	for isRun {
+		fmt.Println("loop iteration with condition")
+		isRun = false
+	}
+
+	// цикл с условие и блоком инициализации
+	for i := 0; i < 2; i++ {
+		fmt.Println("loop iteration", i)
+		if i == 1 {
+			continue
+		}
+	}
+
+	// операции по slice
+	sl := []int{1, 2, 3}
+	idx := 0
+
+	for idx < len(sl) {
+		fmt.Println("while-stype loop, idx:", idx, "value:", sl[idx])
+		idx++
+	}
+
+	for i := 0; i < len(sl); i++ {
+		fmt.Println("c-style loop", i, sl[i])
+	}
+	for idx := range sl {
+		fmt.Println("range slice by index", sl[idx])
+	}
+	for idx, val := range sl {
+		fmt.Println("range slice by idx-value", idx, val)
+	}
+
+	// операции по map
+	profile := map[int]string{1: "Vasily", 2: "Romanov"}
+
+	for key := range profile {
+		fmt.Println("range map by key", key)
+	}
+
+	for key, val := range profile {
+		fmt.Println("range map by key-val", key, val)
+	}
+
+	for _, val := range profile {
+		fmt.Println("range map by val", val)
+	}
+
+	str := "Привет, Мир!"
+	for pos, char := range str {
+		fmt.Printf("%#U at pos %d\n", char, pos)
+	}
+
+}

+ 35 - 0
courses/golang_web/golang_web_services_2024-04-26/1/basics/map.go

@@ -0,0 +1,35 @@
+package main
+
+import "fmt"
+
+func main() {
+	// инициализация при создании
+	var user map[string]string = map[string]string{
+		"name":     "Vasily",
+		"lastName": "Romanov",
+	}
+
+	// сразу с нужной ёмкостью
+	profile := make(map[string]string, 10)
+
+	// количество элементов
+	mapLength := len(user)
+
+	fmt.Printf("%d %+v\n", mapLength, profile)
+
+	// если ключа нет - вернёт значение по умолчанию для типа
+	mName := user["middleName"]
+	fmt.Println("mName:", mName)
+
+	// проверка на существование ключа
+	mName, mNameExist := user["middleName"]
+	fmt.Println("mName:", mName, "mNameExist:", mNameExist)
+
+	// пустая переменная - только проверяем что ключ есть
+	_, mNameExist2 := user["middleName"]
+	fmt.Println("mNameExist2", mNameExist2)
+
+	// удаление ключа
+	delete(user, "lastName")
+	fmt.Printf("%#v\n", user)
+}

+ 20 - 0
courses/golang_web/golang_web_services_2024-04-26/1/basics/pointers.go

@@ -0,0 +1,20 @@
+package main
+
+import "fmt"
+
+func main() {
+	a := 2
+	b := &a
+	*b = 3  // a = 3
+	c := &a // новый указатель на переменную a
+
+	// получение указателя на переменнут типа int
+	// инициализировано значением по-умолчанию
+	d := new(int)
+	*d = 12
+	*c = *d // c = 12 -> a = 12
+	*d = 13 // c и a не изменились
+
+	c = d   // теперь с указывает туда же, куда d
+	*c = 14 // с = 14 -> d = 14, a = 12
+}

+ 40 - 0
courses/golang_web/golang_web_services_2024-04-26/1/basics/slice_1.go

@@ -0,0 +1,40 @@
+package main
+
+import "fmt"
+
+func main() {
+	// создание
+	var buf0 []int             // len=0, cap=0
+	buf1 := []int{}            // len=0, cap=0
+	buf2 := []int{42}          // len=1, cap=1
+	buf3 := make([]int, 0)     // len=0, cap=0
+	buf4 := make([]int, 5)     // len=5, cap=5
+	buf5 := make([]int, 5, 10) // len=5, cap=10
+
+	println(buf0, buf1, buf2, buf3, buf4, buf5)
+
+	// обращение к элементам
+	someInt := buf2[0]
+
+	// ошибка при выполнении
+	// panic: runtime error: index out of range
+	// someOtherInt := buf2[1]
+
+	fmt.Println(someInt)
+
+	// добавление элементов
+	var buf []int            // len=0, cap=0
+	buf = append(buf, 9, 10) // len=2, cap=2
+	buf = append(buf, 12)    // len=3, cap=4
+
+	// добавление друго слайса
+	otherBuf := make([]int, 3)     // [0,0,0]
+	buf = append(buf, otherBuf...) // len=6, cap=8
+
+	fmt.Println(buf, otherBuf)
+
+	// просмотр информации о слайсе
+	var bufLen, bufCap int = len(buf), cap(buf)
+
+	fmt.Println(bufLen, bufCap)
+}

+ 43 - 0
courses/golang_web/golang_web_services_2024-04-26/1/basics/slice_2.go

@@ -0,0 +1,43 @@
+package main
+
+import "fmt"
+
+func main() {
+	buf := []int{1, 2, 3, 4, 5}
+	fmt.Println(buf)
+
+	// получение среза, указывающего на ту же память
+	sl1 := buf[1:4] // [2, 3, 4]
+	sl2 := buf[:2]  // [1, 2]
+	sl3 := buf[2:]  // [3, 4, 5]
+	fmt.Println(sl1, sl2, sl3)
+
+	newBuf := buf[:] // [1, 2, 3, 4, 5]
+	// buf = [9, 2, 3, 4, 5], т.к. та же память
+	newBuf[0] = 9
+
+	// newBuf теперь указывает на другие данные
+	newBuf = append(newBuf, 6)
+
+	// buf    = [9, 2, 3, 4, 5], не изменился
+	// newBuf = [1, 2, 3, 4, 5, 6], изменился
+	newBuf[0] = 1
+	fmt.Println("buf", buf)
+	fmt.Println("newBuf", newBuf)
+
+	// копирование одного слайса в другой
+	var emptyBuf []int // len=0, cap=0
+	// неправильно - скопирует меньшее (по len) из 2-х слайсов
+	copied := copy(emptyBuf, buf) // copied = 0
+	fmt.Println(copied, emptyBuf)
+
+	// правильно
+	newBuf = make([]int, len(buf), len(buf))
+	copy(newBuf, buf)
+	fmt.Println(newBuf)
+
+	// можно копировать в часть существующего слайса
+	ints := []int{1, 2, 3, 4}
+	copy(ints[1:3], []int{5, 6}) // ints = [1, 5, 6, 4]
+	fmt.Println(ints)
+}

+ 63 - 0
courses/golang_web/golang_web_services_2024-04-26/1/basics/strings.go

@@ -0,0 +1,63 @@
+package main
+
+import (
+	"fmt"
+	"unicode/utf8"
+)
+
+func main() {
+	// пустая строка по-умолчанию
+	var str string
+
+	// со спец символами
+	var hello string = "Привет\n\t"
+
+	// без спец символов
+	var world string = `Мир\n\t`
+
+	fmt.Println("str", str)
+	fmt.Println("hello", hello)
+	fmt.Println("world", world)
+
+	// UTF-8 из коробки
+	var helloWorld = "Привет, Мир!"
+	hi := "你好,世界"
+
+	fmt.Println("helloWorld", helloWorld)
+	fmt.Println("hi", hi)
+
+	// одинарные кавычки для байт (uint8)
+	var rawBinary byte = '\x27'
+
+	// rune (uint32) для UTF-8 символов
+	var someChinese rune = '茶'
+
+	fmt.Println(rawBinary, someChinese)
+
+	helloWorld = "Привет Мир"
+	// конкатенация строк
+	andGoodMorning := helloWorld + " и доброе утро!"
+
+	fmt.Println(helloWorld, andGoodMorning)
+
+	// строки неизменяемы
+	// cannot assign to helloWorld[0]
+	// helloWorld[0] = 72
+
+	// получение длины строки
+	byteLen := len(helloWorld)                    // 19 байт
+	symbols := utf8.RuneCountInString(helloWorld) // 10 рун
+
+	fmt.Println(byteLen, symbols)
+
+	// получение подстроки, в байтах, не символах!
+	hello = helloWorld[:12] // Привет, 0-11 байты
+	H := helloWorld[0]      // byte, 72, не "П"
+	fmt.Println(H)
+
+	// конвертация в слайс байт и обратно
+	byteString := []byte(helloWorld)
+	helloWorld = string(byteString)
+
+	fmt.Println(byteString, helloWorld)
+}

+ 16 - 0
courses/golang_web/golang_web_services_2024-04-26/1/basics/types.go

@@ -0,0 +1,16 @@
+package main 
+
+type UserID int
+
+func main() {
+	idx := 1
+	var uid UserID = 42
+
+	// даже если базовый тип одинаковый, разные типы несовместимы
+	// cannot use uid (type UserID) as type int64 in assignment
+	// myID := idx
+
+	myID := UserID(idx)
+	
+	println(uid, myID)
+}

+ 46 - 0
courses/golang_web/golang_web_services_2024-04-26/1/basics/vars_1.go

@@ -0,0 +1,46 @@
+package main
+
+import "fmt"
+
+func main() {
+	// значение по умолчанию
+	var num0 int
+
+	// значение при инициализации
+	var num1 int = 1
+
+	// пропуск типа
+	var num2 = 20
+	fmt.Println(num0, num1, num2)
+
+	// короткое объявление переменной
+	num := 30
+	// только для новых переменных
+	// no new variables on left side of :=
+	// num := 31
+
+	num += 1
+	fmt.Println("+=", num)
+
+	// ++num нету
+	num++
+	fmt.Println("++", num)
+
+	// camelCase - принятый стиль
+	userIndex := 10
+	// under_score - не принято
+	user_index := 10
+	fmt.Println(userIndex, user_index)
+
+	// объявление нескольких переменных
+	var weight, height int = 10, 20
+
+	// присваивание в существующие переменные
+	weight, height = 11, 21
+
+	// короткое присваивание
+	// хотя-бы одна переменная должна быть новой!
+	weight, age := 12, 22
+
+	fmt.Println(weight, height, age)
+}

+ 43 - 0
courses/golang_web/golang_web_services_2024-04-26/1/basics/vars_2.go

@@ -0,0 +1,43 @@
+package main
+
+import "fmt"
+
+func main() {
+	// int - платформозависимый тип, 32/64
+	var i int = 10
+
+	// автоматически выбранный int
+	var autoInt = -10
+
+	// int8, int16, int32, int64
+	var bigInt int64 = 1<<32 - 1
+
+	// платформозависимый тип, 32/64
+	var unsignedInt uint = 100500
+
+	// uint8, unit16, uint32, unit64
+	var unsignedBigInt uint64 = 1<<64 - 1
+
+	fmt.Println(i, autoInt, bigInt, unsignedInt, unsignedBigInt)
+
+	// float32, float64
+	var pi float32 = 3.141
+	var e = 2.718
+	goldenRatio := 1.618
+
+	fmt.Println(pi, e, goldenRatio)
+
+	// bool
+	var b bool // false по-умолчанию
+	var isOk bool = true
+	var success = true
+	cond := true
+
+	fmt.Println(b, isOk, success, cond)
+
+	// complex64, complex128
+	var c complex128 = -1.1 + 7.12i
+	c2 := -1.1 + 7.12i
+
+	fmt.Println(c, c2)
+}

+ 16 - 0
courses/golang_web/golang_web_services_2024-04-26/1/functions/defer.go

@@ -0,0 +1,16 @@
+package main
+
+import "fmt"
+
+func getSomeVars() string {
+	fmt.Println("getSomeVars execution")
+	return "getSomeVars result"
+}
+
+func main() {
+	defer fmt.Println("After work")
+	defer func() {
+		fmt.Println(getSomeVars())
+	}()
+	fmt.Println("Some userful work")
+}

+ 39 - 0
courses/golang_web/golang_web_services_2024-04-26/1/functions/firstclass.go

@@ -0,0 +1,39 @@
+package main
+
+import "fmt"
+
+// обычная функция
+func doNothing() {
+	fmt.Println("i'm regular function")
+}
+
+func main() {
+	// анонимная функция
+	func(in string) {
+		fmt.Println("anon func out:", in)
+	}("nobody")
+
+	// присванивание анонимной функции в переменную
+	printer := func(in string) {
+		fmt.Println("printer outs:", in)
+	}
+	printer("as variable")
+
+	// определяем тип функции
+	type strFuncType func(string)
+
+	// функция принимает коллбек
+	worker := func(callback strFuncType) {
+		callback("as callback")
+	}
+	worker(printer)
+
+	// функиция возвращает замыкание
+	prefixer := func(prefix string) strFuncType {
+		return func(in string) {
+			fmt.Printf("[%s] %s\n", prefix, in)
+		}
+	}
+	successLogger := prefixer("SUCCESS")
+	successLogger("expected behaviour")
+}

+ 58 - 0
courses/golang_web/golang_web_services_2024-04-26/1/functions/functions.go

@@ -0,0 +1,58 @@
+package main
+
+import "fmt"
+
+// обычное объявление
+func singleIn(in int) int {
+	return in
+}
+
+// много параметров
+func multIn(a, b int, c int) int {
+	return a + b + c
+}
+
+// именованный результат
+func namedReturn() (out int) {
+	out = 2
+	return
+}
+
+// несколько результатов
+func multipleReturn(in int) (int, error) {
+	if in > 2 {
+		return 0, fmt.Errorf("some error happend")
+	}
+	return in, nil
+}
+
+// несколько именованных результатов
+func multipleNamedReturn(ok bool) (rez int, err error) {
+	rez = 1
+	if ok {
+		err = fmt.Errorf("some error happend")
+		// аналогично return rez, err
+		return 3, fmt.Errorf("some error happend")
+		return
+	}
+	rez = 2
+	return
+}
+
+// не фиксированное количество параметров
+func sum(in ...int) (result int) {
+	fmt.Printf("in := %#v \n", in)
+	for _, val := range in {
+		result += val
+	}
+	return
+}
+
+func main() {
+	// fmt.Println(multipleNamedReturn(false))
+	// return
+
+	nums := []int{1, 2, 3, 4}
+	fmt.Println(nums, sum(nums...))
+	return
+}

+ 27 - 0
courses/golang_web/golang_web_services_2024-04-26/1/functions/recover.go

@@ -0,0 +1,27 @@
+package main
+
+import (
+	"fmt"
+)
+
+func deferTest() {
+	defer func() {
+		if err := recover(); err != nil {
+			fmt.Println("panic happend FIRST:", err)
+		}
+	}()
+	defer func() {
+		if err := recover(); err != nil {
+			fmt.Println("panic happend SECOND:", err)
+			// panic("second panic")
+		}
+	}()
+	fmt.Println("Some userful work")
+	panic("something bad happend")
+	return
+}
+
+func main() {
+	deferTest()
+	return
+}

+ 34 - 0
courses/golang_web/golang_web_services_2024-04-26/1/interfaces/basic.go

@@ -0,0 +1,34 @@
+package main
+
+import (
+	"fmt"
+)
+
+type Payer interface {
+	Pay(int) error
+}
+
+type Wallet struct {
+	Cash int
+}
+
+func (w *Wallet) Pay(amount int) error {
+	if w.Cash < amount {
+		return fmt.Errorf("Не хватает денег в кошельке")
+	}
+	w.Cash -= amount
+	return nil
+}
+
+func Buy(p Payer) {
+	err := p.Pay(10)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Printf("Спасибо за покупку через %T\n\n", p)
+}
+
+func main() {
+	myWallet := &Wallet{Cash: 100}
+	Buy(myWallet)
+}

+ 97 - 0
courses/golang_web/golang_web_services_2024-04-26/1/interfaces/cast.go

@@ -0,0 +1,97 @@
+package main
+
+import (
+	"fmt"
+)
+
+// --------------
+
+type Wallet struct {
+	Cash int
+}
+
+func (w *Wallet) Pay(amount int) error {
+	if w.Cash < amount {
+		return fmt.Errorf("Not enough cash")
+	}
+	w.Cash -= amount
+	return nil
+}
+
+// --------------
+
+type Card struct {
+	Balance    int
+	ValidUntil string
+	Cardholder string
+	CVV        string
+	Number     string
+}
+
+func (c *Card) Pay(amount int) error {
+	if c.Balance < amount {
+		return fmt.Errorf("Not enough money on balance")
+	}
+	c.Balance -= amount
+	return nil
+}
+
+// --------------
+
+type ApplePay struct {
+	Money   int
+	AppleID string
+}
+
+func (a *ApplePay) Pay(amount int) error {
+	if a.Money < amount {
+		return fmt.Errorf("Not enough money on account")
+	}
+	a.Money -= amount
+	return nil
+}
+
+// --------------
+
+type Payer interface {
+	Pay(int) error
+}
+
+// --------------
+
+func Buy(p Payer) {
+	switch p.(type) {
+	case *Wallet:
+		fmt.Println("Оплата наличными?")
+	case *Card:
+		plasticCard, ok := p.(*Card)
+		if !ok {
+			fmt.Println("Не удалось преобразовать к типу *Card")
+		}
+		fmt.Println("Вставляйте карту,", plasticCard.Cardholder)
+	default:
+		fmt.Println("Что-то новое!")
+	}
+
+	err := p.Pay(10)
+	if err != nil {
+		fmt.Printf("Ошибка при оплате %T: %v\n\n", p, err)
+		return
+	}
+	fmt.Printf("Спасибо за покупку через %T\n\n", p)
+}
+
+// --------------
+
+func main() {
+
+	myWallet := &Wallet{Cash: 100}
+	Buy(myWallet)
+
+	var myMoney Payer
+	myMoney = &Card{Balance: 100, Cardholder: "rvasily"}
+	Buy(myMoney)
+
+	myMoney = &ApplePay{Money: 9}
+	Buy(myMoney)
+}

+ 58 - 0
courses/golang_web/golang_web_services_2024-04-26/1/interfaces/embed.go

@@ -0,0 +1,58 @@
+package main
+
+import (
+	"fmt"
+)
+
+type Phone struct {
+	Money   int
+	AppleID string
+}
+
+func (p *Phone) Pay(amount int) error {
+	if p.Money < amount {
+		return fmt.Errorf("Not enough money on account")
+	}
+	p.Money -= amount
+	return nil
+}
+
+func (p *Phone) Ring(number string) error {
+	if number == "" {
+		return fmt.Errorf("PLease, enter phone")
+	}
+	return nil
+}
+
+// --------------
+
+type Payer interface {
+	Pay(int) error
+}
+
+type Ringer interface {
+	Ring(string) error
+}
+
+type NFCPhone interface {
+	Payer
+	Ringer
+}
+
+// --------------
+
+func PayForMetwiWithPhone(phone NFCPhone) {
+	err := phone.Pay(1)
+	if err != nil {
+		fmt.Printf("Ошибка при оплате %v\n\n", err)
+		return
+	}
+	fmt.Printf("Турникет открыт через %T\n\n", phone)
+}
+
+// --------------
+
+func main() {
+	myPhone := &Phone{Money: 9}
+	PayForMetwiWithPhone(myPhone)
+}

+ 30 - 0
courses/golang_web/golang_web_services_2024-04-26/1/interfaces/empty_1.go

@@ -0,0 +1,30 @@
+package main
+
+import (
+	"fmt"
+	"strconv"
+)
+
+type Wallet struct {
+	Cash int
+}
+
+func (w *Wallet) Pay(amount int) error {
+	if w.Cash < amount {
+		return fmt.Errorf("Not enough cash")
+	}
+	w.Cash -= amount
+	return nil
+}
+
+func (w *Wallet) String() string {
+	return "Кошелёк в котором " + strconv.Itoa(w.Cash) + " денег"
+}
+
+// -----
+
+func main() {
+	myWallet := &Wallet{Cash: 100}
+	fmt.Printf("Raw payment : %#v\n", myWallet)
+	fmt.Printf("Способ оплаты: %s\n", myWallet)
+}

+ 58 - 0
courses/golang_web/golang_web_services_2024-04-26/1/interfaces/empty_2.go

@@ -0,0 +1,58 @@
+package main
+
+import (
+	"fmt"
+	"strconv"
+)
+
+// --------------
+
+type Wallet struct {
+	Cash int
+}
+
+func (w *Wallet) Pay(amount int) error {
+	if w.Cash < amount {
+		return fmt.Errorf("Not enough cash")
+	}
+	w.Cash -= amount
+	return nil
+}
+
+func (w *Wallet) String() string {
+	return "Кошелёк в котором " + strconv.Itoa(w.Cash) + " денег"
+}
+
+// --------------
+
+type Payer interface {
+	Pay(int) error
+}
+
+// --------------
+
+func Buy(in interface{}) {
+	var p Payer
+	var ok bool
+	if p, ok = in.(Payer); !ok {
+		fmt.Printf("%T не не является платежным средством\n\n", in)
+		return
+	}
+
+	err := p.Pay(10)
+	if err != nil {
+		fmt.Printf("Ошибка при оплате %T: %v\n\n", p, err)
+		return
+	}
+	fmt.Printf("Спасибо за покупку через %T\n\n", p)
+
+}
+
+// --------------
+
+func main() {
+	myWallet := &Wallet{Cash: 100}
+	Buy(myWallet)
+	Buy([]int{1, 2, 3})
+	Buy(3.14)
+}

+ 84 - 0
courses/golang_web/golang_web_services_2024-04-26/1/interfaces/many.go

@@ -0,0 +1,84 @@
+package main
+
+import (
+	"fmt"
+)
+
+// --------------
+
+type Wallet struct {
+	Cash int
+}
+
+func (w *Wallet) Pay(amount int) error {
+	if w.Cash < amount {
+		return fmt.Errorf("Не хватает денег в кошельке")
+	}
+	w.Cash -= amount
+	return nil
+}
+
+// --------------
+
+type Card struct {
+	Balance    int
+	ValidUntil string
+	Cardholder string
+	CVV        string
+	Number     string
+}
+
+func (c *Card) Pay(amount int) error {
+	if c.Balance < amount {
+		return fmt.Errorf("Не хватает денег на карте")
+	}
+	c.Balance -= amount
+	return nil
+}
+
+// --------------
+
+type ApplePay struct {
+	Money   int
+	AppleID string
+}
+
+func (a *ApplePay) Pay(amount int) error {
+	if a.Money < amount {
+		return fmt.Errorf("Не хватает денег на аккаунте")
+	}
+	a.Money -= amount
+	return nil
+}
+
+// --------------
+
+type Payer interface {
+	Pay(int) error
+}
+
+// --------------
+
+func Buy(p Payer) {
+	err := p.Pay(10)
+	if err != nil {
+		fmt.Printf("Ошибка при оплате %T: %v\n\n", p, err)
+		return
+	}
+	fmt.Printf("Спасибо за покупку через %T\n\n", p)
+}
+
+// --------------
+
+func main() {
+
+	myWallet := &Wallet{Cash: 100}
+	Buy(myWallet)
+
+	var myMoney Payer
+	myMoney = &Card{Balance: 100, Cardholder: "rvasily"}
+	Buy(myMoney)
+
+	myMoney = &ApplePay{Money: 9}
+	Buy(myMoney)
+}

+ 53 - 0
courses/golang_web/golang_web_services_2024-04-26/1/readings_1.md

@@ -0,0 +1,53 @@
+Материалы для дополнительного чтения на английском:
+
+* https://golang.org/ref/spec - спецификация по язык
+* https://golang.org/ref/mem - модель памяти го. на начальном этапе не надо, но знать полезно
+* https://golang.org/doc/code.html - про организацию кода. GOPATH и пакеты
+* https://golang.org/cmd/go/
+* https://blog.golang.org/strings
+* https://blog.golang.org/slices
+* https://blog.golang.org/go-slices-usage-and-internals
+* https://github.com/golang/go/wiki - вики го на гитхабе. очень много полезной информации
+* https://blog.golang.org/go-maps-in-action
+* https://blog.golang.org/organizing-go-code
+* https://golang.org/doc/effective_go.html - основной сборник тайного знания, сюда вы будуте обращатсья в первое время часто
+* https://github.com/golang/go/wiki/CodeReviewComments как ревьювить (и писать код). обязательно к прочтению
+* https://divan.github.io/posts/avoid_gotchas/ - материал аналогичный 50 оттенков го
+* https://research.swtch.com/interfaces
+* https://research.swtch.com/godata
+* http://jordanorelli.com/post/42369331748/function-types-in-go-golang
+* https://www.devdungeon.com/content/working-files-go - работа с файлами
+* http://www.golangprograms.com - много how-to касательно базовых вещей в go
+* http://yourbasic.org/golang/ - ещё большой набор how-to где можно получить углублённую информацию по всем базовым вещам. очень полезны http://yourbasic.org/golang/blueprint/
+* https://go101.org/article/101.html - похожий на предыдущий сайт с кучей информации по основам и основным местам
+* https://github.com/Workiva/go-datastructures
+* https://github.com/enocom/gopher-reading-list - большая подборка статей по многим темам ( не только данной лекции )
+* https://www.youtube.com/watch?v=MzTcsI6tn-0 - как организовать код
+* https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1 - статья на предыдущую тему
+
+Материалы для дополнительного чтения на русском:
+* https://habrahabr.ru/company/mailru/blog/314804/ - 50 оттенков го. обязательно к прочтению. многое оттуда мы ещё не проходили, но на будущее - имейте ввиду
+* https://habrahabr.ru/post/306914/ - Разбираемся в Go: пакет io
+* https://habrahabr.ru/post/272383/ - постулаты go. Маленькая статья об основными принципах языка
+* https://habrahabr.ru/company/mailru/blog/301036/ - лучшие практики go
+* https://habrahabr.ru/post/308198/ - организация кода в go
+* https://habrahabr.ru/post/339192/ - Зачем в Go амперсанд и звёздочка (& и *)
+* https://habrahabr.ru/post/325468/ - как не наступать на грабли в Go
+* https://habrahabr.ru/post/276981/ - Краш-курс по интерфейсам в Go
+* http://golang-book.ru
+
+Литература по го на русском языке:
+
+* Язык программирования Go, Алан А. А. Донован, Брайан У. Керниган
+* Go на практике, Matt Butcher, Мэтт Фарина Мэтт
+* Программирование на Go. Разработка приложений XXI века, Марк Саммерфильд
+
+Дополнительные упражнения:
+
+* https://go-tour-ru-ru.appspot.com/list - упражнения на овладение базовым синтаксисом, на случай если вам нужна небольшая практика перед первым заданием курса
+
+
+
+
+
+

+ 64 - 0
courses/golang_web/golang_web_services_2024-04-26/1/structs/methods.go

@@ -0,0 +1,64 @@
+package main
+
+import "fmt"
+
+type Person struct {
+	Id   int
+	Name string
+}
+
+// не изменит оригинальной структуры, для который вызван
+func (p Person) UpdateName(name string) {
+	p.Name = name
+}
+
+// изменяет оригинальную структуру
+func (p *Person) SetName(name string) {
+	p.Name = name
+}
+
+type Account struct {
+	Id   int
+	Name string
+	Person
+}
+
+func (p *Account) SetName(name string) {
+	p.Name = name
+}
+
+type MySlice []int
+
+func (sl *MySlice) Add(val int) {
+	*sl = append(*sl, val)
+}
+
+func (sl *MySlice) Count() int {
+	return len(*sl)
+}
+
+func main() {
+	// pers := &Person{1, "Vasily"}
+	pers := new(Person)
+	pers.SetName("Vasily Romanov")
+	// (&pers).SetName("Vasily Romanov")
+	// fmt.Printf("updated person: %#v\n", pers)
+
+	var acc Account = Account{
+		Id:   1,
+		Name: "rvasily",
+		Person: Person{
+			Id:   2,
+			Name: "Vasily Romanov",
+		},
+	}
+
+	acc.SetName("romanov.vasily")
+	acc.Person.SetName("Test")
+
+	// fmt.Printf("%#v \n", acc)
+
+	sl := MySlice([]int{1, 2})
+	sl.Add(5)
+	fmt.Println(sl.Count(), sl)
+}

+ 38 - 0
courses/golang_web/golang_web_services_2024-04-26/1/structs/structs.go

@@ -0,0 +1,38 @@
+package main
+
+import "fmt"
+
+type Person struct {
+	Id      int
+	Name    string
+	Address string
+}
+
+type Account struct {
+	Id int
+	// Name    string
+	Cleaner func(string) string
+	Owner   Person
+	Person
+}
+
+func main() {
+	// полное объявление структуры
+	var acc Account = Account{
+		Id: 1,
+		// Name: "rvasily",
+		Person: Person{
+			Name:    "Василий",
+			Address: "Москва",
+		},
+	}
+	fmt.Printf("%#v\n", acc)
+
+	// короткое объявление структуры
+	acc.Owner = Person{2, "Romanov Vasily", "Moscow"}
+
+	fmt.Printf("%#v\n", acc)
+
+	fmt.Println(acc.Name)
+	fmt.Println(acc.Person.Name)
+}

+ 8 - 0
courses/golang_web/golang_web_services_2024-04-26/1/uniq/basic/data.txt

@@ -0,0 +1,8 @@
+1
+2
+3
+3
+3
+3
+4
+5

+ 4 - 0
courses/golang_web/golang_web_services_2024-04-26/1/uniq/basic/data_bad.txt

@@ -0,0 +1,4 @@
+1
+1
+2
+1

+ 5 - 0
courses/golang_web/golang_web_services_2024-04-26/1/uniq/basic/data_map.txt

@@ -0,0 +1,5 @@
+1
+3
+2
+1
+2

+ 23 - 0
courses/golang_web/golang_web_services_2024-04-26/1/uniq/basic/main.go

@@ -0,0 +1,23 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+)
+
+func main() {
+	in := bufio.NewScanner(os.Stdin)
+	var prev string
+	for in.Scan() {
+		txt := in.Text()
+		if txt == prev {
+			continue
+		}
+		if txt < prev {
+			panic("file not sorted")
+		}
+		prev = txt
+		fmt.Println(txt)
+	}
+}

+ 8 - 0
courses/golang_web/golang_web_services_2024-04-26/1/uniq/with_tests/data.txt

@@ -0,0 +1,8 @@
+1
+2
+3
+3
+3
+3
+4
+5

+ 4 - 0
courses/golang_web/golang_web_services_2024-04-26/1/uniq/with_tests/data_bad.txt

@@ -0,0 +1,4 @@
+1
+1
+2
+1

+ 5 - 0
courses/golang_web/golang_web_services_2024-04-26/1/uniq/with_tests/data_map.txt

@@ -0,0 +1,5 @@
+1
+3
+2
+1
+2

+ 33 - 0
courses/golang_web/golang_web_services_2024-04-26/1/uniq/with_tests/main.go

@@ -0,0 +1,33 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"os"
+)
+
+func uniq(input io.Reader, output io.Writer) error {
+	in := bufio.NewScanner(input)
+	var prev string
+	for in.Scan() {
+		txt := in.Text()
+		if txt == prev {
+			continue
+		}
+		if txt < prev {
+			return fmt.Errorf("file not sorted")
+		}
+		prev = txt
+		fmt.Fprintln(output, txt)
+	}
+	return nil
+}
+
+func main() {
+	err := uniq(os.Stdin, os.Stdout)
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+}

+ 46 - 0
courses/golang_web/golang_web_services_2024-04-26/1/uniq/with_tests/main_test.go

@@ -0,0 +1,46 @@
+package main
+
+import (
+	"bytes"
+	"testing"
+)
+
+var testOkInput = `1
+2
+3
+3
+4
+5`
+
+var testOkResult = `1
+2
+3
+4
+5
+`
+
+var testFailInput = `1
+2
+1`
+
+func TestOK(t *testing.T) {
+	in := bytes.NewBufferString(testOkInput)
+	out := bytes.NewBuffer(nil)
+	err := uniq(in, out)
+	if err != nil {
+		t.Errorf("Test OK failed: %s", err)
+	}
+	result := out.String()
+	if result != testOkResult {
+		t.Errorf("Test OK failed, result not match")
+	}
+}
+
+func TestFail(t *testing.T) {
+	in := bytes.NewBufferString(testFailInput)
+	out := bytes.NewBuffer(nil)
+	err := uniq(in, out)
+	if err == nil {
+		t.Errorf("Test FAIL failed: expected error")
+	}
+}

+ 13 - 0
courses/golang_web/golang_web_services_2024-04-26/1/visibility/dir.txt

@@ -0,0 +1,13 @@
+C:\Users\User\go
+├───bin
+├───pkg
+└───src
+    ├───coursera
+    │   ├───visibility
+    │   │   │───person
+    │   │   │   │───person.go
+    │   │   │   └───func.go
+    │   │   └───main.go
+    └───github.com
+        └───rvasily
+            └───examplerepo

+ 16 - 0
courses/golang_web/golang_web_services_2024-04-26/1/visibility/main.go

@@ -0,0 +1,16 @@
+package main
+
+import (
+	"fmt"
+	"go-stepik-1/1/visibility/person"
+)
+
+func main() {
+	p := person.NewPerson(1, "rvasily", "secret")
+
+	// p.secret undefined (cannot refer to unexported field or method secret)
+	// fmt.Printf("main.PrintPerson: %+v\n", p.secret)
+
+	secret := person.GetSecret(p)
+	fmt.Println("GetSecret", secret)
+}

+ 21 - 0
courses/golang_web/golang_web_services_2024-04-26/1/visibility/person/func.go

@@ -0,0 +1,21 @@
+package person
+
+import (
+	"fmt"
+)
+
+func NewPerson(id int, name, secret string) *Person {
+	return &Person{
+		ID:     1,
+		Name:   "rvasily",
+		secret: "secret",
+	}
+}
+
+func GetSecret(p *Person) string {
+	return p.secret
+}
+
+func printSecret(p *Person) {
+	fmt.Println(p.secret)
+}

+ 16 - 0
courses/golang_web/golang_web_services_2024-04-26/1/visibility/person/person.go

@@ -0,0 +1,16 @@
+package person
+
+var (
+	Public  = 1
+	private = 1
+)
+
+type Person struct {
+	ID     int
+	Name   string
+	secret string
+}
+
+func (p Person) UpdateSecret(secret string) {
+	p.secret = secret
+}

+ 2 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/Makefile

@@ -0,0 +1,2 @@
+test:
+	go test -v -race

+ 14 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/go.mod

@@ -0,0 +1,14 @@
+module taskbot
+
+go 1.16
+
+require (
+	github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
+	github.com/technoweenie/multipartstreamer v1.0.1 // indirect
+
+)
+
+// это надо для переопределения адреса сервера
+// в оригинале урл телеграма в константе, у меня там строка, которую я переопределяю в тесте
+// replace gopkg.in/telegram-bot-api.v4 => ./local/telegram-bot-api.v4
+replace github.com/go-telegram-bot-api/telegram-bot-api => ./local/telegram-bot-api

+ 6 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/go.sum

@@ -0,0 +1,6 @@
+github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
+github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
+github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
+github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
+gopkg.in/telegram-bot-api.v4 v4.6.4 h1:hpHWhzn4jTCsAJZZ2loNKfy2QWyPDRJVl3aTFXeMW8g=
+gopkg.in/telegram-bot-api.v4 v4.6.4/go.mod h1:5DpGO5dbumb40px+dXcwCpcjmeHNYLpk0bp3XRNvWDM=

+ 3 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/.gitignore

@@ -0,0 +1,3 @@
+.idea/
+coverage.out
+tmp/

+ 6 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/.travis.yml

@@ -0,0 +1,6 @@
+language: go
+
+go:
+  - '1.10'
+  - '1.11'
+  - tip

+ 21 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/LICENSE.txt

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Syfaro
+
+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:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+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.

+ 121 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/README.md

@@ -0,0 +1,121 @@
+# Golang bindings for the Telegram Bot API
+
+[![GoDoc](https://godoc.org/github.com/go-telegram-bot-api/telegram-bot-api?status.svg)](http://godoc.org/github.com/go-telegram-bot-api/telegram-bot-api)
+[![Travis](https://travis-ci.org/go-telegram-bot-api/telegram-bot-api.svg)](https://travis-ci.org/go-telegram-bot-api/telegram-bot-api)
+
+All methods are fairly self explanatory, and reading the godoc page should
+explain everything. If something isn't clear, open an issue or submit
+a pull request.
+
+The scope of this project is just to provide a wrapper around the API
+without any additional features. There are other projects for creating
+something with plugins and command handlers without having to design
+all that yourself.
+
+Join [the development group](https://telegram.me/go_telegram_bot_api) if
+you want to ask questions or discuss development.
+
+## Example
+
+First, ensure the library is installed and up to date by running
+`go get -u github.com/go-telegram-bot-api/telegram-bot-api`.
+
+This is a very simple bot that just displays any gotten updates,
+then replies it to that chat.
+
+```go
+package main
+
+import (
+	"log"
+
+	"github.com/go-telegram-bot-api/telegram-bot-api"
+)
+
+func main() {
+	bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken")
+	if err != nil {
+		log.Panic(err)
+	}
+
+	bot.Debug = true
+
+	log.Printf("Authorized on account %s", bot.Self.UserName)
+
+	u := tgbotapi.NewUpdate(0)
+	u.Timeout = 60
+
+	updates, err := bot.GetUpdatesChan(u)
+
+	for update := range updates {
+		if update.Message == nil { // ignore any non-Message Updates
+			continue
+		}
+
+		log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text)
+
+		msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
+		msg.ReplyToMessageID = update.Message.MessageID
+
+		bot.Send(msg)
+	}
+}
+```
+
+There are more examples on the [wiki](https://github.com/go-telegram-bot-api/telegram-bot-api/wiki)
+with detailed information on how to do many differen kinds of things.
+It's a great place to get started on using keyboards, commands, or other
+kinds of reply markup.
+
+If you need to use webhooks (if you wish to run on Google App Engine),
+you may use a slightly different method.
+
+```go
+package main
+
+import (
+	"log"
+	"net/http"
+
+	"github.com/go-telegram-bot-api/telegram-bot-api"
+)
+
+func main() {
+	bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	bot.Debug = true
+
+	log.Printf("Authorized on account %s", bot.Self.UserName)
+
+	_, err = bot.SetWebhook(tgbotapi.NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, "cert.pem"))
+	if err != nil {
+		log.Fatal(err)
+	}
+	info, err := bot.GetWebhookInfo()
+	if err != nil {
+		log.Fatal(err)
+	}
+	if info.LastErrorDate != 0 {
+		log.Printf("Telegram callback failed: %s", info.LastErrorMessage)
+	}
+	updates := bot.ListenForWebhook("/" + bot.Token)
+	go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil)
+
+	for update := range updates {
+		log.Printf("%+v\n", update)
+	}
+}
+```
+
+If you need, you may generate a self signed certficate, as this requires
+HTTPS / TLS. The above example tells Telegram that this is your
+certificate and that it should be trusted, even though it is not
+properly signed.
+
+    openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3560 -subj "//O=Org\CN=Test" -nodes
+
+Now that [Let's Encrypt](https://letsencrypt.org) is available,
+you may wish to generate your free TLS certificate there.

+ 967 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/bot.go

@@ -0,0 +1,967 @@
+// Package tgbotapi has functions and types used for interacting with
+// the Telegram Bot API.
+package tgbotapi
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"os"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/technoweenie/multipartstreamer"
+)
+
+// BotAPI allows you to interact with the Telegram Bot API.
+type BotAPI struct {
+	Token  string `json:"token"`
+	Debug  bool   `json:"debug"`
+	Buffer int    `json:"buffer"`
+
+	Self   User         `json:"-"`
+	Client *http.Client `json:"-"`
+	shutdownChannel chan interface{}
+}
+
+// NewBotAPI creates a new BotAPI instance.
+//
+// It requires a token, provided by @BotFather on Telegram.
+func NewBotAPI(token string) (*BotAPI, error) {
+	return NewBotAPIWithClient(token, &http.Client{})
+}
+
+// NewBotAPIWithClient creates a new BotAPI instance
+// and allows you to pass a http.Client.
+//
+// It requires a token, provided by @BotFather on Telegram.
+func NewBotAPIWithClient(token string, client *http.Client) (*BotAPI, error) {
+	bot := &BotAPI{
+		Token:  token,
+		Client: client,
+		Buffer: 100,
+		shutdownChannel: make(chan interface{}),
+	}
+
+	self, err := bot.GetMe()
+	if err != nil {
+		return nil, err
+	}
+
+	bot.Self = self
+
+	return bot, nil
+}
+
+// MakeRequest makes a request to a specific endpoint with our token.
+func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse, error) {
+	method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint)
+
+	resp, err := bot.Client.PostForm(method, params)
+	if err != nil {
+		return APIResponse{}, err
+	}
+	defer resp.Body.Close()
+
+	var apiResp APIResponse
+	bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
+	if err != nil {
+		return apiResp, err
+	}
+
+	if bot.Debug {
+		log.Printf("%s resp: %s", endpoint, bytes)
+	}
+
+	if !apiResp.Ok {
+		parameters := ResponseParameters{}
+		if apiResp.Parameters != nil {
+			parameters = *apiResp.Parameters
+		}
+		return apiResp, Error{apiResp.Description, parameters}
+	}
+
+	return apiResp, nil
+}
+
+// decodeAPIResponse decode response and return slice of bytes if debug enabled.
+// If debug disabled, just decode http.Response.Body stream to APIResponse struct
+// for efficient memory usage
+func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) (_ []byte, err error) {
+	if !bot.Debug {
+		dec := json.NewDecoder(responseBody)
+		err = dec.Decode(resp)
+		return
+	}
+
+	// if debug, read reponse body
+	data, err := ioutil.ReadAll(responseBody)
+	if err != nil {
+		return
+	}
+
+	err = json.Unmarshal(data, resp)
+	if err != nil {
+		return
+	}
+
+	return data, nil
+}
+
+// makeMessageRequest makes a request to a method that returns a Message.
+func (bot *BotAPI) makeMessageRequest(endpoint string, params url.Values) (Message, error) {
+	resp, err := bot.MakeRequest(endpoint, params)
+	if err != nil {
+		return Message{}, err
+	}
+
+	var message Message
+	json.Unmarshal(resp.Result, &message)
+
+	bot.debugLog(endpoint, params, message)
+
+	return message, nil
+}
+
+// UploadFile makes a request to the API with a file.
+//
+// Requires the parameter to hold the file not be in the params.
+// File should be a string to a file path, a FileBytes struct,
+// a FileReader struct, or a url.URL.
+//
+// Note that if your FileReader has a size set to -1, it will read
+// the file into memory to calculate a size.
+func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldname string, file interface{}) (APIResponse, error) {
+	ms := multipartstreamer.New()
+
+	switch f := file.(type) {
+	case string:
+		ms.WriteFields(params)
+
+		fileHandle, err := os.Open(f)
+		if err != nil {
+			return APIResponse{}, err
+		}
+		defer fileHandle.Close()
+
+		fi, err := os.Stat(f)
+		if err != nil {
+			return APIResponse{}, err
+		}
+
+		ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle)
+	case FileBytes:
+		ms.WriteFields(params)
+
+		buf := bytes.NewBuffer(f.Bytes)
+		ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf)
+	case FileReader:
+		ms.WriteFields(params)
+
+		if f.Size != -1 {
+			ms.WriteReader(fieldname, f.Name, f.Size, f.Reader)
+
+			break
+		}
+
+		data, err := ioutil.ReadAll(f.Reader)
+		if err != nil {
+			return APIResponse{}, err
+		}
+
+		buf := bytes.NewBuffer(data)
+
+		ms.WriteReader(fieldname, f.Name, int64(len(data)), buf)
+	case url.URL:
+		params[fieldname] = f.String()
+
+		ms.WriteFields(params)
+	default:
+		return APIResponse{}, errors.New(ErrBadFileType)
+	}
+
+	method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint)
+
+	req, err := http.NewRequest("POST", method, nil)
+	if err != nil {
+		return APIResponse{}, err
+	}
+
+	ms.SetupRequest(req)
+
+	res, err := bot.Client.Do(req)
+	if err != nil {
+		return APIResponse{}, err
+	}
+	defer res.Body.Close()
+
+	bytes, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return APIResponse{}, err
+	}
+
+	if bot.Debug {
+		log.Println(string(bytes))
+	}
+
+	var apiResp APIResponse
+
+	err = json.Unmarshal(bytes, &apiResp)
+	if err != nil {
+		return APIResponse{}, err
+	}
+
+	if !apiResp.Ok {
+		return APIResponse{}, errors.New(apiResp.Description)
+	}
+
+	return apiResp, nil
+}
+
+// GetFileDirectURL returns direct URL to file
+//
+// It requires the FileID.
+func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
+	file, err := bot.GetFile(FileConfig{fileID})
+
+	if err != nil {
+		return "", err
+	}
+
+	return file.Link(bot.Token), nil
+}
+
+// GetMe fetches the currently authenticated bot.
+//
+// This method is called upon creation to validate the token,
+// and so you may get this data from BotAPI.Self without the need for
+// another request.
+func (bot *BotAPI) GetMe() (User, error) {
+	resp, err := bot.MakeRequest("getMe", nil)
+	if err != nil {
+		return User{}, err
+	}
+
+	var user User
+	json.Unmarshal(resp.Result, &user)
+
+	bot.debugLog("getMe", nil, user)
+
+	return user, nil
+}
+
+// IsMessageToMe returns true if message directed to this bot.
+//
+// It requires the Message.
+func (bot *BotAPI) IsMessageToMe(message Message) bool {
+	return strings.Contains(message.Text, "@"+bot.Self.UserName)
+}
+
+// Send will send a Chattable item to Telegram.
+//
+// It requires the Chattable to send.
+func (bot *BotAPI) Send(c Chattable) (Message, error) {
+	switch c.(type) {
+	case Fileable:
+		return bot.sendFile(c.(Fileable))
+	default:
+		return bot.sendChattable(c)
+	}
+}
+
+// debugLog checks if the bot is currently running in debug mode, and if
+// so will display information about the request and response in the
+// debug log.
+func (bot *BotAPI) debugLog(context string, v url.Values, message interface{}) {
+	if bot.Debug {
+		log.Printf("%s req : %+v\n", context, v)
+		log.Printf("%s resp: %+v\n", context, message)
+	}
+}
+
+// sendExisting will send a Message with an existing file to Telegram.
+func (bot *BotAPI) sendExisting(method string, config Fileable) (Message, error) {
+	v, err := config.values()
+
+	if err != nil {
+		return Message{}, err
+	}
+
+	message, err := bot.makeMessageRequest(method, v)
+	if err != nil {
+		return Message{}, err
+	}
+
+	return message, nil
+}
+
+// uploadAndSend will send a Message with a new file to Telegram.
+func (bot *BotAPI) uploadAndSend(method string, config Fileable) (Message, error) {
+	params, err := config.params()
+	if err != nil {
+		return Message{}, err
+	}
+
+	file := config.getFile()
+
+	resp, err := bot.UploadFile(method, params, config.name(), file)
+	if err != nil {
+		return Message{}, err
+	}
+
+	var message Message
+	json.Unmarshal(resp.Result, &message)
+
+	bot.debugLog(method, nil, message)
+
+	return message, nil
+}
+
+// sendFile determines if the file is using an existing file or uploading
+// a new file, then sends it as needed.
+func (bot *BotAPI) sendFile(config Fileable) (Message, error) {
+	if config.useExistingFile() {
+		return bot.sendExisting(config.method(), config)
+	}
+
+	return bot.uploadAndSend(config.method(), config)
+}
+
+// sendChattable sends a Chattable.
+func (bot *BotAPI) sendChattable(config Chattable) (Message, error) {
+	v, err := config.values()
+	if err != nil {
+		return Message{}, err
+	}
+
+	message, err := bot.makeMessageRequest(config.method(), v)
+
+	if err != nil {
+		return Message{}, err
+	}
+
+	return message, nil
+}
+
+// GetUserProfilePhotos gets a user's profile photos.
+//
+// It requires UserID.
+// Offset and Limit are optional.
+func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
+	v := url.Values{}
+	v.Add("user_id", strconv.Itoa(config.UserID))
+	if config.Offset != 0 {
+		v.Add("offset", strconv.Itoa(config.Offset))
+	}
+	if config.Limit != 0 {
+		v.Add("limit", strconv.Itoa(config.Limit))
+	}
+
+	resp, err := bot.MakeRequest("getUserProfilePhotos", v)
+	if err != nil {
+		return UserProfilePhotos{}, err
+	}
+
+	var profilePhotos UserProfilePhotos
+	json.Unmarshal(resp.Result, &profilePhotos)
+
+	bot.debugLog("GetUserProfilePhoto", v, profilePhotos)
+
+	return profilePhotos, nil
+}
+
+// GetFile returns a File which can download a file from Telegram.
+//
+// Requires FileID.
+func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
+	v := url.Values{}
+	v.Add("file_id", config.FileID)
+
+	resp, err := bot.MakeRequest("getFile", v)
+	if err != nil {
+		return File{}, err
+	}
+
+	var file File
+	json.Unmarshal(resp.Result, &file)
+
+	bot.debugLog("GetFile", v, file)
+
+	return file, nil
+}
+
+// GetUpdates fetches updates.
+// If a WebHook is set, this will not return any data!
+//
+// Offset, Limit, and Timeout are optional.
+// To avoid stale items, set Offset to one higher than the previous item.
+// Set Timeout to a large number to reduce requests so you can get updates
+// instantly instead of having to wait between requests.
+func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
+	v := url.Values{}
+	if config.Offset != 0 {
+		v.Add("offset", strconv.Itoa(config.Offset))
+	}
+	if config.Limit > 0 {
+		v.Add("limit", strconv.Itoa(config.Limit))
+	}
+	if config.Timeout > 0 {
+		v.Add("timeout", strconv.Itoa(config.Timeout))
+	}
+
+	resp, err := bot.MakeRequest("getUpdates", v)
+	if err != nil {
+		return []Update{}, err
+	}
+
+	var updates []Update
+	json.Unmarshal(resp.Result, &updates)
+
+	bot.debugLog("getUpdates", v, updates)
+
+	return updates, nil
+}
+
+// RemoveWebhook unsets the webhook.
+func (bot *BotAPI) RemoveWebhook() (APIResponse, error) {
+	return bot.MakeRequest("setWebhook", url.Values{})
+}
+
+// SetWebhook sets a webhook.
+//
+// If this is set, GetUpdates will not get any data!
+//
+// If you do not have a legitimate TLS certificate, you need to include
+// your self signed certificate with the config.
+func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
+
+	if config.Certificate == nil {
+		v := url.Values{}
+		v.Add("url", config.URL.String())
+		if config.MaxConnections != 0 {
+			v.Add("max_connections", strconv.Itoa(config.MaxConnections))
+		}
+
+		return bot.MakeRequest("setWebhook", v)
+	}
+
+	params := make(map[string]string)
+	params["url"] = config.URL.String()
+	if config.MaxConnections != 0 {
+		params["max_connections"] = strconv.Itoa(config.MaxConnections)
+	}
+
+	resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate)
+	if err != nil {
+		return APIResponse{}, err
+	}
+
+	return resp, nil
+}
+
+// GetWebhookInfo allows you to fetch information about a webhook and if
+// one currently is set, along with pending update count and error messages.
+func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
+	resp, err := bot.MakeRequest("getWebhookInfo", url.Values{})
+	if err != nil {
+		return WebhookInfo{}, err
+	}
+
+	var info WebhookInfo
+	err = json.Unmarshal(resp.Result, &info)
+
+	return info, err
+}
+
+// GetUpdatesChan starts and returns a channel for getting updates.
+func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (UpdatesChannel, error) {
+	ch := make(chan Update, bot.Buffer)
+
+	go func() {
+		for {
+			select {
+			case <-bot.shutdownChannel:
+				return
+			default:
+			}
+			
+			updates, err := bot.GetUpdates(config)
+			if err != nil {
+				log.Println(err)
+				log.Println("Failed to get updates, retrying in 3 seconds...")
+				time.Sleep(time.Second * 3)
+
+				continue
+			}
+
+			for _, update := range updates {
+				if update.UpdateID >= config.Offset {
+					config.Offset = update.UpdateID + 1
+					ch <- update
+				}
+			}
+		}
+	}()
+
+	return ch, nil
+}
+
+// StopReceivingUpdates stops the go routine which receives updates
+func (bot *BotAPI) StopReceivingUpdates() {
+	if bot.Debug {
+		log.Println("Stopping the update receiver routine...")
+	}
+	close(bot.shutdownChannel)
+}
+
+// ListenForWebhook registers a http handler for a webhook.
+func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
+	ch := make(chan Update, bot.Buffer)
+
+	http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
+		bytes, _ := ioutil.ReadAll(r.Body)
+
+		var update Update
+		json.Unmarshal(bytes, &update)
+
+		ch <- update
+	})
+
+	return ch
+}
+
+// AnswerInlineQuery sends a response to an inline query.
+//
+// Note that you must respond to an inline query within 30 seconds.
+func (bot *BotAPI) AnswerInlineQuery(config InlineConfig) (APIResponse, error) {
+	v := url.Values{}
+
+	v.Add("inline_query_id", config.InlineQueryID)
+	v.Add("cache_time", strconv.Itoa(config.CacheTime))
+	v.Add("is_personal", strconv.FormatBool(config.IsPersonal))
+	v.Add("next_offset", config.NextOffset)
+	data, err := json.Marshal(config.Results)
+	if err != nil {
+		return APIResponse{}, err
+	}
+	v.Add("results", string(data))
+	v.Add("switch_pm_text", config.SwitchPMText)
+	v.Add("switch_pm_parameter", config.SwitchPMParameter)
+
+	bot.debugLog("answerInlineQuery", v, nil)
+
+	return bot.MakeRequest("answerInlineQuery", v)
+}
+
+// AnswerCallbackQuery sends a response to an inline query callback.
+func (bot *BotAPI) AnswerCallbackQuery(config CallbackConfig) (APIResponse, error) {
+	v := url.Values{}
+
+	v.Add("callback_query_id", config.CallbackQueryID)
+	if config.Text != "" {
+		v.Add("text", config.Text)
+	}
+	v.Add("show_alert", strconv.FormatBool(config.ShowAlert))
+	if config.URL != "" {
+		v.Add("url", config.URL)
+	}
+	v.Add("cache_time", strconv.Itoa(config.CacheTime))
+
+	bot.debugLog("answerCallbackQuery", v, nil)
+
+	return bot.MakeRequest("answerCallbackQuery", v)
+}
+
+// KickChatMember kicks a user from a chat. Note that this only will work
+// in supergroups, and requires the bot to be an admin. Also note they
+// will be unable to rejoin until they are unbanned.
+func (bot *BotAPI) KickChatMember(config KickChatMemberConfig) (APIResponse, error) {
+	v := url.Values{}
+
+	if config.SuperGroupUsername == "" {
+		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+	} else {
+		v.Add("chat_id", config.SuperGroupUsername)
+	}
+	v.Add("user_id", strconv.Itoa(config.UserID))
+
+	if config.UntilDate != 0 {
+		v.Add("until_date", strconv.FormatInt(config.UntilDate, 10))
+	}
+
+	bot.debugLog("kickChatMember", v, nil)
+
+	return bot.MakeRequest("kickChatMember", v)
+}
+
+// LeaveChat makes the bot leave the chat.
+func (bot *BotAPI) LeaveChat(config ChatConfig) (APIResponse, error) {
+	v := url.Values{}
+
+	if config.SuperGroupUsername == "" {
+		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+	} else {
+		v.Add("chat_id", config.SuperGroupUsername)
+	}
+
+	bot.debugLog("leaveChat", v, nil)
+
+	return bot.MakeRequest("leaveChat", v)
+}
+
+// GetChat gets information about a chat.
+func (bot *BotAPI) GetChat(config ChatConfig) (Chat, error) {
+	v := url.Values{}
+
+	if config.SuperGroupUsername == "" {
+		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+	} else {
+		v.Add("chat_id", config.SuperGroupUsername)
+	}
+
+	resp, err := bot.MakeRequest("getChat", v)
+	if err != nil {
+		return Chat{}, err
+	}
+
+	var chat Chat
+	err = json.Unmarshal(resp.Result, &chat)
+
+	bot.debugLog("getChat", v, chat)
+
+	return chat, err
+}
+
+// GetChatAdministrators gets a list of administrators in the chat.
+//
+// If none have been appointed, only the creator will be returned.
+// Bots are not shown, even if they are an administrator.
+func (bot *BotAPI) GetChatAdministrators(config ChatConfig) ([]ChatMember, error) {
+	v := url.Values{}
+
+	if config.SuperGroupUsername == "" {
+		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+	} else {
+		v.Add("chat_id", config.SuperGroupUsername)
+	}
+
+	resp, err := bot.MakeRequest("getChatAdministrators", v)
+	if err != nil {
+		return []ChatMember{}, err
+	}
+
+	var members []ChatMember
+	err = json.Unmarshal(resp.Result, &members)
+
+	bot.debugLog("getChatAdministrators", v, members)
+
+	return members, err
+}
+
+// GetChatMembersCount gets the number of users in a chat.
+func (bot *BotAPI) GetChatMembersCount(config ChatConfig) (int, error) {
+	v := url.Values{}
+
+	if config.SuperGroupUsername == "" {
+		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+	} else {
+		v.Add("chat_id", config.SuperGroupUsername)
+	}
+
+	resp, err := bot.MakeRequest("getChatMembersCount", v)
+	if err != nil {
+		return -1, err
+	}
+
+	var count int
+	err = json.Unmarshal(resp.Result, &count)
+
+	bot.debugLog("getChatMembersCount", v, count)
+
+	return count, err
+}
+
+// GetChatMember gets a specific chat member.
+func (bot *BotAPI) GetChatMember(config ChatConfigWithUser) (ChatMember, error) {
+	v := url.Values{}
+
+	if config.SuperGroupUsername == "" {
+		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+	} else {
+		v.Add("chat_id", config.SuperGroupUsername)
+	}
+	v.Add("user_id", strconv.Itoa(config.UserID))
+
+	resp, err := bot.MakeRequest("getChatMember", v)
+	if err != nil {
+		return ChatMember{}, err
+	}
+
+	var member ChatMember
+	err = json.Unmarshal(resp.Result, &member)
+
+	bot.debugLog("getChatMember", v, member)
+
+	return member, err
+}
+
+// UnbanChatMember unbans a user from a chat. Note that this only will work
+// in supergroups and channels, and requires the bot to be an admin.
+func (bot *BotAPI) UnbanChatMember(config ChatMemberConfig) (APIResponse, error) {
+	v := url.Values{}
+
+	if config.SuperGroupUsername != "" {
+		v.Add("chat_id", config.SuperGroupUsername)
+	} else if config.ChannelUsername != "" {
+		v.Add("chat_id", config.ChannelUsername)
+	} else {
+		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+	}
+	v.Add("user_id", strconv.Itoa(config.UserID))
+
+	bot.debugLog("unbanChatMember", v, nil)
+
+	return bot.MakeRequest("unbanChatMember", v)
+}
+
+// RestrictChatMember to restrict a user in a supergroup. The bot must be an
+//administrator in the supergroup for this to work and must have the
+//appropriate admin rights. Pass True for all boolean parameters to lift
+//restrictions from a user. Returns True on success.
+func (bot *BotAPI) RestrictChatMember(config RestrictChatMemberConfig) (APIResponse, error) {
+	v := url.Values{}
+
+	if config.SuperGroupUsername != "" {
+		v.Add("chat_id", config.SuperGroupUsername)
+	} else if config.ChannelUsername != "" {
+		v.Add("chat_id", config.ChannelUsername)
+	} else {
+		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+	}
+	v.Add("user_id", strconv.Itoa(config.UserID))
+
+	if config.CanSendMessages != nil {
+		v.Add("can_send_messages", strconv.FormatBool(*config.CanSendMessages))
+	}
+	if config.CanSendMediaMessages != nil {
+		v.Add("can_send_media_messages", strconv.FormatBool(*config.CanSendMediaMessages))
+	}
+	if config.CanSendOtherMessages != nil {
+		v.Add("can_send_other_messages", strconv.FormatBool(*config.CanSendOtherMessages))
+	}
+	if config.CanAddWebPagePreviews != nil {
+		v.Add("can_add_web_page_previews", strconv.FormatBool(*config.CanAddWebPagePreviews))
+	}
+	if config.UntilDate != 0 {
+		v.Add("until_date", strconv.FormatInt(config.UntilDate, 10))
+	}
+
+	bot.debugLog("restrictChatMember", v, nil)
+
+	return bot.MakeRequest("restrictChatMember", v)
+}
+
+// PromoteChatMember add admin rights to user
+func (bot *BotAPI) PromoteChatMember(config PromoteChatMemberConfig) (APIResponse, error) {
+	v := url.Values{}
+
+	if config.SuperGroupUsername != "" {
+		v.Add("chat_id", config.SuperGroupUsername)
+	} else if config.ChannelUsername != "" {
+		v.Add("chat_id", config.ChannelUsername)
+	} else {
+		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+	}
+	v.Add("user_id", strconv.Itoa(config.UserID))
+
+	if config.CanChangeInfo != nil {
+		v.Add("can_change_info", strconv.FormatBool(*config.CanChangeInfo))
+	}
+	if config.CanPostMessages != nil {
+		v.Add("can_post_messages", strconv.FormatBool(*config.CanPostMessages))
+	}
+	if config.CanEditMessages != nil {
+		v.Add("can_edit_messages", strconv.FormatBool(*config.CanEditMessages))
+	}
+	if config.CanDeleteMessages != nil {
+		v.Add("can_delete_messages", strconv.FormatBool(*config.CanDeleteMessages))
+	}
+	if config.CanInviteUsers != nil {
+		v.Add("can_invite_users", strconv.FormatBool(*config.CanInviteUsers))
+	}
+	if config.CanRestrictMembers != nil {
+		v.Add("can_restrict_members", strconv.FormatBool(*config.CanRestrictMembers))
+	}
+	if config.CanPinMessages != nil {
+		v.Add("can_pin_messages", strconv.FormatBool(*config.CanPinMessages))
+	}
+	if config.CanPromoteMembers != nil {
+		v.Add("can_promote_members", strconv.FormatBool(*config.CanPromoteMembers))
+	}
+
+	bot.debugLog("promoteChatMember", v, nil)
+
+	return bot.MakeRequest("promoteChatMember", v)
+}
+
+// GetGameHighScores allows you to get the high scores for a game.
+func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
+	v, _ := config.values()
+
+	resp, err := bot.MakeRequest(config.method(), v)
+	if err != nil {
+		return []GameHighScore{}, err
+	}
+
+	var highScores []GameHighScore
+	err = json.Unmarshal(resp.Result, &highScores)
+
+	return highScores, err
+}
+
+// AnswerShippingQuery allows you to reply to Update with shipping_query parameter.
+func (bot *BotAPI) AnswerShippingQuery(config ShippingConfig) (APIResponse, error) {
+	v := url.Values{}
+
+	v.Add("shipping_query_id", config.ShippingQueryID)
+	v.Add("ok", strconv.FormatBool(config.OK))
+	if config.OK == true {
+		data, err := json.Marshal(config.ShippingOptions)
+		if err != nil {
+			return APIResponse{}, err
+		}
+		v.Add("shipping_options", string(data))
+	} else {
+		v.Add("error_message", config.ErrorMessage)
+	}
+
+	bot.debugLog("answerShippingQuery", v, nil)
+
+	return bot.MakeRequest("answerShippingQuery", v)
+}
+
+// AnswerPreCheckoutQuery allows you to reply to Update with pre_checkout_query.
+func (bot *BotAPI) AnswerPreCheckoutQuery(config PreCheckoutConfig) (APIResponse, error) {
+	v := url.Values{}
+
+	v.Add("pre_checkout_query_id", config.PreCheckoutQueryID)
+	v.Add("ok", strconv.FormatBool(config.OK))
+	if config.OK != true {
+		v.Add("error", config.ErrorMessage)
+	}
+
+	bot.debugLog("answerPreCheckoutQuery", v, nil)
+
+	return bot.MakeRequest("answerPreCheckoutQuery", v)
+}
+
+// DeleteMessage deletes a message in a chat
+func (bot *BotAPI) DeleteMessage(config DeleteMessageConfig) (APIResponse, error) {
+	v, err := config.values()
+	if err != nil {
+		return APIResponse{}, err
+	}
+
+	bot.debugLog(config.method(), v, nil)
+
+	return bot.MakeRequest(config.method(), v)
+}
+
+// GetInviteLink get InviteLink for a chat
+func (bot *BotAPI) GetInviteLink(config ChatConfig) (string, error) {
+	v := url.Values{}
+
+	if config.SuperGroupUsername == "" {
+		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+	} else {
+		v.Add("chat_id", config.SuperGroupUsername)
+	}
+
+	resp, err := bot.MakeRequest("exportChatInviteLink", v)
+	if err != nil {
+		return "", err
+	}
+
+	var inviteLink string
+	err = json.Unmarshal(resp.Result, &inviteLink)
+
+	return inviteLink, err
+}
+
+// PinChatMessage pin message in supergroup
+func (bot *BotAPI) PinChatMessage(config PinChatMessageConfig) (APIResponse, error) {
+	v, err := config.values()
+	if err != nil {
+		return APIResponse{}, err
+	}
+
+	bot.debugLog(config.method(), v, nil)
+
+	return bot.MakeRequest(config.method(), v)
+}
+
+// UnpinChatMessage unpin message in supergroup
+func (bot *BotAPI) UnpinChatMessage(config UnpinChatMessageConfig) (APIResponse, error) {
+	v, err := config.values()
+	if err != nil {
+		return APIResponse{}, err
+	}
+
+	bot.debugLog(config.method(), v, nil)
+
+	return bot.MakeRequest(config.method(), v)
+}
+
+// SetChatTitle change title of chat.
+func (bot *BotAPI) SetChatTitle(config SetChatTitleConfig) (APIResponse, error) {
+	v, err := config.values()
+	if err != nil {
+		return APIResponse{}, err
+	}
+
+	bot.debugLog(config.method(), v, nil)
+
+	return bot.MakeRequest(config.method(), v)
+}
+
+// SetChatDescription change description of chat.
+func (bot *BotAPI) SetChatDescription(config SetChatDescriptionConfig) (APIResponse, error) {
+	v, err := config.values()
+	if err != nil {
+		return APIResponse{}, err
+	}
+
+	bot.debugLog(config.method(), v, nil)
+
+	return bot.MakeRequest(config.method(), v)
+}
+
+// SetChatPhoto change photo of chat.
+func (bot *BotAPI) SetChatPhoto(config SetChatPhotoConfig) (APIResponse, error) {
+	params, err := config.params()
+	if err != nil {
+		return APIResponse{}, err
+	}
+
+	file := config.getFile()
+
+	return bot.UploadFile(config.method(), params, config.name(), file)
+}
+
+// DeleteChatPhoto delete photo of chat.
+func (bot *BotAPI) DeleteChatPhoto(config DeleteChatPhotoConfig) (APIResponse, error) {
+	v, err := config.values()
+	if err != nil {
+		return APIResponse{}, err
+	}
+
+	bot.debugLog(config.method(), v, nil)
+
+	return bot.MakeRequest(config.method(), v)
+}

+ 1267 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/configs.go

@@ -0,0 +1,1267 @@
+package tgbotapi
+
+import (
+	"encoding/json"
+	"io"
+	"net/url"
+	"strconv"
+)
+
+// Telegram constants
+const (
+	// FileEndpoint is the endpoint for downloading a file from Telegram.
+	FileEndpoint = "https://api.telegram.org/file/bot%s/%s"
+)
+
+var (
+	// APIEndpoint is the endpoint for all API methods,
+	// with formatting for Sprintf.
+	APIEndpoint = "https://api.telegram.org/bot%s/%s"
+)
+
+// Constant values for ChatActions
+const (
+	ChatTyping         = "typing"
+	ChatUploadPhoto    = "upload_photo"
+	ChatRecordVideo    = "record_video"
+	ChatUploadVideo    = "upload_video"
+	ChatRecordAudio    = "record_audio"
+	ChatUploadAudio    = "upload_audio"
+	ChatUploadDocument = "upload_document"
+	ChatFindLocation   = "find_location"
+)
+
+// API errors
+const (
+	// ErrAPIForbidden happens when a token is bad
+	ErrAPIForbidden = "forbidden"
+)
+
+// Constant values for ParseMode in MessageConfig
+const (
+	ModeMarkdown = "Markdown"
+	ModeHTML     = "HTML"
+)
+
+// Library errors
+const (
+	// ErrBadFileType happens when you pass an unknown type
+	ErrBadFileType = "bad file type"
+	ErrBadURL      = "bad or empty url"
+)
+
+// Chattable is any config type that can be sent.
+type Chattable interface {
+	values() (url.Values, error)
+	method() string
+}
+
+// Fileable is any config type that can be sent that includes a file.
+type Fileable interface {
+	Chattable
+	params() (map[string]string, error)
+	name() string
+	getFile() interface{}
+	useExistingFile() bool
+}
+
+// BaseChat is base type for all chat config types.
+type BaseChat struct {
+	ChatID              int64 // required
+	ChannelUsername     string
+	ReplyToMessageID    int
+	ReplyMarkup         interface{}
+	DisableNotification bool
+}
+
+// values returns url.Values representation of BaseChat
+func (chat *BaseChat) values() (url.Values, error) {
+	v := url.Values{}
+	if chat.ChannelUsername != "" {
+		v.Add("chat_id", chat.ChannelUsername)
+	} else {
+		v.Add("chat_id", strconv.FormatInt(chat.ChatID, 10))
+	}
+
+	if chat.ReplyToMessageID != 0 {
+		v.Add("reply_to_message_id", strconv.Itoa(chat.ReplyToMessageID))
+	}
+
+	if chat.ReplyMarkup != nil {
+		data, err := json.Marshal(chat.ReplyMarkup)
+		if err != nil {
+			return v, err
+		}
+
+		v.Add("reply_markup", string(data))
+	}
+
+	v.Add("disable_notification", strconv.FormatBool(chat.DisableNotification))
+
+	return v, nil
+}
+
+// BaseFile is a base type for all file config types.
+type BaseFile struct {
+	BaseChat
+	File        interface{}
+	FileID      string
+	UseExisting bool
+	MimeType    string
+	FileSize    int
+}
+
+// params returns a map[string]string representation of BaseFile.
+func (file BaseFile) params() (map[string]string, error) {
+	params := make(map[string]string)
+
+	if file.ChannelUsername != "" {
+		params["chat_id"] = file.ChannelUsername
+	} else {
+		params["chat_id"] = strconv.FormatInt(file.ChatID, 10)
+	}
+
+	if file.ReplyToMessageID != 0 {
+		params["reply_to_message_id"] = strconv.Itoa(file.ReplyToMessageID)
+	}
+
+	if file.ReplyMarkup != nil {
+		data, err := json.Marshal(file.ReplyMarkup)
+		if err != nil {
+			return params, err
+		}
+
+		params["reply_markup"] = string(data)
+	}
+
+	if file.MimeType != "" {
+		params["mime_type"] = file.MimeType
+	}
+
+	if file.FileSize > 0 {
+		params["file_size"] = strconv.Itoa(file.FileSize)
+	}
+
+	params["disable_notification"] = strconv.FormatBool(file.DisableNotification)
+
+	return params, nil
+}
+
+// getFile returns the file.
+func (file BaseFile) getFile() interface{} {
+	return file.File
+}
+
+// useExistingFile returns if the BaseFile has already been uploaded.
+func (file BaseFile) useExistingFile() bool {
+	return file.UseExisting
+}
+
+// BaseEdit is base type of all chat edits.
+type BaseEdit struct {
+	ChatID          int64
+	ChannelUsername string
+	MessageID       int
+	InlineMessageID string
+	ReplyMarkup     *InlineKeyboardMarkup
+}
+
+func (edit BaseEdit) values() (url.Values, error) {
+	v := url.Values{}
+
+	if edit.InlineMessageID == "" {
+		if edit.ChannelUsername != "" {
+			v.Add("chat_id", edit.ChannelUsername)
+		} else {
+			v.Add("chat_id", strconv.FormatInt(edit.ChatID, 10))
+		}
+		v.Add("message_id", strconv.Itoa(edit.MessageID))
+	} else {
+		v.Add("inline_message_id", edit.InlineMessageID)
+	}
+
+	if edit.ReplyMarkup != nil {
+		data, err := json.Marshal(edit.ReplyMarkup)
+		if err != nil {
+			return v, err
+		}
+		v.Add("reply_markup", string(data))
+	}
+
+	return v, nil
+}
+
+// MessageConfig contains information about a SendMessage request.
+type MessageConfig struct {
+	BaseChat
+	Text                  string
+	ParseMode             string
+	DisableWebPagePreview bool
+}
+
+// values returns a url.Values representation of MessageConfig.
+func (config MessageConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+	v.Add("text", config.Text)
+	v.Add("disable_web_page_preview", strconv.FormatBool(config.DisableWebPagePreview))
+	if config.ParseMode != "" {
+		v.Add("parse_mode", config.ParseMode)
+	}
+
+	return v, nil
+}
+
+// method returns Telegram API method name for sending Message.
+func (config MessageConfig) method() string {
+	return "sendMessage"
+}
+
+// ForwardConfig contains information about a ForwardMessage request.
+type ForwardConfig struct {
+	BaseChat
+	FromChatID          int64 // required
+	FromChannelUsername string
+	MessageID           int // required
+}
+
+// values returns a url.Values representation of ForwardConfig.
+func (config ForwardConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+	v.Add("from_chat_id", strconv.FormatInt(config.FromChatID, 10))
+	v.Add("message_id", strconv.Itoa(config.MessageID))
+	return v, nil
+}
+
+// method returns Telegram API method name for sending Forward.
+func (config ForwardConfig) method() string {
+	return "forwardMessage"
+}
+
+// PhotoConfig contains information about a SendPhoto request.
+type PhotoConfig struct {
+	BaseFile
+	Caption   string
+	ParseMode string
+}
+
+// Params returns a map[string]string representation of PhotoConfig.
+func (config PhotoConfig) params() (map[string]string, error) {
+	params, _ := config.BaseFile.params()
+
+	if config.Caption != "" {
+		params["caption"] = config.Caption
+		if config.ParseMode != "" {
+			params["parse_mode"] = config.ParseMode
+		}
+	}
+
+	return params, nil
+}
+
+// Values returns a url.Values representation of PhotoConfig.
+func (config PhotoConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+
+	v.Add(config.name(), config.FileID)
+	if config.Caption != "" {
+		v.Add("caption", config.Caption)
+		if config.ParseMode != "" {
+			v.Add("parse_mode", config.ParseMode)
+		}
+	}
+
+	return v, nil
+}
+
+// name returns the field name for the Photo.
+func (config PhotoConfig) name() string {
+	return "photo"
+}
+
+// method returns Telegram API method name for sending Photo.
+func (config PhotoConfig) method() string {
+	return "sendPhoto"
+}
+
+// AudioConfig contains information about a SendAudio request.
+type AudioConfig struct {
+	BaseFile
+	Caption   string
+	ParseMode string
+	Duration  int
+	Performer string
+	Title     string
+}
+
+// values returns a url.Values representation of AudioConfig.
+func (config AudioConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+
+	v.Add(config.name(), config.FileID)
+	if config.Duration != 0 {
+		v.Add("duration", strconv.Itoa(config.Duration))
+	}
+
+	if config.Performer != "" {
+		v.Add("performer", config.Performer)
+	}
+	if config.Title != "" {
+		v.Add("title", config.Title)
+	}
+	if config.Caption != "" {
+		v.Add("caption", config.Caption)
+		if config.ParseMode != "" {
+			v.Add("parse_mode", config.ParseMode)
+		}
+	}
+
+	return v, nil
+}
+
+// params returns a map[string]string representation of AudioConfig.
+func (config AudioConfig) params() (map[string]string, error) {
+	params, _ := config.BaseFile.params()
+
+	if config.Duration != 0 {
+		params["duration"] = strconv.Itoa(config.Duration)
+	}
+
+	if config.Performer != "" {
+		params["performer"] = config.Performer
+	}
+	if config.Title != "" {
+		params["title"] = config.Title
+	}
+	if config.Caption != "" {
+		params["caption"] = config.Caption
+		if config.ParseMode != "" {
+			params["parse_mode"] = config.ParseMode
+		}
+	}
+
+	return params, nil
+}
+
+// name returns the field name for the Audio.
+func (config AudioConfig) name() string {
+	return "audio"
+}
+
+// method returns Telegram API method name for sending Audio.
+func (config AudioConfig) method() string {
+	return "sendAudio"
+}
+
+// DocumentConfig contains information about a SendDocument request.
+type DocumentConfig struct {
+	BaseFile
+	Caption   string
+	ParseMode string
+}
+
+// values returns a url.Values representation of DocumentConfig.
+func (config DocumentConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+
+	v.Add(config.name(), config.FileID)
+	if config.Caption != "" {
+		v.Add("caption", config.Caption)
+		if config.ParseMode != "" {
+			v.Add("parse_mode", config.ParseMode)
+		}
+	}
+
+	return v, nil
+}
+
+// params returns a map[string]string representation of DocumentConfig.
+func (config DocumentConfig) params() (map[string]string, error) {
+	params, _ := config.BaseFile.params()
+
+	if config.Caption != "" {
+		params["caption"] = config.Caption
+		if config.ParseMode != "" {
+			params["parse_mode"] = config.ParseMode
+		}
+	}
+
+	return params, nil
+}
+
+// name returns the field name for the Document.
+func (config DocumentConfig) name() string {
+	return "document"
+}
+
+// method returns Telegram API method name for sending Document.
+func (config DocumentConfig) method() string {
+	return "sendDocument"
+}
+
+// StickerConfig contains information about a SendSticker request.
+type StickerConfig struct {
+	BaseFile
+}
+
+// values returns a url.Values representation of StickerConfig.
+func (config StickerConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+
+	v.Add(config.name(), config.FileID)
+
+	return v, nil
+}
+
+// params returns a map[string]string representation of StickerConfig.
+func (config StickerConfig) params() (map[string]string, error) {
+	params, _ := config.BaseFile.params()
+
+	return params, nil
+}
+
+// name returns the field name for the Sticker.
+func (config StickerConfig) name() string {
+	return "sticker"
+}
+
+// method returns Telegram API method name for sending Sticker.
+func (config StickerConfig) method() string {
+	return "sendSticker"
+}
+
+// VideoConfig contains information about a SendVideo request.
+type VideoConfig struct {
+	BaseFile
+	Duration  int
+	Caption   string
+	ParseMode string
+}
+
+// values returns a url.Values representation of VideoConfig.
+func (config VideoConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+
+	v.Add(config.name(), config.FileID)
+	if config.Duration != 0 {
+		v.Add("duration", strconv.Itoa(config.Duration))
+	}
+	if config.Caption != "" {
+		v.Add("caption", config.Caption)
+		if config.ParseMode != "" {
+			v.Add("parse_mode", config.ParseMode)
+		}
+	}
+
+	return v, nil
+}
+
+// params returns a map[string]string representation of VideoConfig.
+func (config VideoConfig) params() (map[string]string, error) {
+	params, _ := config.BaseFile.params()
+
+	if config.Caption != "" {
+		params["caption"] = config.Caption
+		if config.ParseMode != "" {
+			params["parse_mode"] = config.ParseMode
+		}
+	}
+
+	return params, nil
+}
+
+// name returns the field name for the Video.
+func (config VideoConfig) name() string {
+	return "video"
+}
+
+// method returns Telegram API method name for sending Video.
+func (config VideoConfig) method() string {
+	return "sendVideo"
+}
+
+// AnimationConfig contains information about a SendAnimation request.
+type AnimationConfig struct {
+	BaseFile
+	Duration  int
+	Caption   string
+	ParseMode string
+}
+
+// values returns a url.Values representation of AnimationConfig.
+func (config AnimationConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+
+	v.Add(config.name(), config.FileID)
+	if config.Duration != 0 {
+		v.Add("duration", strconv.Itoa(config.Duration))
+	}
+	if config.Caption != "" {
+		v.Add("caption", config.Caption)
+		if config.ParseMode != "" {
+			v.Add("parse_mode", config.ParseMode)
+		}
+	}
+
+	return v, nil
+}
+
+// params returns a map[string]string representation of AnimationConfig.
+func (config AnimationConfig) params() (map[string]string, error) {
+	params, _ := config.BaseFile.params()
+
+	if config.Caption != "" {
+		params["caption"] = config.Caption
+		if config.ParseMode != "" {
+			params["parse_mode"] = config.ParseMode
+		}
+	}
+
+	return params, nil
+}
+
+// name returns the field name for the Animation.
+func (config AnimationConfig) name() string {
+	return "animation"
+}
+
+// method returns Telegram API method name for sending Animation.
+func (config AnimationConfig) method() string {
+	return "sendAnimation"
+}
+
+// VideoNoteConfig contains information about a SendVideoNote request.
+type VideoNoteConfig struct {
+	BaseFile
+	Duration int
+	Length   int
+}
+
+// values returns a url.Values representation of VideoNoteConfig.
+func (config VideoNoteConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+
+	v.Add(config.name(), config.FileID)
+	if config.Duration != 0 {
+		v.Add("duration", strconv.Itoa(config.Duration))
+	}
+
+	// Telegram API seems to have a bug, if no length is provided or it is 0, it will send an error response
+	if config.Length != 0 {
+		v.Add("length", strconv.Itoa(config.Length))
+	}
+
+	return v, nil
+}
+
+// params returns a map[string]string representation of VideoNoteConfig.
+func (config VideoNoteConfig) params() (map[string]string, error) {
+	params, _ := config.BaseFile.params()
+
+	if config.Length != 0 {
+		params["length"] = strconv.Itoa(config.Length)
+	}
+	if config.Duration != 0 {
+		params["duration"] = strconv.Itoa(config.Duration)
+	}
+
+	return params, nil
+}
+
+// name returns the field name for the VideoNote.
+func (config VideoNoteConfig) name() string {
+	return "video_note"
+}
+
+// method returns Telegram API method name for sending VideoNote.
+func (config VideoNoteConfig) method() string {
+	return "sendVideoNote"
+}
+
+// VoiceConfig contains information about a SendVoice request.
+type VoiceConfig struct {
+	BaseFile
+	Caption   string
+	ParseMode string
+	Duration  int
+}
+
+// values returns a url.Values representation of VoiceConfig.
+func (config VoiceConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+
+	v.Add(config.name(), config.FileID)
+	if config.Duration != 0 {
+		v.Add("duration", strconv.Itoa(config.Duration))
+	}
+	if config.Caption != "" {
+		v.Add("caption", config.Caption)
+		if config.ParseMode != "" {
+			v.Add("parse_mode", config.ParseMode)
+		}
+	}
+
+	return v, nil
+}
+
+// params returns a map[string]string representation of VoiceConfig.
+func (config VoiceConfig) params() (map[string]string, error) {
+	params, _ := config.BaseFile.params()
+
+	if config.Duration != 0 {
+		params["duration"] = strconv.Itoa(config.Duration)
+	}
+	if config.Caption != "" {
+		params["caption"] = config.Caption
+		if config.ParseMode != "" {
+			params["parse_mode"] = config.ParseMode
+		}
+	}
+
+	return params, nil
+}
+
+// name returns the field name for the Voice.
+func (config VoiceConfig) name() string {
+	return "voice"
+}
+
+// method returns Telegram API method name for sending Voice.
+func (config VoiceConfig) method() string {
+	return "sendVoice"
+}
+
+// MediaGroupConfig contains information about a sendMediaGroup request.
+type MediaGroupConfig struct {
+	BaseChat
+	InputMedia []interface{}
+}
+
+func (config MediaGroupConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+
+	data, err := json.Marshal(config.InputMedia)
+	if err != nil {
+		return v, err
+	}
+
+	v.Add("media", string(data))
+
+	return v, nil
+}
+
+func (config MediaGroupConfig) method() string {
+	return "sendMediaGroup"
+}
+
+// LocationConfig contains information about a SendLocation request.
+type LocationConfig struct {
+	BaseChat
+	Latitude  float64 // required
+	Longitude float64 // required
+}
+
+// values returns a url.Values representation of LocationConfig.
+func (config LocationConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+
+	v.Add("latitude", strconv.FormatFloat(config.Latitude, 'f', 6, 64))
+	v.Add("longitude", strconv.FormatFloat(config.Longitude, 'f', 6, 64))
+
+	return v, nil
+}
+
+// method returns Telegram API method name for sending Location.
+func (config LocationConfig) method() string {
+	return "sendLocation"
+}
+
+// VenueConfig contains information about a SendVenue request.
+type VenueConfig struct {
+	BaseChat
+	Latitude     float64 // required
+	Longitude    float64 // required
+	Title        string  // required
+	Address      string  // required
+	FoursquareID string
+}
+
+func (config VenueConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+
+	v.Add("latitude", strconv.FormatFloat(config.Latitude, 'f', 6, 64))
+	v.Add("longitude", strconv.FormatFloat(config.Longitude, 'f', 6, 64))
+	v.Add("title", config.Title)
+	v.Add("address", config.Address)
+	if config.FoursquareID != "" {
+		v.Add("foursquare_id", config.FoursquareID)
+	}
+
+	return v, nil
+}
+
+func (config VenueConfig) method() string {
+	return "sendVenue"
+}
+
+// ContactConfig allows you to send a contact.
+type ContactConfig struct {
+	BaseChat
+	PhoneNumber string
+	FirstName   string
+	LastName    string
+}
+
+func (config ContactConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+
+	v.Add("phone_number", config.PhoneNumber)
+	v.Add("first_name", config.FirstName)
+	v.Add("last_name", config.LastName)
+
+	return v, nil
+}
+
+func (config ContactConfig) method() string {
+	return "sendContact"
+}
+
+// GameConfig allows you to send a game.
+type GameConfig struct {
+	BaseChat
+	GameShortName string
+}
+
+func (config GameConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+
+	v.Add("game_short_name", config.GameShortName)
+
+	return v, nil
+}
+
+func (config GameConfig) method() string {
+	return "sendGame"
+}
+
+// SetGameScoreConfig allows you to update the game score in a chat.
+type SetGameScoreConfig struct {
+	UserID             int
+	Score              int
+	Force              bool
+	DisableEditMessage bool
+	ChatID             int64
+	ChannelUsername    string
+	MessageID          int
+	InlineMessageID    string
+}
+
+func (config SetGameScoreConfig) values() (url.Values, error) {
+	v := url.Values{}
+
+	v.Add("user_id", strconv.Itoa(config.UserID))
+	v.Add("score", strconv.Itoa(config.Score))
+	if config.InlineMessageID == "" {
+		if config.ChannelUsername == "" {
+			v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+		} else {
+			v.Add("chat_id", config.ChannelUsername)
+		}
+		v.Add("message_id", strconv.Itoa(config.MessageID))
+	} else {
+		v.Add("inline_message_id", config.InlineMessageID)
+	}
+	v.Add("disable_edit_message", strconv.FormatBool(config.DisableEditMessage))
+
+	return v, nil
+}
+
+func (config SetGameScoreConfig) method() string {
+	return "setGameScore"
+}
+
+// GetGameHighScoresConfig allows you to fetch the high scores for a game.
+type GetGameHighScoresConfig struct {
+	UserID          int
+	ChatID          int
+	ChannelUsername string
+	MessageID       int
+	InlineMessageID string
+}
+
+func (config GetGameHighScoresConfig) values() (url.Values, error) {
+	v := url.Values{}
+
+	v.Add("user_id", strconv.Itoa(config.UserID))
+	if config.InlineMessageID == "" {
+		if config.ChannelUsername == "" {
+			v.Add("chat_id", strconv.Itoa(config.ChatID))
+		} else {
+			v.Add("chat_id", config.ChannelUsername)
+		}
+		v.Add("message_id", strconv.Itoa(config.MessageID))
+	} else {
+		v.Add("inline_message_id", config.InlineMessageID)
+	}
+
+	return v, nil
+}
+
+func (config GetGameHighScoresConfig) method() string {
+	return "getGameHighScores"
+}
+
+// ChatActionConfig contains information about a SendChatAction request.
+type ChatActionConfig struct {
+	BaseChat
+	Action string // required
+}
+
+// values returns a url.Values representation of ChatActionConfig.
+func (config ChatActionConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+	v.Add("action", config.Action)
+	return v, nil
+}
+
+// method returns Telegram API method name for sending ChatAction.
+func (config ChatActionConfig) method() string {
+	return "sendChatAction"
+}
+
+// EditMessageTextConfig allows you to modify the text in a message.
+type EditMessageTextConfig struct {
+	BaseEdit
+	Text                  string
+	ParseMode             string
+	DisableWebPagePreview bool
+}
+
+func (config EditMessageTextConfig) values() (url.Values, error) {
+	v, err := config.BaseEdit.values()
+	if err != nil {
+		return v, err
+	}
+
+	v.Add("text", config.Text)
+	v.Add("parse_mode", config.ParseMode)
+	v.Add("disable_web_page_preview", strconv.FormatBool(config.DisableWebPagePreview))
+
+	return v, nil
+}
+
+func (config EditMessageTextConfig) method() string {
+	return "editMessageText"
+}
+
+// EditMessageCaptionConfig allows you to modify the caption of a message.
+type EditMessageCaptionConfig struct {
+	BaseEdit
+	Caption   string
+	ParseMode string
+}
+
+func (config EditMessageCaptionConfig) values() (url.Values, error) {
+	v, _ := config.BaseEdit.values()
+
+	v.Add("caption", config.Caption)
+	if config.ParseMode != "" {
+		v.Add("parse_mode", config.ParseMode)
+	}
+
+	return v, nil
+}
+
+func (config EditMessageCaptionConfig) method() string {
+	return "editMessageCaption"
+}
+
+// EditMessageReplyMarkupConfig allows you to modify the reply markup
+// of a message.
+type EditMessageReplyMarkupConfig struct {
+	BaseEdit
+}
+
+func (config EditMessageReplyMarkupConfig) values() (url.Values, error) {
+	return config.BaseEdit.values()
+}
+
+func (config EditMessageReplyMarkupConfig) method() string {
+	return "editMessageReplyMarkup"
+}
+
+// UserProfilePhotosConfig contains information about a
+// GetUserProfilePhotos request.
+type UserProfilePhotosConfig struct {
+	UserID int
+	Offset int
+	Limit  int
+}
+
+// FileConfig has information about a file hosted on Telegram.
+type FileConfig struct {
+	FileID string
+}
+
+// UpdateConfig contains information about a GetUpdates request.
+type UpdateConfig struct {
+	Offset  int
+	Limit   int
+	Timeout int
+}
+
+// WebhookConfig contains information about a SetWebhook request.
+type WebhookConfig struct {
+	URL            *url.URL
+	Certificate    interface{}
+	MaxConnections int
+}
+
+// FileBytes contains information about a set of bytes to upload
+// as a File.
+type FileBytes struct {
+	Name  string
+	Bytes []byte
+}
+
+// FileReader contains information about a reader to upload as a File.
+// If Size is -1, it will read the entire Reader into memory to
+// calculate a Size.
+type FileReader struct {
+	Name   string
+	Reader io.Reader
+	Size   int64
+}
+
+// InlineConfig contains information on making an InlineQuery response.
+type InlineConfig struct {
+	InlineQueryID     string        `json:"inline_query_id"`
+	Results           []interface{} `json:"results"`
+	CacheTime         int           `json:"cache_time"`
+	IsPersonal        bool          `json:"is_personal"`
+	NextOffset        string        `json:"next_offset"`
+	SwitchPMText      string        `json:"switch_pm_text"`
+	SwitchPMParameter string        `json:"switch_pm_parameter"`
+}
+
+// CallbackConfig contains information on making a CallbackQuery response.
+type CallbackConfig struct {
+	CallbackQueryID string `json:"callback_query_id"`
+	Text            string `json:"text"`
+	ShowAlert       bool   `json:"show_alert"`
+	URL             string `json:"url"`
+	CacheTime       int    `json:"cache_time"`
+}
+
+// ChatMemberConfig contains information about a user in a chat for use
+// with administrative functions such as kicking or unbanning a user.
+type ChatMemberConfig struct {
+	ChatID             int64
+	SuperGroupUsername string
+	ChannelUsername    string
+	UserID             int
+}
+
+// KickChatMemberConfig contains extra fields to kick user
+type KickChatMemberConfig struct {
+	ChatMemberConfig
+	UntilDate int64
+}
+
+// RestrictChatMemberConfig contains fields to restrict members of chat
+type RestrictChatMemberConfig struct {
+	ChatMemberConfig
+	UntilDate             int64
+	CanSendMessages       *bool
+	CanSendMediaMessages  *bool
+	CanSendOtherMessages  *bool
+	CanAddWebPagePreviews *bool
+}
+
+// PromoteChatMemberConfig contains fields to promote members of chat
+type PromoteChatMemberConfig struct {
+	ChatMemberConfig
+	CanChangeInfo      *bool
+	CanPostMessages    *bool
+	CanEditMessages    *bool
+	CanDeleteMessages  *bool
+	CanInviteUsers     *bool
+	CanRestrictMembers *bool
+	CanPinMessages     *bool
+	CanPromoteMembers  *bool
+}
+
+// ChatConfig contains information about getting information on a chat.
+type ChatConfig struct {
+	ChatID             int64
+	SuperGroupUsername string
+}
+
+// ChatConfigWithUser contains information about getting information on
+// a specific user within a chat.
+type ChatConfigWithUser struct {
+	ChatID             int64
+	SuperGroupUsername string
+	UserID             int
+}
+
+// InvoiceConfig contains information for sendInvoice request.
+type InvoiceConfig struct {
+	BaseChat
+	Title               string          // required
+	Description         string          // required
+	Payload             string          // required
+	ProviderToken       string          // required
+	StartParameter      string          // required
+	Currency            string          // required
+	Prices              *[]LabeledPrice // required
+	PhotoURL            string
+	PhotoSize           int
+	PhotoWidth          int
+	PhotoHeight         int
+	NeedName            bool
+	NeedPhoneNumber     bool
+	NeedEmail           bool
+	NeedShippingAddress bool
+	IsFlexible          bool
+}
+
+func (config InvoiceConfig) values() (url.Values, error) {
+	v, err := config.BaseChat.values()
+	if err != nil {
+		return v, err
+	}
+	v.Add("title", config.Title)
+	v.Add("description", config.Description)
+	v.Add("payload", config.Payload)
+	v.Add("provider_token", config.ProviderToken)
+	v.Add("start_parameter", config.StartParameter)
+	v.Add("currency", config.Currency)
+	data, err := json.Marshal(config.Prices)
+	if err != nil {
+		return v, err
+	}
+	v.Add("prices", string(data))
+	if config.PhotoURL != "" {
+		v.Add("photo_url", config.PhotoURL)
+	}
+	if config.PhotoSize != 0 {
+		v.Add("photo_size", strconv.Itoa(config.PhotoSize))
+	}
+	if config.PhotoWidth != 0 {
+		v.Add("photo_width", strconv.Itoa(config.PhotoWidth))
+	}
+	if config.PhotoHeight != 0 {
+		v.Add("photo_height", strconv.Itoa(config.PhotoHeight))
+	}
+	if config.NeedName != false {
+		v.Add("need_name", strconv.FormatBool(config.NeedName))
+	}
+	if config.NeedPhoneNumber != false {
+		v.Add("need_phone_number", strconv.FormatBool(config.NeedPhoneNumber))
+	}
+	if config.NeedEmail != false {
+		v.Add("need_email", strconv.FormatBool(config.NeedEmail))
+	}
+	if config.NeedShippingAddress != false {
+		v.Add("need_shipping_address", strconv.FormatBool(config.NeedShippingAddress))
+	}
+	if config.IsFlexible != false {
+		v.Add("is_flexible", strconv.FormatBool(config.IsFlexible))
+	}
+
+	return v, nil
+}
+
+func (config InvoiceConfig) method() string {
+	return "sendInvoice"
+}
+
+// ShippingConfig contains information for answerShippingQuery request.
+type ShippingConfig struct {
+	ShippingQueryID string // required
+	OK              bool   // required
+	ShippingOptions *[]ShippingOption
+	ErrorMessage    string
+}
+
+// PreCheckoutConfig conatins information for answerPreCheckoutQuery request.
+type PreCheckoutConfig struct {
+	PreCheckoutQueryID string // required
+	OK                 bool   // required
+	ErrorMessage       string
+}
+
+// DeleteMessageConfig contains information of a message in a chat to delete.
+type DeleteMessageConfig struct {
+	ChatID    int64
+	MessageID int
+}
+
+func (config DeleteMessageConfig) method() string {
+	return "deleteMessage"
+}
+
+func (config DeleteMessageConfig) values() (url.Values, error) {
+	v := url.Values{}
+
+	v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+	v.Add("message_id", strconv.Itoa(config.MessageID))
+
+	return v, nil
+}
+
+// PinChatMessageConfig contains information of a message in a chat to pin.
+type PinChatMessageConfig struct {
+	ChatID              int64
+	MessageID           int
+	DisableNotification bool
+}
+
+func (config PinChatMessageConfig) method() string {
+	return "pinChatMessage"
+}
+
+func (config PinChatMessageConfig) values() (url.Values, error) {
+	v := url.Values{}
+
+	v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+	v.Add("message_id", strconv.Itoa(config.MessageID))
+	v.Add("disable_notification", strconv.FormatBool(config.DisableNotification))
+
+	return v, nil
+}
+
+// UnpinChatMessageConfig contains information of chat to unpin.
+type UnpinChatMessageConfig struct {
+	ChatID int64
+}
+
+func (config UnpinChatMessageConfig) method() string {
+	return "unpinChatMessage"
+}
+
+func (config UnpinChatMessageConfig) values() (url.Values, error) {
+	v := url.Values{}
+
+	v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+
+	return v, nil
+}
+
+// SetChatTitleConfig contains information for change chat title.
+type SetChatTitleConfig struct {
+	ChatID int64
+	Title  string
+}
+
+func (config SetChatTitleConfig) method() string {
+	return "setChatTitle"
+}
+
+func (config SetChatTitleConfig) values() (url.Values, error) {
+	v := url.Values{}
+
+	v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+	v.Add("title", config.Title)
+
+	return v, nil
+}
+
+// SetChatDescriptionConfig contains information for change chat description.
+type SetChatDescriptionConfig struct {
+	ChatID      int64
+	Description string
+}
+
+func (config SetChatDescriptionConfig) method() string {
+	return "setChatDescription"
+}
+
+func (config SetChatDescriptionConfig) values() (url.Values, error) {
+	v := url.Values{}
+
+	v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+	v.Add("description", config.Description)
+
+	return v, nil
+}
+
+// SetChatPhotoConfig contains information for change chat photo
+type SetChatPhotoConfig struct {
+	BaseFile
+}
+
+// name returns the field name for the Photo.
+func (config SetChatPhotoConfig) name() string {
+	return "photo"
+}
+
+// method returns Telegram API method name for sending Photo.
+func (config SetChatPhotoConfig) method() string {
+	return "setChatPhoto"
+}
+
+// DeleteChatPhotoConfig contains information for delete chat photo.
+type DeleteChatPhotoConfig struct {
+	ChatID int64
+}
+
+func (config DeleteChatPhotoConfig) method() string {
+	return "deleteChatPhoto"
+}
+
+func (config DeleteChatPhotoConfig) values() (url.Values, error) {
+	v := url.Values{}
+
+	v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
+
+	return v, nil
+}

+ 5 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/go.mod

@@ -0,0 +1,5 @@
+module github.com/go-telegram-bot-api/telegram-bot-api
+
+go 1.16
+
+require github.com/technoweenie/multipartstreamer v1.0.1

+ 2 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/go.sum

@@ -0,0 +1,2 @@
+github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
+github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=

+ 749 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/helpers.go

@@ -0,0 +1,749 @@
+package tgbotapi
+
+import (
+	"net/url"
+)
+
+// NewMessage creates a new Message.
+//
+// chatID is where to send it, text is the message text.
+func NewMessage(chatID int64, text string) MessageConfig {
+	return MessageConfig{
+		BaseChat: BaseChat{
+			ChatID:           chatID,
+			ReplyToMessageID: 0,
+		},
+		Text:                  text,
+		DisableWebPagePreview: false,
+	}
+}
+
+// NewDeleteMessage creates a request to delete a message.
+func NewDeleteMessage(chatID int64, messageID int) DeleteMessageConfig {
+	return DeleteMessageConfig{
+		ChatID:    chatID,
+		MessageID: messageID,
+	}
+}
+
+// NewMessageToChannel creates a new Message that is sent to a channel
+// by username.
+//
+// username is the username of the channel, text is the message text.
+func NewMessageToChannel(username string, text string) MessageConfig {
+	return MessageConfig{
+		BaseChat: BaseChat{
+			ChannelUsername: username,
+		},
+		Text: text,
+	}
+}
+
+// NewForward creates a new forward.
+//
+// chatID is where to send it, fromChatID is the source chat,
+// and messageID is the ID of the original message.
+func NewForward(chatID int64, fromChatID int64, messageID int) ForwardConfig {
+	return ForwardConfig{
+		BaseChat:   BaseChat{ChatID: chatID},
+		FromChatID: fromChatID,
+		MessageID:  messageID,
+	}
+}
+
+// NewPhotoUpload creates a new photo uploader.
+//
+// chatID is where to send it, file is a string path to the file,
+// FileReader, or FileBytes.
+//
+// Note that you must send animated GIFs as a document.
+func NewPhotoUpload(chatID int64, file interface{}) PhotoConfig {
+	return PhotoConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			File:        file,
+			UseExisting: false,
+		},
+	}
+}
+
+// NewPhotoShare shares an existing photo.
+// You may use this to reshare an existing photo without reuploading it.
+//
+// chatID is where to send it, fileID is the ID of the file
+// already uploaded.
+func NewPhotoShare(chatID int64, fileID string) PhotoConfig {
+	return PhotoConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			FileID:      fileID,
+			UseExisting: true,
+		},
+	}
+}
+
+// NewAudioUpload creates a new audio uploader.
+//
+// chatID is where to send it, file is a string path to the file,
+// FileReader, or FileBytes.
+func NewAudioUpload(chatID int64, file interface{}) AudioConfig {
+	return AudioConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			File:        file,
+			UseExisting: false,
+		},
+	}
+}
+
+// NewAudioShare shares an existing audio file.
+// You may use this to reshare an existing audio file without
+// reuploading it.
+//
+// chatID is where to send it, fileID is the ID of the audio
+// already uploaded.
+func NewAudioShare(chatID int64, fileID string) AudioConfig {
+	return AudioConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			FileID:      fileID,
+			UseExisting: true,
+		},
+	}
+}
+
+// NewDocumentUpload creates a new document uploader.
+//
+// chatID is where to send it, file is a string path to the file,
+// FileReader, or FileBytes.
+func NewDocumentUpload(chatID int64, file interface{}) DocumentConfig {
+	return DocumentConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			File:        file,
+			UseExisting: false,
+		},
+	}
+}
+
+// NewDocumentShare shares an existing document.
+// You may use this to reshare an existing document without
+// reuploading it.
+//
+// chatID is where to send it, fileID is the ID of the document
+// already uploaded.
+func NewDocumentShare(chatID int64, fileID string) DocumentConfig {
+	return DocumentConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			FileID:      fileID,
+			UseExisting: true,
+		},
+	}
+}
+
+// NewStickerUpload creates a new sticker uploader.
+//
+// chatID is where to send it, file is a string path to the file,
+// FileReader, or FileBytes.
+func NewStickerUpload(chatID int64, file interface{}) StickerConfig {
+	return StickerConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			File:        file,
+			UseExisting: false,
+		},
+	}
+}
+
+// NewStickerShare shares an existing sticker.
+// You may use this to reshare an existing sticker without
+// reuploading it.
+//
+// chatID is where to send it, fileID is the ID of the sticker
+// already uploaded.
+func NewStickerShare(chatID int64, fileID string) StickerConfig {
+	return StickerConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			FileID:      fileID,
+			UseExisting: true,
+		},
+	}
+}
+
+// NewVideoUpload creates a new video uploader.
+//
+// chatID is where to send it, file is a string path to the file,
+// FileReader, or FileBytes.
+func NewVideoUpload(chatID int64, file interface{}) VideoConfig {
+	return VideoConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			File:        file,
+			UseExisting: false,
+		},
+	}
+}
+
+// NewVideoShare shares an existing video.
+// You may use this to reshare an existing video without reuploading it.
+//
+// chatID is where to send it, fileID is the ID of the video
+// already uploaded.
+func NewVideoShare(chatID int64, fileID string) VideoConfig {
+	return VideoConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			FileID:      fileID,
+			UseExisting: true,
+		},
+	}
+}
+
+// NewAnimationUpload creates a new animation uploader.
+//
+// chatID is where to send it, file is a string path to the file,
+// FileReader, or FileBytes.
+func NewAnimationUpload(chatID int64, file interface{}) AnimationConfig {
+	return AnimationConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			File:        file,
+			UseExisting: false,
+		},
+	}
+}
+
+// NewAnimationShare shares an existing animation.
+// You may use this to reshare an existing animation without reuploading it.
+//
+// chatID is where to send it, fileID is the ID of the animation
+// already uploaded.
+func NewAnimationShare(chatID int64, fileID string) AnimationConfig {
+	return AnimationConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			FileID:      fileID,
+			UseExisting: true,
+		},
+	}
+}
+
+// NewVideoNoteUpload creates a new video note uploader.
+//
+// chatID is where to send it, file is a string path to the file,
+// FileReader, or FileBytes.
+func NewVideoNoteUpload(chatID int64, length int, file interface{}) VideoNoteConfig {
+	return VideoNoteConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			File:        file,
+			UseExisting: false,
+		},
+		Length: length,
+	}
+}
+
+// NewVideoNoteShare shares an existing video.
+// You may use this to reshare an existing video without reuploading it.
+//
+// chatID is where to send it, fileID is the ID of the video
+// already uploaded.
+func NewVideoNoteShare(chatID int64, length int, fileID string) VideoNoteConfig {
+	return VideoNoteConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			FileID:      fileID,
+			UseExisting: true,
+		},
+		Length: length,
+	}
+}
+
+// NewVoiceUpload creates a new voice uploader.
+//
+// chatID is where to send it, file is a string path to the file,
+// FileReader, or FileBytes.
+func NewVoiceUpload(chatID int64, file interface{}) VoiceConfig {
+	return VoiceConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			File:        file,
+			UseExisting: false,
+		},
+	}
+}
+
+// NewVoiceShare shares an existing voice.
+// You may use this to reshare an existing voice without reuploading it.
+//
+// chatID is where to send it, fileID is the ID of the video
+// already uploaded.
+func NewVoiceShare(chatID int64, fileID string) VoiceConfig {
+	return VoiceConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			FileID:      fileID,
+			UseExisting: true,
+		},
+	}
+}
+
+// NewMediaGroup creates a new media group. Files should be an array of
+// two to ten InputMediaPhoto or InputMediaVideo.
+func NewMediaGroup(chatID int64, files []interface{}) MediaGroupConfig {
+	return MediaGroupConfig{
+		BaseChat: BaseChat{
+			ChatID: chatID,
+		},
+		InputMedia: files,
+	}
+}
+
+// NewInputMediaPhoto creates a new InputMediaPhoto.
+func NewInputMediaPhoto(media string) InputMediaPhoto {
+	return InputMediaPhoto{
+		Type:  "photo",
+		Media: media,
+	}
+}
+
+// NewInputMediaVideo creates a new InputMediaVideo.
+func NewInputMediaVideo(media string) InputMediaVideo {
+	return InputMediaVideo{
+		Type:  "video",
+		Media: media,
+	}
+}
+
+// NewContact allows you to send a shared contact.
+func NewContact(chatID int64, phoneNumber, firstName string) ContactConfig {
+	return ContactConfig{
+		BaseChat: BaseChat{
+			ChatID: chatID,
+		},
+		PhoneNumber: phoneNumber,
+		FirstName:   firstName,
+	}
+}
+
+// NewLocation shares your location.
+//
+// chatID is where to send it, latitude and longitude are coordinates.
+func NewLocation(chatID int64, latitude float64, longitude float64) LocationConfig {
+	return LocationConfig{
+		BaseChat: BaseChat{
+			ChatID: chatID,
+		},
+		Latitude:  latitude,
+		Longitude: longitude,
+	}
+}
+
+// NewVenue allows you to send a venue and its location.
+func NewVenue(chatID int64, title, address string, latitude, longitude float64) VenueConfig {
+	return VenueConfig{
+		BaseChat: BaseChat{
+			ChatID: chatID,
+		},
+		Title:     title,
+		Address:   address,
+		Latitude:  latitude,
+		Longitude: longitude,
+	}
+}
+
+// NewChatAction sets a chat action.
+// Actions last for 5 seconds, or until your next action.
+//
+// chatID is where to send it, action should be set via Chat constants.
+func NewChatAction(chatID int64, action string) ChatActionConfig {
+	return ChatActionConfig{
+		BaseChat: BaseChat{ChatID: chatID},
+		Action:   action,
+	}
+}
+
+// NewUserProfilePhotos gets user profile photos.
+//
+// userID is the ID of the user you wish to get profile photos from.
+func NewUserProfilePhotos(userID int) UserProfilePhotosConfig {
+	return UserProfilePhotosConfig{
+		UserID: userID,
+		Offset: 0,
+		Limit:  0,
+	}
+}
+
+// NewUpdate gets updates since the last Offset.
+//
+// offset is the last Update ID to include.
+// You likely want to set this to the last Update ID plus 1.
+func NewUpdate(offset int) UpdateConfig {
+	return UpdateConfig{
+		Offset:  offset,
+		Limit:   0,
+		Timeout: 0,
+	}
+}
+
+// NewWebhook creates a new webhook.
+//
+// link is the url parsable link you wish to get the updates.
+func NewWebhook(link string) WebhookConfig {
+	u, _ := url.Parse(link)
+
+	return WebhookConfig{
+		URL: u,
+	}
+}
+
+// NewWebhookWithCert creates a new webhook with a certificate.
+//
+// link is the url you wish to get webhooks,
+// file contains a string to a file, FileReader, or FileBytes.
+func NewWebhookWithCert(link string, file interface{}) WebhookConfig {
+	u, _ := url.Parse(link)
+
+	return WebhookConfig{
+		URL:         u,
+		Certificate: file,
+	}
+}
+
+// NewInlineQueryResultArticle creates a new inline query article.
+func NewInlineQueryResultArticle(id, title, messageText string) InlineQueryResultArticle {
+	return InlineQueryResultArticle{
+		Type:  "article",
+		ID:    id,
+		Title: title,
+		InputMessageContent: InputTextMessageContent{
+			Text: messageText,
+		},
+	}
+}
+
+// NewInlineQueryResultArticleMarkdown creates a new inline query article with Markdown parsing.
+func NewInlineQueryResultArticleMarkdown(id, title, messageText string) InlineQueryResultArticle {
+	return InlineQueryResultArticle{
+		Type:  "article",
+		ID:    id,
+		Title: title,
+		InputMessageContent: InputTextMessageContent{
+			Text:      messageText,
+			ParseMode: "Markdown",
+		},
+	}
+}
+
+// NewInlineQueryResultArticleHTML creates a new inline query article with HTML parsing.
+func NewInlineQueryResultArticleHTML(id, title, messageText string) InlineQueryResultArticle {
+	return InlineQueryResultArticle{
+		Type:  "article",
+		ID:    id,
+		Title: title,
+		InputMessageContent: InputTextMessageContent{
+			Text:      messageText,
+			ParseMode: "HTML",
+		},
+	}
+}
+
+// NewInlineQueryResultGIF creates a new inline query GIF.
+func NewInlineQueryResultGIF(id, url string) InlineQueryResultGIF {
+	return InlineQueryResultGIF{
+		Type: "gif",
+		ID:   id,
+		URL:  url,
+	}
+}
+
+// NewInlineQueryResultMPEG4GIF creates a new inline query MPEG4 GIF.
+func NewInlineQueryResultMPEG4GIF(id, url string) InlineQueryResultMPEG4GIF {
+	return InlineQueryResultMPEG4GIF{
+		Type: "mpeg4_gif",
+		ID:   id,
+		URL:  url,
+	}
+}
+
+// NewInlineQueryResultPhoto creates a new inline query photo.
+func NewInlineQueryResultPhoto(id, url string) InlineQueryResultPhoto {
+	return InlineQueryResultPhoto{
+		Type: "photo",
+		ID:   id,
+		URL:  url,
+	}
+}
+
+// NewInlineQueryResultPhotoWithThumb creates a new inline query photo.
+func NewInlineQueryResultPhotoWithThumb(id, url, thumb string) InlineQueryResultPhoto {
+	return InlineQueryResultPhoto{
+		Type:     "photo",
+		ID:       id,
+		URL:      url,
+		ThumbURL: thumb,
+	}
+}
+
+// NewInlineQueryResultVideo creates a new inline query video.
+func NewInlineQueryResultVideo(id, url string) InlineQueryResultVideo {
+	return InlineQueryResultVideo{
+		Type: "video",
+		ID:   id,
+		URL:  url,
+	}
+}
+
+// NewInlineQueryResultAudio creates a new inline query audio.
+func NewInlineQueryResultAudio(id, url, title string) InlineQueryResultAudio {
+	return InlineQueryResultAudio{
+		Type:  "audio",
+		ID:    id,
+		URL:   url,
+		Title: title,
+	}
+}
+
+// NewInlineQueryResultVoice creates a new inline query voice.
+func NewInlineQueryResultVoice(id, url, title string) InlineQueryResultVoice {
+	return InlineQueryResultVoice{
+		Type:  "voice",
+		ID:    id,
+		URL:   url,
+		Title: title,
+	}
+}
+
+// NewInlineQueryResultDocument creates a new inline query document.
+func NewInlineQueryResultDocument(id, url, title, mimeType string) InlineQueryResultDocument {
+	return InlineQueryResultDocument{
+		Type:     "document",
+		ID:       id,
+		URL:      url,
+		Title:    title,
+		MimeType: mimeType,
+	}
+}
+
+// NewInlineQueryResultLocation creates a new inline query location.
+func NewInlineQueryResultLocation(id, title string, latitude, longitude float64) InlineQueryResultLocation {
+	return InlineQueryResultLocation{
+		Type:      "location",
+		ID:        id,
+		Title:     title,
+		Latitude:  latitude,
+		Longitude: longitude,
+	}
+}
+
+// NewEditMessageText allows you to edit the text of a message.
+func NewEditMessageText(chatID int64, messageID int, text string) EditMessageTextConfig {
+	return EditMessageTextConfig{
+		BaseEdit: BaseEdit{
+			ChatID:    chatID,
+			MessageID: messageID,
+		},
+		Text: text,
+	}
+}
+
+// NewEditMessageCaption allows you to edit the caption of a message.
+func NewEditMessageCaption(chatID int64, messageID int, caption string) EditMessageCaptionConfig {
+	return EditMessageCaptionConfig{
+		BaseEdit: BaseEdit{
+			ChatID:    chatID,
+			MessageID: messageID,
+		},
+		Caption: caption,
+	}
+}
+
+// NewEditMessageReplyMarkup allows you to edit the inline
+// keyboard markup.
+func NewEditMessageReplyMarkup(chatID int64, messageID int, replyMarkup InlineKeyboardMarkup) EditMessageReplyMarkupConfig {
+	return EditMessageReplyMarkupConfig{
+		BaseEdit: BaseEdit{
+			ChatID:      chatID,
+			MessageID:   messageID,
+			ReplyMarkup: &replyMarkup,
+		},
+	}
+}
+
+// NewHideKeyboard hides the keyboard, with the option for being selective
+// or hiding for everyone.
+func NewHideKeyboard(selective bool) ReplyKeyboardHide {
+	log.Println("NewHideKeyboard is deprecated, please use NewRemoveKeyboard")
+
+	return ReplyKeyboardHide{
+		HideKeyboard: true,
+		Selective:    selective,
+	}
+}
+
+// NewRemoveKeyboard hides the keyboard, with the option for being selective
+// or hiding for everyone.
+func NewRemoveKeyboard(selective bool) ReplyKeyboardRemove {
+	return ReplyKeyboardRemove{
+		RemoveKeyboard: true,
+		Selective:      selective,
+	}
+}
+
+// NewKeyboardButton creates a regular keyboard button.
+func NewKeyboardButton(text string) KeyboardButton {
+	return KeyboardButton{
+		Text: text,
+	}
+}
+
+// NewKeyboardButtonContact creates a keyboard button that requests
+// user contact information upon click.
+func NewKeyboardButtonContact(text string) KeyboardButton {
+	return KeyboardButton{
+		Text:           text,
+		RequestContact: true,
+	}
+}
+
+// NewKeyboardButtonLocation creates a keyboard button that requests
+// user location information upon click.
+func NewKeyboardButtonLocation(text string) KeyboardButton {
+	return KeyboardButton{
+		Text:            text,
+		RequestLocation: true,
+	}
+}
+
+// NewKeyboardButtonRow creates a row of keyboard buttons.
+func NewKeyboardButtonRow(buttons ...KeyboardButton) []KeyboardButton {
+	var row []KeyboardButton
+
+	row = append(row, buttons...)
+
+	return row
+}
+
+// NewReplyKeyboard creates a new regular keyboard with sane defaults.
+func NewReplyKeyboard(rows ...[]KeyboardButton) ReplyKeyboardMarkup {
+	var keyboard [][]KeyboardButton
+
+	keyboard = append(keyboard, rows...)
+
+	return ReplyKeyboardMarkup{
+		ResizeKeyboard: true,
+		Keyboard:       keyboard,
+	}
+}
+
+// NewInlineKeyboardButtonData creates an inline keyboard button with text
+// and data for a callback.
+func NewInlineKeyboardButtonData(text, data string) InlineKeyboardButton {
+	return InlineKeyboardButton{
+		Text:         text,
+		CallbackData: &data,
+	}
+}
+
+// NewInlineKeyboardButtonURL creates an inline keyboard button with text
+// which goes to a URL.
+func NewInlineKeyboardButtonURL(text, url string) InlineKeyboardButton {
+	return InlineKeyboardButton{
+		Text: text,
+		URL:  &url,
+	}
+}
+
+// NewInlineKeyboardButtonSwitch creates an inline keyboard button with
+// text which allows the user to switch to a chat or return to a chat.
+func NewInlineKeyboardButtonSwitch(text, sw string) InlineKeyboardButton {
+	return InlineKeyboardButton{
+		Text:              text,
+		SwitchInlineQuery: &sw,
+	}
+}
+
+// NewInlineKeyboardRow creates an inline keyboard row with buttons.
+func NewInlineKeyboardRow(buttons ...InlineKeyboardButton) []InlineKeyboardButton {
+	var row []InlineKeyboardButton
+
+	row = append(row, buttons...)
+
+	return row
+}
+
+// NewInlineKeyboardMarkup creates a new inline keyboard.
+func NewInlineKeyboardMarkup(rows ...[]InlineKeyboardButton) InlineKeyboardMarkup {
+	var keyboard [][]InlineKeyboardButton
+
+	keyboard = append(keyboard, rows...)
+
+	return InlineKeyboardMarkup{
+		InlineKeyboard: keyboard,
+	}
+}
+
+// NewCallback creates a new callback message.
+func NewCallback(id, text string) CallbackConfig {
+	return CallbackConfig{
+		CallbackQueryID: id,
+		Text:            text,
+		ShowAlert:       false,
+	}
+}
+
+// NewCallbackWithAlert creates a new callback message that alerts
+// the user.
+func NewCallbackWithAlert(id, text string) CallbackConfig {
+	return CallbackConfig{
+		CallbackQueryID: id,
+		Text:            text,
+		ShowAlert:       true,
+	}
+}
+
+// NewInvoice creates a new Invoice request to the user.
+func NewInvoice(chatID int64, title, description, payload, providerToken, startParameter, currency string, prices *[]LabeledPrice) InvoiceConfig {
+	return InvoiceConfig{
+		BaseChat:       BaseChat{ChatID: chatID},
+		Title:          title,
+		Description:    description,
+		Payload:        payload,
+		ProviderToken:  providerToken,
+		StartParameter: startParameter,
+		Currency:       currency,
+		Prices:         prices}
+}
+
+// NewSetChatPhotoUpload creates a new chat photo uploader.
+//
+// chatID is where to send it, file is a string path to the file,
+// FileReader, or FileBytes.
+//
+// Note that you must send animated GIFs as a document.
+func NewSetChatPhotoUpload(chatID int64, file interface{}) SetChatPhotoConfig {
+	return SetChatPhotoConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			File:        file,
+			UseExisting: false,
+		},
+	}
+}
+
+// NewSetChatPhotoShare shares an existing photo.
+// You may use this to reshare an existing photo without reuploading it.
+//
+// chatID is where to send it, fileID is the ID of the file
+// already uploaded.
+func NewSetChatPhotoShare(chatID int64, fileID string) SetChatPhotoConfig {
+	return SetChatPhotoConfig{
+		BaseFile: BaseFile{
+			BaseChat:    BaseChat{ChatID: chatID},
+			FileID:      fileID,
+			UseExisting: true,
+		},
+	}
+}

+ 27 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/log.go

@@ -0,0 +1,27 @@
+package tgbotapi
+
+import (
+	"errors"
+	stdlog "log"
+	"os"
+)
+
+// BotLogger is an interface that represents the required methods to log data.
+//
+// Instead of requiring the standard logger, we can just specify the methods we
+// use and allow users to pass anything that implements these.
+type BotLogger interface {
+	Println(v ...interface{})
+	Printf(format string, v ...interface{})
+}
+
+var log BotLogger = stdlog.New(os.Stderr, "", stdlog.LstdFlags)
+
+// SetLogger specifies the logger that the package should use.
+func SetLogger(logger BotLogger) error {
+	if logger == nil {
+		return errors.New("logger is nil")
+	}
+	log = logger
+	return nil
+}

+ 315 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/passport.go

@@ -0,0 +1,315 @@
+package tgbotapi
+
+// PassportRequestInfoConfig allows you to request passport info
+type PassportRequestInfoConfig struct {
+	BotID     int            `json:"bot_id"`
+	Scope     *PassportScope `json:"scope"`
+	Nonce     string         `json:"nonce"`
+	PublicKey string         `json:"public_key"`
+}
+
+// PassportScopeElement supports using one or one of several elements.
+type PassportScopeElement interface {
+	ScopeType() string
+}
+
+// PassportScope is the requested scopes of data.
+type PassportScope struct {
+	V    int                    `json:"v"`
+	Data []PassportScopeElement `json:"data"`
+}
+
+// PassportScopeElementOneOfSeveral allows you to request any one of the
+// requested documents.
+type PassportScopeElementOneOfSeveral struct {
+}
+
+// ScopeType is the scope type.
+func (eo *PassportScopeElementOneOfSeveral) ScopeType() string {
+	return "one_of"
+}
+
+// PassportScopeElementOne requires the specified element be provided.
+type PassportScopeElementOne struct {
+	Type        string `json:"type"` // One of “personal_details”, “passport”, “driver_license”, “identity_card”, “internal_passport”, “address”, “utility_bill”, “bank_statement”, “rental_agreement”, “passport_registration”, “temporary_registration”, “phone_number”, “email”
+	Selfie      bool   `json:"selfie"`
+	Translation bool   `json:"translation"`
+	NativeNames bool   `json:"native_name"`
+}
+
+// ScopeType is the scope type.
+func (eo *PassportScopeElementOne) ScopeType() string {
+	return "one"
+}
+
+type (
+	// PassportData contains information about Telegram Passport data shared with
+	// the bot by the user.
+	PassportData struct {
+		// Array with information about documents and other Telegram Passport
+		// elements that was shared with the bot
+		Data []EncryptedPassportElement `json:"data"`
+
+		// Encrypted credentials required to decrypt the data
+		Credentials *EncryptedCredentials `json:"credentials"`
+	}
+
+	// PassportFile represents a file uploaded to Telegram Passport. Currently all
+	// Telegram Passport files are in JPEG format when decrypted and don't exceed
+	// 10MB.
+	PassportFile struct {
+		// Unique identifier for this file
+		FileID string `json:"file_id"`
+
+		// File size
+		FileSize int `json:"file_size"`
+
+		// Unix time when the file was uploaded
+		FileDate int64 `json:"file_date"`
+	}
+
+	// EncryptedPassportElement contains information about documents or other
+	// Telegram Passport elements shared with the bot by the user.
+	EncryptedPassportElement struct {
+		// Element type.
+		Type string `json:"type"`
+
+		// Base64-encoded encrypted Telegram Passport element data provided by
+		// the user, available for "personal_details", "passport",
+		// "driver_license", "identity_card", "identity_passport" and "address"
+		// types. Can be decrypted and verified using the accompanying
+		// EncryptedCredentials.
+		Data string `json:"data,omitempty"`
+
+		// User's verified phone number, available only for "phone_number" type
+		PhoneNumber string `json:"phone_number,omitempty"`
+
+		// User's verified email address, available only for "email" type
+		Email string `json:"email,omitempty"`
+
+		// Array of encrypted files with documents provided by the user,
+		// available for "utility_bill", "bank_statement", "rental_agreement",
+		// "passport_registration" and "temporary_registration" types. Files can
+		// be decrypted and verified using the accompanying EncryptedCredentials.
+		Files []PassportFile `json:"files,omitempty"`
+
+		// Encrypted file with the front side of the document, provided by the
+		// user. Available for "passport", "driver_license", "identity_card" and
+		// "internal_passport". The file can be decrypted and verified using the
+		// accompanying EncryptedCredentials.
+		FrontSide *PassportFile `json:"front_side,omitempty"`
+
+		// Encrypted file with the reverse side of the document, provided by the
+		// user. Available for "driver_license" and "identity_card". The file can
+		// be decrypted and verified using the accompanying EncryptedCredentials.
+		ReverseSide *PassportFile `json:"reverse_side,omitempty"`
+
+		// Encrypted file with the selfie of the user holding a document,
+		// provided by the user; available for "passport", "driver_license",
+		// "identity_card" and "internal_passport". The file can be decrypted
+		// and verified using the accompanying EncryptedCredentials.
+		Selfie *PassportFile `json:"selfie,omitempty"`
+	}
+
+	// EncryptedCredentials contains data required for decrypting and
+	// authenticating EncryptedPassportElement. See the Telegram Passport
+	// Documentation for a complete description of the data decryption and
+	// authentication processes.
+	EncryptedCredentials struct {
+		// Base64-encoded encrypted JSON-serialized data with unique user's
+		// payload, data hashes and secrets required for EncryptedPassportElement
+		// decryption and authentication
+		Data string `json:"data"`
+
+		// Base64-encoded data hash for data authentication
+		Hash string `json:"hash"`
+
+		// Base64-encoded secret, encrypted with the bot's public RSA key,
+		// required for data decryption
+		Secret string `json:"secret"`
+	}
+
+	// PassportElementError represents an error in the Telegram Passport element
+	// which was submitted that should be resolved by the user.
+	PassportElementError interface{}
+
+	// PassportElementErrorDataField represents an issue in one of the data
+	// fields that was provided by the user. The error is considered resolved
+	// when the field's value changes.
+	PassportElementErrorDataField struct {
+		// Error source, must be data
+		Source string `json:"source"`
+
+		// The section of the user's Telegram Passport which has the error, one
+		// of "personal_details", "passport", "driver_license", "identity_card",
+		// "internal_passport", "address"
+		Type string `json:"type"`
+
+		// Name of the data field which has the error
+		FieldName string `json:"field_name"`
+
+		// Base64-encoded data hash
+		DataHash string `json:"data_hash"`
+
+		// Error message
+		Message string `json:"message"`
+	}
+
+	// PassportElementErrorFrontSide represents an issue with the front side of
+	// a document. The error is considered resolved when the file with the front
+	// side of the document changes.
+	PassportElementErrorFrontSide struct {
+		// Error source, must be front_side
+		Source string `json:"source"`
+
+		// The section of the user's Telegram Passport which has the issue, one
+		// of "passport", "driver_license", "identity_card", "internal_passport"
+		Type string `json:"type"`
+
+		// Base64-encoded hash of the file with the front side of the document
+		FileHash string `json:"file_hash"`
+
+		// Error message
+		Message string `json:"message"`
+	}
+
+	// PassportElementErrorReverseSide represents an issue with the reverse side
+	// of a document. The error is considered resolved when the file with reverse
+	// side of the document changes.
+	PassportElementErrorReverseSide struct {
+		// Error source, must be reverse_side
+		Source string `json:"source"`
+
+		// The section of the user's Telegram Passport which has the issue, one
+		// of "driver_license", "identity_card"
+		Type string `json:"type"`
+
+		// Base64-encoded hash of the file with the reverse side of the document
+		FileHash string `json:"file_hash"`
+
+		// Error message
+		Message string `json:"message"`
+	}
+
+	// PassportElementErrorSelfie represents an issue with the selfie with a
+	// document. The error is considered resolved when the file with the selfie
+	// changes.
+	PassportElementErrorSelfie struct {
+		// Error source, must be selfie
+		Source string `json:"source"`
+
+		// The section of the user's Telegram Passport which has the issue, one
+		// of "passport", "driver_license", "identity_card", "internal_passport"
+		Type string `json:"type"`
+
+		// Base64-encoded hash of the file with the selfie
+		FileHash string `json:"file_hash"`
+
+		// Error message
+		Message string `json:"message"`
+	}
+
+	// PassportElementErrorFile represents an issue with a document scan. The
+	// error is considered resolved when the file with the document scan changes.
+	PassportElementErrorFile struct {
+		// Error source, must be file
+		Source string `json:"source"`
+
+		// The section of the user's Telegram Passport which has the issue, one
+		// of "utility_bill", "bank_statement", "rental_agreement",
+		// "passport_registration", "temporary_registration"
+		Type string `json:"type"`
+
+		// Base64-encoded file hash
+		FileHash string `json:"file_hash"`
+
+		// Error message
+		Message string `json:"message"`
+	}
+
+	// PassportElementErrorFiles represents an issue with a list of scans. The
+	// error is considered resolved when the list of files containing the scans
+	// changes.
+	PassportElementErrorFiles struct {
+		// Error source, must be files
+		Source string `json:"source"`
+
+		// The section of the user's Telegram Passport which has the issue, one
+		// of "utility_bill", "bank_statement", "rental_agreement",
+		// "passport_registration", "temporary_registration"
+		Type string `json:"type"`
+
+		// List of base64-encoded file hashes
+		FileHashes []string `json:"file_hashes"`
+
+		// Error message
+		Message string `json:"message"`
+	}
+
+	// Credentials contains encrypted data.
+	Credentials struct {
+		Data SecureData `json:"secure_data"`
+		// Nonce the same nonce given in the request
+		Nonce string `json:"nonce"`
+	}
+
+	// SecureData is a map of the fields and their encrypted values.
+	SecureData map[string]*SecureValue
+	// PersonalDetails       *SecureValue `json:"personal_details"`
+	// Passport              *SecureValue `json:"passport"`
+	// InternalPassport      *SecureValue `json:"internal_passport"`
+	// DriverLicense         *SecureValue `json:"driver_license"`
+	// IdentityCard          *SecureValue `json:"identity_card"`
+	// Address               *SecureValue `json:"address"`
+	// UtilityBill           *SecureValue `json:"utility_bill"`
+	// BankStatement         *SecureValue `json:"bank_statement"`
+	// RentalAgreement       *SecureValue `json:"rental_agreement"`
+	// PassportRegistration  *SecureValue `json:"passport_registration"`
+	// TemporaryRegistration *SecureValue `json:"temporary_registration"`
+
+	// SecureValue contains encrypted values for a SecureData item.
+	SecureValue struct {
+		Data        *DataCredentials   `json:"data"`
+		FrontSide   *FileCredentials   `json:"front_side"`
+		ReverseSide *FileCredentials   `json:"reverse_side"`
+		Selfie      *FileCredentials   `json:"selfie"`
+		Translation []*FileCredentials `json:"translation"`
+		Files       []*FileCredentials `json:"files"`
+	}
+
+	// DataCredentials contains information required to decrypt data.
+	DataCredentials struct {
+		// DataHash checksum of encrypted data
+		DataHash string `json:"data_hash"`
+		// Secret of encrypted data
+		Secret string `json:"secret"`
+	}
+
+	// FileCredentials contains information required to decrypt files.
+	FileCredentials struct {
+		// FileHash checksum of encrypted data
+		FileHash string `json:"file_hash"`
+		// Secret of encrypted data
+		Secret string `json:"secret"`
+	}
+
+	// PersonalDetails https://core.telegram.org/passport#personaldetails
+	PersonalDetails struct {
+		FirstName            string `json:"first_name"`
+		LastName             string `json:"last_name"`
+		MiddleName           string `json:"middle_name"`
+		BirthDate            string `json:"birth_date"`
+		Gender               string `json:"gender"`
+		CountryCode          string `json:"country_code"`
+		ResidenceCountryCode string `json:"residence_country_code"`
+		FirstNameNative      string `json:"first_name_native"`
+		LastNameNative       string `json:"last_name_native"`
+		MiddleNameNative     string `json:"middle_name_native"`
+	}
+
+	// IDDocumentData https://core.telegram.org/passport#iddocumentdata
+	IDDocumentData struct {
+		DocumentNumber string `json:"document_no"`
+		ExpiryDate     string `json:"expiry_date"`
+	}
+)

+ 819 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/local/telegram-bot-api/types.go

@@ -0,0 +1,819 @@
+package tgbotapi
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/url"
+	"strings"
+	"time"
+)
+
+// APIResponse is a response from the Telegram API with the result
+// stored raw.
+type APIResponse struct {
+	Ok          bool                `json:"ok"`
+	Result      json.RawMessage     `json:"result"`
+	ErrorCode   int                 `json:"error_code"`
+	Description string              `json:"description"`
+	Parameters  *ResponseParameters `json:"parameters"`
+}
+
+// ResponseParameters are various errors that can be returned in APIResponse.
+type ResponseParameters struct {
+	MigrateToChatID int64 `json:"migrate_to_chat_id"` // optional
+	RetryAfter      int   `json:"retry_after"`        // optional
+}
+
+// Update is an update response, from GetUpdates.
+type Update struct {
+	UpdateID           int                 `json:"update_id"`
+	Message            *Message            `json:"message"`
+	EditedMessage      *Message            `json:"edited_message"`
+	ChannelPost        *Message            `json:"channel_post"`
+	EditedChannelPost  *Message            `json:"edited_channel_post"`
+	InlineQuery        *InlineQuery        `json:"inline_query"`
+	ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result"`
+	CallbackQuery      *CallbackQuery      `json:"callback_query"`
+	ShippingQuery      *ShippingQuery      `json:"shipping_query"`
+	PreCheckoutQuery   *PreCheckoutQuery   `json:"pre_checkout_query"`
+}
+
+// UpdatesChannel is the channel for getting updates.
+type UpdatesChannel <-chan Update
+
+// Clear discards all unprocessed incoming updates.
+func (ch UpdatesChannel) Clear() {
+	for len(ch) != 0 {
+		<-ch
+	}
+}
+
+// User is a user on Telegram.
+type User struct {
+	ID           int    `json:"id"`
+	FirstName    string `json:"first_name"`
+	LastName     string `json:"last_name"`     // optional
+	UserName     string `json:"username"`      // optional
+	LanguageCode string `json:"language_code"` // optional
+	IsBot        bool   `json:"is_bot"`        // optional
+}
+
+// String displays a simple text version of a user.
+//
+// It is normally a user's username, but falls back to a first/last
+// name as available.
+func (u *User) String() string {
+	if u.UserName != "" {
+		return u.UserName
+	}
+
+	name := u.FirstName
+	if u.LastName != "" {
+		name += " " + u.LastName
+	}
+
+	return name
+}
+
+// GroupChat is a group chat.
+type GroupChat struct {
+	ID    int    `json:"id"`
+	Title string `json:"title"`
+}
+
+// ChatPhoto represents a chat photo.
+type ChatPhoto struct {
+	SmallFileID string `json:"small_file_id"`
+	BigFileID   string `json:"big_file_id"`
+}
+
+// Chat contains information about the place a message was sent.
+type Chat struct {
+	ID                  int64      `json:"id"`
+	Type                string     `json:"type"`
+	Title               string     `json:"title"`                          // optional
+	UserName            string     `json:"username"`                       // optional
+	FirstName           string     `json:"first_name"`                     // optional
+	LastName            string     `json:"last_name"`                      // optional
+	AllMembersAreAdmins bool       `json:"all_members_are_administrators"` // optional
+	Photo               *ChatPhoto `json:"photo"`
+	Description         string     `json:"description,omitempty"` // optional
+	InviteLink          string     `json:"invite_link,omitempty"` // optional
+}
+
+// IsPrivate returns if the Chat is a private conversation.
+func (c Chat) IsPrivate() bool {
+	return c.Type == "private"
+}
+
+// IsGroup returns if the Chat is a group.
+func (c Chat) IsGroup() bool {
+	return c.Type == "group"
+}
+
+// IsSuperGroup returns if the Chat is a supergroup.
+func (c Chat) IsSuperGroup() bool {
+	return c.Type == "supergroup"
+}
+
+// IsChannel returns if the Chat is a channel.
+func (c Chat) IsChannel() bool {
+	return c.Type == "channel"
+}
+
+// ChatConfig returns a ChatConfig struct for chat related methods.
+func (c Chat) ChatConfig() ChatConfig {
+	return ChatConfig{ChatID: c.ID}
+}
+
+// Message is returned by almost every request, and contains data about
+// almost anything.
+type Message struct {
+	MessageID             int                `json:"message_id"`
+	From                  *User              `json:"from"` // optional
+	Date                  int                `json:"date"`
+	Chat                  *Chat              `json:"chat"`
+	ForwardFrom           *User              `json:"forward_from"`            // optional
+	ForwardFromChat       *Chat              `json:"forward_from_chat"`       // optional
+	ForwardFromMessageID  int                `json:"forward_from_message_id"` // optional
+	ForwardDate           int                `json:"forward_date"`            // optional
+	ReplyToMessage        *Message           `json:"reply_to_message"`        // optional
+	EditDate              int                `json:"edit_date"`               // optional
+	Text                  string             `json:"text"`                    // optional
+	Entities              *[]MessageEntity   `json:"entities"`                // optional
+	Audio                 *Audio             `json:"audio"`                   // optional
+	Document              *Document          `json:"document"`                // optional
+	Animation             *ChatAnimation     `json:"animation"`               // optional
+	Game                  *Game              `json:"game"`                    // optional
+	Photo                 *[]PhotoSize       `json:"photo"`                   // optional
+	Sticker               *Sticker           `json:"sticker"`                 // optional
+	Video                 *Video             `json:"video"`                   // optional
+	VideoNote             *VideoNote         `json:"video_note"`              // optional
+	Voice                 *Voice             `json:"voice"`                   // optional
+	Caption               string             `json:"caption"`                 // optional
+	Contact               *Contact           `json:"contact"`                 // optional
+	Location              *Location          `json:"location"`                // optional
+	Venue                 *Venue             `json:"venue"`                   // optional
+	NewChatMembers        *[]User            `json:"new_chat_members"`        // optional
+	LeftChatMember        *User              `json:"left_chat_member"`        // optional
+	NewChatTitle          string             `json:"new_chat_title"`          // optional
+	NewChatPhoto          *[]PhotoSize       `json:"new_chat_photo"`          // optional
+	DeleteChatPhoto       bool               `json:"delete_chat_photo"`       // optional
+	GroupChatCreated      bool               `json:"group_chat_created"`      // optional
+	SuperGroupChatCreated bool               `json:"supergroup_chat_created"` // optional
+	ChannelChatCreated    bool               `json:"channel_chat_created"`    // optional
+	MigrateToChatID       int64              `json:"migrate_to_chat_id"`      // optional
+	MigrateFromChatID     int64              `json:"migrate_from_chat_id"`    // optional
+	PinnedMessage         *Message           `json:"pinned_message"`          // optional
+	Invoice               *Invoice           `json:"invoice"`                 // optional
+	SuccessfulPayment     *SuccessfulPayment `json:"successful_payment"`      // optional
+	PassportData          *PassportData      `json:"passport_data,omitempty"` // optional
+}
+
+// Time converts the message timestamp into a Time.
+func (m *Message) Time() time.Time {
+	return time.Unix(int64(m.Date), 0)
+}
+
+// IsCommand returns true if message starts with a "bot_command" entity.
+func (m *Message) IsCommand() bool {
+	if m.Entities == nil || len(*m.Entities) == 0 {
+		return false
+	}
+
+	entity := (*m.Entities)[0]
+	return entity.Offset == 0 && entity.Type == "bot_command"
+}
+
+// Command checks if the message was a command and if it was, returns the
+// command. If the Message was not a command, it returns an empty string.
+//
+// If the command contains the at name syntax, it is removed. Use
+// CommandWithAt() if you do not want that.
+func (m *Message) Command() string {
+	command := m.CommandWithAt()
+
+	if i := strings.Index(command, "@"); i != -1 {
+		command = command[:i]
+	}
+
+	return command
+}
+
+// CommandWithAt checks if the message was a command and if it was, returns the
+// command. If the Message was not a command, it returns an empty string.
+//
+// If the command contains the at name syntax, it is not removed. Use Command()
+// if you want that.
+func (m *Message) CommandWithAt() string {
+	if !m.IsCommand() {
+		return ""
+	}
+
+	// IsCommand() checks that the message begins with a bot_command entity
+	entity := (*m.Entities)[0]
+	return m.Text[1:entity.Length]
+}
+
+// CommandArguments checks if the message was a command and if it was,
+// returns all text after the command name. If the Message was not a
+// command, it returns an empty string.
+//
+// Note: The first character after the command name is omitted:
+// - "/foo bar baz" yields "bar baz", not " bar baz"
+// - "/foo-bar baz" yields "bar baz", too
+// Even though the latter is not a command conforming to the spec, the API
+// marks "/foo" as command entity.
+func (m *Message) CommandArguments() string {
+	if !m.IsCommand() {
+		return ""
+	}
+
+	// IsCommand() checks that the message begins with a bot_command entity
+	entity := (*m.Entities)[0]
+	if len(m.Text) == entity.Length {
+		return "" // The command makes up the whole message
+	}
+
+	return m.Text[entity.Length+1:]
+}
+
+// MessageEntity contains information about data in a Message.
+type MessageEntity struct {
+	Type   string `json:"type"`
+	Offset int    `json:"offset"`
+	Length int    `json:"length"`
+	URL    string `json:"url"`  // optional
+	User   *User  `json:"user"` // optional
+}
+
+// ParseURL attempts to parse a URL contained within a MessageEntity.
+func (entity MessageEntity) ParseURL() (*url.URL, error) {
+	if entity.URL == "" {
+		return nil, errors.New(ErrBadURL)
+	}
+
+	return url.Parse(entity.URL)
+}
+
+// PhotoSize contains information about photos.
+type PhotoSize struct {
+	FileID   string `json:"file_id"`
+	Width    int    `json:"width"`
+	Height   int    `json:"height"`
+	FileSize int    `json:"file_size"` // optional
+}
+
+// Audio contains information about audio.
+type Audio struct {
+	FileID    string `json:"file_id"`
+	Duration  int    `json:"duration"`
+	Performer string `json:"performer"` // optional
+	Title     string `json:"title"`     // optional
+	MimeType  string `json:"mime_type"` // optional
+	FileSize  int    `json:"file_size"` // optional
+}
+
+// Document contains information about a document.
+type Document struct {
+	FileID    string     `json:"file_id"`
+	Thumbnail *PhotoSize `json:"thumb"`     // optional
+	FileName  string     `json:"file_name"` // optional
+	MimeType  string     `json:"mime_type"` // optional
+	FileSize  int        `json:"file_size"` // optional
+}
+
+// Sticker contains information about a sticker.
+type Sticker struct {
+	FileID    string     `json:"file_id"`
+	Width     int        `json:"width"`
+	Height    int        `json:"height"`
+	Thumbnail *PhotoSize `json:"thumb"`     // optional
+	Emoji     string     `json:"emoji"`     // optional
+	FileSize  int        `json:"file_size"` // optional
+	SetName   string     `json:"set_name"`  // optional
+}
+
+// ChatAnimation contains information about an animation.
+type ChatAnimation struct {
+	FileID    string     `json:"file_id"`
+	Width     int        `json:"width"`
+	Height    int        `json:"height"`
+	Duration  int        `json:"duration"`
+	Thumbnail *PhotoSize `json:"thumb"`     // optional
+	FileName  string     `json:"file_name"` // optional
+	MimeType  string     `json:"mime_type"` // optional
+	FileSize  int        `json:"file_size"` // optional
+}
+
+// Video contains information about a video.
+type Video struct {
+	FileID    string     `json:"file_id"`
+	Width     int        `json:"width"`
+	Height    int        `json:"height"`
+	Duration  int        `json:"duration"`
+	Thumbnail *PhotoSize `json:"thumb"`     // optional
+	MimeType  string     `json:"mime_type"` // optional
+	FileSize  int        `json:"file_size"` // optional
+}
+
+// VideoNote contains information about a video.
+type VideoNote struct {
+	FileID    string     `json:"file_id"`
+	Length    int        `json:"length"`
+	Duration  int        `json:"duration"`
+	Thumbnail *PhotoSize `json:"thumb"`     // optional
+	FileSize  int        `json:"file_size"` // optional
+}
+
+// Voice contains information about a voice.
+type Voice struct {
+	FileID   string `json:"file_id"`
+	Duration int    `json:"duration"`
+	MimeType string `json:"mime_type"` // optional
+	FileSize int    `json:"file_size"` // optional
+}
+
+// Contact contains information about a contact.
+//
+// Note that LastName and UserID may be empty.
+type Contact struct {
+	PhoneNumber string `json:"phone_number"`
+	FirstName   string `json:"first_name"`
+	LastName    string `json:"last_name"` // optional
+	UserID      int    `json:"user_id"`   // optional
+}
+
+// Location contains information about a place.
+type Location struct {
+	Longitude float64 `json:"longitude"`
+	Latitude  float64 `json:"latitude"`
+}
+
+// Venue contains information about a venue, including its Location.
+type Venue struct {
+	Location     Location `json:"location"`
+	Title        string   `json:"title"`
+	Address      string   `json:"address"`
+	FoursquareID string   `json:"foursquare_id"` // optional
+}
+
+// UserProfilePhotos contains a set of user profile photos.
+type UserProfilePhotos struct {
+	TotalCount int           `json:"total_count"`
+	Photos     [][]PhotoSize `json:"photos"`
+}
+
+// File contains information about a file to download from Telegram.
+type File struct {
+	FileID   string `json:"file_id"`
+	FileSize int    `json:"file_size"` // optional
+	FilePath string `json:"file_path"` // optional
+}
+
+// Link returns a full path to the download URL for a File.
+//
+// It requires the Bot Token to create the link.
+func (f *File) Link(token string) string {
+	return fmt.Sprintf(FileEndpoint, token, f.FilePath)
+}
+
+// ReplyKeyboardMarkup allows the Bot to set a custom keyboard.
+type ReplyKeyboardMarkup struct {
+	Keyboard        [][]KeyboardButton `json:"keyboard"`
+	ResizeKeyboard  bool               `json:"resize_keyboard"`   // optional
+	OneTimeKeyboard bool               `json:"one_time_keyboard"` // optional
+	Selective       bool               `json:"selective"`         // optional
+}
+
+// KeyboardButton is a button within a custom keyboard.
+type KeyboardButton struct {
+	Text            string `json:"text"`
+	RequestContact  bool   `json:"request_contact"`
+	RequestLocation bool   `json:"request_location"`
+}
+
+// ReplyKeyboardHide allows the Bot to hide a custom keyboard.
+type ReplyKeyboardHide struct {
+	HideKeyboard bool `json:"hide_keyboard"`
+	Selective    bool `json:"selective"` // optional
+}
+
+// ReplyKeyboardRemove allows the Bot to hide a custom keyboard.
+type ReplyKeyboardRemove struct {
+	RemoveKeyboard bool `json:"remove_keyboard"`
+	Selective      bool `json:"selective"`
+}
+
+// InlineKeyboardMarkup is a custom keyboard presented for an inline bot.
+type InlineKeyboardMarkup struct {
+	InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"`
+}
+
+// InlineKeyboardButton is a button within a custom keyboard for
+// inline query responses.
+//
+// Note that some values are references as even an empty string
+// will change behavior.
+//
+// CallbackGame, if set, MUST be first button in first row.
+type InlineKeyboardButton struct {
+	Text                         string        `json:"text"`
+	URL                          *string       `json:"url,omitempty"`                              // optional
+	CallbackData                 *string       `json:"callback_data,omitempty"`                    // optional
+	SwitchInlineQuery            *string       `json:"switch_inline_query,omitempty"`              // optional
+	SwitchInlineQueryCurrentChat *string       `json:"switch_inline_query_current_chat,omitempty"` // optional
+	CallbackGame                 *CallbackGame `json:"callback_game,omitempty"`                    // optional
+	Pay                          bool          `json:"pay,omitempty"`                              // optional
+}
+
+// CallbackQuery is data sent when a keyboard button with callback data
+// is clicked.
+type CallbackQuery struct {
+	ID              string   `json:"id"`
+	From            *User    `json:"from"`
+	Message         *Message `json:"message"`           // optional
+	InlineMessageID string   `json:"inline_message_id"` // optional
+	ChatInstance    string   `json:"chat_instance"`
+	Data            string   `json:"data"`            // optional
+	GameShortName   string   `json:"game_short_name"` // optional
+}
+
+// ForceReply allows the Bot to have users directly reply to it without
+// additional interaction.
+type ForceReply struct {
+	ForceReply bool `json:"force_reply"`
+	Selective  bool `json:"selective"` // optional
+}
+
+// ChatMember is information about a member in a chat.
+type ChatMember struct {
+	User                  *User  `json:"user"`
+	Status                string `json:"status"`
+	UntilDate             int64  `json:"until_date,omitempty"`                // optional
+	CanBeEdited           bool   `json:"can_be_edited,omitempty"`             // optional
+	CanChangeInfo         bool   `json:"can_change_info,omitempty"`           // optional
+	CanPostMessages       bool   `json:"can_post_messages,omitempty"`         // optional
+	CanEditMessages       bool   `json:"can_edit_messages,omitempty"`         // optional
+	CanDeleteMessages     bool   `json:"can_delete_messages,omitempty"`       // optional
+	CanInviteUsers        bool   `json:"can_invite_users,omitempty"`          // optional
+	CanRestrictMembers    bool   `json:"can_restrict_members,omitempty"`      // optional
+	CanPinMessages        bool   `json:"can_pin_messages,omitempty"`          // optional
+	CanPromoteMembers     bool   `json:"can_promote_members,omitempty"`       // optional
+	CanSendMessages       bool   `json:"can_send_messages,omitempty"`         // optional
+	CanSendMediaMessages  bool   `json:"can_send_media_messages,omitempty"`   // optional
+	CanSendOtherMessages  bool   `json:"can_send_other_messages,omitempty"`   // optional
+	CanAddWebPagePreviews bool   `json:"can_add_web_page_previews,omitempty"` // optional
+}
+
+// IsCreator returns if the ChatMember was the creator of the chat.
+func (chat ChatMember) IsCreator() bool { return chat.Status == "creator" }
+
+// IsAdministrator returns if the ChatMember is a chat administrator.
+func (chat ChatMember) IsAdministrator() bool { return chat.Status == "administrator" }
+
+// IsMember returns if the ChatMember is a current member of the chat.
+func (chat ChatMember) IsMember() bool { return chat.Status == "member" }
+
+// HasLeft returns if the ChatMember left the chat.
+func (chat ChatMember) HasLeft() bool { return chat.Status == "left" }
+
+// WasKicked returns if the ChatMember was kicked from the chat.
+func (chat ChatMember) WasKicked() bool { return chat.Status == "kicked" }
+
+// Game is a game within Telegram.
+type Game struct {
+	Title        string          `json:"title"`
+	Description  string          `json:"description"`
+	Photo        []PhotoSize     `json:"photo"`
+	Text         string          `json:"text"`
+	TextEntities []MessageEntity `json:"text_entities"`
+	Animation    Animation       `json:"animation"`
+}
+
+// Animation is a GIF animation demonstrating the game.
+type Animation struct {
+	FileID   string    `json:"file_id"`
+	Thumb    PhotoSize `json:"thumb"`
+	FileName string    `json:"file_name"`
+	MimeType string    `json:"mime_type"`
+	FileSize int       `json:"file_size"`
+}
+
+// GameHighScore is a user's score and position on the leaderboard.
+type GameHighScore struct {
+	Position int  `json:"position"`
+	User     User `json:"user"`
+	Score    int  `json:"score"`
+}
+
+// CallbackGame is for starting a game in an inline keyboard button.
+type CallbackGame struct{}
+
+// WebhookInfo is information about a currently set webhook.
+type WebhookInfo struct {
+	URL                  string `json:"url"`
+	HasCustomCertificate bool   `json:"has_custom_certificate"`
+	PendingUpdateCount   int    `json:"pending_update_count"`
+	LastErrorDate        int    `json:"last_error_date"`    // optional
+	LastErrorMessage     string `json:"last_error_message"` // optional
+}
+
+// IsSet returns true if a webhook is currently set.
+func (info WebhookInfo) IsSet() bool {
+	return info.URL != ""
+}
+
+// InputMediaPhoto contains a photo for displaying as part of a media group.
+type InputMediaPhoto struct {
+	Type      string `json:"type"`
+	Media     string `json:"media"`
+	Caption   string `json:"caption"`
+	ParseMode string `json:"parse_mode"`
+}
+
+// InputMediaVideo contains a video for displaying as part of a media group.
+type InputMediaVideo struct {
+	Type  string `json:"type"`
+	Media string `json:"media"`
+	// thumb intentionally missing as it is not currently compatible
+	Caption           string `json:"caption"`
+	ParseMode         string `json:"parse_mode"`
+	Width             int    `json:"width"`
+	Height            int    `json:"height"`
+	Duration          int    `json:"duration"`
+	SupportsStreaming bool   `json:"supports_streaming"`
+}
+
+// InlineQuery is a Query from Telegram for an inline request.
+type InlineQuery struct {
+	ID       string    `json:"id"`
+	From     *User     `json:"from"`
+	Location *Location `json:"location"` // optional
+	Query    string    `json:"query"`
+	Offset   string    `json:"offset"`
+}
+
+// InlineQueryResultArticle is an inline query response article.
+type InlineQueryResultArticle struct {
+	Type                string                `json:"type"`                            // required
+	ID                  string                `json:"id"`                              // required
+	Title               string                `json:"title"`                           // required
+	InputMessageContent interface{}           `json:"input_message_content,omitempty"` // required
+	ReplyMarkup         *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
+	URL                 string                `json:"url"`
+	HideURL             bool                  `json:"hide_url"`
+	Description         string                `json:"description"`
+	ThumbURL            string                `json:"thumb_url"`
+	ThumbWidth          int                   `json:"thumb_width"`
+	ThumbHeight         int                   `json:"thumb_height"`
+}
+
+// InlineQueryResultPhoto is an inline query response photo.
+type InlineQueryResultPhoto struct {
+	Type                string                `json:"type"`      // required
+	ID                  string                `json:"id"`        // required
+	URL                 string                `json:"photo_url"` // required
+	MimeType            string                `json:"mime_type"`
+	Width               int                   `json:"photo_width"`
+	Height              int                   `json:"photo_height"`
+	ThumbURL            string                `json:"thumb_url"`
+	Title               string                `json:"title"`
+	Description         string                `json:"description"`
+	Caption             string                `json:"caption"`
+	ReplyMarkup         *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
+	InputMessageContent interface{}           `json:"input_message_content,omitempty"`
+}
+
+// InlineQueryResultGIF is an inline query response GIF.
+type InlineQueryResultGIF struct {
+	Type                string                `json:"type"`    // required
+	ID                  string                `json:"id"`      // required
+	URL                 string                `json:"gif_url"` // required
+	Width               int                   `json:"gif_width"`
+	Height              int                   `json:"gif_height"`
+	Duration            int                   `json:"gif_duration"`
+	ThumbURL            string                `json:"thumb_url"`
+	Title               string                `json:"title"`
+	Caption             string                `json:"caption"`
+	ReplyMarkup         *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
+	InputMessageContent interface{}           `json:"input_message_content,omitempty"`
+}
+
+// InlineQueryResultMPEG4GIF is an inline query response MPEG4 GIF.
+type InlineQueryResultMPEG4GIF struct {
+	Type                string                `json:"type"`      // required
+	ID                  string                `json:"id"`        // required
+	URL                 string                `json:"mpeg4_url"` // required
+	Width               int                   `json:"mpeg4_width"`
+	Height              int                   `json:"mpeg4_height"`
+	Duration            int                   `json:"mpeg4_duration"`
+	ThumbURL            string                `json:"thumb_url"`
+	Title               string                `json:"title"`
+	Caption             string                `json:"caption"`
+	ReplyMarkup         *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
+	InputMessageContent interface{}           `json:"input_message_content,omitempty"`
+}
+
+// InlineQueryResultVideo is an inline query response video.
+type InlineQueryResultVideo struct {
+	Type                string                `json:"type"`      // required
+	ID                  string                `json:"id"`        // required
+	URL                 string                `json:"video_url"` // required
+	MimeType            string                `json:"mime_type"` // required
+	ThumbURL            string                `json:"thumb_url"`
+	Title               string                `json:"title"`
+	Caption             string                `json:"caption"`
+	Width               int                   `json:"video_width"`
+	Height              int                   `json:"video_height"`
+	Duration            int                   `json:"video_duration"`
+	Description         string                `json:"description"`
+	ReplyMarkup         *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
+	InputMessageContent interface{}           `json:"input_message_content,omitempty"`
+}
+
+// InlineQueryResultAudio is an inline query response audio.
+type InlineQueryResultAudio struct {
+	Type                string                `json:"type"`      // required
+	ID                  string                `json:"id"`        // required
+	URL                 string                `json:"audio_url"` // required
+	Title               string                `json:"title"`     // required
+	Caption             string                `json:"caption"`
+	Performer           string                `json:"performer"`
+	Duration            int                   `json:"audio_duration"`
+	ReplyMarkup         *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
+	InputMessageContent interface{}           `json:"input_message_content,omitempty"`
+}
+
+// InlineQueryResultVoice is an inline query response voice.
+type InlineQueryResultVoice struct {
+	Type                string                `json:"type"`      // required
+	ID                  string                `json:"id"`        // required
+	URL                 string                `json:"voice_url"` // required
+	Title               string                `json:"title"`     // required
+	Caption             string                `json:"caption"`
+	Duration            int                   `json:"voice_duration"`
+	ReplyMarkup         *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
+	InputMessageContent interface{}           `json:"input_message_content,omitempty"`
+}
+
+// InlineQueryResultDocument is an inline query response document.
+type InlineQueryResultDocument struct {
+	Type                string                `json:"type"`  // required
+	ID                  string                `json:"id"`    // required
+	Title               string                `json:"title"` // required
+	Caption             string                `json:"caption"`
+	URL                 string                `json:"document_url"` // required
+	MimeType            string                `json:"mime_type"`    // required
+	Description         string                `json:"description"`
+	ReplyMarkup         *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
+	InputMessageContent interface{}           `json:"input_message_content,omitempty"`
+	ThumbURL            string                `json:"thumb_url"`
+	ThumbWidth          int                   `json:"thumb_width"`
+	ThumbHeight         int                   `json:"thumb_height"`
+}
+
+// InlineQueryResultLocation is an inline query response location.
+type InlineQueryResultLocation struct {
+	Type                string                `json:"type"`      // required
+	ID                  string                `json:"id"`        // required
+	Latitude            float64               `json:"latitude"`  // required
+	Longitude           float64               `json:"longitude"` // required
+	Title               string                `json:"title"`     // required
+	ReplyMarkup         *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
+	InputMessageContent interface{}           `json:"input_message_content,omitempty"`
+	ThumbURL            string                `json:"thumb_url"`
+	ThumbWidth          int                   `json:"thumb_width"`
+	ThumbHeight         int                   `json:"thumb_height"`
+}
+
+// InlineQueryResultGame is an inline query response game.
+type InlineQueryResultGame struct {
+	Type          string                `json:"type"`
+	ID            string                `json:"id"`
+	GameShortName string                `json:"game_short_name"`
+	ReplyMarkup   *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
+}
+
+// ChosenInlineResult is an inline query result chosen by a User
+type ChosenInlineResult struct {
+	ResultID        string    `json:"result_id"`
+	From            *User     `json:"from"`
+	Location        *Location `json:"location"`
+	InlineMessageID string    `json:"inline_message_id"`
+	Query           string    `json:"query"`
+}
+
+// InputTextMessageContent contains text for displaying
+// as an inline query result.
+type InputTextMessageContent struct {
+	Text                  string `json:"message_text"`
+	ParseMode             string `json:"parse_mode"`
+	DisableWebPagePreview bool   `json:"disable_web_page_preview"`
+}
+
+// InputLocationMessageContent contains a location for displaying
+// as an inline query result.
+type InputLocationMessageContent struct {
+	Latitude  float64 `json:"latitude"`
+	Longitude float64 `json:"longitude"`
+}
+
+// InputVenueMessageContent contains a venue for displaying
+// as an inline query result.
+type InputVenueMessageContent struct {
+	Latitude     float64 `json:"latitude"`
+	Longitude    float64 `json:"longitude"`
+	Title        string  `json:"title"`
+	Address      string  `json:"address"`
+	FoursquareID string  `json:"foursquare_id"`
+}
+
+// InputContactMessageContent contains a contact for displaying
+// as an inline query result.
+type InputContactMessageContent struct {
+	PhoneNumber string `json:"phone_number"`
+	FirstName   string `json:"first_name"`
+	LastName    string `json:"last_name"`
+}
+
+// Invoice contains basic information about an invoice.
+type Invoice struct {
+	Title          string `json:"title"`
+	Description    string `json:"description"`
+	StartParameter string `json:"start_parameter"`
+	Currency       string `json:"currency"`
+	TotalAmount    int    `json:"total_amount"`
+}
+
+// LabeledPrice represents a portion of the price for goods or services.
+type LabeledPrice struct {
+	Label  string `json:"label"`
+	Amount int    `json:"amount"`
+}
+
+// ShippingAddress represents a shipping address.
+type ShippingAddress struct {
+	CountryCode string `json:"country_code"`
+	State       string `json:"state"`
+	City        string `json:"city"`
+	StreetLine1 string `json:"street_line1"`
+	StreetLine2 string `json:"street_line2"`
+	PostCode    string `json:"post_code"`
+}
+
+// OrderInfo represents information about an order.
+type OrderInfo struct {
+	Name            string           `json:"name,omitempty"`
+	PhoneNumber     string           `json:"phone_number,omitempty"`
+	Email           string           `json:"email,omitempty"`
+	ShippingAddress *ShippingAddress `json:"shipping_address,omitempty"`
+}
+
+// ShippingOption represents one shipping option.
+type ShippingOption struct {
+	ID     string          `json:"id"`
+	Title  string          `json:"title"`
+	Prices *[]LabeledPrice `json:"prices"`
+}
+
+// SuccessfulPayment contains basic information about a successful payment.
+type SuccessfulPayment struct {
+	Currency                string     `json:"currency"`
+	TotalAmount             int        `json:"total_amount"`
+	InvoicePayload          string     `json:"invoice_payload"`
+	ShippingOptionID        string     `json:"shipping_option_id,omitempty"`
+	OrderInfo               *OrderInfo `json:"order_info,omitempty"`
+	TelegramPaymentChargeID string     `json:"telegram_payment_charge_id"`
+	ProviderPaymentChargeID string     `json:"provider_payment_charge_id"`
+}
+
+// ShippingQuery contains information about an incoming shipping query.
+type ShippingQuery struct {
+	ID              string           `json:"id"`
+	From            *User            `json:"from"`
+	InvoicePayload  string           `json:"invoice_payload"`
+	ShippingAddress *ShippingAddress `json:"shipping_address"`
+}
+
+// PreCheckoutQuery contains information about an incoming pre-checkout query.
+type PreCheckoutQuery struct {
+	ID               string     `json:"id"`
+	From             *User      `json:"from"`
+	Currency         string     `json:"currency"`
+	TotalAmount      int        `json:"total_amount"`
+	InvoicePayload   string     `json:"invoice_payload"`
+	ShippingOptionID string     `json:"shipping_option_id,omitempty"`
+	OrderInfo        *OrderInfo `json:"order_info,omitempty"`
+}
+
+// Error is an error containing extra information returned by the Telegram API.
+type Error struct {
+	Message string
+	ResponseParameters
+}
+
+func (e Error) Error() string {
+	return e.Message
+}

+ 45 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/readme.md

@@ -0,0 +1,45 @@
+Тема: менеджер задач в телеграм боте
+
+Боты в месседжерах сейчас используются повсеместно и конечно же в рамках курса стоило бы написать одного :)
+
+В этой задаче мы реализуем простого телеграм-бота по управлению задачами. Немого парсинга, немного моделирования данных, научитесь размещать ботов в телеграме.
+
+Здание ИМХО простое, сильно голову ломать не придётся! Объем кода 300-400 строк.
+Управление происходит через текстовый интерфейс:
+
+* /tasks
+* /new XXX YYY ZZZ - создаёт новую задачу
+* /assign_$ID - делаеть пользователя исполнителем задачи
+* /unassign_$ID - снимает задачу с текущего исполнителя
+* /resolve_$ID - выполняет задачу, удаляет её из списка
+* /my - показывает задачи, которые назначены на меня
+* /owner - показывает задачи которые были созданы мной
+
+Подробности форматирования смотрите в тестах.
+
+В этом задании вам надо будет разделить ваше приложение на слои, как было рассказано в лекции:
+* delivery (handler в лекции) - в этом слое вам надо будет взаимодействовать с телеграмом
+* service - в этом слое у вас будет бизнес логика. Про то что слой выше работает с телеграмом - тут знать нельзя. Если вы к примеру решите переехать в Slack или любой другой мессенджер - в этом слое ничего не поменяется
+* repository (repo / storage в лекции) - тут у вас будут храниться данные, пока - в слайсе. Если вы будете использовать сначала слайс, а потом решите вкрутить базу - то в других слоях ничего поменяться не должно
+
+Мы используем чуть-чуть подправленную библиотеку для работы с телеграм-ботом, чтобы можно было написать тесты, притворившись сервером телеграма. Она переопределена через replace в go.mod. Тесты запускаются с ней нормально через go test -v. Если вдруг вы получаете ошибку `./taskbot_test.go:172:23: cannot assign to tgbotapi.APIEndpoint (declared const)` - значит что-то у вас с переопределением сломалось
+
+Посмотреть в реальной жизни ваш прошект можно при помощи следующих инструментов:
+* ngrok (https://ngrok.com/) - пробрасывает порт к вам (после авторизации там будет команда для созранения токена в конфиге). Хорошо чтобы показать что-то на демо, для прода я бы не рискнул это использовать.
+
+Для создания бота в телеграме стучитесь @BotFather
+
+Для хранения данных сейчас можно использовать слайс и не пока заморачиваться с базой. Или заморачивайтесь :)
+
+В помощь:
+* телеграм бот из лекции в папке 4/bot
+
+Бота надо стартовать на порту 8081 - именно туда шлются уведомления из моего эмулятора телеграма
+
+у вас будет несколько слоев:
+* `delivery` - взаимодействиt с телеграмом - другие слои не должны знать что они в телеге. в это слое вы получаете сообщения от телеги, вызываете следующий слой, получаете результат и возвращаете его в телегу. если вы захотите подменить телегу на ICQ-бота - поменяется только этот слой
+* `router` - у нас несколько разных команд - вам надо распарсить команду и вызвать соответствующий метод. в хттп с этим было попроще, а тут придется делать это руками
+* `service` - тут будет сосредоточена ваша бизнес-логика
+* `repository` - в этом слое вы храните данные. в следующей части домашке сюда надо будет вкрутить БД и это должно пройти абсолютно безболезненно для остального кода
+
+Поначалу можно все делать одной папке, но можете потом попробовать разнести по папкам в соответтсвии с https://github.com/golang-standards/project-layout - позже в лекциях будет про это расказано, можете потом вернуться и докрутить для практики.

+ 31 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/taskbot.go

@@ -0,0 +1,31 @@
+package main
+
+import (
+	"context"
+	"log"
+
+	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
+)
+
+func startTaskBot(ctx context.Context, httpListenAddr string) error {
+	// сюда писать код
+	/*
+		в этом месте вы стартуете бота,
+		стартуете хттп сервер который будет обслуживать этого бота
+		инициализируете ваше приложение
+		и потом будете обрабатывать входящие сообщения
+	*/
+	return nil
+}
+
+func main() {
+	err := startTaskBot(context.Background(), ":8081")
+	if err != nil {
+		log.Fatalln(err)
+	}
+}
+
+//  это заглушка чтобы импорт сохранился
+func __dummy() {
+	tgbotapi.APIEndpoint = "_dummy"
+}

+ 395 - 0
courses/golang_web/golang_web_services_2024-04-26/10/99_hw/taskbot/taskbot_test.go

@@ -0,0 +1,395 @@
+package main
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"reflect"
+	"strconv"
+	"sync/atomic"
+
+	// "encoding/json"
+	"fmt"
+
+	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
+
+	// "io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"sync"
+	"testing"
+	"time"
+)
+
+func init() {
+	// upd global var for testing
+	// we use patched version of gopkg.in/telegram-bot-api.v4 ( WebhookURL const -> var)
+	WebhookURL = "http://127.0.0.1:8081"
+	BotToken = "_golangcourse_test"
+}
+
+var (
+	client = &http.Client{Timeout: time.Second}
+)
+
+// TDS is Telegram Dummy Server
+type TDS struct {
+	*sync.Mutex
+	Answers map[int]string
+}
+
+func NewTDS() *TDS {
+	return &TDS{
+		Mutex:   &sync.Mutex{},
+		Answers: make(map[int]string),
+	}
+}
+
+func (srv *TDS) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	mux := http.NewServeMux()
+	mux.HandleFunc("/getMe", func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte(`{"ok":true,"result":{"id":` +
+			strconv.Itoa(BotChatID) +
+			`,"is_bot":true,"first_name":"game_test_bot","username":"game_test_bot"}}`))
+	})
+	mux.HandleFunc("/setWebhook", func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte(`{"ok":true,"result":true,"description":"Webhook was set"}`))
+	})
+	mux.HandleFunc("/sendMessage", func(w http.ResponseWriter, r *http.Request) {
+		chatID, _ := strconv.Atoi(r.FormValue("chat_id"))
+		text := r.FormValue("text")
+		srv.Lock()
+		srv.Answers[chatID] = text
+		srv.Unlock()
+
+		//fmt.Println("TDS sendMessage", chatID, text)
+	})
+
+	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		panic(fmt.Errorf("unknown command %s", r.URL.Path))
+	})
+
+	handler := http.StripPrefix("/bot"+BotToken, mux)
+	handler.ServeHTTP(w, r)
+}
+
+const (
+	Ivanov     int = 256
+	Petrov     int = 512
+	Alexandrov int = 1024
+	BotChatID      = 100500
+)
+
+var (
+	users = map[int]*tgbotapi.User{
+		Ivanov: &tgbotapi.User{
+			ID:           Ivanov,
+			FirstName:    "Ivan",
+			LastName:     "Ivanov",
+			UserName:     "ivanov",
+			LanguageCode: "ru",
+			IsBot:        false,
+		},
+		Petrov: &tgbotapi.User{
+			ID:           Petrov,
+			FirstName:    "Petr",
+			LastName:     "Pertov",
+			UserName:     "ppetrov",
+			LanguageCode: "ru",
+			IsBot:        false,
+		},
+		Alexandrov: &tgbotapi.User{
+			ID:           Alexandrov,
+			FirstName:    "Alex",
+			LastName:     "Alexandrov",
+			UserName:     "aalexandrov",
+			LanguageCode: "ru",
+			IsBot:        false,
+		},
+	}
+
+	updID uint64
+	msgID uint64
+)
+
+func SendMsgToBot(userID int, text string) error {
+	// reqText := `{
+	// 	"update_id":175894614,
+	// 	"message":{
+	// 		"message_id":29,
+	// 		"from":{"id":133250764,"is_bot":false,"first_name":"Vasily Romanov","username":"rvasily","language_code":"ru"},
+	// 		"chat":{"id":133250764,"first_name":"Vasily Romanov","username":"rvasily","type":"private"},
+	// 		"date":1512168732,
+	// 		"text":"THIS SEND FROM USER"
+	// 	}
+	// }`
+
+	atomic.AddUint64(&updID, 1)
+	myUpdID := atomic.LoadUint64(&updID)
+
+	// better have it per user, but lazy now
+	atomic.AddUint64(&msgID, 1)
+	myMsgID := atomic.LoadUint64(&msgID)
+
+	user, ok := users[userID]
+	if !ok {
+		return fmt.Errorf("no user %v", userID)
+	}
+
+	upd := &tgbotapi.Update{
+		UpdateID: int(myUpdID),
+		Message: &tgbotapi.Message{
+			MessageID: int(myMsgID),
+			From:      user,
+			Chat: &tgbotapi.Chat{
+				ID:        int64(user.ID),
+				FirstName: user.FirstName,
+				UserName:  user.UserName,
+				Type:      "private",
+			},
+			Text: text,
+			Date: int(time.Now().Unix()),
+		},
+	}
+	reqData, _ := json.Marshal(upd)
+
+	reqBody := bytes.NewBuffer(reqData)
+	req, _ := http.NewRequest(http.MethodPost, WebhookURL, reqBody)
+	_, err := client.Do(req)
+	return err
+}
+
+type testCase struct {
+	user    int
+	command string
+	answers map[int]string
+}
+
+func TestTasks(t *testing.T) {
+
+	tds := NewTDS() // это мой эмулятор телеграм сервера
+	ts := httptest.NewServer(tds)
+	tgbotapi.APIEndpoint = ts.URL + "/bot%s/%s"
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+	go func() {
+		err := startTaskBot(ctx, ":8081")
+		if err != nil {
+			t.Fatalf("startTaskBot error: %s", err)
+		}
+	}()
+
+	// give server time to start
+	time.Sleep(10 * time.Millisecond)
+
+	cases := []testCase{
+		{
+			// команда /tasks - выводит список всех активных задач
+			Ivanov,
+			"/tasks",
+			map[int]string{
+				Ivanov: "Нет задач",
+			},
+		},
+		{
+			// команда /new - создаёт новую задачу, всё что после /new - идёт в название задачи
+			Ivanov,
+			"/new написать бота",
+			map[int]string{
+				Ivanov: `Задача "написать бота" создана, id=1`,
+			},
+		},
+		{
+			Ivanov,
+			"/tasks",
+			map[int]string{
+				Ivanov: `1. написать бота by @ivanov
+/assign_1`,
+			},
+		},
+		{
+			// /assign_* - назначает задачу на себя
+			Alexandrov,
+			"/assign_1",
+			map[int]string{
+				Alexandrov: `Задача "написать бота" назначена на вас`,
+				Ivanov:     `Задача "написать бота" назначена на @aalexandrov`,
+			},
+		},
+		{
+			// в случае если задача была назначена на кого-то - он получает уведомление об этом
+			// в данном случае она была назначена на Alexandrov, поэтому ему отправляется уведомление
+			Petrov,
+			"/assign_1",
+			map[int]string{
+				Petrov:     `Задача "написать бота" назначена на вас`,
+				Alexandrov: `Задача "написать бота" назначена на @ppetrov`,
+			},
+		},
+		{
+			// если задача назначена и на мне - показывается "на меня"
+			Petrov,
+			"/tasks",
+			map[int]string{
+				Petrov: `1. написать бота by @ivanov
+assignee: я
+/unassign_1 /resolve_1`,
+			},
+		},
+		{
+			// если задача назначена и не на мне - показывается логин исполнителя
+			// при
+			Ivanov,
+			"/tasks",
+			map[int]string{
+				Ivanov: `1. написать бота by @ivanov
+assignee: @ppetrov`,
+			},
+		},
+
+		{
+			// /unassign_ - снимает задачу с себя
+			// нельзя снять задачу которая не на вас
+			Alexandrov,
+			"/unassign_1",
+			map[int]string{
+				Alexandrov: `Задача не на вас`,
+			},
+		},
+
+		{
+			// /unassign_ - снимает задачу с себя
+			// автору отправляется уведомление что задача осталась без исполнителя
+			Petrov,
+			"/unassign_1",
+			map[int]string{
+				Petrov: `Принято`,
+				Ivanov: `Задача "написать бота" осталась без исполнителя`,
+			},
+		},
+
+		{
+			// повтор
+			// в случае если задача была назначена на кого-то - автор получает уведомление об этом
+			Petrov,
+			"/assign_1",
+			map[int]string{
+				Petrov: `Задача "написать бота" назначена на вас`,
+				Ivanov: `Задача "написать бота" назначена на @ppetrov`,
+			},
+		},
+		{
+			// /resolve_* завершает задачу, удаляет её из хранилища
+			// автору отправляется уведомление об этом
+			Petrov,
+			"/resolve_1",
+			map[int]string{
+				Petrov: `Задача "написать бота" выполнена`,
+				Ivanov: `Задача "написать бота" выполнена @ppetrov`,
+			},
+		},
+
+		{
+			Petrov,
+			"/tasks",
+			map[int]string{
+				Petrov: `Нет задач`,
+			},
+		},
+
+		{
+			// обратите внимание, id=2 - автоинкремент
+			Petrov,
+			"/new сделать ДЗ по курсу",
+			map[int]string{
+				Petrov: `Задача "сделать ДЗ по курсу" создана, id=2`,
+			},
+		},
+		{
+			// обратите внимание, id=3 - автоинкремент
+			Ivanov,
+			"/new прийти на хакатон",
+			map[int]string{
+				Ivanov: `Задача "прийти на хакатон" создана, id=3`,
+			},
+		},
+		{
+			Petrov,
+			"/tasks",
+			map[int]string{
+				Petrov: `2. сделать ДЗ по курсу by @ppetrov
+/assign_2
+
+3. прийти на хакатон by @ivanov
+/assign_3`,
+			},
+		},
+		{
+			// повтор
+			// в случае если задача была назначена на кого-то - автор получает уведомление об этом
+			// если он автор задачи - ему не приходит дополнительного уведомления о том что она назначена на кого-то
+			Petrov,
+			"/assign_2",
+			map[int]string{
+				Petrov: `Задача "сделать ДЗ по курсу" назначена на вас`,
+			},
+		},
+		{
+			Petrov,
+			"/tasks",
+			map[int]string{
+				Petrov: `2. сделать ДЗ по курсу by @ppetrov
+assignee: я
+/unassign_2 /resolve_2
+
+3. прийти на хакатон by @ivanov
+/assign_3`,
+			},
+		},
+		{
+			// /my показывает задачи которые назначены на меня
+			// при этому тут нет метки assegnee
+			Petrov,
+			"/my",
+			map[int]string{
+				Petrov: `2. сделать ДЗ по курсу by @ppetrov
+/unassign_2 /resolve_2`,
+			},
+		},
+		{
+			// /owner - показывает задачи, которы я создал
+			// при этому тут нет метки assegnee
+			Ivanov,
+			"/owner",
+			map[int]string{
+				Ivanov: `3. прийти на хакатон by @ivanov
+/assign_3`,
+			},
+		},
+	}
+
+	for idx, item := range cases {
+
+		tds.Lock()
+		tds.Answers = make(map[int]string)
+		tds.Unlock()
+
+		caseName := fmt.Sprintf("[case%d, %d: %s]", idx, item.user, item.command)
+		err := SendMsgToBot(item.user, item.command)
+		if err != nil {
+			t.Fatalf("%s SendMsgToBot error: %s", caseName, err)
+		}
+		// give TDS time to process request
+		time.Sleep(10 * time.Millisecond)
+
+		tds.Lock()
+		result := reflect.DeepEqual(tds.Answers, item.answers)
+		if !result {
+			t.Fatalf("%s bad results:\n\tWant: %v\n\tHave: %v", caseName, item.answers, tds.Answers)
+		}
+		tds.Unlock()
+
+	}
+
+}

+ 12 - 0
courses/golang_web/golang_web_services_2024-04-26/10/oauth/go.mod

@@ -0,0 +1,12 @@
+module oauth
+
+go 1.20
+
+require golang.org/x/oauth2 v0.15.0
+
+require (
+	github.com/golang/protobuf v1.5.3 // indirect
+	golang.org/x/net v0.19.0 // indirect
+	google.golang.org/appengine v1.6.7 // indirect
+	google.golang.org/protobuf v1.31.0 // indirect
+)

+ 23 - 0
courses/golang_web/golang_web_services_2024-04-26/10/oauth/go.sum

@@ -0,0 +1,23 @@
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
+golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

+ 105 - 0
courses/golang_web/golang_web_services_2024-04-26/10/oauth/main.go

@@ -0,0 +1,105 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"strconv"
+
+	"golang.org/x/oauth2"
+	"golang.org/x/oauth2/vk"
+)
+
+const (
+	APP_ID     = "7065390"              // вам надо заменить это значение на свое на https://vk.com/apps?act=manage https://dev.vk.com/ru/api/getting-started
+	APP_KEY    = "cQZe3Vvo4mHotmetUdXK" // вам надо заменить это значение на свое на https://vk.com/apps?act=manage https://dev.vk.com/ru/api/getting-started
+	APP_SECRET = "1bbf49951bbf49951bbf49953b1bd486bb11bbf1bbf4995468b3d76e2cb2114610654e0"
+	API_URL    = "https://api.vk.com/method/users.get?fields=email,photo_50&access_token=%s&v=5.131"
+
+	AUTH_URL = "https://oauth.vk.com/authorize?client_id=7065390&redirect_uri=http://localhost:8080/&response_type=code&scope=email"
+)
+
+type Response struct {
+	Response []struct {
+		FirstName string `json:"first_name"`
+		Photo     string `json:"photo_50"`
+	}
+}
+
+// https://oauth.vk.com/authorize?client_id=7065390&redirect_uri=http://localhost:8080/&response_type=code&scope=email
+
+func main() {
+	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		ctx := r.Context()
+		code := r.FormValue("code")
+
+		if code == "" {
+			w.Write([]byte(`<div><a href="` + AUTH_URL + `">authorize</a></div>`))
+			return
+		}
+
+		conf := oauth2.Config{
+			ClientID:     APP_ID,
+			ClientSecret: APP_KEY,
+			RedirectURL:  "http://localhost:8080/",
+			Endpoint:     vk.Endpoint,
+		}
+
+		token, err := conf.Exchange(ctx, code)
+		if err != nil {
+			log.Println("cannot exchange", err)
+			http.Error(w, err.Error(), 500)
+			return
+		}
+
+		email := token.Extra("email").(string)
+		userIDraw := token.Extra("user_id").(float64)
+		userID := int(userIDraw)
+
+		w.Write([]byte(`
+		<div> Oauth token:<br>
+			` + fmt.Sprintf("%#v", token) + `
+		</div>
+		<div>Email: ` + email + `</div>
+		<div>UserID: ` + strconv.Itoa(userID) + `</div>
+		<br>
+		`))
+
+		client := conf.Client(ctx, token)
+		resp, err := client.Get(fmt.Sprintf(API_URL, token.AccessToken))
+		if err != nil {
+			log.Println("cannot request data", err)
+			http.Error(w, err.Error(), 500)
+			return
+		}
+
+		defer resp.Body.Close()
+
+		body, err := ioutil.ReadAll(resp.Body)
+		if err != nil {
+			log.Println("cannot read buffer", err)
+			http.Error(w, err.Error(), 500)
+			return
+		}
+
+		data := &Response{}
+		err = json.Unmarshal(body, data)
+		if err != nil {
+			log.Println("cannot json.Unmarshal", err)
+			http.Error(w, err.Error(), 500)
+			return
+		}
+
+		w.Write([]byte(`
+		<div>
+			<img src="` + data.Response[0].Photo + `"/>
+			` + data.Response[0].FirstName + `
+		</div>
+		`))
+	})
+
+	log.Println("starting server at :8080")
+	http.ListenAndServe(":8080", nil)
+}

+ 26 - 0
courses/golang_web/golang_web_services_2024-04-26/10/oauth/notes.txt

@@ -0,0 +1,26 @@
+https://vk.com/apps?act=manage
+https://dev.vk.com/ru/api/getting-started
+
+-----
+
+https://sequencediagram.org/
+
+title OAuth авторизация
+
+Client->Photolist:Авторизоваться через ВК
+Client<--Photolist:302 https://oauth.vk.com/authorize
+
+Client->VK:Авторизоваться через ВК
+Client<--VK:302 https://example.com/user/login?code=123456
+
+Client->Photolist:https://example.com/user/login?code=123456
+
+Photolist->VK: Code
+Photolist<-VK: Access token\nEmail\nUser ID
+
+Photolist->VK: Get info
+Photolist<-VK: Avatar URL
+
+note over Photolist:Create user\nCreate session
+
+Client<-Photolist:SetCookie

+ 13 - 0
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/Makefile

@@ -0,0 +1,13 @@
+COMMIT?=$(shell git rev-parse --short HEAD)
+BUILD_TIME?=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
+
+all: run
+
+run: 
+	@echo "-- starting app"
+	go run .
+
+.PHONY: dc
+dc: 
+	@echo "-- starting docker compose"
+	docker-compose up

+ 45 - 0
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/_mysql/db_init.sql

@@ -0,0 +1,45 @@
+-- Adminer 4.7.0 MySQL dump
+
+SET NAMES utf8;
+SET time_zone = '+00:00';
+SET foreign_key_checks = 0;
+SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';
+
+DROP TABLE IF EXISTS `photos`;
+CREATE TABLE `photos` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `user_id` int(11) NOT NULL,
+  `path` varchar(255) NOT NULL,
+  `rating` bigint(20) NOT NULL DEFAULT '0',
+  `comment` varchar(512) NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `user_id` (`user_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
+
+
+SET NAMES utf8mb4;
+
+DROP TABLE IF EXISTS `sessions`;
+CREATE TABLE `sessions` (
+  `id` varchar(32) NOT NULL,
+  `user_id` int(10) unsigned NOT NULL,
+  UNIQUE KEY `id` (`id`),
+  KEY `user_id` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+
+DROP TABLE IF EXISTS `users`;
+CREATE TABLE `users` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `login` varchar(100) NOT NULL,
+  `password` varbinary(100) NOT NULL,
+  `ver` tinyint(4) NOT NULL DEFAULT '0',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `login` (`login`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
+
+INSERT INTO `users` (`id`, `login`, `password`) VALUES
+(1,	'golangcourse',	UNHEX('7362415A62716D7072AC09EAE839A4A1C95E73EC5FA3FC6EACE4D2C78BF4BF1C6906789B557F8C55'));
+
+
+-- 2019-07-14 15:02:20

+ 98 - 0
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/crypt_token.go

@@ -0,0 +1,98 @@
+package main
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/rand"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"io"
+	// "strings"
+	"time"
+)
+
+type CryptToken struct {
+	Secret []byte
+}
+
+type TokenData struct {
+	SessionID string
+	UserID    uint32
+	Exp       int64
+}
+
+func NewAesCryptHashToken(secret string) (*CryptToken, error) {
+	key := []byte(secret)
+	_, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, fmt.Errorf("cypher problem %v", err)
+	}
+	return &CryptToken{Secret: key}, nil
+}
+
+func (tk *CryptToken) Create(s *Session, tokenExpTime int64) (string, error) {
+	block, err := aes.NewCipher(tk.Secret)
+	if err != nil {
+		return "", err
+	}
+
+	aesgcm, err := cipher.NewGCM(block)
+	if err != nil {
+		return "", err
+	}
+
+	nonce := make([]byte, aesgcm.NonceSize())
+	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
+		return "", err
+	}
+
+	td := &TokenData{SessionID: s.ID, UserID: s.UserID, Exp: tokenExpTime}
+	data, _ := json.Marshal(td)
+	ciphertext := aesgcm.Seal(nil, nonce, data, nil)
+
+	res := append([]byte(nil), nonce...)
+	res = append(res, ciphertext...)
+
+	token := base64.StdEncoding.EncodeToString(res)
+	return token, nil
+}
+
+func (tk *CryptToken) Check(s *Session, inputToken string) (bool, error) {
+	block, err := aes.NewCipher(tk.Secret)
+	if err != nil {
+		return false, err
+	}
+	aesgcm, err := cipher.NewGCM(block)
+	if err != nil {
+		return false, err
+	}
+	ciphertext, err := base64.StdEncoding.DecodeString(inputToken)
+	if err != nil {
+		return false, err
+	}
+	nonceSize := aesgcm.NonceSize()
+	if len(ciphertext) < nonceSize {
+		return false, fmt.Errorf("ciphertext too short")
+	}
+
+	nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
+	plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
+	if err != nil {
+		return false, fmt.Errorf("decrypt fail: %v", err)
+	}
+
+	td := TokenData{}
+	err = json.Unmarshal(plaintext, &td)
+	if err != nil {
+		return false, fmt.Errorf("bad json: %v", err)
+	}
+
+	if td.Exp < time.Now().Unix() {
+		return false, fmt.Errorf("token expired")
+	}
+
+	expected := TokenData{SessionID: s.ID, UserID: s.UserID}
+	td.Exp = 0
+	return td == expected, nil
+}

+ 18 - 0
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/docker-compose.yaml

@@ -0,0 +1,18 @@
+version: '3.1'
+services:
+  adminer:
+    image: adminer
+    restart: always
+    ports:
+      - 8090:8080
+  dbMysql:
+    image: mariadb:10.7
+    command: --default-authentication-plugin=mysql_native_password
+    restart: always
+    ports:
+      - 3306:3306
+    environment:
+      MYSQL_ROOT_PASSWORD: "love"
+      MYSQL_DATABASE: photolist
+    volumes:
+      - './_mysql/:/docker-entrypoint-initdb.d/'

BIN
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/favicon.ico


+ 19 - 0
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/go.mod

@@ -0,0 +1,19 @@
+module photolist
+
+go 1.20
+
+require (
+	github.com/go-sql-driver/mysql v1.7.1
+	github.com/golang-jwt/jwt v3.2.2+incompatible
+	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
+	golang.org/x/crypto v0.17.0
+	golang.org/x/oauth2 v0.15.0
+)
+
+require (
+	github.com/golang/protobuf v1.5.3 // indirect
+	golang.org/x/net v0.19.0 // indirect
+	golang.org/x/sys v0.15.0 // indirect
+	google.golang.org/appengine v1.6.7 // indirect
+	google.golang.org/protobuf v1.31.0 // indirect
+)

+ 33 - 0
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/go.sum

@@ -0,0 +1,33 @@
+github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
+github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
+github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
+golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

+ 152 - 0
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/handlers.go

@@ -0,0 +1,152 @@
+package main
+
+import (
+	"encoding/json"
+	"html/template"
+	"log"
+	"net/http"
+	"strconv"
+	"time"
+)
+
+type Storage interface {
+	Add(*Photo) error
+	GetPhotos(uint32) ([]*Photo, error)
+	Rate(uint32, int) error
+}
+
+type TokenManager interface {
+	Create(*Session, int64) (string, error)
+	Check(*Session, string) (bool, error)
+}
+
+// -----------------------------
+
+type PhotolistHandler struct {
+	St     Storage
+	Tmpl   *template.Template
+	Tokens TokenManager
+}
+
+func (h *PhotolistHandler) List(w http.ResponseWriter, r *http.Request) {
+	sess, _ := SessionFromContext(r.Context())
+	items, err := h.St.GetPhotos(sess.UserID)
+	if err != nil {
+		log.Println("cant get items", err)
+		http.Error(w, "storage error", http.StatusInternalServerError)
+		return
+	}
+
+	token, err := h.Tokens.Create(sess, time.Now().Add(24*time.Hour).Unix())
+	if err != nil {
+		log.Println("csrf token creation error:", err)
+		http.Error(w, "internal error", http.StatusInternalServerError)
+		return
+	}
+
+	err = h.Tmpl.ExecuteTemplate(w, "list",
+		struct {
+			Items     []*Photo
+			CSRFToken string
+		}{
+			Items:     items,
+			CSRFToken: token,
+		})
+	if err != nil {
+		log.Println("cant execute template", err)
+		http.Error(w, "template error", http.StatusInternalServerError)
+		return
+	}
+}
+
+func (h *PhotolistHandler) Upload(w http.ResponseWriter, r *http.Request) {
+	sess, _ := SessionFromContext(r.Context())
+	CSRFToken := r.FormValue("csrf-token")
+	ok, err := h.Tokens.Check(sess, CSRFToken)
+	if !ok || err != nil {
+		log.Println("csrf token check fail:", ok, err)
+		http.Error(w, "bad token", http.StatusUnauthorized)
+		return
+	}
+
+	uploadData, _, err := r.FormFile("my_file")
+	if err != nil {
+		log.Println("cant parse file", err)
+		http.Error(w, "request error", http.StatusInternalServerError)
+		return
+	}
+	defer uploadData.Close()
+
+	comment := r.FormValue("comment")
+
+	md5Sum, err := SaveFile(uploadData)
+	if err != nil {
+		log.Println("cant save file", err)
+		http.Error(w, "Internal error", http.StatusInternalServerError)
+		return
+	}
+
+	realFile := "./images/" + md5Sum + ".jpg"
+	err = MakeThumbnails(realFile, md5Sum)
+	if err != nil {
+		log.Println("cant resize file", err)
+		http.Error(w, "Internal error", http.StatusInternalServerError)
+		return
+	}
+
+	err = h.St.Add(&Photo{
+		UserID:  sess.UserID,
+		Path:    md5Sum,
+		Comment: comment,
+	})
+	if err != nil {
+		log.Println("cant store item", err)
+		http.Error(w, "storage error", http.StatusInternalServerError)
+		return
+	}
+
+	http.Redirect(w, r, "/photos", 302)
+}
+
+func (h *PhotolistHandler) Rate(w http.ResponseWriter, r *http.Request) {
+	w.Header().Add("Content-Type", "application/json")
+
+	sess, _ := SessionFromContext(r.Context())
+	CSRFToken := r.Header.Get("csrf-token")
+	ok, err := h.Tokens.Check(sess, CSRFToken)
+	if !ok || err != nil {
+		log.Println("csrf token check fail:", ok, err)
+		http.Error(w, `{"err": "bad token"}`, http.StatusUnauthorized)
+		return
+	}
+
+	id, err := strconv.Atoi(r.FormValue("id"))
+	if err != nil {
+		http.Error(w, `{"err": "bad id"}`, http.StatusBadRequest)
+		return
+	}
+	vote := r.FormValue("vote")
+	rate := 0
+	switch vote {
+	case "up":
+		rate = 1
+	case "down":
+		rate = -1
+	default:
+		http.Error(w, `{"err": "bad vote"}`, http.StatusBadRequest)
+		return
+	}
+
+	err = h.St.Rate(uint32(id), rate)
+	if err != nil {
+		log.Println("rate err: ", err)
+		http.Error(w, `{"err": "db err"}`, http.StatusBadRequest)
+		return
+	}
+
+	result := map[string]interface{}{
+		"id": id,
+	}
+	resp, _ := json.Marshal(result)
+	w.Write(resp)
+}

+ 54 - 0
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/hash_token.go

@@ -0,0 +1,54 @@
+package main
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/hex"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+)
+
+type HashToken struct {
+	Secret []byte
+}
+
+func NewHMACHashToken(secret string) (*HashToken, error) {
+	return &HashToken{Secret: []byte(secret)}, nil
+}
+
+func (tk *HashToken) Create(s *Session, tokenExpTime int64) (string, error) {
+	h := hmac.New(sha256.New, []byte(tk.Secret))
+	data := fmt.Sprintf("%s:%d:%d", s.ID, s.UserID, tokenExpTime)
+	h.Write([]byte(data))
+	token := hex.EncodeToString(h.Sum(nil)) + ":" + strconv.FormatInt(tokenExpTime, 10)
+	return token, nil
+}
+
+func (tk *HashToken) Check(s *Session, inputToken string) (bool, error) {
+	tokenData := strings.Split(inputToken, ":")
+	if len(tokenData) != 2 {
+		return false, fmt.Errorf("bad token data")
+	}
+
+	tokenExp, err := strconv.ParseInt(tokenData[1], 10, 64)
+	if err != nil {
+		return false, fmt.Errorf("bad token time")
+	}
+
+	if tokenExp < time.Now().Unix() {
+		return false, fmt.Errorf("token expired")
+	}
+
+	h := hmac.New(sha256.New, []byte(tk.Secret))
+	data := fmt.Sprintf("%s:%d:%d", s.ID, s.UserID, tokenExp)
+	h.Write([]byte(data))
+	expectedMAC := h.Sum(nil)
+	messageMAC, err := hex.DecodeString(tokenData[0])
+	if err != nil {
+		return false, fmt.Errorf("cand hex decode token")
+	}
+
+	return hmac.Equal(messageMAC, expectedMAC), nil
+}

+ 14 - 0
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/index.go

@@ -0,0 +1,14 @@
+package main
+
+import (
+	"net/http"
+)
+
+func Index(w http.ResponseWriter, r *http.Request) {
+	_, err := SessionFromContext(r.Context())
+	if err != nil {
+		http.Redirect(w, r, "/user/login", http.StatusFound)
+		return
+	}
+	http.Redirect(w, r, "/photos/", http.StatusFound)
+}

+ 56 - 0
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/jwt_token.go

@@ -0,0 +1,56 @@
+package main
+
+import (
+	"fmt"
+	"time"
+
+	jwt "github.com/golang-jwt/jwt"
+)
+
+type JwtToken struct {
+	Secret []byte
+}
+
+type JwtCsrfClaims struct {
+	SessionID string `json:"sid"`
+	UserID    uint32 `json:"uid"`
+	jwt.StandardClaims
+}
+
+func NewJwtToken(secret string) (*JwtToken, error) {
+	return &JwtToken{Secret: []byte(secret)}, nil
+}
+
+func (tk *JwtToken) Create(s *Session, tokenExpTime int64) (string, error) {
+	data := JwtCsrfClaims{
+		SessionID: s.ID,
+		UserID:    s.UserID,
+		StandardClaims: jwt.StandardClaims{
+			ExpiresAt: tokenExpTime,
+			IssuedAt:  time.Now().Unix(),
+		},
+	}
+	token := jwt.NewWithClaims(jwt.SigningMethodHS256, data)
+	return token.SignedString(tk.Secret)
+}
+
+func (tk *JwtToken) parseSecretGetter(token *jwt.Token) (interface{}, error) {
+	method, ok := token.Method.(*jwt.SigningMethodHMAC)
+	if !ok || method.Alg() != "HS256" {
+		return nil, fmt.Errorf("bad sign method")
+	}
+	return tk.Secret, nil
+}
+
+func (tk *JwtToken) Check(s *Session, inputToken string) (bool, error) {
+	payload := &JwtCsrfClaims{}
+	_, err := jwt.ParseWithClaims(inputToken, payload, tk.parseSecretGetter)
+	if err != nil {
+		return false, fmt.Errorf("cant parse jwt token: %v", err)
+	}
+	// проверка exp, iat
+	if payload.Valid() != nil {
+		return false, fmt.Errorf("invalid jwt token: %v", err)
+	}
+	return payload.SessionID == s.ID && payload.UserID == s.UserID, nil
+}

+ 79 - 0
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/main.go

@@ -0,0 +1,79 @@
+package main
+
+import (
+	"database/sql"
+	"fmt"
+	"log"
+	"math/rand"
+	"net/http"
+	"time"
+
+	_ "github.com/go-sql-driver/mysql"
+)
+
+func main() {
+	rand.Seed(time.Now().UnixNano())
+
+	// основные настройки к базе
+	dsn := "root:love@tcp(127.0.0.1:3306)/photolist?charset=utf8&interpolateParams=true"
+	db, err := sql.Open("mysql", dsn)
+
+	err = db.Ping() // вот тут будет первое подключение к базе
+	if err != nil {
+		log.Fatalf("cant connect to db, err: %v\n", err)
+	}
+
+	tmpls := NewTemplates()
+	if err != nil {
+		log.Fatalf("cant init tokens: %v\n", err)
+	}
+
+	// tokens, err := NewHMACHashToken("golangcourse")
+	// tokens, err := NewAesCryptHashToken("qsRY2e4hcM5T7X984E9WQ5uZ8Nty7fxB")
+	tokens, err := NewJwtToken("qsRY2e4hcM5T7X984E9WQ5uZ8Nty7fxB")
+
+	h := &PhotolistHandler{
+		St:     NewDbStorage(db),
+		Tmpl:   tmpls,
+		Tokens: tokens,
+	}
+
+	// sm := NewSessionsDB(db)
+	// sm := NewSessionsJWT("golangcourseSessionSecret")
+	sm := NewSessionsJWTVer("golangcourseSessionSecret", db)
+
+	u := &UserHandler{
+		DB:       db,
+		Tmpl:     tmpls,
+		Sessions: sm,
+	}
+
+	mux := http.NewServeMux()
+
+	mux.HandleFunc("/photos/", h.List)
+	mux.HandleFunc("/photos/upload", h.Upload)
+	mux.HandleFunc("/photos/rate", h.Rate)
+
+	mux.HandleFunc("/user/login", u.Login)
+	mux.HandleFunc("/user/login_oauth", u.LoginOauth)
+	mux.HandleFunc("/user/logout", u.Logout)
+	mux.HandleFunc("/user/reg", u.Reg)
+	mux.HandleFunc("/user/change_pass", u.ChangePassword)
+
+	mux.HandleFunc("/", Index)
+
+	http.Handle("/", AuthMiddleware(sm, mux))
+
+	staticHandler := http.StripPrefix(
+		"/images/",
+		http.FileServer(http.Dir("./images")),
+	)
+	http.Handle("/images/", staticHandler)
+
+	http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
+		http.ServeFile(w, r, "./favicon.ico")
+	})
+
+	fmt.Println("starting server at :8080")
+	http.ListenAndServe(":8080", nil)
+}

+ 14 - 0
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/note.txt

@@ -0,0 +1,14 @@
+docker run --name photolist -e MYSQL_ROOT_PASSWORD=love -d mysql:8.0.13
+
+
+docker run --name photolist -p 3306:3306 -v $(PWD):/docker-entrypoint-initdb.d -e MYSQL_ROOT_PASSWORD=love -e MYSQL_DATABASE=photolist -d mysql
+
+
+docker-compose -f mysql-dc.yaml up
+docker-compose -f mysql-dc.yaml down
+
+
+docker run -it --rm mysql mysql -h127.0.0.1 -uroot -p
+
+
+docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:8.0.13

+ 91 - 0
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/photos.go

@@ -0,0 +1,91 @@
+package main
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"fmt"
+	"image/jpeg"
+	"io"
+	"math/rand"
+	"os"
+
+	"github.com/nfnt/resize"
+)
+
+var (
+	sizes       = []uint{80, 160, 320}
+	letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
+)
+
+func RandStringRunes(n int) string {
+	b := make([]rune, n)
+	for i := range b {
+		b[i] = letterRunes[rand.Intn(len(letterRunes))]
+	}
+	return string(b)
+}
+
+func SaveFile(in io.Reader) (string, error) {
+	tmpName := RandStringRunes(32)
+
+	tmpFile := "./images/" + tmpName + ".jpg"
+	newFile, err := os.Create(tmpFile)
+	if err != nil {
+		return "", err
+	}
+
+	hasher := md5.New()
+	_, err = io.Copy(newFile, io.TeeReader(in, hasher))
+	if err != nil {
+		return "", err
+	}
+	newFile.Sync()
+	newFile.Close()
+
+	md5Sum := hex.EncodeToString(hasher.Sum(nil))
+
+	realFile := "./images/" + md5Sum + ".jpg"
+	err = os.Rename(tmpFile, realFile)
+	if err != nil {
+		return "", err
+	}
+
+	return md5Sum, nil
+}
+
+func MakeThumbnails(realFile, md5Sum string) error {
+	for _, size := range sizes {
+		resizedPath := fmt.Sprintf("./images/%s_%d.jpg", md5Sum, size)
+		err := ResizeImage(realFile, resizedPath, size)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+//  не очень эффективно - каждый раз вычитываем файл
+func ResizeImage(originalPath string, resizedPath string, size uint) error {
+	file, err := os.Open(originalPath)
+	if err != nil {
+		return fmt.Errorf("cant open file %s: %s", originalPath, err)
+	}
+
+	img, err := jpeg.Decode(file)
+	if err != nil {
+		return fmt.Errorf("cant jpeg decode file %s", err)
+	}
+	file.Close()
+
+	resizeImage := resize.Resize(size, 0, img, resize.Lanczos3)
+
+	out, err := os.Create(resizedPath)
+	if err != nil {
+		return fmt.Errorf("cant create file %s: %s", resizedPath, err)
+	}
+	defer out.Close()
+
+	jpeg.Encode(out, resizeImage, nil)
+
+	return nil
+}

+ 62 - 0
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/session_common.go

@@ -0,0 +1,62 @@
+package main
+
+import (
+	"context"
+	"errors"
+	"net/http"
+)
+
+type Session struct {
+	UserID uint32
+	ID     string
+}
+
+type SessionManager interface {
+	Check(*http.Request) (*Session, error)
+	Create(http.ResponseWriter, *User) error
+	DestroyCurrent(http.ResponseWriter, *http.Request) error
+	DestroyAll(http.ResponseWriter, *User) error
+}
+
+// линтер ругается если используем базовые типы в Value контекста
+// типа так безопаснее разграничивать
+type ctxKey int
+
+const sessionKey ctxKey = 1
+
+var (
+	ErrNoAuth = errors.New("No session found")
+)
+
+func SessionFromContext(ctx context.Context) (*Session, error) {
+	sess, ok := ctx.Value(sessionKey).(*Session)
+	if !ok {
+		return nil, ErrNoAuth
+	}
+	return sess, nil
+}
+
+var (
+	noAuthUrls = map[string]struct{}{
+		"/user/login_oauth": struct{}{},
+		"/user/login":       struct{}{},
+		"/user/reg":         struct{}{},
+		"/":                 struct{}{},
+	}
+)
+
+func AuthMiddleware(sm SessionManager, next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if _, ok := noAuthUrls[r.URL.Path]; ok {
+			next.ServeHTTP(w, r)
+			return
+		}
+		sess, err := sm.Check(r)
+		if err != nil {
+			http.Error(w, "No auth", http.StatusUnauthorized)
+			return
+		}
+		ctx := context.WithValue(r.Context(), sessionKey, sess)
+		next.ServeHTTP(w, r.WithContext(ctx))
+	})
+}

+ 87 - 0
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/session_db.go

@@ -0,0 +1,87 @@
+package main
+
+import (
+	"database/sql"
+	"log"
+	"net/http"
+	"time"
+)
+
+type SessionsDB struct {
+	DB *sql.DB
+}
+
+func NewSessionsDB(db *sql.DB) *SessionsDB {
+	return &SessionsDB{
+		DB: db,
+	}
+}
+
+func (sm *SessionsDB) Check(r *http.Request) (*Session, error) {
+	sessionCookie, err := r.Cookie("session_id")
+	if err == http.ErrNoCookie {
+		log.Println("CheckSession no cookie")
+		return nil, ErrNoAuth
+	}
+
+	sess := &Session{}
+	row := sm.DB.QueryRow(`SELECT user_id FROM sessions WHERE id = ?`, sessionCookie.Value)
+	err = row.Scan(&sess.UserID)
+	if err == sql.ErrNoRows {
+		log.Println("CheckSession no rows")
+		return nil, ErrNoAuth
+	} else if err != nil {
+		log.Println("CheckSession err:", err)
+		return nil, err
+	}
+
+	sess.ID = sessionCookie.Value
+	return sess, nil
+}
+
+func (sm *SessionsDB) Create(w http.ResponseWriter, user *User) error {
+	sessID := RandStringRunes(32)
+	_, err := sm.DB.Exec("INSERT INTO sessions(id, user_id) VALUES(?, ?)", sessID, user.ID)
+	if err != nil {
+		return err
+	}
+
+	cookie := &http.Cookie{
+		Name:    "session_id",
+		Value:   sessID,
+		Expires: time.Now().Add(90 * 24 * time.Hour),
+		Path:    "/",
+	}
+	http.SetCookie(w, cookie)
+	return nil
+}
+
+func (sm *SessionsDB) DestroyCurrent(w http.ResponseWriter, r *http.Request) error {
+	sess, err := SessionFromContext(r.Context())
+	if err == nil {
+		_, err = sm.DB.Exec("DELETE FROM sessions WHERE id = ?", sess.ID)
+		if err != nil {
+			return err
+		}
+	}
+	cookie := http.Cookie{
+		Name:    "session_id",
+		Expires: time.Now().AddDate(0, 0, -1),
+		Path:    "/",
+	}
+	http.SetCookie(w, &cookie)
+	return nil
+}
+
+func (sm *SessionsDB) DestroyAll(w http.ResponseWriter, user *User) error {
+	result, err := sm.DB.Exec("DELETE FROM sessions WHERE user_id = ?",
+		user.ID)
+	if err != nil {
+		return err
+	}
+
+	affected, _ := result.RowsAffected()
+	log.Println("destroyed sessions", affected, "for user", user.ID)
+
+	return nil
+}

+ 99 - 0
courses/golang_web/golang_web_services_2024-04-26/10/photolist/7_oauth/session_jwt.go

@@ -0,0 +1,99 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+	"time"
+
+	jwt "github.com/golang-jwt/jwt"
+)
+
+type SessionsJWT struct {
+	Secret []byte
+}
+
+type SessionJWTClaims struct {
+	UserID uint32 `json:"uid"`
+	jwt.StandardClaims
+}
+
+func NewSessionsJWT(secret string) *SessionsJWT {
+	return &SessionsJWT{
+		Secret: []byte(secret),
+	}
+}
+
+func (sm *SessionsJWT) parseSecretGetter(token *jwt.Token) (interface{}, error) {
+	method, ok := token.Method.(*jwt.SigningMethodHMAC)
+	if !ok || method.Alg() != "HS256" {
+		return nil, fmt.Errorf("bad sign method")
+	}
+	return sm.Secret, nil
+}
+
+func (sm *SessionsJWT) Check(r *http.Request) (*Session, error) {
+	sessionCookie, err := r.Cookie("session")
+	if err == http.ErrNoCookie {
+		log.Println("CheckSession no cookie")
+		return nil, ErrNoAuth
+	}
+
+	payload := &SessionJWTClaims{}
+	_, err = jwt.ParseWithClaims(sessionCookie.Value, payload, sm.parseSecretGetter)
+	if err != nil {
+		return nil, fmt.Errorf("cant parse jwt token: %v", err)
+	}
+	// проверка exp, iat
+	if payload.Valid() != nil {
+		return nil, fmt.Errorf("invalid jwt token: %v", err)
+	}
+
+	return &Session{
+		ID:     payload.Id,
+		UserID: payload.UserID,
+	}, nil
+}
+
+func (sm *SessionsJWT) Create(w http.ResponseWriter, user *User) error {
+	data := SessionJWTClaims{
+		UserID: user.ID,
+		StandardClaims: jwt.StandardClaims{
+			ExpiresAt: time.Now().Add(90 * 24 * time.Hour).Unix(), // 90 days
+			IssuedAt:  time.Now().Unix(),
+			Id:        RandStringRunes(32),
+		},
+	}
+	sessVal, _ := jwt.NewWithClaims(jwt.SigningMethodHS256, data).SignedString(sm.Secret)
+
+	cookie := &http.Cookie{
+		Name:    "session",
+		Value:   sessVal,
+		Expires: time.Now().Add(90 * 24 * time.Hour),
+		Path:    "/",
+	}
+	http.SetCookie(w, cookie)
+	return nil
+}
+
+func (sm *SessionsJWT) DestroyCurrent(w http.ResponseWriter, r *http.Request) error {
+	cookie := http.Cookie{
+		Name:    "session",
+		Expires: time.Now().AddDate(0, 0, -1),
+		Path:    "/",
+	}
+	http.SetCookie(w, &cookie)
+
+	// но!
+	// если куку украли - ее не отозвать
+	// ¯ \ _ (ツ) _ / ¯
+
+	return nil
+}
+
+func (sm *SessionsJWT) DestroyAll(w http.ResponseWriter, user *User) error {
+	// но!
+	// мы никак не можем дотянуться до других сессий
+	// ¯ \ _ (ツ) _ / ¯
+	return nil
+}

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov