Guest User

autogartic.pyw

a guest
Feb 14th, 2022
81
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 21.32 KB | None | 0 0
  1. '''
  2. Бот для автопостинга картинок и текста в гартик
  3. v0.1
  4.  
  5. FAQ:
  6. Q: как запустить бота?
  7. A: 1) устанавливаете python версии не ниже 3.6
  8.   2) устанавливаете библиотеки через консоль командой: python -m pip install websocket_client requests pillow
  9.   3) запускаете этот файл (если не работает через даблклик, то через консоль командой: python autogartic.pyw)
  10.   4) вставляете в бота ссылку на игру, никнейм/куки, путь к файлу с фразами и к папке с картинками, нажимаете "подключиться". Бот сам наченет постить при старте игры
  11.  
  12. Q: что за файл с фразами?
  13. A: это строки, которые будут использоваться как предложения в начале игры и при описании рисунков. Они вставляются в игру в случайном порядке, как и картинки из папки
  14.  
  15. Q: куда сохраняются настройки?
  16. A: в файл autogartic_settings.json в той же папке, где находится бот
  17.  
  18. Q: ссылка не вставляется через ctrl-V
  19. A: переключи раскладку на английскую
  20.  
  21. Q: где взять куки для логина через твич?
  22. A: 1) в браузере открываете сетевой лог (в firefox F12, в хроме и прочих ctrl-shift-J)
  23.   2) создаете свое лобби в гартике с авторизацией через твич
  24.   3) в сетевых запросах ищете запрос на домен id.twitch.tv
  25.   4) в запросе ищете куки с названием persistent, должно быть что-то типа 701234671::safnk4jtn3nbgieru656hosd7, копируете эту строчку в бота
  26. '''
  27.  
  28. from PIL import Image
  29. import websocket
  30. import requests
  31.  
  32. import tkinter as tk
  33. from tkinter import RIGHT, ttk
  34. import tkinter.scrolledtext as scrolledtext
  35. from tkinter import filedialog
  36. import random
  37. import time
  38. import threading
  39. import json
  40. import re
  41. import datetime
  42. import os
  43.  
  44. # global vars
  45. logBox = None
  46. imagesList = None
  47. phrasesList = None
  48. playerUuid = None
  49.  
  50. def log(s):
  51.     t = datetime.datetime.now().strftime("%H:%M:%S")
  52.     logBox.insert(tk.END, f'[{t}] {s}\n')
  53.     logBox.see(tk.END)
  54.  
  55. def getRandomUuid():
  56.     s = []
  57.     for i in range(36):
  58.         s.append(random.choice('af'))
  59.     s[8] = '-'
  60.     s[13] = '-'
  61.     s[18] = '-'
  62.     s[23] = '-'
  63.     return ''.join(s)
  64.  
  65. class ImagesList:
  66.     def __init__(self, dir) -> None:
  67.         if not os.path.isdir(dir):
  68.             raise Exception('указана неверная папка с картинками')
  69.         self.filenames = [os.path.join(dir, fname) for fname in os.listdir(dir) \
  70.             if (fname.lower().endswith('.png') or fname.lower().endswith('.jpg'))]
  71.         if len(self.filenames) < 1:
  72.             raise Exception('в указанной папке нет картинок')
  73.         random.shuffle(self.filenames)
  74.         self.imageNum = 0
  75.  
  76.     def getNext(self):
  77.         fname = self.filenames[self.imageNum]
  78.         self.imageNum += 1
  79.         if self.imageNum == len(self.filenames):
  80.             self.imageNum = 0
  81.         return fname
  82.  
  83. class PhrasesList:
  84.     def __init__(self, path) -> None:
  85.         if not os.path.isfile(path):
  86.             raise Exception('указан неверный файл с фразами')
  87.         with open(path, encoding='utf8') as f:
  88.             self.phrases = f.read().split('\n')
  89.         random.shuffle(self.phrases)
  90.         self.phraseNum = 0
  91.  
  92.     def getNext(self):
  93.         phrase = self.phrases[self.phraseNum]
  94.         self.phraseNum += 1
  95.         if self.phraseNum == len(self.phrases):
  96.             self.phraseNum = 0
  97.         return phrase
  98.  
  99. def rgb2hex(r, g, b):
  100.     return '#{:02x}{:02x}{:02x}'.format(r, g, b)
  101.  
  102. def imgToMoves(turnNum, shakalValue, filename):
  103.     colorsDict = {}
  104.     img = Image.open(filename)
  105.     img = img.resize((758, 424), Image.ANTIALIAS)
  106.     for x in range(img.width):
  107.         for y in range(img.height):
  108.             if shakalValue > 1 and (x % shakalValue != 0 or y % shakalValue != 0):
  109.                 continue
  110.             pixel = img.getpixel((x,y))
  111.             colorHex = rgb2hex(pixel[0], pixel[1], pixel[2])
  112.             if not colorsDict.get(colorHex):
  113.                 colorsDict[colorHex] = f'[8,-1,["{colorHex}",1],{x},{y},{shakalValue},{shakalValue}'
  114.             else:
  115.                 colorsDict[colorHex] += f',{x},{y},{shakalValue},{shakalValue}'
  116.     img.close()
  117.  
  118.     movesArray = [colorsDict[i] for i in colorsDict]
  119.     random.shuffle(movesArray)
  120.  
  121.     moves = ['42[2,7,{"t":'+str(turnNum)+',"d":1,"v":'+move+']}]' for move in movesArray]
  122.     return moves
  123.  
  124. class WebsocketClient(threading.Thread):
  125.     def __init__(self, svServerHost, sid, userId) -> None:
  126.         threading.Thread.__init__(self)
  127.         self.isCloseRequest = False
  128.         self.svServerHost = svServerHost
  129.         self.sid = sid
  130.         self.userId = userId
  131.  
  132.     def on_message(self, ws, message):
  133.         if type(message) != str or message == '3' or message == '3probe':
  134.             return
  135.  
  136.         if message.startswith('42[2,5'):
  137.             log('Игра началась')
  138.         elif message.startswith('42[2,11'):
  139.             turnNum = json.loads(message[2:])[2]['turnNum']
  140.             screen = json.loads(message[2:])[2]['screen']
  141.             if screen == 3 or screen == 4:
  142.                 phrase = phrasesList.getNext()
  143.                 log(f'Ход {turnNum}, отправляем фразу "{phrase}"')
  144.                 ws.send('42[2,6,{"t":'+str(turnNum)+',"v":"'+phrase+'"}]')
  145.             elif screen == 5:
  146.                 try:
  147.                     filename = imagesList.getNext()
  148.                     log(f'Ход {turnNum}, отправляем картинку "{os.path.basename(filename)}"')
  149.                     for shakalValue in [4,2]:
  150.                         movesArray = imgToMoves(turnNum, shakalValue, filename)
  151.                         for move in movesArray:
  152.                             ws.send(move)
  153.                 except Exception as e:
  154.                     log(f'Ошибка при выгрузке картинки: {e}')
  155.             ws.send('42[2,15,true]') # сообщаем о готовности
  156.         elif message.startswith('42[2,15'):
  157.             pass # статус готовности других игроков
  158.         elif message.startswith('42[2,24'):
  159.             log('Игра закончена, игроки смотрят альбомы')
  160.         elif message.startswith('42[2,23') or message.startswith('42[2,12') or message.startswith('42[2,9'):
  161.             pass # просмотр ходов в альбоме
  162.         elif message.startswith('42[2,20]'):
  163.             log('Альбомы просмотрены, выходим в лобби')
  164.         elif message.startswith('42[2,14'):
  165.             id = json.loads(message[2:])[2]
  166.             if id != self.userId:
  167.                 log(f'Игрок с id {id} был забанен')
  168.             else:
  169.                 log(f'Бот был забанен, отключаемся и создаем новый ID')
  170.                 global playerUuid
  171.                 playerUuid = getRandomUuid()
  172.                 self.isCloseRequest = True
  173.         else:
  174.             # log(f'Неизвестный тип сообщения: {message}')
  175.             pass
  176.  
  177.     def on_error(self, ws, error):
  178.         log(f'WebSocket error: {error}')
  179.  
  180.     def on_close(self, ws, close_status_code, close_msg):
  181.         self.isCloseRequest = True
  182.         log(f'WebSocket closed: status: {close_status_code}, msg: {close_msg}')
  183.  
  184.     def on_open(self, ws):
  185.         log('WebSocket opened, starting new thread')
  186.         def pingFunc():
  187.             ws.send("2probe")
  188.             time.sleep(3)
  189.             ws.send("5")
  190.             self.onConnected()
  191.             time.sleep(3)
  192.             while True:
  193.                 time.sleep(3)
  194.                 if self.isCloseRequest:
  195.                    break
  196.                 ws.send("2")
  197.             ws.close()
  198.         self.pingThread = threading.Thread(target=pingFunc)
  199.         self.pingThread.start()
  200.  
  201.     def setHandlers(self, onConnected, onDisconnected):
  202.         self.onConnected = onConnected
  203.         self.onDisconnected = onDisconnected
  204.  
  205.     def stop(self):
  206.         self.isCloseRequest = True
  207.  
  208.     def run(self):
  209.         ws = websocket.WebSocketApp(f"wss://{self.svServerHost}/socket.io/?EIO=3&transport=websocket&sid={self.sid}",
  210.                                 on_open=self.on_open,
  211.                                 on_message=self.on_message,
  212.                                 on_error=self.on_error,
  213.                                 on_close=self.on_close,
  214.                                 cookie=f'io={self.sid}')
  215.         ws.run_forever()
  216.         self.pingThread.join()
  217.         log('WebSocket threads terminated')
  218.         self.onDisconnected()
  219.  
  220. # method: 'anon' or 'twitch'
  221. def authentication(inviteLink, method, twitchCookie = None, nickname = None):
  222.     global playerUuid
  223.  
  224.     if method == 'anon' and not nickname:
  225.         raise Exception('пустой ник')
  226.     elif method == 'twitch' and not twitchCookie:
  227.         raise Exception('отсутствуют cookie')
  228.     elif method != 'twitch' and method != 'anon':
  229.         raise Exception('неизвестный метод аутентификации')
  230.  
  231.     avatarNum = random.choice([31,32,33,40,16,39])
  232.     inviteCode = re.search('.*/\?c=(.*)', inviteLink)
  233.     if not inviteCode:
  234.         raise Exception('не удалось найти код приглашения в ссылке')
  235.     inviteCode = inviteCode.groups()[0]
  236.  
  237.     garticSession = requests.Session()
  238.  
  239.     if method == 'twitch':
  240.         r = garticSession.get(f'https://garticphone.com/api/auth/providers')
  241.  
  242.         # 1) get csrf
  243.         r = garticSession.get(f'https://garticphone.com/api/auth/csrf')
  244.         csrfToken = json.loads(r.content.decode('utf8'))['csrfToken']
  245.  
  246.         # 2) get twitch auth url
  247.         r = garticSession.post('https://garticphone.com/api/auth/signin/twitch',
  248.             data={
  249.                 'callbackUrl': f'https://garticphone.com/ru/redirecting?avatar={avatarNum}&c={inviteCode}',
  250.                 'csrfToken': csrfToken,
  251.                 'json': 'true'
  252.             }
  253.         )
  254.         twitchAuthUrl = json.loads(r.content.decode('utf8'))['url']
  255.  
  256.         # 3) twitch auth
  257.         r = requests.get(twitchAuthUrl, cookies={
  258.             'persistent':twitchCookie,
  259.         })
  260.  
  261.         content = r.content.decode('utf8')
  262.         nextUrl = re.search('.*URL=\'(.*)\'" />.*', content)
  263.         if not nextUrl:
  264.             raise Exception(f'не удалось пройти аутентификацию через твич')
  265.         nextUrl = nextUrl.groups()[0]
  266.         nextUrl = nextUrl.replace('&amp;', '&')
  267.         nextUrl = nextUrl.replace('%3A', ':')
  268.  
  269.         # 4) get session token from gartic
  270.         garticSession.get(nextUrl)
  271.  
  272.     # get server
  273.     r = garticSession.get(f'https://garticphone.com/api/server?code={inviteCode}')
  274.     svServerHost = r.content.decode('utf8')
  275.  
  276.     # get sid
  277.     r = garticSession.get(f'{svServerHost}/socket.io/?EIO=3&transport=polling&t=1')
  278.     content = r.content.decode('utf8')
  279.     sid = re.search('.*sid":"(.*)","upgrades.*', content)
  280.     if not sid:
  281.         raise Exception(f'не удалось найти sid: {content}')
  282.     sid = sid.groups()[0]
  283.  
  284.     # register in lobby
  285.     if method == 'twitch':
  286.         r = garticSession.get('https://garticphone.com/api/auth/session')
  287.         twitchJwt = json.loads(r.content.decode('utf8'))['code']
  288.  
  289.         data = f'42[1,"{playerUuid}",null,"{avatarNum}","ru",false,"{inviteCode}","{twitchJwt}",null]'
  290.         r = garticSession.post(f'{svServerHost}/socket.io/?EIO=3&transport=polling&t=2&sid={sid}',
  291.             data=f'{len(data)}:{data}'.encode('utf-8'),
  292.             cookies={'io':sid})
  293.     else:
  294.         data = f'42[1,"{playerUuid}","{nickname}","{avatarNum}","ru",false,"{inviteCode}",null,null]'
  295.         r = requests.post(f'{svServerHost}/socket.io/?EIO=3&transport=polling&t=2&sid={sid}',
  296.             data=f'{len(data)}:{data}'.encode('utf-8'))
  297.         if r.status_code != 200:
  298.             raise Exception(f'anon register status_code={r.status_code}')
  299.  
  300.     # get lobby object
  301.     r = requests.get(f'{svServerHost}/socket.io/?EIO=3&transport=polling&t=3&sid={sid}',
  302.         cookies={'io':sid})
  303.     if r.status_code != 200:
  304.         raise Exception(f'get lobby obj status_code={r.status_code}')
  305.     s = r.content.decode('utf8')
  306.     lobbyObj = json.loads(s[s.find('['):])
  307.     garticError = lobbyObj[1].get('error')
  308.     if garticError == 1:
  309.         raise Exception('некорректный ник')
  310.     elif garticError == 2:
  311.         playerUuid = getRandomUuid()
  312.         if method == 'anon':
  313.             raise Exception('вы забанены в этом лобби. Новый ID создан, переподключитесь')
  314.         else:
  315.             raise Exception('вы забанены в этом лобби. Используйте другой аккаунт')
  316.     elif garticError == 3:
  317.         raise Exception('лобби не существует')
  318.     elif garticError == 4:
  319.         raise Exception('в лобби уже максимальное число игроков')
  320.     elif garticError == 6:
  321.         raise Exception('вы уже находитесь в этом лобби')
  322.     elif garticError == 8:
  323.         raise Exception('cookie expired')
  324.     elif garticError == 9:
  325.         raise Exception('неверный метод аутентификации')
  326.     elif garticError:
  327.         raise Exception(f'ошибка при входе в лобби: {garticError}')
  328.  
  329.     garticUser = lobbyObj[1].get('user')
  330.     if not garticUser:
  331.         raise Exception(f'неизвестная ошибка при входе в лобби. Данные лобби: {s}')
  332.  
  333.     return WebsocketClient(svServerHost[8:], sid, garticUser['id'])
  334.  
  335. def start():
  336.     global logBox
  337.     global playerUuid
  338.     playerUuid = getRandomUuid()
  339.  
  340.     window = tk.Tk()
  341.     window.title('Autogartic v0.1 (by 2ch anon)')
  342.     window.geometry('700x800')
  343.  
  344.     logBox = scrolledtext.ScrolledText(window)
  345.  
  346.     ttk.Label(window, text='Ссылка на приглашение в игру:').pack(padx=5, pady=5)
  347.     inviteLinkEntry = ttk.Entry(window, width=70)
  348.     inviteLinkEntry.pack(padx=5, pady=5)
  349.  
  350.     AUTH_METHOD_ANON_STR = 'Анонимно'
  351.     AUTH_METHOD_TWITCH_STR = 'Через twitch'
  352.     ttk.Label(window, text='Метод аутентификации:').pack(padx=5, pady=5)
  353.     authMethodStr = tk.StringVar()
  354.     authMethodCombobox = ttk.Combobox(window, textvariable=authMethodStr)
  355.     authMethodCombobox['values'] = [AUTH_METHOD_ANON_STR, AUTH_METHOD_TWITCH_STR]
  356.     authMethodCombobox['state'] = 'readonly'
  357.     authMethodCombobox.pack(padx=5, pady=5)
  358.  
  359.     authFrame = ttk.Frame(window)
  360.     authFrame.pack()
  361.  
  362.     nickLabel = ttk.Label(authFrame, text='Ник:')
  363.     nickEntry = ttk.Entry(authFrame, width=30)
  364.  
  365.     twitchCookieLabel = ttk.Label(authFrame, text='Cookie (см. readme):')
  366.     twitchCookieEntry = ttk.Entry(authFrame, width=50)
  367.  
  368.     def authMethodChanged(event):
  369.         if authMethodStr.get() == AUTH_METHOD_ANON_STR:
  370.             nickLabel.pack(padx=5, pady=1, side=tk.LEFT)
  371.             nickEntry.pack(padx=5, pady=1, side=tk.LEFT)
  372.             twitchCookieLabel.pack_forget()
  373.             twitchCookieEntry.pack_forget()
  374.         elif authMethodStr.get() == AUTH_METHOD_TWITCH_STR:
  375.             twitchCookieLabel.pack(padx=5, pady=1, side=tk.LEFT)
  376.             twitchCookieEntry.pack(padx=5, pady=1, side=tk.LEFT)
  377.             nickLabel.pack_forget()
  378.             nickEntry.pack_forget()
  379.  
  380.     authMethodCombobox.bind('<<ComboboxSelected>>', authMethodChanged)
  381.  
  382.     ttk.Label(window, text='Путь к файлу с фразами:').pack(padx=5, pady=1)
  383.     phrasesPathEntry = ttk.Entry(window, width=70)
  384.     phrasesPathEntry.pack(padx=5, pady=1)
  385.     def onChoosePhrases(event=None):
  386.         filePathSelected = filedialog.askopenfilename()
  387.         phrasesPathEntry.delete(0, tk.END)
  388.         phrasesPathEntry.insert(0, filePathSelected)
  389.     choosePhrasesButton = ttk.Button(window, text="Открыть...", command=onChoosePhrases)
  390.     choosePhrasesButton.pack(padx=5, pady=5)
  391.  
  392.     ttk.Label(window, text='Путь к папке с картинками:').pack(padx=5, pady=1)
  393.     imagesDirEntry = ttk.Entry(window, width=70)
  394.     imagesDirEntry.pack(padx=5, pady=1)
  395.     def onChooseImages(event=None):
  396.         dirPathSelected = filedialog.askdirectory()
  397.         imagesDirEntry.delete(0, tk.END)
  398.         imagesDirEntry.insert(0, dirPathSelected)
  399.     chooseImagesButton = ttk.Button(window, text="Открыть...", command=onChooseImages)
  400.     chooseImagesButton.pack(padx=5, pady=5)
  401.  
  402.     startButton = None
  403.     isConnected = False
  404.     websocketClient = None
  405.  
  406.     def onConnected():
  407.         nonlocal isConnected
  408.         log('Подключено!')
  409.         startButton.config(text='Отключиться')
  410.         startButton["state"] = "enabled"
  411.         isConnected = True
  412.  
  413.     def onDisconnected():
  414.         nonlocal isConnected
  415.         log('Отключено!')
  416.         startButton.config(text='Подключиться')
  417.         startButton["state"] = "enabled"
  418.         isConnected = False
  419.  
  420.     def onConnectClick(event=None):
  421.         nonlocal isConnected
  422.         nonlocal websocketClient
  423.         inviteLink = inviteLinkEntry.get()
  424.         startButton["state"] = "disabled"
  425.         if not isConnected:
  426.             try:
  427.                 global phrasesList
  428.                 phrasesList = PhrasesList(phrasesPathEntry.get())
  429.                 global imagesList
  430.                 imagesList = ImagesList(imagesDirEntry.get())
  431.  
  432.                 if authMethodStr.get() == AUTH_METHOD_ANON_STR:
  433.                     websocketClient = authentication(inviteLink, 'anon', nickname=nickEntry.get())
  434.                 elif authMethodStr.get() == AUTH_METHOD_TWITCH_STR:
  435.                     websocketClient = authentication(inviteLink, 'twitch', twitchCookie=twitchCookieEntry.get())
  436.                 else:
  437.                     raise Exception('не выбран метод аутентификации')
  438.                 websocketClient.setHandlers(onConnected, onDisconnected)
  439.                 websocketClient.start()
  440.             except Exception as e:
  441.                 log(f'Ошибка при подключении: {e}')
  442.                 startButton.config(text='Подключиться')
  443.                 startButton["state"] = "enabled"
  444.         else:
  445.             if websocketClient:
  446.                 websocketClient.stop()
  447.             else:
  448.                 startButton.config(text='Подключиться')
  449.                 startButton["state"] = "enabled"
  450.                 isConnected = False
  451.  
  452.     def onSaveSettingsClick(event=None):
  453.         try:
  454.             with open('autogartic_settings.json', 'w', encoding='utf8') as f:
  455.                 settings = {
  456.                     'inviteLink':inviteLinkEntry.get(),
  457.                     'authMethod':authMethodStr.get(),
  458.                     'nick':nickEntry.get(),
  459.                     'twitchCookie':twitchCookieEntry.get(),
  460.                     'phrasesPath':phrasesPathEntry.get(),
  461.                     'imagesDir':imagesDirEntry.get(),
  462.                 }
  463.                 json.dump(settings, f)
  464.                 log('Настройки сохранены')
  465.         except Exception as e:
  466.             log(f'Ошибка при сохранении настроек: {e}')
  467.  
  468.     saveSettingsButton = ttk.Button(window, text="Сохранить настройки", command=onSaveSettingsClick)
  469.     saveSettingsButton.pack(padx=5, pady=(15, 5))
  470.  
  471.     startButton = ttk.Button(window, text="Подключиться", command=onConnectClick)
  472.     startButton.pack(padx=5, pady=5)
  473.  
  474.     ttk.Label(window, text='Лог:').pack()
  475.     logBox.pack(padx=5, pady=5)
  476.  
  477.     settings = {
  478.         'inviteLink':'',
  479.         'authMethod':AUTH_METHOD_ANON_STR,
  480.         'nick':'',
  481.         'twitchCookie':'',
  482.         'phrasesPath':'',
  483.         'imagesDir':'',
  484.     }
  485.     try:
  486.         with open('autogartic_settings.json', 'r', encoding='utf8') as f:
  487.             loadedSettings = json.load(f)
  488.             for k in settings.keys():
  489.                 settings[k] = loadedSettings[k]
  490.             log('Настройки загружены')
  491.     except FileNotFoundError:
  492.         pass
  493.     except Exception as e:
  494.         log(f'Ошибка при загрузке настроек: {e}')
  495.  
  496.     # set loaded settings
  497.     inviteLinkEntry.delete(0, tk.END)
  498.     inviteLinkEntry.insert(0, settings['inviteLink'])
  499.     authMethodCombobox.set(settings['authMethod'])
  500.     authMethodChanged(None)
  501.     nickEntry.delete(0, tk.END)
  502.     nickEntry.insert(0, settings['nick'])
  503.     twitchCookieEntry.delete(0, tk.END)
  504.     twitchCookieEntry.insert(0, settings['twitchCookie'])
  505.     phrasesPathEntry.delete(0, tk.END)
  506.     phrasesPathEntry.insert(0, settings['phrasesPath'])
  507.     imagesDirEntry.delete(0, tk.END)
  508.     imagesDirEntry.insert(0, settings['imagesDir'])
  509.  
  510.     window.mainloop()
  511.  
  512. start()
Advertisement
Add Comment
Please, Sign In to add comment