<commit_before>package twitch

import (
	"bufio"
	"database/sql"
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"strings"
	"sync"
	"time"

	"github.com/dankeroni/gotwitch"
	twitch "github.com/gempir/go-twitch-irc"
	"github.com/go-sql-driver/mysql"
	"github.com/pajlada/pajbot2/pkg"
	"github.com/pajlada/pajbot2/pkg/channels"
	"github.com/pajlada/pajbot2/pkg/common"
	"github.com/pajlada/pajbot2/pkg/users"
	"github.com/pajlada/pajbot2/pkg/utils"
	"golang.org/x/oauth2"
)

type ModeState int

var _ pkg.Sender = &Bot{}

const (
	ModeUnset = iota
	ModeEnabled
	ModeDisabled
)

type botFlags struct {
	PermaSubMode bool
}

type BotCredentials struct {
	AccessToken  string
	RefreshToken string
	Expiry       mysql.NullTime
}

// Bot is a wrapper around go-twitch-irc's twitch.Client with a few extra features
type Bot struct {
	*twitch.Client

	app pkg.Application

	TokenSource oauth2.TokenSource

	DatabaseID int

	twitchAccount *User

	QuitChannel chan string

	Flags botFlags

	// Filled in with user IDs when bot is loaded, from SQL
	channelsMutex *sync.Mutex
	channels      []*BotChannel

	// TODO: Store one point server per channel the bot is in. share between bots
	pointServer *PointServer

	ticker *time.Ticker

	userStore   pkg.UserStore
	userContext pkg.UserContext
	streamStore pkg.StreamStore

	pubSub pkg.PubSub

	sql *sql.DB

	IsConnected bool

	onNewChannelJoined func(channelID string)
}

var _ pkg.PubSubConnection = &Bot{}
var _ pkg.PubSubSource = &Bot{}

func NewBot(databaseID int, twitchAccount pkg.TwitchAccount, tokenSource oauth2.TokenSource, app pkg.Application) (*Bot, error) {
	token, err := tokenSource.Token()
	if err != nil {
		return nil, err
	}
	// TODO(pajlada): share user store between twitch bots
	// TODO(pajlada): mutex lock user store
	b := &Bot{
		app: app,

		TokenSource: tokenSource,
		Client:      twitch.NewClient(twitchAccount.Name(), "oauth:"+token.AccessToken),

		DatabaseID: databaseID,

		twitchAccount: &User{
			name: twitchAccount.Name(),
		},

		channelsMutex: &sync.Mutex{},

		userStore:   app.UserStore(),
		userContext: app.UserContext(),
		streamStore: app.StreamStore(),

		pubSub: app.PubSub(),

		sql: app.SQL(),

		QuitChannel: app.QuitChannel(),
	}

	b.pubSub.Subscribe(b, "Ban")
	b.pubSub.Subscribe(b, "Timeout")
	b.pubSub.Subscribe(b, "Untimeout")

	b.twitchAccount.fillIn(b.userStore)

	return b, nil
}

func (b *Bot) GetTokenSource() oauth2.TokenSource {
	return b.TokenSource
}

func (b *Bot) OnNewChannelJoined(cb func(channelID string)) {
	b.onNewChannelJoined = cb
}

func (b *Bot) IsApplication() bool {
	return true
}

func (b *Bot) Connection() pkg.PubSubConnection {
	return b
}

func (b *Bot) FetchDatabaseID() int {
	return b.DatabaseID
}

func (b *Bot) AuthenticatedUser() pkg.User {
	return nil
}

func (b *Bot) addBotChannel(botChannel *BotChannel) error {
	b.channelsMutex.Lock()
	b.channels = append(b.channels, botChannel)
	b.channelsMutex.Unlock()

	// Initialize bot channel
	return botChannel.Initialize(b)
}

func (b *Bot) getBotChannel(channelID string) (int, *BotChannel) {
	b.channelsMutex.Lock()
	defer b.channelsMutex.Unlock()

	for i, botChannel := range b.channels {
		if botChannel.Channel().GetID() == channelID {
			return i, botChannel
		}
	}

	return -1, nil
}

