async def on_incoming_message(msg): # find the guild/channel it belongs to and add it if isinstance(msg.channel, DMChannel) or \ isinstance(msg.channel, GroupChannel): guild_log = gc.guild_log_tree[0] for channel_log in guild_log.logs: users = [] for user in channel_log.channel.members: users.append(user) if msg.author in users and \ msg.channel is channel_log.channel._channel: await process_message(msg, channel_log) return log("Could not find matching channel log") elif isinstance(msg.channel, TextChannel): doBreak = False for guild_log in gc.guild_log_tree: if guild_log.guild == msg.guild: for channel_log in guild_log.logs: if channel_log.channel == msg.channel: await process_message(msg, channel_log) doBreak = True break if doBreak: break
def parseCommand(command, arg=None): if arg is None: if command in ("refresh", "update"): ui.draw_screen() log("Manual update done", logging.info) elif command in ("quit", "exit"): try: gc.exit_thread.start() except SystemExit: pass elif command in ("help", 'h'): ui.draw_help() elif command in ("guilds", "glds", "servers", "servs"): ui.draw_guildlist() elif command in ("channels", "chans"): ui.draw_channellist() elif command == "emojis": ui.draw_emojilist() elif command in ("users", "members"): ui.draw_userlist() elif command == "nick": change_nick() elif command == "dm": change_guild("private messages") elif command in ("del", "rm"): gc.client.remove_last_message() elif command[0] == 'c': try: if command[1].isdigit(): channel_jump(command) ui.draw_screen() except IndexError: pass return if command in ('guild', 'g', 'server', 's'): change_guild(arg) elif command in ("channel", 'c'): gc.client.current_channel = arg gc.ui.channel_log_offset = -1 ui.draw_screen() elif command == "nick": change_nick(arg) return elif command in ("game", "activity"): gc.client.wait_until_client_task_completes( (gc.client.set_activity, arg)) elif command == "file": send_file(arg) elif command == "status": status = arg.lower() if status in ("away", "afk"): status = "idle" elif "disturb" in status: status = "dnd" if status in ("online", "offline", "idle", "dnd"): gc.client.wait_until_client_task_completes(\ (gc.client.set_status, status))
def change_guild(arg): prev_guild = gc.client.current_guild gc.client.set_current_guild(arg) if gc.client.current_guild is prev_guild: return log("changed guild") gc.ui.channel_log_offset = -1 ui.draw_screen()
def key_input(): # if the next two aren't here, input does not work curses.cbreak() curses.noecho() editWin = gc.ui.editWin gc.ui_thread.wait_until_ui_task_completes((ui.draw_edit_win, True)) while not gc.doExit: ch = editWin.getch() if ch == -1 or not gc.ui.displayPanel.hidden(): time.sleep(0.01) continue if chr(ch) != '\n' and len(gc.ui.messageEdit.inputBuffer) > 0 and \ gc.ui.messageEdit.inputBuffer[0] != ord('/'): gc.typingBeingHandled = True # prevents crashes when enter is hit and input buf is empty if chr(ch) == '\n' and not gc.ui.messageEdit.inputBuffer: continue if ch == curses.KEY_PPAGE: gc.ui.channel_log_offset -= gc.settings["scroll_lines"] ui.draw_screen() continue elif ch == curses.KEY_NPAGE: gc.ui.channel_log_offset += gc.settings["scroll_lines"] ui.draw_screen() continue elif ch == curses.KEY_RESIZE: gc.ui.resize() ui.draw_screen() continue elif ch == curses.KEY_DC: # TODO: Add functionality here ui.draw_screen() continue # if ESC is pressed, clear messageEdit buffer elif ch == 27: ch = editWin.getch() if ch in (0x7f, ord('\b'), curses.KEY_BACKSPACE): gc.ui.messageEdit.reset() gc.ui_thread.wait_until_ui_task_completes( (ui.draw_edit_win, True)) continue ret = gc.ui.messageEdit.addKey(ch) if ret is not None: input_handler(ret) gc.ui.messageEdit.reset() call = (ui.draw_edit_win, True) gc.ui_thread.funcs.append(call) while not gc.doExit and (call in gc.ui_thread.funcs or \ call[0].__name__ in gc.ui_thread.locks): time.sleep(0.01) log("key_input finished") gc.tasksExited += 1
async def init_channel(self, channel=None): clog = None if channel is None: clog = self.current_channel_log log("Initializing current channel") else: log("Initializing channel {}".format(channel.name)) try: for gldlog in gc.guild_log_tree: for chllog in gldlog.logs: if chllog.channel == channel: clog = chllog raise Found except Found: pass if isinstance(clog.channel, PrivateChannel) or \ isinstance(clog.channel, discord.TextChannel) and \ clog.channel.permissions_for(clog.guild.me).read_messages: try: #TODO: Remove try/except once bug is fixed async for msg in clog.channel.history(limit=gc.settings["max_log_entries"]): if msg.edited_at is not None: msg.content += " **(edited)**" # needed for modification of past messages #self.messages.append(msg) clog.insert(0, calc_mutations(msg)) except discord.Forbidden: log("Cannot enter channel {}: Forbidden.".format(clog.channel.name)) init_view(gc, clog.channel) return except Exception as e: log("error: {}".format(e)) gc.channels_entered.append(clog.channel) init_view(gc, clog.channel) # initialize view for msg in clog.logs: gc.ui.views[str(clog.channel.id)].formattedText.addMessage(msg)
def set_current_guild(self, guild): if isinstance(guild, str): for gldlog in gc.guild_log_tree: gld = gldlog.guild if guild.lower() in gld.name.lower(): if not gld.channels: set_display("This guild is empty!") return self._current_guild = gld # find first non-ignored channel, set channel, mark flags as False def_chan = None lowest = 999 for chan in gld.channels: if isinstance(chan, discord.TextChannel) and \ chan.permissions_for(gld.me).read_messages and \ chan.position < lowest: try: # Skip over ignored channels for serv_key in gc.settings["channel_ignore_list"]: if serv_key["guild_name"].lower() == gld.name: for name in serv_key["ignores"]: if chan.name.lower() == name.lower(): raise Found except Found: continue except: e = sys.exc_info()[0] log("Exception raised during channel ignore list parsing: {}".format(e), logging.error) return lowest = chan.position def_chan = chan elif isinstance(chan, PrivateChannel): def_chan = chan else: continue try: if def_chan is None: raise NoChannelsFoundException self.current_channel = def_chan for chanlog in gldlog.logs: if chanlog.channel is def_chan: chanlog.unread = False chanlog.mentioned_in = False return except NoChannelsFoundException: log("No channels found.") return except AttributeError as e: log("Attribute error: {}".format(e)) return except: e = sys.exc_info()[0] log("Error when setting channel flags!: {}".format(e), logging.error) continue return return self._current_guild = guild
def draw_screen(): log("Updating") # init current channel if needed if gc.client.current_channel not in gc.channels_entered: gc.client.wait_until_client_task_completes(\ (gc.client.init_channel,gc.client.current_channel)) if gc.ui.topWinVisible: gc.ui_thread.wait_until_ui_task_completes((draw_top_win, )) if gc.ui.leftWinVisible: gc.ui_thread.wait_until_ui_task_completes((draw_left_win, )) if gc.ui.userWinVisible: gc.ui_thread.wait_until_ui_task_completes((draw_user_win, )) if gc.guild_log_tree is not None: gc.ui_thread.wait_until_ui_task_completes((draw_channel_log, )) gc.ui_thread.wait_until_ui_task_completes((draw_edit_win, )) curses.doupdate()
def resize(self): self.max_y, self.max_x = self.screen.getmaxyx() self.clearWins() try: if self.separatorsVisible: self.makeFrameWin(resize=True) if self.topWinVisible: self.makeTopWin(resize=True) self.makeBottomWin(resize=True) if self.leftWinVisible: self.makeLeftWin(resize=True) if self.userWinVisible: self.makeUserWin(resize=True) self.makeChatWin(resize=True) self.makeDisplay(resize=True) except Exception as e: log("Failed to resize windows. Error: {}".format(e)) self.messageEdit.termWidth = self.messageEdit.width = self.max_x self.redrawFrames() draw_screen()
async def process_message(msg, channel_log): if channel_log.channel not in gc.channels_entered: await gc.client.init_channel(channel_log.channel) else: channel_log.append(calc_mutations(msg)) gc.ui.channel_log_offset += 1 if msg.guild is not None and \ channel_log.channel is not gc.client.current_channel: if msg.guild.me.mention in msg.content: channel_log.mentioned_in = True else: channel_log.unread = True if msg.guild is not None and \ msg.guild.me.mention in msg.content and \ "beep_mentions" in gc.settings and \ gc.settings["beep_mentions"]: curses.beep() log("Beep!") if channel_log is gc.client.current_channel_log: ui.draw_screen()
def typing_handler(): if not gc.settings["send_is_typing"]: return log("typing_handler started") while not gc.doExit: if gc.typingBeingHandled: call = (gc.client.current_channel.trigger_typing, ) gc.client.async_funcs.append(call) while not gc.doExit and (call in gc.client.async_funcs or \ call[0].__name__ in gc.client.locks): time.sleep(0.1) for second in range(50): if gc.doExit: break time.sleep(0.1) gc.typingBeingHandled = False if gc.doExit: break time.sleep(0.1) log("typing_handler finished") gc.tasksExited += 1
def draw_user_win(): userWin = gc.ui.userWin height, width = userWin.getmaxyx() userWin.erase() for idx, member in enumerate(gc.client.current_channel.members): try: if idx + 2 > height: userWin.addstr(idx, 0, "(more)", gc.ui.colors["green"]) break name = member.display_name if len(name) >= width: name = name[:width - 4] + "..." userWin.addstr(idx, 0, name) except Exception as e: # if we're here, text has failed to draw log("Failed to draw user window. Error: {}".format(e)) userWin.noutrefresh()
async def on_message_delete(msg): log("Attempting to delete") await gc.client.wait_until_ready() # TODO: PM's have 'None' as a guild -- fix this later if msg.guild is None: return try: for guildlog in gc.guild_log_tree: if guildlog.guild == msg.guild: for channellog in guildlog.logs: if channellog.channel == msg.channel: ft = gc.ui.views[str( channellog.channel.id)].formattedText channellog.logs.remove(msg) ft.messages.remove(msg) ft.refresh() log("Deleted, updating") if msg.channel is gc.client.current_channel: draw_screen() return except: # if the message cannot be found, an exception will be raised # this could be #1: if the message was already deleted, # (happens when multiple calls get excecuted within the same time) # or the user was banned, (in which case all their msgs disappear) pass log("Could not delete message: {}".format(msg.clean_content))
def draw_edit_win(update=False): editWin = gc.ui.editWin promptText = gc.client.prompt offset = len(promptText) + 5 width = gc.ui.max_x - offset edit = gc.ui.messageEdit borderColor = gc.ui.colors[gc.settings["prompt_border_color"]] hasHash = False hashColor = 0 promptColor = 0 if gc.client.prompt != gc.settings["default_prompt"]: hasHash = True hashColor = gc.ui.colors[gc.settings["prompt_hash_color"]] promptColor = gc.ui.colors[gc.settings["prompt_color"]] edit.setPrompt(gc.client.prompt) try: editWin.erase() editWin.addstr(0, 0, "[", borderColor) if not hasHash: editWin.addstr(gc.settings["default_prompt"], promptColor) else: editWin.addstr("#", hashColor) editWin.addstr(promptText, promptColor) editWin.addstr("]: ", borderColor) try: text_data, text_data_pos, start_pos = edit.getCurrentData() except: text_data, text_data_pos, start_pos = ('', 0, 0) pos = text_data_pos - start_pos data = (text_data[start_pos:start_pos + width - 1], pos) editWin.addstr(0, offset, data[0]) editWin.move(0, offset + data[1]) editWin.noutrefresh() if update: curses.doupdate() except Exception as e: # if we're here, text has failed to draw log("Failed to draw edit window. Error: {}".format(e))
def draw_top_win(): topWin = gc.ui.topWin width = topWin.getmaxyx()[1] color = gc.ui.colors[gc.settings["guild_display_color"]] guildName = gc.client.current_guild.name topic = "" if gc.client.current_channel.topic is not None: topic = gc.client.current_channel.topic # if there is no channel topic, just print the channel name else: topic = gc.client.current_channel.name topic = topic.replace("\n", " ") if len(topic) >= width // 2: topic = topic[:width // 2 - 3] + "..." topicOffset = width // 2 - len(topic) // 2 # sleep required to get accurate user count time.sleep(0.05) try: online = str(gc.client.online) online_text = "Users online: " + online onlineOffset = width - len(online_text) - 1 topWin.erase() topWin.addstr(0, 0, "Guild: ") topWin.addstr(guildName, color) topWin.addstr(0, topicOffset, topic) topWin.addstr(0, onlineOffset, "Users online: ", color) topWin.addstr(online) except Exception as e: # if we're here, text has failed to draw log("Failed to draw top window. Error: {}".format(e)) topWin.noutrefresh()
def redrawFrames(self): # redraw top frame y_offset = 0 color = gc.ui.colors[gc.settings['separator_color']] self.frameWin.attron(color) try: if self.topWinVisible and self.separatorsVisible: y_offset = 1 self.frameWin.hline(y_offset, 0, curses.ACS_HLINE, self.max_x) # redraw bottom frame if self.separatorsVisible: self.frameWin.hline(self.max_y - 2, 0, curses.ACS_HLINE, self.max_x) # redraw left frame if self.leftWinVisible and self.separatorsVisible: self.frameWin.vline(y_offset + 1, self.leftWinWidth, curses.ACS_VLINE, self.max_y - y_offset - 3) self.frameWin.addch(y_offset, self.leftWinWidth, curses.ACS_TTEE) self.frameWin.addch(self.max_y - 2, self.leftWinWidth, curses.ACS_BTEE) # redraw user frame if self.userWinVisible and self.separatorsVisible: self.frameWin.vline(y_offset + 1, self.max_x - self.userWinWidth - 1, curses.ACS_VLINE, self.max_y - y_offset - 3) self.frameWin.addch(y_offset, self.max_x - self.userWinWidth - 1, curses.ACS_TTEE) self.frameWin.addch(self.max_y - 2, self.max_x - self.userWinWidth - 1, curses.ACS_BTEE) except Exception as e: # if we're here, text has failed to draw log("Failed to draw frames. Error: {}".format(e)) self.frameWin.attroff(color) self.frameWin.refresh()
def remove_last_message(self): log("Attempting to delete last message") messages = gc.ui.views[str(self.current_channel.id)].formattedText.messages message = None i = len(messages)-1 while i >= 0: message = messages[i] if message.author.id == self.user.id: log("Deleting message '{}'".format(message.clean_content)) self.wait_until_client_task_completes((message.delete,)) gc.ui.views[str(self.current_channel.id)].formattedText.refresh() draw_screen() return i -= 1 log("Could not delete last message")
def set_current_channel(self, channel): if isinstance(channel, str): try: gld = self.current_guild channel_found = None channel_score = 0.0 for chl in gld.channels: if channel.lower() in chl.name.lower() and \ isinstance(chl, discord.TextChannel) and \ chl.permissions_for(gld.me).read_messages: score = len(channel) / len(chl.name) if score > channel_score: channel_found = chl channel_score = score elif isinstance(chl, PrivateChannel) and \ channel.lower() in chl.name.lower(): channel_found = chl break if channel_found != None: self._current_channel = channel_found self._prompt = channel_found.name if len(gc.channels_entered) > 0: chanlog = self.current_channel_log chanlog.unread = False chanlog.mentioned_in = False return raise RuntimeError("Could not find channel!") except RuntimeError as e: log("RuntimeError during channel setting: {}".format(e), logging.error) return except AttributeError as e: log("Attribute error, chanlog is None: {}".format(e), logging.error) return except: e = sys.exc_info()[0] log("Unknown exception during channel setting: {}".format(e), logging.error) return self._current_channel = channel self._prompt = channel.name if len(gc.channels_entered) > 0: chanlog = self.current_channel_log chanlog.unread = False chanlog.mentioned_in = False
def run(self): log("Starting UI thread") curses.wrapper(self.ui.run) self.locks = [] while not self.gc.doExit: try: if len(self.funcs) > 0: call = self.funcs.pop() func = call[0] self.locks.append(func.__name__) args = [] if len(call) > 1: args = call[1:] func(*args) self.locks.remove(func.__name__) time.sleep(0.01) except Exception as e: log("Error: {}".format(e)) log("Exiting UI thread")
async def run_calls(self): self.locks = [] while not gc.doExit: if len(self.async_funcs) > 0: call = self.async_funcs.pop() func = call[0] self.locks.append(func.__name__) args = [] opt_args = {} if len(call) > 1: for arg in call[1:]: if isinstance(arg, dict): opt_args = arg else: args.append(arg) try: await func(*args, **opt_args) except Exception as e: log("Could not await {}".format(func)) log("\targs: {}\n\topt_args: {}".format(args, opt_args)) log("\terror: {}".format(e)) self.locks.remove(func.__name__) await asyncio.sleep(0.01)
def draw_channel_log(): chatWin = gc.ui.chatWin ft = None doBreak = False for guild_log in gc.guild_log_tree: if guild_log.guild is gc.client.current_guild: for channel_log in guild_log.logs: if channel_log.channel is gc.client.current_channel: if isinstance(channel_log.channel, VoiceChannel) or \ isinstance(channel_log.channel, CategoryChannel): continue try: ft = gc.ui.views[str( channel_log.channel.id)].formattedText except Exception as e: log("e: {}".format(e)) if len(ft.messages) > 0 and channel_log.logs[-1].id == \ ft.messages[-1].id: doBreak = True break if len(channel_log.logs) > 0: ft.addMessage(channel_log.logs[-1]) doBreak = True break if doBreak: break lines = ft.getLines() for line in lines: words = [] for word in line.words: words.append(word.content) #log("Line: {}".format(" ".join(words))) name_offset = 0 chatWin_height, chatWin_width = chatWin.getmaxyx() # upon entering a new channel, scroll all the way down if gc.ui.channel_log_offset == -1: if len(lines) > chatWin_height: gc.ui.channel_log_offset = len(lines) - chatWin_height else: gc.ui.channel_log_offset = 0 # check to see if scrolling is out of bounds elif len(lines) > chatWin_height and \ gc.ui.channel_log_offset > len(lines)-chatWin_height: gc.ui.channel_log_offset = len(lines) - chatWin_height elif gc.ui.channel_log_offset < -1: gc.ui.channel_log_offset = 0 color = 0 chatWin.erase() if not len(lines): chatWin.noutrefresh() return for idx, line in enumerate( lines[gc.ui.channel_log_offset:gc.ui.channel_log_offset + chatWin_height]): try: if line.isFirst: ts_str = "" if gc.settings["timestamps_enabled"]: ts_now = time.time() ts_offset = datetime.fromtimestamp(ts_now)-\ datetime.utcfromtimestamp(ts_now) dt = line.date + ts_offset ts_str = dt.strftime(gc.settings["timestamp_format"]) +\ " - " author_color = get_role_color(line.topRole, gc) chatWin.addstr(idx, 0, "{}{}: ".format(ts_str, line.user), author_color) name_offset = chatWin.getyx()[1] elif name_offset == 0: # if line is at the top and it's not a "user" line for subline in reversed(lines[0:gc.ui.channel_log_offset]): if subline.isFirst: name_offset = len(subline.user) + 2 break chatWin.move(idx, name_offset) for idy, word in enumerate(line.words): color = 0 if "@" + gc.client.current_guild.me.display_name in word.content: color = gc.ui.colors[gc.settings["mention_color"]] if not word.content: continue try: # if the next word attrs are the same if idy < len(line.words) - 1 and word.attrs == line.words[ idy + 1].attrs: chatWin.addstr(word.content + ' ', word.attrs | color) else: chatWin.addstr(word.content, word.attrs | color) chatWin.addstr(' ', curses.A_NORMAL) except: log("Text drawing failed at {}".format(word.content)) chatWin.noutrefresh() except Exception as e: # if we're here, text has failed to draw log("Failed to draw channel log. Error: {}".format(e))
def run(self): log("Starting {} thread".format(self.func.__name__)) try: self.func() except Exception as e: log("Error in {}: {}".format(self.func.__name__, e))
def draw_left_win(): leftWin = gc.ui.leftWin left_win_height, left_win_width = leftWin.getmaxyx() if gc.ui.separatorsVisible: length = 0 length = gc.term.height - gc.settings["margin"] # Create a new list so we can preserve the guild's channel order channel_logs = [] for servlog in gc.guild_log_tree: if servlog.guild is gc.client.current_guild: for chanlog in servlog.logs: channel_logs.append(chanlog) break channel_logs = quick_sort_channel_logs(channel_logs) leftWin.erase() # TODO: Incorperate guilds into list for idx, clog in enumerate(channel_logs): # should the guild have *too many channels!*, stop them # from spilling over the screen try: if idx == left_win_height - 1: leftWin.addstr(idx, 0, "(more)", gc.ui.colors["green"]) break # don't print categories or voice chats # TODO: this will break on private messages if isinstance(clog.channel, VoiceChannel) or \ isinstance(clog.channel, CategoryChannel): continue text = clog.name length = len(text) offset = 0 if gc.settings["number_channels"]: offset = 3 if idx >= 9: offset = 4 if length > left_win_width - offset: if gc.settings["truncate_channels"]: text = text[0:left_win_width - offset] else: text = text[0:left_win_width - 3 - offset] + "..." leftWin.move(idx, 0) if gc.settings["number_channels"]: leftWin.addstr(str(idx + 1) + ". ") if clog.channel is gc.client.current_channel: leftWin.addstr( text, gc.ui.colors[gc.settings["current_channel_color"]]) else: if clog.channel is not channel_logs[0]: pass if clog.unread and gc.settings["blink_unreads"]: color = gc.settings["unread_channel_color"] if "blink_" in color: split = color.split("blink_")[1] color = gc.ui.colors[split] | curses.A_BLINK elif "on_" in color: color = gc.ui.colors[color.split("on_")[1]] leftWin.addstr(text, color) elif clog.mentioned_in and gc.settings["blink_mentions"]: color = gc.settings["unread_mention_color"] if "blink_" in color: color = gc.ui.colors[color.split("blink_")[1]] leftWin.addstr(text, color) else: leftWin.addstr(text) except Exception as e: # if we're here, text has failed to draw log("Failed to draw left window. Error: {}".format(e)) leftWin.noutrefresh()
async def on_error(self, event_method, *args, **kwargs): import traceback log('Ignoring exception in {}'.format(event_method)) traceback.print_exc()