Repository: mitchweaver/Discline Branch: master Commit: 871379546144 Files: 33 Total size: 103.8 KB Directory structure: gitextract_ta9rwfzc/ ├── .gitignore ├── Discline.py ├── LICENSE ├── README.md ├── client/ │ ├── channellog.py │ ├── client.py │ ├── on_message.py │ └── serverlog.py ├── commands/ │ ├── channel_jump.py │ ├── sendfile.py │ └── text_emoticons.py ├── input/ │ ├── input_handler.py │ ├── kbhit.py │ └── typing_handler.py ├── res/ │ ├── scripts/ │ │ └── discline │ └── settings-skeleton.yaml ├── ui/ │ ├── line.py │ ├── text_manipulation.py │ ├── ui.py │ ├── ui_curses.py │ └── ui_utils.py └── utils/ ├── globals.py ├── hidecursor.py ├── print_utils/ │ ├── channellist.py │ ├── emojis.py │ ├── help.py │ ├── print_utils.py │ ├── serverlist.py │ └── userlist.py ├── quicksort.py ├── settings.py ├── token_utils.py └── updates.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ api-ref.html notes.md # more token things just incase someone tries to push # their branch with their token in here... (smh) token login login.sh login.txt discord-token token.sh run.sh start start.sh note.txt notes.txt token.txt token token_login.txt login.txt # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # curses notes and stuff log.txt loading.txt disTypes/ process.md ================================================ FILE: Discline.py ================================================ #!/usr/bin/env python3 # ------------------------------------------------------- # # # # Discline # # # # http://github.com/MitchWeaver/Discline # # # # Licensed under GNU GPLv3 # # # # ------------------------------------------------------- # import sys import asyncio import os from discord import ChannelType from input.input_handler import input_handler, key_input, init_input from input.typing_handler import is_typing_handler from ui.ui import print_screen from ui.text_manipulation import calc_mutations from utils.print_utils.help import print_help from utils.print_utils.print_utils import * from utils.globals import * from utils.settings import copy_skeleton, settings from utils.updates import check_for_updates from utils.token_utils import get_token,store_token from utils import hidecursor from client.serverlog import ServerLog from client.channellog import ChannelLog from client.on_message import on_incoming_message from client.client import Client # check if using python 3.5+ # TODO: this still fails if they're using python2 if sys.version_info >= (3, 5): pass else: print(gc.term.red + "Sorry, but this requires python 3.5+" + gc.term.normal) quit() init_complete = False # Set terminal X11 window title print('\33]0;Discline\a', end='', flush=True) os.system("clear") gc.initClient() @gc.client.event async def on_ready(): await gc.client.wait_until_login() # completely hide the system's cursor await hidecursor.hide_cursor() # these values are set in settings.yaml if settings["default_prompt"] is not None: gc.client.set_prompt(settings["default_prompt"].lower()) else: gc.client.set_prompt('~') if settings["default_server"] is not None: gc.client.set_current_server(settings["default_server"]) if settings["default_channel"] is not None: gc.client.set_current_channel(settings["default_channel"].lower()) gc.client.set_prompt(settings["default_channel"].lower()) if settings["default_game"] is not None: await gc.client.set_game(settings["default_game"]) # --------------- INIT SERVERS ----------------------------------------- # print("Welcome to " + gc.term.cyan + "Discline" + gc.term.normal + "!") await print_line_break() await print_user() await print_line_break() print("Initializing... \n") try: sys.stdout.flush() except: pass for server in gc.client.servers: # Null check to check server availability if server is None: continue serv_logs = [] for channel in server.channels: # Null checks to test for bugged out channels if channel is None or channel.type is None: continue # Null checks for bugged out members if server.me is None or server.me.id is None \ or channel.permissions_for(server.me) is None: continue if channel.type == ChannelType.text: if channel.permissions_for(server.me).read_messages: try: # try/except in order to 'continue' out of multiple for loops for serv_key in settings["channel_ignore_list"]: if serv_key["server_name"].lower() == server.name.lower(): for name in serv_key["ignores"]: if channel.name.lower() == name.lower(): raise Found serv_logs.append(ChannelLog(channel, [])) except: continue # add the channellog to the tree gc.server_log_tree.append(ServerLog(server, serv_logs)) if settings["debug"]: for slog in gc.server_log_tree: for clog in slog.get_logs(): print(slog.get_name() + " ---- " + clog.get_name()) # start our own coroutines try: asyncio.get_event_loop().create_task(key_input()) except SystemExit: pass except KeyboardInterrupt: pass try: asyncio.get_event_loop().create_task(input_handler()) except SystemExit: pass except KeyboardInterrupt: pass try: asyncio.get_event_loop().create_task(is_typing_handler()) except SystemExit: pass except KeyboardInterrupt: pass # Print initial screen await print_screen() global init_complete init_complete = True # called whenever the client receives a message (from anywhere) @gc.client.event async def on_message(message): await gc.client.wait_until_ready() if init_complete: await on_incoming_message(message) @gc.client.event async def on_message_edit(msg_old, msg_new): await gc.client.wait_until_ready() msg_new.content = msg_new.content + " *(edited)*" if init_complete: await print_screen() @gc.client.event async def on_message_delete(msg): await gc.client.wait_until_ready() # TODO: PM's have 'None' as a server -- fix this later if msg.server is None: return try: for serverlog in gc.server_log_tree: if serverlog.get_server() == msg.server: for channellog in serverlog.get_logs(): if channellog.get_channel()== msg.channel: channellog.get_logs().remove(msg) if init_complete: await print_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 def main(): # start the client coroutine TOKEN="" try: if sys.argv[1] == "--help" or sys.argv[1] == "-h": from utils.print_utils.help import print_help print_help() quit() elif sys.argv[1] == "--token" or sys.argv[1] == "--store-token": store_token() quit() elif sys.argv[1] == "--skeleton" or sys.argv[1] == "--copy-skeleton": # ---- now handled in utils.settings.py ---- # pass elif sys.argv[1] == "--config": # --- now handled in utils.settings.py ---- # pass else: print(gc.term.red("Error: Unknown command.")) print(gc.term.yellow("See --help for options.")) quit() except IndexError: pass check_for_updates() token = get_token() init_input() print(gc.term.yellow("Starting...")) # start the client try: gc.client.run(token, bot=False) except KeyboardInterrupt: pass except SystemExit: pass # if we are here, the client's loop was cancelled or errored, or user exited try: kill() except: # if our cleanly-exit kill function failed for whatever reason, # make sure we at least exit uncleanly quit() if __name__ == "__main__": main() ================================================ FILE: LICENSE ================================================ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004 Copyright (C) 2004 Sam Hocevar Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. You just DO WHAT THE FUCK YOU WANT TO. ================================================ FILE: README.md ================================================ # ![logo_small.png](res/logo/logo_small.png) Discline ------------------------------ ![screenshot_main.png](res/screenshots/screenshot_main.png) # NOTICE: July 20th, 2019 AFAIK discline has become non-functional. * Python3.7 has changed the syntax of its async libraries. * discord.py has also gone through a new release cycle which's syntax is not backwards compatible. Discline will require a full rewrite, possibly done in a new language. Stay tuned for updates via this readme. Thanks to all that have supported the project thus far, I hope you stick around for whats to come. With that said, I'll leave the remainder of the readme intact: ## How to use: ------------------------- 1. Install the dependencies: `sudo pip3 install asyncio discord blessings pyyaml` 2. Clone the repo `git clone https://github.com/MitchWeaver/Discline` 3. Find your discord "token" * Go to http://discordapp.com/channels/@me * Open your browser's developer console. (Normally `F12` or `CTRL-SHIFT-I`) * Look for "storage" or "local storage", then find the discord url. * Clicking this will show you a list of variables. Look for a line that looks like: `"token = 322332r093fwaf032f90323f32f903f23wfa"` If you're having troubles, google around, there's a few guides on the net. If all else fails, join the dev discord and we'll be glad to help! 4. Run `python3 Discline.py --store-token` to store your token 5. Run `python3 Discline.py --copy-skeleton` to get a template config 6. Edit `~/.config/Discline/config` to your choosing. 7. Launch with python3 `python3 Discline.py` *(alternatively if you have python3.6 you can simply use `./Discline.py`)* ### Current Features -------------------------- * /channel to switch channel * /server to switch server * /nick to change nickname (per server) * typing without a leading prefix will submit to current chat * " is typing..." support * private channels * colored output, with user definable colors and custom roles * Channel logs update when users edit messages * /channels, /servers, /users to view information * /game to update the "Now playing: " status * use /help to see more commands * unicode emoji displayal support * sending emojis in messages (unicode *and* custom) * File uploading via path (ex: /file /path/to/file) * italic, bold, and underline font support * inline \`code\` and \`\`\`code\`\`\` block support * URL detection, highlighting in blue + italics * automatic updating, fetching the latest master branch's commit * channel logs blink red upon unread messages * line scrolling * discord "Nitro" emojis * Externalized configs via YAML ~/.config/Discline/config * @member expansion/mentions * /status to change online presence ### Planned Features --------------------------- * emoji reactions * comment editing and deletion * private messaging * message searching ## Dependencies ------------------------ * git (if you want automatic updates) * [Python 3.5+](https://www.python.org/downloads/) * [discord.py](https://github.com/Rapptz/discord.py) * [blessings.py](https://pypi.python.org/pypi/blessings/) * [PyYAML](https://pypi.python.org/pypi/PyYAML/) * asyncio **To install dependencies**: 1. Download Python 3.5/3.6 from the link above 2. Install `pip3`, normally called `python3-pip` in package managers 3. Download the dependencies using pip with the following command: `sudo pip3 install asyncio discord blessings pyyaml` ### Color Customization ------------------------ Almost all aspects of the client can be colored to the user's wishes. You can set these colors from within `~/.config/Discline/config` Note: These assume that you're using the standard terminal colors. If you have colors already defined in your ~/.Xresources or similar, this will be very confusing. ## Launching ------------------------ Discline uses git for automatic updates, so you must be within the Discline directory upon starting. Manually you can launch via `python3.6 ./Discline.py`, however it is advised to create a helper script to do this for you. An example script is in the /res/scripts folder, edit it to suit your system and tastes. ### A Note On Emojis ------------------------- Currently *most* of the standard unicode emojis are displaying. Note your terminal must be able to render these symbols *and* you must be using a font set that contains them. Because some of the emojis that discord uses are non-standard, they may not display properly. Here is an example of a random few. ![Image](https://images-ext-2.discordapp.net/external/iN52NdGOWqdWOxby88wiEGs8R81j33ndPjgKX8eKUNA/https/0x0.st/soIy.png?width=400&height=32) Custom emojis however, are displayed as :emoji_name: ### Note On Font Support ------------------------- Like emojis, not all terminals and fonts support italic/bold/underline and 'background' colors, (which are used for \`code\`). If these features aren't working for you, odds are you are not using a supported terminal/font. Experiment with different setups to see what works. ![Image](https://0x0.st/sHQ0.png) *Letting me know what setups __don't__ work helps a lot!* ### Dude this is awesome! How can I support the project? -------------------------------------------------------- Star it! 🌟 It helps to get it higher in GitHub's search results as well as making me feel good inside. ;) If you'd like to contribute, pull requests are __*always*__ welcome! If you would like to get more info on what could be done or to discuss the project in general, come join the discord server at: https://discord.gg/rBGQMTk ### FAQ ------------------------- > Yet another discord cli? I didn't like any of the implementations I found around github. Too buggy. Too bloated. Bad UI. No customization. Some, after discord updates, no longer functioning at all. > Why use a token and not email/password? Discord's API __does__ allow for email/pass login, but if you were to have 2FA, (2 factor authentication), enabled on your account, Discord would interpret this as a malicious attack against your account and disable it. So, because *"Nobody reads the readme"*, I have disabled this. > How should I submit a GitHub issue? Try to include this format: ``` OS: Linux/Debian Terminal: urxvt Font: source code pro Python Version: 3.6 How to reproduce: xxxxxx ``` > It says my python is out of date even though I have 3.5+ installed? Probably because you have multiple versions installed. Try running with `python3.5` or `python3.6` rather than just "python3" > I'm getting weird encoding errors on startup You probably don't have UTF-8 set. If you're using Linux, look up how to do this according to your distro. If you're on BSD, add this to your /etc/profile: ``` export LC_CTYPE=en_US.UTF-8 export LESSCHARSET=utf-8 ``` and make sure it gets sourced upon opening your terminal. ### Misc Screenshots -------------------------- ![Image](res/screenshots/kingk22-screenshot.png) ![Image](https://0x0.st/sH5g.png) ![Image](https://0x0.st/sHjn.png) It can even be configured to hide most elements of the UI in the config: ![Image](res/screenshots/minimal_brown_ss.png) ### Known Bugs -------------------------- > Line wrapping sometimes doesn't work This happens if there is too much formatting / coloring being done to the message that contains that line. I'm looking for a work around. > When I type many lines before hitting send, the UI sometimes bugs out and/or the separators encroach upon different sections Known. Looking for a work around. > My bug isn't listed here, how can I voice my problem? If you have a specific issue that isn't listed here or in the wiki, post a github issue with a detailed explanation and I can try to get it fixed. Join the discord if you want live help. ### Token Warning ------------------------------- Do *NOT* share your token with anybody, if someone else gets ahold of your token, they may control your account. If you are someone who keeps their ~/.config on github, **add your token to your .gitignore**. Otherwise it will become public. ### License ------------------------------- WTFPL ### Legal Disclaimer -------------------------------- Discord hasn't put out any official statement on whether using their API for 3rd party clients is allowed or not. They *have* said that using their API to make "self-bots" is against their ToS. By self-bots, it is my understanding they mean automating non-bot accounts as bots. My code has no automated functions, or any on_events that provide features not included in the official client. As far as I know, nobody has been banned for using things like this before, but Discord might one day change their mind. With this said, I take **no** responsibility if this gets you banned. ================================================ FILE: client/channellog.py ================================================ # Wrapper class to make dealing with logs easier class ChannelLog(): __channel = "" __logs = [] unread = False mentioned_in = False # the index of where to start printing the messages __index = 0 def __init__(self, channel, logs): self.__channel = channel self.__logs = list(logs) def get_server(self): return self.__channel.server def get_channel(self): return self.__channel def get_logs(self): return self.__logs def get_name(self): return self.__channel.name def get_server_name(self): return self.__channel.server.name def append(self, message): self.__logs.append(message) def index(self, message): return self.__logs.index(message) def insert(self, i, message): self.__logs.insert(i, message) def len(self): return len(self.__logs) def get_index(self): return self.__index def set_index(self, int): self.__index = int def inc_index(self, int): self.__index += int def dec_index(self, int): self.__index -= int ================================================ FILE: client/client.py ================================================ import discord from utils.globals import gc from utils.settings import settings import ui.text_manipulation as tm # inherits from discord.py's Client class Client(discord.Client): # NOTE: These are strings! __current_server = "" __current_channel = "" __prompt = "" # discord.Status object __status = "" # discord.Game object __game = "" # Note: setting only allows for string types def set_prompt(self, string): self.__prompt = string.lower() def set_current_server(self, string): self.__current_server = string.lower() def set_current_channel(self, string): self.__current_channel = string.lower() self.set_prompt(string) def get_prompt(self): return self.__prompt def get_current_server_name(self): return self.__current_server def get_current_channel_name(self): return self.__current_channel def get_current_server(self): for server in self.servers: if server.name.lower() == self.__current_server: return server def get_current_server_log(self): for slog in gc.server_log_tree: if slog.get_server() == self.get_current_server(): return slog def get_current_channel(self): for server in self.servers: if server.name.lower() == self.__current_server.lower(): for channel in server.channels: if channel.type is discord.ChannelType.text: if channel.name.lower() == self.__current_channel.lower(): if channel.permissions_for(server.me).read_messages: return channel async def populate_current_channel_log(self): slog = self.get_current_server_log() for idx, clog in enumerate(slog.get_logs()): if clog.get_channel().type is discord.ChannelType.text: if clog.get_channel().name.lower() == self.__current_channel.lower(): if clog.get_channel().permissions_for(slog.get_server().me).read_messages: async for msg in self.logs_from(clog.get_channel(), limit=settings["max_log_entries"]): clog.insert(0, await tm.calc_mutations(msg)) def get_current_channel_log(self): slog = self.get_current_server_log() for idx, clog in enumerate(slog.get_logs()): if clog.get_channel().type is discord.ChannelType.text: if clog.get_channel().name.lower() == self.__current_channel.lower(): if clog.get_channel().permissions_for(slog.get_server().me).read_messages: return clog # returns online members in current server async def get_online(self): online_count = 0 if not self.get_current_server() == None: for member in self.get_current_server().members: if member is None: continue # happens if a member left the server if member.status is not discord.Status.offline: online_count +=1 return online_count # because the built-in .say is really buggy, just overriding it with my own async def say(self, string): await self.send_message(self.get_current_channel(), string) async def set_game(self, string): self.__game = discord.Game(name=string,type=0) self.__status = discord.Status.online # Note: the 'afk' kwarg handles how the client receives messages, (rates, etc) # This is meant to be a "nice" feature, but for us it causes more headache # than its worth. if self.__game is not None and self.__game != "": if self.__status is not None and self.__status != "": try: await self.change_presence(game=self.__game, status=self.__status, afk=False) except: pass else: try: await self.change_presence(game=self.__game, status=discord.Status.online, afk=False) except: pass async def get_game(self): return self.__game async def set_status(self, string): if string == "online": self.__status = discord.Status.online elif string == "offline": self.__status = discord.Status.offline elif string == "idle": self.__status = discord.Status.idle elif string == "dnd": self.__status = discord.Status.dnd if self.__game is not None and self.__game != "": try: await self.change_presence(game=self.__game, status=self.__status, afk=False) except: pass else: try: await self.change_presence(status=self.__status, afk=False) except: pass async def get_status(self): return self.__status ================================================ FILE: client/on_message.py ================================================ from ui.ui import print_screen from utils.globals import gc from ui.text_manipulation import calc_mutations async def on_incoming_message(msg): # TODO: make sure it isn't a private message # find the server/channel it belongs to and add it for server_log in gc.server_log_tree: if server_log.get_server() == msg.server: for channel_log in server_log.get_logs(): if channel_log.get_channel() == msg.channel: channel_log.append(await calc_mutations(msg)) if channel_log.get_channel() is not gc.client.get_current_channel(): if msg.server.me.mention in msg.content: channel_log.mentioned_in = True else: channel_log.unread = True # redraw the screen await print_screen() ================================================ FILE: client/serverlog.py ================================================ from discord import Server, Channel from client.channellog import ChannelLog # Simple wrapper class to hold a list of ChannelLogs class ServerLog(): __server = "" __channel_logs = [] def __init__(self, server, channel_log_list): self.__server = server self.__channel_logs = list(channel_log_list) def get_server(self): return self.__server def get_name(self): return self.__server.name def get_logs(self): return self.__channel_logs def clear_logs(self): for channel_log in self.__channel_logs: del channel_log[:] # takes list of ChannelLog def add_logs(self, log_list): for logs in log_list: self.__channel_logs.append(logs) ================================================ FILE: commands/channel_jump.py ================================================ from utils.globals import gc from utils.quicksort import quick_sort_channel_logs from utils.settings import settings async def channel_jump(arg): logs = [] num = int(arg[1:]) - 1 # sub one to allow for "/c0" being the top channel if settings["arrays_start_at_zero"]: num -= 1 # in case someone tries to go to a negative index if num <= -1: num = 0 for slog in gc.server_log_tree: if slog.get_server() is gc.client.get_current_server(): for clog in slog.get_logs(): logs.append(clog) logs = quick_sort_channel_logs(logs) if num > len(logs): num = len(logs) - 1 gc.client.set_current_channel(logs[num].get_name()) logs[num].unread = False logs[num].mentioned_in = False ================================================ FILE: commands/sendfile.py ================================================ from getpass import getuser from utils.globals import gc from ui.ui import set_display async def send_file(client, filepath): # try to open the file exactly as user inputs it try: await client.send_file(client.get_current_channel(), filepath) except: # assume the user ommited the prefix of the dir path, # try to load it starting from user's home directory: try: filepath = "/home/" + getuser() + "/" + filepath await client.send_file(client.get_current_channel(), filepath) except: # Either a bad file path, the file was too large, # or encountered a connection problem during upload msg = "Error: Bad filepath" await set_display(gc.term.bold + gc.term.red + gc.term.move(gc.term.height - 1, \ gc.term.width - len(msg) - 1) + msg) ================================================ FILE: commands/text_emoticons.py ================================================ async def check_emoticons(client, cmd): if cmd == "shrug": try: await client.send_message(client.get_current_channel(), "¯\_(ツ)_/¯") except: pass elif cmd == "tableflip": try: await client.send_message(client.get_current_channel(), "(╯°□°)╯︵ ┻━┻") except: pass elif cmd == "unflip": try: await client.send_message(client.get_current_channel(), "┬──┬ ノ( ゜-゜ノ)") except: pass elif cmd == "zoidberg": try: await client.send_message(client.get_current_channel(), "(/) (°,,°) (/)") except: pass elif cmd == "lenny": try: await client.send_message(client.get_current_channel(), "( ͡° ͜ʖ ͡°)") except: pass elif cmd == "lennyx5": try: await client.send_message(client.get_current_channel(), "( ͡°( ͡° ͜ʖ( ͡° ͜ʖ ͡°)ʖ ͡°) ͡°)") except: pass elif cmd == "glasses": try: await client.send_message(client.get_current_channel(), "(•_•) ( •_•)>⌐■-■ (⌐■_■)") except: pass elif cmd == "walking_my_mods": try: await client.send_message(client.get_current_channel(), "⌐( ͡° ͜ʖ ͡°) ╯╲___卐卐卐卐") except: pass ================================================ FILE: input/input_handler.py ================================================ import asyncio import discord from input.kbhit import KBHit import ui.ui as ui from utils.globals import gc, kill from utils.print_utils.help import print_help from utils.print_utils.userlist import print_userlist from utils.print_utils.serverlist import print_serverlist from utils.print_utils.channellist import print_channellist from utils.print_utils.emojis import print_emojilist from utils.settings import settings from commands.text_emoticons import check_emoticons from commands.sendfile import send_file from commands.channel_jump import channel_jump kb = "" def init_input(): global kb kb = KBHit() async def key_input(): await gc.client.wait_until_ready() global kb memory = "" key = "" while True: if await kb.kbhit() or memory == "[": key = await kb.getch() ordkey = ord(key) if memory == "[": if key == "6": # page down gc.client.get_current_channel_log().dec_index(settings["scroll_lines"]) del gc.input_buffer[-1] elif key == "5": # page up gc.client.get_current_channel_log().inc_index(settings["scroll_lines"]) del gc.input_buffer[-1] else: if ordkey == 10 or ordkey == 13: # enter key gc.user_input = "".join(gc.input_buffer) del gc.input_buffer[:] elif ordkey == 127 or ordkey == 8: # backspace if len(gc.input_buffer) > 0: del gc.input_buffer[-1] elif ordkey >= 32 and ordkey <= 256: # all letters and special characters if not (ordkey == 126 and (memory == "5" or memory == "6")): # tilde left over from page up/down gc.input_buffer.append(key) elif ordkey == 9: gc.input_buffer.append(" " * 4) # tab key memory = key if key != "[": await ui.print_screen() if key != "[": await asyncio.sleep(0.015) elif key == "~": await asyncio.sleep(0.1) async def input_handler(): await gc.client.wait_until_ready() while True: # If input is blank, don't do anything if gc.user_input == '': await asyncio.sleep(0.05) continue # # check if input is a command if gc.user_input[0] == settings["prefix"]: # strip the PREFIX gc.user_input = gc.user_input[1:] # check if contains a space if ' ' in gc.user_input: # split into command and argument command,arg = gc.user_input.split(" ", 1) if command == "server" or command == 's': server_name = "" # check if arg is a valid server, then switch for servlog in gc.server_log_tree: if servlog.get_name().lower() == arg.lower(): server_name = servlog.get_name() break # if we didn't find an exact match, assume only partial # Note if there are multiple servers containing the same # word, this will only pick the first one. Better than nothing. if server_name == "": for servlog in gc.server_log_tree: if arg.lower() in servlog.get_name().lower(): server_name = servlog.get_name() break if server_name != "": gc.client.set_current_server(server_name) # discord.py's "server.default_channel" is buggy. # often times it will return 'none' even when # there is a default channel. to combat this, # we can just get it ourselves. def_chan = "" lowest = 999 for chan in servlog.get_server().channels: if chan.type is discord.ChannelType.text: if chan.permissions_for(servlog.get_server().me).read_messages: if chan.position < lowest: try: for serv_key in settings["channel_ignore_list"]: if serv_key["server_name"].lower() == server_name: for name in serv_key["ignores"]: if chan.name.lower() == name.lower(): raise Found except: continue lowest = chan.position def_chan = chan try: gc.client.set_current_channel(def_chan.name) # and set the default channel as read for chanlog in servlog.get_logs(): if chanlog.get_channel() is def_chan: chanlog.unread = False chanlog.mentioned_in = False break # TODO: Bug: def_chan is sometimes "" except: continue else: ui.set_display(gc.term.red + "Can't find server" + gc.term.normal) elif command == "channel" or command == 'c': # check if arg is a valid channel, then switch for servlog in gc.server_log_tree: if servlog.get_server() is gc.client.get_current_server(): final_chanlog = "" for chanlog in servlog.get_logs(): if chanlog.get_name().lower() == arg.lower(): if chanlog.get_channel().type is discord.ChannelType.text: if chanlog.get_channel().permissions_for(servlog.get_server().me).read_messages: final_chanlog = chanlog break # if we didn't find an exact match, assume partial if final_chanlog == "": for chanlog in servlog.get_logs(): if chanlog.get_channel().type is discord.ChannelType.text: if chanlog.get_channel().permissions_for(servlog.get_server().me).read_messages: if arg.lower() in chanlog.get_name().lower(): final_chanlog = chanlog break if final_chanlog != "": gc.client.set_current_channel(final_chanlog.get_name()) final_chanlog.unread = False final_chanlog.mentioned_in = False break else: ui.set_display(gc.term.red + "Can't find channel" + gc.term.normal) elif command == "nick": try: await gc.client.change_nickname(gc.client.get_current_server().me, arg) except: # you don't have permission to do this here pass elif command == "game": await gc.client.set_game(arg) elif command == "file": await send_file(gc.client, arg) elif command == "status": status = arg.lower() if status == "away" or status == "afk": status = "idle" elif "disturb" in status: status = "dnd" if status == "online" or status == "offline" \ or status == "idle" or status == "dnd": await gc.client.set_status(status) # else we must have only a command, no argument else: command = gc.user_input if command == "clear": await ui.clear_screen() elif command == "quit": kill() elif command == "exit": kill() elif command == "help" or command == "h": print_help(gc) elif command == "servers" or command == "servs": await print_serverlist() elif command == "channels" or command == "chans": await print_channellist() elif command == "emojis": await print_emojilist() elif command == "users" or command == "members": await ui.clear_screen() await print_userlist() elif command[0] == 'c': try: if command[1].isdigit(): await channel_jump(command) except IndexError: pass await check_emoticons(gc.client, command) # this must not be a command... else: # check to see if it has any custom-emojis, written as :emoji: # we will need to expand them. # these will look like <:emojiname:39432432903201> # check if there might be an emoji if gc.user_input.count(":") >= 2: # if user has nitro, loop through *all* emojis if settings["has_nitro"]: for emoji in gc.client.get_all_emojis(): short_name = ':' + emoji.name + ':' if short_name in gc.user_input: # find the "full" name of the emoji from the api full_name = "<:" + emoji.name + ":" + emoji.id + ">" gc.user_input = gc.user_input.replace(short_name, full_name) # else the user can only send from this server elif gc.client.get_current_server().emojis is not None \ and len(gc.client.get_current_server().emojis) > 0: for emoji in gc.client.get_current_server().emojis: short_name = ':' + emoji.name + ':' if short_name in gc.user_input: # find the "full" name of the emoji from the api full_name = "<:" + emoji.name + ":" + emoji.id + ">" gc.user_input = gc.user_input.replace(short_name, full_name) # if we're here, we've determined its not a command, # and we've processed all mutations to the input we want # now we will try to send the message. text_to_send = gc.user_input if "@" in gc.user_input: sections = gc.user_input.lower().strip().split(" ") sects_copy = [] for sect in sections: if "@" in sect: for member in gc.client.get_current_server().members: if member is not gc.client.get_current_server().me: if sect[1:] in member.display_name.lower(): sect = "<@!" + member.id + ">" sects_copy.append(sect) text_to_send = " ".join(sects_copy) # sometimes this fails --- this could be due to occasional # bugs in the api, or there was a connection problem # So we will try it 3 times, sleeping a bit inbetween for i in range(0,3): try: await gc.client.send_message(gc.client.get_current_channel(), text_to_send) break except: await asyncio.sleep(3) if i == 2: ui.set_display(gc.term.blink_red + "error: could not send message") # clear our input as we've just sent it gc.user_input = "" # update the screen await ui.print_screen() await asyncio.sleep(0.25) ================================================ FILE: input/kbhit.py ================================================ import os import sys import termios import atexit from select import select class KBHit: def __init__(self): self.fd = sys.stdin.fileno() if self.fd is not None: self.fd = os.fdopen(os.dup(self.fd)) self.new_term = termios.tcgetattr(self.fd) self.old_term = termios.tcgetattr(self.fd) # New terminal setting unbuffered self.new_term[3] = (self.new_term[3] & ~termios.ICANON & ~termios.ECHO) termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.new_term) # Support normal-terminal reset at exit atexit.register(self.set_normal_term) def set_normal_term(self): ''' Resets to normal terminal. ''' termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old_term) async def getch(self): return self.fd.read(1) async def kbhit(self): ''' Returns if keyboard character was hit ''' dr,dw,de = select([self.fd], [], [], 0) return dr != [] ================================================ FILE: input/typing_handler.py ================================================ import asyncio from utils.settings import settings from utils.globals import gc async def is_typing_handler(): # user specified setting in settings.py if not settings["send_is_typing"]: return is_typing = False while True: # if typing a message, display '... is typing' if not is_typing: if len(gc.input_buffer) > 0 and gc.input_buffer[0] is not settings["prefix"]: await gc.client.send_typing(gc.client.get_current_channel()) is_typing = True elif len(gc.input_buffer) == 0 or gc.input_buffer[0] is settings["prefix"]: is_typing = False await asyncio.sleep(0.5) ================================================ FILE: res/scripts/discline ================================================ #!/bin/sh # # This is a script to easily launch discline # from an application launcher or similar. # # http://github.com/mitchweaver/Discline # # These are just examples of what I use, # edit them to match your system and needs. term='st' font='MonteCarlo' shell='/bin/dash' DISCLINE_DIR="${HOME}/workspace/Discline" PYTHON_VERSION='3.6' # ------------------------------------------------------- $term -f $font -e $shell -c "cd $DISCLINE_DIR && \ python$PYTHON_VERSION Discline.py" & ================================================ FILE: res/settings-skeleton.yaml ================================================ --- # ------------------------------------------------------------------------- # You can edit these to your preferences. Note: anything silly # like max_messages=-1 will break the client. Duh. # ------------------------------------------------------------------------- # the default server which will be joined upon startup - CASE SENSITIVE! default_server: discline # the default channel which will be joined upon startup - CASE SENSITIVE! default_channel: test_bed # the leading character used for commands prefix: / # whether you have discord "Nitro" -- this enables external emojis has_nitro: false # the default prompt when not in a channel default_prompt: "~" # the default 'playing ' status in discord default_game: Discline # used for various things, your preference arrays_start_at_zero: false # Margins for inside the terminal and between elements. NOTE: must be >= 2 # NOTE: some ratios have weird glitches. Just experiment. margin: 2 # the max amount of messages to be downloaded + kept # NOTE: minimum = 100! This is normally safe to increase. max_messages: 100 # the max amount of entries in each channel log to be downloaded + kept # NOTE: minimum = 100! The larger this is, the slower the client will start. max_log_entries: 100 # Whether to send "... is typing" when the input buffer is not blank or '/' send_is_typing: true # Whether to show in-line emojis in messages show_emojis: true # Whether to show, or hide the left channel bar show_left_bar: true # Whether to show, or hide the top bar show_top_bar: true # Whether to show the separator lines show_separators: true # the denominator used to calculate the width of the "left bar" # NOTE: larger number here, the smaller the bar will be, # (although there is still a minimum of 8 chars...) left_bar_divider: 9 # Determines whether the left bar 'truncates' the channels or # appends "..." to the end when the screen is too small to display them truncate_channels: false # Whether to number channels in the left bar (ex: "1. general") number_channels: false # the amount of lines to scroll up/down on each trigger scroll_lines: 3 # ---------------- COLOR SETTINGS ------------------------------------ # # Available colors are: "white", "red", "blue", "black" # "green", "yellow", "cyan", "magenta" # Or: you can say "on_" to make it the background (ex: 'on_red') # Or: you can say "blink_" to have it flash (ex: 'blink_blue') separator_color: white server_display_color: cyan prompt_color: white prompt_hash_color: red prompt_border_color: magenta normal_user_color: green # messages that contain @you mentions will be this color mention_color: yellow # the "default" text color for messages and other things text_color: white code_block_color: on_black url_color: cyan channel_list_color: white current_channel_color: green # colors for the channels in the left bar upon unreads unread_channel_color: blink_yellow unread_mention_color: blink_red # whether channels should blink when they have unread messages blink_unreads: true # same as above, but for @mentions blink_mentions: true # here you can define your own custom roles - NOTE: text must match exactly! # These for example could be "helper" or "trusted", whatever roles # your servers use custom_roles: - name: admin color: magenta - name: mod color: blue - name: bot color: yellow # Channel ignore list - This stops the channel from being loaded. # Effectively like the "mute" + "hide" feature on the official client, # However with the added benefit that this means these channels won't # be stored in RAM. # Follow the format as below. # Note it is TWO spaces, not a tab! channel_ignore_list: - server_name: server name ignores: - some_channel - some_other_channel - server_name: another server name ignores: - foo - bar # ignore this unless you know what you're doing debug: false ================================================ FILE: ui/line.py ================================================ class Line(): # the text the line contains text = "" # how offset from the [left_bar_width + MARGIN] it should be printed # this is to offset wrapped lines to better line up with the previous offset = 0 def __init__(self, text, offset): self.text = text self.offset = offset def length(self): return len(self.text) ================================================ FILE: ui/text_manipulation.py ================================================ import re from discord import MessageType from utils.settings import settings from utils.globals import gc, get_color import utils async def calc_mutations(msg): try: # if the message is a file, extract the discord url from it json = str(msg.attachments[0]).split("'") for string in json: if string is not None and string != "": if "cdn.discordapp.com/attachments" in string: msg.content = string break except IndexError: pass # otherwise it must not have any attachments and its a regular message text = msg.content # check for in-line code blocks if text.count("```") > 1: while("```") in text: text = await convert_code_block(text) msg.content = text # TODO: if there are asterics or __'s in the code, then # this will not stop them from being formatted # check for in-line code marks if text.count("`") > 1: while("`") in text: text = await convert_code(text) msg.content = text # check to see if it has any custom-emojis # These will look like <:emojiname:39432432903201> # We will recursively trim this into just :emojiname: if msg.server.emojis is not None and len(msg.server.emojis) > 0: for emoji in msg.server.emojis: full_name = "<:" + emoji.name + ":" + emoji.id + ">" while full_name in text: text = await trim_emoji(full_name, emoji.name, text) msg.content = text # check for boldened font if text.count("**") > 1: while("**") in text: text = await convert_bold(text) msg.content = text # check for italic font if text.count("*") > 1: while("*") in text: text = await convert_italic(text) msg.content = text # check for underlined font if text.count("__") > 1: while("__") in text: text = await convert_underline(text) msg.content = text # check for urls if "http://" in text or "https://" in text or "www." in text \ or "ftp://" in text or ".com" in text: msg.content = await convert_url(text) # check if the message is a "user has pinned..." message if msg.type == MessageType.pins_add: msg.content = await convert_pin(msg) # else it must be a regular message, nothing else return msg async def convert_pin(msg): name = "" if msg.author.nick is not None and msg.author.nick != "": name = msg.author.nick else: name = msg.author.name return "📌 " + str(name) + " has pinned a message to this channel." async def trim_emoji(full_name, short_name, string): return string.replace(full_name, ":" + short_name + ":") async def convert_bold(string): sections = string.split("**") left = sections[0] target = sections[1] right = "".join(sections[2]) return gc.term.normal + gc.term.white + left + " " + gc.term.bold(target) + gc.term.normal + \ gc.term.white + " " + right async def convert_italic(string): sections = string.split("*") left = sections[0] target = sections[1] right = "".join(sections[2]) return gc.term.normal + gc.term.white + left + " " + gc.term.italic(target) + gc.term.normal + \ gc.term.white + " " + right async def convert_underline(string): sections = string.split("__") left = sections[0] target = sections[1] right = "".join(sections[2]) return gc.term.normal + gc.term.white + left + " " + gc.term.underline(target) + gc.term.normal + \ gc.term.white + " " + right async def convert_code(string): sections = string.split("`") left = sections[0] target = sections[1] right = "".join(sections[2]) return gc.term.normal + gc.term.white + left + " " + await get_color(settings["code_block_color"]) \ + target + gc.term.normal \ + gc.term.white + " " + right async def convert_code_block(string): sections = string.split("```") left = sections[0] target = sections[1] right = "".join(sections[2]) return gc.term.normal + gc.term.white + left + " " + gc.term.on_black(target) + gc.term.normal + \ gc.term.white + " " + right async def convert_url(string): formatted_line = [] entities = [] if " " in string: entities = string.split(" ") else: entities.append(string) for entity in entities: if "http://" in entity or "https://" in entity or "www." in entity \ or "ftp://" in entity or ".com" in entity: entity = await get_color(settings["url_color"]) + gc.term.italic + gc.term.underline + entity + gc.term.normal formatted_line.append(entity) return " ".join(formatted_line) ================================================ FILE: ui/ui.py ================================================ import sys from os import system from discord import ChannelType from blessings import Terminal from ui.line import Line from ui.ui_utils import * from utils.globals import gc, get_color from utils.quicksort import quick_sort_channel_logs from utils.settings import settings from utils.print_utils.userlist import print_userlist # maximum number of lines that can be on the screen # is updated every cycle as to allow automatic resizing MAX_LINES = 0 # buffer to allow for double buffering (stops screen flashing) screen_buffer = [] # text that can be set to be displayed for 1 frame display = "" display_frames = 0 async def print_screen(): # Get ready to redraw the screen left_bar_width = await get_left_bar_width() await clear_screen() if settings["show_top_bar"]: await print_top_bar(left_bar_width) if gc.server_log_tree is not None: await print_channel_log(left_bar_width) await print_bottom_bar(left_bar_width) # Print the buffer containing our message logs if settings["show_top_bar"]: if settings["show_separators"]: with gc.term.location(0, 2): print("".join(screen_buffer), end="") else: with gc.term.location(0, 1): print("".join(screen_buffer), end="") else: with gc.term.location(0, 0): print("".join(screen_buffer), end="") if settings["show_left_bar"]: await print_left_bar(left_bar_width) global display, display_frames if display != "": print(display) display_frames -= 1 if display_frames <= 0: display = "" async def print_top_bar(left_bar_width): topic = "" try: if gc.client.get_current_channel().topic is not None: topic = gc.client.get_current_channel().topic except: # if there is no channel topic, just print the channel name try: topic = gc.client.get_current_channel().name except: pass text_length = gc.term.width - (36 + len(gc.client.get_current_server_name())) if len(topic) > text_length: topic = topic[:text_length] with gc.term.location(1,0): print("Server: " + await get_color(settings["server_display_color"]) \ + gc.client.get_current_server_name() + gc.term.normal, end="") with gc.term.location(gc.term.width // 2 - len(topic) // 2, 0): print(topic, end="") online_text = "Users online: " online_count = str(await gc.client.get_online()) online_length = len(online_text) + len(online_count) with gc.term.location(gc.term.width - online_length - 1, 0): print(await get_color(settings["server_display_color"]) + online_text \ + gc.term.normal + online_count, end="") if settings["show_separators"]: divider = await get_color(settings["separator_color"]) \ + ("─" * gc.term.width) + "\n" + gc.term.normal with gc.term.location(0, 1): print(divider, end="") with gc.term.location(left_bar_width, 1): print(await get_color(settings["separator_color"]) + "┬", end="") async def set_display(string): global display, display_frames loc = gc.term.width - 1 - len(string) escape_chars = "\e" for escape_chars in string: loc = loc - 5 display = gc.term.move(gc.term.height - 1, loc) + string display_frames = 3 async def print_left_bar(left_bar_width): start = 0 if settings["show_top_bar"]: start = 2 if settings["show_separators"]: length = 0 length = gc.term.height - settings["margin"] sep_color = await get_color(settings["separator_color"]) for i in range(start, length): print(gc.term.move(i, left_bar_width) + sep_color + "│" \ + gc.term.normal, end="") # Create a new list so we can preserve the server's channel order channel_logs = [] for servlog in gc.server_log_tree: if servlog.get_server() is gc.client.get_current_server(): for chanlog in servlog.get_logs(): channel_logs.append(chanlog) break channel_logs = quick_sort_channel_logs(channel_logs) # buffer to print buffer = [] count = 1 for log in channel_logs: # don't print categories or voice chats # TODO: this will break on private messages if log.get_channel().type != ChannelType.text: continue text = log.get_name() length = len(text) if settings["number_channels"]: if count <= 9: length += 1 else: length += 2 if length > left_bar_width: if settings["truncate_channels"]: text = text[0:left_bar_width - 1] else: text = text[0:left_bar_width - 4] + "..." if log.get_channel() is gc.client.get_current_channel(): if settings["number_channels"]: buffer.append(gc.term.normal + str(count) + ". " + gc.term.green + text + gc.term.normal + "\n") else: buffer.append(gc.term.green + text + gc.term.normal + "\n") else: if log.get_channel() is not channel_logs[0]: pass if log.get_channel() is not gc.client.get_current_channel(): if log.unread and settings["blink_unreads"]: text = await get_color(settings["unread_channel_color"]) + text + gc.term.normal elif log.mentioned_in and settings["blink_mentions"]: text = await get_color(settings["unread_mention_color"]) + text + gc.term.normal if settings["number_channels"]: buffer.append(gc.term.normal + str(count) + ". " + text + "\n") else: buffer.append(text + "\n") count += 1 # should the server have *too many channels!*, stop them # from spilling over the screen if count - 1 == gc.term.height - 2 - settings["margin"]: break with gc.term.location(0, start): print("".join(buffer)) async def print_bottom_bar(left_bar_width): if settings["show_separators"]: with gc.term.location(0, gc.term.height - 2): print(await get_color(settings["separator_color"]) + ("─" * gc.term.width) \ + "\n" + gc.term.normal, end="") with gc.term.location(left_bar_width, gc.term.height - 2): print(await get_color(settings["separator_color"]) + "┴", end="") bottom = await get_prompt() if len(gc.input_buffer) > 0: bottom = bottom + "".join(gc.input_buffer) with gc.term.location(0, gc.term.height - 1): print(bottom, end="") async def clear_screen(): # instead of "clearing", we're actually just overwriting # everything with white space. This mitigates the massive # screen flashing that goes on with "cls" and "clear" del screen_buffer[:] wipe = (" " * (gc.term.width) + "\n") * gc.term.height print(gc.term.move(0,0) + wipe, end="") async def print_channel_log(left_bar_width): global INDEX # If the line would spill over the screen, we need to wrap it # NOTE: gc.term.width is calculating every time this function is called. # Meaning that this will automatically resize the screen. # note: the "1" is the space at the start MAX_LENGTH = gc.term.width - (left_bar_width + settings["margin"]) - 1 # For wrapped lines, offset them to line up with the previous line offset = 0 # List to put our *formatted* lines in, once we have OK'd them to print formatted_lines = [] # the max number of lines that can be shown on the screen MAX_LINES = await get_max_lines() for server_log in gc.server_log_tree: if server_log.get_server() is gc.client.get_current_server(): for channel_log in server_log.get_logs(): if channel_log.get_channel() is gc.client.get_current_channel(): if channel_log.get_channel() not in gc.channels_entered: await gc.client.populate_current_channel_log() gc.channels_entered.append(channel_log.get_channel()) # if the server has a "category" channel named the same # as a text channel, confusion will occur # TODO: private messages are not "text" channeltypes if channel_log.get_channel().type != ChannelType.text: continue for msg in channel_log.get_logs(): # The lines of this unformatted message msg_lines = [] HAS_MENTION = False if "@" + gc.client.get_current_server().me.display_name in msg.clean_content: HAS_MENTION = True author_name = "" try: author_name = msg.author.display_name except: try: author_name = msg.author.name except: author_name = "Unknown Author" author_name_length = len(author_name) author_prefix = await get_role_color(msg) + author_name + ": " color = "" if HAS_MENTION: color = await get_color(settings["mention_color"]) else: color = await get_color(settings["text_color"]) proposed_line = author_prefix + color + msg.clean_content.strip() # If our message actually consists of # of multiple lines separated by new-line # characters, we need to accomodate for this. # --- Otherwise: msg_lines will just consist of one line msg_lines = proposed_line.split("\n") for line in msg_lines: # strip leading spaces - LEFT ONLY line = line.lstrip() # If our line is greater than our max length, # that means the author has a long-line comment # that wasn't using new line chars... # We must manually wrap it. line_length = len(line) # Loop through, wrapping the lines until it behaves while line_length > MAX_LENGTH: line = line.strip() # Take a section out of the line based on our max length sect = line[:MAX_LENGTH - offset] # Make sure we did not cut a word in half sect = sect[:sect.strip().rfind(' ')] # If this section isn't the first line of the comment, # we should offset it to better distinguish it offset = 0 if author_prefix not in sect: if line is not msg_lines[0]: offset = author_name_length + settings["margin"] # add in now formatted line! formatted_lines.append(Line(sect.strip(), offset)) # since we just wrapped a line, we need to # make sure we don't overwrite it next time # Split the line between what has been formatted, and # what still remains needing to be formatted if len(line) > len(sect): line = line.split(sect)[1] # find the "real" length of the line, by subtracting # any escape characters it might have. It would # be wasteful to loop through all of the possibilities # so instead we will simply subtract the length # of the shortest for each that it has. line_length = len(line) target = "\e" for target in line: line_length -= 5 # Once here, the string was either A: already short enough # to begin with, or B: made through our while loop and has # since been chopped down to less than our MAX_LENGTH if len(line.strip()) > 0: offset = 0 if author_prefix not in line: offset = author_name_length + settings["margin"] formatted_lines.append(Line(line.strip(), offset)) # where we should start printing from # clamp the index as not to show whitespace if channel_log.get_index() < MAX_LINES: channel_log.set_index(MAX_LINES) elif channel_log.get_index() > len(formatted_lines): channel_log.set_index(len(formatted_lines)) # ----- Trim out list to print out nicely ----- # # trims off the front of the list, until our index del formatted_lines[0:(len(formatted_lines) - channel_log.get_index())] # retains the amount of lines for our screen, deletes remainder del formatted_lines[MAX_LINES:] # if user does not want the left bar, do not add margin space = " " if not settings["show_left_bar"]: space = "" # add to the buffer! for line in formatted_lines: screen_buffer.append(space * (left_bar_width + \ settings["margin"] + line.offset) + line.text + "\n") # return as not to loop through all channels unnecessarily return ================================================ FILE: ui/ui_curses.py ================================================ import sys import time from os import system import curses from curses import ascii as cAscii from discord import ChannelType from blessings import Terminal from ui.line import Line from ui.ui_utils import * from utils.globals import * from utils.quicksort import quick_sort_channel_logs from utils.settings import settings from utils.print_utils.userlist import print_userlist # maximum number of lines that can be on the screen # is updated every cycle as to allow automatic resizing MAX_LINES = 0 # screen global stdscr stdscr = None global windows windows = [] # buffer to allow for double buffering (stops screen flashing) screen_buffer = [] # text that can be set to be displayed for 1 frame display = "" display_frames = 0 def cursesInit(): stdscr = curses.initscr() curses.noecho() curses.cbreak() stdscr.keypad(True) def cursesDestroy(): curses.nocbreak() stdscr.keypad(False) curses.echo() curses.endwin() def cursesRefresh(): stdscr.noutrefresh() for win in windows: win.noutrefresh() curses.doupdate() async def print_screen(): stdscr.clear() stdscr.addstr("Test") cursesRefresh() ## Get ready to redraw the screen #left_bar_width = await get_left_bar_width() #await clear_screen() #if settings["show_top_bar"]: # await print_top_bar(left_bar_width) #if server_log_tree is not None: # await print_channel_log(left_bar_width) #await print_bottom_bar(left_bar_width) ## Print the buffer containing our message logs #if settings["show_top_bar"]: # if settings["show_separators"]: # with term.location(0, 2): # print("".join(screen_buffer), end="") # else: # with term.location(0, 1): # print("".join(screen_buffer), end="") #else: # with term.location(0, 0): # print("".join(screen_buffer), end="") #if settings["show_left_bar"]: # await print_left_bar(left_bar_width) #global display, display_frames #if display != "": # print(display) # display_frames -= 1 # if display_frames <= 0: # display = "" async def print_top_bar(left_bar_width): topic = "" try: if client.get_current_channel().topic is not None: topic = client.get_current_channel().topic except: # if there is no channel topic, just print the channel name try: topic = client.get_current_channel().name except: pass text_length = term.width - (36 + len(client.get_current_server_name())) if len(topic) > text_length: topic = topic[:text_length] with term.location(1,0): print("Server: " + await get_color(settings["server_display_color"]) \ + client.get_current_server_name() + term.normal, end="") with term.location(term.width // 2 - len(topic) // 2, 0): print(topic, end="") online_text = "Users online: " online_count = str(await client.get_online()) online_length = len(online_text) + len(online_count) with term.location(term.width - online_length - 1, 0): print(await get_color(settings["server_display_color"]) + online_text \ + term.normal + online_count, end="") if settings["show_separators"]: divider = await get_color(settings["separator_color"]) \ + ("─" * term.width) + "\n" + term.normal with term.location(0, 1): print(divider, end="") with term.location(left_bar_width, 1): print(await get_color(settings["separator_color"]) + "┬", end="") async def set_display(string): global display, display_frames loc = term.width - 1 - len(string) escape_chars = "\e" for escape_chars in string: loc = loc - 5 display = term.move(term.height - 1, loc) + string display_frames = 3 async def print_left_bar(left_bar_width): start = 0 if settings["show_top_bar"]: start = 2 if settings["show_separators"]: length = 0 length = term.height - settings["margin"] sep_color = await get_color(settings["separator_color"]) for i in range(start, length): print(term.move(i, left_bar_width) + sep_color + "│" \ + term.normal, end="") # Create a new list so we can preserve the server's channel order channel_logs = [] for servlog in server_log_tree: if servlog.get_server() is client.get_current_server(): for chanlog in servlog.get_logs(): channel_logs.append(chanlog) break channel_logs = quick_sort_channel_logs(channel_logs) # buffer to print buffer = [] count = 1 for log in channel_logs: # don't print categories or voice chats # TODO: this will break on private messages if log.get_channel().type != ChannelType.text: continue text = log.get_name() length = len(text) if settings["number_channels"]: if count <= 9: length += 1 else: length += 2 if length > left_bar_width: if settings["truncate_channels"]: text = text[0:left_bar_width - 1] else: text = text[0:left_bar_width - 4] + "..." if log.get_channel() is client.get_current_channel(): if settings["number_channels"]: buffer.append(term.normal + str(count) + ". " + term.green + text + term.normal + "\n") else: buffer.append(term.green + text + term.normal + "\n") else: if log.get_channel() is not channel_logs[0]: pass if log.get_channel() is not client.get_current_channel(): if log.unread and settings["blink_unreads"]: text = await get_color(settings["unread_channel_color"]) + text + term.normal elif log.mentioned_in and settings["blink_mentions"]: text = await get_color(settings["unread_mention_color"]) + text + term.normal if settings["number_channels"]: buffer.append(term.normal + str(count) + ". " + text + "\n") else: buffer.append(text + "\n") count += 1 # should the server have *too many channels!*, stop them # from spilling over the screen if count - 1 == term.height - 2 - settings["margin"]: break with term.location(0, start): print("".join(buffer)) async def print_bottom_bar(left_bar_width): if settings["show_separators"]: with term.location(0, term.height - 2): print(await get_color(settings["separator_color"]) + ("─" * term.width) \ + "\n" + term.normal, end="") with term.location(left_bar_width, term.height - 2): print(await get_color(settings["separator_color"]) + "┴", end="") bottom = await get_prompt() if len(input_buffer) > 0: bottom = bottom + "".join(input_buffer) with term.location(0, term.height - 1): print(bottom, end="") async def clear_screen(): # This is more efficient cursesRefresh() ## instead of "clearing", we're actually just overwriting ## everything with white space. This mitigates the massive ## screen flashing that goes on with "cls" and "clear" #del screen_buffer[:] #wipe = (" " * (term.width) + "\n") * term.height #print(term.move(0,0) + wipe, end="") async def print_channel_log(left_bar_width): global INDEX # If the line would spill over the screen, we need to wrap it # NOTE: term.width is calculating every time this function is called. # Meaning that this will automatically resize the screen. # note: the "1" is the space at the start MAX_LENGTH = term.width - (left_bar_width + settings["margin"]) - 1 # For wrapped lines, offset them to line up with the previous line offset = 0 # List to put our *formatted* lines in, once we have OK'd them to print formatted_lines = [] # the max number of lines that can be shown on the screen MAX_LINES = await get_max_lines() for server_log in server_log_tree: if server_log.get_server() is client.get_current_server(): for channel_log in server_log.get_logs(): if channel_log.get_channel() is client.get_current_channel(): # if the server has a "category" channel named the same # as a text channel, confusion will occur # TODO: private messages are not "text" channeltypes if channel_log.get_channel().type != ChannelType.text: continue for msg in channel_log.get_logs(): # The lines of this unformatted message msg_lines = [] HAS_MENTION = False if "@" + client.get_current_server().me.display_name in msg.clean_content: HAS_MENTION = True author_name = "" try: author_name = msg.author.display_name except: try: author_name = msg.author.name except: author_name = "Unknown Author" author_name_length = len(author_name) author_prefix = await get_role_color(msg) + author_name + ": " color = "" if HAS_MENTION: color = await get_color(settings["mention_color"]) else: color = await get_color(settings["text_color"]) proposed_line = author_prefix + color + msg.clean_content.strip() # If our message actually consists of # of multiple lines separated by new-line # characters, we need to accomodate for this. # --- Otherwise: msg_lines will just consist of one line msg_lines = proposed_line.split("\n") for line in msg_lines: # strip leading spaces - LEFT ONLY line = line.lstrip() # If our line is greater than our max length, # that means the author has a long-line comment # that wasn't using new line chars... # We must manually wrap it. line_length = len(line) # Loop through, wrapping the lines until it behaves while line_length > MAX_LENGTH: line = line.strip() # Take a section out of the line based on our max length sect = line[:MAX_LENGTH - offset] # Make sure we did not cut a word in half sect = sect[:sect.strip().rfind(' ')] # If this section isn't the first line of the comment, # we should offset it to better distinguish it offset = 0 if author_prefix not in sect: if line is not msg_lines[0]: offset = author_name_length + settings["margin"] # add in now formatted line! formatted_lines.append(Line(sect.strip(), offset)) # since we just wrapped a line, we need to # make sure we don't overwrite it next time # Split the line between what has been formatted, and # what still remains needing to be formatted if len(line) > len(sect): line = line.split(sect)[1] # find the "real" length of the line, by subtracting # any escape characters it might have. It would # be wasteful to loop through all of the possibilities # so instead we will simply subtract the length # of the shortest for each that it has. line_length = len(line) target = "\e" for target in line: line_length -= 5 # Once here, the string was either A: already short enough # to begin with, or B: made through our while loop and has # since been chopped down to less than our MAX_LENGTH if len(line.strip()) > 0: offset = 0 if author_prefix not in line: offset = author_name_length + settings["margin"] formatted_lines.append(Line(line.strip(), offset)) # where we should start printing from # clamp the index as not to show whitespace if channel_log.get_index() < MAX_LINES: channel_log.set_index(MAX_LINES) elif channel_log.get_index() > len(formatted_lines): channel_log.set_index(len(formatted_lines)) # ----- Trim out list to print out nicely ----- # # trims off the front of the list, until our index del formatted_lines[0:(len(formatted_lines) - channel_log.get_index())] # retains the amount of lines for our screen, deletes remainder del formatted_lines[MAX_LINES:] # if user does not want the left bar, do not add margin space = " " if not settings["show_left_bar"]: space = "" # add to the buffer! for line in formatted_lines: screen_buffer.append(space * (left_bar_width + \ settings["margin"] + line.offset) + line.text + "\n") # return as not to loop through all channels unnecessarily return ================================================ FILE: ui/ui_utils.py ================================================ from utils.globals import get_color, gc from utils.settings import settings async def get_prompt(): left = await get_color(settings["prompt_border_color"]) + "[" right = await get_color(settings["prompt_border_color"]) + "]: " + gc.term.normal middle = "" if gc.client.get_prompt() == settings["default_prompt"]: middle = " " + await get_color(settings["prompt_color"]) + settings["default_prompt"] + " " else: middle = await get_color(settings["prompt_hash_color"]) + "#" \ + await get_color(settings["prompt_color"]) + gc.client.get_prompt() return left + middle + right async def get_max_lines(): num = 0 if settings["show_top_bar"] and settings["show_separators"]: num = gc.term.height - settings["margin"] * 2 elif settings["show_top_bar"] and not settings["show_separators"]: num = gc.term.height - settings["margin"] elif not settings["show_top_bar"] and not settings["show_separators"]: num = gc.term.height - 1 return num async def get_left_bar_width(): if not settings["show_left_bar"]: return 0 left_bar_width = gc.term.width // settings["left_bar_divider"] if left_bar_width < 8: return 8 else: return left_bar_width async def get_role_color(msg): color = "" try: r = msg.author.top_role.name.lower() for role in settings["custom_roles"]: if r == role["name"].lower(): color = await get_color(role["color"]) if color is not "": # The user must have already been assigned a custom role pass elif settings["normal_user_color"] is not None: color = await get_color(settings["normal_user_color"]) else: color = gc.term.green # if this fails, the user either left or was banned except: if settings["normal_user_color"] is not None: color = await get_color(settings["normal_user_color"]) else: color = gc.term.green return color ================================================ FILE: utils/globals.py ================================================ from sys import exit from blessings import Terminal from utils.settings import settings import sys NO_SETTINGS=False try: if sys.argv[1] == "--store-token" or sys.argv[1] == "--token": NO_SETTINGS=True except IndexError: pass class GlobalsContainer: def __init__(self): self.term = Terminal() self.client = None self.server_log_tree = [] self.input_buffer = [] self.user_input = "" self.channels_entered = [] def initClient(self): from client.client import Client if NO_SETTINGS: messages=100 else: messages=settings["max_messages"] self.client = Client(max_messages=messages) gc = GlobalsContainer() # kills the program and all its elements gracefully def kill(): # attempt to cleanly close our loops import asyncio try: gc.client.close() except: pass try: asyncio.get_event_loop().close() except: pass try:# since we're exiting, we can be nice and try to clear the screen from os import system system("clear") except: pass exit() # returns a "Channel" object from the given string async def string2channel(channel): for srv in gc.client.servers: if srv.name == channel.server.name: for chan in srv.channels: if chan.name == channel: return chan # returns a "Channellog" object from the given string async def get_channel_log(channel): for srvlog in gc.server_log_tree: if srvlog.get_name().lower() == channel.server.name.lower(): for chanlog in srvlog.get_logs(): if chanlog.get_name().lower() == channel.name.lower(): return chanlog # returns a "Channellog" from a given "Channel" async def chan2log(chan): for srvlog in gc.server_log_tree: if srvlog.get_name().lower() == chan.server.name.lower(): for clog in srvlog.get_logs(): if clog.get_name().lower() == chan.name.lower(): return clog # returns a "Serverlog" from a given "Server" async def serv2log(serv): for srvlog in gc.server_log_tree: if srvlog.get_name().lower() == serv.name.lower(): return srvlog # takes in a string, returns the appropriate term.color async def get_color(string): arg = string.strip().lower() if arg == "white": return gc.term.white if arg == "black": return gc.term.black if arg == "red": return gc.term.red if arg == "blue": return gc.term.blue if arg == "yellow": return gc.term.yellow if arg == "cyan": return gc.term.cyan if arg == "magenta": return gc.term.magenta if arg == "green": return gc.term.green if arg == "on_white": return gc.term.on_white if arg == "on_black": return gc.term.on_black if arg == "on_red": return gc.term.on_red if arg == "on_blue": return gc.term.on_blue if arg == "on_yellow": return gc.term.on_yellow if arg == "on_cyan": return gc.term.on_cyan if arg == "on_magenta": return gc.term.on_magenta if arg == "on_green": return gc.term.on_green if arg == "blink_white": return gc.term.blink_white if arg == "blink_black": return gc.term.blink_black if arg == "blink_red": return gc.term.blink_red if arg == "blink_blue": return gc.term.blink_blue if arg == "blink_yellow": return gc.term.blink_yellow if arg == "blink_cyan": return gc.term.blink_cyan if arg == "blink_magenta": return gc.term.blink_magenta if arg == "blink_green": return gc.term.blink_green # if we're here, someone has one of their settings.yaml # colors defined wrong. We'll be nice and just return white. return gc.term.normal + gc.term.white ================================================ FILE: utils/hidecursor.py ================================================ from sys import stdout # completely hides the system cursor async def hide_cursor(): stdout.write("\033[?25l") stdout.flush() ================================================ FILE: utils/print_utils/channellist.py ================================================ from os import system from discord import ChannelType from ui.ui import clear_screen, set_display from utils.globals import gc async def print_channellist(): if len(gc.client.servers) == 0: set_display(gc.term.red + "Error: You are not in any servers.") return if len(gc.client.get_current_server().channels) == 0: set_display(gc.term.red + "Error: Does this server not have any channels?" + gc.term.normal) return buffer = [] for channel in gc.client.get_current_server().channels: if channel.type == ChannelType.text: name = channel.name name = name.replace("'", "") name = name.replace('"', "") name = name.replace("`", "") name = name.replace("$(", "") buffer.append(name + "\n") await clear_screen() system("echo '" + gc.term.cyan + "Available Channels in " \ + gc.term.magenta + gc.client.get_current_server_name() + ": \n" \ + "---------------------------- \n \n" \ + gc.term.yellow + "".join(buffer) \ + gc.term.green + "~ \n" \ + gc.term.green + "~ \n" \ + gc.term.green + "(press \'q\' to quit this dialog) \n" \ + "' | less -R") ================================================ FILE: utils/print_utils/emojis.py ================================================ from os import system from ui.ui import clear_screen, set_display from utils.globals import gc, get_color from utils.settings import settings async def print_emojilist(): if len(gc.client.servers) == 0: set_display(gc.term.red + "Error: You are not in any servers." + gc.term.normal) return server_name = gc.client.get_current_server_name() server_name = server_name.replace("'", "") server_name = server_name.replace('"', "") server_name = server_name.replace("`", "") server_name = server_name.replace("$(", "") emojis = [] server_emojis = "" try: server_emojis = gc.client.get_current_server().emojis except: pass if server_emojis is not None and server_emojis != "": for emoji in server_emojis: name = emoji.name name = name.replace("'", "") name = name.replace('"', "") name = name.replace("`", "") name = name.replace("$(", "") emojis.append(gc.term.yellow + ":" + name + ":" + "\n") await clear_screen() system("echo '" + gc.term.magenta + "Available Emojis in: " + gc.term.cyan + server_name +"\n" + gc.term.normal \ + "---------------------------- \n" \ + "".join(emojis) \ + gc.term.green + "~ \n" \ + gc.term.green + "~ \n" \ + gc.term.green + "(press q to quit this dialog) \n" \ + "' | less -R") ================================================ FILE: utils/print_utils/help.py ================================================ from os import system def print_help(gc): system("clear") system("echo '" + gc.term.normal \ + gc.term.green("Launch Arguments: \n") + gc.term.red \ + "--------------------------------------------- \n" \ + get_line(gc, "--copy-skeleton", " --- ", "copies template settings") \ + gc.term.cyan("This file can be found at ~/.config/Discline/config \n") \ + "\n" + get_line(gc, "--store-token", " --- ", "stores your token") \ + gc.term.cyan("This file can be found at ~/.config/Discline/token \n") \ + "\n" + get_line(gc, "--config", " --- ", "specify a specific config path") \ + "\n" + gc.term.green("Available Commands: \n") + gc.term.red \ + "--------------------------------------------- \n" \ + get_line(gc, "/channel", " - ", "switch to channel - (alias: 'c')") \ + get_line(gc, "/server", " - ", "switch server - (alias: 's')") \ + gc.term.cyan + "Note: these commands can now fuzzy-find! \n" \ + "\n" \ + get_line(gc, "/servers", " - ", "list available servers") \ + get_line(gc, "/channels", " - ", "list available channels") \ + get_line(gc, "/users", " - ", "list servers users") \ + get_line(gc, "/emojis", " - ", "list servers custom emojis") \ + "\n" \ + get_line(gc, "/nick", " - ", "change server nick name") \ + get_line(gc, "/game", " - ", "change your game status") \ + get_line(gc, "/file", " - ", "upload a file via path") \ + get_line(gc, "/status", " - ", "change online presence") \ + gc.term.cyan + "This can be either 'online', 'offline', 'away', or 'dnd' \n" \ + gc.term.cyan + "(dnd = do not disturb) \n" \ + "\n" \ + get_line(gc, "/cX", " - ", "shorthand to change channel (Ex: /c1)") \ + gc.term.cyan("This can be configured to start at 0 in your config") \ + "\n" \ + "\n" \ + get_line(gc, "/quit", " - ", "exit cleanly") \ + "\n \n" \ + gc.term.magenta + "Note: You can send emojis by using :emojiname: \n" \ + gc.term.cyan("Nitro emojis do work! Make sure you have \n") \ + gc.term.cyan("nitro enabled in your config. \n") \ + "\n" + gc.term.yellow + "You can scroll up/down in channel logs \n" \ + gc.term.yellow + "by using PageUp/PageDown. \n" \ + gc.term.green + "~ \n" \ + gc.term.green + "~ \n" \ + gc.term.green + "~ \n" \ + gc.term.green + "(press q to quit this dialog)" \ + "' | less -R") def get_line(gc, command, div, desc): return gc.term.yellow(command) + gc.term.cyan(div) + gc.term.normal + desc + "\n" ================================================ FILE: utils/print_utils/print_utils.py ================================================ import discord from utils.globals import gc async def print_servers(): print("Available servers: ") print_line_break(); for server in gc.client.servers: print(server.name) async def print_user(): print('Logged in as: ' + gc.term.green + gc.client.user.name + gc.term.normal) async def print_line_break(): print("-" * int(gc.term.width * 0.45)) async def print_channels(server): print("Available channels:") print_line_break(); for channel in server.channels: print(channel.name) ================================================ FILE: utils/print_utils/serverlist.py ================================================ from os import system from ui.ui import clear_screen, set_display from utils.globals import get_color, gc from utils.settings import settings async def print_serverlist(): if len(gc.client.servers) == 0: set_display(gc.term.red + "Error: You are not in any servers." + gc.term.normal) return buffer = [] for slog in gc.server_log_tree: name = slog.get_name() name = name.replace("'", "") name = name.replace('"', "") name = name.replace("`", "") name = name.replace("$(", "") if slog.get_server() is gc.client.get_current_server(): buffer.append(await get_color(settings["current_channel_color"]) + name + gc.term.normal + "\n") continue string = "" for clog in slog.get_logs(): if clog.mentioned_in: string = await get_color(settings["unread_mention_color"]) + name + gc.term.normal + "\n" break elif clog.unread: string = await get_color(settings["unread_channel_color"]) + name + gc.term.normal + "\n" break if string == "": string = await get_color(settings["text_color"]) + name + gc.term.normal + "\n" buffer.append(string) await clear_screen() system("echo '" + gc.term.magenta + "Available Servers: \n" + gc.term.normal \ + "---------------------------- \n \n" \ + "".join(buffer) \ + gc.term.green + "~ \n" \ + gc.term.green + "~ \n" \ + gc.term.green + "(press q to quit this dialog) \n" \ + "' | less -R") ================================================ FILE: utils/print_utils/userlist.py ================================================ from os import system from discord import Status from utils.globals import gc # On call of the /users command, this will print # out a nicely sorted, colored list of all users # connected to the clients current server and pipe # it to the system pager, (in this case `less`) class UserList: def __init__(self): # place to store the names, separted in categories self.online = [] self.offline = [] self.idle = [] self.dnd = [] def add(self, member, tag): listing = member.name + tag + " \n" if member.status is Status.online: self.online.append(listing) elif member.status is Status.offline: self.offline.append(listing) elif member.status is Status.idle: self.idle.append(listing) elif member.status is Status.dnd: self.dnd.append(listing) def sort(self): self.online = sorted(self.online, key=str.lower) self.offline = sorted(self.offline, key=str.lower) self.idle = sorted(self.idle, key=str.lower) self.dnd = sorted(self.dnd, key=str.lower) # now they are sorted, we can colorize them # we couldn't before as the escape codes mess with # the sorting algorithm tmp = [] for name in self.online: tmp.append(gc.term.green + name) self.online = list(tmp) del tmp[:] for name in self.idle: tmp.append(gc.term.yellow + name) self.idle = list(tmp) del tmp[:] for name in self.dnd: tmp.append(gc.term.black + name) self.dnd = list(tmp) del tmp[:] for name in self.offline: tmp.append(gc.term.red + name) self.offline = list(tmp) del tmp[:] return "".join(self.online) + "".join(self.offline) \ + "".join(self.idle) + "".join(self.dnd) async def print_userlist(): if len(gc.client.servers) == 0: print("Error: You are not in any servers.") return if len(gc.client.get_current_server().channels) == 0: print("Error: Does this server not have any channels?") return # lists to contain our "Member" objects nonroles = UserList() admins = UserList() mods = UserList() bots = UserList() everything_else = UserList() for member in gc.client.get_current_server().members: if member is None: continue # happens if a member left the server if member.top_role.name == "admin" or member.top_role.name == "Admin": admins.add(member, " - (Admin)") elif member.top_role.name == "mod" or member.top_role.name == "Mod": mods.add(member, "- (Mod)") elif member.top_role.name == "bot" or member.top_role.name == "Bot": bots.add(member, " - (bot)") elif member.top_role.is_everyone: nonroles.add(member, "") else: everything_else.add(member, " - " + member.top_role.name) # the final buffer that we're actually going to print buffer = [] if admins is not None: buffer.append(admins.sort()) if mods is not None: buffer.append(mods.sort()) buffer.append("\n" + gc.term.magenta + "---------------------------- \n\n") if bots is not None: buffer.append(bots.sort()) if everything_else is not None: buffer.append(everything_else.sort()) buffer.append("\n" + gc.term.magenta + "---------------------------- \n\n") if nonroles is not None: buffer.append(nonroles.sort()) buffer_copy = [] for name in buffer: name = name.replace("'", "") name = name.replace('"', "") name = name.replace("`", "") name = name.replace("$(", "") buffer_copy.append(name) system("echo '" + gc.term.yellow + "Members in " \ + gc.client.get_current_server().name + ": \n" \ + gc.term.magenta + "---------------------------- \n \n" \ + "".join(buffer_copy) \ + gc.term.green + "~ \n" \ + gc.term.green + "~ \n" \ + gc.term.green + "(press \'q\' to quit this dialog) \n" \ # NOTE: the -R flag here enables color escape codes + "' | less -R") # takes in a member, returns a color based on their status def get_status_color(member): if member.status is Status.online: return gc.term.green if member.status is Status.idle: # aka "away" return gc.term.yellow if member.status is Status.offline: return gc.term.red if member.status is Status.dnd: # do not disturb return gc.term.black # if we're still here, something is wrong return "ERROR: get_status_color() has returned 'None' for " \ + member.name + "\n" ================================================ FILE: utils/quicksort.py ================================================ def quick_sort_channel_logs(channel_logs): # sort channels to match the server's default chosen positions if len(channel_logs) <= 1: return channel_logs else: return quick_sort_channel_logs([e for e in channel_logs[1:] \ if e.get_channel().position <= channel_logs[0].get_channel().position]) + \ [channel_logs[0]] + quick_sort_channel_logs([e for e in channel_logs[1:] \ if e.get_channel().position > channel_logs[0].get_channel().position]) ================================================ FILE: utils/settings.py ================================================ import os import sys from yaml import safe_load from blessings import Terminal settings = "" def copy_skeleton(): term = Terminal() try: from shutil import copyfile if not os.path.exists(os.getenv("HOME") + "/.config/Discline"): os.mkdir(os.getenv("HOME") + "/.config/Discline") if os.path.exists(os.getenv("HOME") + "/.config/Discline/config"): try: os.remove(os.getenv("HOME") + "/.config/Discline/config") except: pass copyfile("res/settings-skeleton.yaml", os.getenv("HOME") + "/.config/Discline/config", follow_symlinks=True) print(term.green("Skeleton copied!" + term.normal)) print(term.cyan("Your configuration file can be found at ~/.config/Discline")) except KeyboardInterrupt: print("Cancelling...") quit() except SystemExit: quit() except: print(term.red("Error creating skeleton file.")) quit() def load_config(path): global settings with open(path) as f: settings = safe_load(f) arg = "" try: arg = sys.argv[1] except IndexError: pass if arg == "--store-token" or arg == "--token": pass elif arg == "--skeleton" or arg == "--copy-skeleton": copy_skeleton() quit() elif arg == "--config": try: load_config(sys.argv[2]) except IndexError: print("No path provided?") quit() except: print("Invalid path to config entered.") quit() else: try: load_config(os.getenv("HOME") + "/.config/Discline/config") except: try: load_config(os.getenv("HOME") + "/.Discline") except: print(term.red("ERROR: could not get settings.")) quit() ================================================ FILE: utils/token_utils.py ================================================ import os from utils.globals import gc def get_token(): if os.path.exists(os.getenv("HOME") + "/.config/Discline/token"): token = "" try: f = open(os.getenv("HOME") + "/.config/Discline/token", "r") token = f.read() f.close() except: pass if token != "": return token from blessings import Terminal gc.term = Terminal() print("\n" + gc.term.red("Error reading token.")) print("\n" + gc.term.yellow("Are you sure you stored your token?")) print(gc.term.yellow("Use --store-token to store your token.")) quit() def store_token(): import sys from blessings import Terminal token = "" try: token=sys.argv[2] except IndexError: print(Terminal().red("Error: You did not specify a token!")) quit() if not os.path.exists(os.getenv("HOME") + "/.config/Discline"): os.mkdir(os.getenv("HOME") + "/.config/Discline") if token is not None and token != "": # trim off quotes if user added them token = token.strip('"') token = token.strip("'") # ------- Token format seems to vary, disabling this check for now -------- # # if token is None or len(token) < 59 or len(token) > 88: # print(Terminal().red("Error: Bad token. Did you paste it correctly?")) # quit() # ------------------------------------------------------------------------- # try: f = open(os.getenv("HOME") + "/.config/Discline/token", "w") f.write(token) f.close() print(Terminal().green("Token stored!")) except: print(Terminal().red("Error: Could not write token to file.")) quit() ================================================ FILE: utils/updates.py ================================================ def check_for_updates(): from utils.globals import gc from os import path if not path.exists(".git"): print(gc.term.red("Error: client not started from repo location! Cancelling...")) print(gc.term.yellow("You must start the client from its folder to get automatic updates. \n")) return try:# git pull at start as to automatically update to master repo from subprocess import Popen,PIPE print(gc.term.green + "Checking for updates..." + gc.term.normal) process = Popen(["git", "pull", "--force"], stdout=PIPE) output = process.communicate()[0].decode('utf-8').strip() if "Already up to date" not in output: # print(gc.term.yellow("Updates downloaded! Please restart.")) print("\n \n") # This quit() call is breaking the client on MacOS and Linux Mint # The if statement above is being triggered, even when the output IS # "Already up to date". Why is this happening? # quit() else: print("Already up to date!" + "\n") except KeyboardInterrupt: print("Call to cancel update received, skipping.") except SystemExit: pass except OSError: # (file not found) # They must not have git installed, no automatic updates for them! print(gc.term.red + "Error fetching automatic updates! Do you \ have git installed?" + gc.term.normal) except: print(gc.term.red + "Unkown error occurred during retrieval \ of updates." + gc.term.normal)