|
|
@@ -0,0 +1,211 @@ |
|
|
+# -*- coding: utf-8 -*-
|
|
|
+from string import ascii_letters, digits
|
|
|
+from warnings import filterwarnings
|
|
|
+from random import randint, choice
|
|
|
+from pprint import pprint
|
|
|
+from time import sleep
|
|
|
+import webbrowser
|
|
|
+import argparse
|
|
|
+import requests
|
|
|
+import json
|
|
|
+import sys
|
|
|
+import os
|
|
|
+import re
|
|
|
+
|
|
|
+py3 = sys.version_info[0] > 2
|
|
|
+filterwarnings('ignore')
|
|
|
+
|
|
|
+
|
|
|
+def chunks(l, n):
|
|
|
+ """Yield successive n-sized chunks from l."""
|
|
|
+ for i in range(0, len(l), n):
|
|
|
+ yield l[i:i+n]
|
|
|
+# https://stackoverflow.com/questions/312443/how-do-you-split-a-list-into-evenly-sized-chunks-in-python/312464#312464
|
|
|
+
|
|
|
+
|
|
|
+def natural_sort(l):
|
|
|
+ convert = lambda text: int(text) if text.isdigit() else text.lower()
|
|
|
+ alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
|
|
|
+ return sorted(l, key=alphanum_key)
|
|
|
+# https://stackoverflow.com/questions/4836710/does-python-have-a-built-in-function-for-string-natural-sort/4836734#4836734
|
|
|
+
|
|
|
+
|
|
|
+def has_numbers(input_string):
|
|
|
+ return any(char.isdigit() for char in input_string)
|
|
|
+# https://stackoverflow.com/questions/19859282/check-if-a-string-contains-a-number/19859308#19859308
|
|
|
+
|
|
|
+
|
|
|
+def is_installed(module):
|
|
|
+ try:
|
|
|
+ __import__(module)
|
|
|
+ except ImportError:
|
|
|
+ return False
|
|
|
+ return True
|
|
|
+
|
|
|
+
|
|
|
+def fetch_captcha():
|
|
|
+ r = requests.get('https://8ch.net/8chan-captcha/entrypoint.php?mode=get&extra=abcdefghijklmnopqrstuvwxyz&nojs=true')
|
|
|
+ try:
|
|
|
+ match = 'CAPTCHA ID: [a-z]*'
|
|
|
+ captcha_cookie = re.search(match, r.text).group().split(': ')[1]
|
|
|
+ match = '<image src="data:image/png;base64,[A-Za-z0-9+/=]*">'
|
|
|
+ with open('captcha_image.png', 'wb') as f:
|
|
|
+ f.write(re.search(match, r.text).group().split(',')[1].strip('>').strip('"').decode('base64'))
|
|
|
+ webbrowser.open('captcha_image.png')
|
|
|
+ if py3:
|
|
|
+ captcha_text = input('Enter the captcha: ')
|
|
|
+ else:
|
|
|
+ captcha_text = raw_input('Enter the captcha: ')
|
|
|
+ os.remove('captcha_image.png')
|
|
|
+ data['captcha_cookie'] = captcha_cookie
|
|
|
+ data['captcha_text'] = captcha_text
|
|
|
+ except AttributeError:
|
|
|
+ print('Server error while fetching captcha')
|
|
|
+ pass
|
|
|
+
|
|
|
+
|
|
|
+parser = argparse.ArgumentParser(description='Dump a folder to a thread')
|
|
|
+parser.add_argument('-f', '--folder', help='Folder to upload images from', type=str, required=True)
|
|
|
+parser.add_argument('-u', '--url', help='URL of thread', required=True)
|
|
|
+parser.add_argument('-d', '--delay', help='Delay between posting', type=int, required=True)
|
|
|
+parser.add_argument('-i', '--images', help='Number of images to post', type=int)
|
|
|
+parser.add_argument('-n', '--name', help='Name to post with', type=str, default='')
|
|
|
+parser.add_argument('-e', '--email', help='Email to post with', type=str, default='')
|
|
|
+parser.add_argument('-p', '--progress', help='Post with dump progress? (default y)', choices=('y', 'n'), default='y')
|
|
|
+parser.add_argument('-r', '--random', help='Post with random filenames? (default n)', choices=('y', 'n'), default='n')
|
|
|
+parser.add_argument('-l', '--length', help='To be used with -r/--random. Maximum length of random filename (optional, default 15)', type=int, default=15)
|
|
|
+args = vars(parser.parse_args())
|
|
|
+# https://stackoverflow.com/questions/9234258/in-python-argparse-is-it-possible-to-have-paired-no-something-something-arg
|
|
|
+
|
|
|
+extensions = ['.jpg', '.png', '.gif']
|
|
|
+def make_random_filename():
|
|
|
+ return ''.join(choice(ascii_letters + digits) for _ in range(randint(1, args['length']))) + choice(extensions)
|
|
|
+
|
|
|
+
|
|
|
+delay = args['delay']
|
|
|
+DIRECTORY = args['folder']
|
|
|
+filelist = []
|
|
|
+for file in os.listdir(DIRECTORY):
|
|
|
+ if os.path.splitext(file)[1].lower() in ('.jpg', '.jpeg', '.png', '.gif', '.webm', '.mp4'):
|
|
|
+ filelist.append(os.path.join(DIRECTORY, file))
|
|
|
+# I stole this from stackoverflow somewhere, I can't find the link
|
|
|
+
|
|
|
+if len(args['url'].split('/')) == 6:
|
|
|
+ spliturl = args['url'].split('/')
|
|
|
+ if has_numbers(spliturl[5]) and '.html' in args['url']:
|
|
|
+ url = '{}//{}'.format(spliturl[0], spliturl[2])
|
|
|
+ board = spliturl[3]
|
|
|
+ thread = spliturl[5].split('.')[0]
|
|
|
+ else:
|
|
|
+ sys.exit('\nBad URL. --u/--url should be the URL of a thread.\nExample: https://8ch.net/b/res/1.html')
|
|
|
+else:
|
|
|
+ sys.exit('\nBad URL. --u/--url should be the URL of a thread.\nExample: https://8ch.net/b/res/1.html')
|
|
|
+boardindex = requests.get('{}/{}/index.html'.format(url, board))
|
|
|
+
|
|
|
+if args['images'] == None:
|
|
|
+ if is_installed('bs4') == True:
|
|
|
+ from bs4 import BeautifulSoup
|
|
|
+ soup = BeautifulSoup(boardindex.text)
|
|
|
+ script_tag = str(soup.find_all('script')[1])
|
|
|
+ print('-i/--images not specified. Defaulting to the maximum number of allowed images on this board.')
|
|
|
+ maxfiles = int(re.search('var max_images=[1-5]', script_tag).group(0).split('=')[1])
|
|
|
+ elif is_installed('bs4') == False:
|
|
|
+ sys.exit('Unable to automatically detect the maximum number of images per post, please specify -i/--images')
|
|
|
+else:
|
|
|
+ if args['images'] >= 1 and args['images'] <= 5:
|
|
|
+ maxfiles = args['images']
|
|
|
+ else:
|
|
|
+ sys.exit('-i/--images must be between 1 and 5')
|
|
|
+
|
|
|
+filelist = natural_sort(filelist)
|
|
|
+total = len(filelist)
|
|
|
+chunked_list = list(chunks(filelist, maxfiles))
|
|
|
+
|
|
|
+r = requests.get('{}/settings.php?board={}'.format(url, board))
|
|
|
+try:
|
|
|
+ j = json.loads(r.text)
|
|
|
+ if j['max_newlines'] != 0:
|
|
|
+ max_newlines = j['max_newlines']
|
|
|
+ else:
|
|
|
+ max_newlines = 100
|
|
|
+except ValueError:
|
|
|
+ sys.exit('Server error while getting board settings')
|
|
|
+
|
|
|
+data = {
|
|
|
+ 'board': board,
|
|
|
+ 'thread': thread,
|
|
|
+ 'name': args['name'],
|
|
|
+ 'email': args['email'],
|
|
|
+ 'subject': '',
|
|
|
+ 'body': '',
|
|
|
+ 'embed': '',
|
|
|
+ 'dx': '',
|
|
|
+ 'dy': '',
|
|
|
+ 'dz': '',
|
|
|
+ 'password': 'ayylmao',
|
|
|
+ 'json_response': '1',
|
|
|
+ 'post': 'New Reply',
|
|
|
+}
|
|
|
+
|
|
|
+headers = {
|
|
|
+ 'referer': '',
|
|
|
+ 'user-agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0',
|
|
|
+ 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
|
+ 'accept-language': 'en-US,en;q=0.5',
|
|
|
+ 'cache-control': 'max-age=0',
|
|
|
+}
|
|
|
+
|
|
|
+headers['referer'] = '{}/{}/'.format(url, board)
|
|
|
+
|
|
|
+progress = maxfiles
|
|
|
+for chunk in range(len(chunked_list)):
|
|
|
+ number_of_files = len(chunked_list[chunk])
|
|
|
+ status = ''
|
|
|
+ while 'redirect' not in status:
|
|
|
+ for x in range(len(chunked_list[chunk])):
|
|
|
+ l = natural_sort(chunked_list[chunk])
|
|
|
+ files = []
|
|
|
+ if args['random'] == 'y':
|
|
|
+ for file in range(len(l)):
|
|
|
+ if file == 0:
|
|
|
+ files.append(['file', (make_random_filename(), open(l[file], 'rb'))])
|
|
|
+ else:
|
|
|
+ files.append(['file' + str(file), (make_random_filename(), open(l[file], 'rb'))])
|
|
|
+ else:
|
|
|
+ for file in range(len(l)):
|
|
|
+ if file == 0:
|
|
|
+ files.append(['file', open(l[file], 'rb')])
|
|
|
+ else:
|
|
|
+ files.append(['file' + str(file), open(l[file], 'rb')])
|
|
|
+ files = tuple(files)
|
|
|
+ newlines = '\n' * randint(1, max_newlines)
|
|
|
+ if progress > total:
|
|
|
+ progress = total
|
|
|
+ if args['progress'] == 'y':
|
|
|
+ data['body'] = '{}/{}{}'.format(progress, total, newlines)
|
|
|
+ else:
|
|
|
+ data['body'] = newlines
|
|
|
+ if j['captcha']['enabled'] == True:
|
|
|
+ fetch_captcha()
|
|
|
+ r = requests.post('{}/post.php'.format(url), data=data, headers=headers, files=files)
|
|
|
+ status_code = r.status_code
|
|
|
+ status = r.text
|
|
|
+ if status_code == 200:
|
|
|
+ print(status)
|
|
|
+ else:
|
|
|
+ print('{} server error'.format(status_code))
|
|
|
+ if 'Failed to resize image' and 'corrupt image' in status:
|
|
|
+ print('ONE OF THE FOLLOWING IMAGES ARE CORRUPT, AND AS A RESULT, THE FOLLOWING FILES WERE NOT UPLOADED:')
|
|
|
+ pprint(l)
|
|
|
+ status = 'redirect'
|
|
|
+ if 'already exists' in status:
|
|
|
+ status = 'redirect'
|
|
|
+ if 'Maximum file size' in status:
|
|
|
+ print('THE FOLLOWING FILE(S) WERE OVER THE FILE SIZE LIMIT AND WERE NOT POSTED:')
|
|
|
+ pprint(l)
|
|
|
+ print('Consider setting -i/--images to a lower number so your files fit under the limit')
|
|
|
+ status = 'redirect'
|
|
|
+ del files
|
|
|
+ print('{}/{} - Waiting {} seconds...').format(progress, total, delay)
|
|
|
+ sleep(delay)
|
|
|
+ progress += number_of_files
|