[
  {
    "path": ".circleci/config.yml",
    "content": "version: 2.1\norbs:\n  python: circleci/python@1.0.0\n\njobs:\n  build:\n    executor:\n      name: python/default\n      tag: \"3.9\"\n    steps:\n      - checkout\n      - python/install-packages:\n          args: pytest\n          pkg-manager: pipenv\n      # NEED TO FIX THE STUPID TESTS\n      # - run: pipenv run coverage run --source=./src -m pytest --junitxml=./junit/junit.xml\n      # - run: pipenv run coverage report\n      # - run: pipenv run coverage html\n      - store_artifacts:\n          path: ./htmlcov\n          destination: htmlcov\n      - store_test_results:\n          path: ./junit\n\nworkflows:\n  main:\n    jobs:\n      - build\n"
  },
  {
    "path": ".gitignore",
    "content": "*.pyc\n*.db*\n*.log*\n*.info\nvenv\nsettings.py\n.DS_Store\nmacos.sh\n.vscode\nsrc/db.json\nnode_modules\n.pytest_cache\n.venv\n.env\n__pycache__\n.coverage\nhtmlcov\njunit\nbrainss\nconfig.json\n.envv"
  },
  {
    "path": "Dockerfile",
    "content": "FROM python:3.8\n\nCOPY . /app\nWORKDIR /app\nRUN apt update && apt install -yqq g++ gcc libc6-dev make pkg-config libffi-dev python3-dev git\nRUN pip3 install pipenv\nRUN pipenv install --system --deploy --ignore-pipfile\nRUN chmod +x /app/run_linux.sh\nENTRYPOINT /app/run_linux.sh\n\n\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 MrPowerScripts\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Pipfile",
    "content": "[[source]]\nname = \"pypi\"\nurl = \"https://pypi.org/simple\"\nverify_ssl = true\n\n[dev-packages]\n\n[packages]\npsaw = \"*\"\npraw = \"*\"\npytest = \"*\"\ncoverage = \"*\"\ncobe = \"*\"\nurwide = \"*\"\npyfiglet = \"*\"\nurwid = \"*\"\nrequests = \"*\"\n"
  },
  {
    "path": "README.md",
    "content": "**This project is no longer in active development. I'll consider merging pull requests as time is available, but there will not be further significant updates to it.**\n\nAnd one final message to cap off the project: This is a fun bot that may help you learn about Python. There are plenty of things that need to be fixed, and I hope the logging is good enough to help guide you in solving any issues that still exist. The purpose of this project was to demonstrate and bring awareness to the widespread practice of botting on social media sites. Reddit in this case, but it happens on all other social media platforms too. Bots are used for the purposes of [astrofurfing](https://www.merriam-webster.com/dictionary/astroturfing), and many other practices to program your beliefs by controlling the things you encounter while surfing the internet. It's not unique to social media. It happens to anything that people view as an authentic source of truth. TV before the internet, and [newspapers before that](https://en.wikipedia.org/wiki/Propaganda_of_the_Spanish%E2%80%93American_War#:~:text=Pulitzer%20owned%20the%20New%20York,into%20the%20Spanish%E2%80%93American%20War.). People [build entire businesses](https://www.inc.com/alyssa-satara/if-you-dont-fully-understand-cambridge-analytica-scandal-read-this-simplified-version.html) with the intent to persuade you, or cause you to feel a certain way about subjects. Usually, not for serving your own best interests or the interests of others around you. If you're reading this you probably don't have the money or power to use those services. So, that puts you at a pretty big disadvantage of getting your voice heard, doesn't it? Even if a thousand people are screaming it's not hard to drown those voices out with tens of thousands of bots. It [happens every day](https://www.wired.com/story/bots-broke-fcc-public-comment-system/). Hopefully, with more awareness around these practices it will provoke people to think more critically about where they source truths from. With greater trust and shared understanding it can to more equitable outcomes for people regardless of who they are or where they're from. Well, at least for people [outside of the club](https://www.youtube.com/watch?v=Nyvxt1svxso) who can't afford bot farms. If you aren't questioning where you get your truths from, today is a great day to start! You can avoid being the pawn of election fraud scehems, or causing [far more serious damage](https://www.reddit.com/r/4chan/comments/2gup17/4chan_does_it_again_microwave_chargin_with_ios_8/). Although microwaving your phone may be a solution in this case.\n\n# Reddit Karma Farming Bot\n\n## Videos and links\n\nThis bot is probably the reason you saw that post again on Reddit. Need help with the bot? Join us on Discord https://bit.ly/mrps-discord \n\n![farm karma 1](https://user-images.githubusercontent.com/1307942/86540032-7e1a2c00-bef9-11ea-9266-16830c5b9dfa.png)\n![farm karma bot](https://user-images.githubusercontent.com/1307942/86153469-a40a8f80-baf9-11ea-80b5-d86dd31108d6.png)\n\n### Video install guides\n[Windows](https://youtu.be/6ICjZUHO2_I)\n\n[Linux/macOS](https://youtu.be/ga0OC6lYSRs)\n\n### 2020 update videos\n\n[Definitely Watch This One](https://www.youtube.com/watch?v=nWYRGXesb3I)\n\n[2020 Bot 3.0 Code Walkthrough](https://www.youtube.com/watch?v=83zWIz3b7o0)\n\n### Older videos\n\n[Karma Farming Bot 2.0 Video](https://www.youtube.com/watch?v=CCMGHepPBso)  \n[Karma Farming on Reddit Video](https://www.youtube.com/watch?v=8DrOERA5FGc)  \n[Karma Farming Bot 1.0 Video](https://www.youtube.com/watch?v=KgWsqKkDEtI)  \n\nSubscribe: http://bit.ly/mrps-yt-sub  \nWebsite: https://bit.ly/mrps-site  \n\n## Getting Started\n\n1. Follow the [getting started guide](docs/1-getting-started.md) to create your Reddit app and learn how to configure the bot.\n\n2. Then follow the [macOS/Linux](docs/2-linux-macos.md), or [Windows](docs/3-windows.md) or [docker](docs/4-docker-guide.md) guides to start the bot after everything is set up.\n\n## Features\n\n- Run on Linux, MacOS, or Windows.\n- Automatically reposts popular posts from the past to earn post karma.\n- Automatically generates unique (somewhat) contextually relevant comments using [cobe](https://github.com/pteichman/cobe).\n- Automatically deletes poor performing comments and posts.\n- Configurable frequency of posting, commenting, and other actions.\n- Filter the bot from learning certain words, or avoid certain subreddits.\n- Schedule when the bot wakes up and sleeps to run actions.\n- Auto detects if the account is shadowbanned.\n\n## Warnings\n\n### Reddit\n\nNew Reddit accounts will likely get banned with the bot. Let an account sit for a few days before using it. Do not use an account that you love, as it's possible to be permanently banned.\n\n### Heroku\n\nThe bot used to have a Heroku option - till they found out and now using the bot on heroku will get your account banned.\n\n"
  },
  {
    "path": "deps/windows/windows.ps1",
    "content": "$install_help = \"Read windows installation guide https://github.com/MrPowerScripts/reddit-karma-farming-bot/blob/master/docs/3-windows.md\"\n\nif (!(get-command python)) {\n  write-host \"Python not found\"\n  write-host $install_help\n  exit 1\n} else {write-host \"Python found!\"}\n\n# make sure visual studio C++ build tools\nif (Test-Path -Path \"C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\BuildTools\") {\n  Write-Host \"Found VS 2019 Build Tools - Excellent\"\n} else {\n  Write-Host \"VS 2019 Build Tools not found\"\n  Write-Host $install_help\n  exit 1\n}\n\n#check for PyStemmer\nif (Test-Path -Path \"./deps/windows/PyStemmer-2.0.1-cp39-cp39-win_amd64.whl\") {\n  Write-Host \"Found PyStemmer\"\n} else {\n  Invoke-WebRequest -Uri https://download.lfd.uci.edu/pythonlibs/z4tqcw5k/PyStemmer-2.0.1-cp39-cp39-win_amd64.whl -OutFile ./deps/windows/PyStemmer-2.0.1-cp39-cp39-win_amd64.whl\n}\n\n#check if pipenv is installed\nif (!(get-command pipenv)) {\n  write-host \"Pipenv not found - installing\"\n  & pip3 install pipenv\n  exit 1\n} else {write-host \"Pipenv found!\"}\n\n#check for pipenv dependencies\nif (Test-Path -Path \"./.venv\") {\n  Write-Host \"Pipenv deps installed\"\n} else {\n  & pip3 install ./deps/windows/PyStemmer-2.0.1-cp39-cp39-win_amd64.whl\n  & pipenv install\n}\n"
  },
  {
    "path": "docs/1-getting-started.md",
    "content": "# Getting Started\n\n## Creating the Reddit app\n\nIn your browser, when you are logged in with the Reddit account you want to use, go to this URL: https://www.reddit.com/prefs/apps\n\nOnce there, click the “are you a developer? create an app...” button at the bottom. Name the app anything you want, I recommend not naming it Reddit Karma bot. Instead, go for something generic like “Test script”. Then **CLICK THE SCRIPT OPTION**, this is important.\n\nYou can leave the description and about URL empty, but you need to put a value for the redirect URI. This can be anything as long as it is a valid URL. I recommend doing something similar to http://example.com or http://nourl.com. \n\nBut like I said, it can be anything.\n\nThen click Create App.\nYou will now be presented with this screen:\n\n![app_example](https://user-images.githubusercontent.com/29954899/103455850-f8810880-4cf0-11eb-9002-64c2f1e5a44e.png)\n\nIn this image, you will find your client id and secret. The red highlight is your client id, and cyan is your secret key. Now we are ready to get the bot up and running!\n\n## Using a proxy\n\nThe bot uses the Python `requests` library behind the scenes. Python `requests` library [has some enviroment variables you can set](https://stackoverflow.com/a/8287752) to have it automatically use a proxy server.\n\n## Reddit Configuration\n\n### How to configure the Reddit bot\n\nThe bot has many configuration options, and some are enabled/disabled by default. View all of the config options in the [src/config](/src/config) folder.\n\n#### Limit to specific subreddits\n\nAdd subreddits to the `REDDIT_APPROVED_SUBS` variable [reddit_sub_lists.py](/src/config/reddit/reddit_sub_lists.py) file. This will limit the bot to only repost/learn/comment to these subreddits.\n\n#### Avoid specific subreddits\n\nAdd the subreddits the bot should avoid to [reddit_avoid_subs.txt](/src/config/reddit/reddit_avoid_subs.txt) file, and the bot will ignore posting/commenting to these subreddits. Do not include `/r/`, just the clean subreddit name on each line of the file.\n\n#### Avoid specific words\n\nAdd words the the bot should avoid to [reddit_avoid_words.txt](/src/config/reddit/reddit_avoid_words.txt) file, and the bot will ignore learning from comments, or reposting posts that include these words. Add a words on separate lines.\n\n#### Configure what actions the Reddit bot performs\n\nThe reddit bot actions can be configured in [reddit_config.py](/src/config/reddit_config.py). If you don't want it to perform an action set the chance value to `0`. For instance, to disable commenting set `\"reddit_comment_chance\": 0.005,` to `\"reddit_comment_chance\": 0,`. Increasing the chance values will increase the chance the action is performed. The defualt values are fine, but you can experiment.\n\n##### Sleep schedule\n\nThe bot has a sleep schedule enabled by default, otherwise it will comment/post 24/7 and likely get banned. You can disable the sleep schedule by removing all schedule values. Like `\"reddit_sleep_schedule\": [2, 4]` to `\"reddit_sleep_schedule\": []`.\n\n#### Configure Cobe\n\nCobe is the library the bot uses to generate comments. You may want to conifgure how big the comment databse needs to be before it starts commeting. You can adjust the values in [cobe_config.py](/src/config/cobe_config.py).\n"
  },
  {
    "path": "docs/2-linux-macos.md",
    "content": "# Running the bot on Linux and Macos\n\nRun the `run_linux.sh` script in the root of the repo. This script was designed and tested on Ubuntu 20, so that's what you should be using. It should also work on the latest version of macOS.\n\nThe script will install all OS level dependencies required by the bot, as well as installing python dependencies into a vitual enviroment. After everything has been installed it will automatically run the bot.\n"
  },
  {
    "path": "docs/3-windows.md",
    "content": "# Running the bot on Windows 10\n\n1. Download and install Python for Windows. You can find [all the releases here](https://www.python.org/downloads/windows/). Make sure you have at least v3.7. You can [click here](https://www.python.org/ftp/python/3.9.1/python-3.9.1-amd64.exe) to download the installer for 3.9.1 directly. **Make sure to check the \"add python to PATH\" option** or the script won't be able to find it.\n\n2. Download the Visual Studio 2019 Build tools [from this link](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools&rel=16). Then install the C++ build tools workload. You can disable all optional packages except for the `windows 10 SDK`, `C++ CMake tools for Windows`, and `MSVC v142 VS2019 C++ x64/x86 build tools`. See the screenshot below, and looked at the options checked on the right side. That should be all you need.\n\n![image](https://user-images.githubusercontent.com/1307942/104216961-a77cbd00-5432-11eb-9aec-c56fcef58d2f.png)\n\n3. Run `run_windows.bat`\n"
  },
  {
    "path": "docs/4-docker-guide.md",
    "content": "# Running the bot in Docker\n\n## Update env file with your credentials\nupdate and rename .env.example to .env\n\n## Build docker image\nFrom the root of the project, run this docker build command: `docker build -t reddit_karma_bot:latest . --no-cache`\n\n## Run Docker Image\n`docker run -d --name=reddit-bot reddit_karma_bot:latest`\n\n## View Logs\n`docker logs -f reddit-bot`\n"
  },
  {
    "path": "run_linux.sh",
    "content": "#!/usr/bin/env bash\n\nDEBUG_FILE=\"./run_linux.log\"\nexport PIPENV_VENV_IN_PROJECT=1\n\ndate '+%d/%m/%Y %H:%M:%S' | tee $DEBUG_FILE\n\nunameOut=\"$(uname -s)\"\ncase \"${unameOut}\" in\n    Linux*)     machine=Linux;;\n    Darwin*)    machine=Mac;;\n    CYGWIN*)    machine=Cygwin;;\n    MINGW*)     machine=MinGw;;\n    *)          machine=\"UNKNOWN:${unameOut}\"\nesac\necho \"system is ${machine}\" | tee -a $DEBUG_FILE\n\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" >/dev/null 2>&1 && pwd )\"\n\nif [ ! -d \"$DIR/.venv\" ]; then\n  echo \"no virtualenv detected doing setup before running\" | tee -a $DEBUG_FILE\n  echo \"need to install dependencies\" | tee -a $DEBUG_FILE\n  if [ \"${machine}\" =  \"Linux\" ]; then\n    echo \"this is linux - install linux deps\"\n    apt-get update || { echo 'apt-get failed failed' | tee -a $DEBUG_FILE ; exit 1; }\n\n    apt-get install -y --no-install-recommends \\\n      g++ \\\n      gcc \\\n      libc6-dev \\\n      make \\\n      pkg-config \\\n      libffi-dev \\\n      python3.6 \\\n      python3-pip \\\n      python3-setuptools \\\n      python3-dev \\\n      git || { echo 'Installing dependencies failed' | tee -a $DEBUG_FILE ; exit 1; }\n  elif [ \"${machine}\" =  \"Mac\" ]; then\n    $(xcode-select -p) || xcode-select --install\n  else\n    echo \"No suitable linux version!\" | tee -a $DEBUG_FILE\n  fi\n\n  if [ \"${machine}\" =  \"Linux\" ] || [ \"${machine}\" =  \"Mac\" ]; then\n    pip3 install pipenv || { echo 'Installing virtualenv failed' | tee -a $DEBUG_FILE ; exit 1; }\n    pipenv install || { echo 'Installing python dependencies failed' | tee -a $DEBUG_FILE ; exit 1; }\n  fi\nfi\n\necho \"Trying to run the bot\" | tee -a $DEBUG_FILE\n\n# start bot directly if nomenu passed in to script\nif [[ $1 == *\"menu\"* ]]; then\n  echo \"Running with menu\" | tee -a $DEBUG_FILE\n  pipenv run python3 ./src/menu.py \"$@\"\nelse\n  echo \"Running without menu\" | tee -a $DEBUG_FILE\n  pipenv run python3 ./src/init.py \"$@\"\nfi\n"
  },
  {
    "path": "run_windows.bat",
    "content": "@ECHO OFF\r\n\r\nPowershell.exe -executionpolicy bypass -File ./deps/windows/windows.ps1\r\nif errorlevel 1 pause & exit \r\n\r\nif \"%1\"==\"\" (\r\n  echo running without menu\r\n  pipenv run python ./src/init.py\r\n)\r\n\r\nif \"%1\"==\"menu\" (\r\n  echo running with menu\r\n  pipenv run python ./src/menu.py\r\n)\r\n\r\necho exiting...\r\npause"
  },
  {
    "path": "src/__init__.py",
    "content": ""
  },
  {
    "path": "src/apis/__init__.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom config import reddit_config\nfrom .reddit import RedditAPI\nfrom .pushshift import PS\n\nreddit_api = RedditAPI(**reddit_config.AUTH).api\n\npushshift_api = PS()"
  },
  {
    "path": "src/apis/pushshift.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\n# data sources for comment learning\nfrom psaw import PushshiftAPI\nfrom logs.logger import log\nfrom utils import DAY, YEAR\nimport requests\nimport time\n\n\nclass PS():\n  def __init__(self):\n    self.api = PushshiftAPI()\n\n  def get_posts(self, subreddit, **kwargs):\n    post = self._ps_search(subreddit, **kwargs)\n    # log.info(f\"post: {post}\")\n    return post\n\n  def get_comments(self, subreddit):\n    return self.api.search_comments(q='', subreddit=subreddit)\n\n  def _ps_search(self, subreddit, before=None, after=None, score=None, limit=1):\n    cur_time = int(time.time())\n    after=(cur_time - YEAR) if after is None else None\n    before=(cur_time - (YEAR - DAY)) if before is None else None\n    score = 5000 if score is None else None\n    url = f\"https://api.pushshift.io/reddit/search/submission/?subreddit={subreddit}\"\n    url = url + (f\"&before={before}\" if before else \"\")\n    url = url + (f\"&after={after}\" if after else \"\")\n    url = url + (f\"&score>={score}\" if score else \"\")\n    url = url + (f\"&limit={limit}\" if limit else \"\")\n    url = url + (f\"&author!=[deleted]&selftext:not=[deleted]\") # avoids deleted posts\n    log.info(f\"pushshift-url: {url}\")\n\n    try:\n      response = requests.get(url).json().get(\"data\", [])\n      return response\n    except Exception as e:\n      # unable to get data from pushshift\n      return None\n"
  },
  {
    "path": "src/apis/reddit.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom praw import Reddit\nfrom utils import random_string\n\n\nclass RedditAPI():\n  def __init__(self,\n              reddit_client_id,\n              reddit_client_secret,\n              reddit_password,\n              reddit_username):\n\n    self.client_id = reddit_client_id\n    self.client_secret = reddit_client_secret\n    self.password = reddit_password\n    self.username = reddit_username\n    self.user_agent = random_string(10)\n    self.api = Reddit(\n        client_id=self.client_id,\n        client_secret=self.client_secret,\n        password=self.password,\n        user_agent=self.user_agent,\n        username=self.username,\n    )\n"
  },
  {
    "path": "src/bot.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom bots.reddit import RedditBot\nfrom utils import countdown\nfrom logs.logger import log\n\ndef run():\n  reddit = RedditBot()\n  while True:\n    reddit.run()\n    countdown(1)\n"
  },
  {
    "path": "src/bots/reddit/__init__.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom .bot import RedditBot\n\n"
  },
  {
    "path": "src/bots/reddit/actions/cleanup_actions.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport praw\nimport requests\nfrom apis import pushshift_api, reddit_api\nfrom config import reddit_config\nfrom utils import chance\nfrom logs.logger import log\nimport sys\n\nclass Cleanup():\n  def __init__(self):\n    self.psapi = pushshift_api\n    self.rapi = reddit_api\n    self.username = None\n\n  def init(self):\n    self.me = self.rapi.user.me\n    self.username = self.me().name\n\n  def shadow_check(self, roll=1):\n    if chance(roll):\n      log.info(\"performing a shadowban check\")\n      response = requests.get(f\"https://www.reddit.com/user/{self.username}/about.json\",  headers = {'User-agent': f\"hiiii its {self.username}\"}).json()\n      if \"error\" in response:\n        if response[\"error\"] == 404:\n          log.info(f\"account {self.username} is shadowbanned. poor bot :( shutting down the script...\")\n          sys.exit()\n        else:\n          log.info(response)\n      else:\n        log.info(f\"{self.username} is not shadowbanned! We think..\")\n\n  def remove_low_scores(self, roll=1):\n    comment_count = 0\n    post_count = 0\n    if chance(roll):\n      log.info(\"checking for low score content to remove\")\n      for i in self.rapi.redditor(self.username).new(limit=500):\n        if i.score <= reddit_config.CONFIG[\"reddit_low_score_threshold\"]:\n          if isinstance(i, praw.models.Comment):\n            log.info(f\"deleting comment(id={i.id}, body={i.body}, score={i.score}, subreddit={i.subreddit_name_prefixed}|{i.subreddit_id})\")\n            try:\n              i.delete()\n            except Exception as e:\n              log.info(f\"unable to delete comment(id={i.id}), skip...\\n{e.message}\")\n            comment_count += 1\n          else:\n            log.info(f\"deleting post(id={i.id}, score={i.score}, subreddit={i.subreddit_name_prefixed}|{i.subreddit_id})\")\n            try:\n              i.delete()\n            except Exception as e:\n              log.info(f\"unable to delete post(id={i.id}), skip...\\n{e.message}\")\n            post_count += 1\n            \n          log.info(f'removed {comment_count + post_count} item(s). removed {comment_count} comment(s), {post_count} post(s) with less than {reddit_config.CONFIG[\"reddit_low_score_threshold\"]} score')\n      \n      # GOOD BOT\n      if (comment_count + post_count) == 0:\n        log.info(\"no low score content to clean up. I'm a good bot! :^)\")\n\n  def karma_limit(self):\n    # current karma limit\n    ckl = reddit_config.CONFIG[\"reddit_comment_karma_limit\"]\n    # current post karma\n    cck = self.me().comment_karma\n    # post karma limit\n    pkl = reddit_config.CONFIG[\"reddit_post_karma_limit\"]\n    # current post karma\n    cpk = self.me().link_karma\n\n    if ckl:\n      if ckl < cck:\n        log.info(f\"Comment karma limit ({ckl}) exceeded! Your current comment karma: {cck}. Shutting down the script.\")\n        sys.exit()\n      else:\n        log.info(f\"Comment karma limit ({ckl}) not reached. Current comment karma: {cck}\")\n        return\n    \n    if pkl:\n      if pkl < cpk:\n        log.info(f\"Post karma limit ({pkl}) exceeded! Your current post karma: {cpk}. Shutting down the script.\")\n        sys.exit()\n      else:\n        log.info(f\"Post karma limit ({pkl}) not reached. Current post karma: {cpk}\")\n        return\n\n    log.info(f\"No limits - ignoring.\")\n\n"
  },
  {
    "path": "src/bots/reddit/actions/comments/comment_actions.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom .sources.cobe import Cobe\nfrom logs.logger import log\nfrom collections import namedtuple\nfrom utils import chance\nfrom apis import reddit_api\nfrom config import reddit_config\nfrom ..utils import get_subreddit, AVOID_WORDS\nimport random\nfrom praw.exceptions import APIException\n\nSource = namedtuple('Source', ['name', 'api'])\n\nclass Comments():\n  def __init__(self, source='cobe'):\n    self.ready = False\n    self.config = reddit_config.CONFIG\n    self.rapi = reddit_api\n    self.source_name = source\n    self.sources = {\n      \"cobe\": Source('cobe', Cobe)\n    }\n    self.comments = self.sources.get(self.source_name).api()\n\n\n  def init(self):\n    log.info(\"intiializing comments\")\n    self.ready = False\n    self.comments.init()\n    self.ready = True\n    log.info(\"commenting ready\")\n\n  def comment(self, roll=1):\n    if not self.ready:\n      log.info(\"comments need to be initialized\")\n      self.init()\n\n    if chance(roll):\n      log.info(\"going to make a comment\")\n\n      # keep searching posts until we find one with comments\n      post_with_comments = False\n      while not post_with_comments:\n        # pick a subreddit to comment on\n        subreddit = get_subreddit(getsubclass=True)\n        # get a random hot post from the subreddit\n        post = random.choice(list(subreddit.hot()))\n        # replace the \"MoreReplies\" with all of the submission replies\n        post.comments.replace_more(limit=0)\n\n        if len(post.comments.list()) > 0:\n          post_with_comments = True\n\n      try:\n        # choose if we're replying to the post or to a comment\n        if chance(self.config.get('reddit_reply_to_comment')):\n          # reply to the post with a response based on the post title\n          log.info('replying directly to post')\n          post.reply(self.comments.get_reply(post.title))\n        else:\n          # get a random comment from the post\n          comment = random.choice(post.comments.list())\n          # reply to the comment\n          log.info('replying to comment')\n          comment.reply(self.comments.get_reply(comment.body))\n      except APIException as e:\n        log.info(f\"error commenting: {e}\")\n\n\n\n\n"
  },
  {
    "path": "src/bots/reddit/actions/comments/sources/cobe.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom cobe.brain import Brain\nfrom config.cobe_config import CONFIG\nfrom apis import pushshift_api, reddit_api\nfrom logs.logger import log\nfrom utils import bytesto, tobytes\nfrom ...utils import AVOID_WORDS, get_subreddit\nimport os, sys\n\nclass Cobe():\n  def __init__(self, config=CONFIG):\n    self.ready = False\n    self.psapi = pushshift_api\n    self.rapi = reddit_api\n    self.config = CONFIG\n    self.brain = Brain(self.config.get(\"cobe_main_db\"))\n    self.size = 0\n\n  def get_reply(self, replyto: str=''):\n    if self.ready:\n      return self.brain.reply(replyto)\n    else:\n      log.info(f\"cobe not initialized, run init\")\n\n  def init(self):\n    log.info(\"using cobe to generate comments\")\n    main_db = self.config.get(\"cobe_main_db\")\n    \n    # make sure db was initialized correctly\n    if os.path.isfile(main_db):\n      # set the initial size\n      self.size = os.path.getsize(main_db)\n    else:\n      log.info(f\"cobe db failed to initialize. exiting\")\n      sys.exit()\n\n    log.debug('filling cobe database for commenting')\n    # loop through learning comments until we reach the min db size\n    while self.size <= tobytes(self.config.get(\"cobe_min_db_size\")):\n\n      log.info(f\"cobe db size is: {str(bytesto(self.size, 'm'))}mb, need {self.config.get('cobe_min_db_size')} - learning...\")\n      \n      # just learn from random subreddits for now\n      subreddit = get_subreddit(getsubclass=True)\n      \n      log.info(f\"learning from /r/{subreddit}\")\n      \n      # get the comment generator function from pushshift\n      comments = self.psapi.get_comments(subreddit)\n\n      # go through 500 comments per subreddit\n      for x in range(500):\n        # get the comment from the generator function\n        try:\n          comment = next(comments)\n        except StopIteration as e:\n          log.info(f\"end of comments\")\n        \n        # bot responses are better when it learns from short comments\n        if len(comment.body) < 240:\n          log.debug(f\"learning comment: {comment.body.encode('utf8')}\")\n          \n          # only learn comments that don't contain an avoid word\n          if not any(word in comment.body for word in AVOID_WORDS):\n            self.brain.learn(comment.body.encode(\"utf8\")) \n\n      # update the class size variable so the while loop\n      # knows when to break\n      self.size = os.path.getsize(main_db)\n\n    log.info(f\"database min size ({self.config.get('cobe_min_db_size')}) reached\")\n    self.ready = True"
  },
  {
    "path": "src/bots/reddit/actions/post_actions.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport random, requests, re\nfrom time import sleep as s\nfrom apis import pushshift_api, reddit_api\nfrom utils import chance\nfrom .utils import get_subreddit, AVOID_WORDS\nfrom config.reddit_config import CONFIG\nfrom config.reddit.reddit_sub_lists import CROSSPOST_SUBS\nfrom logs.logger import log\nfrom praw.exceptions import APIException\n\nwordgroup = [['last','recent'],['in','by','at'],['looking at','watching'],['took','attended'],['arrives','comes'],['approach','procedure'],['order','buy','purchase'],['recommended','suggested','endorsed','proposed'],['approved','accepted'],['employees','workers'],['amazing','incredible','unbelievable','wonderful','fantastic','extraordinary'],['anger','enrage','infuriate'],['angry','mad','furious','enraged'],['answer','reply','response'],['ask','question','request','query'],['awful','dreadful','terrible','abominable','bad','poor','unpleasant'],['rotten','contaminated','spoiled','tainted'],['faulty','improper','inappropriate','unsuitable','disagreeable','unpleasant'],['bad','evil','immoral','wicked','corrupt','harmful','deplorable','gross','heinous','obnoxious','despicable'],['beautiful','pretty','lovely','handsome','attractive','gorgeous','dazzling','splendid','magnificent','comely','fair','ravishing','graceful','elegant','fine','exquisite','aesthetic','pleasing','shapely','delicate','stunning','glorious','heavenly','resplendent','radiant','glowing','blooming','sparkling'],['begin','start','open','launch','initiate','commence','inaugurate','originate'],['big','enormous','huge','immense','gigantic','vast','colossal','gargantuan','large','sizable','grand','great','tall','substantial','mammoth','astronomical','ample','broad','expansive','spacious','stout','tremendous','titanic','mountainous'],['brave','courageous','fearless','dauntless','intrepid','plucky','daring','heroic','valorous','audacious','bold','gallant','valiant','doughty','mettlesome'],['break','fracture','rupture','shatter','smash','wreck','crash','demolish'],['bright','shiny','intellectual'],['calm','quiet','peaceful','still','collected'],['come','approach'],['cool','cold','frosty','icy'],['crooked','bent','twisted','curved','hooked'],['shout','yell','scream'],['cry','sob'],['cut','slice','slit','chop','crop'],['dangerous','risky','unsafe'],['dark','shadowy','dim','shaded'],['decide','determine','settle','choose','resolve'],['definite','certain','sure','positive','determined','clear','distinct','obvious'],['delicious','appetizing','scrumptious','delightful','enjoyable','toothsome','exquisite'],['describe','portray','characterize','picture','narrate','relate','recount','represent','report','record'],['destroy','ruin','demolish','kill','slay'],['difference','disagreement','inequity','contrast','dissimilarity','incompatibility'],['do','execute','finish','conclude','accomplish','achieve','attain'],['boring','tiring','tiresome','uninteresting'],['slow','dumb','stupid','unimaginative'],['eager','enthusiastic','involve','interest'],['enjoy','appreciate','like'],['explain','elaborate','clarify','define','interpret','justify','account for'],['fair','unbiased','objective','unprejudiced','honest'],['fall','drop','descend','tumble'],['false','untrue','deceptive','fallacious'],['famous','well-known','celebrated','famed','illustrious','distinguished','noted','notorious'],['fast','quick','rapid','speedy','hasty'],['fat','corpulent','beefy','plump','chubby','chunky','bulky'],['fear','anxiety','panic'],['funny','humorous','amusing','comical','laughable'],['get','acquire','obtain','secure','gather'],['go','recede','depart','fade','disappear','move','travel','proceed'],['good','excellent','fine','superior','marvelous','suitable','proper','pleasant','satisfactory','reliable','helpful','valid','genuine','great','respectable','edifying'],['great','noteworthy','worthy','distinguished','remarkable','grand','considerable','powerful','much','mighty'],['gross','rude','vulgar'],['happy','pleased','contented','satisfied','joyful','cheerful','glad','blissful'],['hate','despise','disfavor','dislike'],['have','possess','own'],['help','aid','assist','support','encourage'],['hide','cover'],['hurry','rush','run','speed','race','hasten','urge','accelerate','bustle'],['hurt','damage','harm','injure','wound','pain'],['idea','thought','concept','conception','plan'],['important','necessary','vital','critical','indispensable','valuable','essential','significant'],['interesting','fascinating','engaging','thought-provoking','curious','appealing'],['keep','hold','retain','withhold','preserve','maintain','sustain'],['kill','slay','execute','assassinate','murder','destroy','abolish'],['lazy','inactive','sluggish'],['little','tiny','small','petite'],['look','gaze','see','glance','watch','peek','stare','observe','view','spy','sight','discover','notice','recognize','peer','eye'],['love','like','admire','esteem','fancy','care for','cherish','adore','treasure','worship','appreciate','savor'],['make','create','form','construct','design','fabricate','manufacture','produce','develop','do','execute','compose','perform','acquire'],['mark','label','tag','price','ticket','sign','note','notice'],['mischievous','prankish','playful'],['move','go','walk','jog','run','sprint','hurry','wander','roam'],['moody','temperamental','short-tempered'],['neat','clean','orderly','elegant','well-organized','super','desirable','well-kept','shapely'],['new','fresh','modern','recent'],['old','ancient','aged','used','worn','faded','broken-down','old-fashioned'],['part','portion','share','piece','allotment','section','fraction','fragment'],['place','area','spot','region','location','position','residence','set','state'],['plan','plot','scheme','design','map','diagram','procedure','arrangement','intention','device','contrivance','method','way','blueprint'],['popular','well-liked','celebrate','common'],['predicament','quandary','dilemma','problem'],['put','place','set','attach','set aside','effect','achieve','do'],['quiet','silent','still','soundless','mute','peaceful','calm','restful'],['right','correct','good','honest','moral','proper','suitable'],['run','race','hurry','sprint','rush'],['say','tell','inform','notify','advise','narrate','explain','reveal','disclose','remark','converse','speak','affirm','suppose','utter','negate','express','verbalize','articulate','pronounce','convey','impart','state','announce'],['scared','afraid','frightened','terrified','fearful','worried','horrified','shocked'],['show','display','present','note','reveal','demonstrate'],['slow','unhurried','tedious'],['stop','end','finish','quit'],['story','myth','legend','fable','narrative','chronicle','anecdote','memoir'],['strange','odd','unusual','unfamiliar','uncommon','weird','outlandish','curious','unique','exclusive','irregular'],['remove','steal','lift','rob',],['purchase','buy'],['tell','disclose','reveal','narrate','talk','explain'],['think','reflect'],['trouble','disaster','misfortune','inconvenience'],['true','accurate','right','proper','precise','exact','valid','genuine'],['ugly','unpleasant','terrifying','gross','unsightly'],['unhappy','miserable','uncomfortable','unfortunate','depressed','sad'],['use','utilize'],['wrong','incorrect','inaccurate','mistaken'],['know','acknowledge','recognise','recognize'],['people','citizenry','masses','mass'],['now','forthwith','nowadays','instantly'],['first','beginning','initiatory','initiative','firstly'],]\n\ndef find_synonyms(keyword):\n    keyword=keyword.lower()\n    for sub_list in wordgroup:\n        if keyword in sub_list:\n            while True:\n                word = random.choice(sub_list)\n                if word != keyword:\n                    # print('found \"'+keyword+'\"; chosen synonym \"'+word+'\"')\n                    return word\n\ntitle_chars=['!','.',';','?']\ninvisible_chars = ['‍      ','   ‏‏‎   ','‏‏‎‏‏‎‏‏‎‏‏‎­',' ⠀']\n\ndef edit_text(var, mode):\n    if mode == 'body':\n        mychars=[]\n        if ' ' in var:\n            for index, x in enumerate(var):\n                if x == ' ':mychars.append(index)\n            editedtext = list(var)\n            editedtext[random.choice(mychars)] = random.choice(invisible_chars)\n            editedtext = ''.join(editedtext)\n            for x in editedtext.split():\n                synonym=find_synonyms(x)\n                if synonym != None:\n                    words = editedtext.split()\n                    words[words.index(x)] = synonym\n                    editedtext = \" \".join(words)\n                    return editedtext\n        else:return var\n    elif mode == 'title':\n        if any(not c.isalnum() for c in var[-2:]):return var.replace(var[-2:], var[-2]+' '+random.choice(title_chars))\n        else:return var+random.choice(title_chars)\n\nclass Posts():\n  def __init__(self):\n    self.psapi = pushshift_api\n    self.rapi = reddit_api\n\n  def get_post(self, subreddit=None):\n    log.info(f\"finding a post to re-post\")    \n    got_post = False\n    attempts = 0\n    while not got_post:\n      # use the supplied subreddit\n      # otherwise choose one randomly\n      if subreddit:\n        log.info(f\"searching post in sub: {subreddit}\")\n        sub = self.rapi.subreddit(subreddit)\n      else:\n        # if there are subreddits in the subreddit list pull randomly from that\n        # otherwise pull a totally random subreddit\n        sub = self.rapi.subreddit(random.choice(CONFIG['reddit_sub_list'])) if CONFIG['reddit_sub_list'] else get_subreddit(getsubclass=True)\n          \n        log.info(f\"searching post in sub: {sub.display_name}\")\n      try:\n        post_id = self.psapi.get_posts(sub.display_name)[0]['id']\n        # don't use posts that have avoid words in title\n        if not any(word in comment.body for word in AVOID_WORDS):\n          got_post = True\n      except Exception as e:\n        log.info(f\"couldn't find post in {sub}\")\n        # sub = self.rapi.random_subreddit(nsfw=False)\n        # log.info(f\"trying in: {subreddit}\")\n        attempts += 1\n        log.info(f\"repost attempts: {attempts}\")\n        if attempts > 3:\n          log.info(f\"couldn't find any posts - skipping reposting for now\")\n          return\n\n    return self.rapi.submission(id=post_id)\n\n  def crosspost(self, subreddit):\n    for idx, subs in enumerate(CROSSPOST_SUBS):\n      if subs[0] == subreddit:\n        return random.choice(subs[idx])\n\n  # why do my eyes hurt\n  def repost(self, roll=1, subreddit=None):\n    if chance(roll):\n      log.info(\"running repost\")\n      # log.info(\"running _repost\")\n      post = self.get_post(subreddit=subreddit)\n      if not post: return\n      api_call=requests.get(post.url).status_code\n      if api_call != 200:\n        if api_call == 429:\n          print('too many requests to pushshift')\n          s(random.uniform(3,8))\n        else:\n          print('pushshift http error: '+str(api_call))\n        return\n      else:\n        log.info(f\"reposting post: {post.id}\")\n        \n        if post.is_self:\n          if post.selftext not in ('[removed]','[deleted]') and bool(re.findall(r'20[0-9][0-9]|v.redd.it', post.selftext)) == False:\n            params = {\"title\": edit_text(post.title, 'title'), \"selftext\": edit_text(post.selftext, 'body')}\n          else:\n            print('Info: skipping post; it was malformed or date indicated')\n            # print(post.selftext)\n        else:params = {\"title\": edit_text(post.title, 'title'), \"url\": post.url}\n\n        sub = post.subreddit\n\n        # randomly choose a potential subreddit to cross post\n        if CONFIG['reddit_crosspost_enabled']:\n          sub = self.rapi.subreddit(self.crosspost(sub.display_name))\n        try:\n          self.rapi.subreddit(sub.display_name).submit(**params)\n          return\n        except (UnboundLocalError, TypeError):pass\n        except APIException as e:\n          log.info(f\"REPOST ERROR: {e}\")\n          return\n    else:\n      pass\n      # log.info(\"not running repost\")\n      # log.info(\"not running _repost\")\n\n\n## to do: add flairs compability or a way to avoid flairs\n"
  },
  {
    "path": "src/bots/reddit/actions/utils.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport random\nfrom apis import reddit_api\nfrom logs.logger import log\nfrom config.reddit.reddit_sub_lists import REDDIT_APPROVED_SUBS\nfrom config.common_config import CONFIG_ROOT\n\nwith open(f\"{CONFIG_ROOT}/reddit/reddit_avoid_subs.txt\", \"r\") as subfile:\n  AVOID_SUBS = subfile.read().splitlines()\n  subfile.close()\n\nwith open(f\"{CONFIG_ROOT}/reddit/reddit_avoid_words.txt\", \"r\") as wordfile:\n  AVOID_WORDS = wordfile.read().splitlines()\n  wordfile.close()\n\nlog.debug(f\"avoiding subs: {AVOID_SUBS}\")\n\ndef get_subreddit(nsfw=False, getsubclass=False):\n\n  # if the subreddit list is being used jut return one from there\n  if REDDIT_APPROVED_SUBS:\n    log.info(f\"picking subreddit from approved list\")\n    subreddit = reddit_api.subreddit(random.choice(REDDIT_APPROVED_SUBS).strip())\n    log.info(f\"using subreddit: {subreddit.display_name}\")\n  else:\n    log.info(f\"picking a random subreddit\")\n    # otherwise we'll do some logic to get a random subreddit\n    subreddit_ok = False\n    while not subreddit_ok:\n      subreddit = reddit_api.random_subreddit(nsfw=nsfw)\n      log.info(f\"checking subreddit: {subreddit.display_name}\")\n      # make sure the radom sub isn't in the avoid sub list\n      # keep searching for a subreddit until it meets this condition\n      if subreddit.display_name not in AVOID_SUBS:\n        subreddit_ok = True\n\n  if getsubclass:\n    return subreddit\n  else:\n    return subreddit.display_name"
  },
  {
    "path": "src/bots/reddit/bot.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom apis import reddit_api\nfrom config import reddit_config\nfrom utils import chance\nfrom bots.reddit.actions.post_actions import Posts\nfrom bots.reddit.actions.comments.comment_actions import Comments\nfrom bots.reddit.actions.cleanup_actions import Cleanup\nfrom logs.logger import log\nfrom logs.log_utils import log_json\nimport time, sys, random\nfrom collections import namedtuple\nfrom .utils import should_we_sleep, parse_user\n\nBotAction = namedtuple(\"BotAction\", 'name call')\n\nclass RedditBot():\n  def __init__(self, config=reddit_config.CONFIG):\n    self.api = reddit_api\n    self.ready = False\n    self.config = config\n    self.user = None\n    self.posts = Posts()\n    self.comments = Comments()\n    self.cleanup = Cleanup()\n    self.actions = [\n      BotAction('reddit_post_chance', self.posts.repost),\n      BotAction('reddit_comment_chance', self.comments.comment),\n      BotAction('reddit_shadowban_check', self.cleanup.shadow_check),\n      BotAction('reddit_remove_low_scores', self.cleanup.remove_low_scores),\n      BotAction('reddit_karma_limit_check', self.cleanup.karma_limit),\n    ]\n\n  def _init(self):\n    # check if account is set\n    user = self.api.user.me()\n    if user is None:\n      log.info(\"User auth failed, Reddit bot shutting down\")\n      sys.exit()\n    else:\n      log.info(f\"running as user: {user}\")\n      \n    # check if account is shadowbanned\n    self.cleanup.init()\n    self.cleanup.shadow_check()\n    self.user = parse_user(user)\n    log.info(f\"account info:\\n{log_json(self.user)}\")\n    self.ready = True\n    log.info(\"The bot is now running. It has a chance to perform an action every second. Be patient\")\n\n  def tick(self):\n    if not should_we_sleep():\n      report = f\"\"\n      for action in self.actions:\n        roll = random.random()\n        result = roll < self.config[action.name] \n        print(f\"{roll} < {self.config[action.name]} = {result}         \", end=\"\\r\")\n        if result:\n          log.info(f\"\\nrunning action: {action.name}\")\n          action.call()\n\n\n  def run(self):\n    if self.ready:\n      self.tick()\n    else:\n      self._init()\n      self.run()\n      # log.info(\"not running reddit bot - not ready\")\n"
  },
  {
    "path": "src/bots/reddit/utils.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport datetime\nfrom logs.logger import log\nfrom config.reddit_config import CONFIG\nimport time\nfrom praw.models.redditors import Redditors\n\n## USER UTILS\n\ndef parse_user(user: Redditors):\n  i = {}\n  i['comment_karma'] = user.comment_karma\n  i['link_karma'] = user.link_karma\n  i['username'] = user.name\n  i['created_utc'] = user.created_utc\n  i['created_utc_human'] = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(user.created_utc)) \n  return i\n\n## SCHEDULE UTILS\n\nEASY_SCHEDULES = {\n  1: ((7,00),(10,00)),\n  2: ((10,00),(14,00)),\n  3: ((14,00),(18,00)),\n  4: ((18,00),(22,00)),\n  5: ((22,00),(2,00)),\n}\n\n# convert the easy schedules to the tuple values\nBOT_SCHEDULE = [EASY_SCHEDULES.get(schedule) for schedule in CONFIG['reddit_sleep_schedule']]\n\nlog.info(f\"using schedules: {BOT_SCHEDULE}\")\n\n# transform the schedule with datetime formatting\nupdated_schedules = [((datetime.time(schedule[0][0], schedule[0][1])), (datetime.time(schedule[1][0], schedule[1][1]))) for schedule in BOT_SCHEDULE]\n\nBOT_SCHEDULE = updated_schedules\n\n\ndef is_time_between(begin_time, end_time, check_time=None):\n    # If check time is not given, default to current UTC time\n    check_time = check_time or datetime.datetime.utcnow().time()\n    if begin_time < end_time:\n        return check_time >= begin_time and check_time <= end_time\n    else: # crosses midnight\n        return check_time >= begin_time or check_time <= end_time\n\ndef should_we_sleep():\n    CHECKS = [True for schedule in BOT_SCHEDULE if is_time_between(schedule[0], schedule[1])]\n    # check if any of the time between checks returned true.\n    # if there's a True in the list, it means we're between one of the scheduled times\n    # and so this function returns False so the bot doesn't sleep\n    if True in CHECKS or not CONFIG.get('reddit_sleep_schedule'):\n      # no need to sleep - the bot is within one of the time ranges\n      return False\n    else:\n      log.info(\"it's sleepy time.. zzzzz :snore: zzzz\")\n      whats_left = []\n      TIME_LEFT = [schedule[0] for schedule in BOT_SCHEDULE]\n      for time_stamp in TIME_LEFT:\n        # log.info(time_stamp)\n        next_start = datetime.datetime.combine(datetime.date.today(), time_stamp)\n        # log.info(f\"next start: {next_start}\")\n        ts = int(next_start.timestamp())\n        # if this goes negative then the next start is probably tomorrow\n        if ts < int(time.time()):\n          next_start = datetime.datetime.combine((datetime.date.today() + datetime.timedelta(days=1)), time_stamp)\n          ts = next_start.timestamp()\n          \n        # collect all the seconds left for each time schedule to start\n        # log.info(f\"ts: {ts}\")\n        # log.info(f\"time: {int(time.time())}\")\n        whats_left.append(ts - int(time.time()))\n      \n      #remove negative values and\n      # get the shortest duration of time left before starting\n      # log.info(whats_left)\n      whats_left = [item for item in whats_left if item >= 0]\n\n      # log.info(whats_left)\n      time_left = int(min(whats_left))\n\n      if time_left > 600:\n        log.info(f\"waking up in: {datetime.timedelta(seconds=time_left)} at {next_start}\")\n      \n      sleep_time = int(time_left / 3)\n\n      # have the bot sleep for a short while instead of tons of messages every second\n      time.sleep(sleep_time)\n      return True\n"
  },
  {
    "path": "src/config/cobe_config.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom utils import prefer_envar\nfrom pathlib import Path\nfrom logs.logger import log\nfrom logs.log_utils import log_json\nfrom .common_config import SRC_ROOT\nimport os\n\nBASE_DIR = os.path.join(SRC_ROOT, 'bots/reddit/actions/comments')\nDB_DIR = os.path.join(BASE_DIR, \"brains\")\nMAIN_DB = os.path.join(DB_DIR, \"brain.db\")\n\nif not os.path.exists(DB_DIR):\n  os.makedirs(DB_DIR, exist_ok=True)\n\nCONFIG = prefer_envar({\n  # cobe config\n  \"cobe_base_dir\": BASE_DIR,\n  \"cobe_db_dir\": DB_DIR,\n  \"cobe_main_db\": MAIN_DB,\n  \"cobe_min_db_size\":\"50mb\",\n  \"cobe_max_db_size\":\"300mb\",\n})\n\nlog.info(f\"COBE CONFIG:\\n {log_json(CONFIG)}\")"
  },
  {
    "path": "src/config/common_config.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport os\nfrom logs.logger import log\nfrom pathlib import Path\n\n# Prefix that the bot uses to discover envars settings for the bots\nENVAR_PREFIX=\"BOT_\"\n\n\nCONFIG_ROOT = os.path.dirname(os.path.abspath(__file__))\nconfig_root = Path(CONFIG_ROOT)\nREPO_ROOT = config_root.parents[1].absolute()\nSRC_ROOT = os.path.join(REPO_ROOT, \"src\")\nENV_FILE= os.path.join(REPO_ROOT, \".env\")\n\nlog.info(f\"config root: {CONFIG_ROOT}\")\nlog.info(f\"repo root: {REPO_ROOT}\")\nlog.info(f\"src root: {SRC_ROOT}\")\n\n# Common Values\nDAY = 86400  # POSIX day (exact value in seconds)\nMINUTE = 60  # seconds in a minute\n\n"
  },
  {
    "path": "src/config/config_menu.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport os\nimport sys\nimport json\nimport menu\nimport pathlib\nfrom utils import prefer_envar\nfrom libs import urwide\nfrom .common_config import SRC_ROOT\n\nCONFIG_JSON_FILE = os.path.join(SRC_ROOT, \"config/config.json\")\n\nif os.path.isfile(CONFIG_JSON_FILE):\n  with open(CONFIG_JSON_FILE, \"r\") as config_json:\n    config_data = prefer_envar(json.load(config_json))\nelse:\n  config_data = prefer_envar({\n    \"reddit_client_id\":\"\",\n    \"reddit_client_secret\":\"\",\n    \"reddit_username\":\"\",\n    \"reddit_password\":\"\",\n  })\n\nCONSOLE_STYLE = \"\"\"\"\"\"\n\nCONSOLE_UI = f'''\\\nHdr Reddit Karma Bot Settings\n---\nEdt   Client ID          [{config_data[\"reddit_client_id\"]}]          #clientid\nEdt   Secret            [{config_data[\"reddit_client_secret\"]}]    #secret\n---\nEdt   Username       [{config_data[\"reddit_username\"]}]               #user\nEdt   Password       [{config_data[\"reddit_password\"]}]               #password\n===\nGFl\nBtn [Cancel]                        #btn_cancel &press=cancel\nBtn [Save]                          #btn_save   &press=save\nEnd\n'''\n\n# Event handler\nclass Handler(urwide.Handler):\n\n  def onSave( self, button ):\n    self.ui.info(\"Saving\")\n    fields = self.ui.widgets\n    \n    config_data[\"reddit_client_id\"] = fields.clientid.edit_text\n    config_data[\"reddit_client_secret\"] = fields.secret.edit_text\n    config_data[\"reddit_username\"] = fields.user.edit_text\n    config_data[\"reddit_password\"] = fields.password.edit_text\n\n    with open(CONFIG_JSON_FILE, \"w+\") as config_file:\n      config_file.write(json.dumps(config_data, indent=4, sort_keys=True))\n      config_file.close()\n    \n    menu.run()\n\n\n  def onCancel( self, button ):\n    self.ui.info(\"Cancel\")\n    menu.run()\n\nui = urwide.Console()\nui.create(CONSOLE_STYLE, CONSOLE_UI, Handler())\n\n# Main\ndef run():\n  ui.main()\n\nif __name__ == \"__main__\":\n  run()\n\n\n# EOF\n"
  },
  {
    "path": "src/config/reddit/config_gen.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport praw\nimport sys\nimport os\nfrom logs.logger import log\nfrom prawcore import ResponseException\nfrom ..common_config import ENV_FILE\n\n\n\ndef config_gen():\n  # ASK FOR CREDENTIALS\n  CLIENT_ID = input('please input your account client id :')\n  CLIENT_SECRET = input('please input your account client secret :')\n  PASSWORD = input('please input your account password :')\n  USERNAME = input('please input your account username :')\n\n  reddit = praw.Reddit(\n      client_id=CLIENT_ID,\n      client_secret=CLIENT_SECRET,\n      user_agent=\"my user agent\",\n      username=USERNAME,\n      password=PASSWORD\n  )\n\n  # CHECK IF CREDENTIALS ARE CORRECT\n  def authenticated(reddit):\n      try:\n          reddit.user.me()\n      except ResponseException:\n          return False\n      else:\n          return True\n\n\n  # SAVE CONFIG FILE\n  if authenticated(reddit):\n      with open(ENV_FILE, \"w\") as file_object:\n            file_object.write(f'bot_reddit_client_id=\"{CLIENT_ID}\"\\n')\n            file_object.write(f'bot_reddit_client_secret=\"{CLIENT_SECRET}\"\\n')\n            file_object.write(f'bot_reddit_password=\"{PASSWORD}\"\\n')\n            file_object.write(f'bot_reddit_username=\"{USERNAME}\"\\n')\n            print(\"Config file '.env' created. Please re-run the bot\")\n            sys.exit()\n          \n  else:\n      print('WRONG CREDENTIALS!! TRY AGAIN')\n      config_gen()\n\n"
  },
  {
    "path": "src/config/reddit/reddit_avoid_subs.txt",
    "content": "Agoraphobia\nAnxiety\nAssistance\nAtheistTwelveSteppers\nBipolarReddit\nCBTpractice\nDPDR\nDepression\nDiagnosed\nDomesticViolence\t\nHardShipMates\nHelpingHands\nIHaveIssues\nLGBT\nLostALovedOne\nNeedAFriend\nOffMyChest\t\nOpiatesRecovery\nPetloss\nRandomKindness\nRapeCounseling\nSFTS\nSMARTRecovery\nSelfHelp\nSingleParents\nStopGaming\nStopSelfHarm\t\nSuicideBereavement\nSuicideWatch\nTalkTherapy\nabuse\nabusiverelationships\naddiction \naddictionprevention \nadhd\nalcoholism\nanarchism \nanimetitties\nanxiety\nanxietyhelp\nanythinggoesnews\naspergers\nbabyloss\nbehaviortherapy\nbenzorecovery\nbidenpro \nbipolar\nbipolar2\nbipolarreddit\nbodyacceptance \nbodydysmorphia\nborderlinepdisorder\nbuylling\nchapotraphouse \nchildrenofdeadparents\ncomingout\ncommunism\ncompleteanarchy \nconfession\nconservative\ncptsd\ncripplingalcoholism\ncutters\ndbtselfhelp\ndeadredditors\ndemocrats\ndepressed\ndepression\ndepression_help\ndepression_memes\ndepressionandPTSD\ndepressionregimens\ndownsyndrome\ndrugs\ndysmorphicdisorder \neatingdisorders\nenergy\nenoughtrumpspam\nesist \nfeminism \nforeveralone\nfuckthealtright\nfull_news\nfullnews\ngamernews \ngeopolitics\ngriefsupport\nhealthanxiety\nhealthproject\nhelpmecope\nhumantrafficking\nimpeach_trump \ninmemoryof\ninthenews\nintrovert\nitgetsbetter\nkeep_track\nkindvoice\nleaves\nlgbteens\nliberal\nlibertarian\nlonely\nmarchagainsttrump\nmensrights\nmentalhealth\nmentalillness\nmixednuts\nneutralnews\nnews\nobits\nobituaries\nocd\nocpd\noffbeat \nouthere\npandys\npetioles\npolitical_revolution\npolitics\nproblemgambling\nptsd\nqualitynews\nquestioning\nquittingkratom\nraisedbynarcissists\nrant\nrants\nrape\nrapecounseling\nrecoverywithoutAA\nredditorsinrecovery\nrelationship_advice\nrelationships\nsad\nsafespace\nsandersforpresident \nschizophrenia\nsecondary_survivors\nsecularsobriety\nselfHarmScars\nselfharm\nselfhelp\nsextrafficking\nsfts\nshizoaffective\nsocialanxiety\nsocialism \nstopdrinking\nstopselfharm\nstopsmoking\nstopspeeding\nsuicidewatch\nsurvivorsofabuse\nteenrelationships\nthanksobama\nthe_mueller\nthefallen\nthenews\ntherapy\ntinytrumps\ntourettes\ntraumatoolbox\ntrumpcriticizestrump \ntrumpgret\ntwoxchromosomes\nukpolitics\nupliftingnews\nusnews\nvent\nwidowers\nwordpolitics\nworldnews\n"
  },
  {
    "path": "src/config/reddit/reddit_avoid_words.txt",
    "content": ""
  },
  {
    "path": "src/config/reddit/reddit_sub_lists.py",
    "content": "# The karma bot will only use \n# the subs in the list below\n\n# EXAMPLE\n# REDDIT_APPROVED_SUBS = [\n# \"aww\",\n# \"pics\",\n# \"pcmasterrace\",\n# ]\n\n# The bot will only use the subs defined in this list\n# if this list is empty it will choose subreddits randomly\nREDDIT_APPROVED_SUBS = [\n]\n\n# array of arrays with subreddits\n# where content can be crossposted\n# the first array item is the source,\n# and the rest are where it could be re-posted to\nCROSSPOST_SUBS = [\n  [\"aww\", \"pics\", \"animals\"],\n  [\"catpictures\", \"aww\"]\n]"
  },
  {
    "path": "src/config/reddit_config.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom utils import prefer_envar\nfrom logs.logger import log\nfrom logs.log_utils import log_json\nfrom config.reddit.reddit_sub_lists import REDDIT_APPROVED_SUBS\nfrom config.reddit.config_gen import config_gen\nimport sys\nimport json\nimport os\n\nAUTH = prefer_envar({\n  # app creds\n  \"reddit_client_id\":\"\",\n  \"reddit_client_secret\":\"\",\n  # reddit account creds\n  \"reddit_username\":\"\",\n  \"reddit_password\":\"\",\n})\n\nfor key in AUTH:\n  if AUTH[key] == \"\":\n    # reddit auth not configured correctly. \n    # instruct user to generate a .env file\n    config_gen()\n\nlog.info(f\"REDDIT AUTH CONFIG:\\n {log_json(AUTH)}\")\n\nCONFIG = prefer_envar({\n  \"reddit_crosspost_enabled\": False,\n  # the chance the bot will repost a post\n  \"reddit_post_chance\": 0.005,\n  # the chance the bot will make a comment\n  \"reddit_comment_chance\": 0.005,\n  # the chance the bot will reply to a comment\n  # otherwise it will reply to a post\n  \"reddit_reply_to_comment\": 0.002,\n  # chance the bot will remove poor performing\n  # posts and comments\n  \"reddit_remove_low_scores\": 0.002,\n  # posts/comments that get downvoted to this score will be deleted\n  \"reddit_low_score_threshold\": 0,\n  # chance to check if the bot is shadowbanned, \n  # and shut down the script automatically\n  \"reddit_shadowban_check\": 0.002,\n  # list of subreddits for the bot to use\n  \"reddit_sub_list\": REDDIT_APPROVED_SUBS,\n  # bot schedules. all times are UTC\n  # add the schedule number to the array\n  # and the bot will run within that time range\n  # leave the array empty for no schedule: []\n  # 1 - 7am-10am ((7,00),(10,00))\n  # 2 - 10am-2pm ((10,00),(14,00))\n  # 3 - 2pm-6pm ((14,00),(18,00))\n  # 4 - 6pm-10pm ((18,00),(22,00))\n  # 5 - 10pm-2am ((22,00),(2,00))\n  \"reddit_sleep_schedule\": [2, 4],\n  # Frequency to check if the bot hit karma limits\n  \"reddit_karma_limit_check\": 0.002,\n  # Set to integer with the max comment karma \n  # before the bot shuts down. Set as None to ignore\n  \"reddit_comment_karma_limit\": None,\n  # Set to integer with the max post/submission karma\n  # before the bot shuts down. Set as None to ignore\n  \"reddit_post_karma_limit\": None,\n})\n\nlog.info(f\"REDDIT CONNFIG:\\n {log_json(CONFIG)}\")\n"
  },
  {
    "path": "src/config/test.yml",
    "content": "loaded: true"
  },
  {
    "path": "src/init.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport sys\nfrom logs.logger import log\nfrom utils import check_internet , get_public_ip\nimport bot\n\nif __name__ == \"__main__\":\n    if check_internet() is True:\n        try:\n            log.info(f'Internet connection found : {get_public_ip()}')\n            bot.run()\n        except KeyboardInterrupt:\n            # quit\n            sys.exit()\n        else:\n            log.info('Please check your internet connection')\n            sys.exit()\n"
  },
  {
    "path": "src/libs/urwide.py",
    "content": "#!/usr/bin/env python\n# encoding: utf8\n# -----------------------------------------------------------------------------\n# Project   : URWIDE - Extended URWID\n# -----------------------------------------------------------------------------\n# Author    : Sébastien Pierre                     <sebastien.pierre@gmail.com>\n# License   : Lesser GNU Public License  http://www.gnu.org/licenses/lgpl.html>\n# -----------------------------------------------------------------------------\n# Creation  : 14-07-2006\n# Last mod  : 15-12-2016\n# -----------------------------------------------------------------------------\n\nimport sys, string, re, curses\nimport urwid, urwid.raw_display, urwid.curses_display\nfrom   urwid.widget import FLOW, FIXED, PACK, BOX, GIVEN, WEIGHT, LEFT, RIGHT, RELATIVE, TOP, BOTTOM, CLIP, RELATIVE_100\n\n__version__ = \"0.2.1\"\n__doc__ = \"\"\"\\\nURWIDE provides a nice wrapper around the awesome URWID Python library. It\nenables the creation of complex console user-interfaces, using an easy to use\nAPI .\n\nURWIDE provides a simple notation to describe text-based UIs, and also provides\nextensions to support events, tooltips, dialogs as well as other goodies for\nevery URWID widget.\n\nURWID can be downloaded at <http://www.excess.org/urwid>.\n\"\"\"\n\nCOLORS =  {\n\t# Colors\n\t\"WH\": \"white\",\n\t\"BL\": \"black\",\n\t\"YL\": \"yellow\",\n\t\"BR\": \"brown\",\n\t\"LR\": \"light red\",\n\t\"LG\": \"light green\",\n\t\"LB\": \"light blue\",\n\t\"LC\": \"light cyan\",\n\t\"LM\": \"light magenta\",\n\t\"Lg\": \"light gray\",\n\t\"DR\": \"dark red\",\n\t\"DG\": \"dark green\",\n\t\"DB\": \"dark blue\",\n\t\"DC\": \"dark cyan\",\n\t\"DM\": \"dark magenta\",\n\t\"Dg\": \"dark gray\",\n\t# Font attributes\n\t\"BO\": \"bold\",\n\t\"SO\": \"standout\",\n\t\"UL\": \"underline\",\n\t\"_\" : \"default\"\n}\nRIGHT  = \"right\"\nLEFT   = \"left\"\nCENTER = \"center\"\n\n\nIS_PYTHON3 = sys.version_info[0] > 2\n\nif IS_PYTHON3:\n\t# Python3 only defines str\n\tunicode = str\n\tlong    = int\nelse:\n\tunicode = unicode\n\ndef isString( t ):\n\treturn isinstance(t, (unicode, str))\n\ndef ensureString( t, encoding=\"utf8\" ):\n\tif IS_PYTHON3:\n\t\treturn t if isinstance(t, str) else str(t, encoding)\n\telse:\n\t\treturn t.encode(\"utf8\") if isinstance (t, unicode) else str(t)\n\ndef safeEnsureString( t,  encoding=\"utf8\" ):\n\tif IS_PYTHON3:\n\t\treturn ensureString(t, encoding)\n\telse:\n\t\treturn t.encode(\"utf8\", \"ignore\") if isinstance (t, unicode) else str(t)\n\ndef ensureUnicode( t, encoding=\"utf8\" ):\n\tif IS_PYTHON3:\n\t\treturn t if isinstance(t, str) else str(t, encoding)\n\telse:\n\t\treturn t if isinstance(t, unicode) else str(t).decode(encoding)\n\ndef ensureBytes( t, encoding=\"utf8\" ):\n\tif IS_PYTHON3:\n\t\treturn t if isinstance(t, bytes) else bytes(t, encoding)\n\telse:\n\t\treturn t\n\ndef add_widget( container, widget, options=None  ):\n\tw = widget\n\tif isinstance(container, urwid.Pile):\n\t\t# See: urwid.container.py Pile.__init__\n\t\tw = widget\n\t\tif not isinstance(w, tuple):\n\t\t\tcontainer.contents.append((w, (WEIGHT, 1)))\n\t\telif w[0] in (FLOW, PACK):\n\t\t\tf, w = w\n\t\t\tcontaine.contents.append((w, (PACK, None)))\n\t\telif len(w) == 2:\n\t\t\theight, w = w\n\t\t\tcontainer.contents.append((w, (GIVEN, height)))\n\t\telif w[0] == FIXED: # backwards compatibility\n\t\t\t_ignore, height, w = w\n\t\t\tcontainer.contents.append((w, (GIVEN, height)))\n\t\telif w[0] == WEIGHT:\n\t\t\tf, height, w = w\n\t\t\tcontainer.contents.append((w, (f, height)))\n\t\telse:\n\t\t\traise ValueError(\"Widget not as expected: {0}\".format(widet))\n\telse:\n\t\tcontainer.contents.append(widget)\n\n\ndef remove_widgets( container ):\n\tw = [_ for _ in container.contents]\n\tfor _ in w:\n\t\tcontainer.contents.remove(_)\n\ndef original_widgets( widget ):\n\tif not widget:\n\t\treturn []\n\tstack = [widget]\n\tif stack:\n\t\twhile hasattr(stack[0], \"original_widget\"):\n\t\t\toriginal = stack[0].original_widget\n\t\t\tif original not in stack:\n\t\t\t\tstack.insert(0,original)\n\t\t\telse:\n\t\t\t\tbreak\n\treturn stack\n\ndef original_widget(widget):\n\tr = original_widgets(widget)\n\treturn r[0] if r else widget\n\ndef original_focus(widget):\n\tw = original_widgets(widget)\n\tfor _ in w:\n\t\tif hasattr(_, \"focus\"):\n\t\t\treturn _.focus\n\treturn w[0]\n\n# ------------------------------------------------------------------------------\n#\n# URWID Patching\n#\n# ------------------------------------------------------------------------------\n\nclass PatchedListBox(urwid.ListBox):\n\n\t_parent = None\n\n\tdef __init__( self, *args, **kwargs ):\n\t\tPatchedListBox._parent.__init__(self, *args, **kwargs)\n\n\tdef remove_widgets( self ):\n\t\t\"\"\"Remove all widgets from the body.\"\"\"\n\t\tif isinstance(self.body, SimpleListWalker):\n\t\t\tself.body = SimpleListWalker([])\n\t\telse:\n\t\t\traise Exception(\"Method only supported for SimpleListWalker\")\n\n\tdef add_widget( self, widget ):\n\t\t\"\"\"Adds a widget to the body of this list box.\"\"\"\n\t\tif isinstance(self.body, SimpleListWalker):\n\t\t\tself.body.contents.append(widget)\n\t\telse:\n\t\t\traise Exception(\"Method only supported for SimpleListWalker\")\n\nclass PatchedPile(urwid.Pile):\n\n\t_parent = None\n\n\tdef __init__(self, widget_list, focus_item=None):\n\t\t# No need to call the constructor\n\t\t#super(PatchedPile, self).__init__(widget_list, focus_item)\n\t\tself.__super.__init__(widget_list, focus_item)\n\t\tself.widget_list = []\n\t\tself.item_types  = []\n\t\tfor _ in widget_list: add_widget(self, _)\n\t\tif focus_item: self.set_focus(focus_item)\n\t\tself.pref_col = None\n\n\tdef add_widget( self, widget ):\n\t\t\"\"\"Adds a widget to this pile\"\"\"\n\t\tw = widget\n\t\tself.widget_list.append(widget)\n\t\tif type(w) != type(()):\n\t\t\tself.item_types.append(('weight',1))\n\t\telif w[0] == 'flow':\n\t\t\tf, widget = w\n\t\t\tself.widget_list[i] = widget\n\t\t\tself.item_types.append((f,None))\n\t\telif w[0] in ('fixed', 'weight'):\n\t\t\tf, height, widget = w\n\t\t\tself.widget_list[i] = widget\n\t\t\tself.item_types.append((f,height))\n\t\telse:\n\t\t\traise PileError(\"widget list item invalid %s\" % (w))\n\n\tdef remove_widget( self, widget ):\n\t\t\"\"\"Removes a widget from this pile\"\"\"\n\t\tif type(widget) != type(()): widget = widget[1]\n\t\ti = self.widget_list.index(widget)\n\t\tdel self.widget_list[i]\n\t\tdel self.item_types[i]\n\n\tdef remove_widgets( self ):\n\t\t\"\"\"Removes all widgets from this pile\"\"\"\n\t\tself.widget_list = []\n\t\tself.item_types  = []\n\nclass PatchedColumns(urwid.Columns):\n\t_parent = None\n\tdef set_focus(self, widget):\n\t\t\"\"\"Set the column in focus with a widget in self.widget_list.\"\"\"\n\t\tposition = self.widget_list.index(widget) if type(widget) != int else widget\n\t\tself.focus_col = position\n\nPatchedPile._parent    = urwid.Pile\nPatchedListBox._parent = urwid.ListBox\nPatchedColumns._parent = urwid.Columns\n# urwid.Pile    = PatchedPile\n# urwid.ListBox = PatchedListBox\n# urwid.Columns = PatchedColumns\n\n# ------------------------------------------------------------------------------\n#\n# UI CLASS\n#\n# ------------------------------------------------------------------------------\n\nclass UISyntaxError(Exception): pass\nclass UIRuntimeError(Exception): pass\nclass UI:\n\t\"\"\"The UI class allows to build an URWID user-interface from a simple set of\n\tstring definitions.\n\n\tInstanciation of this class, may raise syntax error if the given text data\n\tis not formatted as expected, but you can easily get detailed information on\n\twhat the problem was.\"\"\"\n\n\tBLANK = urwid.Text(\"\")\n\tEMPTY = urwid.Text(\"\")\n\tNOP   = lambda self:self\n\n\tclass Collection(object):\n\t\t\"\"\"Keys of the given collection are recognized as attributes.\"\"\"\n\n\t\tdef __init__( self, collection=None ):\n\t\t\tobject.__init__(self)\n\t\t\tif collection is None: collection = {}\n\t\t\tself.w_w_content = collection\n\n\t\tdef __getattr__( self, name ):\n\t\t\tif name.startswith(\"w_w_\"):\n\t\t\t\treturn super(UI.Collection, self).__getattribute__(name)\n\t\t\tw = self.w_w_content\n\t\t\tif name not in w: raise UIRuntimeError(\"No widget with name: \" + name )\n\t\t\treturn w[name]\n\n\t\tdef __setattr__( self, name, value):\n\t\t\tif name.startswith(\"w_w_\"):\n\t\t\t\treturn super(UI.Collection, self).__setattr__(name, value)\n\t\t\tif name in self.w_w_content:\n\t\t\t\traise SyntaxError(\"Item name already used: \" + name)\n\t\t\tself.w_w_content[name] = value\n\n\tdef __init__( self ):\n\t\t\"\"\"Creates a new user interface object from the given text\n\t\tdescription.\"\"\"\n\t\tself._content     = None\n\t\tself._stack       = None\n\t\tself._currentLine = None\n\t\tself._ui          = None\n\t\tself._palette     = None\n\t\tself._header      = None\n\t\tself._currentSize = None\n\t\tself._widgets     = {}\n\t\tself._groups      = {}\n\t\tself._strings     = {}\n\t\tself._data        = {}\n\t\tself._handlers    = []\n\t\tself.widgets      = UI.Collection(self._widgets)\n\t\tself.groups       = UI.Collection(self._groups)\n\t\tself.strings      = UI.Collection(self._strings)\n\t\tself.data         = UI.Collection(self._data)\n\n\tdef id( self, widget ):\n\t\t\"\"\"Returns the id for the given widget.\"\"\"\n\t\tif hasattr(widget, \"_urwideId\"):\n\t\t\treturn widget._urwideId\n\t\telse:\n\t\t\treturn None\n\n\tdef new( self, widgetClass, *args, **kwargs ):\n\t\t\"\"\"Creates the given widget by instanciating @widgetClass with the given\n\t\targs and kwargs. Basically, this is equivalent to\n\n\t\t>\treturn widgetClass(*kwargs['args'], **kwargs['kwargs'])\n\n\t\tExcepted that the widget is wrapped in an `urwid.AttrWrap` object, with the\n\t\tproper attributes. Also, the given @kwargs are preprocessed before being\n\t\tforwarded to the widget:\n\n\t\t - `data` is the text data describing ui attributes, constructor args\n\t\t   and kwargs (in the same format as the text UI description)\n\n\t\t - `ui`, `args` and `kwargs` allow to pass preprocessed data to the\n\t\t   constructor.\n\n\t\tIn all cases, if you want to pass args and kwargs, you should\n\t\texplicitely use the `args` and `kwargs` arguments. I know that this is a\n\t\tbit confusing...\"\"\"\n\t\treturn self._createWidget( widgetClass, *args, **kwargs )\n\n\tdef wrap( self, widget, properties ):\n\t\t\"\"\"Wraps the given in the given properties.\"\"\"\n\t\t_ui, _, _ = self._parseAttributes(properties)\n\t\treturn self._wrapWidget( widget, _ui )\n\n\tdef unwrap( self, widget ):\n\t\t\"\"\"Unwraps the widget (see `new` method).\"\"\"\n\t\tif isinstance(widget, urwid.AttrWrap) and widget.w: widget = widget.w\n\t\treturn widget\n\n\t# EVENT HANDLERS\n\t# -------------------------------------------------------------------------\n\n\tdef handler( self, handler = None ):\n\t\t\"\"\"Sets/Gets the current event handler.\n\n\t\tThis modifies the 'handler.ui' and sets it to this ui.\"\"\"\n\t\tif handler is None:\n\t\t\tif not  self._handlers: raise UIRuntimeError(\"No handler defined for: %s\" % (self))\n\t\t\treturn self._handlers[-1][0]\n\t\telse:\n\t\t\told_ui     = handler.ui\n\t\t\thandler.ui = self\n\t\t\tif not self._handlers:\n\t\t\t\tself._handlers.append((handler, old_ui))\n\t\t\telse:\n\t\t\t\tself._handlers[-1] = (handler, old_ui)\n\n\tdef responder( self, event ):\n\t\t\"\"\"Returns the function that responds to the given event.\"\"\"\n\t\treturn self.handler().responder(event)\n\n\tdef pushHandler( self, handler ):\n\t\t\"\"\"Push a new handler on the list of handlers. This handler will handle\n\t\tevents until it is popped out or replaced.\"\"\"\n\t\tself._handlers.append((handler, handler.ui))\n\t\thandler.ui = self\n\n\tdef popHandler( self ):\n\t\t\"\"\"Pops the current handler of the list of handlers. The handler will\n\t\tnot handle events anymore, while the previous handler will start to\n\t\thandle events.\"\"\"\n\t\thandler, ui = self._handlers.pop()\n\t\thandler.ui = ui\n\n\tdef _handle( self, event_name, widget, *args, **kwargs ):\n\t\t\"\"\"Handle the given given event name.\"\"\"\n\t\t# If the event is an event name, we use the handler mechanism\n\t\tif type(event_name) in (str, unicode):\n\t\t\thandler = self.handler()\n\t\t\tif handler.responds(event_name):\n\t\t\t\treturn handler.respond(event_name, widget, *args, **kwargs)\n\t\t\telif hasattr(widget, event_name):\n\t\t\t\tgetattr(widget, event_name, *args, **kwargs)\n\t\t\telse:\n\t\t\t\traise UIRuntimeError(\"No handler for event: %s in %s\" % (event_name, widget))\n\t\t# Otherwise we assume it is a callback\n\t\telse:\n\t\t\treturn event_name(widget,  *args, **kwargs)\n\n\tdef setTooltip( self, widget, tooltip ):\n\t\twidget._urwideTooltip = tooltip\n\n\tdef setInfo( self, widget, info ):\n\t\twidget._urwideInfo = info\n\n\tdef onKey( self, widget, callback ):\n\t\t\"\"\"Sets a callback to the given widget for the 'key' event\"\"\"\n\t\twidget = self.unwrap(widget)\n\t\twidget._urwideOnKey = callback\n\n\tdef onFocus( self, widget, callback ):\n\t\t\"\"\"Sets a callback to the given widget for the 'focus' event\"\"\"\n\t\twidget = self.unwrap(widget)\n\t\twidget._urwideOnFocus = callback\n\n\tdef onEdit( self, widget, callback ):\n\t\t\"\"\"Sets a callback to the given widget for the 'edit' event\"\"\"\n\t\twidget = self.unwrap(widget)\n\t\twidget._urwideOnEdit = callback\n\n\tdef onPress( self, widget, callback ):\n\t\t\"\"\"Sets a callback to the given widget for the 'edit' event\"\"\"\n\t\twidget = self.unwrap(widget)\n\t\twidget._urwideOnPress = callback\n\n\tdef _doPress( self, button, *args ):\n\t\tif hasattr(button, \"_urwideOnPress\"):\n\t\t\tevent_name = button._urwideOnPress\n\t\t\tself._handle(event_name, button, *args)\n\t\telif isinstance(button, urwid.RadioButton):\n\t\t\treturn False\n\t\telse:\n\t\t\traise UIRuntimeError(\"Widget does not respond to press event: %s\" % (button))\n\n\tdef _doFocus( self, widget, ensure=True ):\n\t\tif hasattr(widget, \"_urwideOnFocus\"):\n\t\t\tevent_name = widget._urwideOnFocus\n\t\t\tself._handle(event_name, widget)\n\t\telif ensure:\n\t\t\traise UIRuntimeError(\"Widget does not respond to focus event: %s\" % (widget))\n\n\tdef _doEdit( self, widget, before, after, ensure=True ):\n\t\tif hasattr(widget, \"_urwideOnEdit\"):\n\t\t\tevent_name = widget._urwideOnEdit\n\t\t\tself._handle(event_name, widget, before, after)\n\t\telif ensure:\n\t\t\traise UIRuntimeError(\"Widget does not respond to focus edit: %s\" % (widget))\n\n\tdef _doKeyPress( self, widget, key ):\n\t\t# THE RULES\n\t\t# ---------\n\t\t#\n\t\t# 1) Widget defines an onKey event handler, it is triggered\n\t\t# 2) If the handler returned False, or was not existent, we\n\t\t#    forward to the top widget\n\t\t# 3) The onKeyPress event is handled by the keyPress handler if the\n\t\t#    focused widget is not editable\n\t\t# 4) If no keyPresss handler is defined, the default key_press event is\n\t\t#    handled\n\t\ttopwidget = self.getToplevel()\n\t\tcurrent_widget = widget\n\t\t# We traverse the `original_widget` in case the widgets are nested.\n\t\t# This allows to get the deepest widget.\n\t\tstack = original_widgets(widget)\n\t\t# FIXME: Dialogs should prevent processing of events at a lower level\n\t\tif stack:\n\t\t\tfor widget in stack:\n\t\t\t\tif hasattr(widget, \"_urwideOnKey\"):\n\t\t\t\t\tevent_name = widget._urwideOnKey\n\t\t\t\t\tif self._handle(event_name, widget, key):\n\t\t\t\t\t\treturn\n\t\t\tif current_widget != topwidget and current_widget not in stack:\n\t\t\t\tself._doKeyPress(topwidget, key)\n\t\t\telse:\n\t\t\t\tself._doKeyPress(None, key)\n\t\telif widget and widget != topwidget:\n\t\t\tself._doKeyPress(topwidget, key)\n\t\telse:\n\t\t\tif key == \"tab\":\n\t\t\t\tself.focusNext()\n\t\t\telif key == \"shift tab\":\n\t\t\t\tself.focusPrevious()\n\t\t\tif self.isEditable(self.getFocused()):\n\t\t\t\tres = False\n\t\t\telse:\n\t\t\t\ttry:\n\t\t\t\t\tres = self._handle(\"keyPress\", topwidget, key)\n\t\t\t\texcept UIRuntimeError:\n\t\t\t\t\tres = False\n\t\t\tif not res:\n\t\t\t\ttopwidget.keypress(self._currentSize, key)\n\n\tdef getFocused( self ):\n\t\traise Exception(\"Must be implemented by subclasses\")\n\n\tdef focusNext( self ):\n\t\traise Exception(\"Must be implemented by subclasses\")\n\n\tdef focusPrevious( self ):\n\t\traise Exception(\"Must be implemented by subclasses\")\n\n\tdef getToplevel( self ):\n\t\traise Exception(\"Must be implemented by subclasses\")\n\n\tdef isEditable( self, widget ):\n\t\treturn isinstance(widget, (urwid.Edit, urwid.IntEdit))\n\n\tdef isFocusable( self, widget ):\n\t\tif   isinstance(widget, urwid.Edit):        return True\n\t\telif isinstance(widget, urwid.IntEdit):     return True\n\t\telif isinstance(widget, urwid.Button):      return True\n\t\telif isinstance(widget, urwid.CheckBox):    return True\n\t\telif isinstance(widget, urwid.RadioButton): return True\n\t\telse:                                       return False\n\n\t# PARSING WIDGETS STACK MANAGEMENT\n\t# -------------------------------------------------------------------------\n\n\tdef _add( self, widget ):\n\t\t\"\"\"Adds the given widget to the @_content list. This list will be\n\t\tadded to the current parent widget when the UI is finished or when an\n\t\t`End` block is encountered (see @_push and @_pop)\"\"\"\n\t\t# Piles cannot be created with [] as content, so we fill them with the\n\t\t# EMPTY widget, which is replaced whenever we add something\n\t\tif self._content == [self.EMPTY]: self._content[0] = widget\n\t\tself._content.append(widget)\n\n\tdef _push( self, endCallback, ui=None, args=(), kwargs={} ):\n\t\t\"\"\"Pushes the given arguments (@ui, @args, @kwargs) on the stack,\n\t\ttogether with the @endCallback which will be invoked with the given\n\t\targuments when an `End` block will be encountered (and that a @_pop is\n\t\ttriggered).\"\"\"\n\t\tself._stack.append((self._content, endCallback, ui, args, kwargs))\n\t\tself._content = []\n\t\treturn self._content\n\n\tdef _pop( self ):\n\t\t\"\"\"Pops out the widget on the top of the stack and invokes the\n\t\t_callback_ previously associated with it (using @_push).\"\"\"\n\t\tprevious_content = self._content\n\t\tself._content, end_callback, end_ui, end_args, end_kwargs = self._stack.pop()\n\t\treturn previous_content, end_callback, end_ui, end_args, end_kwargs\n\n\t# GENERIC PARSING METHODS\n\t# -------------------------------------------------------------------------\n\n\tdef create( self, style, ui, handler=None ):\n\t\tself.parseStyle(style)\n\t\tself.parseUI(ui)\n\t\tif handler: self.handler(handler)\n\t\treturn self\n\n\tdef parseUI( self, text ):\n\t\t\"\"\"Parses the given text and initializes this user interface object.\"\"\"\n\t\ttext = string.Template(text).substitute(self._strings)\n\t\tself._content = []\n\t\tself._stack   = []\n\t\tself._currentLine = 0\n\t\tfor line in text.split(\"\\n\"):\n\t\t\tline = line.strip()\n\t\t\tif not line.startswith(\"#\"): self._parseLine(line)\n\t\t\tself._currentLine += 1\n\t\tself._listbox     = self._createWidget(urwid.ListBox,self._content)\n\t\treturn self._content\n\n\tdef parseStyle( self, data ):\n\t\t\"\"\"Parses the given style.\"\"\"\n\t\tres = []\n\t\tfor line in data.split(\"\\n\"):\n\t\t\tif not line.strip(): continue\n\t\t\tline = line.replace(\"\\t\", \" \").replace(\"  \", \" \")\n\t\t\tname, attributes = [_.strip() for _ in line.split(\":\")]\n\t\t\tres_line = [name]\n\t\t\tfor attribute in attributes.split(\",\"):\n\t\t\t\tattribute = attribute.strip()\n\t\t\t\tcolor     = COLORS.get(attribute)\n\t\t\t\tif not color: raise UISyntaxError(\"Unsupported color: \" + attribute)\n\t\t\t\tres_line.append(color)\n\t\t\tif len(res_line) != 4:\n\t\t\t\traise UISyntaxError(\"Expected NAME: FOREGROUND BACKGROUND FONT\")\n\t\t\tres.append(tuple(res_line))\n\t\tself._palette = res\n\t\treturn res\n\n\tRE_LINE = re.compile(\"^\\s*(...)\\s?\")\n\tdef _parseLine( self, line ):\n\t\t\"\"\"Parses a line of the UI definition file. This automatically invokes\n\t\tthe specialized parsers.\"\"\"\n\t\tif not line:\n\t\t\tself._add( self.BLANK )\n\t\t\treturn\n\t\tmatch = self.RE_LINE.match(line)\n\t\tif not match: raise UISyntaxError(\"Unrecognized line: \" + line)\n\t\tname  = match.group(1)\n\t\tdata  = line[match.end():]\n\t\tif hasattr(self, \"_parse\" + name ):\n\t\t\tgetattr(self, \"_parse\" + name)(data)\n\t\telif name[0] == name[1] == name[2]:\n\t\t\tself._parseDvd(name + data)\n\t\telse:\n\t\t\traise UISyntaxError(\"Unrecognized widget: `\" + name + \"`\")\n\n\tdef _parseAttributes( self, data ):\n\t\tassert type(data) in (str, unicode)\n\t\tui_attrs, data = self._parseUIAttributes(data)\n\t\targs, kwargs   = self._parseArguments(data)\n\t\treturn ui_attrs, args, kwargs\n\n\tRE_UI_ATTRIBUTE = re.compile(\"\\s*([#@\\?\\:]|\\&[\\w]+\\=)([\\w\\d_\\-]+)\\s*\")\n\tdef _parseUIAttributes( self, data ):\n\t\t\"\"\"Parses the given UI attributes from the data and returns the rest of\n\t\tthe data (which corresponds to something else thatn the UI\n\t\tattributes.\"\"\"\n\t\tassert type(data) in (str, unicode)\n\t\tui = {\"events\":{}}\n\t\twhile True:\n\t\t\tmatch = self.RE_UI_ATTRIBUTE.match(data)\n\t\t\tif not match: break\n\t\t\tui_type, ui_value = match.groups()\n\t\t\tassert type(ui_value) in (str, unicode)\n\t\t\tif   ui_type    == \"#\": ui[\"id\"]      = ui_value\n\t\t\telif ui_type    == \"@\": ui[\"style\"]   = ui_value\n\t\t\telif ui_type    == \"?\": ui[\"info\"]    = ui_value\n\t\t\telif ui_type    == \"!\": ui[\"tooltip\"] = ui_value\n\t\t\telif ui_type[0] == \"&\": ui[\"events\"][ui_type[1:-1]]=ui_value\n\t\t\tdata = data[match.end():]\n\t\treturn ui, data\n\n\tdef _parseArguments( self, data ):\n\t\t\"\"\"Parses the given text data which should be a list of attributes. This\n\t\treturns a dict with the attributes.\"\"\"\n\t\tassert type(data) in (str, unicode)\n\t\tdef as_dict(*args, **kwargs): return args, kwargs\n\t\tres = eval(\"as_dict(%s)\" % (data))\n\t\ttry:\n\t\t\tres = eval(\"as_dict(%s)\" % (data))\n\t\texcept:\n\t\t\traise SyntaxError(\"Malformed arguments: \" + repr(data))\n\t\treturn res\n\n\tdef hasStyle( self, *styles ):\n\t\tfor s in styles:\n\t\t\tfor r in self._palette:\n\t\t\t\tif r[0] == s: return s\n\t\treturn False\n\n\tdef _styleWidget( self, widget, ui ):\n\t\t\"\"\"Wraps the given widget so that it belongs to the given style.\"\"\"\n\t\tstyles = []\n\t\tif \"id\" in ui: styles.append(\"#\" + ui[\"id\"])\n\t\tif \"style\" in ui:\n\t\t\ts = ui[\"style\"]\n\t\t\tif type(s) in (tuple, list): styles.extend(s)\n\t\t\telse: styles.append(s)\n\t\tstyles.append( widget.__class__.__name__ )\n\t\tunf_styles = [_ for _ in styles if self.hasStyle(_)]\n\t\tfoc_styles = [_ + \"*\" for _ in styles if self.hasStyle(_ + \"*\")]\n\t\tif unf_styles:\n\t\t\tif foc_styles:\n\t\t\t\treturn urwid.AttrWrap(widget, unf_styles[0], foc_styles[0])\n\t\t\telse:\n\t\t\t\treturn urwid.AttrWrap(widget, unf_styles[0])\n\t\telse:\n\t\t\treturn widget\n\n\tdef _createWidget( self, widgetClass, *args, **kwargs ):\n\t\t\"\"\"Creates the given widget by instanciating @widgetClass with the given\n\t\targs and kwargs. Basically, this is equivalent to\n\n\t\t>\treturn widgetClass(*kwargs['args'], **kwargs['kwargs'])\n\n\t\tExcepted that the widget is wrapped in an `urwid.AttrWrap` object, with the\n\t\tproper attributes. Also, the given @kwargs are preprocessed before being\n\t\tforwarded to the widget:\n\n\t\t - `data` is the text data describing ui attributes, constructor args\n\t\t   and kwargs (in the same format as the text UI description)\n\n\t\t - `ui`, `args` and `kwargs` allow to pass preprocessed data to the\n\t\t   constructor.\n\n\t\tIn all cases, if you want to pass args and kwargs, you should\n\t\texplicitely use the `args` and `kwargs` arguments. I know that this is a\n\t\tbit confusing...\"\"\"\n\t\t_data = _ui = _args = _kwargs = None\n\t\tfor arg, value in kwargs.items():\n\t\t\tif   arg == \"data\":   _data = value\n\t\t\telif arg == \"ui\":     _ui = value\n\t\t\telif arg == \"args\":   _args = value\n\t\t\telif arg == \"kwargs\": _kwargs = value\n\t\t\telse: raise Exception(\"Unrecognized optional argument: \" + arg)\n\t\tif _data:\n\t\t\t_ui, _args, _kwargs = self._parseAttributes(_data)\n\t\targs = list(args)\n\t\tif _args: args.extend(_args)\n\t\tkwargs = _kwargs or {}\n\t\twidget = widgetClass(*args, **kwargs)\n\t\treturn self._wrapWidget(widget, _ui)\n\n\tdef _wrapWidget( self, widget, _ui ):\n\t\t\"\"\"Wraps the given widget into anotger widget, and applies the various\n\t\tproperties listed in the '_ui' (internal structure).\"\"\"\n\t\t# And now we process the ui information\n\t\tif not _ui: _ui = {}\n\t\tif \"id\" in _ui:\n\t\t\tsetattr(self.widgets, _ui[\"id\"], widget)\n\t\t\twidget._urwideId = _ui[\"id\"]\n\t\tif _ui.get(\"events\"):\n\t\t\tfor event, handler in _ui[\"events\"].items():\n\t\t\t\tif   event == \"press\":\n\t\t\t\t\tif not isinstance(widget, urwid.Button)\\\n\t\t\t\t\tand not isinstance(widget, urwid.RadioButton):\n\t\t\t\t\t\traise UISyntaxError(\"Press event only applicable to Button: \" + repr(widget))\n\t\t\t\t\twidget._urwideOnPress = handler\n\t\t\t\telif event == \"edit\":\n\t\t\t\t\tif not isinstance(widget, urwid.Edit):\n\t\t\t\t\t\traise UISyntaxError(\"Edit event only applicable to Edit: \" + repr(widget))\n\t\t\t\t\twidget._urwideOnEdit = handler\n\t\t\t\telif event == \"focus\":\n\t\t\t\t\twidget._urwideOnFocus = handler\n\t\t\t\telif event == \"key\":\n\t\t\t\t\twidget._urwideOnKey = handler\n\t\t\t\telse:\n\t\t\t\t\traise UISyntaxError(\"Unknown event type: \" + event)\n\t\tif _ui.get(\"info\"):\n\t\t\twidget._urwideInfo = _ui[\"info\"]\n\t\tif _ui.get(\"tooltip\"):\n\t\t\twidget._urwideTooltip = _ui[\"tooltip\"]\n\t\treturn self._styleWidget( widget, _ui )\n\n\t# WIDGET-SPECIFIC METHODS\n\t# -------------------------------------------------------------------------\n\n\tdef _argsFind( self, data ):\n\t\targs = data.find(\"args:\")\n\t\tif args == -1:\n\t\t\tattr = \"\"\n\t\telse:\n\t\t\tattr = data[args+5:]\n\t\t\tdata = data[:args]\n\t\treturn attr, data\n\n\tdef _parseTxt( self, data ):\n\t\tattr, data = self._argsFind(data)\n\t\tui, args, kwargs = self._parseAttributes(attr)\n\t\tself._add(self._createWidget(urwid.Text,data, ui=ui, args=args, kwargs=kwargs))\n\n\tdef _parseHdr( self, data ):\n\t\tif self._header is not None:\n\t\t\traise UISyntaxError(\"Header can occur only once\")\n\t\tattr, data = self._argsFind(data)\n\t\tui, args, kwargs = self._parseAttributes(attr)\n\t\tui.setdefault(\"style\", \"header\")\n\t\tself._header = self._createWidget(urwid.Text, data, ui=ui, args=args, kwargs=kwargs)\n\n\tRE_BTN = re.compile(\"\\s*\\[([^\\]]+)\\]\")\n\tdef _parseBtn( self, data ):\n\t\tmatch = self.RE_BTN.match(data)\n\t\tif not match: raise SyntaxError(\"Malformed button: \" + repr(data))\n\t\tdata  = data[match.end():]\n\t\tself._add(self._createWidget(urwid.Button, match.group(1), self._doPress, data=data))\n\n\tRE_CHC = re.compile(\"\\s*\\[([xX ])\\:(\\w+)\\](.+)\")\n\tdef _parseChc( self, data ):\n\t\tattr, data = self._argsFind(data)\n\t\t# Parses the declaration\n\t\tmatch = self.RE_CHC.match(data)\n\t\tif not match: raise SyntaxError(\"Malformed choice: \" + repr(data))\n\t\tstate = match.group(1) != \" \"\n\t\tgroup = group_name = match.group(2).strip()\n\t\tgroup = self._groups.setdefault(group,[])\n\t\tassert self._groups[group_name] == group\n\t\tassert getattr(self.groups,group_name) == group\n\t\tlabel = match.group(3)\n\t\t# Parses the attributes\n\t\tui, args, kwargs = self._parseAttributes(attr)\n\t\t# Creates the widget\n\t\tself._add(self._createWidget(urwid.RadioButton, group, label, state,\n\t\tself._doPress,  ui=ui, args=args, kwargs=kwargs))\n\n\tdef _parseDvd( self, data ):\n\t\tui, args, kwargs = self._parseAttributes(data[3:])\n\t\tself._add(self._createWidget(urwid.Divider, data, ui=ui, args=args, kwargs=kwargs))\n\n\tdef _parseBox( self, data ):\n\t\tdef end( content, ui=None, **kwargs ):\n\t\t\tif not content: content = [self.EMPTY]\n\t\t\tif len(content) == 1: w = content[0]\n\t\t\telse: w = self._createWidget(urwid.Pile, content)\n\t\t\tborder = kwargs.get('border') or 1\n\t\t\tw = self._createWidget(urwid.Padding, w, ('fixed left', border), ('fixed right', border) )\n\t\t\t# TODO: Filler does not work\n\t\t\t# w = self._createWidget(urwid.Filler, w, ('fixed top', border), ('fixed bottom', border) )\n\t\t\t# w = urwid.Filler(w,  ('fixed top', 1),  ('fixed bottom',1))\n\t\t\tself._add(w)\n\t\tui, args, kwargs = self._parseAttributes(data)\n\t\tself._push(end, ui=ui, args=args, kwargs=kwargs)\n\n\tRE_EDT = re.compile(\"([^\\[]*)\\[([^\\]]*)\\]\")\n\tdef _parseEdt( self, data ):\n\t\tmatch = self.RE_EDT.match(data)\n\t\tdata  = data[match.end():]\n\t\tlabel, text = match.groups()\n\t\tui, args, kwargs = self._parseAttributes(data)\n\t\tif label and self.hasStyle('label'): label = ('label', label)\n\t\tself._add(self._createWidget(urwid.Edit, label, text,\n\t\tui=ui, args=args, kwargs=kwargs))\n\n\tdef _parsePle( self, data ):\n\t\tdef end( content, ui=None, **kwargs ):\n\t\t\tif not content: content = [self.EMPTY]\n\t\t\tself._add(self._createWidget(urwid.Pile, content, ui=ui, kwargs=kwargs))\n\t\tui, args, kwargs = self._parseAttributes(data)\n\t\tself._push(end, ui=ui, args=args, kwargs=kwargs)\n\n\tdef _parseCol( self, data ):\n\t\tdef end( content, ui=None, **kwargs ):\n\t\t\tif not content: content = [self.EMPTY]\n\t\t\tself._add(self._createWidget(urwid.Columns, content, ui=ui, kwargs=kwargs))\n\t\tui, args, kwargs = self._parseAttributes(data)\n\t\tself._push(end, ui=ui, args=args, kwargs=kwargs)\n\n\tdef _parseGFl( self, data ):\n\t\tdef end( content, ui=None, **kwargs ):\n\t\t\tmax_width = 0\n\t\t\t# Gets the maximum width for the content\n\t\t\tfor widget in content:\n\t\t\t\tif hasattr(widget, \"get_text\"):\n\t\t\t\t\tmax_width = max(len(widget.get_text()), max_width)\n\t\t\t\tif hasattr(widget, \"get_label\"):\n\t\t\t\t\tmax_width = max(len(widget.get_label()), max_width)\n\t\t\tkwargs.setdefault(\"cell_width\", max_width + 4)\n\t\t\tkwargs.setdefault(\"h_sep\", 1)\n\t\t\tkwargs.setdefault(\"v_sep\", 1)\n\t\t\tkwargs.setdefault(\"align\", \"center\")\n\t\t\tself._add(self._createWidget(urwid.GridFlow, content, ui=ui, kwargs=kwargs))\n\t\tui, args, kwargs = self._parseAttributes(data)\n\t\tself._push(end, ui=ui, args=args, kwargs=kwargs)\n\n\tdef _parseLBx( self, data ):\n\t\tdef end( content, ui=None, **kwargs ):\n\t\t\tself._add(self._createWidget(urwid.ListBox, content, ui=ui, kwargs=kwargs))\n\t\tui, args, kwargs = self._parseAttributes(data)\n\t\tself._push(end, ui=ui, args=args, kwargs=kwargs)\n\n\tdef _parseEnd( self, data ):\n\t\tif data.strip(): raise UISyntaxError(\"End takes no argument: \" + repr(data))\n\t\t# We get the end callback that will instanciate the widget and add it to\n\t\t# the content.\n\t\tif not self._stack: raise SyntaxError(\"End called without container widget\")\n\t\tend_content, end_callback, end_ui, end_args, end_kwargs = self._pop()\n\t\tend_callback(end_content, end_ui, *end_args, **end_kwargs)\n\n# ------------------------------------------------------------------------------\n#\n# CONSOLE CLASS\n#\n# ------------------------------------------------------------------------------\n\nclass Console(UI):\n\t\"\"\"The console class allows to create console applications that work 'full\n\tscreen' within a terminal.\"\"\"\n\n\tdef __init__( self ):\n\t\tUI.__init__(self)\n\t\tself._ui          = None\n\t\tself._frame       = None\n\t\tself._header      = None\n\t\tself._footer      = None\n\t\tself._listbox     = None\n\t\tself._dialog      = None\n\t\tself._tooltiptext = \"\"\n\t\tself._infotext    = \"\"\n\t\tself._footertext  = \"\"\n\t\tself.isRunning    = False\n\t\tself.endMessage   = \"\"\n\t\tself.endStatus    = 1\n\n\t# USER INTERACTION API\n\t# -------------------------------------------------------------------------\n\n\tdef tooltip( self, text=-1 ):\n\t\t\"\"\"Sets/Gets the current tooltip text.\"\"\"\n\t\tif text == -1:\n\t\t\treturn self._tooltiptext\n\t\telse:\n\t\t\tself._tooltiptext = ensureUnicode(text)\n\n\tdef info( self, text=-1 ):\n\t\t\"\"\"Sets/Gets the current info text.\"\"\"\n\t\tif text == -1:\n\t\t\treturn self._infotext\n\t\telse:\n\t\t\tself._infotext = ensureUnicode(text)\n\n\tdef footer( self, text=-1 ):\n\t\t\"\"\"Sets/Gets the current footer text.\"\"\"\n\t\tif text == -1:\n\t\t\treturn self._footertext\n\t\telse:\n\t\t\tself._footertext = ensureUnicode(text)\n\n\tdef dialog( self, dialog ):\n\t\t\"\"\"Sets the dialog as this UI dialog. All events will be forwarded to\n\t\tthe dialog until exit.\"\"\"\n\t\tself._dialog = dialog\n\n\t# WIDGET INFORMATION\n\t# -------------------------------------------------------------------------\n\n\tdef getFocused( self ):\n\t\t\"\"\"Gets the focused widget\"\"\"\n\t\t# We get the original widget to focus on\n\t\tfocused     = original_widget(self._listbox.get_focus()[0])\n\t\told_focused = None\n\t\twhile focused != old_focused:\n\t\t\told_focused = focused\n\t\t\t# There are some types that are not focuable\n\t\t\tif isinstance(focused, urwid.AttrWrap):\n\t\t\t\tif focused.w: focused = focused.w\n\t\t\telif isinstance(focused, urwid.Padding):\n\t\t\t\tif focused.min_width: focused = focused.min_width\n\t\t\telif isinstance(focused, urwid.Filler):\n\t\t\t\tif focused.w: focused = focused.w\n\t\t\telif hasattr(focused, \"get_focus\"):\n\t\t\t\tif focused.get_focus(): focused = focused.get_focus()\n\t\treturn focused\n\n\tdef focusNext( self ):\n\t\tfocused = self._listbox.get_focus()[1] + 1\n\t\tself._listbox.set_focus(focused)\n\t\twhile True:\n\t\t\tif not self.isFocusable(self.getFocused()) \\\n\t\t\tand self._listbox.body.get_next(focused)[0] is not None:\n\t\t\t\tfocused += 1\n\t\t\t\tself._listbox.set_focus(focused)\n\t\t\telse:\n\t\t\t\tbreak\n\n\tdef focusPrevious( self ):\n\t\tfocused = max(self._listbox.get_focus()[1] - 1, 0)\n\t\tself._listbox.set_focus(focused)\n\t\twhile True:\n\t\t\tif not self.isFocusable(self.getFocused()) \\\n\t\t\tand focused > 0:\n\t\t\t\tfocused -= 1\n\t\t\t\tself._listbox.set_focus(focused)\n\t\t\telse:\n\t\t\t\tbreak\n\n\tdef getToplevel( self ):\n\t\t\"\"\"Returns the toplevel widget, which may be a dialog's view, if there\n\t\twas a dialog.\"\"\"\n\t\tif self._dialog:\n\t\t\treturn self._dialog.view()\n\t\telse:\n\t\t\treturn self._frame\n\n\tdef getCurrentSize( self ):\n\t\t\"\"\"Returns the current size for this UI as a couple.\"\"\"\n\t\treturn self._currentSize\n\n\t# URWID EVENT-LOOP\n\t# -------------------------------------------------------------------------\n\n\tdef main( self ):\n\t\t\"\"\"This is the main event-loop. That is what you should invoke to start\n\t\tyour application.\"\"\"\n\t\t#self._ui = urwid.curses_display.Screen()\n\t\tself._ui  = urwid.raw_display.Screen()\n\t\tself._ui.clear()\n\t\tif self._palette: self._ui.register_palette(self._palette)\n\t\tself._ui.run_wrapper( self.run )\n\t\t# We clear the screen (I know, I should use URWID, but that was the\n\t\t# quickest way I found)\n\t\tcurses.setupterm()\n\t\tsys.stdout.write(curses.tigetstr('clear').decode())\n\t\tif self.endMessage:\n\t\t\tprint (self.endMessage)\n\t\treturn self.endStatus\n\n\tdef run( self ):\n\t\t\"\"\"Run function to be used by URWID. You should not call it directly,\n\t\tuse the 'main' function instead.\"\"\"\n\t\t#self._ui.set_mouse_tracking()\n\t\tself._currentSize = self._ui.get_cols_rows()\n\t\tself.isRunning    = True\n\t\twhile self.isRunning:\n\t\t\tself._currentSize = self._ui.get_cols_rows()\n\t\t\tself.loop()\n\n\tdef end( self, msg=None, status=1 ):\n\t\t\"\"\"Ends the application, registering the given 'msg' as end message, and\n\t\treturning the given 'status' ('1' by default).\"\"\"\n\t\tself.isRunning = False\n\t\tself.endMessage = msg\n\t\tself.endStatus  = status\n\n\tdef loop( self ):\n\t\t\"\"\"This is the main URWID loop, where the event processing and\n\t\tdispatching is done.\"\"\"\n\t\t# We get the focused element, and update the info and and tooltip\n\t\tif self._dialog:\n\t\t\tfocused = self._dialog.view()\n\t\telse:\n\t\t\tfocused = self.getFocused() or self._frame\n\t\t# We trigger the on focus event\n\t\tself._doFocus(focused, ensure=False)\n\t\t# We update the tooltip and info in the footer\n\t\tif hasattr(focused, \"_urwideInfo\"):\n\t\t\tself.info(self._strings.get(focused._urwideInfo) or focused._urwideInfo)\n\t\tif hasattr(focused, \"_urwideTooltip\"):\n\t\t\tself.tooltip(self._strings.get(focused._urwideTooltip) or focused._urwideTooltip)\n\t\t# We draw the screen\n\t\tself._updateFooter()\n\t\tself.draw()\n\t\tself.tooltip(\"\")\n\t\tself.info(\"\")\n\t\t# And process keys\n\t\tif not self.isRunning: return\n\t\tkeys    = self._ui.get_input()\n\t\tif isinstance(focused, urwid.Edit): old_text = focused.get_edit_text()\n\t\t# We handle keys\n\t\tfor key in keys:\n\t\t\t#if urwid.is_mouse_event(key):\n\t\t\t\t# event, button, col, row = key\n\t\t\t\t# self.view.mouse_event( self._currentSize, event, button, col, row, focus=True )\n\t\t\t\t#pass\n\t\t\t# NOTE: The key press might actually be send not to the focused\n\t\t\t# widget but to its original_widget\n\t\t\tif key == \"window resize\":\n\t\t\t\tself._currentSize = self._ui.get_cols_rows()\n\t\t\telif self._dialog:\n\t\t\t\tself._doKeyPress(self._dialog.view(), key)\n\t\t\telse:\n\t\t\t\tself._doKeyPress(focused, key)\n\t\t# We check if there was a change in the edit, and we fire and event\n\t\tif isinstance(focused, urwid.Edit):\n\t\t\tself._doEdit( focused, old_text, focused.get_edit_text(), ensure=False)\n\n\tdef draw( self ):\n\t\t\"\"\"Main loop to draw the console. This takes into account the fact that\n\t\tthere may be a dialog to display.\"\"\"\n\t\tif self._dialog is not None:\n\t\t\to = urwid.Overlay( self._dialog.view(), self._frame,\n\t\t\t\t\"center\",\n\t\t\t\tself._dialog.width(),\n\t\t\t\t\"middle\",\n\t\t\t\tself._dialog.height()\n\t\t\t)\n\t\t\tcanvas = o.render( self._currentSize, focus=True )\n\t\telse:\n\t\t\tcanvas = self._frame.render( self._currentSize, focus=True )\n\t\tself._ui.draw_screen( self._currentSize, canvas )\n\n\tdef _updateFooter(self):\n\t\t\"\"\"Updates the frame footer according to info and tooltip\"\"\"\n\t\tremove_widgets(self._footer)\n\t\tfooter = []\n\t\tif self.tooltip():\n\t\t\tfooter.append(self._styleWidget(urwid.Text(self.tooltip()), {'style':'tooltip'}))\n\t\tif self.info():\n\t\t\tfooter.append(self._styleWidget(urwid.Text(self.info()), {'style':'info'}))\n\t\tif self.footer():\n\t\t\tfooter.append(self._styleWidget(urwid.Text(self.footer()), {'style':'footer'}))\n\t\tif footer:\n\t\t\tfor _ in footer:\n\t\t\t\tadd_widget(self._footer, _)\n\t\t\tself._footer.set_focus(0)\n\n\tdef parseUI( self, text ):\n\t\t\"\"\"Parses the given text and initializes this user interface object.\"\"\"\n\t\tUI.parseUI(self, text)\n\t\tself._listbox     = self._createWidget(urwid.ListBox,self._content)\n\t\tself._footer      = urwid.Pile([self.EMPTY])\n\t\tself._frame       = self._createWidget(urwid.Frame,\n\t\t\tself._listbox,\n\t\t\tself._header,\n\t\t\tself._footer\n\t\t)\n\t\treturn self._content\n\n\tdef _parseFtr( self, data ):\n\t\tself.footer(data)\n\n# ------------------------------------------------------------------------------\n#\n# DIALOG CLASSES\n#\n# ------------------------------------------------------------------------------\n\nclass Dialog(UI):\n\t\"\"\"Utility class to create dialogs that will fit within a console\n\tapplication.\n\n\tSee the constructor documentation for more information.\"\"\"\n\n\tPALETTE = \"\"\"\n\tdialog        : BL, Lg, SO\n\tdialog.shadow : DB, BL, SO\n\tdialog.border : Lg, DB, SO\n\t\"\"\"\n\n\tdef __init__( self, parent, ui, width=40, height=-1, style=\"dialog\",\n\theader=\"\", palette=\"\"):\n\t\t\"\"\"Creates a new dialog that will be attached to the given 'parent'. The\n\t\tuser interface is described by the 'ui' string. The dialog 'width' and\n\t\t'height' will indicate the dialog size, when 'height' is '-1', it will\n\t\tbe automatically computed from the given 'ui'.\"\"\"\n\t\tUI.__init__(self)\n\t\tif height == -1: height = ui.count(\"\\n\") + 1\n\t\tself._width         = width\n\t\tself._height        = height\n\t\tself._style         = style\n\t\tself._view          = None\n\t\tself._headertext    = header\n\t\tself._parent        = parent\n\t\tself._startCallback = lambda x:x\n\t\tself._endCallback   = lambda x:x\n\t\tself._palette       = None\n\t\tself.make(ui, palette)\n\n\tdef width( self ):\n\t\t\"\"\"Returns the dialog width\"\"\"\n\t\treturn self._width\n\n\tdef height( self ):\n\t\t\"\"\"Returns the dialog height\"\"\"\n\t\treturn self._height\n\n\tdef view( self ):\n\t\t\"\"\"Returns the view attached to this 'Dialog'. The _view_ is created by\n\t\tthe 'make' method, and is an 'urwid.Frame' instance.\"\"\"\n\t\tassert self._view\n\t\treturn self._view\n\n\tdef make( self, uitext, palui=None ):\n\t\t\"\"\"Makes the dialog using a UI description ('uitext') and a style\n\t\tdefinition for the palette ('palui'), which can be 'None', in which case\n\t\tthe value will be 'Dialog.PALETTE'.\"\"\"\n\t\tif not palui: palui = self.PALETTE\n\t\tself.parseStyle(palui)\n\t\tstyle = self._styleWidget\n\t\tassert self._view is None\n\t\tcontent = []\n\t\tif self._headertext:\n\t\t\tcontent.append(style(urwid.Text(self._headertext), {'style':(self._style +'.header', \"dialog.header\", 'header')}))\n\t\t\tcontent.append(urwid.Text(\"\"))\n\t\t\tcontent.append(urwid.Divider(\"_\"))\n\t\tcontent.extend(self.parseUI(uitext))\n\t\tw = style(urwid.ListBox(content), {'style':(self._style +'.content', \"dialog.content\", self._style)})\n\t\t# We wrap the dialog into a box\n\t\tw = urwid.Padding(w, ('fixed left', 1), ('fixed right', 1))\n\t\t#w = urwid.Filler(w,  ('fixed top', 1),  ('fixed bottom',1))\n\t\tw = style(w,  {'style':(self._style+\".body\", \"dialog.body\", self._style)} )\n\t\tw = style( w, {'style':(self._style, \"dialog\")} )\n\t\t# Shadow\n\t\tshadow = self.hasStyle( self._style + \".shadow\", \"dialog.shadow\", \"shadow\")\n\t\tborder = self.hasStyle( self._style + \".border\", \"dialog.border\", \"border\")\n\t\tif shadow:\n\t\t\tborder = (border, '  ') if border else '  '\n\t\t\tw = urwid.Columns([w,('fixed', 2, urwid.AttrWrap(urwid.Filler(urwid.Text(border), \"top\") ,shadow))])\n\t\t\tw = urwid.Frame( w, footer = urwid.AttrWrap(urwid.Text(border),shadow))\n\t\tself._view = w\n\t\tself._startCallback(self)\n\t\tw._urwideOnKey = self.doKeyPress\n\n\tdef onStart( self, callback ):\n\t\t\"\"\"Registers the callback that will be triggered on dialog start.\"\"\"\n\t\tself._startCallback = callback\n\n\tdef onEnd( self, callback ):\n\t\t\"\"\"Registers the callback that will be triggered on dialog end.\"\"\"\n\t\tself._endCallback = callback\n\n\tdef doKeyPress( self, widget, key ):\n\t\tself._handle(\"keyPress\", widget, key)\n\n\tdef end( self ):\n\t\t\"\"\"Call this to close the dialog.\"\"\"\n\t\tself._endCallback(self)\n\t\tself._parent._dialog = None\n\n\tdef _parseHdr( self, data ):\n\t\tif self._header is not None:\n\t\t\traise UISyntaxError(\"Header can occur only once\")\n\t\tattr, data = self._argsFind(data)\n\t\tui, args, kwargs = self._parseAttributes(attr)\n\t\tui.setdefault(\"style\", (\"dialog.header\", \"header\") )\n\t\tself._content.append( self._createWidget(urwid.Text, data, ui=ui, args=args, kwargs=kwargs))\n\n# ------------------------------------------------------------------------------\n#\n# HANDLER CLASS\n#\n# ------------------------------------------------------------------------------\n\nFORWARD = False\n\nclass Handler(object):\n\t\"\"\"A handler can be subclassed an can be plugged into a UI to react to a\n\tspecific set of events. The interest of handlers is that they can be\n\tdynamically switched, then making \"modal UI\" implementation easier.\n\n\tFor instance, you could have a handler for your UI in \"normal mode\", and\n\thave another handler when a dialog box is displayed.\"\"\"\n\n\tdef __init__( self ):\n\t\tself.ui = None\n\n\tdef respond( self, event, *args, **kwargs ):\n\t\t\"\"\"Responds to the given event name. An exception must be raised if the\n\t\tevent cannot be responded to. False is returned if the handler does not\n\t\twant to handle the event, True if the event was handled.\"\"\"\n\t\tresponder = self.responder(event)\n\t\treturn responder(*args, **kwargs) != FORWARD\n\n\tdef responds( self, event ):\n\t\t\"\"\"Tells if the handler responds to the given event.\"\"\"\n\t\t_event_name = \"on\" + event[0].upper() + event[1:]\n\t\tif hasattr(self, _event_name): return _event_name\n\t\telse: return None\n\n\tdef responder( self, event ):\n\t\t\"\"\"Returns the function that responds to the given event.\"\"\"\n\t\t_event_name = \"on\" + event[0].upper() + event[1:]\n\t\tif not hasattr(self, _event_name):\n\t\t\traise UIRuntimeError(\"Event not implemented: \" + event)\n\n\t\tres = getattr(self, _event_name)\n\t\tassert res\n\t\treturn res\n\n# EOF - vim: tw=80 ts=4 sw=4 noet\n"
  },
  {
    "path": "src/logs/log_utils.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport logging\nimport json\nfrom logging.handlers import RotatingFileHandler\n\ndef log_json(tojson: object) -> str:\n  return json.dumps(tojson, indent=2, sort_keys=True)\n\nclass NewLineFileHandler(RotatingFileHandler):\n  \"\"\"Handler that controls the writing of the newline character\"\"\"\n\n  special_code = '[!n]'\n\n  def emit(self, record) -> None:\n\n    if self.special_code in record.msg:\n      record.msg = record.msg.replace( self.special_code, '' )\n      self.terminator = ''\n    else:\n      self.terminator = '\\n'\n\n    return super().emit(record)\n\nclass NewLineStreamHandler(logging.StreamHandler):\n  \"\"\"Handler that controls the writing of the newline character\"\"\"\n\n  special_code = '[!n]'\n\n  def emit(self, record) -> None:\n\n    if self.special_code in record.msg:\n      record.msg = record.msg.replace( self.special_code, '' )\n      self.terminator = ''\n    else:\n      self.terminator = '\\n'\n\n    return super().emit(record)"
  },
  {
    "path": "src/logs/logger.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nimport logging\nfrom logging.handlers import RotatingFileHandler\nimport os\nfrom logs.log_utils import NewLineFileHandler, NewLineStreamHandler\n\n\nfile_log_format = logging.Formatter(\"%(asctime)s %(levelname)s %(funcName)s(%(lineno)d) %(message)s\")\nstream_log_format = logging.Formatter(\"%(asctime)s %(message)s\", \"%H:%M:%S\")\nlogFile = \"info.log\"\n\nfile_handler = RotatingFileHandler(logFile, mode=\"a\", maxBytes=15 * 1024 * 1024, backupCount=2, encoding=None, delay=0)\nfile_handler.setFormatter(file_log_format)\n\nstream_handler =  logging.StreamHandler()\nstream_handler.setFormatter(stream_log_format)\n\nlog = logging.getLogger(__name__)\n\nif \"DEBUG\" in os.environ:\n    log.setLevel(logging.DEBUG)\n    print(\"debug logging\")\nelse:\n    log.setLevel(logging.INFO)\n    print(\"info loggin\")\n\nlog.addHandler(stream_handler)\nlog.addHandler(file_handler)\n"
  },
  {
    "path": "src/menu.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport pyfiglet\nimport sys\nfrom logs.logger import log\nfrom config import config_menu\nfrom libs import urwide\nimport bot\n\n# This is the description of the actual interface\nCONSOLE_STYLE = \"\"\"\"\"\"\nCONSOLE_UI = \"\"\"\\\nHdr Header    args:#header\n---\nTxt Status: stopped                     args:#status\n===\n\nGFl\nBtn [Start]                       #start &press=started\nBtn [Config]                         &press=config\nBtn [Exit]                          &press=exit\nEnd\n\"\"\"\n\n# add when working: Btn [Log]                         &press=log\n\n# This is the handling code, providing the logic\nclass Handler(urwide.Handler):\n    def onStarted( self, button ):\n        if self.ui.widgets.status.text == \"Status: stopped\":\n            bot.run()\n            self.ui.widgets.status.set_text(\"Status: started\")\n        else:\n            self.ui.widgets.status.set_text(\"Status: stopped\")\n    def onConfig( self, button ):\n        config_menu.run()\n\n    def onExit( self, button ):\n        self.ui.end(\"Exit\")\n        log.info(\"Exiting Karma Bot Menu: Bye! :D\")\n        sys.exit()\n\n# We create a console application\nui = urwide.Console()\nui.create(CONSOLE_STYLE, CONSOLE_UI, Handler())\nui.widgets.header.set_text(\"Reddit Karma Farming Bot\")\n\n# bring this back later pyfiglet.figlet_format(\"Reddit Karma Farming Bot\", font=\"slant\")\n\ndef run():\n    ui.main()\n\nif __name__ == \"__main__\":\n    run()\n"
  },
  {
    "path": "src/tests/__init__.py",
    "content": ""
  },
  {
    "path": "src/tests/test_utils.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom .. import utils\n\ndef test_random_string():\n  string = utils.random_string(5)\n  assert type(string) is str\n  assert len(string) == 5\n"
  },
  {
    "path": "src/utils.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport os\nimport collections\nimport random\nimport time\nimport string\nfrom config.common_config import ENVAR_PREFIX\nfrom logs.logger import log\nimport urllib.request\n## HELPER VARS\n\nDAY = 86400\nMONTH = 2678400\nYEAR = 31536000\n\n\ndef random_string(length: int) -> str:\n  letters = string.ascii_lowercase\n  return ''.join(random.choice(letters) for i in range(length))\n\ndef prefer_envar(configs: dict) -> dict:\n  for config in list(configs):\n    config_envar = f\"{ENVAR_PREFIX}{config}\".lower()\n    if os.environ.get(config_envar):\n      configs[config]=os.environ.get(config_envar)\n      log.info(f\"loading {config_envar} from envar. Value: {configs.get(config)}\")\n    else:\n      log.debug(f\"no environment config for: {config_envar}\")\n\n  return configs\n\n# Checks if the machine has internet and also can connect to reddit\ndef check_internet(host=\"https://reddit.com\", timeout=5):\n    try:\n        urllib.request.urlopen(host, None, timeout)\n        return True\n    except Exception as ex:\n        log.error(ex)\n        return False\n\n\ndef get_public_ip():\n    try:\n        external_ip = urllib.request.urlopen(\"https://api.ipify.org\")\n        if external_ip:\n            return external_ip.read().decode(\"utf-8\")\n    except Exception as e:\n        log.error(\"could not check external ip\")\n\n\ndef bytesto(bytes, to, bsize=1024):\n    \"\"\"convert bytes to megabytes, etc.\n      sample code:\n          print('mb= ' + str(bytesto(314575262000000, 'm')))\n      sample output:\n          mb= 300002347.946\n  \"\"\"\n\n    a = {\"k\": 1, \"m\": 2, \"g\": 3, \"t\": 4, \"p\": 5, \"e\": 6}\n    r = float(bytes)\n    for i in range(a[to]):\n        r = r / bsize\n\n    return round(r)\n\n\ndef is_past_one_day(time_to_compare):\n    return int(time.time()) - time_to_compare >= DAY\n\n\ndef countdown(seconds=1):\n    # log.info(\"sleeping: \" + str(seconds) + \" seconds\")\n    # for i in range(seconds, 0, -1):\n    #     # print(\"\\x1b[2K\\r\" + str(i) + \" \")\n    #     time.sleep(3)\n    # log.info(\"waking up\")\n    time.sleep(seconds)\n\n\ndef chance(value=.20):\n    rando = random.random()\n    # log.info(\"prob: \" + str(value) + \" rolled: \" + str(rando))\n    return rando < value\n\n\n\ndef tobytes(size_str):\n    \"\"\"Convert human filesizes to bytes.\n    https://stackoverflow.com/questions/44307480/convert-size-notation-with-units-100kb-32mb-to-number-of-bytes-in-python\n    Special cases:\n     - singular units, e.g., \"1 byte\"\n     - byte vs b\n     - yottabytes, zetabytes, etc.\n     - with & without spaces between & around units.\n     - floats (\"5.2 mb\")\n    To reverse this, see hurry.filesize or the Django filesizeformat template\n    filter.\n    :param size_str: A human-readable string representing a file size, e.g.,\n    \"22 megabytes\".\n    :return: The number of bytes represented by the string.\n    \"\"\"\n    multipliers = {\n        'kilobyte':  1024,\n        'megabyte':  1024 ** 2,\n        'gigabyte':  1024 ** 3,\n        'kb': 1024,\n        'mb': 1024**2,\n        'gb': 1024**3,\n    }\n\n    for suffix in multipliers:\n        size_str = size_str.lower().strip().strip('s')\n        if size_str.lower().endswith(suffix):\n            return int(float(size_str[0:-len(suffix)]) * multipliers[suffix])\n    else:\n        if size_str.endswith('b'):\n            size_str = size_str[0:-1]\n        elif size_str.endswith('byte'):\n            size_str = size_str[0:-4]\n    return int(size_str)\n"
  }
]