Разработка твиттер ботнета на основе цепей Маркова

6 minute read

Перевод “Developing a Twitter botnet based on Markov chains in Go

Основная идея этой статьи - рассказать как написать твиттер ботнет с автономными ботами которые смогут отвечать на другие твиты текстом сгенерированным с помощью алгоритма цепей Маркова. Так как это обучающий минипроект, то мы будем делать все сами и с самого нуля.

Идея совместить алгоритм цепей Маркова и твиттер ботов появилась после общения с x0rz.

Цепи Маркова

Цепь маркова это последовательность стохастических событий(основанных на вероятности) где текущее состояние переменной или системы не зависит только от предыдущего события и не зависит от всех остальных прошедших событий.

https://en.wikipedia.org/wiki/Markov_chain

В нашем случае мы будем использовать цепочки маркова для анализа вероятности что после некоторого слова идет другое определенное слово. Нам нужно будет сгенерировать граф, вроде того что на рисунке ниже, только с тысячами слов.

На вход нам нужно подавать документ с тысячами слов для более качественного результата. Для нашего примера мы будем использовать книгу Иммануила Канта “The Critique of Pure Reason”. Просто потому что это первая книга которая мне попалась в текстовом формате.

Расчет цепи

Прежде всего нам нужно прочитать файл

1func readTxt(path string) (string, error) {
2	data, err := ioutil.ReadFile(path)
3	if err != nil {
4		//выполняем необходимую работу
5	}
6	dataClean := strings.Replace(string(data), "\n", " ", -1)
7	content := string(dataClean)
8	return content, err
9}

Для вычисления вероятности состояний нам нужно написать функцию, которая будет на вход принимать текст, анализировать его и сохранять состояния Маркова.

 1func calcMarkovStates(words []string) []State {
 2	var states []State
 3	// считаем слова
 4	for i := 0; i < len(words)-1; i++ {
 5		var iState int
 6		states, iState = addWordToStates(states, words[i])
 7		if iState < len(words) {
 8			states[iState].NextStates, _ = addWordToStates(states[iState].NextStates, words[i+1])
 9		}
10
11		printLoading(i, len(words))
12	}
13
14	// считаем вероятность
15	for i := 0; i < len(states); i++ {
16		states[i].Prob = (float64(states[i].Count) / float64(len(words)) * 100)
17		for j := 0; j < len(states[i].NextStates); j++ {
18			states[i].NextStates[j].Prob = (float64(states[i].NextStates[j].Count) / float64(len(words)) * 100)
19		}
20	}
21	fmt.Println("\ntotal words computed: " + strconv.Itoa(len(words)))
22	return states
23}

Функция printLoading выводит в теримал прогресбар просто для удобства.

 1func printLoading(n int, total int) {
 2	var bar []string
 3	tantPerFourty := int((float64(n) / float64(total)) * 40)
 4	tantPerCent := int((float64(n) / float64(total)) * 100)
 5	for i := 0; i < tantPerFourty; i++ {
 6		bar = append(bar, "█")
 7	}
 8	progressBar := strings.Join(bar, "")
 9	fmt.Printf("\r " + progressBar + " - " + strconv.Itoa(tantPerCent) + "")
10}

И выглядит это вот так:

Генерация текста по цепи Маркова

Для генерации текста нам нужно первое слово и длина генерируемого текста. После этого запускается цикл в котором мы выбираем слова по вероятностям, рассчитанным при составлении цепи на прошлом шаге.

 1func (markov Markov) generateText(states []State, initWord string, count int) string {
 2	var generatedText []string
 3	word := initWord
 4	generatedText = append(generatedText, word)
 5	for i := 0; i < count; i++ {
 6		word = getNextMarkovState(states, word)
 7		if word == "word no exist on the memory" {
 8			return "word no exist on the memory"
 9		}
10		generatedText = append(generatedText, word)
11	}
12	text := strings.Join(generatedText, " ")
13	return text
14}

Для генерации нам нужна функция, котрая принимает на вход всю цепь и некоторое слово, а возвращает другое слово на основе вероятности:

 1func getNextMarkovState(states []State, word string) string {
 2	iState := -1
 3	for i := 0; i < len(states); i++ {
 4		if states[i].Word == word {
 5			iState = i
 6		}
 7	}
 8	if iState < 0 {
 9		return "word no exist on the memory"
10	}
11	var next State
12	next = states[iState].NextStates[0]
13	next.Prob = rand.Float64() * states[iState].Prob
14	for i := 0; i < len(states[iState].NextStates); i++ {
15		if (rand.Float64()*states[iState].NextStates[i].Prob) > next.Prob && states[iState-1].Word != states[iState].NextStates[i].Word {
16			next = states[iState].NextStates[i]
17		}
18	}
19	return next.Word
20}

Твиттер АПИ

Для работы с АПИ твиттера будем использовать пакет go-twitter

