├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── api.go ├── api_test.go ├── bot.go ├── debug.go ├── doc.go ├── examples └── vkbot │ ├── README.md │ ├── bot.go │ ├── config.json │ ├── main.go │ ├── strings.go │ └── utils.go ├── exapmple_test.go ├── go.mod ├── go.sum ├── longpoll_group.go ├── longpoll_user.go ├── longpoll_user_test.go ├── mocks ├── friends.add.json ├── friends.delete.json ├── friends.getRequests.json ├── longpoll.json ├── messages.get.json ├── messages.getChat.json ├── messages.getChatUsers.json ├── messages.getConversationMembers.json ├── messages.getConversationsById.json ├── messages.getLongPollHistory.json ├── messages.getLongPollServer.json ├── messages.markAsRead.json ├── messages.send.json ├── nojson.json ├── users.get.json ├── utils.getServerTime.json └── vkerr.json ├── router.go ├── router_test.go ├── structs.go ├── structs_test.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | .vscode 27 | .idea/ 28 | examples/vkbot/vkbot 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - 1.14 5 | - tip 6 | 7 | env: 8 | GO111MODULE=on 9 | 10 | script: 11 | - go mod download 12 | - go test -v -covermode=count -coverprofile=coverage.out 13 | 14 | after_success: 15 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go VK bot package 2 | 3 | [![Build Status](https://travis-ci.org/nikepan/govkbot.svg?branch=master)](https://travis-ci.org/nikepan/govkbot) 4 | [![codecov](https://codecov.io/gh/nikepan/govkbot/branch/master/graph/badge.svg)](https://codecov.io/gh/nikepan/govkbot) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/nikepan/govkbot)](https://goreportcard.com/report/github.com/nikepan/govkbot) 6 | [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/nikepan/govkbot) 7 | 8 | 9 | This is simple VK.com bot API. 10 | 11 | 12 | > At 2019-03-01 VK was restrict messages.send for user_tokens. This bot can work with group_token, and access to chat members if has admin rights in chat. You can use v1.0.1 also, if you need only user_token access. 13 | 14 | 15 | It can: 16 | 17 | * Reply to messages (private and chat) 18 | * Send greetings message when invited to chat 19 | * Add and remove mutual friends 20 | * Send notifies to admin 21 | 22 | Installatioin: 23 | 24 | Use `go mod` 25 | For old Go versions you can also use 26 | `go get github.com/nikepan/govkbot/v2` 27 | 28 | For work you need get VK access token with rights: messages,friends,offline (see below). 29 | 30 | 31 | # Quickstart 32 | 33 | ```Go 34 | package main 35 | import "github.com/nikepan/govkbot/v2" 36 | import "log" 37 | 38 | var VKAdminID = 3759927 39 | var VKToken = "efjr98j9fj8jf4j958jj4985jfj9joijerf0fj548jf94jfiroefije495jf48" 40 | 41 | func helpHandler(m *govkbot.Message) (reply string) { 42 | return "help received" 43 | } 44 | 45 | func startHandler(m *govkbot.Message) (reply govkbot.Reply) { 46 | keyboard := govkbot.Keyboard{Buttons: make([][]govkbot.Button, 0)} 47 | button := govkbot.NewButton("/help", nil) 48 | row := make([]govkbot.Button, 0) 49 | row = append(row, button) 50 | keyboard.Buttons = append(keyboard.Buttons, row) 51 | 52 | return govkbot.Reply{Msg: availableCommands, Keyboard: &keyboard} 53 | } 54 | 55 | func errorHandler(m *govkbot.Message, err error) { 56 | log.Fatal(err.Error()) 57 | } 58 | 59 | func main() { 60 | //govkbot.HandleMessage("/", anyHandler) 61 | //govkbot.HandleMessage("/me", meHandler) 62 | govkbot.HandleMessage("/help", helpHandler) 63 | govkbot.HandleAdvancedMessage("/start", startHandler) 64 | 65 | //govkbot.HandleAction("chat_invite_user", inviteHandler) 66 | //govkbot.HandleAction("chat_kick_user", kickHandler) 67 | //govkbot.HandleAction("friend_add", addFriendHandler) 68 | //govkbot.HandleAction("friend_delete", deleteFriendHandler) 69 | 70 | govkbot.HandleError(errorHandler) 71 | 72 | govkbot.SetAutoFriend(true) // enable auto accept/delete friends 73 | 74 | govkbot.SetDebug(true) // log debug messages 75 | 76 | // Optional Direct VK API access 77 | govkbot.SetAPI(VKToken, "", "") // Need only before Listen, if you use direct API 78 | me, _ := govkbot.API.Me() // call API method 79 | log.Printf("current user: %+v\n", me.FullName()) 80 | // Optional end 81 | 82 | govkbot.Listen(VKToken, "", "", VKAdminID) 83 | } 84 | ``` 85 | # Getting group token 86 | 87 | Open group manage and select "Work with API" 88 | 89 | # Getting user token (most likely will not work for messages) 90 | 91 | You need standalone vk app_id. You can use any app_id from https://vk.com/apps?act=wingames, for example 4775211 92 | (Or you can create own app and get app_id on page https://vk.com/editapp?act=create (standalone app)) 93 | 94 | You can get token from you server ip with this node.js package: 95 | https://www.npmjs.com/package/vk-auth (you need login, pass and app_id) 96 | 97 | 98 | To manual get token you need: 99 | 100 | 1. Open in browser with logged in VK (you must use IP, where you want run bot) 101 | ``` 102 | https://oauth.vk.com/authorize?client_id={{app_id}}&scope=offline,groups,messages,friends&display=page&response_type=token&redirect_uri=https://oauth.vk.com/blank.html 103 | ``` 104 | 2. Copy token query parameter from URL string. Token valid only for IP from what you get it. 105 | 106 | 107 | If you receive validation check (for example, you use ip first time) 108 | ```json 109 | {"error":{"error_code":17,"error_msg":"Validation required: please open redirect_uri in browser ...", 110 | "redirect_uri":"https://m.vk.com/login?act=security_check&api_hash=Qwerty1234567890"}} 111 | ``` 112 | you can use https://github.com/Yashko/vk-validation-node. 113 | 114 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package govkbot 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "math/rand" 9 | "net/http" 10 | "net/url" 11 | "strconv" 12 | "strings" 13 | "time" 14 | "unicode/utf8" 15 | ) 16 | 17 | // H - simple object struct 18 | type H map[string]string 19 | 20 | // VkAPI - api config 21 | type VkAPI struct { 22 | Token string 23 | URL string 24 | Ver string 25 | UID int64 26 | GroupID int64 27 | Lang string 28 | HTTPS bool 29 | AdminID int64 30 | MessagesCount int 31 | RequestInterval int 32 | DEBUG bool 33 | } 34 | 35 | const ( 36 | apiUsersGet = "users.get" 37 | apiGroupsGet = "groups.getById" 38 | apiMessagesGet = "messages.get" 39 | apiMessagesGetChat = "messages.getChat" 40 | apiMessagesGetConversationsById = "messages.getConversationsById" 41 | apiMessagesGetChatUsers = "messages.getChatUsers" 42 | apiMessagesGetConversationMembers = "messages.getConversationMembers" 43 | apiMessagesSend = "messages.send" 44 | apiMessagesMarkARead = "messages.markAsRead" 45 | apiFriendsGetRequests = "friends.getRequests" 46 | apiFriendsAdd = "friends.add" 47 | apiFriendsDelete = "friends.delete" 48 | ) 49 | 50 | func (api *VkAPI) IsGroup() bool { 51 | if api.Token == "" { 52 | return true 53 | } 54 | if api.GroupID != 0 { 55 | return true 56 | } else if api.UID != 0 { 57 | return false 58 | } 59 | 60 | g, err := API.CurrentGroup() 61 | if err != nil || g.ID == 0 { 62 | u, err := API.Me() 63 | if err != nil || u == nil { 64 | fmt.Printf("Get current user/group error %+v\n", err) 65 | } else { 66 | api.UID = u.ID 67 | } 68 | } else { 69 | api.GroupID = g.ID 70 | } 71 | return api.GroupID != 0 72 | } 73 | 74 | // Call - main api call method 75 | func (api *VkAPI) Call(method string, params map[string]string) ([]byte, error) { 76 | debugPrint("vk req: %+v params: %+v\n", api.URL+method, params) 77 | params["access_token"] = api.Token 78 | params["v"] = api.Ver 79 | if api.Lang != "" { 80 | params["lang"] = api.Lang 81 | } 82 | if api.HTTPS { 83 | params["https"] = "1" 84 | } 85 | 86 | parameters := url.Values{} 87 | for k, v := range params { 88 | parameters.Add(k, v) 89 | } 90 | 91 | if api.URL == "test" { 92 | content, err := ioutil.ReadFile("./mocks/" + method + ".json") 93 | return content, err 94 | } 95 | resp, err := http.PostForm(api.URL+method, parameters) 96 | if err != nil { 97 | debugPrint("%+v\n", err.Error()) 98 | time.Sleep(time.Duration(time.Millisecond * time.Duration(api.RequestInterval))) 99 | return nil, err 100 | } 101 | buf, err := ioutil.ReadAll(resp.Body) 102 | time.Sleep(time.Duration(time.Millisecond * time.Duration(api.RequestInterval))) 103 | debugPrint("vk resp: %+v\n", string(buf)) 104 | 105 | return buf, err 106 | } 107 | 108 | // CallMethod - call VK API method by name to interfce 109 | func (api *VkAPI) CallMethod(method string, params map[string]string, result interface{}) error { 110 | buf, err := api.Call(method, params) 111 | if err != nil { 112 | return err 113 | } 114 | r := ErrorResponse{} 115 | err = json.Unmarshal(buf, &r) 116 | if err != nil { 117 | return &ResponseError{errors.New("vkapi: vk response is not json"), string(buf)} 118 | } 119 | if r.Error != nil { 120 | debugPrint("%+v\n", r.Error.ErrorMsg) 121 | return r.Error 122 | } 123 | 124 | err = json.Unmarshal(buf, result) 125 | return err 126 | } 127 | 128 | // GetMessages - get user messages (up to 200) 129 | func (api *VkAPI) GetMessages(count int, offset int) (*Messages, error) { 130 | 131 | m := MessagesResponse{} 132 | err := api.CallMethod(apiMessagesGet, H{ 133 | "count": strconv.Itoa(count), 134 | "offset": strconv.Itoa(offset), 135 | }, &m) 136 | 137 | return &m.Response, err 138 | } 139 | 140 | // Me - get current user info 141 | func (api *VkAPI) Me() (*User, error) { 142 | 143 | r := UsersResponse{} 144 | err := api.CallMethod(apiUsersGet, H{"fields": "screen_name"}, &r) 145 | 146 | if len(r.Response) > 0 { 147 | debugPrint("me: %+v - %+v\n", r.Response[0].ID, r.Response[0].ScreenName) 148 | return r.Response[0], err 149 | } 150 | return nil, err 151 | } 152 | 153 | // CurrentGroup - get current group info 154 | func (api *VkAPI) CurrentGroup() (*GroupProfile, error) { 155 | 156 | r := MembersResponse{} 157 | err := api.CallMethod(apiGroupsGet, H{"fields": "screen_name"}, &r) 158 | 159 | if len(r.Response.Groups) > 0 { 160 | group := r.Response.Groups[0] 161 | debugPrint("me: %+v - %+v\n", group.ID, group.ScreenName) 162 | return &group, err 163 | } 164 | return nil, err 165 | } 166 | 167 | // GetChatInfo - returns Chat info by id 168 | func (api *VkAPI) GetUserChatInfo(chatID int64) (*ChatInfo, error) { 169 | r := ChatInfoResponse{} 170 | err := api.CallMethod(apiMessagesGetChat, H{ 171 | "chat_id": strconv.FormatInt(chatID, 10), 172 | "fields": "photo,city,country,sex,bdate", 173 | }, &r) 174 | 175 | return &r.Response, err 176 | } 177 | 178 | // GetChatInfo - returns Chat info by id 179 | func (api *VkAPI) GetConversation(chatID int64) (*ChatInfo, error) { 180 | r := ConversationsResponse{} 181 | err := api.CallMethod(apiMessagesGetConversationsById, H{ 182 | "peer_ids": strconv.FormatInt(chatID, 10), 183 | "extended": "1", 184 | "fields": "photo,city,country,sex,bdate", 185 | }, &r) 186 | if err != nil { 187 | return nil, err 188 | } 189 | if len(r.Response.Items) == 0 { 190 | return nil, VKError{ErrorMsg: "no conversation returned"} 191 | } 192 | 193 | c := r.Response.Items[0] 194 | chat := ChatInfo{} 195 | chat.ID = c.Peer.ID 196 | chat.Type = c.Peer.Type 197 | chat.Title = c.ChatSettings.Title 198 | chat.AdminID = c.ChatSettings.OwnerID 199 | 200 | chat.Users = make([]*User, 0) 201 | for _, u := range r.Response.Profiles { 202 | user := User{} 203 | user.ID = u.ID 204 | user.FirstName = u.FirstName 205 | user.LastName = u.LastName 206 | user.Photo = u.Photo 207 | user.City = u.City 208 | user.Country = u.Country 209 | user.Sex = u.Sex 210 | chat.Users = append(chat.Users, &user) 211 | } 212 | 213 | return &chat, err 214 | } 215 | 216 | // GetChatInfo - returns Chat info by id 217 | func (api *VkAPI) GetChatInfo(chatID int64) (*ChatInfo, error) { 218 | if api.IsGroup() { 219 | return api.GetConversation(chatID) 220 | } 221 | return api.GetUserChatInfo(chatID) 222 | } 223 | 224 | func (api *VkAPI) GetChatFullInfo(chatID int64) (*ChatInfo, error) { 225 | info, err := api.GetChatInfo(chatID) 226 | if err != nil { 227 | return nil, err 228 | } 229 | members, err := api.GetChatUsers(chatID) 230 | if err != nil { 231 | return nil, err 232 | } 233 | if info != nil && members != nil { 234 | info.Users = members 235 | } 236 | return info, err 237 | } 238 | 239 | // GetChatUsers - get chat users 240 | func (api *VkAPI) GetUserChatUsers(chatID int64) (users []*User, err error) { 241 | 242 | r := UsersResponse{} 243 | err = api.CallMethod(apiMessagesGetChatUsers, H{ 244 | "chat_id": strconv.FormatInt(chatID, 10), 245 | "fields": "photo,city,country,sex,bdate", 246 | }, &r) 247 | 248 | return r.Response, err 249 | } 250 | 251 | // GetChatUsers - get chat users 252 | func (api *VkAPI) GetConversationMembers(chatID int64) (users []*User, err error) { 253 | 254 | r := MembersResponse{} 255 | 256 | err = api.CallMethod(apiMessagesGetConversationMembers, H{ 257 | "peer_id": strconv.FormatInt(chatID, 10), 258 | "fields": "photo,city,country,sex,bdate", 259 | }, &r) 260 | 261 | users = make([]*User, 0) 262 | for _, u := range r.Response.Profiles { 263 | user := User{} 264 | user.ID = u.ID 265 | user.FirstName = u.FirstName 266 | user.LastName = u.LastName 267 | user.ScreenName = u.ScreenName 268 | user.Photo = u.Photo 269 | user.Sex = u.Sex 270 | user.City = u.City 271 | user.Country = u.Country 272 | user.BDate = u.BDate 273 | for _, i := range r.Response.Items { 274 | if i.MemberID == u.ID { 275 | user.IsAdmin = i.IsAdmin 276 | user.IsOwner = i.IsOwner 277 | user.InvitedBy = i.InvitedBy 278 | users = append(users, &user) 279 | break 280 | } 281 | } 282 | } 283 | 284 | return users, err 285 | } 286 | 287 | func (api *VkAPI) GetChatUsers(chatID int64) (users []*User, err error) { 288 | if api.IsGroup() { 289 | return api.GetConversationMembers(chatID) 290 | } 291 | return api.GetUserChatUsers(chatID) 292 | } 293 | 294 | func FindUser(users []*User, ID int64) *User { 295 | for _, u := range users { 296 | if u.ID == ID { 297 | return u 298 | } 299 | } 300 | return nil 301 | } 302 | 303 | // GetFriendRequests - get friend requests 304 | func (api *VkAPI) GetFriendRequests(out bool) (friends []int64, err error) { 305 | p := H{} 306 | if out { 307 | p["out"] = "1" 308 | } 309 | 310 | r := FriendRequestsResponse{} 311 | err = api.CallMethod(apiFriendsGetRequests, p, &r) 312 | 313 | return r.Response.Items, err 314 | } 315 | 316 | // AddFriend - add friend 317 | func (api *VkAPI) AddFriend(uid int64) bool { 318 | 319 | r := SimpleResponse{} 320 | err := api.CallMethod(apiFriendsAdd, H{"user_id": strconv.FormatInt(uid, 10)}, &r) 321 | if err != nil { 322 | return false 323 | } 324 | 325 | return r.Response == 1 326 | } 327 | 328 | // DeleteFriend - delete friend 329 | func (api *VkAPI) DeleteFriend(uid int64) bool { 330 | 331 | u := FriendDeleteResponse{} 332 | err := api.CallMethod(apiFriendsDelete, H{"user_id": strconv.FormatInt(uid, 10)}, &u) 333 | 334 | if err != nil { 335 | return false 336 | } 337 | 338 | return u.Response["success"] == 1 339 | } 340 | 341 | // User - get simple user info 342 | func (api *VkAPI) User(uid int64) (*User, error) { 343 | 344 | r := UsersResponse{} 345 | err := api.CallMethod(apiUsersGet, H{ 346 | "user_ids": strconv.FormatInt(uid, 10), 347 | "fields": "sex,screen_name, city, country, bdate", 348 | }, &r) 349 | 350 | if err != nil { 351 | return nil, err 352 | } 353 | if len(r.Response) > 0 { 354 | return r.Response[0], err 355 | } 356 | return nil, errors.New("no users returned") 357 | } 358 | 359 | // MarkAsRead - mark message as read 360 | func (m Message) MarkAsRead() (err error) { 361 | 362 | //r := SimpleResponse{} 363 | //err = API.CallMethod(apiMessagesMarkAsRead, H{"message_ids": strconv.Itoa(m.ID)}, &r) 364 | return nil 365 | } 366 | 367 | func (m Message) GetMentions() []Mention { 368 | mentions := make([]Mention, 0) 369 | ok := false 370 | ustr := "" 371 | uname := "" 372 | msg := m.Body 373 | i := strings.Index(msg, "[id") 374 | imax := len(msg) 375 | if i >= 0 { 376 | i += 2 377 | for { 378 | i++ 379 | if i >= imax { 380 | break 381 | } 382 | if '0' <= msg[i] && msg[i] <= '9' { 383 | ustr += string(msg[i]) 384 | } else { 385 | ok = true 386 | break 387 | } 388 | } 389 | w := 1 390 | var runeValue rune 391 | for { 392 | i += w 393 | if i >= imax { 394 | ok = false 395 | break 396 | } 397 | runeValue, w = utf8.DecodeRuneInString(msg[i:]) 398 | if msg[i] != ']' { 399 | if msg[i] != '|' { 400 | uname += fmt.Sprintf("%c", runeValue) //string(msg[i]) 401 | } 402 | } else { 403 | ok = true 404 | break 405 | } 406 | } 407 | } 408 | if ok { 409 | u, err := strconv.ParseInt(ustr, 10, 64) 410 | if err == nil { 411 | mentions = append(mentions, Mention{u, uname}) 412 | } 413 | } 414 | return mentions 415 | } 416 | 417 | func (api *VkAPI) GetRandomID() string { 418 | return strconv.FormatUint(uint64(rand.Uint32()), 10) 419 | } 420 | 421 | // SendAdvancedPeerMessage sending a message to chat 422 | func (api *VkAPI) SendAdvancedPeerMessage(peerID int64, message Reply) (id int64, err error) { 423 | r := SimpleResponse{} 424 | params := H{ 425 | "peer_id": strconv.FormatInt(peerID, 10), 426 | "message": message.Msg, 427 | "dont_parse_links": "1", 428 | "random_id": api.GetRandomID(), 429 | } 430 | if message.Keyboard != nil { 431 | keyboard, err := json.Marshal(message.Keyboard) 432 | if err != nil { 433 | fmt.Printf("ERROR encode keyboard %+v\n", message.Keyboard) 434 | } else { 435 | params["keyboard"] = string(keyboard) 436 | } 437 | } 438 | err = api.CallMethod(apiMessagesSend, params, &r) 439 | return r.Response, err 440 | } 441 | 442 | // SendPeerMessage sending a message to chat 443 | func (api *VkAPI) SendPeerMessage(peerID int64, msg string) (id int64, err error) { 444 | r := SimpleResponse{} 445 | err = api.CallMethod(apiMessagesSend, H{ 446 | "peer_id": strconv.FormatInt(peerID, 10), 447 | "message": msg, 448 | "dont_parse_links": "1", 449 | "random_id": api.GetRandomID(), 450 | }, &r) 451 | return r.Response, err 452 | } 453 | 454 | // SendChatMessage sending a message to chat 455 | func (api *VkAPI) SendChatMessage(chatID int64, msg string) (id int64, err error) { 456 | r := SimpleResponse{} 457 | err = api.CallMethod(apiMessagesSend, H{ 458 | "chat_id": strconv.FormatInt(chatID, 10), 459 | "message": msg, 460 | "dont_parse_links": "1", 461 | "random_id": api.GetRandomID(), 462 | }, &r) 463 | return r.Response, err 464 | } 465 | 466 | // SendMessage sending a message to user 467 | func (api *VkAPI) SendMessage(userID int64, msg string) (id int64, err error) { 468 | r := SimpleResponse{} 469 | if msg != "" { 470 | err = api.CallMethod(apiMessagesSend, H{ 471 | "user_id": strconv.FormatInt(userID, 10), 472 | "message": msg, 473 | "dont_parse_links": "1", 474 | "random_id": api.GetRandomID(), 475 | }, &r) 476 | } 477 | return r.Response, err 478 | } 479 | 480 | func NewButton(label string, payload interface{}) Button { 481 | button := Button{} 482 | button.Action.Type = "text" 483 | button.Action.Label = label 484 | button.Action.Payload = "{}" 485 | if payload != nil { 486 | jPayoad, err := json.Marshal(payload) 487 | if err == nil { 488 | button.Action.Payload = string(jPayoad) 489 | } 490 | } 491 | button.Color = "default" 492 | return button 493 | } 494 | 495 | // NotifyAdmin - send notify to admin 496 | func (api *VkAPI) NotifyAdmin(msg string) (err error) { 497 | if api.AdminID != 0 { 498 | _, err = api.SendMessage(api.AdminID, msg) 499 | } 500 | return err 501 | } 502 | -------------------------------------------------------------------------------- /api_test.go: -------------------------------------------------------------------------------- 1 | package govkbot 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | const ( 8 | wrongValueReturned = "Wrong value returned" 9 | ) 10 | 11 | //func TestCall(t *testing.T) { 12 | // r := SimpleResponse{} 13 | // err := API.CallMethod("utils.getServerTime", H{}, &r) 14 | // if err != nil { 15 | // t.Error(err.Error()) 16 | // } 17 | //} 18 | 19 | //func TestVkAPI_Call(t *testing.T) { 20 | // SetDebug(true) 21 | // log.SetOutput(ioutil.Discard) 22 | // r := SimpleResponse{} 23 | // err := API.CallMethod("messages.get", H{}, &r) 24 | // SetDebug(false) 25 | // log.SetOutput(os.Stdout) 26 | // if err == nil { 27 | // t.Error("no error returned") 28 | // } 29 | //} 30 | 31 | func TestNoJSON(t *testing.T) { 32 | SetAPI("", "test", "") 33 | r := SimpleResponse{} 34 | err := API.CallMethod("nojson", H{}, &r) 35 | if err == nil { 36 | t.Error("no error returned") 37 | } 38 | if _, ok := err.(*ResponseError); !ok { 39 | t.Error("wrong error type") 40 | } 41 | } 42 | 43 | func TestVkError(t *testing.T) { 44 | SetAPI("", "test", "") 45 | r := SimpleResponse{} 46 | err := API.CallMethod("vkerr", H{}, &r) 47 | if err == nil { 48 | t.Error("no error returned") 49 | } 50 | if _, ok := err.(*VKError); !ok { 51 | t.Error("wrong error type") 52 | } 53 | } 54 | 55 | func TestVkAPI_Me(t *testing.T) { 56 | SetAPI("", "test", "") 57 | me, err := API.Me() 58 | if err != nil { 59 | t.Error(err.Error()) 60 | } 61 | if me.FullName() != "First Last" { 62 | t.Error(me.FullName()) 63 | } 64 | } 65 | 66 | func TestVkAPI_GetChatInfo(t *testing.T) { 67 | SetAPI("", "test", "") 68 | chat, err := API.GetChatInfo(1) 69 | if err != nil { 70 | t.Error(err.Error()) 71 | } 72 | if chat == nil { 73 | t.Error("Chat info == nil") 74 | } 75 | } 76 | 77 | func TestVkAPI_GetChatUsers(t *testing.T) { 78 | SetAPI("", "test", "") 79 | users, err := API.GetChatUsers(1) 80 | if err != nil { 81 | t.Error(err.Error()) 82 | } 83 | if users == nil { 84 | t.Error("users == nil") 85 | } 86 | } 87 | 88 | func TestVkAPI_GetMessages(t *testing.T) { 89 | SetAPI("", "test", "") 90 | messages, err := API.GetMessages(100, 0) 91 | if err != nil { 92 | t.Error(err.Error()) 93 | } 94 | if messages == nil { 95 | t.Error("messages == nil") 96 | } 97 | } 98 | 99 | func TestVkAPI_GetFriendRequests(t *testing.T) { 100 | SetAPI("", "test", "") 101 | _, err := API.GetFriendRequests(false) 102 | if err != nil { 103 | t.Error(err.Error()) 104 | } 105 | } 106 | 107 | func TestVkAPI_AddFriend(t *testing.T) { 108 | SetAPI("", "test", "") 109 | ok := API.AddFriend(1) 110 | if !ok { 111 | t.Error(wrongValueReturned) 112 | } 113 | } 114 | 115 | func TestVkAPI_DeleteFriend(t *testing.T) { 116 | SetAPI("", "test", "") 117 | ok := API.DeleteFriend(1) 118 | if !ok { 119 | t.Error(wrongValueReturned) 120 | } 121 | } 122 | 123 | func TestMessage_MarkAsRead(t *testing.T) { 124 | SetAPI("", "test", "") 125 | m := Message{} 126 | err := m.MarkAsRead() 127 | if err != nil { 128 | t.Error(err.Error()) 129 | } 130 | } 131 | 132 | func TestVKBot_Reply(t *testing.T) { 133 | SetAPI("", "test", "") 134 | bot := API.NewBot() 135 | m := Message{} 136 | mid, err := bot.Reply(&m, Reply{Msg: "ok"}) 137 | if err != nil { 138 | t.Error(err.Error()) 139 | } 140 | if mid == 0 { 141 | t.Error(wrongValueReturned) 142 | } 143 | m = Message{ChatID: 1} 144 | mid, err = bot.Reply(&m, Reply{Msg: "ok"}) 145 | if err != nil { 146 | t.Error(err.Error()) 147 | } 148 | if mid == 0 { 149 | t.Error(wrongValueReturned) 150 | } 151 | } 152 | 153 | func TestVkAPI_SendChatMessage(t *testing.T) { 154 | SetAPI("", "test", "") 155 | _, err := API.SendChatMessage(1, "ok") 156 | if err != nil { 157 | t.Error(err.Error()) 158 | } 159 | } 160 | 161 | func TestVkAPI_SendMessage(t *testing.T) { 162 | SetAPI("", "test", "") 163 | _, err := API.SendMessage(1, "ok") 164 | if err != nil { 165 | t.Error(err.Error()) 166 | } 167 | } 168 | 169 | func TestVkAPI_User(t *testing.T) { 170 | SetAPI("", "test", "") 171 | u, err := API.User(1) 172 | if err != nil { 173 | t.Error(err.Error()) 174 | } 175 | if u.ID == 0 { 176 | t.Error(wrongValueReturned) 177 | } 178 | } 179 | 180 | func TestVkAPI_NotifyAdmin(t *testing.T) { 181 | SetAPI("", "test", "") 182 | API.AdminID = 1 183 | err := API.NotifyAdmin("ok") 184 | if err != nil { 185 | t.Error(err.Error()) 186 | } 187 | } 188 | 189 | func TestMessage_GetMentions(t *testing.T) { 190 | testStr := "/who [id373336876|@sociobesed]" 191 | m := Message{Body: testStr} 192 | mentions := m.GetMentions() 193 | if len(mentions) != 1 { 194 | t.Error("wrong mentions count") 195 | } 196 | if mentions[0].ID != 373336876 { 197 | t.Error("wrong mention") 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /bot.go: -------------------------------------------------------------------------------- 1 | package govkbot 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // VKBot - bot config 11 | type VKBot struct { 12 | msgRoutes map[string]msgRoute 13 | actionRoutes map[string]func(*Message) string 14 | cmdHandlers map[string]func(*Message) string 15 | msgHandlers map[string]func(*Message) string 16 | errorHandler func(*Message, error) 17 | LastMsg int64 18 | lastUserMessages map[int64]int64 19 | lastChatMessages map[int64]int64 20 | autoFriend bool 21 | IgnoreBots bool 22 | API *VkAPI 23 | } 24 | 25 | type msgRoute struct { 26 | SimpleHandler func(*Message) string 27 | Handler func(*Message) Reply 28 | } 29 | 30 | // NewBot - create new instance of bot 31 | func (api *VkAPI) NewBot() *VKBot { 32 | return &VKBot{ 33 | msgRoutes: make(map[string]msgRoute), 34 | actionRoutes: make(map[string]func(*Message) string), 35 | lastUserMessages: make(map[int64]int64), 36 | lastChatMessages: make(map[int64]int64), 37 | API: api, 38 | } 39 | } 40 | 41 | // ListenUser - listen User VK API (deprecated) 42 | func (bot *VKBot) ListenUser(api *VkAPI) error { 43 | poller := NewUserLongPollServer(false, longPollVersion, API.RequestInterval) 44 | go bot.friendReceiver() 45 | 46 | c := time.Tick(3 * time.Second) 47 | for range c { 48 | bot.MainRoute(poller) 49 | } 50 | return nil 51 | } 52 | 53 | // ListenGroup - listen group VK API 54 | func (bot *VKBot) ListenGroup(api *VkAPI) error { 55 | poller := NewGroupLongPollServer(API.RequestInterval) 56 | c := time.Tick(3 * time.Second) 57 | for range c { 58 | bot.MainRoute(poller) 59 | } 60 | return nil 61 | } 62 | 63 | // HandleMessage - add substr message handler. 64 | // Function must return string to reply or "" (if no reply) 65 | func (bot *VKBot) HandleMessage(command string, handler func(*Message) string) { 66 | bot.msgRoutes[command] = msgRoute{SimpleHandler: handler} 67 | } 68 | 69 | // HandleAdvancedMessage - add substr message handler. 70 | // Function must return string to reply or "" (if no reply) 71 | func (bot *VKBot) HandleAdvancedMessage(command string, handler func(*Message) Reply) { 72 | bot.msgRoutes[command] = msgRoute{Handler: handler} 73 | } 74 | 75 | // HandleAction - add action handler. 76 | // Function must return string to reply or "" (if no reply) 77 | func (bot *VKBot) HandleAction(command string, handler func(*Message) string) { 78 | bot.actionRoutes[command] = handler 79 | } 80 | 81 | // HandleError - add error handler 82 | func (bot *VKBot) HandleError(handler func(*Message, error)) { 83 | bot.errorHandler = handler 84 | } 85 | 86 | // SetAutoFriend - auto add friends 87 | func (bot *VKBot) SetAutoFriend(af bool) { 88 | bot.autoFriend = af 89 | } 90 | 91 | // SetIgnoreBots - ignore bots messages 92 | func (bot *VKBot) SetIgnoreBots(ignore bool) { 93 | bot.IgnoreBots = ignore 94 | } 95 | 96 | // GetMessages - request unread messages from VK (more than 200) 97 | func (bot *VKBot) GetMessages() ([]*Message, error) { 98 | var allMessages []*Message 99 | lastMsg := bot.LastMsg 100 | offset := 0 101 | var err error 102 | var messages *Messages 103 | for { 104 | messages, err = API.GetMessages(API.MessagesCount, offset) 105 | if len(messages.Items) > 0 { 106 | if messages.Items[0].ID > lastMsg { 107 | lastMsg = messages.Items[0].ID 108 | } 109 | } 110 | allMessages = append(allMessages, messages.Items...) 111 | if bot.LastMsg > 0 { 112 | if len(messages.Items) > 0 { 113 | if messages.Items[len(messages.Items)-1].ID <= bot.LastMsg { 114 | bot.LastMsg = lastMsg 115 | break 116 | } 117 | } else { 118 | break 119 | } 120 | offset += API.MessagesCount 121 | } else { 122 | bot.LastMsg = lastMsg 123 | break 124 | } 125 | } 126 | if offset > 0 { 127 | API.NotifyAdmin("many messages in interval. offset: " + strconv.Itoa(offset)) 128 | } 129 | return allMessages, err 130 | } 131 | 132 | // RouteAction routes an action 133 | func (bot *VKBot) RouteAction(m *Message) (replies []string, err error) { 134 | if m.Action != "" { 135 | debugPrint("route action: %+v\n", m.Action) 136 | for k, v := range bot.actionRoutes { 137 | if m.Action == k { 138 | msg := v(m) 139 | if msg != "" { 140 | replies = append(replies, msg) 141 | } 142 | } 143 | } 144 | } 145 | return replies, nil 146 | } 147 | 148 | // RouteMessage routes single message 149 | func (bot *VKBot) RouteMessage(m *Message) (replies []Reply, err error) { 150 | message := strings.TrimSpace(strings.ToLower(m.Body)) 151 | if HasPrefix(message, "/ ") { 152 | message = "/" + TrimPrefix(message, "/ ") 153 | } 154 | if m.Action != "" { 155 | actionReplies, err := bot.RouteAction(m) 156 | for _, r := range actionReplies { 157 | replies = append(replies, Reply{Msg: r}) 158 | } 159 | return replies, err 160 | } 161 | for k, v := range bot.msgRoutes { 162 | if HasPrefix(message, k) { 163 | if v.Handler != nil { 164 | reply := v.Handler(m) 165 | if reply.Msg != "" || reply.Keyboard != nil { 166 | replies = append(replies, reply) 167 | } 168 | } else { 169 | msg := v.SimpleHandler(m) 170 | if msg != "" { 171 | replies = append(replies, Reply{Msg: msg}) 172 | } 173 | 174 | } 175 | } 176 | } 177 | return replies, nil 178 | } 179 | 180 | // RouteMessages routes inbound messages 181 | func (bot *VKBot) RouteMessages(messages []*Message) (result map[*Message][]Reply) { 182 | result = make(map[*Message][]Reply) 183 | for _, m := range messages { 184 | if m.ReadState == 0 { 185 | if bot.IgnoreBots && m.UserID < 0 { 186 | continue 187 | } 188 | replies, err := bot.RouteMessage(m) 189 | if err != nil { 190 | sendError(m, err) 191 | } 192 | if len(replies) > 0 { 193 | result[m] = replies 194 | } 195 | } 196 | } 197 | return result 198 | } 199 | 200 | // MainRoute - main router func. Working cycle Listen. 201 | func (bot *VKBot) MainRoute(poller LongPollServer) { 202 | messages, err := poller.GetLongPollMessages() 203 | if err != nil { 204 | sendError(nil, err) 205 | } 206 | debugPrint("inbox: %+v\n", messages) 207 | replies := bot.RouteMessages(messages) 208 | for m, msgs := range replies { 209 | for _, reply := range msgs { 210 | debugPrint("outbox: ", reply.Msg) 211 | if reply.Msg != "" || reply.Keyboard != nil { 212 | _, err = bot.Reply(m, reply) 213 | if err != nil { 214 | log.Printf("Error sending message: '%+v'\n", reply) 215 | sendError(m, err) 216 | _, err = bot.Reply(m, Reply{Msg: "Cant send message, maybe wrong/china letters?"}) 217 | if err != nil { 218 | sendError(m, err) 219 | } 220 | } 221 | } 222 | } 223 | } 224 | } 225 | 226 | // Reply - reply message 227 | func (bot *VKBot) Reply(m *Message, reply Reply) (id int64, err error) { 228 | if m.PeerID != 0 { 229 | return bot.API.SendAdvancedPeerMessage(m.PeerID, reply) 230 | } 231 | if m.ChatID != 0 { 232 | return bot.API.SendChatMessage(m.ChatID, reply.Msg) 233 | } 234 | return bot.API.SendMessage(m.UserID, reply.Msg) 235 | } 236 | 237 | // CheckFriends checking friend invites and matсhes and deletes mutual 238 | func (bot *VKBot) CheckFriends() { 239 | uids, _ := bot.API.GetFriendRequests(false) 240 | if len(uids) > 0 { 241 | for _, uid := range uids { 242 | bot.API.AddFriend(uid) 243 | for k, v := range bot.actionRoutes { 244 | if k == "friend_add" { 245 | m := Message{Action: "friend_add", UserID: uid} 246 | v(&m) 247 | } 248 | } 249 | } 250 | } 251 | uids, _ = bot.API.GetFriendRequests(true) 252 | if len(uids) > 0 { 253 | for _, uid := range uids { 254 | bot.API.DeleteFriend(uid) 255 | for k, v := range bot.actionRoutes { 256 | if k == "friend_delete" { 257 | m := Message{Action: "friend_delete", UserID: uid} 258 | v(&m) 259 | } 260 | } 261 | } 262 | } 263 | } 264 | 265 | func (bot *VKBot) friendReceiver() { 266 | if bot.API.UID > 0 { 267 | bot.CheckFriends() 268 | c := time.Tick(30 * time.Second) 269 | for range c { 270 | bot.CheckFriends() 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /debug.go: -------------------------------------------------------------------------------- 1 | package govkbot 2 | 3 | import "log" 4 | 5 | func isDebugging() bool { 6 | return API.DEBUG 7 | } 8 | 9 | func debugPrint(format string, values ...interface{}) { 10 | if isDebugging() { 11 | log.Printf("[VKBOT-DEBUG] "+format, values...) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package govkbot is a pure Go client library for https://vk.com messaging API. 3 | 4 | It includes a API for receive and send messages, chats, users info and friending. 5 | 6 | It works simply, but need to manually get user token. 7 | 8 | This implementation don't use vk long pool API and proxies, and have limit 3 requests per second (VK API limit). 9 | 10 | To get token you need: 11 | 12 | 1. You can use any app id from https://vk.com/apps?act=wingames, for example 4775211 13 | 14 | (You can create own app and get app_id on page https://vk.com/editapp?act=create (standalone app)) 15 | 16 | 2. Open in browser with logged in VK (you must use IP, where you want run bot) 17 | 18 | https://oauth.vk.com/authorize?client_id={{app_id}}&scope=offline,group,messages,friends&display=page&response_type=token&redirect_uri=https://oauth.vk.com/blank.html 19 | 20 | 3. Copy token query parameter from URL string. Token valid only for IP from what you get it. 21 | 22 | */ 23 | package govkbot 24 | -------------------------------------------------------------------------------- /examples/vkbot/README.md: -------------------------------------------------------------------------------- 1 | # Example VK bot 2 | 3 | This is simple VK.com bot and bot API. 4 | 5 | Run bot and send /me or /info to it. 6 | Also you can add it to friends or invite to chat. 7 | 8 | For work you need valid VK access token with rights: messages,friends,offline. 9 | 10 | # Getting group token 11 | 12 | Open group manage and select "Work with API" 13 | 14 | # Getting user token (most likely will not work for messages) 15 | 16 | You can get it by this url in browser (for your IP): 17 | 18 | https://oauth.vk.com/authorize?client_id={{app_id}}&scope=offline,group,messages,friends&display=page&response_type=token&redirect_uri=https://oauth.vk.com/blank.html 19 | 20 | app_id you can get on page https://vk.com/editapp?act=create (standalone app) 21 | 22 | Take this token to config.json and run app. 23 | -------------------------------------------------------------------------------- /examples/vkbot/bot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/nikepan/govkbot/v2" 8 | ) 9 | 10 | type configuration struct { 11 | VKToken string 12 | ServiceURL string 13 | ServiceToken string 14 | AdminID int64 15 | } 16 | 17 | func getMeMessage(uid int64) (reply string) { 18 | me, _ := govkbot.API.Me() 19 | return fmt.Sprintf("You: %+v %+v", me.FirstName, me.LastName) 20 | } 21 | 22 | func anyHandler(m *govkbot.Message) (reply string) { 23 | notifyAdmin(fmt.Sprintf("Command %+v by user vk.com/id%+v in chat %+v", m.Body, m.UserID, m.PeerID)) 24 | return reply 25 | } 26 | 27 | func meHandler(m *govkbot.Message) (reply string) { 28 | return getMeMessage(m.UserID) 29 | } 30 | 31 | func helpHandler(m *govkbot.Message) (reply govkbot.Reply) { 32 | keyboard := govkbot.Keyboard{Buttons: make([][]govkbot.Button, 0)} 33 | button := govkbot.NewButton("/me", nil) 34 | row := make([]govkbot.Button, 0) 35 | row = append(row, button) 36 | keyboard.Buttons = append(keyboard.Buttons, row) 37 | 38 | return govkbot.Reply{Msg: availableCommands, Keyboard: &keyboard} 39 | } 40 | 41 | func errorHandler(msg *govkbot.Message, err error) { 42 | if _, ok := err.(*govkbot.VKError); !ok { 43 | notifyAdmin("VK ERROR: " + err.Error()) // err.(govkbot.VKError).ErrorCode 44 | } 45 | notifyAdmin("ERROR: " + err.Error()) 46 | } 47 | 48 | func inviteHandler(m *govkbot.Message) (reply string) { 49 | log.Printf("invite: %+v %+v %+v\n", m.ActionMID, govkbot.API.UID, m.ActionMID == govkbot.API.UID) 50 | if m.ActionMID == govkbot.API.UID { 51 | go m.MarkAsRead() 52 | notifyAdmin(fmt.Sprintf("I'm invited to chat %+v )", m.Title)) 53 | reply = replyGreet() 54 | } else { 55 | log.Printf("greet: %+v %+v\n", m.ActionMID, m) 56 | reply = greetUser(m.ActionMID) 57 | } 58 | return reply 59 | } 60 | 61 | func kickHandler(m *govkbot.Message) (reply string) { 62 | if m.ActionMID == govkbot.API.UID { 63 | go m.MarkAsRead() 64 | notifyAdmin(fmt.Sprintf("I'm kicked from chat %+v (", m.Title)) 65 | } 66 | return reply 67 | } 68 | 69 | func greetUser(uid int64) (reply string) { 70 | u, err := govkbot.API.User(uid) 71 | if err == nil { 72 | reply = fmt.Sprintf("Hello, %+v", u.FullName()) 73 | } 74 | return reply 75 | } 76 | 77 | func replyGreet() (reply string) { 78 | reply = "Hi all. I'am bot\n" + availableCommands 79 | return reply 80 | } 81 | 82 | func addFriendHandler(m *govkbot.Message) (reply string) { 83 | log.Printf("friend %+v added\n", m.UserID) 84 | notifyAdmin(fmt.Sprintf("user vk.com/id%+v add me to friends", m.UserID)) 85 | return reply 86 | } 87 | 88 | func deleteFriendHandler(m *govkbot.Message) (reply string) { 89 | log.Printf("friend %+v deleted\n", m.UserID) 90 | notifyAdmin(fmt.Sprintf("user vk.com/id%+v delete me from friends", m.UserID)) 91 | return reply 92 | } 93 | 94 | func notifyAdmin(msg string) { 95 | err := govkbot.NotifyAdmin(msg) 96 | if err != nil { 97 | log.Printf("VK Admin Notify ERROR: %+v\n", msg) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /examples/vkbot/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "VKToken": "e36e9d72db396bfe0740fc9cf0affea772a6fca6a8da791c59ca9b9e378b3d26732e88d103ded38bd4753", 3 | "AdminID": 3759927 4 | } -------------------------------------------------------------------------------- /examples/vkbot/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/nikepan/govkbot/v2" 7 | ) 8 | 9 | var config configuration 10 | 11 | func main() { 12 | config = configuration{ServiceURL: "http://vk1.mysocio.ru/api/"} 13 | 14 | readJSON("config.json", &config) 15 | 16 | govkbot.HandleMessage("/", anyHandler) // any commands starts with "/" 17 | govkbot.HandleMessage("/me", meHandler) 18 | govkbot.HandleAdvancedMessage("/help", helpHandler) 19 | 20 | govkbot.HandleAction("chat_invite_user", inviteHandler) 21 | govkbot.HandleAction("chat_kick_user", kickHandler) 22 | govkbot.HandleAction("friend_add", addFriendHandler) 23 | govkbot.HandleAction("friend_delete", deleteFriendHandler) 24 | 25 | govkbot.HandleError(errorHandler) 26 | 27 | govkbot.SetAutoFriend(true) // enable auto accept/delete friends 28 | 29 | govkbot.SetDebug(true) // log debug messages 30 | 31 | // Optional Direct VK API access 32 | govkbot.SetAPI(config.VKToken, "", "") // Need only before Listen, if you use direct API 33 | me, _ := govkbot.API.Me() // call API method 34 | log.Printf("current user: %+v\n", me.FullName()) 35 | // Optional end 36 | 37 | govkbot.Listen(config.VKToken, "", "", config.AdminID) // start bot 38 | } 39 | -------------------------------------------------------------------------------- /examples/vkbot/strings.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | availableCommands = "Available commands: /help, /me" 5 | ) 6 | -------------------------------------------------------------------------------- /examples/vkbot/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | ) 9 | 10 | func readJSON(fn string, v interface{}) { 11 | file, _ := os.Open(fn) 12 | defer file.Close() 13 | decoder := json.NewDecoder(file) 14 | err := decoder.Decode(v) 15 | if err != nil { 16 | log.Println("error:", err) 17 | } 18 | } 19 | 20 | func readText(fn string) string { 21 | content, err := ioutil.ReadFile(fn) 22 | if err != nil { 23 | log.Println("error:", err) 24 | } 25 | return string(content) 26 | } 27 | -------------------------------------------------------------------------------- /exapmple_test.go: -------------------------------------------------------------------------------- 1 | package govkbot_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/nikepan/govkbot/v2" 8 | ) 9 | 10 | func helpHandler(m *govkbot.Message) (reply string) { 11 | return "Available commands: /help, /me\nYou message" + m.Body 12 | } 13 | 14 | func errorHandler(msg *govkbot.Message, err error) { 15 | // Check gor VK Error code 16 | if _, ok := err.(*govkbot.VKError); !ok { 17 | log.Fatal( 18 | err.(govkbot.VKError).ErrorCode, 19 | err.Error(), msg.Body) 20 | } 21 | log.Fatal(err.Error(), msg.Body) 22 | } 23 | 24 | func addFriendHandler(m *govkbot.Message) (reply string) { 25 | log.Printf("friend %+v added\n", m.UserID) 26 | govkbot.NotifyAdmin(fmt.Sprintf("user vk.com/id%+v add me to friends", m.UserID)) 27 | return reply 28 | } 29 | 30 | func ExampleListen() { 31 | 32 | //govkbot.HandleMessage("/", anyHandler) // any commands starts with "/" 33 | //govkbot.HandleMessage("/me", meHandler) 34 | govkbot.HandleMessage("/help", helpHandler) // any commands starts with "/help" 35 | 36 | //govkbot.HandleAction("chat_invite_user", inviteHandler) 37 | //govkbot.HandleAction("chat_kick_user", kickHandler) 38 | govkbot.HandleAction("friend_add", addFriendHandler) 39 | //govkbot.HandleAction("friend_delete", deleteFriendHandler) 40 | 41 | govkbot.HandleError(errorHandler) 42 | 43 | govkbot.SetAutoFriend(true) // enable auto accept/delete friends 44 | 45 | govkbot.SetDebug(true) // log debug messages 46 | 47 | // Optional Direct VK API access 48 | govkbot.SetAPI("!!!!VK_TOKEN!!!!", "", "") // Need only before Listen, if you use direct API 49 | me, _ := govkbot.API.Me() // call API method 50 | log.Printf("current user: %+v\n", me.FullName()) 51 | // Optional end 52 | 53 | govkbot.Listen("!!!!VK_TOKEN!!!!", "", "", 12345678) // start bot 54 | 55 | } 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nikepan/govkbot/v2 2 | 3 | go 1.19 4 | 5 | require github.com/labstack/gommon v0.2.8 6 | 7 | require ( 8 | github.com/mattn/go-colorable v0.1.0 // indirect 9 | github.com/mattn/go-isatty v0.0.4 // indirect 10 | github.com/stretchr/testify v1.8.2 // indirect 11 | github.com/valyala/bytebufferpool v1.0.0 // indirect 12 | github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect 13 | golang.org/x/sys v0.6.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0= 5 | github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= 6 | github.com/mattn/go-colorable v0.1.0 h1:v2XXALHHh6zHfYTJ+cSkwtyffnaOyR1MXaA91mTrb8o= 7 | github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 8 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 9 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 14 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 15 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 16 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 17 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 18 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 19 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 20 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 21 | github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8= 22 | github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw= 23 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 24 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 26 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | -------------------------------------------------------------------------------- /longpoll_group.go: -------------------------------------------------------------------------------- 1 | package govkbot 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "net/url" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | // LongPollServer - longpoll server structure 17 | type GroupLongPollServer struct { 18 | Key string 19 | Server string 20 | Ts string 21 | Wait int 22 | Mode int 23 | Version int 24 | RequestInterval int 25 | NeedPts bool 26 | API *VkAPI 27 | LpVersion int 28 | ReadMessages map[int64]time.Time 29 | } 30 | 31 | type GroupLongPollServerResponse struct { 32 | Response GroupLongPollServer 33 | } 34 | 35 | type GroupLongPollMessage struct { 36 | Type string 37 | Object struct { 38 | Date int `json:"date"` 39 | FromID int64 `json:"from_id"` 40 | ID int64 `json:"id"` 41 | Out int `json:"out"` 42 | PeerID int `json:"peer_id"` 43 | Text string `json:"text"` 44 | ConversationMessageID int `json:"conversation_message_id"` 45 | FwdMessages []GroupLongPollMessage `json:"fwd_messages"` 46 | Important bool `json:"important"` 47 | RandomID int64 `json:"random_id"` 48 | //Attachments [] `json:"attachments"` 49 | IsHidden bool `json:"is_hidden"` 50 | Action struct { 51 | Type string 52 | MemberID int64 `json:"member_id"` 53 | } 54 | } 55 | GroupID int64 56 | } 57 | 58 | type GroupLongPollEvent struct { 59 | Type string 60 | GroupID int64 61 | } 62 | 63 | type GroupFailResponse struct { 64 | Failed int 65 | Date int `json:"date,omitempty"` 66 | Ts interface{} `json:"ts,omitempty"` 67 | MinVersion int `json:"min_version"` 68 | MaxVersion int `json:"max_version"` 69 | } 70 | 71 | func (g *GroupFailResponse) TS() (string, error) { 72 | switch g.Ts.(type) { 73 | case string: 74 | return g.Ts.(string), nil 75 | case float64: 76 | return strconv.FormatFloat(g.Ts.(float64), 'f', 0, 64), nil 77 | default: 78 | return "", errors.New("unknown type") 79 | } 80 | } 81 | 82 | // NewLongPollServer - get longpoll server 83 | func NewGroupLongPollServer(requestInterval int) (resp *GroupLongPollServer) { 84 | server := GroupLongPollServer{} 85 | server.Wait = DefaultWait 86 | server.Mode = DefaultMode 87 | server.Version = DefaultVersion 88 | server.RequestInterval = requestInterval 89 | server.ReadMessages = make(map[int64]time.Time) 90 | return &server 91 | } 92 | 93 | // Init - init longpoll server 94 | func (server *GroupLongPollServer) Init() (err error) { 95 | r := GroupLongPollServerResponse{} 96 | err = API.CallMethod("groups.getLongPollServer", H{ 97 | "group_id": strconv.FormatInt(API.GroupID, 10), 98 | }, &r) 99 | server.Wait = DefaultWait 100 | server.Mode = DefaultMode 101 | server.Version = DefaultVersion 102 | server.RequestInterval = API.RequestInterval 103 | server.Server = r.Response.Server 104 | server.Ts = r.Response.Ts 105 | server.Key = r.Response.Key 106 | return err 107 | } 108 | 109 | type GroupLongPollResponse struct { 110 | Ts string 111 | Messages []*Message 112 | } 113 | 114 | // Request - make request to longpoll server 115 | func (server *GroupLongPollServer) Request() ([]byte, error) { 116 | var err error 117 | 118 | if server.Server == "" { 119 | err = server.Init() 120 | if err != nil { 121 | log.Fatal(err) 122 | } 123 | } 124 | 125 | parameters := url.Values{} 126 | parameters.Add("act", "a_check") 127 | parameters.Add("ts", server.Ts) 128 | parameters.Add("wait", strconv.Itoa(server.Wait)) 129 | parameters.Add("key", server.Key) 130 | query := server.Server + "?" + parameters.Encode() 131 | if server.Server == "test" { 132 | content, err := ioutil.ReadFile("./mocks/longpoll.json") 133 | return content, err 134 | } 135 | resp, err := http.Get(query) 136 | if err != nil { 137 | debugPrint("%+v\n", err.Error()) 138 | time.Sleep(time.Duration(time.Millisecond * time.Duration(server.RequestInterval))) 139 | return nil, err 140 | } 141 | buf, err := ioutil.ReadAll(resp.Body) 142 | time.Sleep(time.Duration(time.Millisecond * time.Duration(server.RequestInterval))) 143 | //debugPrint("longpoll vk resp: %+v\n", string(buf)) 144 | 145 | failResp := GroupFailResponse{} 146 | err = json.Unmarshal(buf, &failResp) 147 | if err != nil { 148 | return nil, err 149 | } 150 | switch failResp.Failed { 151 | case 1: 152 | server.Ts, err = failResp.TS() 153 | if err != nil { 154 | log.Printf("error ts: %+v\n", err) 155 | } 156 | return server.Request() 157 | case 2: 158 | err = server.Init() 159 | if err != nil { 160 | log.Fatal(err) 161 | } 162 | return server.Request() 163 | case 3: 164 | err = server.Init() 165 | if err != nil { 166 | log.Fatal(err) 167 | } 168 | return server.Request() 169 | case 4: 170 | return nil, errors.New("vkapi: wrong longpoll version") 171 | default: 172 | server.Ts, err = failResp.TS() 173 | if err != nil { 174 | log.Printf("error ts: %+v\n", err) 175 | } 176 | return buf, nil 177 | } 178 | } 179 | 180 | // GetLongPollMessages - get messages via longpoll 181 | func (server *GroupLongPollServer) GetLongPollMessages() ([]*Message, error) { 182 | resp, err := server.Request() 183 | if err != nil { 184 | return nil, err 185 | } 186 | messages, err := server.ParseLongPollMessages(string(resp)) 187 | return messages.Messages, nil 188 | } 189 | 190 | func (server *GroupLongPollServer) ParseMessage(obj map[string]interface{}) (Message, error) { 191 | msg := Message{} 192 | msg.ID = getJSONInt64(obj["id"]) 193 | if obj["text"] != nil { 194 | msg.Body = obj["text"].(string) 195 | } else { 196 | msg.Body = "" 197 | fmt.Printf("error parse message: %+v\n", obj) 198 | return msg, errors.New("error parse message") 199 | } 200 | userID := getJSONInt64(obj["from_id"]) 201 | if userID != 0 { 202 | msg.UserID = userID 203 | } 204 | msg.PeerID = getJSONInt64(obj["peer_id"]) 205 | if int64(msg.UserID) == msg.PeerID { 206 | msg.ChatID = 0 207 | } else { 208 | msg.ChatID = msg.PeerID 209 | } 210 | msg.Date = getJSONInt(obj["date"]) 211 | fwd, ok := obj["fwd_messages"] 212 | if ok { 213 | for _, m := range fwd.([]interface{}) { 214 | fwdMsg, err := server.ParseMessage(m.(map[string]interface{})) 215 | if err != nil { 216 | fmt.Printf("error parse fwd message: %+v\n", err) 217 | } else { 218 | msg.FwdMessages = append(msg.FwdMessages, fwdMsg) 219 | } 220 | } 221 | } 222 | return msg, nil 223 | } 224 | 225 | // ParseLongPollMessages - parse longpoll messages 226 | func (server *GroupLongPollServer) ParseLongPollMessages(j string) (*GroupLongPollResponse, error) { 227 | //fmt.Printf("\n>>>>>>>>>>>>>updates0: %+v\n\n", j) 228 | d := json.NewDecoder(strings.NewReader(j)) 229 | d.UseNumber() 230 | var lp interface{} 231 | if err := d.Decode(&lp); err != nil { 232 | return nil, err 233 | } 234 | lpMap := lp.(map[string]interface{}) 235 | result := GroupLongPollResponse{Messages: []*Message{}} 236 | result.Ts = lpMap["ts"].(string) 237 | updates := lpMap["updates"].([]interface{}) 238 | //fmt.Printf("\n>>>>>>>>>>>>>updates: %+v\n\n", updates) 239 | for _, event := range updates { 240 | //el := event.(interface{}) 241 | eventType := event.(map[string]interface{})["type"].(string) 242 | if eventType == "message_new" { 243 | obj := event.(map[string]interface{})["object"].(map[string]interface{}) 244 | out := getJSONInt(obj["out"]) 245 | if out == 0 { 246 | debugPrint("new message: %+v\n", obj) 247 | msg, err := server.ParseMessage(obj) 248 | result.Messages = append(result.Messages, &msg) 249 | if err != nil { 250 | fmt.Printf("error parse message: %+v \n>>>%+v \n>>>>> %+v\n", err, obj, msg) 251 | } 252 | } 253 | } 254 | } 255 | if len(result.Messages) == 0 { 256 | debugPrint(j) 257 | } 258 | if len(result.Messages) > 0 { 259 | debugPrint("new messages: ts: %+v = %+v\n", result.Ts, len(result.Messages)) 260 | } 261 | // result.Messages = server.FilterReadMesages(result.Messages) 262 | // fmt.Printf("\n>>>>>>>>>>>>>messages2: %+v\n\n", result) 263 | return &result, nil 264 | } 265 | 266 | // FilterReadMesages - filter read messages 267 | func (server *GroupLongPollServer) FilterReadMesages(messages []*Message) (result []*Message) { 268 | for _, m := range messages { 269 | t, ok := server.ReadMessages[m.ID] 270 | if ok { 271 | if time.Since(t).Minutes() > 1 { 272 | delete(server.ReadMessages, m.ID) 273 | } 274 | } else { 275 | result = append(result, m) 276 | server.ReadMessages[m.ID] = time.Now() 277 | } 278 | } 279 | return result 280 | } 281 | -------------------------------------------------------------------------------- /longpoll_user.go: -------------------------------------------------------------------------------- 1 | package govkbot 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/labstack/gommon/log" 14 | ) 15 | 16 | const DefaultWait = 25 17 | const ( 18 | LongPollModeGetAttachments = 2 19 | LongPollModeGetExtendedEvents = 8 20 | LongPollModeGetPts = 32 21 | LongPollModeGetExtraData = 64 22 | LongPollModeGetRandomID = 128 23 | ) 24 | const DefaultMode = LongPollModeGetAttachments 25 | const DefaultVersion = 2 26 | const ChatPrefix = 2000000000 27 | 28 | type LongPollServer interface { 29 | Init() (err error) 30 | Request() ([]byte, error) 31 | GetLongPollMessages() ([]*Message, error) 32 | FilterReadMesages(messages []*Message) (result []*Message) 33 | } 34 | 35 | // LongPollServer - longpoll server structure 36 | type UserLongPollServer struct { 37 | Key string 38 | Server string 39 | Ts int 40 | Wait int 41 | Mode int 42 | Version int 43 | RequestInterval int 44 | NeedPts bool 45 | API *VkAPI 46 | LpVersion int 47 | ReadMessages map[int64]time.Time 48 | } 49 | 50 | // LongPollServerResponse - response format for longpoll info 51 | type UserLongPollServerResponse struct { 52 | Response UserLongPollServer 53 | } 54 | 55 | type LongPollUpdate []interface{} 56 | type LongPollUpdateNum []int64 57 | 58 | type LongPollResponse struct { 59 | Ts uint 60 | Messages []*Message 61 | } 62 | 63 | type Attachment struct { 64 | AttachType string 65 | Attach string 66 | Fwd string 67 | From int 68 | Geo int 69 | GeoProvider int 70 | Title string 71 | AttachProductID int 72 | AttachPhoto string 73 | AttachTitle string 74 | AttachDesc string 75 | AttachURL string 76 | Emoji bool 77 | FromAdmin int 78 | SourceAct string 79 | SourceMid int 80 | } 81 | 82 | type LongPollMessage struct { 83 | MessageType int 84 | MessageID int 85 | Flags int 86 | PeerID int64 87 | Timestamp int64 88 | Text string 89 | Attachments []Attachment 90 | RandomID int 91 | } 92 | 93 | type FailResponse struct { 94 | Failed int 95 | Ts int 96 | MinVersion int `json:"min_version"` 97 | MaxVersion int `json:"max_version"` 98 | } 99 | 100 | // NewLongPollServer - get longpoll server 101 | func NewUserLongPollServer(needPts bool, lpVersion int, requestInterval int) (resp *UserLongPollServer) { 102 | server := UserLongPollServer{} 103 | server.NeedPts = needPts 104 | server.Wait = DefaultWait 105 | server.Mode = DefaultMode 106 | server.Version = DefaultVersion 107 | server.RequestInterval = requestInterval 108 | server.LpVersion = lpVersion 109 | server.ReadMessages = make(map[int64]time.Time) 110 | return &server 111 | } 112 | 113 | // Init - init longpoll server 114 | func (server *UserLongPollServer) Init() (err error) { 115 | r := UserLongPollServerResponse{} 116 | pts := 0 117 | if server.NeedPts { 118 | pts = 1 119 | } 120 | err = API.CallMethod("messages.getLongPollServer", H{ 121 | "need_pts": strconv.Itoa(pts), 122 | "message": strconv.Itoa(server.LpVersion), 123 | }, &r) 124 | server.Wait = DefaultWait 125 | server.Mode = DefaultMode 126 | server.Version = DefaultVersion 127 | server.RequestInterval = API.RequestInterval 128 | server.Server = r.Response.Server 129 | server.Ts = r.Response.Ts 130 | server.Key = r.Response.Key 131 | return err 132 | } 133 | 134 | // Request - make request to longpoll server 135 | func (server *UserLongPollServer) Request() ([]byte, error) { 136 | var err error 137 | 138 | if server.Server == "" { 139 | err = server.Init() 140 | if err != nil { 141 | log.Fatal(err) 142 | } 143 | } 144 | 145 | parameters := url.Values{} 146 | parameters.Add("act", "a_check") 147 | parameters.Add("ts", strconv.Itoa(server.Ts)) 148 | parameters.Add("wait", strconv.Itoa(server.Wait)) 149 | parameters.Add("key", server.Key) 150 | parameters.Add("mode", strconv.Itoa(DefaultMode)) 151 | parameters.Add("version", strconv.Itoa(server.Version)) 152 | query := "https://" + server.Server + "?" + parameters.Encode() 153 | if server.Server == "test" { 154 | content, err := ioutil.ReadFile("./mocks/longpoll.json") 155 | return content, err 156 | } 157 | resp, err := http.Get(query) 158 | if err != nil { 159 | debugPrint("%+v\n", err.Error()) 160 | time.Sleep(time.Duration(time.Millisecond * time.Duration(server.RequestInterval))) 161 | return nil, err 162 | } 163 | buf, err := ioutil.ReadAll(resp.Body) 164 | time.Sleep(time.Duration(time.Millisecond * time.Duration(server.RequestInterval))) 165 | //debugPrint("longpoll vk resp: %+v\n", string(buf)) 166 | 167 | failResp := FailResponse{} 168 | err = json.Unmarshal(buf, &failResp) 169 | if err != nil { 170 | log.Printf("longpoll vk resp: %+v\n", string(buf)) 171 | return nil, err 172 | } 173 | switch failResp.Failed { 174 | case 1: 175 | server.Ts = failResp.Ts 176 | return server.Request() 177 | case 2: 178 | err = server.Init() 179 | if err != nil { 180 | log.Fatal(err) 181 | } 182 | return server.Request() 183 | case 3: 184 | err = server.Init() 185 | if err != nil { 186 | log.Fatal(err) 187 | } 188 | return server.Request() 189 | case 4: 190 | return nil, errors.New("vkapi: wrong longpoll version") 191 | default: 192 | return buf, nil 193 | } 194 | } 195 | 196 | // GetLongPollMessage - get message from longpoll json row 197 | func GetLongPollMessage(resp []interface{}) *Message { 198 | message := Message{} 199 | mid, _ := resp[1].(json.Number).Int64() 200 | message.ID = mid 201 | flags, _ := resp[2].(json.Number).Int64() 202 | message.Flags = int(flags) 203 | message.PeerID, _ = resp[3].(json.Number).Int64() 204 | message.Timestamp, _ = resp[4].(json.Number).Int64() 205 | message.Body = resp[5].(string) 206 | return &message 207 | } 208 | 209 | // GetLongPollMessages - get messages via longpoll 210 | func (server *UserLongPollServer) GetLongPollMessages() ([]*Message, error) { 211 | resp, err := server.Request() 212 | if err != nil { 213 | return nil, err 214 | } 215 | messages, err := server.ParseLongPollMessages(string(resp)) 216 | return messages.Messages, nil 217 | } 218 | 219 | func getJSONInt64(el interface{}) int64 { 220 | if el == nil { 221 | return 0 222 | } 223 | v, _ := el.(json.Number).Int64() 224 | return v 225 | } 226 | 227 | func getJSONInt(el interface{}) int { 228 | return int(getJSONInt64(el)) 229 | } 230 | 231 | // ParseLongPollMessages - parse longpoll messages 232 | func (server *UserLongPollServer) ParseLongPollMessages(j string) (*LongPollResponse, error) { 233 | d := json.NewDecoder(strings.NewReader(j)) 234 | d.UseNumber() 235 | var lp interface{} 236 | if err := d.Decode(&lp); err != nil { 237 | return nil, err 238 | } 239 | lpMap := lp.(map[string]interface{}) 240 | result := LongPollResponse{Messages: []*Message{}} 241 | ts, _ := lpMap["ts"].(json.Number).Int64() 242 | result.Ts = uint(ts) 243 | updates := lpMap["updates"].([]interface{}) 244 | for _, event := range updates { 245 | el := event.([]interface{}) 246 | eventType := getJSONInt(el[0]) 247 | if eventType == 4 { 248 | out := getJSONInt(el[2]) & 2 249 | if out == 0 { 250 | msg := Message{} 251 | msg.ID = getJSONInt64(el[1]) 252 | msg.Body = el[5].(string) 253 | userID := el[6].(map[string]interface{})["from"] 254 | if userID != nil { 255 | msg.UserID, _ = strconv.ParseInt(userID.(string), 10, 64) 256 | } 257 | msg.PeerID = getJSONInt64(el[3]) 258 | if msg.UserID == 0 { 259 | msg.UserID = msg.PeerID 260 | } else { 261 | msg.ChatID = msg.PeerID - ChatPrefix 262 | } 263 | msg.Date = getJSONInt(el[4]) 264 | debugPrint(msg.Body) 265 | result.Messages = append(result.Messages, &msg) 266 | } 267 | } 268 | } 269 | if len(result.Messages) == 0 { 270 | debugPrint(j) 271 | } 272 | result.Messages = server.FilterReadMesages(result.Messages) 273 | return &result, nil 274 | } 275 | 276 | // FilterReadMesages - filter read messages 277 | func (server *UserLongPollServer) FilterReadMesages(messages []*Message) (result []*Message) { 278 | for _, m := range messages { 279 | t, ok := server.ReadMessages[m.ID] 280 | if ok { 281 | if time.Since(t).Minutes() > 1 { 282 | delete(server.ReadMessages, m.ID) 283 | } 284 | } else { 285 | result = append(result, m) 286 | server.ReadMessages[m.ID] = time.Now() 287 | } 288 | } 289 | return result 290 | } 291 | -------------------------------------------------------------------------------- /longpoll_user_test.go: -------------------------------------------------------------------------------- 1 | package govkbot 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestGetMessage(t *testing.T) { 11 | 12 | data := "[4, 606838, 1, 329007844, 1508267602, \"тест\"]" 13 | d := json.NewDecoder(strings.NewReader(data)) 14 | d.UseNumber() 15 | var lp interface{} 16 | if err := d.Decode(&lp); err != nil { 17 | log.Fatal(err) 18 | } 19 | log.Printf("%+v\n", lp) 20 | message := GetLongPollMessage(lp.([]interface{})) 21 | if message.Body != "тест" { 22 | t.Error("wrong longpoll message") 23 | } 24 | } 25 | 26 | func TestUserLongPollServer_ParseLongPollMessages(t *testing.T) { 27 | SetAPI("", "test", "") 28 | data := `{"ts":1668805076,"updates":[[4,2105994,561,123456,1496404246,"hello",{"title":" ... "},{"attach1_type":"photo","attach1":"123456_417336473","attach2_type":"audio","attach2":"123456_456239018"}]]}` 29 | server := NewUserLongPollServer(false, longPollVersion, 25) 30 | messages, err := server.ParseLongPollMessages(data) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | for _, msg := range messages.Messages { 35 | if msg.Body == "" { 36 | t.Error("empty message") 37 | } 38 | } 39 | if len(messages.Messages) != 1 { 40 | t.Error("wrong messages count") 41 | } 42 | if messages.Messages[0].Body != "hello" { 43 | t.Error("wrong messages text", messages.Messages[0].Body) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /mocks/friends.add.json: -------------------------------------------------------------------------------- 1 | { 2 | "response": 1 3 | } -------------------------------------------------------------------------------- /mocks/friends.delete.json: -------------------------------------------------------------------------------- 1 | { 2 | "response": { 3 | "success": 1, 4 | "out_request_deleted": 1 5 | } 6 | } -------------------------------------------------------------------------------- /mocks/friends.getRequests.json: -------------------------------------------------------------------------------- 1 | { 2 | "response": { 3 | "count": 1, 4 | "items": [1] 5 | } 6 | } -------------------------------------------------------------------------------- /mocks/longpoll.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts": 1668805076, 3 | "updates": [ 4 | [ 5 | 4, 6 | 2105994, 7 | 561, 8 | 123456, 9 | 1496404246, 10 | "hello", 11 | { 12 | "title": " ... " 13 | }, 14 | { 15 | "attach1_type": "photo", 16 | "attach1": "123456_417336473", 17 | "attach2_type": "audio", 18 | "attach2": "123456_456239018" 19 | } 20 | ] 21 | ] 22 | } -------------------------------------------------------------------------------- /mocks/messages.get.json: -------------------------------------------------------------------------------- 1 | { 2 | "response": { 3 | "count": 462409, 4 | "items": [{ 5 | "id": 552885, 6 | "date": 1376054836, 7 | "out": 0, 8 | "user_id": 185014513, 9 | "read_state": 0, 10 | "title": " ... ", 11 | "body": "test" 12 | }] 13 | } 14 | } -------------------------------------------------------------------------------- /mocks/messages.getChat.json: -------------------------------------------------------------------------------- 1 | { 2 | "response": { 3 | "id": 11, 4 | "type": "chat", 5 | "title": "hi all", 6 | "admin_id": 1, 7 | "users": [{ 8 | "id": 1, 9 | "first_name": "First", 10 | "last_name": "Last", 11 | "city": { 12 | "id": 1, 13 | "title": "Moscow" 14 | }, 15 | "country": { 16 | "id": 1, 17 | "title": "Russia" 18 | }, 19 | "photo": "https://pp.vk.me/...39e/JQIpttMYiTU.jpg", 20 | "invited_by": 1, 21 | "type": "profile" 22 | }] 23 | } 24 | } -------------------------------------------------------------------------------- /mocks/messages.getChatUsers.json: -------------------------------------------------------------------------------- 1 | { 2 | "response": [{ 3 | "id": 1, 4 | "first_name": "Denis", 5 | "last_name": "Shevtsov", 6 | "photo": "http://cs410918.v...861/5s42QydZ6_0.jpg", 7 | "invited_by": 1 8 | }, { 9 | "id": 2, 10 | "first_name": "Olga", 11 | "last_name": "Lysenko", 12 | "photo": "http://cs10742.vk...5945/e_54379a30.jpg", 13 | "invited_by": 1 14 | }, { 15 | "id": 3, 16 | "first_name": "Irina", 17 | "last_name": "Denezhkina", 18 | "photo": "http://cs11432.vk...57e/iVy-cd362Yg.jpg", 19 | "invited_by": 1 20 | }] 21 | } -------------------------------------------------------------------------------- /mocks/messages.getConversationMembers.json: -------------------------------------------------------------------------------- 1 | { 2 | "response": { 3 | "count": 2, 4 | "items": [ 5 | { 6 | "member_id": -1 7 | }, 8 | { 9 | "member_id": 1 10 | } 11 | ], 12 | "profiles": [ 13 | { 14 | "id": 1, 15 | "sex": 2, 16 | "screen_name": "durov", 17 | "photo_50": "", 18 | "photo_100": "", 19 | "online_info": { 20 | "visible": true, 21 | "is_online": false, 22 | "is_mobile": false 23 | }, 24 | "online": 0, 25 | "first_name": "P", 26 | "last_name": "D", 27 | "can_access_closed": true, 28 | "is_closed": false 29 | } 30 | ], 31 | "groups": [ 32 | { 33 | "id": 1, 34 | "name": "", 35 | "screen_name": "abcdef", 36 | "is_closed": 0, 37 | "type": "page", 38 | "photo_50": "", 39 | "photo_100": "", 40 | "photo_200": "" 41 | } 42 | ] 43 | } 44 | } -------------------------------------------------------------------------------- /mocks/messages.getConversationsById.json: -------------------------------------------------------------------------------- 1 | { 2 | "response": { 3 | "count": 1, 4 | "items": [ 5 | { 6 | "peer": { 7 | "id": 1, 8 | "type": "user", 9 | "local_id": 1 10 | }, 11 | "last_message_id": 0, 12 | "in_read": 0, 13 | "out_read": 0, 14 | "sort_id": { 15 | "major_id": 0, 16 | "minor_id": 0 17 | }, 18 | "last_conversation_message_id": 0, 19 | "in_read_cmid": 0, 20 | "out_read_cmid": 0, 21 | "is_marked_unread": false, 22 | "important": false, 23 | "can_write": { 24 | "allowed": false, 25 | "reason": 901 26 | }, 27 | "peer_flags": 0 28 | } 29 | ] 30 | } 31 | } -------------------------------------------------------------------------------- /mocks/messages.getLongPollHistory.json: -------------------------------------------------------------------------------- 1 | { 2 | "response": { 3 | "history": [[6, 2000000194, 790341], [80, 2, 2], [63, 2000000172, 1531428628], [4, 790342, 532481, 2000000172], [80, 3, 3]], 4 | "messages": { 5 | "count": 1, 6 | "items": [{ 7 | "date": 1531428629, 8 | "from_id": 150883522, 9 | "id": 790342, 10 | "out": 0, 11 | "peer_id": 2000000172, 12 | "text": "test", 13 | "conversation_message_id": 1078, 14 | "fwd_messages": [], 15 | "important": false, 16 | "random_id": 0, 17 | "attachments": [], 18 | "is_hidden": false 19 | }] 20 | }, 21 | "new_pts": 10556760, 22 | "profiles": [{ 23 | "id": 150883522, 24 | "first_name": "opopop", 25 | "last_name": "opop", 26 | "sex": 1, 27 | "screen_name": "opopopopop", 28 | "photo": "https://pp.userap...5SGy2-1-c.jpg?ava=1", 29 | "photo_medium_rec": "https://pp.userap...Hz3YgvLiY.jpg?ava=1", 30 | "online": 1, 31 | "online_app": "3140623", 32 | "online_mobile": 1 33 | }], 34 | "conversations": [{ 35 | "peer": { 36 | "id": 2000000172, 37 | "type": "chat", 38 | "local_id": 172 39 | }, 40 | "in_read": 789884, 41 | "out_read": 790342, 42 | "last_message_id": 790342, 43 | "unread_count": 1, 44 | "can_write": { 45 | "allowed": true 46 | }, 47 | "chat_settings": { 48 | "title": "eeeeee", 49 | "members_count": 126, 50 | "state": "in", 51 | "photo": { 52 | "photo_50": "https://pp.userap...Ncv2G8GXA.jpg?ava=1", 53 | "photo_100": "https://pp.userap...7a584heJk.jpg?ava=1", 54 | "photo_200": "https://pp.userap...SvGdmn-yM.jpg?ava=1" 55 | }, 56 | "active_ids": [150883522, 20256680, 6787519, 46580800] 57 | }, 58 | "push_settings": { 59 | "no_sound": false, 60 | "disabled_until": 0, 61 | "disabled_forever": true 62 | } 63 | }] 64 | } 65 | } -------------------------------------------------------------------------------- /mocks/messages.getLongPollServer.json: -------------------------------------------------------------------------------- 1 | { 2 | "response": { 3 | "key": "80d9de42848e8298c5934ecefe3d71a2018ed132", 4 | "server": "test", 5 | "ts": 1757519170 6 | } 7 | } -------------------------------------------------------------------------------- /mocks/messages.markAsRead.json: -------------------------------------------------------------------------------- 1 | { 2 | "response": 1 3 | } -------------------------------------------------------------------------------- /mocks/messages.send.json: -------------------------------------------------------------------------------- 1 | { 2 | "response": 532537 3 | } -------------------------------------------------------------------------------- /mocks/nojson.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikepan/govkbot/26ff42f7f832b4c2d285c9c74387c9abe9446ff4/mocks/nojson.json -------------------------------------------------------------------------------- /mocks/users.get.json: -------------------------------------------------------------------------------- 1 | { 2 | "response": [{ 3 | "id": 1, 4 | "first_name": "First", 5 | "last_name": "Last", 6 | "city": { 7 | "id": 1, 8 | "title": "Moscow" 9 | }, 10 | "photo_50": "", 11 | "verified": 0 12 | }] 13 | } -------------------------------------------------------------------------------- /mocks/utils.getServerTime.json: -------------------------------------------------------------------------------- 1 | { 2 | "response": 1467726682 3 | } -------------------------------------------------------------------------------- /mocks/vkerr.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "error_code": 113, 4 | "error_msg": "Invalid user id", 5 | "request_params": [ 6 | { 7 | "key": "oauth", 8 | "value": "1" 9 | }, 10 | { 11 | "key": "method", 12 | "value": "users.get" 13 | }, 14 | { 15 | "key": "fields", 16 | "value": "photo_50,city,verified" 17 | }, 18 | { 19 | "key": "name_case", 20 | "value": "Nom" 21 | }, 22 | { 23 | "key": "user_ids", 24 | "value": "210700erfrefr286" 25 | }, 26 | { 27 | "key": "v", 28 | "value": "5.131" 29 | } 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package govkbot 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | const ( 8 | vkAPIURL = "https://api.vk.com/method/" 9 | vkAPIVer = "5.154" 10 | messagesCount = 200 11 | requestInterval = 400 // 3 requests per second VK limit 12 | longPollVersion = 3 13 | ) 14 | 15 | // API - bot API 16 | var API = newAPI() 17 | 18 | var Bot = API.NewBot() 19 | 20 | // SetDebug - enable/disable debug messages logging 21 | func SetDebug(debug bool) { 22 | API.DEBUG = debug 23 | } 24 | 25 | func newAPI() *VkAPI { 26 | return &VkAPI{ 27 | Token: "", 28 | URL: vkAPIURL, 29 | Ver: vkAPIVer, 30 | MessagesCount: messagesCount, 31 | RequestInterval: requestInterval, 32 | DEBUG: false, 33 | HTTPS: true, 34 | } 35 | } 36 | 37 | // SetToken - set bot token 38 | func SetToken(token string) { 39 | API.Token = token 40 | } 41 | 42 | // SetAutoFriend - enables mutual auto friending 43 | func SetAutoFriend(af bool) { 44 | Bot.SetAutoFriend(af) 45 | } 46 | 47 | // SetAPI - setup API config 48 | func SetAPI(token string, url string, ver string) { 49 | SetToken(token) 50 | if url != "" { 51 | API.URL = url 52 | } 53 | if ver != "" { 54 | API.Ver = ver 55 | } 56 | } 57 | 58 | // SetLang - sets VK response language. Default auto. Available: en, ru, ua, be, es, fi, de, it 59 | func SetLang(lang string) { 60 | API.Lang = lang 61 | } 62 | 63 | // HandleMessage - add substr message handler. 64 | // Function must return string to reply or "" (if no reply) 65 | func HandleMessage(command string, handler func(*Message) string) { 66 | Bot.HandleMessage(command, handler) 67 | } 68 | 69 | // HandleAdvancedMessage - add substr message handler. 70 | // Function must return string to reply or "" (if no reply) 71 | func HandleAdvancedMessage(command string, handler func(*Message) Reply) { 72 | Bot.HandleAdvancedMessage(command, handler) 73 | } 74 | 75 | // HandleAction - add action handler. 76 | // Function must return string to reply or "" (if no reply) 77 | func HandleAction(command string, handler func(*Message) string) { 78 | Bot.HandleAction(command, handler) 79 | } 80 | 81 | // HandleError - add error handler 82 | func HandleError(handler func(*Message, error)) { 83 | Bot.HandleError(handler) 84 | } 85 | 86 | func sendError(msg *Message, err error) { 87 | if Bot.errorHandler != nil { 88 | Bot.errorHandler(msg, err) 89 | } else { 90 | log.Fatalf("VKBot error: %+v\n", err.Error()) 91 | } 92 | 93 | } 94 | 95 | // Listen - start server 96 | func Listen(token string, url string, ver string, adminID int64) error { 97 | if API.Token == "" { 98 | SetAPI(token, url, ver) 99 | } 100 | API.AdminID = adminID 101 | if Bot.API.IsGroup() { 102 | return Bot.ListenGroup(API) 103 | } 104 | return Bot.ListenUser(API) 105 | } 106 | 107 | // NotifyAdmin - notify AdminID by VK 108 | func NotifyAdmin(msg string) error { 109 | return API.NotifyAdmin(msg) 110 | } 111 | -------------------------------------------------------------------------------- /router_test.go: -------------------------------------------------------------------------------- 1 | package govkbot 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func baseHandler(m *Message) (reply string) { 8 | return m.Body 9 | } 10 | 11 | func errorHandler(msg *Message, err error) { 12 | return 13 | } 14 | 15 | func TestHandleMessage(t *testing.T) { 16 | HandleMessage("/help", baseHandler) 17 | if len(Bot.msgRoutes) == 0 { 18 | t.Error("Error adding message handler") 19 | } 20 | } 21 | 22 | func TestHandleAction(t *testing.T) { 23 | HandleAction("/help", baseHandler) 24 | if len(Bot.actionRoutes) == 0 { 25 | t.Error("Error adding action handler") 26 | } 27 | } 28 | 29 | func TestHandleError(t *testing.T) { 30 | HandleError(errorHandler) 31 | if Bot.errorHandler == nil { 32 | t.Error("Error set error handler") 33 | } 34 | } 35 | 36 | func TestSetAPI(t *testing.T) { 37 | token := "12345" 38 | SetAPI(token, "https://vk.com/api/", "5.131") 39 | if API.Token != token { 40 | t.Error("Error setup API") 41 | } 42 | SetLang("ru") 43 | if API.Lang != "ru" { 44 | t.Error("Error setup Lang") 45 | } 46 | SetLang("") 47 | } 48 | 49 | func TestSetToken(t *testing.T) { 50 | token := "12345" 51 | SetToken(token) 52 | if API.Token != token { 53 | t.Error("Error setup API") 54 | } 55 | } 56 | 57 | func TestSetAutoFriend(t *testing.T) { 58 | SetAutoFriend(true) 59 | if !Bot.autoFriend { 60 | t.Error("Error set auto friend") 61 | } 62 | SetAutoFriend(false) 63 | } 64 | 65 | func TestSetDebug(t *testing.T) { 66 | SetDebug(true) 67 | if !API.DEBUG { 68 | t.Error("Error set debug mode") 69 | } 70 | SetDebug(false) 71 | } 72 | 73 | func TestRouteMessage(t *testing.T) { 74 | HandleMessage("/help", baseHandler) 75 | SetAPI("", "test", "") 76 | m := Message{Body: "/help"} 77 | replies, err := Bot.RouteMessage(&m) 78 | if err != nil { 79 | t.Error(err.Error()) 80 | } 81 | if replies[0].Msg != "/help" { 82 | t.Error(wrongValueReturned) 83 | } 84 | } 85 | 86 | func TestRouteAction(t *testing.T) { 87 | HandleAction("friend_add", baseHandler) 88 | SetAPI("", "test", "") 89 | m := Message{Action: "friend_add", Body: "ok"} 90 | replies, err := Bot.RouteAction(&m) 91 | if err != nil { 92 | t.Error(err.Error()) 93 | } 94 | if replies[0] != "ok" { 95 | t.Error(wrongValueReturned) 96 | } 97 | } 98 | 99 | func TestRouteMessages(t *testing.T) { 100 | HandleMessage("/help", baseHandler) 101 | SetAPI("", "test", "") 102 | var messages []*Message 103 | m1 := Message{Body: "/help"} 104 | messages = append(messages, &m1) 105 | m2 := Message{Action: "friend_add", Body: "ok"} 106 | messages = append(messages, &m2) 107 | m3 := Message{Body: "skip"} 108 | messages = append(messages, &m3) 109 | replies := Bot.RouteMessages(messages) 110 | 111 | if replies[&m1][0].Msg != "/help" { 112 | t.Error(wrongValueReturned) 113 | } 114 | if replies[&m2][0].Msg != "ok" { 115 | t.Error(wrongValueReturned) 116 | } 117 | } 118 | 119 | func TestGetMessages(t *testing.T) { 120 | SetAPI("", "test", "") 121 | messages, err := Bot.GetMessages() 122 | if err != nil { 123 | t.Error(err.Error()) 124 | } 125 | if len(messages) == 0 { 126 | t.Error("No messages") 127 | } 128 | } 129 | 130 | func TestCheckFriends(t *testing.T) { 131 | SetAPI("", "test", "") 132 | Bot.CheckFriends() 133 | } 134 | 135 | func TestMainRoute(t *testing.T) { 136 | SetAPI("", "test", "") 137 | HandleError(errorHandler) 138 | poller := NewUserLongPollServer(false, longPollVersion, API.RequestInterval) 139 | Bot.MainRoute(poller) 140 | } 141 | 142 | func TestNotifyAdmin(t *testing.T) { 143 | SetAPI("", "test", "") 144 | err := NotifyAdmin("ok") 145 | if err != nil { 146 | t.Error(err.Error()) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /structs.go: -------------------------------------------------------------------------------- 1 | package govkbot 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Mention - user mention in message 8 | type Mention struct { 9 | ID int64 10 | Name string 11 | } 12 | 13 | // Button for keyboard, which sends to user 14 | type Button struct { 15 | Action struct { 16 | Type string `json:"type"` 17 | Payload string `json:"payload"` 18 | Label string `json:"label"` 19 | } `json:"action"` 20 | Color string `json:"color"` 21 | } 22 | 23 | // Keyboard to send for user 24 | type Keyboard struct { 25 | OneTime bool `json:"one_time"` 26 | Buttons [][]Button `json:"buttons"` 27 | } 28 | 29 | // Reply for message 30 | type Reply struct { 31 | Msg string 32 | Keyboard *Keyboard 33 | } 34 | 35 | // Message - VK message struct 36 | type Message struct { 37 | ID int64 38 | Date int 39 | Out int 40 | UserID int64 `json:"user_id"` 41 | ChatID int64 `json:"chat_id"` 42 | PeerID int64 `json:"peer_id"` 43 | ReadState int `json:"read_state"` 44 | Title string 45 | Body string 46 | Action string 47 | ActionMID int64 `json:"action_mid"` 48 | Flags int 49 | Timestamp int64 50 | Payload string 51 | FwdMessages []Message `json:"fwd_messages"` 52 | } 53 | 54 | // Messages - VK Messages 55 | type Messages struct { 56 | Count int 57 | Items []*Message 58 | } 59 | 60 | // MessagesResponse - VK messages response 61 | type MessagesResponse struct { 62 | Response Messages 63 | Error *VKError 64 | } 65 | 66 | // Geo - City and Country info 67 | type Geo struct { 68 | ID int `json:"id"` 69 | Title string `json:"title"` 70 | } 71 | 72 | // User - simple VK user struct 73 | type User struct { 74 | ID int64 `json:"id"` 75 | FirstName string `json:"first_name"` 76 | LastName string `json:"last_name"` 77 | ScreenName string `json:"screen_name"` 78 | Photo string `json:"photo"` 79 | InvitedBy int64 `json:"invited_by"` 80 | City Geo `json:"city"` 81 | Country Geo `json:"country"` 82 | Sex int `json:"sex"` 83 | BDate string `json:"bdate"` 84 | Photo50 string `json:"photo_50"` 85 | Photo100 string `json:"photo_100"` 86 | Status string `json:"status"` 87 | About string `json:"about"` 88 | Relation int `json:"relation"` 89 | Hidden int `json:"hidden"` 90 | Closed bool `json:"is_closed"` 91 | CanAccessClosed bool `json:"can_access_closed"` 92 | Deactivated string `json:"deactivated"` 93 | IsAdmin bool `json:"is_admin"` 94 | IsOwner bool `json:"is_owner"` 95 | } 96 | 97 | // FullName - returns full name of user 98 | func (u *User) FullName() string { 99 | if u != nil { 100 | return strings.Trim(u.FirstName+" "+u.LastName, " ") 101 | } 102 | return "" 103 | } 104 | 105 | // VKUsers - Users list. Can be sort by full name 106 | type VKUsers []*User 107 | 108 | // MemberItem - conversation item 109 | type MemberItem struct { 110 | MemberID int64 `json:"member_id"` 111 | JoinDate int `json:"join_date"` 112 | IsOwner bool `json:"is_owner"` 113 | IsAdmin bool `json:"is_admin"` 114 | InvitedBy int64 `json:"invited_by"` 115 | } 116 | 117 | // UserProfile - conversation user profile 118 | type UserProfile struct { 119 | ID int64 120 | FirstName string `json:"first_name"` 121 | LastName string `json:"last_name"` 122 | IsClosed bool `json:"is_closed"` 123 | CanAccessClosed bool `json:"can_access_closed"` 124 | Sex int 125 | ScreenName string `json:"screen_name"` 126 | BDate string `json:"bdate"` 127 | Photo string 128 | Online int 129 | City Geo 130 | Country Geo 131 | } 132 | 133 | // GroupProfile - conversation group profile 134 | type GroupProfile struct { 135 | ID int64 `json:"id"` 136 | Name string `json:"name"` 137 | ScreenName string `json:"screen_name"` 138 | IsClosed int `json:"is_closed"` 139 | Type string `json:"type"` 140 | Photo50 string `json:"photo50"` 141 | Photo100 string `json:"photo100"` 142 | Photo200 string `json:"photo200"` 143 | } 144 | 145 | // VKMembers - conversation members info 146 | type VKMembers struct { 147 | Items []MemberItem `json:"items"` 148 | Profiles []UserProfile `json:"profiles"` 149 | Groups []GroupProfile `json:"groups"` 150 | } 151 | 152 | // UsersResponse - VK user response 153 | type UsersResponse struct { 154 | Response VKUsers 155 | Error *VKError 156 | } 157 | 158 | // MembersResponse - VK user response 159 | type MembersResponse struct { 160 | Response VKMembers 161 | Error *VKError 162 | } 163 | 164 | // FriendRequests - VK friend requests 165 | type FriendRequests struct { 166 | Count int 167 | Items []int64 168 | } 169 | 170 | // FriendRequestsResponse - VK friend requests response 171 | type FriendRequestsResponse struct { 172 | Response FriendRequests 173 | Error *VKError 174 | } 175 | 176 | // FriendDeleteResponse - VK friend delete response 177 | type FriendDeleteResponse struct { 178 | Response map[string]int 179 | Error *VKError 180 | } 181 | 182 | // SimpleResponse - simple int response 183 | type SimpleResponse struct { 184 | Response int64 185 | Error *VKError 186 | } 187 | 188 | // ErrorResponse - need to parse VK error 189 | type ErrorResponse struct { 190 | Error *VKError 191 | } 192 | 193 | // ChatInfo - chat info 194 | type ChatInfo struct { 195 | ID int64 `json:"id"` 196 | Type string `json:"type"` 197 | Title string `json:"title"` 198 | Kicked int `json:"kicked"` 199 | AdminID int64 `json:"admin_id"` 200 | Users VKUsers `json:"users"` 201 | } 202 | 203 | // VKError - error info 204 | type VKError struct { 205 | ErrorCode int `json:"error_code"` 206 | ErrorMsg string `json:"error_msg"` 207 | // RequestParams 208 | } 209 | 210 | // VKError - error with response content 211 | type ResponseError struct { 212 | err error 213 | content string 214 | } 215 | 216 | func (err ResponseError) Error() string { 217 | return err.err.Error() 218 | } 219 | 220 | // Content - error content 221 | func (err ResponseError) Content() string { 222 | return err.content 223 | } 224 | 225 | // ConversationInfo - conversation info 226 | type ConversationInfo struct { 227 | Peer struct { 228 | ID int64 229 | Type string 230 | LocalID int `json:"local_id"` 231 | } 232 | InRead int `json:"in_read"` 233 | OutRead int `json:"out_read"` 234 | LastMessageID int `json:"last_message_id"` 235 | CanWrite struct { 236 | Allowed bool 237 | } `json:"can_write"` 238 | ChatSettings struct { 239 | Title string 240 | MembersCount int `json:"members_count"` 241 | State string 242 | ActiveIDs []int `json:"active_ids"` 243 | ACL struct { 244 | CanInvite bool `json:"can_invite"` 245 | CanChangeInfo bool `json:"can_change_info"` 246 | CanChangePin bool `json:"can_change_pin"` 247 | CanPromoteUsers bool `json:"can_promote_users"` 248 | CanSeeInviteLink bool `json:"can_see_invite_link"` 249 | CanChangeInviteLink bool `json:"can_change_invite_link"` 250 | } 251 | IsGroupChannel bool `json:"is_group_channel"` 252 | OwnerID int64 `json:"owner_id"` 253 | } `json:"chat_settings"` 254 | } 255 | 256 | // ConversationsResponse - resonse of confersations info 257 | type ConversationsResponse struct { 258 | Response struct { 259 | Items []ConversationInfo 260 | Profiles []UserProfile 261 | } 262 | Error *VKError 263 | } 264 | 265 | // ChatInfoResponse - chat info vk struct 266 | type ChatInfoResponse struct { 267 | Response ChatInfo 268 | Error *VKError 269 | } 270 | 271 | func (err VKError) Error() string { 272 | return "vk: " + err.ErrorMsg 273 | } 274 | 275 | func (a VKUsers) Len() int { return len(a) } 276 | func (a VKUsers) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 277 | func (a VKUsers) Less(i, j int) bool { return a[i].FullName() < a[j].FullName() } 278 | -------------------------------------------------------------------------------- /structs_test.go: -------------------------------------------------------------------------------- 1 | package govkbot 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestUser_FullName(t *testing.T) { 8 | var u *User 9 | if u.FullName() != "" { 10 | t.Error("User name must be blank") 11 | } 12 | u1 := User{FirstName: "First", LastName: "Last"} 13 | if u1.FullName() != "First Last" { 14 | t.Error("Wrong full user name") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package govkbot 2 | 3 | import "strings" 4 | 5 | // HasPrefix tests case insensitive whether the string s begins with prefix. 6 | func HasPrefix(s, prefix string) bool { 7 | return len(s) >= len(prefix) && strings.ToLower(s[0:len(prefix)]) == strings.ToLower(prefix) 8 | } 9 | 10 | // TrimPrefix returns s without the provided leading case insensitive prefix string. 11 | // If s doesn't start with prefix, s is returned unchanged. 12 | func TrimPrefix(s, prefix string) string { 13 | if HasPrefix(s, prefix) { 14 | return s[len(prefix):] 15 | } 16 | return s 17 | } 18 | --------------------------------------------------------------------------------