master 4dd8373063de cached
44 files
100.7 KB
28.3k tokens
179 symbols
1 requests
Download .txt
Repository: MrPowerScripts/reddit-karma-farming-bot
Branch: master
Commit: 4dd8373063de
Files: 44
Total size: 100.7 KB

Directory structure:
gitextract_wcx03rw_/

├── .circleci/
│   └── config.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Pipfile
├── README.md
├── deps/
│   └── windows/
│       ├── PyStemmer-2.0.1-cp39-cp39-win_amd64.whl
│       └── windows.ps1
├── docs/
│   ├── 1-getting-started.md
│   ├── 2-linux-macos.md
│   ├── 3-windows.md
│   └── 4-docker-guide.md
├── run_linux.sh
├── run_windows.bat
└── src/
    ├── __init__.py
    ├── apis/
    │   ├── __init__.py
    │   ├── pushshift.py
    │   └── reddit.py
    ├── bot.py
    ├── bots/
    │   └── reddit/
    │       ├── __init__.py
    │       ├── actions/
    │       │   ├── cleanup_actions.py
    │       │   ├── comments/
    │       │   │   ├── comment_actions.py
    │       │   │   └── sources/
    │       │   │       └── cobe.py
    │       │   ├── post_actions.py
    │       │   └── utils.py
    │       ├── bot.py
    │       └── utils.py
    ├── config/
    │   ├── cobe_config.py
    │   ├── common_config.py
    │   ├── config_menu.py
    │   ├── reddit/
    │   │   ├── config_gen.py
    │   │   ├── reddit_avoid_subs.txt
    │   │   ├── reddit_avoid_words.txt
    │   │   └── reddit_sub_lists.py
    │   ├── reddit_config.py
    │   └── test.yml
    ├── init.py
    ├── libs/
    │   └── urwide.py
    ├── logs/
    │   ├── log_utils.py
    │   └── logger.py
    ├── menu.py
    ├── tests/
    │   ├── __init__.py
    │   └── test_utils.py
    └── utils.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .circleci/config.yml
================================================
version: 2.1
orbs:
  python: circleci/python@1.0.0

jobs:
  build:
    executor:
      name: python/default
      tag: "3.9"
    steps:
      - checkout
      - python/install-packages:
          args: pytest
          pkg-manager: pipenv
      # NEED TO FIX THE STUPID TESTS
      # - run: pipenv run coverage run --source=./src -m pytest --junitxml=./junit/junit.xml
      # - run: pipenv run coverage report
      # - run: pipenv run coverage html
      - store_artifacts:
          path: ./htmlcov
          destination: htmlcov
      - store_test_results:
          path: ./junit

workflows:
  main:
    jobs:
      - build


================================================
FILE: .gitignore
================================================
*.pyc
*.db*
*.log*
*.info
venv
settings.py
.DS_Store
macos.sh
.vscode
src/db.json
node_modules
.pytest_cache
.venv
.env
__pycache__
.coverage
htmlcov
junit
brainss
config.json
.envv

================================================
FILE: Dockerfile
================================================
FROM python:3.8

COPY . /app
WORKDIR /app
RUN apt update && apt install -yqq g++ gcc libc6-dev make pkg-config libffi-dev python3-dev git
RUN pip3 install pipenv
RUN pipenv install --system --deploy --ignore-pipfile
RUN chmod +x /app/run_linux.sh
ENTRYPOINT /app/run_linux.sh




================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 MrPowerScripts

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Pipfile
================================================
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
psaw = "*"
praw = "*"
pytest = "*"
coverage = "*"
cobe = "*"
urwide = "*"
pyfiglet = "*"
urwid = "*"
requests = "*"


================================================
FILE: README.md
================================================
**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.**

And 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.

# Reddit Karma Farming Bot

## Videos and links

This 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 

