| 1 | package bvk |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "regexp" |
| 7 | "strconv" |
| 8 | "strings" |
| 9 | "time" |
| 10 | |
| 11 | "github.com/matterbridge-org/matterbridge/bridge" |
| 12 | "github.com/matterbridge-org/matterbridge/bridge/config" |
| 13 | "github.com/matterbridge-org/matterbridge/bridge/helper" |
| 14 | |
| 15 | "github.com/SevereCloud/vksdk/v2/api" |
| 16 | "github.com/SevereCloud/vksdk/v2/events" |
| 17 | longpoll "github.com/SevereCloud/vksdk/v2/longpoll-bot" |
| 18 | "github.com/SevereCloud/vksdk/v2/object" |
| 19 | ) |
| 20 | |
| 21 | const ( |
| 22 | audioMessage = "audio_message" |
| 23 | document = "doc" |
| 24 | photo = "photo" |
| 25 | video = "video" |
| 26 | graffiti = "graffiti" |
| 27 | sticker = "sticker" |
| 28 | wall = "wall" |
| 29 | ) |
| 30 | |
| 31 | type user struct { |
| 32 | lastname, firstname, avatar string |
| 33 | } |
| 34 | |
| 35 | type Bvk struct { |
| 36 | c *api.VK |
| 37 | lp *longpoll.LongPoll |
| 38 | usernamesMap map[int]user // cache of user names and avatar URLs |
| 39 | *bridge.Config |
| 40 | } |
| 41 | |
| 42 | func New(cfg *bridge.Config) bridge.Bridger { |
| 43 | return &Bvk{usernamesMap: make(map[int]user), Config: cfg} |
| 44 | } |
| 45 | |
| 46 | func (b *Bvk) Connect() error { |
| 47 | b.Log.Info("Connecting") |
| 48 | b.c = api.NewVK(b.GetString("Token")) |
| 49 | |
| 50 | var err error |
| 51 | b.lp, err = longpoll.NewLongPollCommunity(b.c) |
| 52 | if err != nil { |
| 53 | b.Log.Debugf("%#v", err) |
| 54 | |
| 55 | return err |
| 56 | } |
| 57 | |
| 58 | b.lp.MessageNew(func(ctx context.Context, obj events.MessageNewObject) { |
| 59 | b.handleMessage(obj.Message, false) |
| 60 | }) |
| 61 | |
| 62 | b.Log.Info("Connection succeeded") |
| 63 | |
| 64 | go func() { |
| 65 | err := b.lp.Run() |
| 66 | if err != nil { |
| 67 | b.Log.WithError(err).Fatal("Enable longpoll in group management") |
| 68 | } |
| 69 | }() |
| 70 | |
| 71 | return nil |
| 72 | } |
| 73 | |
| 74 | func (b *Bvk) Disconnect() error { |
| 75 | b.lp.Shutdown() |
| 76 | |
| 77 | return nil |
| 78 | } |
| 79 | |
| 80 | func (b *Bvk) JoinChannel(channel config.ChannelInfo) error { |
| 81 | return nil |
| 82 | } |
| 83 | |
| 84 | func (b *Bvk) Send(msg config.Message) (string, error) { |
| 85 | b.Log.Debugf("=> Receiving %#v", msg) |
| 86 | |
| 87 | peerID, err := strconv.Atoi(msg.Channel) |
| 88 | if err != nil { |
| 89 | return "", err |
| 90 | } |
| 91 | |
| 92 | params := api.Params{} |
| 93 | |
| 94 | text := msg.Username + msg.Text |
| 95 | |
| 96 | if msg.Extra != nil { |
| 97 | if len(msg.Extra["file"]) > 0 { |
| 98 | // generate attachments string |
| 99 | attachment, urls := b.uploadFiles(msg.Extra, peerID) |
| 100 | params["attachment"] = attachment |
| 101 | text += urls |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | params["message"] = text |
| 106 | |
| 107 | if msg.ID == "" { |
| 108 | // New message |
| 109 | params["random_id"] = time.Now().Unix() |
| 110 | params["peer_ids"] = msg.Channel |
| 111 | |
| 112 | res, e := b.c.MessagesSendPeerIDs(params) |
| 113 | if e != nil { |
| 114 | return "", err |
| 115 | } |
| 116 | |
| 117 | return strconv.Itoa(res[0].ConversationMessageID), nil |
| 118 | } |
| 119 | // Edit message |
| 120 | messageID, err := strconv.ParseInt(msg.ID, 10, 64) |
| 121 | if err != nil { |
| 122 | return "", err |
| 123 | } |
| 124 | |
| 125 | params["peer_id"] = peerID |
| 126 | params["conversation_message_id"] = messageID |
| 127 | |
| 128 | _, err = b.c.MessagesEdit(params) |
| 129 | if err != nil { |
| 130 | return "", err |
| 131 | } |
| 132 | |
| 133 | return msg.ID, nil |
| 134 | } |
| 135 | |
| 136 | func (b *Bvk) getUser(id int) user { |
| 137 | u, found := b.usernamesMap[id] |
| 138 | if !found { |
| 139 | b.Log.Debug("Fetching username for ", id) |
| 140 | |
| 141 | if id >= 0 { |
| 142 | result, _ := b.c.UsersGet(api.Params{ |
| 143 | "user_ids": id, |
| 144 | "fields": "photo_200", |
| 145 | }) |
| 146 | |
| 147 | resUser := result[0] |
| 148 | u = user{lastname: resUser.LastName, firstname: resUser.FirstName, avatar: resUser.Photo200} |
| 149 | b.usernamesMap[id] = u |
| 150 | } else { |
| 151 | result, _ := b.c.GroupsGetByID(api.Params{ |
| 152 | "group_id": id * -1, |
| 153 | }) |
| 154 | |
| 155 | resGroup := result[0] |
| 156 | u = user{lastname: resGroup.Name, avatar: resGroup.Photo200} |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | return u |
| 161 | } |
| 162 | |
| 163 | func (b *Bvk) handleMessage(msg object.MessagesMessage, isFwd bool) { |
| 164 | b.Log.Debug("ChatID: ", msg.PeerID) |
| 165 | // fetch user info |
| 166 | u := b.getUser(msg.FromID) |
| 167 | |
| 168 | rmsg := config.Message{ |
| 169 | Text: msg.Text, |
| 170 | Username: u.firstname + " " + u.lastname, |
| 171 | Avatar: u.avatar, |
| 172 | Channel: strconv.Itoa(msg.PeerID), |
| 173 | Account: b.Account, |
| 174 | UserID: strconv.Itoa(msg.FromID), |
| 175 | ID: strconv.Itoa(msg.ConversationMessageID), |
| 176 | Extra: make(map[string][]interface{}), |
| 177 | } |
| 178 | |
| 179 | if msg.ReplyMessage != nil { |
| 180 | ur := b.getUser(msg.ReplyMessage.FromID) |
| 181 | rmsg.Text = "Re: " + ur.firstname + " " + ur.lastname + "\n" + rmsg.Text |
| 182 | } |
| 183 | |
| 184 | if isFwd { |
| 185 | rmsg.Username = "Fwd: " + rmsg.Username |
| 186 | } |
| 187 | |
| 188 | if len(msg.Attachments) > 0 { |
| 189 | urls, text := b.getFiles(msg.Attachments) |
| 190 | |
| 191 | if text != "" { |
| 192 | rmsg.Text += "\n" + text |
| 193 | } |
| 194 | |
| 195 | // download |
| 196 | b.downloadFiles(&rmsg, urls) |
| 197 | } |
| 198 | |
| 199 | if len(msg.FwdMessages) > 0 { |
| 200 | rmsg.Text += strconv.Itoa(len(msg.FwdMessages)) + " forwarded messages" |
| 201 | } |
| 202 | |
| 203 | b.Remote <- rmsg |
| 204 | |
| 205 | if len(msg.FwdMessages) > 0 { |
| 206 | // recursive processing of forwarded messages |
| 207 | for _, m := range msg.FwdMessages { |
| 208 | m.PeerID = msg.PeerID |
| 209 | b.handleMessage(m, true) |
| 210 | } |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | func (b *Bvk) uploadFiles(extra map[string][]interface{}, peerID int) (string, string) { |
| 215 | var attachments []string |
| 216 | text := "" |
| 217 | |
| 218 | for _, f := range extra["file"] { |
| 219 | fi := f.(config.FileInfo) |
| 220 | |
| 221 | if fi.Comment != "" { |
| 222 | text += fi.Comment + "\n" |
| 223 | } |
| 224 | a, err := b.uploadFile(fi, peerID) |
| 225 | if err != nil { |
| 226 | b.Log.WithError(err).Error("File upload error ", fi.Name) |
| 227 | } |
| 228 | |
| 229 | attachments = append(attachments, a) |
| 230 | } |
| 231 | |
| 232 | return strings.Join(attachments, ","), text |
| 233 | } |
| 234 | |
| 235 | func (b *Bvk) uploadFile(file config.FileInfo, peerID int) (string, error) { |
| 236 | r := bytes.NewReader(*file.Data) |
| 237 | |
| 238 | photoRE := regexp.MustCompile(".(jpg|jpe|png)$") |
| 239 | if photoRE.MatchString(file.Name) { |
| 240 | // BUG(VK): for community chat peerID=0 |
| 241 | p, err := b.c.UploadMessagesPhoto(0, r) |
| 242 | if err != nil { |
| 243 | return "", err |
| 244 | } |
| 245 | |
| 246 | return photo + strconv.Itoa(p[0].OwnerID) + "_" + strconv.Itoa(p[0].ID), nil |
| 247 | } |
| 248 | |
| 249 | var doctype string |
| 250 | if strings.Contains(file.Name, ".ogg") { |
| 251 | doctype = audioMessage |
| 252 | } else { |
| 253 | doctype = document |
| 254 | } |
| 255 | |
| 256 | doc, err := b.c.UploadMessagesDoc(peerID, doctype, file.Name, "", r) |
| 257 | if err != nil { |
| 258 | return "", err |
| 259 | } |
| 260 | |
| 261 | switch doc.Type { |
| 262 | case audioMessage: |
| 263 | return document + strconv.Itoa(doc.AudioMessage.OwnerID) + "_" + strconv.Itoa(doc.AudioMessage.ID), nil |
| 264 | case document: |
| 265 | return document + strconv.Itoa(doc.Doc.OwnerID) + "_" + strconv.Itoa(doc.Doc.ID), nil |
| 266 | } |
| 267 | |
| 268 | return "", nil |
| 269 | } |
| 270 | |
| 271 | func (b *Bvk) getFiles(attachments []object.MessagesMessageAttachment) ([]string, string) { |
| 272 | var urls []string |
| 273 | var text []string |
| 274 | |
| 275 | for _, a := range attachments { |
| 276 | switch a.Type { |
| 277 | case photo: |
| 278 | var resolution float64 = 0 |
| 279 | url := a.Photo.Sizes[0].URL |
| 280 | for _, size := range a.Photo.Sizes { |
| 281 | r := size.Height * size.Width |
| 282 | if resolution < r { |
| 283 | resolution = r |
| 284 | url = size.URL |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | urls = append(urls, url) |
| 289 | |
| 290 | case document: |
| 291 | urls = append(urls, a.Doc.URL) |
| 292 | |
| 293 | case graffiti: |
| 294 | urls = append(urls, a.Graffiti.URL) |
| 295 | |
| 296 | case audioMessage: |
| 297 | urls = append(urls, a.AudioMessage.DocsDocPreviewAudioMessage.LinkOgg) |
| 298 | |
| 299 | case sticker: |
| 300 | var resolution float64 = 0 |
| 301 | url := a.Sticker.Images[0].URL |
| 302 | for _, size := range a.Sticker.Images { |
| 303 | r := size.Height * size.Width |
| 304 | if resolution < r { |
| 305 | resolution = r |
| 306 | url = size.URL |
| 307 | } |
| 308 | } |
| 309 | urls = append(urls, url+".png") |
| 310 | case video: |
| 311 | text = append(text, "https://vk.com/video"+strconv.Itoa(a.Video.OwnerID)+"_"+strconv.Itoa(a.Video.ID)) |
| 312 | |
| 313 | case wall: |
| 314 | text = append(text, "https://vk.com/wall"+strconv.Itoa(a.Wall.FromID)+"_"+strconv.Itoa(a.Wall.ID)) |
| 315 | |
| 316 | default: |
| 317 | text = append(text, "This attachment is not supported ("+a.Type+")") |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | return urls, strings.Join(text, "\n") |
| 322 | } |
| 323 | |
| 324 | func (b *Bvk) downloadFiles(rmsg *config.Message, urls []string) { |
| 325 | for _, url := range urls { |
| 326 | data, err := helper.DownloadFile(url) |
| 327 | if err == nil { |
| 328 | urlPart := strings.Split(url, "/") |
| 329 | name := strings.Split(urlPart[len(urlPart)-1], "?")[0] |
| 330 | helper.HandleDownloadData(b.Log, rmsg, name, "", url, data, b.General) |
| 331 | } |
| 332 | } |
| 333 | } |
| 334 | |