func (b *Bot) ChannelIDs() (channelIDs []string) {
	b.channelsMutex.Lock()
	defer b.channelsMutex.Unlock()

	for _, botChannel := range b.channels {
		channelIDs = append(channelIDs, botChannel.Channel().GetID())
	}

	return
}

func (b *Bot) InChannel(channelID string) bool {
	b.channelsMutex.Lock()
	defer b.channelsMutex.Unlock()

	for _, botChannel := range b.channels {
		if botChannel.Channel().GetID() == channelID {
			return true
		}
	}

	return false
}

// channelsMutex needs to be locked before calling this function
func (b *Bot) removeBotChannelAtIndex(index int) {
	b.channels = append(b.channels[:index], b.channels[index+1:]...)
}

func (b *Bot) LoadChannels(sql *sql.DB) error {
	const queryF = `SELECT id, twitch_channel_id FROM BotChannel WHERE bot_id=?`

	rows, err := sql.Query(queryF, b.DatabaseID)
	if err != nil {
		return err
	}
	defer rows.Close()

	var channelIDs []string
	var channels []*BotChannel

	for rows.Next() {
		var botChannel BotChannel

		if err = rows.Scan(&botChannel.ID, &botChannel.channel.id); err != nil {
			return err
		}

		channelIDs = append(channelIDs, botChannel.Channel().GetID())
		channels = append(channels, &botChannel)
	}

	m := b.userStore.GetNames(channelIDs)

	for id, name := range m {
		for _, c := range channels {
			if c.Channel().GetID() == id {
				c.channel.SetName(name)
			}
		}
	}

	for _, c := range channels {
		if c.channel.Valid() {
			if err = b.addBotChannel(c); err != nil {
				return nil
			}
		}
	}

	return nil
}

func (b *Bot) Join(channelName string) {
	b.Client.Join(channelName)
	channelID := b.userStore.GetID(channelName)
	if channelID == "" {
		fmt.Println("[pajbot2:pkg/twitch/bot.go] Unable to get ID of channel we just tride to join")
		return
	}

	if b.onNewChannelJoined != nil {
		b.onNewChannelJoined(channelID)
	}
}

func (b *Bot) JoinChannels() {
	b.channelsMutex.Lock()
	defer b.channelsMutex.Unlock()

	for _, c := range b.channels {
		b.Join(c.Channel().GetName())
	}
}

func (b *Bot) GetUserStore() pkg.UserStore {
	return b.userStore
}

func (b *Bot) GetUserContext() pkg.UserContext {
	return b.userContext
}

type emoteReader struct {
	index int

	emotes *[]*common.Emote

	started bool
}

func newEmoteHolder(emotes *[]*common.Emote) *emoteReader {
	return &emoteReader{
		index:  0,
		emotes: emotes,
	}
}

func (h *emoteReader) Next() bool {
	if !h.started {
		h.started = true

		if len(*h.emotes) == 0 {
			return false
		}

		return true
	}

	h.index++

	if h.index >= len(*h.emotes) {
		return false
	}

	return true
}

func (h *emoteReader) Get() pkg.Emote {
	return (*h.emotes)[h.index]
}

// TwitchMessage is a wrapper for twitch.Message with some extra stuff
type TwitchMessage struct {
	twitch.Message

	twitchEmotes      []*common.Emote
	twitchEmoteReader *emoteReader

	bttvEmotes      []*common.Emote
	bttvEmoteReader *emoteReader
	// TODO: BTTV Emotes

	// TODO: FFZ Emotes

	// TODO: Emojis
}

func NewTwitchMessage(message twitch.Message) *TwitchMessage {
	msg := &TwitchMessage{
		Message: message,
	}
	msg.twitchEmoteReader = newEmoteHolder(&msg.twitchEmotes)
	msg.bttvEmoteReader = newEmoteHolder(&msg.bttvEmotes)

	return msg
}

func (m TwitchMessage) GetText() string {
	return m.Text
}

func (m TwitchMessage) GetTwitchReader() pkg.EmoteReader {
	return m.twitchEmoteReader
}