Нам нужно настроить стриминг соединение - мы будем фильтровать твиты по определенным словам, которые есть в нашем исходном наборе:

 1func startStreaming(states []State, flock Flock, flockUser *twitter.Client, botScreenName string, keywords []string) {
 2	// Convenience Demux demultiplexed stream messages
 3	demux := twitter.NewSwitchDemux()
 4	demux.Tweet = func(tweet *twitter.Tweet) {
 5		if isRT(tweet) == false && isFromBot(flock, tweet) == false {
 6			processTweet(states, flockUser, botScreenName, keywords, tweet)
 7		}
 8	}
 9	demux.DM = func(dm *twitter.DirectMessage) {
10		fmt.Println(dm.SenderID)
11	}
12	demux.Event = func(event *twitter.Event) {
13		fmt.Printf("%#v\n", event)
14	}
15
16	fmt.Println("Starting Stream...")
17	// фильтруем все что нам нужно
18	filterParams := &twitter.StreamFilterParams{
19		Track:         keywords,
20		StallWarnings: twitter.Bool(true),
21	}
22	stream, err := flockUser.Streams.Filter(filterParams)
23	if err != nil {
24		log.Fatal(err)
25	}
26	//  получаем сообщения пока стрим не будет остановлен
27	demux.HandleChan(stream.Messages)
28}

Теперь когда нам будет попадаться твит с искомыми словами, то будет срабатывать функция processTweet в которой генерируется ответ с помощью алгоритма, описанного выше:

 1func processTweet(states []State, flockUser *twitter.Client, botScreenName string, keywords []string, tweet *twitter.Tweet) {
 2	c.Yellow("bot @" + botScreenName + " - New tweet detected:")
 3	fmt.Println(tweet.Text)
 4
 5	tweetWords := strings.Split(tweet.Text, " ")
 6	generatedText := "word no exist on the memory"
 7	for i := 0; i < len(tweetWords) && generatedText == "word no exist on the memory"; i++ {
 8		fmt.Println(strconv.Itoa(i) + " - " + tweetWords[i])
 9		generatedText = generateMarkovResponse(states, tweetWords[i])
10	}
11	c.Yellow("bot @" + botScreenName + " posting response")
12	fmt.Println(tweet.ID)
13	replyTweet(flockUser, "@"+tweet.User.ScreenName+" "+generatedText, tweet.ID)
14	waitTime(1)
15}

И постим твит с помощью replyTweet:

 1func replyTweet(client *twitter.Client, text string, inReplyToStatusID int64) {
 2	tweet, httpResp, err := client.Statuses.Update(text, &twitter.StatusUpdateParams{
 3		InReplyToStatusID: inReplyToStatusID,
 4	})
 5	if err != nil {
 6		fmt.Println(err)
 7	}
 8	if httpResp.Status != "200 OK" {
 9		c.Red("error: " + httpResp.Status)
10		c.Purple("maybe twitter has blocked the account, CTRL+C, wait 15 minutes and try again")
11	}
12	fmt.Print("tweet posted: ")
13	c.Green(tweet.Text)
14}

Стадный ботнет или как избежать ограничение твиттер АПИ

Если вы когда ни будь пользовались твиттер АПИ, то наверняка в курсе что есть целый ряд ограничений и лимитов. Это означает, что если ваш бот будет делать лишком много запросов, то его будут периодически блокировать на некоторое время.

Чтобы избежать этого мы будем использовать целую сеть ботов. Когда в стриме появится твит с нужным словом, один из ботов ответит на него и “уйдет в ждущий режим” на минуту, а обработкой следующих сообщений займутся другие боты. И так по кругу.

Собираем все вместе

В нашем примере используются всего 3 бота. Это значит нам нужно три отдельных аккаунта. Ключи для этих аккаунтов вынесем в отдельный JSON файл который будем использовать как конфиг для нашего приложения.

 1[
 2    {
 3        "title": "bot1",
 4        "consumer_key": "xxxxxxxxxxxxx",
 5        "consumer_secret": "xxxxxxxxxxxxx",
 6        "access_token_key": "xxxxxxxxxxxxx",
 7        "access_token_secret": "xxxxxxxxxxxxx"
 8    },
 9    {
10        "title": "bot2",
11        "consumer_key": "xxxxxxxxxxxxx",
12        "consumer_secret": "xxxxxxxxxxxxx",
13        "access_token_key": "xxxxxxxxxxxxx",
14        "access_token_secret": "xxxxxxxxxxxxx"
15    },
16    {
17        "title": "bot3",
18        "consumer_key": "xxxxxxxxxxxxx",
19        "consumer_secret": "xxxxxxxxxxxxx",
20        "access_token_key": "xxxxxxxxxxxxx",
21        "access_token_secret": "xxxxxxxxxxxxx"
22    }
23]

Демо

Мы настроили небольшую версию нашего ботнета с тремя ботами. Как уже говорилось, в качестве входных данных для генерации цепи Маркова мы использовали книгу “The Critique of Pure Reason”.

Когда ботнет запускается то все боты подключаются к стримингу и ждут когда появатся твиты с необходимыми ключевыми словами.

Каждый бот получает один из твитов, обрабатывает его и отправляет ответ с использованием цепи Маркова.

В терминале это выглядит вот так:

И вот так выглядит все в процессе:

Ниже примеры твиттов, сгенерированные нашей цепью Маркова

Заключение

У нас получилось создать небольшой ботнет на основе алгоритма цепи Маркова, который может генерировать ответы на твиты.

Мы использовали только 1 класс цепей маркова и сгенерированный текст не очень поход на настоящий человеческий. Но этого вполне достаточно для начала и в будущем можно будет использовать различные классы цепей маркова и другие техники для генерации более человеческого текста.

Твиттер АПИ может использоваться для самых различных целей. Надеюсь в будущем я смогу написать на эту тему еще несколько статей, например про анализ нод или пользователей и хештегов.

Весь код проекта можно найти на гихабе https://github.com/arnaucode/flock-botnet.

Страница проекта: http://arnaucode.com/flock-botnet/