diff --git a/howdoi/how.py b/howdoi/how.py new file mode 100644 index 0000000..41ec520 --- /dev/null +++ b/howdoi/how.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python + +###################################################### +# +# howdoi - instant coding answers via the command line +# written by Benjamin Gleitzman (gleitz@mit.edu) +# inspired by Rich Jones (rich@anomos.info) +# +###################################################### + +import argparse +import glob +import os +import random +import re +import requests +import requests_cache +import sys +from . import __version__ + +from pygments import highlight +from pygments.lexers import guess_lexer, get_lexer_by_name +from pygments.formatters.terminal import TerminalFormatter +from pygments.util import ClassNotFound + +from pyquery import PyQuery as pq +from requests.exceptions import ConnectionError +from requests.exceptions import SSLError + +# Handle imports for Python 2 and 3 +if sys.version < '3': + import codecs + from urllib import quote as url_quote + from urllib import getproxies + + # Handling Unicode: http://stackoverflow.com/a/6633040/305414 + def u(x): + return codecs.unicode_escape_decode(x)[0] +else: + from urllib.request import getproxies + from urllib.parse import quote as url_quote + + def u(x): + return x + + +if os.getenv('HOWDOI_DISABLE_SSL'): # Set http instead of https + SCHEME = 'http://' + VERIFY_SSL_CERTIFICATE = False +else: + SCHEME = 'https://' + VERIFY_SSL_CERTIFICATE = True + +URL = os.getenv('HOWDOI_URL') or 'stackoverflow.com' + +USER_AGENTS = ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:11.0) Gecko/20100101 Firefox/11.0', + 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:22.0) Gecko/20100 101 Firefox/22.0', + 'Mozilla/5.0 (Windows NT 6.1; rv:11.0) Gecko/20100101 Firefox/11.0', + ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4) AppleWebKit/536.5 (KHTML, like Gecko) ' + 'Chrome/19.0.1084.46 Safari/536.5'), + ('Mozilla/5.0 (Windows; Windows NT 6.1) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.46' + 'Safari/536.5'), ) +SEARCH_URLS = { + 'bing': SCHEME + 'www.bing.com/search?q=site:{0}%20{1}', + 'google': SCHEME + 'www.google.com/search?q=site:{0}%20{1}' +} +STAR_HEADER = u('\u2605') +ANSWER_HEADER = u('{2} Answer from {0} {2}\n{1}') +NO_ANSWER_MSG = '< no answer given >' +XDG_CACHE_DIR = os.environ.get('XDG_CACHE_HOME', + os.path.join(os.path.expanduser('~'), '.cache')) +CACHE_DIR = os.path.join(XDG_CACHE_DIR, 'howdoi') +CACHE_FILE = os.path.join(CACHE_DIR, 'cache{0}'.format( + sys.version_info[0] if sys.version_info[0] == 3 else '')) +howdoi_session = requests.session() + + +def get_proxies(): + proxies = getproxies() + filtered_proxies = {} + for key, value in proxies.items(): + if key.startswith('http'): + if not value.startswith('http'): + filtered_proxies[key] = 'http://%s' % value + else: + filtered_proxies[key] = value + return filtered_proxies + + +def _get_result(url): + try: + return howdoi_session.get(url, headers={'User-Agent': random.choice(USER_AGENTS)}, proxies=get_proxies(), + verify=VERIFY_SSL_CERTIFICATE).text + except requests.exceptions.SSLError as e: + print('[ERROR] Encountered an SSL Error. Try using HTTP instead of ' + 'HTTPS by setting the environment variable "HOWDOI_DISABLE_SSL".\n') + raise e + + +def _extract_links_from_bing(html): + html.remove_namespaces() + return [a.attrib['href'] for a in html('.b_algo')('h2')('a')] + + +def _extract_links_from_google(html): + return [a.attrib['href'] for a in html('.l')] or \ + [a.attrib['href'] for a in html('.r')('a')] + + +def _extract_links(html, search_engine): + if search_engine == 'bing': + return _extract_links_from_bing(html) + return _extract_links_from_google(html) + + +def _get_search_url(search_engine): + return SEARCH_URLS.get(search_engine, SEARCH_URLS['google']) + + +def _get_links(query): + search_engine = os.getenv('HOWDOI_SEARCH_ENGINE', 'google') + search_url = _get_search_url(search_engine) + + result = _get_result(search_url.format(URL, url_quote(query))) + html = pq(result) + return _extract_links(html, search_engine) + + +def get_link_at_pos(links, position): + if not links: + return False + + if len(links) >= position: + link = links[position - 1] + else: + link = links[-1] + return link + + +def _format_output(code, args): + if not args['color']: + return code + lexer = None + + # try to find a lexer using the StackOverflow tags + # or the query arguments + for keyword in args['query'].split() + args['tags']: + try: + lexer = get_lexer_by_name(keyword) + break + except ClassNotFound: + pass + + # no lexer found above, use the guesser + if not lexer: + try: + lexer = guess_lexer(code) + except ClassNotFound: + return code + + return highlight(code, + lexer, + TerminalFormatter(bg='dark')) + + +def _is_question(link): + return re.search('questions/\d+/', link) + + +def _get_questions(links): + return [link for link in links if _is_question(link)] + + +def _get_answer(args, links): + link = get_link_at_pos(links, args['pos']) + if not link: + return False + if args.get('link'): + return link + page = _get_result(link + '?answertab=votes') + html = pq(page) + + first_answer = html('.answer').eq(0) + instructions = first_answer.find('pre') or first_answer.find('code') + args['tags'] = [t.text for t in html('.post-tag')] + + if not instructions and not args['all']: + text = first_answer.find('.post-text').eq(0).text() + elif args['all']: + texts = [] + for html_tag in first_answer.items('.post-text > *'): + current_text = html_tag.text() + if current_text: + if html_tag[0].tag in ['pre', 'code']: + texts.append(_format_output(current_text, args)) + else: + texts.append(current_text) + text = '\n'.join(texts) + else: + text = _format_output(instructions.eq(0).text(), args) + if text is None: + text = NO_ANSWER_MSG + text = text.strip() + return text + + +def _get_instructions(args): + links = _get_links(args['query']) + if not links: + return False + + question_links = _get_questions(links) + if not question_links: + return False + + only_hyperlinks = args.get('link') + star_headers = (args['num_answers'] > 1 or args['all']) + + answers = [] + initial_position = args['pos'] + spliter_length = 80 + answer_spliter = '\n' + '=' * spliter_length + '\n\n' + + for answer_number in range(args['num_answers']): + current_position = answer_number + initial_position + args['pos'] = current_position + link = get_link_at_pos(question_links, current_position) + answer = _get_answer(args, question_links) + if not answer: + continue + if not only_hyperlinks: + answer = format_answer(link, answer, star_headers) + answer += '\n' + answers.append(answer) + return answer_spliter.join(answers) + + +def format_answer(link, answer, star_headers): + if star_headers: + return ANSWER_HEADER.format(link, answer, STAR_HEADER) + return answer + + +def _enable_cache(): + if not os.path.exists(CACHE_DIR): + os.makedirs(CACHE_DIR) + requests_cache.install_cache(CACHE_FILE) + + +def _clear_cache(): + for cache in glob.iglob('{0}*'.format(CACHE_FILE)): + os.remove(cache) + + +def howdoi(args): + args['query'] = ' '.join(args['query']).replace('?', '') + try: + return _get_instructions(args) or 'Sorry, couldn\'t find any help with that topic\n' + except (ConnectionError, SSLError): + return 'Failed to establish network connection\n' + + +def get_parser(): + parser = argparse.ArgumentParser(description='instant coding answers via the command line') + parser.add_argument('query', metavar='QUERY', type=str, nargs='*', + help='the question to answer') + parser.add_argument('-p', '--pos', help='select answer in specified position (default: 1)', default=1, type=int) + parser.add_argument('-a', '--all', help='display the full text of the answer', + action='store_true') + parser.add_argument('-l', '--link', help='display only the answer link', + action='store_true') + parser.add_argument('-c', '--color', help='enable colorized output', + action='store_true') + parser.add_argument('-n', '--num-answers', help='number of answers to return', default=1, type=int) + parser.add_argument('-C', '--clear-cache', help='clear the cache', + action='store_true') + parser.add_argument('-v', '--version', help='displays the current version of howdoi', + action='store_true') + return parser + + +def command_line_runner(): + parser = get_parser() + args = vars(parser.parse_args()) + + if args['version']: + print(__version__) + return + + if args['clear_cache']: + _clear_cache() + print('Cache cleared successfully') + return + + if not args['query']: + parser.print_help() + return + + # enable the cache if user doesn't want it to be disabled + if not os.getenv('HOWDOI_DISABLE_CACHE'): + _enable_cache() + + if os.getenv('HOWDOI_COLORIZE'): + args['color'] = True + + utf8_result = howdoi(args).encode('utf-8', 'ignore') + if sys.version < '3': + print(utf8_result) + else: + # Write UTF-8 to stdout: https://stackoverflow.com/a/3603160 + sys.stdout.buffer.write(utf8_result) + # close the session to release connection + howdoi_session.close() + + +if __name__ == '__main__': + command_line_runner() diff --git a/howdoi/howdoi.py b/howdoi/howdoi.py new file mode 100644 index 0000000..5b59487 --- /dev/null +++ b/howdoi/howdoi.py @@ -0,0 +1,86 @@ +import discord + +from discord.ext import commands + +from .utils.chat_formatting import pagify +from .utils.chat_formatting import box + +from howdoi import howdoi + + +class Howdoi: + """Cog for answering coding questions""" + + def __init__(self, bot): + self.bot = bot + self.query = "" + self.args = { + "query": self.query, + "pos": 1, + "all": False, + "link": True, + "color": False, + "num_answers": 1, + "clear_cache": False, + "version": False + } + + @commands.group(pass_context=True) + async def howdoiset(self, ctx): + """Adjust howdoi settings + Settings are reset on reload""" + if ctx.invoked_subcommand is None: + await self.bot.send_cmd_help(ctx) + + @howdoiset.command(pass_context=True, name="answers") + async def howdoiset_answers(self, ctx, num_answers: int=1): + """Adjust number of answers provided. + Defaults to 1""" + + self.args['num_answers'] = num_answers + await self.bot.say("Number of answers provided will now be {}".format(num_answers)) + + @howdoiset.command(pass_context=True, name="link") + async def howdoiset_link(self, ctx): + """Toggles providing in-line answers or a link + Default On""" + + self.args['link'] = not self.args['link'] + + if self.args['link']: + await self.bot.say("Answers will now be provided as a link") + else: + await self.bot.say("Answers will now be provided as the response") + + @howdoiset.command(pass_context=True, name="full") + async def howdoiset_full(self, ctx): + """Toggles providing full answers or just first code found + Default Off + Only works if links are turned off""" + + self.args['all'] = not self.args['all'] + + if self.args['all']: + await self.bot.say("Answers will now be provided in full context") + else: + await self.bot.say("Answers will now be provided as a code snippet") + + @commands.command(pass_context=True) + async def howdoi(self, ctx, *question): + """Ask a coding question""" + self.query = " ".join(question) + + self.args["query"] = self.query + + out = howdoi.howdoi(self.args.copy()) # .encode('utf-8', 'ignore') + + if self.args['link']: + await self.bot.say(out) + else: + await self.bot.say(box(out,"python")) + # for page in pagify(out, shorten_by=24): + # await self.bot.say(box(page)) + +def setup(bot): + n = Howdoi(bot) + bot.add_cog(n) diff --git a/howdoi/info.json b/howdoi/info.json new file mode 100644 index 0000000..f791930 --- /dev/null +++ b/howdoi/info.json @@ -0,0 +1,10 @@ +{ + "AUTHOR" : "Bobloy", + "INSTALL_MSG" : "Thank you for installing! Ask a question with [p]howdoi", + "NAME" : "tourney", + "SHORT" : "Answer coding questions", + "DESCRIPTION" : "Answer coding questions using gleitz's howdoi code @ https://github.com/gleitz/howdoi", + "TAGS" : ["howdoi", "coding","bobloy"], + "REQUIREMENTS" : ["Pygments", "argparse", "cssselect", "lxml", "pyquery", "requests", "requests-cache", "howdoi"], + "HIDDEN" : false +} \ No newline at end of file