func (m TwitchMessage) GetBTTVReader() pkg.EmoteReader {
	return m.bttvEmoteReader
}

func (m *TwitchMessage) AddBTTVEmote(emote pkg.Emote) {
	m.bttvEmotes = append(m.bttvEmotes, emote.(*common.Emote))
}

// Reply will reply to the message in the same way it received the message
// If the message was received in a twitch channel, reply in that twitch channel.
// IF the message was received in a twitch whisper, reply using twitch whispers.
func (b *Bot) Reply(channel pkg.Channel, user pkg.User, message string) {
	if channel == nil {
		b.Whisper(user, message)
	} else {
		b.Say(channel, message)
	}
}

func (b *Bot) TwitchAccount() pkg.TwitchAccount {
	return b.twitchAccount
}

func (b *Bot) Connected() bool {
	return b.IsConnected
}

func (b *Bot) Say(channel pkg.Channel, message string) {
	b.Client.Say(channel.GetName(), message)
}

func (b *Bot) Mention(channel pkg.Channel, user pkg.User, message string) {
	b.Client.Say(channel.GetName(), "@"+user.GetName()+", "+message)
}

func (b *Bot) Whisper(user pkg.User, message string) {
	b.Client.Whisper(user.GetName(), message)
}

func (b *Bot) Timeout(channel pkg.Channel, user pkg.User, duration int, reason string) {
	if !user.IsModerator() {
		b.Say(channel, fmt.Sprintf(".timeout %s %d %s", user.GetName(), duration, reason))
	}
}

func (b *Bot) Ban(channel pkg.Channel, user pkg.User, reason string) {
	if !user.IsModerator() {
		b.Say(channel, fmt.Sprintf(".ban %s %s", user.GetName(), reason))
	}
}

