def main(argv): def __call__(self): pass dlog = DebugLog("debug.log") dlog.msg("Logging started\n") stdscr = curses.initscr() curses.noecho() curses.cbreak() stdscr.keypad(1) curses.start_color() try: wl = WindowLogic(stdscr) wl.start() ci = CommandInterpreter(stdscr, wl) ci.start() except Exception as e: dlog.excpt(e) raise ci.join() dlog.msg("Command Interpreter joined.\n") wl.stop() wl.join() dlog.msg("Thread Fetcher joined.\n") curses.nocbreak() stdscr.keypad(0) curses.echo() curses.endwin() curses.resetty() dlog.msg("Terminal restored.\n")
class WindowLogic(threading.Thread): ''' classdocs ''' def __init__(self, stdscr): self.stdscr = stdscr self.dlog = DebugLog() try: self.windowList = [] self.compad = CommandPad(stdscr) self.windowList.append(self.compad) self.set_active_window(0) self.compad.draw() # board = "int" # threadno = "50294416" self.nickname = "asdfasd" # self.bp = BoardPad(stdscr) # self.bp.join(board, threadno, self.nickname) # self.windowList.append(self.bp) # self.set_active_window(1) Thread.__init__(self) self._stop = threading.Event() except: raise def join_thread(self, board, thread): try: self.dlog.msg("Creating new boardpad for " + thread + " on /" + board + "/\n") boardpad = BoardPad(self.stdscr) boardpad.join(board, thread, self.nickname) self.windowList.append(boardpad) self.set_active_window(1) except Exception, err: self.dlog.excpt(err)
def run(self): dlog = DebugLog() dlog.msg("ThreadFetcher: Running on /" + self.board + "/" + self.threadno + "\n", 3) try: dictOutput = DictOutput(self.bp) getThread = Autism(self.threadno, self.board) except Exception as e: dlog.excpt(e) self.stdscr.addstr(0, 0, str(e), curses.A_REVERSE) self.stdscr.refresh() self.tb.draw() while True: dlog.msg("ThreadFetcher: Fetching for /" + self.board + "/" + self.threadno + "\n", 3) if self._stop.is_set(): dlog.msg("ThreadFetcher: Stop signal for /" + self.board + "/" + self.threadno + "\n", 3) break try: getThread.setstdscr(self.stdscr) getThread.get() thread = getattr(getThread, "threadjson") dictOutput.refresh(thread) self.tb.set_title(dictOutput.getTitle()) except Exception as e: self.sb.setStatus(str(e)) dlog.excpt(e) pass for update_n in range (9, -1, -1): if self._stop.is_set(): break try: if self._active: self.sb.draw(update_n) except Exception as e: dlog.excpt(e) pass time.sleep(1)
def refresh(self, json): self.thread = json try: debug = DebugLog("debug.log") except: raise for posts in self.thread['posts']: try: # skip if record found in dictionary no = posts['no'] # Post is OP try: if posts['resto'] is 0: self.tdict['OP'] = {'no': posts['no'], 'sub': posts['sub'].encode('utf-8'), 'semantic_url': posts['semantic_url'].encode('utf-8')} except Exception as e: debug.msg("Exception:" + e.msg() + "\n") raise if no in self.tdict: continue name = posts['name'] time = datetime.datetime.fromtimestamp(posts['time']).strftime('%H:%M') except: continue curses.use_default_colors() for i in range(0, curses.COLORS): # @UndefinedVariable curses.init_pair(i + 1, i, -1) # assign color to post number color = randint(2, 255) try: country = posts['country'] except: country = "" try: com = posts['com'] com = re.sub('<br>', ' ', com) refposts = "" refposts = re.findall('>>(\d+)', com) com = re.sub('>>(\d+)', '\g<1> ', com) com = re.sub(''', '\'', com) com = re.sub('>', '>', com) com = re.sub('<', '<', com) com = re.sub('"', '"', com) com = re.sub('<[^<]+?>', '', com) except: com = "[File only]" try: trip = posts['trip'] except: trip = "" self.tdict[no] = {'country':country, 'name':name, 'time':time, 'com':com, 'trip':trip, 'color':color} # try: # line = u' '.join((time, ">>" + str(no), country, "<" + name + ">", com)).encode('utf-8') # except: # raise try: self.bp.addstr("", curses.color_pair(color)) self.bp.addstr(time) self.bp.addstr(" >>" + str(no), curses.color_pair(color)) self.bp.addstr(" " + country) if re.match(self.nickname, name) is not None: self.bp.addstr(" <" + self.nickname + "> ", curses.A_BOLD) else: self.bp.addstr(" <" + name.encode('utf8') + "> ") #self.bp.addstr(com.encode('utf8')) comlist = com.split() try: for word in comlist: if word not in refposts: self.bp.addstr(u''.join((word + " ")).encode('utf8')) else: # Comment and reference color encoding try: refcolor = self.tdict[int(word)]['color'] self.bp.addstr(">>" + word + " ", curses.color_pair(refcolor)) # if reference points to nickname, higligt the name if re.match(self.tdict[int(word)]['name'], self.nickname): self.bp.addstr("(You) ", curses.A_BOLD | curses.color_pair(221)) Notifier.send(com) # if re.match(word, threadno): # self.bp.addstr("(OP) ", curses.A_BOLD | curses.color_pair(197)) except: self.bp.addstr(word) except: self.bp.addstr("[File only]") except: raise self.bp.addstr("\n") try: self.title = self.tdict['OP']['sub'] except Exception as e: debug.msg("Couldn't set title" + str(e) + "\n") pass
class PostReply(object): ''' classdocs ''' def __init__(self, board, threadno): self.board = board self.threadno = threadno self.sitekey = "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc" self.captcha2_url = "https://www.google.com/recaptcha/api/fallback?k=" + self.sitekey self.captcha2_payload_url = "https://www.google.com/recaptcha/api2/payload" self.captcha2_image_base_url = "" self.site_ref = "https://boards.4chan.org/" self.user_agent = 'Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0' self.captcha2_challenge_text = None # "Select all images with ducks." self.captcha2_challenge_id = None self.captcha2_image = "" # Binary image self.captcha2_image_filename = "yottu-captcha.jpg" # TODO don't save the image self.captcha2_solution = None # Array of integers associated to the captcha checkboxes, usually 0-8 self.captcha2_response = None # Response the Reply post form wants (the actual solution) self.lock = thread.allocate_lock() self.dictOutput = None self.bp = None self.dlog = DebugLog() class PostError(Exception): def __init__(self, *args, **kwargs): Exception.__init__(self, *args, **kwargs) # def get_captcha_solution(self): # return self.__captcha_solution def set_captcha2_solution(self, value): # Append checkbox integer values to response array try: self.captcha2_solution = [] for i in str(value): self.captcha2_solution.append(str(int(i) - 1)) except ValueError as err: self.dlog.excpt(err, msg=">>>in PostReply.set_captcha2_solution()", cn=self.__class__.__name__) raise except Exception as err: self.dlog.excpt(err, msg=">>>in PostReply.set_captcha2_solution()", cn=self.__class__.__name__) def _query(self): pass def get_captcha_challenge(self): """ 1. Query captcha url with site key 2. From the result get a) the challenge text b) the challenge id 3. Query payload url with site key and cid and get c) the captcha image """ try: headers = {'Referer': self.site_ref, 'User-Agent': self.user_agent} r = requests.get(self.captcha2_url, headers=headers) r.raise_for_status() html_content = r.content soup = BeautifulSoup(html_content, 'html.parser') try: self.captcha2_challenge_text = soup.find( "div", { 'class': 'rc-imageselect-desc-no-canonical' }).text except: self.captcha2_challenge_text = soup.find( "div", { 'class': 'rc-imageselect-desc' }).text self.captcha2_challenge_id = soup.find( "div", { 'class': 'fbc-imageselect-challenge' }).find('input')['value'] # Get captcha image headers = { 'Referer': self.captcha2_url, 'User-Agent': self.user_agent } r = requests.get(self.captcha2_payload_url + '?c=' + self.captcha2_challenge_id + '&k=' + self.sitekey, headers=headers) self.captcha2_image = r.content #self.save_image(self.captcha2_image_filename) except Exception as err: self.dlog.excpt(err, msg=">>>in PostReply.get_captcha_challenge()", cn=self.__class__.__name__) raise def save_image(self, filename): """save image to file system""" try: with open(filename, "w") as f: f.write(self.captcha2_image) except Exception as err: self.dlog.excpt(err, msg=">>>in PostReply.save_image()", cn=self.__class__.__name__) def display_captcha(self): # Reformat picture to be displayed horizontally try: TermImage.image_split_h(StringIO(self.captcha2_image), self.captcha2_image_filename) except Exception as err: self.dlog.warn(err, msg=">>>in PostReply.display_captcha()", cn=self.__class__.__name__) # Overlay the captcha in the terminal try: TermImage.display(self.captcha2_image_filename) return True except: pass # On failure fall back to using the external image viewer try: TermImage.display_img(self.captcha2_image_filename) return False except: raise # FIXME captchav2 update def defer(self, time_wait, **kwargs): ''' wait for timer to run out before posting ''' if not self.bp.cfg.get('user.pass.enabled'): self.get_response() captcha2_response = self.captcha2_response self.dlog.msg("Waiting C: " + captcha2_response[:12] + str(kwargs), 4) self.bp.sb.setStatus("Deferring comment: " + str(time_wait) + "s") self.lock.acquire() self.dlog.msg("Lock acquired for deferred post " + str(kwargs)) try: while time_wait > 0: time.sleep(time_wait) # get new lastpost value and see if post needs to be deferred further time_wait = self.bp.time_last_posted_thread + 60 - int( time.time()) if not self.bp.cfg.get('user.pass.enabled'): kwargs.update(dict(captcha2_response=captcha2_response)) self.dlog.msg( "Now posting: C: " + captcha2_response[:12] + str(kwargs), 4) else: self.dlog.msg("Now posting deferred comment", 4) rc = self.post(**kwargs) if rc != 200: self.bp.sb.setStatus("Deferred comment was not posted: " + str(rc)) except Exception as err: self.bp.sb.setStatus("Deferred: " + str(err)) finally: self.lock.release() def get_response(self): try: headers = { 'Referer': self.captcha2_url, 'User-Agent': self.user_agent } data = { 'c': self.captcha2_challenge_id, 'response': self.captcha2_solution } r = requests.post(self.captcha2_url, headers=headers, data=data) html_post = r.content soup = BeautifulSoup(html_post, 'html.parser') except Exception as err: self.dlog.excpt(err, msg=">>>in PostReply.get_response()", cn=self.__class__.__name__) raise try: self.captcha2_response = soup.find( "div", { 'class': 'fbc-verification-token' }).text except AttributeError as err: self.dlog.warn( err, msg= "Could not get verification token, captcha input error (input: " + str(self.captcha2_solution) + ")?", cn=self.__class__.__name__) raise except Exception as err: self.dlog.excpt(err, msg=">>>in PostReply.get_response()", cn=self.__class__.__name__) def auth(self): # Set user.pass.cookie by posting the auth form containing user.pass.token and user.pass.pin try: token = self.bp.cfg.get('user.pass.token') pin = self.bp.cfg.get('user.pass.pin') if not token: token = self.bp.query_userinput(label="Token: ", input_type='text') if not pin: pin = self.bp.query_userinput(label="PIN: ", input_type='number') self.dlog.msg( "Authenticating Pass with PIN/Token length: " + str(len(pin)) + "/" + str(len(token)), 3) data = dict(act='do_login', id=token, pin=pin) res = requests.post('https://sys.4chan.org/auth', data=data) self.bp.cfg.set('user.pass.cookie', res.cookies['pass_id']) if self.bp.cfg.get('config.autosave'): self.dlog.msg("Autosaving user.pass.cookie ..") self.bp.cfg.writeConfig() except KeyError as err: self.dlog.excpt(err, msg=">>>in PostReply.auth()", cn=self.__class__.__name__) raise PostReply.PostError("Could not authenticate pass token/pin.") except Exception as err: self.dlog.excpt(err, msg=">>>in PostReply.auth()", cn=self.__class__.__name__) def post(self, nickname="", comment="", subject="", file_attach="", ranger=False, captcha2_response=""): ''' subject: not implemented file_attach: (/path/to/file.ext) will be uploaded as "file" + extension ranger: extract path from ranger's --choosefile file ''' cookies = None try: if self.bp.cfg.get('user.pass.enabled'): if not self.bp.cfg.get('user.pass.cookie'): # get and set new cookie from pass and pin self.auth() cookies = dict(pass_id=self.bp.cfg.get('user.pass.cookie'), pass_enabled="1") elif not captcha2_response: self.get_response() captcha2_response = self.captcha2_response if nickname == None: nickname = "" else: nickname = u''.join(nickname) # Read file / get mime type try: if file_attach: # extract file path from ranger file and re-assign it if ranger: with open(file_attach, "r") as f: file_attach = f.read() _, file_ext = os.path.splitext(file_attach) filename = "file" + file_ext content_type, _ = mimetypes.guess_type(filename) with open(file_attach, "rb") as f: filedata = f.read() if content_type is None: raise TypeError("Could not detect mime type of file " + str(filename)) else: filename = filedata = content_type = "" except Exception as err: self.dlog.excpt(err, msg=">>>in PostReply.post() -> file_attach", cn=self.__class__.__name__) raise url = "https://sys.4chan.org/" + self.board + "/post" #url = 'http://httpbin.org/status/404' #url = "http://localhost/" + self.board + "/post" #url = 'http://httpbin.org/post' #url = 'http://requestbin.fullcontact.com/1i28ed51' values = { 'MAX_FILE_SIZE': (None, '4194304'), 'mode': (None, 'regist'), # 'pwd' : ('', 'tefF92alij2j'), 'name': (None, nickname), # 'sub' : ('', ''), 'resto': (None, str(self.threadno)), # 'email' : ('', ''), 'com': (None, comment), 'g-recaptcha-response': (None, captcha2_response), 'upfile': (filename, filedata, content_type) } headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64)'} response = requests.post(url, headers=headers, files=values, cookies=cookies) # raise exception on error code response.raise_for_status() if re.search("is_error = \"true\"", response.text): perror = "Unknown Error." if self.bp.cfg.get('user.pass.cookie'): perror += " user.pass.cookie might be invalid." try: perror = re.search(r"Error: ([A-Za-z.,]\w*\s*)+", response.text).group(0) except: if re.search("blocked due to abuse", response.text): perror = "You are range banned ;_;" finally: raise PostReply.PostError(perror) if response.status_code == 200 and self.dictOutput: self.dictOutput.mark(comment) self.bp.post_success(int(time.time())) else: self.dlog.msg("response.status_code: " + str(response.status_code)) self.dlog.msg("self.dictOutput: " + str(self.dictOutput)) return response.status_code except Exception as err: self.dlog.excpt(err, msg=">>>in PostReply.post()", cn=self.__class__.__name__) raise
class CommandInterpreter(threading.Thread): def __init__(self, stdscr, wl): self.stdscr = stdscr self.wl = wl self.screensize_x, self.screensize_y = stdscr.getmaxyx(); Thread.__init__(self) self.cfg = Config.Config(".config/yottu/", "config") self.cmode = False # command mode self.tmode = False # talking mode (no need to prefix /say) self.clinepos = 4 self.command = "" self.context = "int" # context in which command is executed self.stdscr.addstr(self.screensize_x-1, 0, "[^] ") curses.curs_set(False) self.terminate = 0 self.dlog = DebugLog() def cout(self, c): self.stdscr.addstr(self.screensize_x-1, self.clinepos, c) self.command += c self.clinepos += 1 def clean(self): self.stdscr.addstr(self.screensize_x-1, 0, str(" "*(self.screensize_y-1))) self.command = "" self.clinepos = 4 def parse_param(self, string): ''' return list from whitespace separated string ''' ''' TODO: Implement validity matching logic ''' return string.split() def exec_com(self): self.dlog.msg("Trying to execute command: " + self.command + "\n") #cmd_arg = re.compile('\w+\W+(\w+)') cmd_args = self.command.split() if re.match("join", self.command): try: self.wl.join_thread(self.context, cmd_args[1]) self.wl.compadout("Joining /" + self.context + "/" + cmd_args[1]) except IndexError: self.wl.compadout("Usage: /join <thread number>") except: raise elif re.match("board", self.command): try: self.context = cmd_args[1] except IndexError: self.wl.compadout("Usage /board <board>") except: raise elif re.match("part", self.command): self.wl.destroy_active_window() elif re.match("load", self.command): self.cfg.readConfig() elif re.match("save", self.command): self.cfg.writeConfig() elif re.match("set", self.command): # Show settings if no args are given if len(cmd_args) is 1: configItems = self.cfg.getSettings() self.wl.compadout("[Main]") for pair in configItems: self.wl.compadout(pair[0] + ": " + pair[1]) # Else assign new value to setting else: key = cmd_args[1] val = ' '.join(cmd_args[2:]) try: self.cfg.set(key, val) except Exception as e: self.dlog.excpt(e) elif re.match("quit", self.command): self.terminate = 1 else: self.dlog.msg("Invalid command: " + self.command + "\n") def run(self): curses.mousemask(-1) while True: if self.terminate is 1: break self.stdscr.move(self.screensize_x-1, self.clinepos) if self.cmode: curses.curs_set(True) c = self.stdscr.getkey() if self.cmode: curses.curs_set(True) self.dlog.msg("getkey(): "+ c + "\n", 5) #c = self.stdscr.getch() if self.cmode: if c == "KEY_BACKSPACE": if self.clinepos > 4: self.command = self.command[:-1] self.clinepos = self.clinepos - 1 self.stdscr.addstr(self.screensize_x-1, self.clinepos, " ") self.stdscr.move(self.screensize_x-1, self.clinepos) continue try: if c == u'\n' or ord(c) == 27: if c == u'\n': self.exec_com() self.cmode = False self.clean() self.stdscr.addstr(self.screensize_x-1, 0, "[^] ") curses.curs_set(False) continue except Exception as e: self.dlog.excpt(e) pass try: self.cout(c) continue except: pass if c == u'q': break elif c == u'/' or c == u'i': self.stdscr.addstr(self.screensize_x-1, 0, "[/] ") self.cmode = True elif c == u't': self.stdscr.addstr(self.screensize_x-1, 0, "[>] ") self.tmode = True elif c == u'w': self.wl.moveup() elif c == u's': self.wl.movedown() elif c == u'1': try: self.wl.raise_window(0) except: raise elif c == u'2': try: self.wl.raise_window(1) except: raise elif c == u'3': try: self.wl.raise_window(2) except: raise elif c == u'n': try: self.wl.next() #TODO: implement in wl except: raise elif c == "KEY_MOUSE": mouse_state = curses.getmouse()[4] self.dlog.msg("getmouse(): "+ str(mouse_state) + "\n", 5) #self.stdscr.addstr(str(mouse_state)) if int(mouse_state) == 134217728: self.wl.movedown(5) elif int(mouse_state) == 524288: self.wl.moveup(5)
class WindowLogic(object): ''' classdocs ''' def __init__(self, stdscr): self.curses = curses self.stdscr = stdscr curses.use_default_colors() # @UndefinedVariable # assign color to post number, pairs 1-10 are reserved for i in range(0, curses.COLORS): # @UndefinedVariable curses.init_pair(i + 10, i, -1) # @UndefinedVariable # reserved color pairs curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_GREEN) # @UndefinedVariable curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_GREEN) # @UndefinedVariable curses.init_pair(3, curses.COLOR_RED, curses.COLOR_GREEN) # @UndefinedVariable curses.init_pair(4, curses.COLOR_RED, -1) # @UndefinedVariable self.dlog = DebugLog(self) try: self.cfg = Config() self.cfg.register(self) self.sb = None self.tw = None self.db = Database(self) self.tw = ThreadWatcher(self) self.windowList = [] # Array of all window objects (i.e. Pads) self.windowListProperties = { } # Associating a window object with its properties self.ci = None # Set by CommandInterpreter.__init__() self.compad = CommandPad(stdscr, self) self.msgpad = MessagePad(stdscr, self) self.append_pad(self.compad) self.append_pad(self.msgpad) self.set_active_window(0) self.nickname = "" # Thread.__init__(self) # self._stop = threading.Event() except Exception as err: self.dlog.excpt(err, msg=">>>in WindowLogic.__init__()", cn=self.__class__.__name__) raise def on_config_change(self, *args, **kwargs): self.cfg = Config() self.db.on_config_change(*args, **kwargs) self.dlog.msg("Config change detected") if self.cfg.get('threadwatcher.enable') and not self.tw: self.dlog.msg("Starting ThreadWatcher") self.tw = ThreadWatcher(self) def set_nickname(self, value): self.__nickname = value if self.__nickname: self.__nickname = value for window in self.windowList: window.set_nickname(self.get_nickname()) def get_nickname(self): return self.__nickname def get_window_list(self): return self.__windowList def get_property(self, window, prop): return self.windowListProperties[window][prop] def set_property(self, window, prop, value): self.windowListProperties[window][prop] = value def set_window_list(self, value): self.__windowList = value def append_pad(self, window): try: self.windowList.append(window) # Properties of a window instance, note: use deepcopy from copy if not assigning it directly self.windowListProperties[window] = { 'sb_unread': False, 'sb_lines': 0, 'sb_mentioned': False } # Let statusbar of window know what window number it has # TODO: This needs to be reset when a window gets destroyed or moved window.sb.set_sb_windowno(len(self.windowList)) except Exception as err: self.dlog.excpt(err, msg=">>>in WindowLogic.append_pad()", cn=self.__class__.__name__) def join_thread(self, board, thread): try: boardpad = BoardPad(self.stdscr, self) boardpad.join(board, thread, self.nickname) self.append_pad(boardpad) self.raise_window(len(self.windowList) - 1) except Exception, err: self.dlog.excpt(err, msg=">>>in WindowLogic.join_thread()", cn=self.__class__.__name__)