wechaty/wechaty.go
/**
* Go Wechaty - https://github.com/wechaty/go-wechaty
*
* Authors: Huan LI (李卓桓) <https://github.com/huan>
* Xiaoyu DING (丁小雨) <https://github.com/dingdayu>
*
* 2020-now @ Copyright Wechaty <https://github.com/wechaty>
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Package wechaty ...
package wechaty
import (
"context"
"errors"
"fmt"
"github.com/lucsky/cuid"
wp "github.com/wechaty/go-wechaty/wechaty-puppet"
puppetservice "github.com/wechaty/go-wechaty/wechaty-puppet-service"
"github.com/wechaty/go-wechaty/wechaty-puppet/events"
mc "github.com/wechaty/go-wechaty/wechaty-puppet/memory-card"
"github.com/wechaty/go-wechaty/wechaty-puppet/schemas"
"github.com/wechaty/go-wechaty/wechaty/factory"
"github.com/wechaty/go-wechaty/wechaty/interface"
"os"
"os/signal"
"reflect"
"runtime/debug"
"time"
)
// Wechaty ...
type Wechaty struct {
*Option
// cuid
id string
puppet wp.IPuppetAbstract
events events.EventEmitter
message _interface.IMessageFactory
room _interface.IRoomFactory
contact _interface.IContactFactory
tag _interface.ITagFactory
friendship _interface.IFriendshipFactory
image _interface.IImageFactory
urlLink _interface.IUrlLinkFactory
roomInvitation _interface.IRoomInvitationFactory
}
// NewWechaty ...
// instance by golang.
func NewWechaty(optFns ...OptionFn) *Wechaty {
var w = &Wechaty{events: events.New(), Option: &Option{}}
for _, fn := range optFns {
fn(w.Option)
}
w.id = cuid.New()
return w
}
func (w *Wechaty) String() string {
return fmt.Sprintf("Wechaty#%s", w.id)
}
// Name Wechaty bot name set by `options.name`
func (w *Wechaty) Name() string {
if len(w.Option.name) != 0 {
return w.Option.name
}
return "wechaty"
}
// register event
func (w *Wechaty) registerEvent(name schemas.PuppetEventName, f interface{}) {
w.events.On(name, func(data ...interface{}) {
defer func() {
if err := recover(); err != nil {
log.Error("panic: ", err)
log.Error(string(debug.Stack()))
w.emit(schemas.PuppetEventNameError, NewContext(), fmt.Errorf("panic: event %s %v", name, err))
}
}()
values := make([]reflect.Value, 0, len(data))
for _, v := range data {
values = append(values, reflect.ValueOf(v))
}
_ = reflect.ValueOf(f).Call(values)
})
}
// OnScan ...
func (w *Wechaty) OnScan(f EventScan) *Wechaty {
w.registerEvent(schemas.PuppetEventNameScan, f)
return w
}
// OnLogin ...
func (w *Wechaty) OnLogin(f EventLogin) *Wechaty {
w.registerEvent(schemas.PuppetEventNameLogin, f)
return w
}
// OnMessage ...
func (w *Wechaty) OnMessage(f EventMessage) *Wechaty {
w.registerEvent(schemas.PuppetEventNameMessage, f)
return w
}
// OnDong ...
func (w *Wechaty) OnDong(f EventDong) *Wechaty {
w.registerEvent(schemas.PuppetEventNameDong, f)
return w
}
// OnError ...
func (w *Wechaty) OnError(f EventError) *Wechaty {
w.registerEvent(schemas.PuppetEventNameError, f)
return w
}
// OnFriendship ...
func (w *Wechaty) OnFriendship(f EventFriendship) *Wechaty {
w.registerEvent(schemas.PuppetEventNameFriendship, f)
return w
}
// OnHeartbeat ...
func (w *Wechaty) OnHeartbeat(f EventHeartbeat) *Wechaty {
w.registerEvent(schemas.PuppetEventNameHeartbeat, f)
return w
}
// OnLogout ...
func (w *Wechaty) OnLogout(f EventLogout) *Wechaty {
w.registerEvent(schemas.PuppetEventNameLogout, f)
return w
}
// OnReady ...
func (w *Wechaty) OnReady(f EventReady) *Wechaty {
w.registerEvent(schemas.PuppetEventNameReady, f)
return w
}
// OnRoomInvite ...
func (w *Wechaty) OnRoomInvite(f EventRoomInvite) *Wechaty {
w.registerEvent(schemas.PuppetEventNameRoomInvite, f)
return w
}
// OnRoomJoin ...
func (w *Wechaty) OnRoomJoin(f EventRoomJoin) *Wechaty {
w.registerEvent(schemas.PuppetEventNameRoomJoin, f)
return w
}
// OnRoomLeave ...
func (w *Wechaty) OnRoomLeave(f EventRoomLeave) *Wechaty {
w.registerEvent(schemas.PuppetEventNameRoomLeave, f)
return w
}
// OnRoomTopic ...
func (w *Wechaty) OnRoomTopic(f EventRoomTopic) *Wechaty {
w.registerEvent(schemas.PuppetEventNameRoomTopic, f)
return w
}
// OnStart ...
func (w *Wechaty) OnStart(f EventStart) *Wechaty {
w.registerEvent(schemas.PuppetEventNameStart, f)
return w
}
// OnStop ...
func (w *Wechaty) OnStop(f EventStop) *Wechaty {
w.registerEvent(schemas.PuppetEventNameStop, f)
return w
}
// Use loads a plugin.
func (w *Wechaty) Use(plugin *Plugin) *Wechaty {
plugin.registerPluginEvent(w)
return w
}
func (w *Wechaty) emit(name schemas.PuppetEventName, data ...interface{}) {
w.events.Emit(name, data...)
}
// init puppet
func (w *Wechaty) initPuppet() error {
if w.puppet != nil {
log.Warn("Puppet already inited.")
return nil
}
if w.memoryCard == nil {
return errors.New("memory card not init")
}
// TODO: set puppet memory
if w.Option.puppet == nil {
// 之前是 puppet.Option 现在改为了 puppetservice.Options 需要兼容处理
puppetServiceOption := w.Option.puppetServiceOptions
if w.puppetOption.Token != "" {
puppetServiceOption.Option = w.puppetOption
}
//nolint:staticcheck
if w.puppetOption.GrpcReconnectInterval != 0 {
puppetServiceOption.GrpcReconnectInterval = w.puppetOption.GrpcReconnectInterval
}
puppet, err := puppetservice.NewNewPuppetService(puppetServiceOption)
if err != nil {
return err
}
w.puppet = puppet
} else {
w.puppet = w.Option.puppet
}
w.initPuppetEventBridge()
w.initPuppetAccessory()
return nil
}
func (w *Wechaty) initPuppetAccessory() {
accessory := &Accessory{
puppet: w.puppet,
wechaty: w,
}
w.message = &factory.MessageFactory{IAccessory: accessory}
w.contact = factory.NewContactFactory(accessory)
w.room = factory.NewRoomFactory(accessory)
w.tag = factory.NewTagFactory(accessory)
w.friendship = &factory.FriendshipFactory{IAccessory: accessory}
w.image = &factory.ImageFactory{IAccessory: accessory}
w.urlLink = &factory.UrlLinkFactory{}
w.roomInvitation = &factory.RoomInvitationFactory{IAccessory: accessory}
}
// Start ...
func (w *Wechaty) Start() error {
var err error
// TODO: check wechaty on, impl state events
if w.memoryCard == nil {
w.memoryCard, err = mc.NewMemoryCard(w.name)
if err != nil {
log.Error("memory card new err: ", err)
return err
}
}
err = w.memoryCard.Load()
if err != nil {
log.Error("memory card load err: ", err)
return err
}
err = w.initPuppet()
if err != nil {
log.Error("w.initPuppet err: ", err)
return err
}
err = w.puppet.Start()
if err != nil {
log.Error("puppet start err: ", err)
return err
}
// TODO: io start
go w.emit(schemas.PuppetEventNameStart, NewContext())
return nil
}
// DaemonStart 守护进程运行
func (w *Wechaty) DaemonStart() {
if err := w.Start(); err != nil {
panic(err)
}
var quitSig = make(chan os.Signal, 1)
signal.Notify(quitSig, os.Interrupt)
<-quitSig
log.Fatal("exit.by.signal")
}
func (w *Wechaty) initPuppetEventBridge() {
// TODO temporary
for _, name := range schemas.GetEventNames() {
name := name
switch name {
case schemas.PuppetEventNameDong:
w.puppet.On(name, func(i ...interface{}) {
w.emit(name, NewContext(), i[0].(*schemas.EventDongPayload).Data)
})
case schemas.PuppetEventNameError:
w.puppet.On(name, func(i ...interface{}) {
w.emit(name, NewContext(), errors.New(i[0].(*schemas.EventErrorPayload).Data))
})
case schemas.PuppetEventNameHeartbeat:
w.puppet.On(name, func(i ...interface{}) {
w.emit(name, NewContext(), i[0].(*schemas.EventHeartbeatPayload).Data)
})
case schemas.PuppetEventNameLogin:
w.puppet.On(name, func(i ...interface{}) {
contact := w.contact.LoadSelf(i[0].(*schemas.EventLoginPayload).ContactId)
if err := contact.Ready(false); err != nil {
log.Errorf("emit login contact.Ready err: %s\n", err.Error())
w.emit(schemas.PuppetEventNameError, NewContext(), err)
return
}
w.emit(name, NewContext(), contact)
})
case schemas.PuppetEventNameLogout:
w.puppet.On(name, func(i ...interface{}) {
payload := i[0].(*schemas.EventLogoutPayload)
contact := w.contact.LoadSelf(payload.ContactId)
if err := contact.Ready(false); err != nil {
log.Errorf("emit logout contact.Ready err: %s\n", err.Error())
w.emit(schemas.PuppetEventNameError, NewContext(), err)
return
}
w.emit(name, NewContext(), contact, payload.Data)
})
case schemas.PuppetEventNameScan:
w.puppet.On(name, func(i ...interface{}) {
payload := i[0].(*schemas.EventScanPayload)
w.emit(name, NewContext(), payload.QrCode, payload.Status, payload.Data)
})
case schemas.PuppetEventNameMessage:
w.puppet.On(name, func(i ...interface{}) {
messageID := i[0].(*schemas.EventMessagePayload).MessageId
message := w.message.Load(messageID)
if err := message.Ready(); err != nil {
log.Errorf("emit message message.Ready(%s) err: %s\n", messageID, err.Error())
w.emit(schemas.PuppetEventNameError, NewContext(), err)
return
}
w.emit(name, NewContext(), message)
})
case schemas.PuppetEventNameFriendship:
w.puppet.On(name, func(i ...interface{}) {
friendship := w.friendship.Load(i[0].(*schemas.EventFriendshipPayload).FriendshipID)
if err := friendship.Ready(); err != nil {
log.Errorf("emit friendship friendship.Ready() err: %s\n", err.Error())
w.emit(schemas.PuppetEventNameError, NewContext(), err)
return
}
w.emit(name, NewContext(), friendship)
})
case schemas.PuppetEventNameReady:
w.puppet.On(name, func(i ...interface{}) {
w.emit(name, NewContext())
})
case schemas.PuppetEventNameRoomInvite:
w.puppet.On(name, func(i ...interface{}) {
roomInvitation := w.roomInvitation.Load(i[0].(*schemas.EventRoomInvitePayload).RoomInvitationId)
w.emit(name, NewContext(), roomInvitation)
})
case schemas.PuppetEventNameRoomJoin:
w.puppet.On(name, func(i ...interface{}) {
payload := i[0].(*schemas.EventRoomJoinPayload)
room := w.room.Load(payload.RoomId)
if err := room.Sync(); err != nil {
log.Errorf("emit roomjoin room.Sync() err: %s\n", err.Error())
w.emit(schemas.PuppetEventNameError, NewContext(), err)
return
}
var inviteeList []_interface.IContact
for _, id := range payload.InviteeIdList {
c := w.contact.Load(id)
if err := c.Ready(false); err != nil {
log.Errorf("emit roomjoin contact.Ready() err: %s\n", err.Error())
w.emit(schemas.PuppetEventNameError, NewContext(), err)
return
}
inviteeList = append(inviteeList, c)
}
inviter := w.contact.Load(payload.InviterId)
if err := inviter.Ready(false); err != nil {
log.Errorf("emit roomjoin inviter.Ready() err: %s\n", err.Error())
w.emit(schemas.PuppetEventNameError, NewContext(), err)
return
}
w.emit(name, NewContext(), room, inviteeList, inviter, time.Unix(payload.Timestamp, 0))
})
case schemas.PuppetEventNameRoomLeave:
w.puppet.On(name, func(i ...interface{}) {
payload := i[0].(*schemas.EventRoomLeavePayload)
room := w.room.Load(payload.RoomId)
if err := room.Sync(); err != nil {
log.Errorf("emit roomleave room.Sync() err: %s\n", err.Error())
w.emit(schemas.PuppetEventNameError, NewContext(), err)
return
}
var leaverList []_interface.IContact
for _, id := range payload.RemoveeIdList {
c := w.contact.Load(id)
if err := c.Ready(false); err != nil {
log.Errorf("emit roomleave contact.Ready() err: %s\n", err.Error())
w.emit(schemas.PuppetEventNameError, NewContext(), err)
return
}
leaverList = append(leaverList, c)
}
remover := w.contact.Load(payload.RemoverId)
if err := remover.Ready(false); err != nil {
log.Errorf("emit roomleave inviter.Ready() err: %s\n", err.Error())
w.emit(schemas.PuppetEventNameError, NewContext(), err)
return
}
w.emit(name, NewContext(), room, leaverList, remover, time.Unix(payload.Timestamp, 0))
selfID := w.puppet.SelfID()
for _, id := range payload.RemoveeIdList {
if id != selfID {
continue
}
_ = w.puppet.DirtyPayload(schemas.PayloadTypeRoom, payload.RoomId)
_ = w.puppet.DirtyPayload(schemas.PayloadTypeRoomMember, payload.RoomId)
}
})
case schemas.PuppetEventNameRoomTopic:
w.puppet.On(name, func(i ...interface{}) {
payload := i[0].(*schemas.EventRoomTopicPayload)
room := w.room.Load(payload.RoomId)
if err := room.Sync(); err != nil {
log.Errorf("emit roomtopic room.Sync() err: %s\n", err.Error())
w.emit(schemas.PuppetEventNameError, NewContext(), err)
return
}
changer := w.contact.Load(payload.ChangerId)
if err := changer.Ready(false); err != nil {
log.Errorf("emit roomtopic changer.Ready() err: %s\n", err.Error())
w.emit(schemas.PuppetEventNameError, NewContext(), err)
return
}
w.emit(name, NewContext(), room, payload.NewTopic, payload.OldTopic, changer, time.Unix(payload.Timestamp, 0))
})
case schemas.PuppetEventNameDirty:
/**
* https://github.com/wechaty/go-wechaty/issues/72
*/
w.puppet.On(name, func(i ...interface{}) {
payload := i[0].(*schemas.EventDirtyPayload)
switch payload.PayloadType {
case schemas.PayloadTypeRoomMember,
schemas.PayloadTypeContact:
if err := w.contact.Load(payload.PayloadId).Ready(true); err != nil {
log.Errorf("emit dirty contact.Ready() err: %s\n", err.Error())
w.emit(schemas.PuppetEventNameError, NewContext(), err)
return
}
case schemas.PayloadTypeRoom:
if err := w.room.Load(payload.PayloadId).Ready(true); err != nil {
log.Errorf("emit dirty room.Ready() err: %s\n", err.Error())
w.emit(schemas.PuppetEventNameError, NewContext(), err)
return
}
case schemas.PayloadTypeFriendship:
// Friendship has no payload
return
case schemas.PayloadTypeMessage:
// Message does not need to dirty (?)
return
case schemas.PayloadTypeUnknown:
fallthrough
default:
log.Errorf("unknown payload type: %s\n", payload.PayloadType)
}
})
default:
}
}
}
// Room ...
func (w *Wechaty) Room() _interface.IRoomFactory {
return w.room
}
// Message ...
func (w *Wechaty) Message() _interface.IMessageFactory {
return w.message
}
// Contact ...
func (w *Wechaty) Contact() _interface.IContactFactory {
return w.contact
}
// Tag ...
func (w *Wechaty) Tag() _interface.ITagFactory {
return w.tag
}
// Friendship ...
func (w *Wechaty) Friendship() _interface.IFriendshipFactory {
return w.friendship
}
// Image ...
func (w *Wechaty) Image() _interface.IImageFactory {
return w.image
}
// URLLink ...
func (w *Wechaty) URLLink() _interface.IUrlLinkFactory {
return w.urlLink
}
// RoomInvitation ...
func (w *Wechaty) RoomInvitation() _interface.IRoomInvitationFactory {
return w.roomInvitation
}
// Puppet return puppet impl
func (w *Wechaty) Puppet() wp.IPuppetAbstract {
return w.puppet
}
// UserSelf return contact self
func (w *Wechaty) UserSelf() _interface.IContactSelf {
userID := w.puppet.SelfID()
return w.Contact().LoadSelf(userID)
}
// Context ...
type Context struct {
context.Context
cancel func()
abort bool
disableOncePlugins []*Plugin
data map[string]interface{}
}
// NewContext ...
func NewContext() *Context {
ctx, cancel := context.WithCancel(context.Background())
return &Context{
abort: false,
Context: ctx,
cancel: cancel,
data: map[string]interface{}{},
}
}
// IsActive returns whether the plugin is active now.
func (c *Context) IsActive(plugin *Plugin) bool {
if !plugin.IsEnable() {
return false
}
for _, p := range c.disableOncePlugins {
if p == plugin {
return false
}
}
return true
}
// DisableOnce disables a plugin temperarily.
// The plugin will be active again(if it is enable).
func (c *Context) DisableOnce(plugin *Plugin) {
c.disableOncePlugins = append(c.disableOncePlugins, plugin)
}
// Abort stops executing all follow-up plugins
// and terminates goroutuines which listen to Context.Done() (See go programming language context.Context. https://golang.org/pkg/context/)
func (c *Context) Abort() {
c.abort = true
c.cancel()
}
// GetData returns temperary data
// which only exists in the current context.
func (c *Context) GetData(name string) interface{} {
return c.data[name]
}
// SetData sets temperary data
// which only exists in the current context.
func (c *Context) SetData(name string, value interface{}) {
c.data[name] = value
}