func (b *Bot) Untimeout(channel pkg.Channel, user pkg.User) {<commit_msg>add delayed second timeout to timeout function<commit_after>package twitch

import (
	"bufio"
	"database/sql"
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"strings"
	"sync"
	"time"

	"github.com/dankeroni/gotwitch"
	twitch "github.com/gempir/go-twitch-irc"
	"github.com/go-sql-driver/mysql"
	"github.com/pajlada/pajbot2/pkg"
	"github.com/pajlada/pajbot2/pkg/channels"
	"github.com/pajlada/pajbot2/pkg/common"
	"github.com/pajlada/pajbot2/pkg/users"
	"github.com/pajlada/pajbot2/pkg/utils"
	"golang.org/x/oauth2"
)

type ModeState int

var _ pkg.Sender = &Bot{}

const (
	ModeUnset = iota
	ModeEnabled
	ModeDisabled
)

type botFlags struct {
	PermaSubMode bool
}

type BotCredentials struct {
	AccessToken  string
	RefreshToken string
	Expiry       mysql.NullTime
}

// Bot is a wrapper around go-twitch-irc's twitch.Client with a few extra features
type Bot struct {
	*twitch.Client

	app pkg.Application

	TokenSource oauth2.TokenSource

	DatabaseID int

	twitchAccount *User

	QuitChannel chan string

	Flags botFlags

	// Filled in with user IDs when bot is loaded, from SQL
	channelsMutex *sync.Mutex
	channels      []*BotChannel

	// TODO: Store one point server per channel the bot is in. share between bots
	pointServer *PointServer

	ticker *time.Ticker

	userStore   pkg.UserStore
	userContext pkg.UserContext
	streamStore pkg.StreamStore

	pubSub pkg.PubSub

	sql *sql.DB

	IsConnected bool

	onNewChannelJoined func(channelID string)
}

var _ pkg.PubSubConnection = &Bot{}
var _ pkg.PubSubSource = &Bot{}

func NewBot(databaseID int, twitchAccount pkg.TwitchAccount, tokenSource oauth2.TokenSource, app pkg.Application) (*Bot, error) {
	token, err := tokenSource.Token()
	if err != nil {
		return nil, err
	}
	// TODO(pajlada): share user store between twitch bots
	// TODO(pajlada): mutex lock user store
	b := &Bot{
		app: app,

		TokenSource: tokenSource,
		Client:      twitch.NewClient(twitchAccount.Name(), "oauth:"+token.AccessToken),

		DatabaseID: databaseID,

		twitchAccount: &User{
			name: twitchAccount.Name(),
		},

		channelsMutex: &sync.Mutex{},

		userStore:   app.UserStore(),
		userContext: app.UserContext(),
		streamStore: app.StreamStore(),

		pubSub: app.PubSub(),

		sql: app.SQL(),

		QuitChannel: app.QuitChannel(),
	}

	b.pubSub.Subscribe(b, "Ban")
	b.pubSub.Subscribe(b, "Timeout")
	b.pubSub.Subscribe(b, "Untimeout")

	b.twitchAccount.fillIn(b.userStore)

	return b, nil
}

func (b *Bot) GetTokenSource() oauth2.TokenSource {
	return b.TokenSource
}

func (b *Bot) OnNewChannelJoined(cb func(channelID string)) {
	b.onNewChannelJoined = cb
}

func (b *Bot) IsApplication() bool {
	return true
}

func (b *Bot) Connection() pkg.PubSubConnection {
	return b
}

func (b *Bot) FetchDatabaseID() int {
	return b.DatabaseID
}

func (b *Bot) AuthenticatedUser() pkg.User {
	return nil
}

func (b *Bot) addBotChannel(botChannel *BotChannel) error {
	b.channelsMutex.Lock()
	b.channels = append(b.channels, botChannel)
	b.channelsMutex.Unlock()

	// Initialize bot channel
	return botChannel.Initialize(b)
}

func (b *Bot) getBotChannel(channelID string) (int, *BotChannel) {
	b.channelsMutex.Lock()
	defer b.channelsMutex.Unlock()

	for i, botChannel := range b.channels {
		if botChannel.Channel().GetID() == channelID {
			return i, botChannel
		}
	}

	return -1, nil
}

func (b *Bot) ChannelIDs() (channelIDs []string) {
	b.channelsMutex.Lock()
	defer b.channelsMutex.Unlock()

	for _, botChannel := range b.channels {
		channelIDs = append(channelIDs, botChannel.Channel().GetID())
	}

	return
}

func (b *Bot) InChannel(channelID string) bool {
	b.channelsMutex.Lock()
	defer b.channelsMutex.Unlock()

	for _, botChannel := range b.channels {
		if botChannel.Channel().GetID() == channelID {
			return true
		}
	}

	return false
}

// channelsMutex needs to be locked before calling this function
func (b *Bot) removeBotChannelAtIndex(index int) {
	b.channels = append(b.channels[:index], b.channels[index+1:]...)
}

func (b *Bot) LoadChannels(sql *sql.DB) error {
	const queryF = `SELECT id, twitch_channel_id FROM BotChannel WHERE bot_id=?`

	rows, err := sql.Query(queryF, b.DatabaseID)
	if err != nil {
		return err
	}
	defer rows.Close()

	var channelIDs []string
	var channels []*BotChannel

	for rows.Next() {
		var botChannel BotChannel

		if err = rows.Scan(&botChannel.ID, &botChannel.channel.id); err != nil {
			return err
		}

		channelIDs = append(channelIDs, botChannel.Channel().GetID())
		channels = append(channels, &botChannel)
	}

	m := b.userStore.GetNames(channelIDs)

	for id, name := range m {
		for _, c := range channels {
			if c.Channel().GetID() == id {
				c.channel.SetName(name)
			}
		}
	}

	for _, c := range channels {
		if c.channel.Valid() {
			if err = b.addBotChannel(c); err != nil {
				return nil
			}
		}
	}

	return nil
}

func (b *Bot) Join(channelName string) {
	b.Client.Join(channelName)
	channelID := b.userStore.GetID(channelName)
	if channelID == "" {
		fmt.Println("[pajbot2:pkg/twitch/bot.go] Unable to get ID of channel we just tride to join")
		return
	}

	if b.onNewChannelJoined != nil {
		b.onNewChannelJoined(channelID)
	}
}

func (b *Bot) JoinChannels() {
	b.channelsMutex.Lock()
	defer b.channelsMutex.Unlock()

	for _, c := range b.channels {
		b.Join(c.Channel().GetName())
	}
}

func (b *Bot) GetUserStore() pkg.UserStore {
	return b.userStore
}

func (b *Bot) GetUserContext() pkg.UserContext {
	return b.userContext
}

type emoteReader struct {
	index int

	emotes *[]*common.Emote

	started bool
}

func newEmoteHolder(emotes *[]*common.Emote) *emoteReader {
	return &emoteReader{
		index:  0,
		emotes: emotes,
	}
}

func (h *emoteReader) Next() bool {
	if !h.started {
		h.started = true

		if len(*h.emotes) == 0 {
			return false
		}

		return true
	}

	h.index++

	if h.index >= len(*h.emotes) {
		return false
	}

	return true
}

func (h *emoteReader) Get() pkg.Emote {
	return (*h.emotes)[h.index]
}

// TwitchMessage is a wrapper for twitch.Message with some extra stuff
type TwitchMessage struct {
	twitch.Message

	twitchEmotes      []*common.Emote
	twitchEmoteReader *emoteReader

	bttvEmotes      []*common.Emote
	bttvEmoteReader *emoteReader
	// TODO: BTTV Emotes

	// TODO: FFZ Emotes

	// TODO: Emojis
}

func NewTwitchMessage(message twitch.Message) *TwitchMessage {
	msg := &TwitchMessage{
		Message: message,
	}
	msg.twitchEmoteReader = newEmoteHolder(&msg.twitchEmotes)
	msg.bttvEmoteReader = newEmoteHolder(&msg.bttvEmotes)

	return msg
}

func (m TwitchMessage) GetText() string {
	return m.Text
}

func (m TwitchMessage) GetTwitchReader() pkg.EmoteReader {
	return m.twitchEmoteReader
}

func (m TwitchMessage) GetBTTVReader() pkg.EmoteReader {
	return m.bttvEmoteReader
}

func (m *TwitchMessage) AddBTTVEmote(emote pkg.Emote) {
	m.bttvEmotes = append(m.bttvEmotes, emote.(*common.Emote))
}

// Reply will reply to the message in the same way it received the message
// If the message was received in a twitch channel, reply in that twitch channel.
// IF the message was received in a twitch whisper, reply using twitch whispers.
func (b *Bot) Reply(channel pkg.Channel, user pkg.User, message string) {
	if channel == nil {
		b.Whisper(user, message)
	} else {
		b.Say(channel, message)
	}
}

func (b *Bot) TwitchAccount() pkg.TwitchAccount {
	return b.twitchAccount
}

func (b *Bot) Connected() bool {
	return b.IsConnected
}

func (b *Bot) Say(channel pkg.Channel, message string) {
	b.Client.Say(channel.GetName(), message)
}

func (b *Bot) Mention(channel pkg.Channel, user pkg.User, message string) {
	b.Client.Say(channel.GetName(), "@"+user.GetName()+", "+message)
}

func (b *Bot) Whisper(user pkg.User, message string) {
	b.Client.Whisper(user.GetName(), message)
}

func (b *Bot) Timeout(channel pkg.Channel, user pkg.User, duration int, reason string) {
	if !user.IsModerator() {
		b.Say(channel, fmt.Sprintf(".timeout %s %d %s", user.GetName(), duration, reason))
		time.AfterFunc(1200*time.Millisecond, func() {
			b.Say(channel, fmt.Sprintf(".timeout %s %d %s", user.GetName(), duration, reason))
		})
	}
}

func (b *Bot) Ban(channel pkg.Channel, user pkg.User, reason string) {
	if !user.IsModerator() {
		b.Say(channel, fmt.Sprintf(".ban %s %s", user.GetName(), reason))
	}
}

func (b *Bot) Untimeout(channel pkg.Channel, user pkg.User) {
{'fraction_non_alphanumeric': 0.13075192997386176, 'fraction_numerical': 0.0019451705063521973, 'mean_word_length': 4.025045815516188, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5241017', 'n_tokens_mistral': 6330, 'n_tokens_neox': 5987, 'n_words': 1506}