![farm karma 1](https://user-images.githubusercontent.com/1307942/86540032-7e1a2c00-bef9-11ea-9266-16830c5b9dfa.png)
![farm karma bot](https://user-images.githubusercontent.com/1307942/86153469-a40a8f80-baf9-11ea-80b5-d86dd31108d6.png)

### Video install guides
[Windows](https://youtu.be/6ICjZUHO2_I)

[Linux/macOS](https://youtu.be/ga0OC6lYSRs)

### 2020 update videos

[Definitely Watch This One](https://www.youtube.com/watch?v=nWYRGXesb3I)

[2020 Bot 3.0 Code Walkthrough](https://www.youtube.com/watch?v=83zWIz3b7o0)

### Older videos

[Karma Farming Bot 2.0 Video](https://www.youtube.com/watch?v=CCMGHepPBso)  
[Karma Farming on Reddit Video](https://www.youtube.com/watch?v=8DrOERA5FGc)  
[Karma Farming Bot 1.0 Video](https://www.youtube.com/watch?v=KgWsqKkDEtI)  

Subscribe: http://bit.ly/mrps-yt-sub  
Website: https://bit.ly/mrps-site  

## Getting Started

1. Follow the [getting started guide](docs/1-getting-started.md) to create your Reddit app and learn how to configure the bot.

2. 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.

## Features

- Run on Linux, MacOS, or Windows.
- Automatically reposts popular posts from the past to earn post karma.
- Automatically generates unique (somewhat) contextually relevant comments using [cobe](https://github.com/pteichman/cobe).
- Automatically deletes poor performing comments and posts.
- Configurable frequency of posting, commenting, and other actions.
- Filter the bot from learning certain words, or avoid certain subreddits.
- Schedule when the bot wakes up and sleeps to run actions.
- Auto detects if the account is shadowbanned.

## Warnings

### Reddit

New 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.

### Heroku

The bot used to have a Heroku option - till they found out and now using the bot on heroku will get your account banned.



================================================
FILE: deps/windows/windows.ps1
================================================
$install_help = "Read windows installation guide https://github.com/MrPowerScripts/reddit-karma-farming-bot/blob/master/docs/3-windows.md"

if (!(get-command python)) {
  write-host "Python not found"
  write-host $install_help
  exit 1
} else {write-host "Python found!"}

# make sure visual studio C++ build tools
if (Test-Path -Path "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools") {
  Write-Host "Found VS 2019 Build Tools - Excellent"
} else {
  Write-Host "VS 2019 Build Tools not found"
  Write-Host $install_help
  exit 1
}

#check for PyStemmer
if (Test-Path -Path "./deps/windows/PyStemmer-2.0.1-cp39-cp39-win_amd64.whl") {
  Write-Host "Found PyStemmer"
} else {
  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
}

#check if pipenv is installed
if (!(get-command pipenv)) {
  write-host "Pipenv not found - installing"
  & pip3 install pipenv
  exit 1
} else {write-host "Pipenv found!"}

#check for pipenv dependencies
if (Test-Path -Path "./.venv") {
  Write-Host "Pipenv deps installed"
} else {
  & pip3 install ./deps/windows/PyStemmer-2.0.1-cp39-cp39-win_amd64.whl
  & pipenv install
}


================================================
FILE: docs/1-getting-started.md
================================================
# Getting Started

## Creating the Reddit app

In 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

Once 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.

You 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. 

But like I said, it can be anything.

Then click Create App.
You will now be presented with this screen:

![app_example](https://user-images.githubusercontent.com/29954899/103455850-f8810880-4cf0-11eb-9002-64c2f1e5a44e.png)

In 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!

## Using a proxy

The 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.

## Reddit Configuration

### How to configure the Reddit bot

The 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.

#### Limit to specific subreddits

Add 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.

#### Avoid specific subreddits

Add 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.

#### Avoid specific words

Add 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.

#### Configure what actions the Reddit bot performs

The 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.

##### Sleep schedule

The 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": []`.

#### Configure Cobe

Cobe 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).


================================================
FILE: docs/2-linux-macos.md
================================================
# Running the bot on Linux and Macos

Run 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.

The 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.


================================================
FILE: docs/3-windows.md
================================================
# Running the bot on Windows 10

1. 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.

2. 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.

![image](https://user-images.githubusercontent.com/1307942/104216961-a77cbd00-5432-11eb-9aec-c56fcef58d2f.png)

3. Run `run_windows.bat`


================================================
FILE: docs/4-docker-guide.md
================================================
# Running the bot in Docker

## Update env file with your credentials
update and rename .env.example to .env

## Build docker image
From the root of the project, run this docker build command: `docker build -t reddit_karma_bot:latest . --no-cache`

## Run Docker Image
`docker run -d --name=reddit-bot reddit_karma_bot:latest`

## View Logs
`docker logs -f reddit-bot`


================================================
FILE: run_linux.sh
================================================
#!/usr/bin/env bash

DEBUG_FILE="./run_linux.log"
export PIPENV_VENV_IN_PROJECT=1

date '+%d/%m/%Y %H:%M:%S' | tee $DEBUG_FILE

unameOut="$(uname -s)"
case "${unameOut}" in
    Linux*)     machine=Linux;;
    Darwin*)    machine=Mac;;
    CYGWIN*)    machine=Cygwin;;
    MINGW*)     machine=MinGw;;
    *)          machine="UNKNOWN:${unameOut}"
esac
echo "system is ${machine}" | tee -a $DEBUG_FILE

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

if [ ! -d "$DIR/.venv" ]; then
  echo "no virtualenv detected doing setup before running" | tee -a $DEBUG_FILE
  echo "need to install dependencies" | tee -a $DEBUG_FILE
  if [ "${machine}" =  "Linux" ]; then
    echo "this is linux - install linux deps"
    apt-get update || { echo 'apt-get failed failed' | tee -a $DEBUG_FILE ; exit 1; }

    apt-get install -y --no-install-recommends \
      g++ \
      gcc \
      libc6-dev \
      make \
      pkg-config \
      libffi-dev \
      python3.6 \
      python3-pip \
      python3-setuptools \
      python3-dev \
      git || { echo 'Installing dependencies failed' | tee -a $DEBUG_FILE ; exit 1; }
  elif [ "${machine}" =  "Mac" ]; then
    $(xcode-select -p) || xcode-select --install
  else
    echo "No suitable linux version!" | tee -a $DEBUG_FILE
  fi

  if [ "${machine}" =  "Linux" ] || [ "${machine}" =  "Mac" ]; then
    pip3 install pipenv || { echo 'Installing virtualenv failed' | tee -a $DEBUG_FILE ; exit 1; }
    pipenv install || { echo 'Installing python dependencies failed' | tee -a $DEBUG_FILE ; exit 1; }
  fi
fi

echo "Trying to run the bot" | tee -a $DEBUG_FILE

# start bot directly if nomenu passed in to script
if [[ $1 == *"menu"* ]]; then
  echo "Running with menu" | tee -a $DEBUG_FILE
  pipenv run python3 ./src/menu.py "$@"
else
  echo "Running without menu" | tee -a $DEBUG_FILE
  pipenv run python3 ./src/init.py "$@"
fi


================================================
FILE: run_windows.bat
================================================
@ECHO OFF

Powershell.exe -executionpolicy bypass -File ./deps/windows/windows.ps1
if errorlevel 1 pause & exit 

if "%1"=="" (
  echo running without menu
  pipenv run python ./src/init.py
)

if "%1"=="menu" (
  echo running with menu
  pipenv run python ./src/menu.py
)

echo exiting...
pause

================================================
FILE: src/__init__.py
================================================


================================================
FILE: src/apis/__init__.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from config import reddit_config
from .reddit import RedditAPI
from .pushshift import PS

reddit_api = RedditAPI(**reddit_config.AUTH).api

pushshift_api = PS()

================================================
FILE: src/apis/pushshift.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
# data sources for comment learning
from psaw import PushshiftAPI
from logs.logger import log
from utils import DAY, YEAR
import requests
import time


class PS():
  def __init__(self):
    self.api = PushshiftAPI()

  def get_posts(self, subreddit, **kwargs):
    post = self._ps_search(subreddit, **kwargs)
    # log.info(f"post: {post}")
    return post

  def get_comments(self, subreddit):
    return self.api.search_comments(q='', subreddit=subreddit)

  def _ps_search(self, subreddit, before=None, after=None, score=None, limit=1):
    cur_time = int(time.time())
    after=(cur_time - YEAR) if after is None else None
    before=(cur_time - (YEAR - DAY)) if before is None else None
    score = 5000 if score is None else None
    url = f"https://api.pushshift.io/reddit/search/submission/?subreddit={subreddit}"
    url = url + (f"&before={before}" if before else "")
    url = url + (f"&after={after}" if after else "")
    url = url + (f"&score>={score}" if score else "")
    url = url + (f"&limit={limit}" if limit else "")
    url = url + (f"&author!=[deleted]&selftext:not=[deleted]") # avoids deleted posts
    log.info(f"pushshift-url: {url}")

    try:
      response = requests.get(url).json().get("data", [])
      return response
    except Exception as e:
      # unable to get data from pushshift
      return None


================================================
FILE: src/apis/reddit.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from praw import Reddit
from utils import random_string


class RedditAPI():
  def __init__(self,
              reddit_client_id,
              reddit_client_secret,
              reddit_password,
              reddit_username):

    self.client_id = reddit_client_id
    self.client_secret = reddit_client_secret
    self.password = reddit_password
    self.username = reddit_username
    self.user_agent = random_string(10)
    self.api = Reddit(
        client_id=self.client_id,
        client_secret=self.client_secret,
        password=self.password,
        user_agent=self.user_agent,
        username=self.username,
    )


================================================
FILE: src/bot.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from bots.reddit import RedditBot
from utils import countdown
from logs.logger import log

def run():
  reddit = RedditBot()
  while True:
    reddit.run()
    countdown(1)


================================================
FILE: src/bots/reddit/__init__.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from .bot import RedditBot



================================================
FILE: src/bots/reddit/actions/cleanup_actions.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import praw
import requests
from apis import pushshift_api, reddit_api
from config import reddit_config
from utils import chance
from logs.logger import log
import sys

class Cleanup():
  def __init__(self):
    self.psapi = pushshift_api
    self.rapi = reddit_api
    self.username = None

  def init(self):
    self.me = self.rapi.user.me
    self.username = self.me().name

  def shadow_check(self, roll=1):
    if chance(roll):
      log.info("performing a shadowban check")
      response = requests.get(f"https://www.reddit.com/user/{self.username}/about.json",  headers = {'User-agent': f"hiiii its {self.username}"}).json()
      if "error" in response:
        if response["error"] == 404:
          log.info(f"account {self.username} is shadowbanned. poor bot :( shutting down the script...")
          sys.exit()
        else:
          log.info(response)
      else:
        log.info(f"{self.username} is not shadowbanned! We think..")

  def remove_low_scores(self, roll=1):
    comment_count = 0
    post_count = 0
    if chance(roll):
      log.info("checking for low score content to remove")
      for i in self.rapi.redditor(self.username).new(limit=500):
        if i.score <= reddit_config.CONFIG["reddit_low_score_threshold"]:
          if isinstance(i, praw.models.Comment):
            log.info(f"deleting comment(id={i.id}, body={i.body}, score={i.score}, subreddit={i.subreddit_name_prefixed}|{i.subreddit_id})")
            try:
              i.delete()
            except Exception as e:
              log.info(f"unable to delete comment(id={i.id}), skip...\n{e.message}")
            comment_count += 1
          else:
            log.info(f"deleting post(id={i.id}, score={i.score}, subreddit={i.subreddit_name_prefixed}|{i.subreddit_id})")
            try:
              i.delete()
            except Exception as e:
              log.info(f"unable to delete post(id={i.id}), skip...\n{e.message}")
            post_count += 1
            
          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')
      
      # GOOD BOT
      if (comment_count + post_count) == 0:
        log.info("no low score content to clean up. I'm a good bot! :^)")

  def karma_limit(self):
    # current karma limit
    ckl = reddit_config.CONFIG["reddit_comment_karma_limit"]
    # current post karma
    cck = self.me().comment_karma
    # post karma limit
    pkl = reddit_config.CONFIG["reddit_post_karma_limit"]
    # current post karma
    cpk = self.me().link_karma

    if ckl:
      if ckl < cck:
        log.info(f"Comment karma limit ({ckl}) exceeded! Your current comment karma: {cck}. Shutting down the script.")
        sys.exit()
      else:
        log.info(f"Comment karma limit ({ckl}) not reached. Current comment karma: {cck}")
        return
    
    if pkl:
      if pkl < cpk:
        log.info(f"Post karma limit ({pkl}) exceeded! Your current post karma: {cpk}. Shutting down the script.")
        sys.exit()
      else:
        log.info(f"Post karma limit ({pkl}) not reached. Current post karma: {cpk}")
        return

    log.info(f"No limits - ignoring.")



================================================
FILE: src/bots/reddit/actions/comments/comment_actions.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from .sources.cobe import Cobe
from logs.logger import log
from collections import namedtuple
from utils import chance
from apis import reddit_api
from config import reddit_config
from ..utils import get_subreddit, AVOID_WORDS
import random
from praw.exceptions import APIException

Source = namedtuple('Source', ['name', 'api'])

class Comments():
  def __init__(self, source='cobe'):
    self.ready = False
    self.config = reddit_config.CONFIG
    self.rapi = reddit_api
    self.source_name = source
    self.sources = {
      "cobe": Source('cobe', Cobe)
    }
    self.comments = self.sources.get(self.source_name).api()


  def init(self):
    log.info("intiializing comments")
    self.ready = False
    self.comments.init()
    self.ready = True
    log.info("commenting ready")

  def comment(self, roll=1):
    if not self.ready:
      log.info("comments need to be initialized")
      self.init()

    if chance(roll):
      log.info("going to make a comment")

      # keep searching posts until we find one with comments
      post_with_comments = False
      while not post_with_comments:
        # pick a subreddit to comment on
        subreddit = get_subreddit(getsubclass=True)
        # get a random hot post from the subreddit
        post = random.choice(list(subreddit.hot()))
        # replace the "MoreReplies" with all of the submission replies
        post.comments.replace_more(limit=0)

        if len(post.comments.list()) > 0:
          post_with_comments = True

      try:
        # choose if we're replying to the post or to a comment
        if chance(self.config.get('reddit_reply_to_comment')):
          # reply to the post with a response based on the post title
          log.info('replying directly to post')
          post.reply(self.comments.get_reply(post.title))
        else:
          # get a random comment from the post
          comment = random.choice(post.comments.list())
          # reply to the comment
          log.info('replying to comment')
          comment.reply(self.comments.get_reply(comment.body))
      except APIException as e:
        log.info(f"error commenting: {e}")






================================================
FILE: src/bots/reddit/actions/comments/sources/cobe.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from cobe.brain import Brain
from config.cobe_config import CONFIG
from apis import pushshift_api, reddit_api
from logs.logger import log
from utils import bytesto, tobytes
from ...utils import AVOID_WORDS, get_subreddit
import os, sys

class Cobe():
  def __init__(self, config=CONFIG):
    self.ready = False
    self.psapi = pushshift_api
    self.rapi = reddit_api
    self.config = CONFIG
    self.brain = Brain(self.config.get("cobe_main_db"))
    self.size = 0

  def get_reply(self, replyto: str=''):
    if self.ready:
      return self.brain.reply(replyto)
    else:
      log.info(f"cobe not initialized, run init")

  def init(self):
    log.info("using cobe to generate comments")
    main_db = self.config.get("cobe_main_db")
    
    # make sure db was initialized correctly
    if os.path.isfile(main_db):
      # set the initial size
      self.size = os.path.getsize(main_db)
    else:
      log.info(f"cobe db failed to initialize. exiting")
      sys.exit()

    log.debug('filling cobe database for commenting')
    # loop through learning comments until we reach the min db size
    while self.size <= tobytes(self.config.get("cobe_min_db_size")):

      log.info(f"cobe db size is: {str(bytesto(self.size, 'm'))}mb, need {self.config.get('cobe_min_db_size')} - learning...")
      
      # just learn from random subreddits for now
      subreddit = get_subreddit(getsubclass=True)
      
      log.info(f"learning from /r/{subreddit}")
      
      # get the comment generator function from pushshift
      comments = self.psapi.get_comments(subreddit)

      # go through 500 comments per subreddit
      for x in range(500):
        # get the comment from the generator function
        try:
          comment = next(comments)
        except StopIteration as e:
          log.info(f"end of comments")
        
        # bot responses are better when it learns from short comments
        if len(comment.body) < 240:
          log.debug(f"learning comment: {comment.body.encode('utf8')}")
          
          # only learn comments that don't contain an avoid word
          if not any(word in comment.body for word in AVOID_WORDS):
            self.brain.learn(comment.body.encode("utf8")) 

      # update the class size variable so the while loop
      # knows when to break
      self.size = os.path.getsize(main_db)

    log.info(f"database min size ({self.config.get('cobe_min_db_size')}) reached")
    self.ready = True

================================================
FILE: src/bots/reddit/actions/post_actions.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import random, requests, re
from time import sleep as s
from apis import pushshift_api, reddit_api
from utils import chance
from .utils import get_subreddit, AVOID_WORDS
from config.reddit_config import CONFIG
from config.reddit.reddit_sub_lists import CROSSPOST_SUBS
from logs.logger import log
from praw.exceptions import APIException

wordgroup = [['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'],]

def find_synonyms(keyword):
    keyword=keyword.lower()
    for sub_list in wordgroup:
        if keyword in sub_list:
            while True:
                word = random.choice(sub_list)
                if word != keyword:
                    # print('found "'+keyword+'"; chosen synonym "'+word+'"')
                    return word

title_chars=['!','.',';','?']
invisible_chars = ['‍      ','   ‏‏‎   ','‏‏‎‏‏‎‏‏‎‏‏‎­',' ⠀']

def edit_text(var, mode):
    if mode == 'body':
        mychars=[]
        if ' ' in var:
            for index, x in enumerate(var):
                if x == ' ':mychars.append(index)
            editedtext = list(var)
            editedtext[random.choice(mychars)] = random.choice(invisible_chars)
            editedtext = ''.join(editedtext)
            for x in editedtext.split():
                synonym=find_synonyms(x)
                if synonym != None:
                    words = editedtext.split()
                    words[words.index(x)] = synonym
                    editedtext = " ".join(words)
                    return editedtext
        else:return var
    elif mode == 'title':
        if any(not c.isalnum() for c in var[-2:]):return var.replace(var[-2:], var[-2]+' '+random.choice(title_chars))
        else:return var+random.choice(title_chars)

class Posts():
  def __init__(self):
    self.psapi = pushshift_api
    self.rapi = reddit_api

  def get_post(self, subreddit=None):
    log.info(f"finding a post to re-post")    
    got_post = False
    attempts = 0
    while not got_post:
      # use the supplied subreddit
      # otherwise choose one randomly
      if subreddit:
        log.info(f"searching post in sub: {subreddit}")
        sub = self.rapi.subreddit(subreddit)
      else:
        # if there are subreddits in the subreddit list pull randomly from that
        # otherwise pull a totally random subreddit
        sub = self.rapi.subreddit(random.choice(CONFIG['reddit_sub_list'])) if CONFIG['reddit_sub_list'] else get_subreddit(getsubclass=True)
          
        log.info(f"searching post in sub: {sub.display_name}")
      try:
        post_id = self.psapi.get_posts(sub.display_name)[0]['id']
        # don't use posts that have avoid words in title
        if not any(word in comment.body for word in AVOID_WORDS):
          got_post = True
      except Exception as e:
        log.info(f"couldn't find post in {sub}")
        # sub = self.rapi.random_subreddit(nsfw=False)
        # log.info(f"trying in: {subreddit}")
        attempts += 1
        log.info(f"repost attempts: {attempts}")
        if attempts > 3:
          log.info(f"couldn't find any posts - skipping reposting for now")
          return

    return self.rapi.submission(id=post_id)

  def crosspost(self, subreddit):
    for idx, subs in enumerate(CROSSPOST_SUBS):
      if subs[0] == subreddit:
        return random.choice(subs[idx])

  # why do my eyes hurt
  def repost(self, roll=1, subreddit=None):
    if chance(roll):
      log.info("running repost")
      # log.info("running _repost")
      post = self.get_post(subreddit=subreddit)
      if not post: return
      api_call=requests.get(post.url).status_code
      if api_call != 200:
        if api_call == 429:
          print('too many requests to pushshift')
          s(random.uniform(3,8))
        else:
          print('pushshift http error: '+str(api_call))
        return
      else:
        log.info(f"reposting post: {post.id}")
        
        if post.is_self:
          if post.selftext not in ('[removed]','[deleted]') and bool(re.findall(r'20[0-9][0-9]|v.redd.it', post.selftext)) == False:
            params = {"title": edit_text(post.title, 'title'), "selftext": edit_text(post.selftext, 'body')}
          else:
            print('Info: skipping post; it was malformed or date indicated')
            # print(post.selftext)
        else:params = {"title": edit_text(post.title, 'title'), "url": post.url}

        sub = post.subreddit

        # randomly choose a potential subreddit to cross post
        if CONFIG['reddit_crosspost_enabled']:
          sub = self.rapi.subreddit(self.crosspost(sub.display_name))
        try:
          self.rapi.subreddit(sub.display_name).submit(**params)
          return
        except (UnboundLocalError, TypeError):pass
        except APIException as e:
          log.info(f"REPOST ERROR: {e}")
          return
    else:
      pass
      # log.info("not running repost")
      # log.info("not running _repost")


## to do: add flairs compability or a way to avoid flairs


================================================
FILE: src/bots/reddit/actions/utils.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import random
from apis import reddit_api
from logs.logger import log
from config.reddit.reddit_sub_lists import REDDIT_APPROVED_SUBS
from config.common_config import CONFIG_ROOT

with open(f"{CONFIG_ROOT}/reddit/reddit_avoid_subs.txt", "r") as subfile:
  AVOID_SUBS = subfile.read().splitlines()
  subfile.close()

with open(f"{CONFIG_ROOT}/reddit/reddit_avoid_words.txt", "r") as wordfile:
  AVOID_WORDS = wordfile.read().splitlines()
  wordfile.close()

log.debug(f"avoiding subs: {AVOID_SUBS}")

def get_subreddit(nsfw=False, getsubclass=False):

  # if the subreddit list is being used jut return one from there
  if REDDIT_APPROVED_SUBS:
    log.info(f"picking subreddit from approved list")
    subreddit = reddit_api.subreddit(random.choice(REDDIT_APPROVED_SUBS).strip())
    log.info(f"using subreddit: {subreddit.display_name}")
  else:
    log.info(f"picking a random subreddit")
    # otherwise we'll do some logic to get a random subreddit
    subreddit_ok = False
    while not subreddit_ok:
      subreddit = reddit_api.random_subreddit(nsfw=nsfw)
      log.info(f"checking subreddit: {subreddit.display_name}")
      # make sure the radom sub isn't in the avoid sub list
      # keep searching for a subreddit until it meets this condition
      if subreddit.display_name not in AVOID_SUBS:
        subreddit_ok = True

  if getsubclass:
    return subreddit
  else:
    return subreddit.display_name

================================================
FILE: src/bots/reddit/bot.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from apis import reddit_api
from config import reddit_config
from utils import chance
from bots.reddit.actions.post_actions import Posts
from bots.reddit.actions.comments.comment_actions import Comments
from bots.reddit.actions.cleanup_actions import Cleanup
from logs.logger import log
from logs.log_utils import log_json
import time, sys, random
from collections import namedtuple
from .utils import should_we_sleep, parse_user

BotAction = namedtuple("BotAction", 'name call')

class RedditBot():
  def __init__(self, config=reddit_config.CONFIG):
    self.api = reddit_api
    self.ready = False
    self.config = config
    self.user = None
    self.posts = Posts()
    self.comments = Comments()
    self.cleanup = Cleanup()
    self.actions = [
      BotAction('reddit_post_chance', self.posts.repost),
      BotAction('reddit_comment_chance', self.comments.comment),
      BotAction('reddit_shadowban_check', self.cleanup.shadow_check),
      BotAction('reddit_remove_low_scores', self.cleanup.remove_low_scores),
      BotAction('reddit_karma_limit_check', self.cleanup.karma_limit),
    ]

  def _init(self):
    # check if account is set
    user = self.api.user.me()
    if user is None:
      log.info("User auth failed, Reddit bot shutting down")
      sys.exit()
    else:
      log.info(f"running as user: {user}")
      
    # check if account is shadowbanned
    self.cleanup.init()
    self.cleanup.shadow_check()
    self.user = parse_user(user)
    log.info(f"account info:\n{log_json(self.user)}")
    self.ready = True
    log.info("The bot is now running. It has a chance to perform an action every second. Be patient")

  def tick(self):
    if not should_we_sleep():
      report = f""
      for action in self.actions:
        roll = random.random()
        result = roll < self.config[action.name] 
        print(f"{roll} < {self.config[action.name]} = {result}         ", end="\r")
        if result:
          log.info(f"\nrunning action: {action.name}")
          action.call()


  def run(self):
    if self.ready:
      self.tick()
    else:
      self._init()
      self.run()
      # log.info("not running reddit bot - not ready")


================================================
FILE: src/bots/reddit/utils.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import datetime
from logs.logger import log
from config.reddit_config import CONFIG
import time
from praw.models.redditors import Redditors

## USER UTILS

def parse_user(user: Redditors):
  i = {}
  i['comment_karma'] = user.comment_karma
  i['link_karma'] = user.link_karma
  i['username'] = user.name
  i['created_utc'] = user.created_utc
  i['created_utc_human'] = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(user.created_utc)) 
  return i

## SCHEDULE UTILS

EASY_SCHEDULES = {
  1: ((7,00),(10,00)),
  2: ((10,00),(14,00)),
  3: ((14,00),(18,00)),
  4: ((18,00),(22,00)),
  5: ((22,00),(2,00)),
}

# convert the easy schedules to the tuple values
BOT_SCHEDULE = [EASY_SCHEDULES.get(schedule) for schedule in CONFIG['reddit_sleep_schedule']]

log.info(f"using schedules: {BOT_SCHEDULE}")

# transform the schedule with datetime formatting
updated_schedules = [((datetime.time(schedule[0][0], schedule[0][1])), (datetime.time(schedule[1][0], schedule[1][1]))) for schedule in BOT_SCHEDULE]

BOT_SCHEDULE = updated_schedules


def is_time_between(begin_time, end_time, check_time=None):
    # If check time is not given, default to current UTC time
    check_time = check_time or datetime.datetime.utcnow().time()
    if begin_time < end_time:
        return check_time >= begin_time and check_time <= end_time
    else: # crosses midnight
        return check_time >= begin_time or check_time <= end_time

def should_we_sleep():
    CHECKS = [True for schedule in BOT_SCHEDULE if is_time_between(schedule[0], schedule[1])]
    # check if any of the time between checks returned true.
    # if there's a True in the list, it means we're between one of the scheduled times
    # and so this function returns False so the bot doesn't sleep
    if True in CHECKS or not CONFIG.get('reddit_sleep_schedule'):
      # no need to sleep - the bot is within one of the time ranges
      return False
    else:
      log.info("it's sleepy time.. zzzzz :snore: zzzz")
      whats_left = []
      TIME_LEFT = [schedule[0] for schedule in BOT_SCHEDULE]
      for time_stamp in TIME_LEFT:
        # log.info(time_stamp)
        next_start = datetime.datetime.combine(datetime.date.today(), time_stamp)
        # log.info(f"next start: {next_start}")
        ts = int(next_start.timestamp())
        # if this goes negative then the next start is probably tomorrow
        if ts < int(time.time()):
          next_start = datetime.datetime.combine((datetime.date.today() + datetime.timedelta(days=1)), time_stamp)
          ts = next_start.timestamp()
          
        # collect all the seconds left for each time schedule to start
        # log.info(f"ts: {ts}")
        # log.info(f"time: {int(time.time())}")
        whats_left.append(ts - int(time.time()))
      
      #remove negative values and
      # get the shortest duration of time left before starting
      # log.info(whats_left)
      whats_left = [item for item in whats_left if item >= 0]

      # log.info(whats_left)
      time_left = int(min(whats_left))

      if time_left > 600:
        log.info(f"waking up in: {datetime.timedelta(seconds=time_left)} at {next_start}")
      
      sleep_time = int(time_left / 3)

      # have the bot sleep for a short while instead of tons of messages every second
      time.sleep(sleep_time)
      return True


================================================
FILE: src/config/cobe_config.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from utils import prefer_envar
from pathlib import Path
from logs.logger import log
from logs.log_utils import log_json
from .common_config import SRC_ROOT
import os

BASE_DIR = os.path.join(SRC_ROOT, 'bots/reddit/actions/comments')
DB_DIR = os.path.join(BASE_DIR, "brains")
MAIN_DB = os.path.join(DB_DIR, "brain.db")

if not os.path.exists(DB_DIR):
  os.makedirs(DB_DIR, exist_ok=True)

CONFIG = prefer_envar({
  # cobe config
  "cobe_base_dir": BASE_DIR,
  "cobe_db_dir": DB_DIR,
  "cobe_main_db": MAIN_DB,
  "cobe_min_db_size":"50mb",
  "cobe_max_db_size":"300mb",
})

log.info(f"COBE CONFIG:\n {log_json(CONFIG)}")

================================================
FILE: src/config/common_config.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import os
from logs.logger import log
from pathlib import Path

# Prefix that the bot uses to discover envars settings for the bots
ENVAR_PREFIX="BOT_"


CONFIG_ROOT = os.path.dirname(os.path.abspath(__file__))
config_root = Path(CONFIG_ROOT)
REPO_ROOT = config_root.parents[1].absolute()
SRC_ROOT = os.path.join(REPO_ROOT, "src")
ENV_FILE= os.path.join(REPO_ROOT, ".env")

log.info(f"config root: {CONFIG_ROOT}")
log.info(f"repo root: {REPO_ROOT}")
log.info(f"src root: {SRC_ROOT}")

# Common Values
DAY = 86400  # POSIX day (exact value in seconds)
MINUTE = 60  # seconds in a minute



================================================
FILE: src/config/config_menu.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import os
import sys
import json
import menu
import pathlib
from utils import prefer_envar
from libs import urwide
from .common_config import SRC_ROOT

CONFIG_JSON_FILE = os.path.join(SRC_ROOT, "config/config.json")

if os.path.isfile(CONFIG_JSON_FILE):
  with open(CONFIG_JSON_FILE, "r") as config_json:
    config_data = prefer_envar(json.load(config_json))
else:
  config_data = prefer_envar({
    "reddit_client_id":"",
    "reddit_client_secret":"",
    "reddit_username":"",
    "reddit_password":"",
  })

CONSOLE_STYLE = """"""

CONSOLE_UI = f'''\
Hdr Reddit Karma Bot Settings
---
Edt   Client ID          [{config_data["reddit_client_id"]}]          #clientid
Edt   Secret            [{config_data["reddit_client_secret"]}]    #secret
---
Edt   Username       [{config_data["reddit_username"]}]               #user
Edt   Password       [{config_data["reddit_password"]}]               #password
===
GFl
Btn [Cancel]                        #btn_cancel &press=cancel
Btn [Save]                          #btn_save   &press=save
End
'''

# Event handler
class Handler(urwide.Handler):

  def onSave( self, button ):
    self.ui.info("Saving")
    fields = self.ui.widgets
    
    config_data["reddit_client_id"] = fields.clientid.edit_text
    config_data["reddit_client_secret"] = fields.secret.edit_text
    config_data["reddit_username"] = fields.user.edit_text
    config_data["reddit_password"] = fields.password.edit_text

    with open(CONFIG_JSON_FILE, "w+") as config_file:
      config_file.write(json.dumps(config_data, indent=4, sort_keys=True))
      config_file.close()
    
    menu.run()


  def onCancel( self, button ):
    self.ui.info("Cancel")
    menu.run()

ui = urwide.Console()
ui.create(CONSOLE_STYLE, CONSOLE_UI, Handler())

# Main
def run():
  ui.main()

if __name__ == "__main__":
  run()


# EOF


================================================
FILE: src/config/reddit/config_gen.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import praw
import sys
import os
from logs.logger import log
from prawcore import ResponseException
from ..common_config import ENV_FILE



def config_gen():
  # ASK FOR CREDENTIALS
  CLIENT_ID = input('please input your account client id :')
  CLIENT_SECRET = input('please input your account client secret :')
  PASSWORD = input('please input your account password :')
  USERNAME = input('please input your account username :')

  reddit = praw.Reddit(
      client_id=CLIENT_ID,
      client_secret=CLIENT_SECRET,
      user_agent="my user agent",
      username=USERNAME,
      password=PASSWORD
  )

  # CHECK IF CREDENTIALS ARE CORRECT
  def authenticated(reddit):
      try:
          reddit.user.me()
      except ResponseException:
          return False
      else:
          return True


  # SAVE CONFIG FILE
  if authenticated(reddit):
      with open(ENV_FILE, "w") as file_object:
            file_object.write(f'bot_reddit_client_id="{CLIENT_ID}"\n')
            file_object.write(f'bot_reddit_client_secret="{CLIENT_SECRET}"\n')
            file_object.write(f'bot_reddit_password="{PASSWORD}"\n')
            file_object.write(f'bot_reddit_username="{USERNAME}"\n')
            print("Config file '.env' created. Please re-run the bot")
            sys.exit()
          
  else:
      print('WRONG CREDENTIALS!! TRY AGAIN')
      config_gen()



================================================
FILE: src/config/reddit/reddit_avoid_subs.txt
================================================
Agoraphobia
Anxiety
Assistance
AtheistTwelveSteppers
BipolarReddit
CBTpractice
DPDR
Depression
Diagnosed
DomesticViolence	
HardShipMates
HelpingHands
IHaveIssues
LGBT
LostALovedOne
NeedAFriend
OffMyChest	
OpiatesRecovery
Petloss
RandomKindness
RapeCounseling
SFTS
SMARTRecovery
SelfHelp
SingleParents
StopGaming
StopSelfHarm	
SuicideBereavement
SuicideWatch
TalkTherapy
abuse
abusiverelationships
addiction 
addictionprevention 
adhd
alcoholism
anarchism 
animetitties
anxiety
anxietyhelp
anythinggoesnews
aspergers
babyloss
behaviortherapy
benzorecovery
bidenpro 
bipolar
bipolar2
bipolarreddit
bodyacceptance 
bodydysmorphia
borderlinepdisorder
buylling
chapotraphouse 
childrenofdeadparents
comingout
communism
completeanarchy 
confession
conservative
cptsd
cripplingalcoholism
cutters
dbtselfhelp
deadredditors
democrats
depressed
depression
depression_help
depression_memes
depressionandPTSD
depressionregimens
downsyndrome
drugs
dysmorphicdisorder 
eatingdisorders
energy
enoughtrumpspam
esist 
feminism 
foreveralone
fuckthealtright
full_news
fullnews
gamernews 
geopolitics
griefsupport
healthanxiety
healthproject
helpmecope
humantrafficking
impeach_trump 
inmemoryof
inthenews
introvert
itgetsbetter
keep_track
kindvoice
leaves
lgbteens
liberal
libertarian
lonely
marchagainsttrump
mensrights
mentalhealth
mentalillness
mixednuts
neutralnews
news
obits
obituaries
ocd
ocpd
offbeat 
outhere
pandys
petioles
political_revolution
politics
problemgambling
ptsd
qualitynews
questioning
quittingkratom
raisedbynarcissists
rant
rants
rape
rapecounseling
recoverywithoutAA
redditorsinrecovery
relationship_advice
relationships
sad
safespace
sandersforpresident 
schizophrenia
secondary_survivors
secularsobriety
selfHarmScars
selfharm
selfhelp
sextrafficking
sfts
shizoaffective
socialanxiety
socialism 
stopdrinking
stopselfharm
stopsmoking
stopspeeding
suicidewatch
survivorsofabuse
teenrelationships
thanksobama
the_mueller
thefallen
thenews
therapy
tinytrumps
tourettes
traumatoolbox
trumpcriticizestrump 
trumpgret
twoxchromosomes
ukpolitics
upliftingnews
usnews
vent
widowers
wordpolitics
worldnews


================================================
FILE: src/config/reddit/reddit_avoid_words.txt
================================================


================================================
FILE: src/config/reddit/reddit_sub_lists.py
================================================
# The karma bot will only use 
# the subs in the list below

# EXAMPLE
# REDDIT_APPROVED_SUBS = [
# "aww",
# "pics",
# "pcmasterrace",
# ]

# The bot will only use the subs defined in this list
# if this list is empty it will choose subreddits randomly
REDDIT_APPROVED_SUBS = [
]

# array of arrays with subreddits
# where content can be crossposted
# the first array item is the source,
# and the rest are where it could be re-posted to
CROSSPOST_SUBS = [
  ["aww", "pics", "animals"],
  ["catpictures", "aww"]
]

================================================
FILE: src/config/reddit_config.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from utils import prefer_envar
from logs.logger import log
from logs.log_utils import log_json
from config.reddit.reddit_sub_lists import REDDIT_APPROVED_SUBS
from config.reddit.config_gen import config_gen
import sys
import json
import os

AUTH = prefer_envar({
  # app creds
  "reddit_client_id":"",
  "reddit_client_secret":"",
  # reddit account creds
  "reddit_username":"",
  "reddit_password":"",
})

for key in AUTH:
  if AUTH[key] == "":
    # reddit auth not configured correctly. 
    # instruct user to generate a .env file
    config_gen()

log.info(f"REDDIT AUTH CONFIG:\n {log_json(AUTH)}")

CONFIG = prefer_envar({
  "reddit_crosspost_enabled": False,
  # the chance the bot will repost a post
  "reddit_post_chance": 0.005,
  # the chance the bot will make a comment
  "reddit_comment_chance": 0.005,
  # the chance the bot will reply to a comment
  # otherwise it will reply to a post
  "reddit_reply_to_comment": 0.002,
  # chance the bot will remove poor performing
  # posts and comments
  "reddit_remove_low_scores": 0.002,
  # posts/comments that get downvoted to this score will be deleted
  "reddit_low_score_threshold": 0,
  # chance to check if the bot is shadowbanned, 
  # and shut down the script automatically
  "reddit_shadowban_check": 0.002,
  # list of subreddits for the bot to use
  "reddit_sub_list": REDDIT_APPROVED_SUBS,
  # bot schedules. all times are UTC
  # add the schedule number to the array
  # and the bot will run within that time range
  # leave the array empty for no schedule: []
  # 1 - 7am-10am ((7,00),(10,00))
  # 2 - 10am-2pm ((10,00),(14,00))
  # 3 - 2pm-6pm ((14,00),(18,00))
  # 4 - 6pm-10pm ((18,00),(22,00))
  # 5 - 10pm-2am ((22,00),(2,00))
  "reddit_sleep_schedule": [2, 4],
  # Frequency to check if the bot hit karma limits
  "reddit_karma_limit_check": 0.002,
  # Set to integer with the max comment karma 
  # before the bot shuts down. Set as None to ignore
  "reddit_comment_karma_limit": None,
  # Set to integer with the max post/submission karma
  # before the bot shuts down. Set as None to ignore
  "reddit_post_karma_limit": None,
})

log.info(f"REDDIT CONNFIG:\n {log_json(CONFIG)}")


================================================
FILE: src/config/test.yml
================================================
loaded: true

================================================
FILE: src/init.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import sys
from logs.logger import log
from utils import check_internet , get_public_ip
import bot

if __name__ == "__main__":
    if check_internet() is True:
        try:
            log.info(f'Internet connection found : {get_public_ip()}')
            bot.run()
        except KeyboardInterrupt:
            # quit
            sys.exit()
        else:
            log.info('Please check your internet connection')
            sys.exit()


================================================
FILE: src/libs/urwide.py
================================================
#!/usr/bin/env python
# encoding: utf8
# -----------------------------------------------------------------------------
# Project   : URWIDE - Extended URWID
# -----------------------------------------------------------------------------
# Author    : Sébastien Pierre                     <sebastien.pierre@gmail.com>
# License   : Lesser GNU Public License  http://www.gnu.org/licenses/lgpl.html>
# -----------------------------------------------------------------------------
# Creation  : 14-07-2006
# Last mod  : 15-12-2016
# -----------------------------------------------------------------------------

import sys, string, re, curses
import urwid, urwid.raw_display, urwid.curses_display
from   urwid.widget import FLOW, FIXED, PACK, BOX, GIVEN, WEIGHT, LEFT, RIGHT, RELATIVE, TOP, BOTTOM, CLIP, RELATIVE_100

__version__ = "0.2.1"
__doc__ = """\
URWIDE provides a nice wrapper around the awesome URWID Python library. It
enables the creation of complex console user-interfaces, using an easy to use
API .

URWIDE provides a simple notation to describe text-based UIs, and also provides
extensions to support events, tooltips, dialogs as well as other goodies for
every URWID widget.

URWID can be downloaded at <http://www.excess.org/urwid>.
"""

COLORS =  {
	# Colors
	"WH": "white",
	"BL": "black",
	"YL": "yellow",
	"BR": "brown",
	"LR": "light red",
	"LG": "light green",
	"LB": "light blue",
	"LC": "light cyan",
	"LM": "light magenta",
	"Lg": "light gray",
	"DR": "dark red",
	"DG": "dark green",
	"DB": "dark blue",
	"DC": "dark cyan",
	"DM": "dark magenta",
	"Dg": "dark gray",
	# Font attributes
	"BO": "bold",
	"SO": "standout",
	"UL": "underline",
	"_" : "default"
}
RIGHT  = "right"
LEFT   = "left"
CENTER = "center"


IS_PYTHON3 = sys.version_info[0] > 2

if IS_PYTHON3:
	# Python3 only defines str
	unicode = str
	long    = int
else:
	unicode = unicode

def isString( t ):
	return isinstance(t, (unicode, str))

def ensureString( t, encoding="utf8" ):
	if IS_PYTHON3:
		return t if isinstance(t, str) else str(t, encoding)
	else:
		return t.encode("utf8") if isinstance (t, unicode) else str(t)

def safeEnsureString( t,  encoding="utf8" ):
	if IS_PYTHON3:
		return ensureString(t, encoding)
	else:
		return t.encode("utf8", "ignore") if isinstance (t, unicode) else str(t)

def ensureUnicode( t, encoding="utf8" ):
	if IS_PYTHON3:
		return t if isinstance(t, str) else str(t, encoding)
	else:
		return t if isinstance(t, unicode) else str(t).decode(encoding)

def ensureBytes( t, encoding="utf8" ):
	if IS_PYTHON3:
		return t if isinstance(t, bytes) else bytes(t, encoding)
	else:
		return t

def add_widget( container, widget, options=None  ):
	w = widget
	if isinstance(container, urwid.Pile):
		# See: urwid.container.py Pile.__init__
		w = widget
		if not isinstance(w, tuple):
			container.contents.append((w, (WEIGHT, 1)))
		elif w[0] in (FLOW, PACK):
			f, w = w
			containe.contents.append((w, (PACK, None)))
		elif len(w) == 2:
			height, w = w
			container.contents.append((w, (GIVEN, height)))
		elif w[0] == FIXED: # backwards compatibility
			_ignore, height, w = w
			container.contents.append((w, (GIVEN, height)))
		elif w[0] == WEIGHT:
			f, height, w = w
			container.contents.append((w, (f, height)))
		else:
			raise ValueError("Widget not as expected: {0}".format(widet))
	else:
		container.contents.append(widget)


def remove_widgets( container ):
	w = [_ for _ in container.contents]
	for _ in w:
		container.contents.remove(_)

def original_widgets( widget ):
	if not widget:
		return []
	stack = [widget]
	if stack:
		while hasattr(stack[0], "original_widget"):
			original = stack[0].original_widget
			if original not in stack:
				stack.insert(0,original)
			else:
				break
	return stack

def original_widget(widget):
	r = original_widgets(widget)
	return r[0] if r else widget

def original_focus(widget):
	w = original_widgets(widget)
	for _ in w:
		if hasattr(_, "focus"):
			return _.focus
	return w[0]

# ------------------------------------------------------------------------------
#
# URWID Patching
#
# ------------------------------------------------------------------------------

class PatchedListBox(urwid.ListBox):

	_parent = None

	def __init__( self, *args, **kwargs ):
		PatchedListBox._parent.__init__(self, *args, **kwargs)

	def remove_widgets( self ):
		"""Remove all widgets from the body."""
		if isinstance(self.body, SimpleListWalker):
			self.body = SimpleListWalker([])
		else:
			raise Exception("Method only supported for SimpleListWalker")

	def add_widget( self, widget ):
		"""Adds a widget to the body of this list box."""
		if isinstance(self.body, SimpleListWalker):
			self.body.contents.append(widget)
		else:
			raise Exception("Method only supported for SimpleListWalker")

class PatchedPile(urwid.Pile):

	_parent = None

	def __init__(self, widget_list, focus_item=None):
		# No need to call the constructor
		#super(PatchedPile, self).__init__(widget_list, focus_item)
		self.__super.__init__(widget_list, focus_item)
		self.widget_list = []
		self.item_types  = []
		for _ in widget_list: add_widget(self, _)
		if focus_item: self.set_focus(focus_item)
		self.pref_col = None

	def add_widget( self, widget ):
		"""Adds a widget to this pile"""
		w = widget
		self.widget_list.append(widget)
		if type(w) != type(()):
			self.item_types.append(('weight',1))
		elif w[0] == 'flow':
			f, widget = w
			self.widget_list[i] = widget
			self.item_types.append((f,None))
		elif w[0] in ('fixed', 'weight'):
			f, height, widget = w
			self.widget_list[i] = widget
			self.item_types.append((f,height))
		else:
			raise PileError("widget list item invalid %s" % (w))

	def remove_widget( self, widget ):
		"""Removes a widget from this pile"""
		if type(widget) != type(()): widget = widget[1]
		i = self.widget_list.index(widget)
		del self.widget_list[i]
		del self.item_types[i]

	def remove_widgets( self ):
		"""Removes all widgets from this pile"""
		self.widget_list = []
		self.item_types  = []

class PatchedColumns(urwid.Columns):
	_parent = None
	def set_focus(self, widget):
		"""Set the column in focus with a widget in self.widget_list."""
		position = self.widget_list.index(widget) if type(widget) != int else widget
		self.focus_col = position

PatchedPile._parent    = urwid.Pile
PatchedListBox._parent = urwid.ListBox
PatchedColumns._parent = urwid.Columns
# urwid.Pile    = PatchedPile
# urwid.ListBox = PatchedListBox
# urwid.Columns = PatchedColumns

# ------------------------------------------------------------------------------
#
# UI CLASS
#
# ------------------------------------------------------------------------------

class UISyntaxError(Exception): pass
class UIRuntimeError(Exception): pass
class UI:
	"""The UI class allows to build an URWID user-interface from a simple set of
	string definitions.

	Instanciation of this class, may raise syntax error if the given text data
	is not formatted as expected, but you can easily get detailed information on
	what the problem was."""

	BLANK = urwid.Text("")
	EMPTY = urwid.Text("")
	NOP   = lambda self:self

	class Collection(object):
		"""Keys of the given collection are recognized as attributes."""

		def __init__( self, collection=None ):
			object.__init__(self)
			if collection is None: collection = {}
			self.w_w_content = collection

		def __getattr__( self, name ):
			if name.startswith("w_w_"):
				return super(UI.Collection, self).__getattribute__(name)
			w = self.w_w_content
			if name not in w: raise UIRuntimeError("No widget with name: " + name )
			return w[name]

		def __setattr__( self, name, value):
			if name.startswith("w_w_"):
				return super(UI.Collection, self).__setattr__(name, value)
			if name in self.w_w_content:
				raise SyntaxError("Item name already used: " + name)
			self.w_w_content[name] = value

	def __init__( self ):
		"""Creates a new user interface object from the given text
		description."""
		self._content     = None
		self._stack       = None
		self._currentLine = None
		self._ui          = None
		self._palette     = None
		self._header      = None
		self._currentSize = None
		self._widgets     = {}
		self._groups      = {}
		self._strings     = {}
		self._data        = {}
		self._handlers    = []
		self.widgets      = UI.Collection(self._widgets)
		self.groups       = UI.Collection(self._groups)
		self.strings      = UI.Collection(self._strings)
		self.data         = UI.Collection(self._data)

	def id( self, widget ):
		"""Returns the id for the given widget."""
		if hasattr(widget, "_urwideId"):
			return widget._urwideId
		else:
			return None

	def new( self, widgetClass, *args, **kwargs ):
		"""Creates the given widget by instanciating @widgetClass with the given
		args and kwargs. Basically, this is equivalent to

		>	return widgetClass(*kwargs['args'], **kwargs['kwargs'])

		Excepted that the widget is wrapped in an `urwid.AttrWrap` object, with the
		proper attributes. Also, the given @kwargs are preprocessed before being
		forwarded to the widget:

		 - `data` is the text data describing ui attributes, constructor args
		   and kwargs (in the same format as the text UI description)

		 - `ui`, `args` and `kwargs` allow to pass preprocessed data to the
		   constructor.

		In all cases, if you want to pass args and kwargs, you should
		explicitely use the `args` and `kwargs` arguments. I know that this is a
		bit confusing..."""
		return self._createWidget( widgetClass, *args, **kwargs )

	def wrap( self, widget, properties ):
		"""Wraps the given in the given properties."""
		_ui, _, _ = self._parseAttributes(properties)
		return self._wrapWidget( widget, _ui )

	def unwrap( self, widget ):
		"""Unwraps the widget (see `new` method)."""
		if isinstance(widget, urwid.AttrWrap) and widget.w: widget = widget.w
		return widget

	# EVENT HANDLERS
	# -------------------------------------------------------------------------

	def handler( self, handler = None ):
		"""Sets/Gets the current event handler.

		This modifies the 'handler.ui' and sets it to this ui."""
		if handler is None:
			if not  self._handlers: raise UIRuntimeError("No handler defined for: %s" % (self))
			return self._handlers[-1][0]
		else:
			old_ui     = handler.ui
			handler.ui = self
			if not self._handlers:
				self._handlers.append((handler, old_ui))
			else:
				self._handlers[-1] = (handler, old_ui)

	def responder( self, event ):
		"""Returns the function that responds to the given event."""
		return self.handler().responder(event)

	def pushHandler( self, handler ):
		"""Push a new handler on the list of handlers. This handler will handle
		events until it is popped out or replaced."""
		self._handlers.append((handler, handler.ui))
		handler.ui = self

	def popHandler( self ):
		"""Pops the current handler of the list of handlers. The handler will
		not handle events anymore, while the previous handler will start to
		handle events."""
		handler, ui = self._handlers.pop()
		handler.ui = ui

	def _handle( self, event_name, widget, *args, **kwargs ):
		"""Handle the given given event name."""
		# If the event is an event name, we use the handler mechanism
		if type(event_name) in (str, unicode):
			handler = self.handler()
			if handler.responds(event_name):
				return handler.respond(event_name, widget, *args, **kwargs)
			elif hasattr(widget, event_name):
				getattr(widget, event_name, *args, **kwargs)
			else:
				raise UIRuntimeError("No handler for event: %s in %s" % (event_name, widget))
		# Otherwise we assume it is a callback
		else:
			return event_name(widget,  *args, **kwargs)

	def setTooltip( self, widget, tooltip ):
		widget._urwideTooltip = tooltip

	def setInfo( self, widget, info ):
		widget._urwideInfo = info

	def onKey( self, widget, callback ):
		"""Sets a callback to the given widget for the 'key' event"""
		widget = self.unwrap(widget)
		widget._urwideOnKey = callback

	def onFocus( self, widget, callback ):
		"""Sets a callback to the given widget for the 'focus' event"""
		widget = self.unwrap(widget)
		widget._urwideOnFocus = callback

	def onEdit( self, widget, callback ):
		"""Sets a callback to the given widget for the 'edit' event"""
		widget = self.unwrap(widget)
		widget._urwideOnEdit = callback

	def onPress( self, widget, callback ):
		"""Sets a callback to the given widget for the 'edit' event"""
		widget = self.unwrap(widget)
		widget._urwideOnPress = callback

	def _doPress( self, button, *args ):
		if hasattr(button, "_urwideOnPress"):
			event_name = button._urwideOnPress
			self._handle(event_name, button, *args)
		elif isinstance(button, urwid.RadioButton):
			return False
		else:
			raise UIRuntimeError("Widget does not respond to press event: %s" % (button))

	def _doFocus( self, widget, ensure=True ):
		if hasattr(widget, "_urwideOnFocus"):
			event_name = widget._urwideOnFocus
			self._handle(event_name, widget)
		elif ensure:
			raise UIRuntimeError("Widget does not respond to focus event: %s" % (widget))

	def _doEdit( self, widget, before, after, ensure=True ):
		if hasattr(widget, "_urwideOnEdit"):
			event_name = widget._urwideOnEdit
			self._handle(event_name, widget, before, after)
		elif ensure:
			raise UIRuntimeError("Widget does not respond to focus edit: %s" % (widget))

	def _doKeyPress( self, widget, key ):
		# THE RULES
		# ---------
		#
		# 1) Widget defines an onKey event handler, it is triggered
		# 2) If the handler returned False, or was not existent, we
		#    forward to the top widget
		# 3) The onKeyPress event is handled by the keyPress handler if the
		#    focused widget is not editable
		# 4) If no keyPresss handler is defined, the default key_press event is
		#    handled
		topwidget = self.getToplevel()
		current_widget = widget
		# We traverse the `original_widget` in case the widgets are nested.
		# This allows to get the deepest widget.
		stack = original_widgets(widget)
		# FIXME: Dialogs should prevent processing of events at a lower level
		if stack:
			for widget in stack:
				if hasattr(widget, "_urwideOnKey"):
					event_name = widget._urwideOnKey
					if self._handle(event_name, widget, key):
						return
			if current_widget != topwidget and current_widget not in stack:
				self._doKeyPress(topwidget, key)
			else:
				self._doKeyPress(None, key)
		elif widget and widget != topwidget:
			self._doKeyPress(topwidget, key)
		else:
			if key == "tab":
				self.focusNext()
			elif key == "shift tab":
				self.focusPrevious()
			if self.isEditable(self.getFocused()):
				res = False
			else:
				try:
					res = self._handle("keyPress", topwidget, key)
				except UIRuntimeError:
					res = False
			if not res:
				topwidget.keypress(self._currentSize, key)

	def getFocused( self ):
		raise Exception("Must be implemented by subclasses")

	def focusNext( self ):
		raise Exception("Must be implemented by subclasses")

	def focusPrevious( self ):
		raise Exception("Must be implemented by subclasses")

	def getToplevel( self ):
		raise Exception("Must be implemented by subclasses")

	def isEditable( self, widget ):
		return isinstance(widget, (urwid.Edit, urwid.IntEdit))

	def isFocusable( self, widget ):
		if   isinstance(widget, urwid.Edit):        return True
		elif isinstance(widget, urwid.IntEdit):     return True
		elif isinstance(widget, urwid.Button):      return True
		elif isinstance(widget, urwid.CheckBox):    return True
		elif isinstance(widget, urwid.RadioButton): return True
		else:                                       return False

	# PARSING WIDGETS STACK MANAGEMENT
	# -------------------------------------------------------------------------

	def _add( self, widget ):
		"""Adds the given widget to the @_content list. This list will be
		added to the current parent widget when the UI is finished or when an
		`End` block is encountered (see @_push and @_pop)"""
		# Piles cannot be created with [] as content, so we fill them with the
		# EMPTY widget, which is replaced whenever we add something
		if self._content == [self.EMPTY]: self._content[0] = widget
		self._content.append(widget)

	def _push( self, endCallback, ui=None, args=(), kwargs={} ):
		"""Pushes the given arguments (@ui, @args, @kwargs) on the stack,
		together with the @endCallback which will be invoked with the given
		arguments when an `End` block will be encountered (and that a @_pop is
		triggered)."""
		self._stack.append((self._content, endCallback, ui, args, kwargs))
		self._content = []
		return self._content

	def _pop( self ):
		"""Pops out the widget on the top of the stack and invokes the
		_callback_ previously associated with it (using @_push)."""
		previous_content = self._content
		self._content, end_callback, end_ui, end_args, end_kwargs = self._stack.pop()
		return previous_content, end_callback, end_ui, end_args, end_kwargs

	# GENERIC PARSING METHODS
	# -------------------------------------------------------------------------

	def create( self, style, ui, handler=None ):
		self.parseStyle(style)
		self.parseUI(ui)
		if handler: self.handler(handler)
		return self

	def parseUI( self, text ):
		"""Parses the given text and initializes this user interface object."""
		text = string.Template(text).substitute(self._strings)
		self._content = []
		self._stack   = []
		self._currentLine = 0
		for line in text.split("\n"):
			line = line.strip()
			if not line.startswith("#"): self._parseLine(line)
			self._currentLine += 1
		self._listbox     = self._createWidget(urwid.ListBox,self._content)
		return self._content

	def parseStyle( self, data ):
		"""Parses the given style."""
		res = []
		for line in data.split("\n"):
			if not line.strip(): continue
			line = line.replace("\t", " ").replace("  ", " ")
			name, attributes = [_.strip() for _ in line.split(":")]
			res_line = [name]
			for attribute in attributes.split(","):
				attribute = attribute.strip()
				color     = COLORS.get(attribute)
				if not color: raise UISyntaxError("Unsupported color: " + attribute)
				res_line.append(color)
			if len(res_line) != 4:
				raise UISyntaxError("Expected NAME: FOREGROUND BACKGROUND FONT")
			res.append(tuple(res_line))
		self._palette = res
		return res

	RE_LINE = re.compile("^\s*(...)\s?")
	def _parseLine( self, line ):
		"""Parses a line of the UI definition file. This automatically invokes
		the specialized parsers."""
		if not line:
			self._add( self.BLANK )
			return
		match = self.RE_LINE.match(line)
		if not match: raise UISyntaxError("Unrecognized line: " + line)
		name  = match.group(1)
		data  = line[match.end():]
		if hasattr(self, "_parse" + name ):
			getattr(self, "_parse" + name)(data)
		elif name[0] == name[1] == name[2]:
			self._parseDvd(name + data)
		else:
			raise UISyntaxError("Unrecognized widget: `" + name + "`")

	def _parseAttributes( self, data ):
		assert type(data) in (str, unicode)
		ui_attrs, data = self._parseUIAttributes(data)
		args, kwargs   = self._parseArguments(data)
		return ui_attrs, args, kwargs

	RE_UI_ATTRIBUTE = re.compile("\s*([#@\?\:]|\&[\w]+\=)([\w\d_\-]+)\s*")
	def _parseUIAttributes( self, data ):
		"""Parses the given UI attributes from the data and returns the rest of
		the data (which corresponds to something else thatn the UI
		attributes."""
		assert type(data) in (str, unicode)
		ui = {"events":{}}
		while True:
			match = self.RE_UI_ATTRIBUTE.match(data)
			if not match: break
			ui_type, ui_value = match.groups()
			assert type(ui_value) in (str, unicode)
			if   ui_type    == "#": ui["id"]      = ui_value
			elif ui_type    == "@": ui["style"]   = ui_value
			elif ui_type    == "?": ui["info"]    = ui_value
			elif ui_type    == "!": ui["tooltip"] = ui_value
			elif ui_type[0] == "&": ui["events"][ui_type[1:-1]]=ui_value
			data = data[match.end():]
		return ui, data

	def _parseArguments( self, data ):
		"""Parses the given text data which should be a list of attributes. This
		returns a dict with the attributes."""
		assert type(data) in (str, unicode)
		def as_dict(*args, **kwargs): return args, kwargs
		res = eval("as_dict(%s)" % (data))
		try:
			res = eval("as_dict(%s)" % (data))
		except:
			raise SyntaxError("Malformed arguments: " + repr(data))
		return res

	def hasStyle( self, *styles ):
		for s in styles:
			for r in self._palette:
				if r[0] == s: return s
		return False

	def _styleWidget( self, widget, ui ):
		"""Wraps the given widget so that it belongs to the given style."""
		styles = []
		if "id" in ui: styles.append("#" + ui["id"])
		if "style" in ui:
			s = ui["style"]
			if type(s) in (tuple, list): styles.extend(s)
			else: styles.append(s)
		styles.append( widget.__class__.__name__ )
		unf_styles = [_ for _ in styles if self.hasStyle(_)]
		foc_styles = [_ + "*" for _ in styles if self.hasStyle(_ + "*")]
		if unf_styles:
			if foc_styles:
				return urwid.AttrWrap(widget, unf_styles[0], foc_styles[0])
			else:
				return urwid.AttrWrap(widget, unf_styles[0])
		else:
			return widget

	def _createWidget( self, widgetClass, *args, **kwargs ):
		"""Creates the given widget by instanciating @widgetClass with the given
		args and kwargs. Basically, this is equivalent to

		>	return widgetClass(*kwargs['args'], **kwargs['kwargs'])

		Excepted that the widget is wrapped in an `urwid.AttrWrap` object, with the
		proper attributes. Also, the given @kwargs are preprocessed before being
		forwarded to the widget:

		 - `data` is the text data describing ui attributes, constructor args
		   and kwargs (in the same format as the text UI description)

		 - `ui`, `args` and `kwargs` allow to pass preprocessed data to the
		   constructor.

		In all cases, if you want to pass args and kwargs, you should
		explicitely use the `args` and `kwargs` arguments. I know that this is a
		bit confusing..."""
		_data = _ui = _args = _kwargs = None
		for arg, value in kwargs.items():
			if   arg == "data":   _data = value
			elif arg == "ui":     _ui = value
			elif arg == "args":   _args = value
			elif arg == "kwargs": _kwargs = value
			else: raise Exception("Unrecognized optional argument: " + arg)
		if _data:
			_ui, _args, _kwargs = self._parseAttributes(_data)
		args = list(args)
		if _args: args.extend(_args)
		kwargs = _kwargs or {}
		widget = widgetClass(*args, **kwargs)
		return self._wrapWidget(widget, _ui)

	def _wrapWidget( self, widget, _ui ):
		"""Wraps the given widget into anotger widget, and applies the various
		properties listed in the '_ui' (internal structure)."""
		# And now we process the ui information
		if not _ui: _ui = {}
		if "id" in _ui:
			setattr(self.widgets, _ui["id"], widget)
			widget._urwideId = _ui["id"]
		if _ui.get("events"):
			for event, handler in _ui["events"].items():
				if   event == "press":
					if not isinstance(widget, urwid.Button)\
					and not isinstance(widget, urwid.RadioButton):
						raise UISyntaxError("Press event only applicable to Button: " + repr(widget))
					widget._urwideOnPress = handler
				elif event == "edit":
					if not isinstance(widget, urwid.Edit):
						raise UISyntaxError("Edit event only applicable to Edit: " + repr(widget))
					widget._urwideOnEdit = handler
				elif event == "focus":
					widget._urwideOnFocus = handler
				elif event == "key":
					widget._urwideOnKey = handler
				else:
					raise UISyntaxError("Unknown event type: " + event)
		if _ui.get("info"):
			widget._urwideInfo = _ui["info"]
		if _ui.get("tooltip"):
			widget._urwideTooltip = _ui["tooltip"]
		return self._styleWidget( widget, _ui )

	# WIDGET-SPECIFIC METHODS
	# -------------------------------------------------------------------------

	def _argsFind( self, data ):
		args = data.find("args:")
		if args == -1:
			attr = ""
		else:
			attr = data[args+5:]
			data = data[:args]
		return attr, data

	def _parseTxt( self, data ):
		attr, data = self._argsFind(data)
		ui, args, kwargs = self._parseAttributes(attr)
		self._add(self._createWidget(urwid.Text,data, ui=ui, args=args, kwargs=kwargs))

	def _parseHdr( self, data ):
		if self._header is not None:
			raise UISyntaxError("Header can occur only once")
		attr, data = self._argsFind(data)
		ui, args, kwargs = self._parseAttributes(attr)
		ui.setdefault("style", "header")
		self._header = self._createWidget(urwid.Text, data, ui=ui, args=args, kwargs=kwargs)

	RE_BTN = re.compile("\s*\[([^\]]+)\]")
	def _parseBtn( self, data ):
		match = self.RE_BTN.match(data)
		if not match: raise SyntaxError("Malformed button: " + repr(data))
		data  = data[match.end():]
		self._add(self._createWidget(urwid.Button, match.group(1), self._doPress, data=data))

	RE_CHC = re.compile("\s*\[([xX ])\:(\w+)\](.+)")
	def _parseChc( self, data ):
		attr, data = self._argsFind(data)
		# Parses the declaration
		match = self.RE_CHC.match(data)
		if not match: raise SyntaxError("Malformed choice: " + repr(data))
		state = match.group(1) != " "
		group = group_name = match.group(2).strip()
		group = self._groups.setdefault(group,[])
		assert self._groups[group_name] == group
		assert getattr(self.groups,group_name) == group
		label = match.group(3)
		# Parses the attributes
		ui, args, kwargs = self._parseAttributes(attr)
		# Creates the widget
		self._add(self._createWidget(urwid.RadioButton, group, label, state,
		self._doPress,  ui=ui, args=args, kwargs=kwargs))

	def _parseDvd( self, data ):
		ui, args, kwargs = self._parseAttributes(data[3:])
		self._add(self._createWidget(urwid.Divider, data, ui=ui, args=args, kwargs=kwargs))

	def _parseBox( self, data ):
		def end( content, ui=None, **kwargs ):
			if not content: content = [self.EMPTY]
			if len(content) == 1: w = content[0]
			else: w = self._createWidget(urwid.Pile, content)
			border = kwargs.get('border') or 1
			w = self._createWidget(urwid.Padding, w, ('fixed left', border), ('fixed right', border) )
			# TODO: Filler does not work
			# w = self._createWidget(urwid.Filler, w, ('fixed top', border), ('fixed bottom', border) )
			# w = urwid.Filler(w,  ('fixed top', 1),  ('fixed bottom',1))
			self._add(w)
		ui, args, kwargs = self._parseAttributes(data)
		self._push(end, ui=ui, args=args, kwargs=kwargs)

	RE_EDT = re.compile("([^\[]*)\[([^\]]*)\]")
	def _parseEdt( self, data ):
		match = self.RE_EDT.match(data)
		data  = data[match.end():]
		label, text = match.groups()
		ui, args, kwargs = self._parseAttributes(data)
		if label and self.hasStyle('label'): label = ('label', label)
		self._add(self._createWidget(urwid.Edit, label, text,
		ui=ui, args=args, kwargs=kwargs))

	def _parsePle( self, data ):
		def end( content, ui=None, **kwargs ):
			if not content: content = [self.EMPTY]
			self._add(self._createWidget(urwid.Pile, content, ui=ui, kwargs=kwargs))
		ui, args, kwargs = self._parseAttributes(data)
		self._push(end, ui=ui, args=args, kwargs=kwargs)

	def _parseCol( self, data ):
		def end( content, ui=None, **kwargs ):
			if not content: content = [self.EMPTY]
			self._add(self._createWidget(urwid.Columns, content, ui=ui, kwargs=kwargs))
		ui, args, kwargs = self._parseAttributes(data)
		self._push(end, ui=ui, args=args, kwargs=kwargs)

	def _parseGFl( self, data ):
		def end( content, ui=None, **kwargs ):
			max_width = 0
			# Gets the maximum width for the content
			for widget in content:
				if hasattr(widget, "get_text"):
					max_width = max(len(widget.get_text()), max_width)
				if hasattr(widget, "get_label"):
					max_width = max(len(widget.get_label()), max_width)
			kwargs.setdefault("cell_width", max_width + 4)
			kwargs.setdefault("h_sep", 1)
			kwargs.setdefault("v_sep", 1)
			kwargs.setdefault("align", "center")
			self._add(self._createWidget(urwid.GridFlow, content, ui=ui, kwargs=kwargs))
		ui, args, kwargs = self._parseAttributes(data)
		self._push(end, ui=ui, args=args, kwargs=kwargs)

	def _parseLBx( self, data ):
		def end( content, ui=None, **kwargs ):
			self._add(self._createWidget(urwid.ListBox, content, ui=ui, kwargs=kwargs))
		ui, args, kwargs = self._parseAttributes(data)
		self._push(end, ui=ui, args=args, kwargs=kwargs)

	def _parseEnd( self, data ):
		if data.strip(): raise UISyntaxError("End takes no argument: " + repr(data))
		# We get the end callback that will instanciate the widget and add it to
		# the content.
		if not self._stack: raise SyntaxError("End called without container widget")
		end_content, end_callback, end_ui, end_args, end_kwargs = self._pop()
		end_callback(end_content, end_ui, *end_args, **end_kwargs)

# ------------------------------------------------------------------------------
#
# CONSOLE CLASS
#
# ------------------------------------------------------------------------------

class Console(UI):
	"""The console class allows to create console applications that work 'full
	screen' within a terminal."""

	def __init__( self ):
		UI.__init__(self)
		self._ui          = None
		self._frame       = None
		self._header      = None
		self._footer      = None
		self._listbox     = None
		self._dialog      = None
		self._tooltiptext = ""
		self._infotext    = ""
		self._footertext  = ""
		self.isRunning    = False
		self.endMessage   = ""
		self.endStatus    = 1

	# USER INTERACTION API
	# -------------------------------------------------------------------------

	def tooltip( self, text=-1 ):
		"""Sets/Gets the current tooltip text."""
		if text == -1:
			return self._tooltiptext
		else:
			self._tooltiptext = ensureUnicode(text)

	def info( self, text=-1 ):
		"""Sets/Gets the current info text."""
		if text == -1:
			return self._infotext
		else:
			self._infotext = ensureUnicode(text)

	def footer( self, text=-1 ):
		"""Sets/Gets the current footer text."""
		if text == -1:
			return self._footertext
		else:
			self._footertext = ensureUnicode(text)

	def dialog( self, dialog ):
		"""Sets the dialog as this UI dialog. All events will be forwarded to
		the dialog until exit."""
		self._dialog = dialog

	# WIDGET INFORMATION
	# -------------------------------------------------------------------------

	def getFocused( self ):
		"""Gets the focused widget"""
		# We get the original widget to focus on
		focused     = original_widget(self._listbox.get_focus()[0])
		old_focused = None
		while focused != old_focused:
			old_focused = focused
			# There are some types that are not focuable
			if isinstance(focused, urwid.AttrWrap):
				if focused.w: focused = focused.w
			elif isinstance(focused, urwid.Padding):
				if focused.min_width: focused = focused.min_width
			elif isinstance(focused, urwid.Filler):
				if focused.w: focused = focused.w
			elif hasattr(focused, "get_focus"):
				if focused.get_focus(): focused = focused.get_focus()
		return focused

	def focusNext( self ):
		focused = self._listbox.get_focus()[1] + 1
		self._listbox.set_focus(focused)
		while True:
			if not self.isFocusable(self.getFocused()) \
			and self._listbox.body.get_next(focused)[0] is not None:
				focused += 1
				self._listbox.set_focus(focused)
			else:
				break

	def focusPrevious( self ):
		focused = max(self._listbox.get_focus()[1] - 1, 0)
		self._listbox.set_focus(focused)
		while True:
			if not self.isFocusable(self.getFocused()) \
			and focused > 0:
				focused -= 1
				self._listbox.set_focus(focused)
			else:
				break

	def getToplevel( self ):
		"""Returns the toplevel widget, which may be a dialog's view, if there
		was a dialog."""
		if self._dialog:
			return self._dialog.view()
		else:
			return self._frame

	def getCurrentSize( self ):
		"""Returns the current size for this UI as a couple."""
		return self._currentSize

	# URWID EVENT-LOOP
	# -------------------------------------------------------------------------

	def main( self ):
		"""This is the main event-loop. That is what you should invoke to start
		your application."""
		#self._ui = urwid.curses_display.Screen()
		self._ui  = urwid.raw_display.Screen()
		self._ui.clear()
		if self._palette: self._ui.register_palette(self._palette)
		self._ui.run_wrapper( self.run )
		# We clear the screen (I know, I should use URWID, but that was the
		# quickest way I found)
		curses.setupterm()
		sys.stdout.write(curses.tigetstr('clear').decode())
		if self.endMessage:
			print (self.endMessage)
		return self.endStatus

	def run( self ):
		"""Run function to be used by URWID. You should not call it directly,
		use the 'main' function instead."""
		#self._ui.set_mouse_tracking()
		self._currentSize = self._ui.get_cols_rows()
		self.isRunning    = True
		while self.isRunning:
			self._currentSize = self._ui.get_cols_rows()
			self.loop()

	def end( self, msg=None, status=1 ):
		"""Ends the application, registering the given 'msg' as end message, and
		returning the given 'status' ('1' by default)."""
		self.isRunning = False
		self.endMessage = msg
		self.endStatus  = status

	def loop( self ):
		"""This is the main URWID loop, where the event processing and
		dispatching is done."""
		# We get the focused element, and update the info and and tooltip
		if self._dialog:
			focused = self._dialog.view()
		else:
			focused = self.getFocused() or self._frame
		# We trigger the on focus event
		self._doFocus(focused, ensure=False)
		# We update the tooltip and info in the footer
		if hasattr(focused, "_urwideInfo"):
			self.info(self._strings.get(focused._urwideInfo) or focused._urwideInfo)
		if hasattr(focused, "_urwideTooltip"):
			self.tooltip(self._strings.get(focused._urwideTooltip) or focused._urwideTooltip)
		# We draw the screen
		self._updateFooter()
		self.draw()
		self.tooltip("")
		self.info("")
		# And process keys
		if not self.isRunning: return
		keys    = self._ui.get_input()
		if isinstance(focused, urwid.Edit): old_text = focused.get_edit_text()
		# We handle keys
		for key in keys:
			#if urwid.is_mouse_event(key):
				# event, button, col, row = key
				# self.view.mouse_event( self._currentSize, event, button, col, row, focus=True )
				#pass
			# NOTE: The key press might actually be send not to the focused
			# widget but to its original_widget
			if key == "window resize":
				self._currentSize = self._ui.get_cols_rows()
			elif self._dialog:
				self._doKeyPress(self._dialog.view(), key)
			else:
				self._doKeyPress(focused, key)
		# We check if there was a change in the edit, and we fire and event
		if isinstance(focused, urwid.Edit):
			self._doEdit( focused, old_text, focused.get_edit_text(), ensure=False)

	def draw( self ):
		"""Main loop to draw the console. This takes into account the fact that
		there may be a dialog to display."""
		if self._dialog is not None:
			o = urwid.Overlay( self._dialog.view(), self._frame,
				"center",
				self._dialog.width(),
				"middle",
				self._dialog.height()
			)
			canvas = o.render( self._currentSize, focus=True )
		else:
			canvas = self._frame.render( self._currentSize, focus=True )
		self._ui.draw_screen( self._currentSize, canvas )

	def _updateFooter(self):
		"""Updates the frame footer according to info and tooltip"""
		remove_widgets(self._footer)
		footer = []
		if self.tooltip():
			footer.append(self._styleWidget(urwid.Text(self.tooltip()), {'style':'tooltip'}))
		if self.info():
			footer.append(self._styleWidget(urwid.Text(self.info()), {'style':'info'}))
		if self.footer():
			footer.append(self._styleWidget(urwid.Text(self.footer()), {'style':'footer'}))
		if footer:
			for _ in footer:
				add_widget(self._footer, _)
			self._footer.set_focus(0)

	def parseUI( self, text ):
		"""Parses the given text and initializes this user interface object."""
		UI.parseUI(self, text)
		self._listbox     = self._createWidget(urwid.ListBox,self._content)
		self._footer      = urwid.Pile([self.EMPTY])
		self._frame       = self._createWidget(urwid.Frame,
			self._listbox,
			self._header,
			self._footer
		)
		return self._content

	def _parseFtr( self, data ):
		self.footer(data)

# ------------------------------------------------------------------------------
#
# DIALOG CLASSES
#
# ------------------------------------------------------------------------------

class Dialog(UI):
	"""Utility class to create dialogs that will fit within a console
	application.

	See the constructor documentation for more information."""

	PALETTE = """
	dialog        : BL, Lg, SO
	dialog.shadow : DB, BL, SO
	dialog.border : Lg, DB, SO
	"""

	def __init__( self, parent, ui, width=40, height=-1, style="dialog",
	header="", palette=""):
		"""Creates a new dialog that will be attached to the given 'parent'. The
		user interface is described by the 'ui' string. The dialog 'width' and
		'height' will indicate the dialog size, when 'height' is '-1', it will
		be automatically computed from the given 'ui'."""
		UI.__init__(self)
		if height == -1: height = ui.count("\n") + 1
		self._width         = width
		self._height        = height
		self._style         = style
		self._view          = None
		self._headertext    = header
		self._parent        = parent
		self._startCallback = lambda x:x
		self._endCallback   = lambda x:x
		self._palette       = None
		self.make(ui, palette)

	def width( self ):
		"""Returns the dialog width"""
		return self._width

	def height( self ):
		"""Returns the dialog height"""
		return self._height

	def view( self ):
		"""Returns the view attached to this 'Dialog'. The _view_ is created by
		the 'make' method, and is an 'urwid.Frame' instance."""
		assert self._view
		return self._view

	def make( self, uitext, palui=None ):
		"""Makes the dialog using a UI description ('uitext') and a style
		definition for the palette ('palui'), which can be 'None', in which case
		the value will be 'Dialog.PALETTE'."""
		if not palui: palui = self.PALETTE
		self.parseStyle(palui)
		style = self._styleWidget
		assert self._view is None
		content = []
		if self._headertext:
			content.append(style(urwid.Text(self._headertext), {'style':(self._style +'.header', "dialog.header", 'header')}))
			content.append(urwid.Text(""))
			content.append(urwid.Divider("_"))
		content.extend(self.parseUI(uitext))
		w = style(urwid.ListBox(content), {'style':(self._style +'.content', "dialog.content", self._style)})
		# We wrap the dialog into a box
		w = urwid.Padding(w, ('fixed left', 1), ('fixed right', 1))
		#w = urwid.Filler(w,  ('fixed top', 1),  ('fixed bottom',1))
		w = style(w,  {'style':(self._style+".body", "dialog.body", self._style)} )
		w = style( w, {'style':(self._style, "dialog")} )
		# Shadow
		shadow = self.hasStyle( self._style + ".shadow", "dialog.shadow", "shadow")
		border = self.hasStyle( self._style + ".border", "dialog.border", "border")
		if shadow:
			border = (border, '  ') if border else '  '
			w = urwid.Columns([w,('fixed', 2, urwid.AttrWrap(urwid.Filler(urwid.Text(border), "top") ,shadow))])
			w = urwid.Frame( w, footer = urwid.AttrWrap(urwid.Text(border),shadow))
		self._view = w
		self._startCallback(self)
		w._urwideOnKey = self.doKeyPress

	def onStart( self, callback ):
		"""Registers the callback that will be triggered on dialog start."""
		self._startCallback = callback

	def onEnd( self, callback ):
		"""Registers the callback that will be triggered on dialog end."""
		self._endCallback = callback

	def doKeyPress( self, widget, key ):
		self._handle("keyPress", widget, key)

	def end( self ):
		"""Call this to close the dialog."""
		self._endCallback(self)
		self._parent._dialog = None

	def _parseHdr( self, data ):
		if self._header is not None:
			raise UISyntaxError("Header can occur only once")
		attr, data = self._argsFind(data)
		ui, args, kwargs = self._parseAttributes(attr)
		ui.setdefault("style", ("dialog.header", "header") )
		self._content.append( self._createWidget(urwid.Text, data, ui=ui, args=args, kwargs=kwargs))

# ------------------------------------------------------------------------------
#
# HANDLER CLASS
#
# ------------------------------------------------------------------------------

FORWARD = False

class Handler(object):
	"""A handler can be subclassed an can be plugged into a UI to react to a
	specific set of events. The interest of handlers is that they can be
	dynamically switched, then making "modal UI" implementation easier.

	For instance, you could have a handler for your UI in "normal mode", and
	have another handler when a dialog box is displayed."""

	def __init__( self ):
		self.ui = None

	def respond( self, event, *args, **kwargs ):
		"""Responds to the given event name. An exception must be raised if the
		event cannot be responded to. False is returned if the handler does not
		want to handle the event, True if the event was handled."""
		responder = self.responder(event)
		return responder(*args, **kwargs) != FORWARD

	def responds( self, event ):
		"""Tells if the handler responds to the given event."""
		_event_name = "on" + event[0].upper() + event[1:]
		if hasattr(self, _event_name): return _event_name
		else: return None

	def responder( self, event ):
		"""Returns the function that responds to the given event."""
		_event_name = "on" + event[0].upper() + event[1:]
		if not hasattr(self, _event_name):
			raise UIRuntimeError("Event not implemented: " + event)

		res = getattr(self, _event_name)
		assert res
		return res

# EOF - vim: tw=80 ts=4 sw=4 noet


================================================
FILE: src/logs/log_utils.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import logging
import json
from logging.handlers import RotatingFileHandler

def log_json(tojson: object) -> str:
  return json.dumps(tojson, indent=2, sort_keys=True)

class NewLineFileHandler(RotatingFileHandler):
  """Handler that controls the writing of the newline character"""

  special_code = '[!n]'

  def emit(self, record) -> None:

    if self.special_code in record.msg:
      record.msg = record.msg.replace( self.special_code, '' )
      self.terminator = ''
    else:
      self.terminator = '\n'

    return super().emit(record)

class NewLineStreamHandler(logging.StreamHandler):
  """Handler that controls the writing of the newline character"""

  special_code = '[!n]'

  def emit(self, record) -> None:

    if self.special_code in record.msg:
      record.msg = record.msg.replace( self.special_code, '' )
      self.terminator = ''
    else:
      self.terminator = '\n'

    return super().emit(record)

================================================
FILE: src/logs/logger.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import logging
from logging.handlers import RotatingFileHandler
import os
from logs.log_utils import NewLineFileHandler, NewLineStreamHandler


file_log_format = logging.Formatter("%(asctime)s %(levelname)s %(funcName)s(%(lineno)d) %(message)s")
stream_log_format = logging.Formatter("%(asctime)s %(message)s", "%H:%M:%S")
logFile = "info.log"

file_handler = RotatingFileHandler(logFile, mode="a", maxBytes=15 * 1024 * 1024, backupCount=2, encoding=None, delay=0)
file_handler.setFormatter(file_log_format)

stream_handler =  logging.StreamHandler()
stream_handler.setFormatter(stream_log_format)

log = logging.getLogger(__name__)

if "DEBUG" in os.environ:
    log.setLevel(logging.DEBUG)
    print("debug logging")
else:
    log.setLevel(logging.INFO)
    print("info loggin")

log.addHandler(stream_handler)
log.addHandler(file_handler)


================================================
FILE: src/menu.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import pyfiglet
import sys
from logs.logger import log
from config import config_menu
from libs import urwide
import bot

# This is the description of the actual interface
CONSOLE_STYLE = """"""
CONSOLE_UI = """\
Hdr Header    args:#header
---
Txt Status: stopped                     args:#status
===

GFl
Btn [Start]                       #start &press=started
Btn [Config]                         &press=config
Btn [Exit]                          &press=exit
End
"""

# add when working: Btn [Log]                         &press=log

# This is the handling code, providing the logic
class Handler(urwide.Handler):
    def onStarted( self, button ):
        if self.ui.widgets.status.text == "Status: stopped":
            bot.run()
            self.ui.widgets.status.set_text("Status: started")
        else:
            self.ui.widgets.status.set_text("Status: stopped")
    def onConfig( self, button ):
        config_menu.run()

    def onExit( self, button ):
        self.ui.end("Exit")
        log.info("Exiting Karma Bot Menu: Bye! :D")
        sys.exit()

# We create a console application
ui = urwide.Console()
ui.create(CONSOLE_STYLE, CONSOLE_UI, Handler())
ui.widgets.header.set_text("Reddit Karma Farming Bot")

# bring this back later pyfiglet.figlet_format("Reddit Karma Farming Bot", font="slant")

def run():
    ui.main()

if __name__ == "__main__":
    run()


================================================
FILE: src/tests/__init__.py
================================================


================================================
FILE: src/tests/test_utils.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from .. import utils

def test_random_string():
  string = utils.random_string(5)
  assert type(string) is str
  assert len(string) == 5


================================================
FILE: src/utils.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import os
import collections
import random
import time
import string
from config.common_config import ENVAR_PREFIX
from logs.logger import log
import urllib.request
## HELPER VARS

DAY = 86400
MONTH = 2678400
YEAR = 31536000


def random_string(length: int) -> str:
  letters = string.ascii_lowercase
  return ''.join(random.choice(letters) for i in range(length))

def prefer_envar(configs: dict) -> dict:
  for config in list(configs):
    config_envar = f"{ENVAR_PREFIX}{config}".lower()
    if os.environ.get(config_envar):
      configs[config]=os.environ.get(config_envar)
      log.info(f"loading {config_envar} from envar. Value: {configs.get(config)}")
    else:
      log.debug(f"no environment config for: {config_envar}")

  return configs

# Checks if the machine has internet and also can connect to reddit
def check_internet(host="https://reddit.com", timeout=5):
    try:
        urllib.request.urlopen(host, None, timeout)
        return True
    except Exception as ex:
        log.error(ex)
        return False


def get_public_ip():
    try:
        external_ip = urllib.request.urlopen("https://api.ipify.org")
        if external_ip:
            return external_ip.read().decode("utf-8")
    except Exception as e:
        log.error("could not check external ip")


def bytesto(bytes, to, bsize=1024):
    """convert bytes to megabytes, etc.
      sample code:
          print('mb= ' + str(bytesto(314575262000000, 'm')))
      sample output:
          mb= 300002347.946
  """

    a = {"k": 1, "m": 2, "g": 3, "t": 4, "p": 5, "e": 6}
    r = float(bytes)
    for i in range(a[to]):
        r = r / bsize

    return round(r)


def is_past_one_day(time_to_compare):
    return int(time.time()) - time_to_compare >= DAY


def countdown(seconds=1):
    # log.info("sleeping: " + str(seconds) + " seconds")
    # for i in range(seconds, 0, -1):
    #     # print("\x1b[2K\r" + str(i) + " ")
    #     time.sleep(3)
    # log.info("waking up")
    time.sleep(seconds)


def chance(value=.20):
    rando = random.random()
    # log.info("prob: " + str(value) + " rolled: " + str(rando))
    return rando < value



def tobytes(size_str):
    """Convert human filesizes to bytes.
    https://stackoverflow.com/questions/44307480/convert-size-notation-with-units-100kb-32mb-to-number-of-bytes-in-python
    Special cases:
     - singular units, e.g., "1 byte"
     - byte vs b
     - yottabytes, zetabytes, etc.
     - with & without spaces between & around units.
     - floats ("5.2 mb")
    To reverse this, see hurry.filesize or the Django filesizeformat template
    filter.
    :param size_str: A human-readable string representing a file size, e.g.,
    "22 megabytes".
    :return: The number of bytes represented by the string.
    """
    multipliers = {
        'kilobyte':  1024,
        'megabyte':  1024 ** 2,
        'gigabyte':  1024 ** 3,
        'kb': 1024,
        'mb': 1024**2,
        'gb': 1024**3,
    }

    for suffix in multipliers:
        size_str = size_str.lower().strip().strip('s')
        if size_str.lower().endswith(suffix):
            return int(float(size_str[0:-len(suffix)]) * multipliers[suffix])
    else:
        if size_str.endswith('b'):
            size_str = size_str[0:-1]
        elif size_str.endswith('byte'):
            size_str = size_str[0:-4]
    return int(size_str)
Download .txt
gitextract_wcx03rw_/

├── .circleci/
│   └── config.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Pipfile
├── README.md
├── deps/
│   └── windows/
│       ├── PyStemmer-2.0.1-cp39-cp39-win_amd64.whl
│       └── windows.ps1
├── docs/
│   ├── 1-getting-started.md
│   ├── 2-linux-macos.md
│   ├── 3-windows.md
│   └── 4-docker-guide.md
├── run_linux.sh
├── run_windows.bat
└── src/
    ├── __init__.py
    ├── apis/
    │   ├── __init__.py
    │   ├── pushshift.py
    │   └── reddit.py
    ├── bot.py
    ├── bots/
    │   └── reddit/
    │       ├── __init__.py
    │       ├── actions/
    │       │   ├── cleanup_actions.py
    │       │   ├── comments/
    │       │   │   ├── comment_actions.py
    │       │   │   └── sources/
    │       │   │       └── cobe.py
    │       │   ├── post_actions.py
    │       │   └── utils.py
    │       ├── bot.py
    │       └── utils.py
    ├── config/
    │   ├── cobe_config.py
    │   ├── common_config.py
    │   ├── config_menu.py
    │   ├── reddit/
    │   │   ├── config_gen.py
    │   │   ├── reddit_avoid_subs.txt
    │   │   ├── reddit_avoid_words.txt
    │   │   └── reddit_sub_lists.py
    │   ├── reddit_config.py
    │   └── test.yml
    ├── init.py
    ├── libs/
    │   └── urwide.py
    ├── logs/
    │   ├── log_utils.py
    │   └── logger.py
    ├── menu.py
    ├── tests/
    │   ├── __init__.py
    │   └── test_utils.py
    └── utils.py
Download .txt
SYMBOL INDEX (179 symbols across 17 files)

FILE: src/apis/pushshift.py
  class PS (line 11) | class PS():
    method __init__ (line 12) | def __init__(self):
    method get_posts (line 15) | def get_posts(self, subreddit, **kwargs):
    method get_comments (line 20) | def get_comments(self, subreddit):
    method _ps_search (line 23) | def _ps_search(self, subreddit, before=None, after=None, score=None, l...

FILE: src/apis/reddit.py
  class RedditAPI (line 7) | class RedditAPI():
    method __init__ (line 8) | def __init__(self,

FILE: src/bot.py
  function run (line 7) | def run():

FILE: src/bots/reddit/actions/cleanup_actions.py
  class Cleanup (line 11) | class Cleanup():
    method __init__ (line 12) | def __init__(self):
    method init (line 17) | def init(self):
    method shadow_check (line 21) | def shadow_check(self, roll=1):
    method remove_low_scores (line 34) | def remove_low_scores(self, roll=1):
    method karma_limit (line 62) | def karma_limit(self):

FILE: src/bots/reddit/actions/comments/comment_actions.py
  class Comments (line 15) | class Comments():
    method __init__ (line 16) | def __init__(self, source='cobe'):
    method init (line 27) | def init(self):
    method comment (line 34) | def comment(self, roll=1):

FILE: src/bots/reddit/actions/comments/sources/cobe.py
  class Cobe (line 11) | class Cobe():
    method __init__ (line 12) | def __init__(self, config=CONFIG):
    method get_reply (line 20) | def get_reply(self, replyto: str=''):
    method init (line 26) | def init(self):

FILE: src/bots/reddit/actions/post_actions.py
  function find_synonyms (line 15) | def find_synonyms(keyword):
  function edit_text (line 28) | def edit_text(var, mode):
  class Posts (line 49) | class Posts():
    method __init__ (line 50) | def __init__(self):
    method get_post (line 54) | def get_post(self, subreddit=None):
    method crosspost (line 87) | def crosspost(self, subreddit):
    method repost (line 93) | def repost(self, roll=1, subreddit=None):

FILE: src/bots/reddit/actions/utils.py
  function get_subreddit (line 19) | def get_subreddit(nsfw=False, getsubclass=False):

FILE: src/bots/reddit/bot.py
  class RedditBot (line 17) | class RedditBot():
    method __init__ (line 18) | def __init__(self, config=reddit_config.CONFIG):
    method _init (line 34) | def _init(self):
    method tick (line 51) | def tick(self):
    method run (line 63) | def run(self):

FILE: src/bots/reddit/utils.py
  function parse_user (line 11) | def parse_user(user: Redditors):
  function is_time_between (line 41) | def is_time_between(begin_time, end_time, check_time=None):
  function should_we_sleep (line 49) | def should_we_sleep():

FILE: src/config/config_menu.py
  class Handler (line 43) | class Handler(urwide.Handler):
    method onSave (line 45) | def onSave( self, button ):
    method onCancel (line 61) | def onCancel( self, button ):
  function run (line 69) | def run():

FILE: src/config/reddit/config_gen.py
  function config_gen (line 12) | def config_gen():

FILE: src/libs/urwide.py
  function isString (line 68) | def isString( t ):
  function ensureString (line 71) | def ensureString( t, encoding="utf8" ):
  function safeEnsureString (line 77) | def safeEnsureString( t,  encoding="utf8" ):
  function ensureUnicode (line 83) | def ensureUnicode( t, encoding="utf8" ):
  function ensureBytes (line 89) | def ensureBytes( t, encoding="utf8" ):
  function add_widget (line 95) | def add_widget( container, widget, options=None  ):
  function remove_widgets (line 120) | def remove_widgets( container ):
  function original_widgets (line 125) | def original_widgets( widget ):
  function original_widget (line 138) | def original_widget(widget):
  function original_focus (line 142) | def original_focus(widget):
  class PatchedListBox (line 155) | class PatchedListBox(urwid.ListBox):
    method __init__ (line 159) | def __init__( self, *args, **kwargs ):
    method remove_widgets (line 162) | def remove_widgets( self ):
    method add_widget (line 169) | def add_widget( self, widget ):
  class PatchedPile (line 176) | class PatchedPile(urwid.Pile):
    method __init__ (line 180) | def __init__(self, widget_list, focus_item=None):
    method add_widget (line 190) | def add_widget( self, widget ):
    method remove_widget (line 207) | def remove_widget( self, widget ):
    method remove_widgets (line 214) | def remove_widgets( self ):
  class PatchedColumns (line 219) | class PatchedColumns(urwid.Columns):
    method set_focus (line 221) | def set_focus(self, widget):
  class UISyntaxError (line 239) | class UISyntaxError(Exception): pass
  class UIRuntimeError (line 240) | class UIRuntimeError(Exception): pass
  class UI (line 241) | class UI:
    class Collection (line 253) | class Collection(object):
      method __init__ (line 256) | def __init__( self, collection=None ):
      method __getattr__ (line 261) | def __getattr__( self, name ):
      method __setattr__ (line 268) | def __setattr__( self, name, value):
    method __init__ (line 275) | def __init__( self ):
    method id (line 295) | def id( self, widget ):
    method new (line 302) | def new( self, widgetClass, *args, **kwargs ):
    method wrap (line 323) | def wrap( self, widget, properties ):
    method unwrap (line 328) | def unwrap( self, widget ):
    method handler (line 336) | def handler( self, handler = None ):
    method responder (line 351) | def responder( self, event ):
    method pushHandler (line 355) | def pushHandler( self, handler ):
    method popHandler (line 361) | def popHandler( self ):
    method _handle (line 368) | def _handle( self, event_name, widget, *args, **kwargs ):
    method setTooltip (line 383) | def setTooltip( self, widget, tooltip ):
    method setInfo (line 386) | def setInfo( self, widget, info ):
    method onKey (line 389) | def onKey( self, widget, callback ):
    method onFocus (line 394) | def onFocus( self, widget, callback ):
    method onEdit (line 399) | def onEdit( self, widget, callback ):
    method onPress (line 404) | def onPress( self, widget, callback ):
    method _doPress (line 409) | def _doPress( self, button, *args ):
    method _doFocus (line 418) | def _doFocus( self, widget, ensure=True ):
    method _doEdit (line 425) | def _doEdit( self, widget, before, after, ensure=True ):
    method _doKeyPress (line 432) | def _doKeyPress( self, widget, key ):
    method getFocused (line 476) | def getFocused( self ):
    method focusNext (line 479) | def focusNext( self ):
    method focusPrevious (line 482) | def focusPrevious( self ):
    method getToplevel (line 485) | def getToplevel( self ):
    method isEditable (line 488) | def isEditable( self, widget ):
    method isFocusable (line 491) | def isFocusable( self, widget ):
    method _add (line 502) | def _add( self, widget ):
    method _push (line 511) | def _push( self, endCallback, ui=None, args=(), kwargs={} ):
    method _pop (line 520) | def _pop( self ):
    method create (line 530) | def create( self, style, ui, handler=None ):
    method parseUI (line 536) | def parseUI( self, text ):
    method parseStyle (line 549) | def parseStyle( self, data ):
    method _parseLine (line 569) | def _parseLine( self, line ):
    method _parseAttributes (line 586) | def _parseAttributes( self, data ):
    method _parseUIAttributes (line 593) | def _parseUIAttributes( self, data ):
    method _parseArguments (line 612) | def _parseArguments( self, data ):
    method hasStyle (line 624) | def hasStyle( self, *styles ):
    method _styleWidget (line 630) | def _styleWidget( self, widget, ui ):
    method _createWidget (line 649) | def _createWidget( self, widgetClass, *args, **kwargs ):
    method _wrapWidget (line 683) | def _wrapWidget( self, widget, _ui ):
    method _argsFind (line 717) | def _argsFind( self, data ):
    method _parseTxt (line 726) | def _parseTxt( self, data ):
    method _parseHdr (line 731) | def _parseHdr( self, data ):
    method _parseBtn (line 740) | def _parseBtn( self, data ):
    method _parseChc (line 747) | def _parseChc( self, data ):
    method _parseDvd (line 764) | def _parseDvd( self, data ):
    method _parseBox (line 768) | def _parseBox( self, data ):
    method _parseEdt (line 783) | def _parseEdt( self, data ):
    method _parsePle (line 792) | def _parsePle( self, data ):
    method _parseCol (line 799) | def _parseCol( self, data ):
    method _parseGFl (line 806) | def _parseGFl( self, data ):
    method _parseLBx (line 823) | def _parseLBx( self, data ):
    method _parseEnd (line 829) | def _parseEnd( self, data ):
  class Console (line 843) | class Console(UI):
    method __init__ (line 847) | def __init__( self ):
    method tooltip (line 865) | def tooltip( self, text=-1 ):
    method info (line 872) | def info( self, text=-1 ):
    method footer (line 879) | def footer( self, text=-1 ):
    method dialog (line 886) | def dialog( self, dialog ):
    method getFocused (line 894) | def getFocused( self ):
    method focusNext (line 912) | def focusNext( self ):
    method focusPrevious (line 923) | def focusPrevious( self ):
    method getToplevel (line 934) | def getToplevel( self ):
    method getCurrentSize (line 942) | def getCurrentSize( self ):
    method main (line 949) | def main( self ):
    method run (line 965) | def run( self ):
    method end (line 975) | def end( self, msg=None, status=1 ):
    method loop (line 982) | def loop( self ):
    method draw (line 1024) | def draw( self ):
    method _updateFooter (line 1039) | def _updateFooter(self):
    method parseUI (line 1054) | def parseUI( self, text ):
    method _parseFtr (line 1066) | def _parseFtr( self, data ):
  class Dialog (line 1075) | class Dialog(UI):
    method __init__ (line 1087) | def __init__( self, parent, ui, width=40, height=-1, style="dialog",
    method width (line 1106) | def width( self ):
    method height (line 1110) | def height( self ):
    method view (line 1114) | def view( self ):
    method make (line 1120) | def make( self, uitext, palui=None ):
    method onStart (line 1151) | def onStart( self, callback ):
    method onEnd (line 1155) | def onEnd( self, callback ):
    method doKeyPress (line 1159) | def doKeyPress( self, widget, key ):
    method end (line 1162) | def end( self ):
    method _parseHdr (line 1167) | def _parseHdr( self, data ):
  class Handler (line 1183) | class Handler(object):
    method __init__ (line 1191) | def __init__( self ):
    method respond (line 1194) | def respond( self, event, *args, **kwargs ):
    method responds (line 1201) | def responds( self, event ):
    method responder (line 1207) | def responder( self, event ):

FILE: src/logs/log_utils.py
  function log_json (line 7) | def log_json(tojson: object) -> str:
  class NewLineFileHandler (line 10) | class NewLineFileHandler(RotatingFileHandler):
    method emit (line 15) | def emit(self, record) -> None:
  class NewLineStreamHandler (line 25) | class NewLineStreamHandler(logging.StreamHandler):
    method emit (line 30) | def emit(self, record) -> None:

FILE: src/menu.py
  class Handler (line 28) | class Handler(urwide.Handler):
    method onStarted (line 29) | def onStarted( self, button ):
    method onConfig (line 35) | def onConfig( self, button ):
    method onExit (line 38) | def onExit( self, button ):
  function run (line 50) | def run():

FILE: src/tests/test_utils.py
  function test_random_string (line 5) | def test_random_string():

FILE: src/utils.py
  function random_string (line 18) | def random_string(length: int) -> str:
  function prefer_envar (line 22) | def prefer_envar(configs: dict) -> dict:
  function check_internet (line 34) | def check_internet(host="https://reddit.com", timeout=5):
  function get_public_ip (line 43) | def get_public_ip():
  function bytesto (line 52) | def bytesto(bytes, to, bsize=1024):
  function is_past_one_day (line 68) | def is_past_one_day(time_to_compare):
  function countdown (line 72) | def countdown(seconds=1):
  function chance (line 81) | def chance(value=.20):
  function tobytes (line 88) | def tobytes(size_str):
Condensed preview — 44 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (112K chars).
[
  {
    "path": ".circleci/config.yml",
    "chars": 629,
    "preview": "version: 2.1\norbs:\n  python: circleci/python@1.0.0\n\njobs:\n  build:\n    executor:\n      name: python/default\n      tag: \""
  },
  {
    "path": ".gitignore",
    "chars": 181,
    "preview": "*.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"
  },
  {
    "path": "Dockerfile",
    "chars": 278,
    "preview": "FROM python:3.8\n\nCOPY . /app\nWORKDIR /app\nRUN apt update && apt install -yqq g++ gcc libc6-dev make pkg-config libffi-de"
  },
  {
    "path": "LICENSE",
    "chars": 1071,
    "preview": "MIT License\n\nCopyright (c) 2020 MrPowerScripts\n\nPermission is hereby granted, free of charge, to any person obtaining a "
  },
  {
    "path": "Pipfile",
    "chars": 219,
    "preview": "[[source]]\nname = \"pypi\"\nurl = \"https://pypi.org/simple\"\nverify_ssl = true\n\n[dev-packages]\n\n[packages]\npsaw = \"*\"\npraw ="
  },
  {
    "path": "README.md",
    "chars": 4899,
    "preview": "**This project is no longer in active development. I'll consider merging pull requests as time is available, but there w"
  },
  {
    "path": "deps/windows/windows.ps1",
    "chars": 1249,
    "preview": "$install_help = \"Read windows installation guide https://github.com/MrPowerScripts/reddit-karma-farming-bot/blob/master/"
  },
  {
    "path": "docs/1-getting-started.md",
    "chars": 3288,
    "preview": "# Getting Started\n\n## Creating the Reddit app\n\nIn your browser, when you are logged in with the Reddit account you want "
  },
  {
    "path": "docs/2-linux-macos.md",
    "chars": 442,
    "preview": "# Running the bot on Linux and Macos\n\nRun the `run_linux.sh` script in the root of the repo. This script was designed an"
  },
  {
    "path": "docs/3-windows.md",
    "chars": 1018,
    "preview": "# Running the bot on Windows 10\n\n1. Download and install Python for Windows. You can find [all the releases here](https:"
  },
  {
    "path": "docs/4-docker-guide.md",
    "chars": 369,
    "preview": "# Running the bot in Docker\n\n## Update env file with your credentials\nupdate and rename .env.example to .env\n\n## Build d"
  },
  {
    "path": "run_linux.sh",
    "chars": 1885,
    "preview": "#!/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 $DEBU"
  },
  {
    "path": "run_windows.bat",
    "chars": 310,
    "preview": "@ECHO OFF\r\n\r\nPowershell.exe -executionpolicy bypass -File ./deps/windows/windows.ps1\r\nif errorlevel 1 pause & exit \r\n\r\ni"
  },
  {
    "path": "src/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/apis/__init__.py",
    "chars": 207,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom config import reddit_config\nfrom .reddit import RedditAPI\nfrom .push"
  },
  {
    "path": "src/apis/pushshift.py",
    "chars": 1386,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\n# data sources for comment learning\nfrom psaw import PushshiftAPI\nfrom lo"
  },
  {
    "path": "src/apis/reddit.py",
    "chars": 678,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom praw import Reddit\nfrom utils import random_string\n\n\nclass RedditAPI"
  },
  {
    "path": "src/bot.py",
    "chars": 220,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom bots.reddit import RedditBot\nfrom utils import countdown\nfrom logs.l"
  },
  {
    "path": "src/bots/reddit/__init__.py",
    "chars": 75,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom .bot import RedditBot\n\n"
  },
  {
    "path": "src/bots/reddit/actions/cleanup_actions.py",
    "chars": 3282,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport praw\nimport requests\nfrom apis import pushshift_api, reddit_api\nfr"
  },
  {
    "path": "src/bots/reddit/actions/comments/comment_actions.py",
    "chars": 2190,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom .sources.cobe import Cobe\nfrom logs.logger import log\nfrom collectio"
  },
  {
    "path": "src/bots/reddit/actions/comments/sources/cobe.py",
    "chars": 2498,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom cobe.brain import Brain\nfrom config.cobe_config import CONFIG\nfrom a"
  },
  {
    "path": "src/bots/reddit/actions/post_actions.py",
    "chars": 11973,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport random, requests, re\nfrom time import sleep as s\nfrom apis import "
  },
  {
    "path": "src/bots/reddit/actions/utils.py",
    "chars": 1463,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport random\nfrom apis import reddit_api\nfrom logs.logger import log\nfro"
  },
  {
    "path": "src/bots/reddit/bot.py",
    "chars": 2213,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom apis import reddit_api\nfrom config import reddit_config\nfrom utils i"
  },
  {
    "path": "src/bots/reddit/utils.py",
    "chars": 3362,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport datetime\nfrom logs.logger import log\nfrom config.reddit_config imp"
  },
  {
    "path": "src/config/cobe_config.py",
    "chars": 665,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom utils import prefer_envar\nfrom pathlib import Path\nfrom logs.logger "
  },
  {
    "path": "src/config/common_config.py",
    "chars": 634,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport os\nfrom logs.logger import log\nfrom pathlib import Path\n\n# Prefix "
  },
  {
    "path": "src/config/config_menu.py",
    "chars": 1880,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport os\nimport sys\nimport json\nimport menu\nimport pathlib\nfrom utils im"
  },
  {
    "path": "src/config/reddit/config_gen.py",
    "chars": 1409,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport praw\nimport sys\nimport os\nfrom logs.logger import log\nfrom prawcor"
  },
  {
    "path": "src/config/reddit/reddit_avoid_subs.txt",
    "chars": 2107,
    "preview": "Agoraphobia\nAnxiety\nAssistance\nAtheistTwelveSteppers\nBipolarReddit\nCBTpractice\nDPDR\nDepression\nDiagnosed\nDomesticViolenc"
  },
  {
    "path": "src/config/reddit/reddit_avoid_words.txt",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/config/reddit/reddit_sub_lists.py",
    "chars": 513,
    "preview": "# 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# \""
  },
  {
    "path": "src/config/reddit_config.py",
    "chars": 2209,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom utils import prefer_envar\nfrom logs.logger import log\nfrom logs.log_"
  },
  {
    "path": "src/config/test.yml",
    "chars": 12,
    "preview": "loaded: true"
  },
  {
    "path": "src/init.py",
    "chars": 488,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport sys\nfrom logs.logger import log\nfrom utils import check_internet ,"
  },
  {
    "path": "src/libs/urwide.py",
    "chars": 40997,
    "preview": "#!/usr/bin/env python\n# encoding: utf8\n# -----------------------------------------------------------------------------\n#"
  },
  {
    "path": "src/logs/log_utils.py",
    "chars": 974,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport logging\nimport json\nfrom logging.handlers import RotatingFileHandl"
  },
  {
    "path": "src/logs/logger.py",
    "chars": 889,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nimport logging\nfrom logging.handlers import RotatingFileHandler\nimport os"
  },
  {
    "path": "src/menu.py",
    "chars": 1427,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport pyfiglet\nimport sys\nfrom logs.logger import log\nfrom config import"
  },
  {
    "path": "src/tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/tests/test_utils.py",
    "chars": 184,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nfrom .. import utils\n\ndef test_random_string():\n  string = utils.random_s"
  },
  {
    "path": "src/utils.py",
    "chars": 3388,
    "preview": "#!/usr/bin/env python3\n# -*- coding: UTF-8 -*-\nimport os\nimport collections\nimport random\nimport time\nimport string\nfrom"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the MrPowerScripts/reddit-karma-farming-bot GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 44 files (100.7 KB), approximately 28.3k tokens, and a symbol index with 179 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!