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  }