github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/plugins/telegram/actor.go (about) 1 // This file is part of the Smart Home 2 // Program complex distribution https://github.com/e154/smart-home 3 // Copyright (C) 2016-2023, Filippov Alex 4 // 5 // This library is free software: you can redistribute it and/or 6 // modify it under the terms of the GNU Lesser General Public 7 // License as published by the Free Software Foundation; either 8 // version 3 of the License, or (at your option) any later version. 9 // 10 // This library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 // Library General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public 16 // License along with this library. If not, see 17 // <https://www.gnu.org/licenses/>. 18 19 package telegram 20 21 import ( 22 "context" 23 "fmt" 24 "path/filepath" 25 "strconv" 26 "strings" 27 "time" 28 29 "github.com/pkg/errors" 30 "go.uber.org/atomic" 31 tele "gopkg.in/telebot.v3" 32 33 "github.com/e154/smart-home/common" 34 "github.com/e154/smart-home/common/apperr" 35 "github.com/e154/smart-home/common/events" 36 m "github.com/e154/smart-home/models" 37 "github.com/e154/smart-home/plugins/notify" 38 notifyCommon "github.com/e154/smart-home/plugins/notify/common" 39 "github.com/e154/smart-home/system/supervisor" 40 ) 41 42 // Actor ... 43 type Actor struct { 44 *supervisor.BaseActor 45 isStarted *atomic.Bool 46 AccessToken string 47 bot *tele.Bot 48 actionPool chan events.EventCallEntityAction 49 notify *notify.Notify 50 } 51 52 // NewActor ... 53 func NewActor(entity *m.Entity, 54 service supervisor.Service) (*Actor, error) { 55 56 settings := NewSettings() 57 _, _ = settings.Deserialize(entity.Settings.Serialize()) 58 59 actor := &Actor{ 60 BaseActor: supervisor.NewBaseActor(entity, service), 61 actionPool: make(chan events.EventCallEntityAction, 1000), 62 isStarted: atomic.NewBool(false), 63 AccessToken: settings[AttrToken].Decrypt(), 64 notify: notify.NewNotify(service.Adaptors()), 65 } 66 67 if actor.Attrs == nil { 68 actor.Attrs = NewAttr() 69 } 70 71 if actor.Setts == nil { 72 actor.Setts = NewSettings() 73 } 74 75 // action worker 76 go func() { 77 for msg := range actor.actionPool { 78 actor.runAction(msg) 79 } 80 }() 81 82 return actor, nil 83 } 84 85 func (e *Actor) Destroy() { 86 if !e.isStarted.Load() { 87 return 88 } 89 _ = e.Service.EventBus().Unsubscribe(notify.TopicNotify, e.eventHandler) 90 e.notify.Shutdown() 91 92 if e.bot != nil { 93 e.bot.Stop() 94 } 95 e.isStarted.Store(false) 96 } 97 98 func (e *Actor) Spawn() { 99 100 var err error 101 if e.isStarted.Load() { 102 return 103 } 104 defer func() { 105 if err == nil { 106 e.isStarted.Store(true) 107 } 108 }() 109 110 if !common.TestMode() { 111 112 pref := tele.Settings{ 113 Token: e.AccessToken, 114 Poller: &tele.LongPoller{Timeout: 10 * time.Second}, 115 } 116 117 e.bot, err = tele.NewBot(pref) 118 if err != nil { 119 err = errors.Wrap(apperr.ErrInternal, err.Error()) 120 return 121 } 122 123 e.bot.Handle("/start", e.commandStart) 124 e.bot.Handle("/quit", e.commandQuit) 125 e.bot.Handle(tele.OnText, e.commandAction) 126 127 go e.bot.Start() 128 } 129 130 _ = e.Service.EventBus().Subscribe(notify.TopicNotify, e.eventHandler, false) 131 e.notify.Start() 132 133 e.BaseActor.Spawn() 134 } 135 136 func (e *Actor) sendMsg(message *m.Message, chatId int64) (messageID int, err error) { 137 138 var msg *tele.Message 139 defer func() { 140 if err == nil { 141 if msg != nil { 142 messageID = msg.ID 143 } 144 //go func() { _ = e.UpdateStatus() }() 145 log.Infof("Sent message '%v' to chatId '%d'", message.Attributes, chatId) 146 } 147 }() 148 if common.TestMode() { 149 messageID = 123 150 return 151 } 152 var chat *tele.Chat 153 if chat, err = e.bot.ChatByID(chatId); err != nil { 154 log.Error(err.Error()) 155 return 156 } 157 158 params := NewMessageParams() 159 if _, err = params.Deserialize(message.Attributes); err != nil { 160 return 161 } 162 163 var body interface{} 164 165 keys := params[AttrKeys].ArrayString() 166 167 // photos 168 urls := params[AttrPhotoUri].ArrayString() 169 if len(urls) > 0 { 170 for _, uri := range urls { 171 log.Infof("send photo %s", uri) 172 if msg, err = e.bot.Send(chat, &tele.Photo{File: tele.FromURL(uri)}); err != nil { 173 return 174 } 175 } 176 } 177 path := params[AttrPhotoPath].ArrayString() 178 if len(path) > 0 { 179 for _, uri := range path { 180 log.Infof("send photo %s", uri) 181 if msg, err = e.bot.Send(chat, &tele.Photo{File: tele.FromDisk(uri)}); err != nil { 182 return 183 } 184 } 185 } 186 187 // files 188 urls = params[AttrFileUri].ArrayString() 189 if len(urls) > 0 { 190 for _, uri := range urls { 191 log.Infof("send file %s", uri) 192 fileName := filepath.Base(uri) 193 if msg, err = e.bot.Send(chat, &tele.Document{File: tele.FromURL(uri), FileName: fileName}); err != nil { 194 return 195 } 196 } 197 } 198 path = params[AttrFilePath].ArrayString() 199 if len(path) > 0 { 200 for _, uri := range path { 201 log.Infof("send file %s", uri) 202 fileName := filepath.Base(uri) 203 if msg, err = e.bot.Send(chat, &tele.Document{File: tele.FromDisk(uri), FileName: fileName}); err != nil { 204 return 205 } 206 } 207 } 208 209 if body = params[AttrBody].String(); body != "" { 210 msg, err = e.bot.Send(chat, body, e.genPlainKeyboard(keys)) 211 } 212 return 213 } 214 215 func (e *Actor) getChatList() (list []m.TelegramChat, err error) { 216 list, _, err = e.Service.Adaptors().TelegramChat.List(context.Background(), 999, 0, "", "", e.Id) 217 return 218 } 219 220 // UpdateStatus ... 221 func (e *Actor) UpdateStatus() (err error) { 222 223 var attributeValues = make(m.AttributeValue) 224 // ... 225 226 e.AttrMu.Lock() 227 var changed bool 228 if changed, err = e.Attrs.Deserialize(attributeValues); !changed { 229 if err != nil { 230 log.Warn(err.Error()) 231 } 232 } 233 e.AttrMu.Unlock() 234 235 e.SaveState(false, true) 236 237 return 238 } 239 240 func (e *Actor) commandStart(c tele.Context) (err error) { 241 242 var ( 243 user = c.Sender() 244 chat = c.Chat() 245 text = c.Text() 246 ) 247 248 if pin := e.Setts[AttrPin].Decrypt(); pin != "" { 249 enterdPin := strings.Replace(text, "/start ", "", -1) 250 if pin != enterdPin { 251 log.Warnf("received start command with bad pin code: \"%s\", username \"%s\"", enterdPin, chat.Username) 252 return 253 } 254 } 255 256 _ = e.Service.Adaptors().TelegramChat.Add(context.Background(), m.TelegramChat{ 257 EntityId: e.Id, 258 ChatId: chat.ID, 259 Username: user.Username, 260 }) 261 log.Infof("user '%s' added to chat", user.Username) 262 263 e.runAction(events.EventCallEntityAction{ 264 ActionName: "/start", 265 EntityId: &e.Id, 266 Args: map[string]interface{}{ 267 "chatId": c.Chat().ID, 268 "username": c.Chat().Username, 269 "firstName": c.Chat().FirstName, 270 "lastName": c.Chat().LastName, 271 }, 272 }) 273 274 return 275 } 276 277 func (e *Actor) commandQuit(c tele.Context) (err error) { 278 279 var ( 280 chat = c.Chat() 281 ) 282 283 _ = e.Service.Adaptors().TelegramChat.Delete(context.Background(), e.Id, chat.ID) 284 menu := &tele.ReplyMarkup{RemoveKeyboard: true} 285 var message = "/start - subscriber again" 286 if pin := e.Setts[AttrPin].Decrypt(); pin != "" { 287 message = "/start [pin] - subscriber again" 288 } 289 err = c.Send(message, menu) 290 return 291 } 292 293 func (e *Actor) commandAction(c tele.Context) (err error) { 294 295 var ( 296 text = c.Text() 297 ) 298 299 e.runAction(events.EventCallEntityAction{ 300 ActionName: text, 301 EntityId: &e.Id, 302 Args: map[string]interface{}{ 303 "chatId": c.Chat().ID, 304 "username": c.Chat().Username, 305 }, 306 }) 307 return 308 } 309 310 func (e *Actor) addAction(event events.EventCallEntityAction) { 311 e.actionPool <- event 312 } 313 314 func (e *Actor) runAction(msg events.EventCallEntityAction) { 315 if action, ok := e.Actions[msg.ActionName]; ok { 316 if action.ScriptEngine != nil && action.ScriptEngine.Engine() != nil { 317 if _, err := action.ScriptEngine.Engine().AssertFunction(FuncEntityAction, msg.EntityId, action.Name, msg.Args); err != nil { 318 log.Error(errors.Wrapf(err, "entity id: %s ", e.Id).Error()) 319 } 320 return 321 } 322 } 323 if e.ScriptsEngine != nil && e.ScriptsEngine.Engine() != nil { 324 if _, err := e.ScriptsEngine.AssertFunction(FuncEntityAction, msg.EntityId, msg.ActionName, msg.Args); err != nil { 325 log.Error(errors.Wrapf(err, "entity id: %s ", e.Id).Error()) 326 } 327 } 328 } 329 330 // gen keyboard from actions 331 // [button][button][button] 332 // [button][button][button] 333 // [button][button][button] 334 func (e *Actor) genActionKeyboard() (menu *tele.ReplyMarkup) { 335 menu = &tele.ReplyMarkup{ 336 ResizeKeyboard: true, 337 RemoveKeyboard: len(e.Actions) == 0, 338 } 339 var row []tele.Btn 340 if len(e.Actions) == 0 { 341 return 342 } 343 for _, action := range e.Actions { 344 row = append(row, menu.Text(action.Name)) 345 } 346 menu.Reply(menu.Split(3, row)...) 347 return 348 } 349 350 func (e *Actor) genPlainKeyboard(keys []string) (menu *tele.ReplyMarkup) { 351 menu = &tele.ReplyMarkup{ 352 ResizeKeyboard: true, 353 RemoveKeyboard: len(keys) == 0, 354 } 355 var row []tele.Btn 356 for _, key := range keys { 357 row = append(row, menu.Text(key)) 358 } 359 menu.Reply(menu.Split(3, row)...) 360 return 361 } 362 363 // todo: prepare state 364 func (e *Actor) updateState(connected bool) { 365 info := e.Info() 366 var newStat = AttrOffline 367 if connected { 368 newStat = AttrConnected 369 } 370 if info.State != nil && info.State.Name == newStat { 371 return 372 } 373 _ = e.SetState(supervisor.EntityStateParams{ 374 NewState: common.String(newStat), 375 StorageSave: true, 376 }) 377 } 378 379 // Save ... 380 func (e *Actor) Save(msg notifyCommon.Message) (addresses []string, message *m.Message) { 381 message = &m.Message{ 382 Type: Name, 383 Attributes: msg.Attributes, 384 } 385 var err error 386 if message.Id, err = e.Service.Adaptors().Message.Add(context.Background(), message); err != nil { 387 log.Error(err.Error()) 388 } 389 390 attr := NewMessageParams() 391 _, _ = attr.Deserialize(message.Attributes) 392 393 params := NewMessageParams() 394 _, _ = params.Deserialize(message.Attributes) 395 396 if val := params[AttrChatID].Int64(); val != 0 { 397 addresses = []string{fmt.Sprintf("%d", val)} 398 } else { 399 addresses = []string{"broadcast"} 400 } 401 402 return 403 } 404 405 // Send ... 406 func (e *Actor) Send(address string, message *m.Message) (err error) { 407 if !e.isStarted.Load() { 408 return 409 } 410 411 var chatID *int64 412 if address != "" && address != "broadcast" { 413 var val int64 414 if val, err = strconv.ParseInt(address, 10, 64); err == nil { 415 chatID = common.Int64(val) 416 } 417 } 418 419 if chatID != nil { 420 if _, err = e.sendMsg(message, *chatID); err != nil { 421 log.Warn(err.Error()) 422 } 423 return 424 } 425 426 var list []m.TelegramChat 427 if list, err = e.getChatList(); err != nil { 428 return 429 } 430 for _, chat := range list { 431 if _, err = e.sendMsg(message, chat.ChatId); err != nil { 432 log.Warn(err.Error()) 433 } 434 } 435 436 return 437 } 438 439 // MessageParams ... 440 func (e *Actor) MessageParams() m.Attributes { 441 return NewMessageParams() 442 } 443 444 func (e *Actor) eventHandler(topic string, msg interface{}) { 445 446 switch v := msg.(type) { 447 case notifyCommon.Message: 448 if v.EntityId != nil && v.EntityId.PluginName() == Name { 449 e.notify.SaveAndSend(v, e) 450 } 451 } 452 }