package main import ( "fmt" "net/url" "strings" "time" "github.com/charmbracelet/log" "github.com/spf13/viper" "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) type simp struct { client *mautrix.Client dimensionServer string holodexToken string startTime time.Time currStream int maxStream int vtubers []*Vtuber stop chan bool } func newSimp() *simp { return &simp{ startTime: time.Now(), stop: make(chan bool), } } func (s *simp) Run() { ticker := time.NewTicker(time.Second * 30) for { select { case <-s.stop: return case <-ticker.C: roomResp, err := s.client.JoinedRooms() if err != nil { log.Errorf("error getting joined rooms: %v, skipping iteration", err) continue } rooms := roomResp.JoinedRooms // We're going to assume they're only stream one video at a time for _, v := range s.vtubers { err = v.Update(s.holodexToken) if err != nil { log.Error("error pinging holodex", "error", err) } for _, room := range rooms { if v.IsLive() { // check to see if already embeded var content YoutubeWidget err = s.client.StateEvent( room, event.NewEventType("im.vector.modular.widgets"), "dimension-m.video-simp-"+v.Name, &content, ) if err != nil { log.Errorf("error getting state event in room %v: %v", room, err) } if content.ID == "" { if v.AnnounceLive { s.client.SendText(room, v.LiveMsg) } else { if isValidURL(v.LiveMsg) { s.client.SendNotice(room, fmt.Sprintf("%v has gone live", v.Name)) } else { s.client.SendNotice(room, v.LiveMsg) } } s.client.SendNotice(room, fmt.Sprintf("%v's Title: %v", v.Name, v.CurrentStreamTitle)) var subs string for k := range v.Subs { subs += k.String() + " " } if len(v.Subs) > 0 { s.client.SendText(room, fmt.Sprintf("Pinging %v", subs)) } resp, err := s.client.SendStateEvent( room, event.NewEventType("im.vector.modular.widgets"), "dimension-m.video-simp-"+v.Name, s.NewYT(v.Name+"'s stream", v.CurrentStream, string(room)), ) if err != nil { log.Errorf("error embeding video: %v", err) } v.TotalStreams = v.TotalStreams + 1 s.currStream++ if s.currStream > s.maxStream { s.maxStream = s.currStream } log.Info("Embed stream added", "event", resp, "vtuber", v.Name) } } else { var content YoutubeWidget err = s.client.StateEvent(room, event.NewEventType("im.vector.modular.widgets"), "dimension-m.video-simp-"+v.Name, &content) if err == nil && content.ID != "" { // event found, kill it resp, err := s.client.SendStateEvent(room, event.NewEventType("im.vector.modular.widgets"), "dimension-m.video-simp-"+v.Name, struct{}{}) if err != nil { log.Errorf("error removing embed: %v", err) } s.currStream-- log.Info("Embed stream removed", "event", resp, "vtuber", v.Name) } } } } } } } func (s *simp) SetupMatrix(uname, pass, token, hs, domain, dserver string) error { uid := id.NewUserID(strings.ToLower(uname), strings.ToLower(domain)) if token == "" { client, err := mautrix.NewClient(hs, "", "") if err != nil { return err } s.client = client } else { log.Info("using token login") client, err := mautrix.NewClient(hs, uid, token) if err != nil { return err } s.client = client } dataFilter := &mautrix.Filter{ AccountData: mautrix.FilterPart{ Limit: 20, NotTypes: []event.Type{ event.NewEventType("s.batch"), }, }, } store := mautrix.NewAccountDataStore("simp.batch", s.client) fID, err := s.client.CreateFilter(dataFilter) if err != nil { return err } store.SaveFilterID(uid, fID.FilterID) s.client.Store = store if token == "" { loginRes, err := s.client.Login(&mautrix.ReqLogin{ Type: "m.login.password", Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: uname}, Password: pass, StoreCredentials: true, }) if err != nil { return err } token = loginRes.AccessToken viper.Set("access_token", token) log.Info("Login succesful, saving access_token to config file") err = viper.WriteConfig() if err != nil { return err } } else { log.Info("skipping login since token provided") } syncer := s.client.Syncer.(*mautrix.DefaultSyncer) syncer.OnEventType(event.EventMessage, func(source mautrix.EventSource, evt *event.Event) { if evt.Sender == s.client.UserID { return // ignore events from self } log.Debugf("<%[1]s> %[4]s (%[2]s/%[3]s)\n", evt.Sender, evt.Type.String(), evt.ID, evt.Content.AsMessage().Body) body := evt.Content.AsMessage().Body bodyS := strings.Split(body, " ") if bodyS[0] != "!simp" { return } if len(bodyS) < 2 { return // nothing to parse } switch bodyS[1] { case "info": // print info page var infomsg string vlist := []string{} for _, vt := range s.vtubers { ann := "" if vt.AnnounceLive { ann = "*" } vlist = append(vlist, fmt.Sprintf("%v%v", vt.Name, ann)) } infomsg = fmt.Sprintf("Currently Simping For: \n%v", strings.Join(vlist, "\n")) s.client.SendText(evt.RoomID, infomsg) case "stats": var statmsg string vlist := []string{} t := 0 for _, vt := range s.vtubers { vlist = append(vlist, fmt.Sprintf("%v Total:%v", vt.Name, vt.TotalStreams)) t = t + vt.TotalStreams } statmsg = fmt.Sprintf( "Current Stats Since %v:\n%v\n\nTotal Streams: %v\nMost Concurrent: %v/%v\n", s.startTime, strings.Join(vlist, "\n"), t, s.maxStream, len(s.vtubers), ) s.client.SendText(evt.RoomID, statmsg) case "version": s.client.SendText(evt.RoomID, "not implemented") case "reload": // reload config s.client.SendText(evt.RoomID, "Reloading config") log.Info("Reload requested,reloading vtubers") s.vtubers = loadVtubers() case "subscribe": if len(bodyS) < 3 { s.client.SendText(evt.RoomID, "Need a member to subscribe to") } vt := bodyS[2] var subbed bool for _, v := range s.vtubers { if strings.ToUpper(v.Name) == strings.ToUpper(vt) { v.Subs[evt.Sender] = true subbed = true } } if subbed { s.client.SendText(evt.RoomID, "subbed") } else { s.client.SendText(evt.RoomID, "could not identify talent to subscribe to") } case "help": s.client.SendText(evt.RoomID, "Supported commands: info,version,stats,reload,subscribe") default: // command not found s.client.SendText(evt.RoomID, "command not recognized") } }) syncer.OnEventType(event.StateMember, func(source mautrix.EventSource, evt *event.Event) { log.Infof("<%[1]s> %[4]s (%[2]s/%[3]s)\n", evt.Sender, evt.Type.String(), evt.ID, evt.Content.AsMessage().Body) if evt.Content.AsMember().Membership.IsInviteOrJoin() { _, err = s.client.JoinRoomByID(evt.RoomID) if err != nil { log.Errorf("error joining room %v", evt.RoomID) } else { log.Infof("joined room %v", evt.RoomID) } } }) s.dimensionServer = dserver return nil } func (s *simp) Stop() { s.stop <- true s.client.StopSync() } func (s *simp) NewYT(videoName, videoID, roomID string) *YoutubeWidget { encodedVod := url.QueryEscape("https://youtube.com/embed/" + videoID) return &YoutubeWidget{ Type: "im.vector.modular.widgets", URL: "https://" + s.dimensionServer + "/widgets/video?url=" + encodedVod, Name: videoName, Data: VideoData{ VideoURL: "https://www.youtube.com/watch?v=" + videoID, URL: "https://youtube.com/embed/" + videoID, DimensionAppMetadata: DimensionAppMetadata{ InRoomID: roomID, WrapperURLBase: "https://" + s.dimensionServer + "/widgets/video?url=", WrapperID: "video", ScalarWrapperID: "youtube", Integration: Integration{ Category: "widget", Type: "youtube", }, LastUpdatedTs: time.Now().UnixNano() / int64(time.Millisecond), }, }, CreatorUserID: string(s.client.UserID), ID: "dimension-m.video-simp", } }