├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── contrib └── Dockerfile ├── hygdrop.hy ├── plugins ├── codeeval.hy └── github.hy ├── requirements-dev.txt ├── requirements.txt └── tests ├── test_codeeval.py └── test_github.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | /.noseids 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.2" 4 | - "3.3" 5 | # command to install dependencies 6 | install: 7 | - pip install -r requirements.txt 8 | - pip install -r requirements-dev.txt 9 | script: make travis 10 | notifications: 11 | email: 12 | - hylang-discuss@googlegroups.com 13 | irc: 14 | channels: 15 | - "irc.freenode.net#hy" 16 | on_success: change 17 | on_failure: change 18 | use_notice: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a 2 | copy of this software and associated documentation files (the "Software"), 3 | to deal in the Software without restriction, including without limitation 4 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 5 | and/or sell copies of the Software, and to permit persons to whom the 6 | Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 14 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 16 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | DEALINGS IN THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | test: 4 | nosetests -sv 5 | travis: 6 | nosetests -s --with-coverage 7 | flake8 tests 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hygdrop 2 | ======= 3 | 4 | IRC bot for Hy. Has following capabiltiy for now 5 | 1. Can get issue details from github 6 | 2. Can get commit details from github 7 | 3. Prints core members of hylang 8 | 4. evaluates hycode 9 | 10 | 11 | Usage 12 | ----- 13 | 14 | Listing Core Team 15 | 16 | > list core team members 17 | 18 | Bot only finds *members* and *core team* word in the message. 19 | 20 | Github issue details 21 | 22 | > hy-mode#14 23 | 24 | > paultag/snitch#2 25 | 26 | The input should be in form `project/repo#issue_number`, in this case 27 | bot doesn't check if line begins with ,. Similary Github commit can be 28 | accessed 29 | 30 | > hy@3e8941c 31 | 32 | > hylang/hy@3e8941c 33 | 34 | In both cases `project` and `repo` is not mandatory, if not given then 35 | bot gets details for `hylang/hy` repository. 36 | 37 | TODO 38 | ==== 39 | - [x] Code can not handle referencing function defined in the same line 40 | - [x] Write a new driver and remove `hygdrop/__init__.hy` 41 | - [x] Integrate spy mode to dump python code 42 | - [x] Pastebin the looong lines and give the pastebin link to IRC 43 | - [x] Move second level exception messages from stderr to IRC 44 | - [ ] Port command.clj from [Cjoey](https://github.com/Foxboron/Parjer/blob/master/src/parjer/commands.clj) 45 | to command.hy 46 | - [ ] Implement private message handling by bot 47 | -------------------------------------------------------------------------------- /contrib/Dockerfile: -------------------------------------------------------------------------------- 1 | # Run hygdrop 2 | # 3 | # VERSION 0.1 4 | 5 | FROM debian:sid 6 | MAINTAINER Vasudev Kamath 7 | 8 | 9 | 10 | # Lets install python3 and pip and git 11 | RUN apt-get update && apt-get install -y python3 python3-pip git 12 | RUN git clone https://github.com/hylang/hygdrop.git 13 | RUN pip3 install -r hygdrop/requirements.txt 14 | 15 | ENTRYPOINT ["hy", "hygdrop/hygdrop.hy"] 16 | -------------------------------------------------------------------------------- /hygdrop.hy: -------------------------------------------------------------------------------- 1 | (import os 2 | irc.bot 3 | [hy.importer [import_file_to_module]] 4 | [docopt [docopt]]) 5 | 6 | (def *plugins* []) 7 | (def *usage* "Hygdrop - Hy IRC bot 8 | Usage: 9 | hygdrop.hy 10 | hygdrop.hy [--server=] [--channel=] [--port=] [--nick=] 11 | hygdrop.hy [-h | --help] 12 | hygdrop.hy [-v | --version] 13 | hygdrop.hy [--dry-run] 14 | 15 | Options: 16 | -h --help Show this help screen 17 | -v --version Show version 18 | --server= Servers to connect 19 | --channel=, Channel to join 20 | --port= Port number to connect to IRC server 21 | --nick= Nick the bot should use") 22 | 23 | (def *arguments* (apply docopt [*usage*] {"version" "Hygdrop 0.1"})) 24 | 25 | (defun welcome-handler [connection event] 26 | (for [(, dir subdir files) (os.walk (-> (os.path.dirname 27 | (os.path.realpath __file__)) 28 | (os.path.join "plugins")))] 29 | (for [file files] 30 | (if (.endswith file ".hy") 31 | (.append *plugins* (import_file_to_module (get (.split file ".") 0) 32 | (os.path.join dir file)))))) 33 | (if (get *arguments* "--channel") 34 | (for [channel (.split (get *arguments* "--channel") ",")] 35 | (.join connection channel)) 36 | (.join connection "#hy"))) 37 | 38 | (defun pubmsg-handler [connection event] 39 | (let [[arg (get event.arguments 0)]] 40 | (for [plugin *plugins*] 41 | (.process plugin connection event arg)))) 42 | 43 | (defun start[] 44 | (let [[server (fn[] (if (get *arguments* "--server") 45 | (get *arguments* "--server") "irc.freenode.net"))] 46 | [port (fn[] (if (get *arguments* "--port") 47 | (get *arguments* "--port") 6667))] 48 | [nick (fn[] (if (get *arguments* "--nick") 49 | (get *arguments* "--nick") "hygdrop"))] 50 | [bot 51 | (irc.bot.SingleServerIRCBot [(, (server) (port))] 52 | (nick) "Hy five!")]] 53 | (.add_global_handler bot.connection "welcome" welcome-handler) 54 | (.add_global_handler bot.connection "pubmsg" pubmsg-handler) 55 | (.start bot))) 56 | 57 | (if (= __name__ "__main__") 58 | (start)) 59 | -------------------------------------------------------------------------------- /plugins/codeeval.hy: -------------------------------------------------------------------------------- 1 | (import ast 2 | sys 3 | builtins 4 | astor 5 | requests 6 | code 7 | traceback 8 | [io [StringIO]] 9 | [time [sleep]] 10 | [hy.lex [LexException PrematureEndOfInput tokenize]] 11 | [hy.importer [import_buffer_to_hst ast_compile]] 12 | [hy.compiler [hy_compile HyTypeError]]) 13 | 14 | 15 | (defclass HygdropREPL [code.InteractiveConsole] 16 | "Hygdrops REPL - Class is a REPL to execute the code for Hygdrop, 17 | this class doesn't strictly adhere to REPL rules and raises 18 | exception so it can be communicated to end user." 19 | [[--init-- 20 | (fn [self &optional [locals null] [filename ""] [symbol "single"]] 25 | (try 26 | (setv tokens (tokenize source)) 27 | (catch [p PrematureEndOfInput] 28 | (progn 29 | (.setexception-source self p source filename) 30 | (raise p))) 31 | (catch [l LexException] 32 | (progn 33 | (.setexception-source self l source filename) 34 | (raise l)))) 35 | (try 36 | (progn 37 | (setv -ast (hy_compile tokens "__console__" ast.Interactive)) 38 | (setv code (ast_compile -ast filename symbol))) 39 | (catch [ht HyTypeError] 40 | (progn 41 | (.setexception-source self ht source filename) 42 | (raise ht))) 43 | (catch [e Exception] 44 | (raise e))) 45 | (.runcode self code) 46 | False)] 47 | [setexception-source 48 | (fn [self e source filename] 49 | "When exceptions are raised if e.source is not set then set it to 50 | current source otherwise the code will break at __str__ 51 | function. This functions are called only for Hy specific exceptions" 52 | (if (= e.source None) 53 | (setv e.source source) 54 | (setv e.filename filename)))]]) 55 | 56 | (def *hr* (HygdropREPL)) 57 | 58 | (defmacro paste-code [payload &rest body] 59 | `(let [[r (.post requests "https://www.refheap.com/api/paste" ~payload)]] 60 | ~@body)) 61 | 62 | (defun dump-exception [connection target e] 63 | (paste-code {"contents" (str e) "language" "Python Traceback"} 64 | (if (= r.status_code 201) 65 | (.privmsg connection target 66 | (.format "Aargh something broke, traceback: {}" 67 | (get (.json r) "url"))) 68 | (progn 69 | (.write sys.stderr (str e)) 70 | (.write sys.stderr "\n") 71 | (.flush sys.stderr))))) 72 | 73 | (defun dump-traceback [connection target] 74 | (let [[tblist (.extract_tb traceback sys.last_traceback)] 75 | [lines (.format_list traceback (cdr tblist))]] 76 | (if lines 77 | (.insert lines 0 "Traceback (most recent call last):\n")) 78 | (.extend lines (.format_exception_only traceback sys.last_type 79 | sys.last_value)) 80 | (paste-code {"contents" (.join "" lines) "language" "Python Traceback"} 81 | (if (= r.status_code 201) 82 | (.privmsg connection target 83 | (.format "Aargh something broke, traceback {}" 84 | (get (.json r) "url"))) 85 | (for [line (.split lines "\n")] 86 | (.privmsg connection target line) 87 | (sleep 0.5)))) 88 | ;; Cleanup the sys attributes so that we can recognize next error, 89 | ;; if not cleared this will get always executed that is IIUC. 90 | (delattr sys "last_value") 91 | (delattr sys "last_type") 92 | (delattr sys "last_traceback") 93 | (setv tblist null))) 94 | 95 | (defun eval-code [connection target code &optional [dry-run False]] 96 | (setv sys.stdout (StringIO)) 97 | (.runsource *hr* code) 98 | ;; (exec (ast_compile (-> (import_buffer_to_hst code) 99 | ;; (hy_compile "__main__" ast.Interactive)) 100 | ;; "" "single")) 101 | (if dry-run 102 | (.replace (.getvalue sys.stdout) "\n" " ") 103 | (let [[output (.getvalue sys.stdout)] 104 | [message ["Output was too long bro, so here is the paste:"]]] 105 | (if (>= (len output) 256) 106 | (paste-code {"contents" output "language" "IRC Logs"} 107 | (if (= r.status_code 201) 108 | (progn 109 | (.append message (get (.json r) "url")) 110 | (.privmsg connection target (.join " " message))))) 111 | (.privmsg connection target (.replace output "\n" " ")))))) 112 | 113 | (defun source-code [connection target hy-code &optional [dry-run False]] 114 | (let [[astorcode (-> (import_buffer_to_hst hy-code) 115 | (hy_compile __name__ ))] 116 | [pysource (.to_source astor.codegen astorcode)]] 117 | (if dry-run 118 | pysource 119 | (if (or (!= (.find pysource "\n") -1) (>= (len pysource) 512)) 120 | (paste-code {"contents" pysource "language" "Python"} 121 | (if (= r.status_code 201) 122 | (.privmsg connection target 123 | (.format "Yo bro your source is ready at {}" 124 | (get (.json r) "url"))) 125 | (.privmsg connection target 126 | "Something went wrong while creating paste"))) 127 | (for [srcline (.split pysource "\n")] 128 | (.privmsg connection target srcline) 129 | (sleep 0.5)))))) 130 | 131 | (defun process [connection event message] 132 | (try 133 | (progn 134 | (if (or (.startswith message ",") 135 | (.startswith message (+ connection.nickname ": "))) 136 | (progn 137 | (setv code-startpos ((fn[] (if (.startswith message ",") 1 138 | (+ (len connection.nickname) 1))))) 139 | (eval-code connection event.target (slice message code-startpos)) 140 | ;; check if an error occured and is printed by show_traceback 141 | ;; of REPL to stderr in that case print this traceback to IRC 142 | (if (hasattr sys "last_traceback") 143 | (dump-traceback connection event.target)))) 144 | (if (.startswith message "!source") 145 | (source-code connection event.target (slice message 146 | (+ (len "!source") 1))))) 147 | (catch [e Exception] 148 | (try 149 | (for [line (.split (str e) "\n")] 150 | (.privmsg connection event.target line) 151 | (sleep 0.5)) 152 | (catch [f Exception] 153 | (progn 154 | (dump-exception connection event.target e) 155 | (dump-exception connection event.target f))))))) 156 | -------------------------------------------------------------------------------- /plugins/github.hy: -------------------------------------------------------------------------------- 1 | (import requests 2 | re 3 | [functools [partial lru_cache]]) 4 | 5 | (defmacro kwonly [f kwargs] `(apply ~f [] ~kwargs)) 6 | 7 | (with-decorator (kwonly lru_cache {"maxsize" 256}) 8 | (defun get-github-issue [connection target issue 9 | &optional [project "hylang"] [repo "hy"] 10 | [dry-run False]] 11 | (let [[api-url (.format "https://api.github.com/repos/{}/{}/issues/{}" 12 | project repo issue)] 13 | [api-result (.get requests api-url)] 14 | [api-json (.json api-result)]] 15 | (if (= (getattr api-result "status_code") 200) 16 | (let [[title (get api-json "title")] 17 | [status (get api-json "state")] 18 | [issue-url (get api-json "html_url")] 19 | [get-name (fn [x] (get x "name"))] 20 | [labels (.join "|" (map get-name (get api-json "labels")))] 21 | [author (get (get api-json "user") "login")] 22 | [message (list)]] 23 | (if (get (get api-json "pull_request") "html_url") 24 | (.extend message [(+ "Pull Request #" issue)]) 25 | (.extend message [(+ "Issue #" issue)])) 26 | (.extend message ["on" (+ project "/" repo) 27 | "by" (+ author ":") title 28 | (+ "(" status ")")]) 29 | (if labels 30 | (setv please-hy-don-t-return-when-i 31 | (.append message (+ "[" labels "]")))) 32 | (.append message (+ "<" issue-url ">")) 33 | (if dry-run 34 | (.join " " message) 35 | (.notice connection target (.join " " message)))))))) 36 | 37 | (with-decorator (kwonly lru_cache {"maxsize" 256}) 38 | (defun get-github-commit [connection target commit 39 | &optional [project "hylang"] [repo "hy"] 40 | [dry-run False]] 41 | (let [[api-url (.format "https://api.github.com/repos/{}/{}/commits/{}" 42 | project repo commit)] 43 | [api-result (.get requests api-url)] 44 | [api-json (.json api-result)]] 45 | (if (= (getattr api-result "status_code") 200) 46 | (let [[commit-json (get api-json "commit")] 47 | 48 | [title (get (.splitlines (get commit-json "message")) 0)] 49 | [author (get (get commit-json "author") "name")] 50 | [commit-url (get api-json "html_url")] 51 | [shasum (get api-json "sha")] 52 | [message ["Commit" (slice shasum 0 7) "on" 53 | (+ project "/" repo) 54 | "by" (+ author ":") title 55 | (+ "<" commit-url ">")]]] 56 | (if dry-run 57 | (.join " " message) 58 | (.notice connection target (.join " " message)))))))) 59 | 60 | (with-decorator (kwonly lru_cache {"maxsize" 256}) 61 | (defun get-core-members [connection target &optional [project "hylang"] 62 | [dry-run False]] 63 | (let [[api-url (.format "https://api.github.com/orgs/{}/members" 64 | project)] 65 | [api-result (.get requests api-url)] 66 | [api-json (.json api-result)] 67 | [message (list)]] 68 | (if (= (getattr api-result "status_code") 200) 69 | (do 70 | (for [dev api-json] 71 | (do 72 | (setv dev-result (.get requests 73 | (.format "https://api.github.com/users/{}" 74 | (get dev "login")))) 75 | (if (= (getattr dev-result "status_code") 200) 76 | ;; special case handling specifically for khinsen, his 77 | ;; null name breaks our code :( 78 | (setv don-t-return-damit 79 | (.extend message [(if (not (get (.json dev-result) "name")) 80 | (get dev "login") 81 | (get (.json dev-result) "name"))]))))))) 82 | (if dry-run 83 | (+ "Core Team consists of: " (.join ", " message)) 84 | (.notice connection target (+ "Core Team consists of: " 85 | (.join ", " message))))))) 86 | 87 | (defun handle-github-msg [github-fn github-msg 88 | &optional [dry-run False]] 89 | (let [[project (.group github-msg "project")] 90 | [repo (.group github-msg "repo")] 91 | [query (.group github-msg "query")]] 92 | (if (not project) (setv project "hylang")) 93 | (if (not repo) (setv repo "hy")) 94 | (apply github-fn [query] {"project" project "repo" repo 95 | "dry_run" dry-run}))) 96 | 97 | (defun process[connection event message] 98 | (let [[issue-msg (re.search "(((?P[a-zA-Z0-9._-]+)/)?(?P[a-zA-Z0-9._-]+))?#(?P\\d+)" message)] 99 | [commit-msg (re.search "(((?P[a-zA-Z0-9._-]+)/)?(?P[a-zA-Z0-9._-]+))?@(?P[a-f0-9]+)" message)] 100 | [issue-fn (partial get-github-issue connection event.target)] 101 | [commit-fn (partial get-github-issue connection event.target)]] 102 | (if issue-msg 103 | (handle-github-msg issue-fn issue-msg)) 104 | (if commit-msg 105 | (handle-github-msg commit-fn commit-msg)) 106 | (if (not (= (re.search 107 | "(?:(.*core team.*members?.*|.*members?.*core team.*))" 108 | message) null)) 109 | (get-core-members connection event.target)))) 110 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | nose 3 | coverage 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | irc 2 | requests 3 | -e git+https://github.com/hylang/hy.git#egg=hy 4 | astor>=0.3 5 | docopt 6 | -------------------------------------------------------------------------------- /tests/test_codeeval.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | plugin_to_load = os.path.join(os.path.dirname(os.path.realpath(__file__)), 5 | "../plugins/codeeval.hy") 6 | 7 | from hy.importer import import_file_to_module 8 | 9 | g = import_file_to_module("codeeval", plugin_to_load) 10 | 11 | 12 | def test_eval_code(): 13 | expected = "[2, 2, 2, 4, 6, 10, 16, 26, 42, 68]" 14 | actual = g.eval_code(None, None, 15 | "(defn fib [x] (if (<= x 2) 2 (+ (fib (- x 1)) " + 16 | "(fib (- x 2))))) (list-comp (fib x) [x (range 10)])", 17 | dry_run=True) 18 | 19 | # strip the space resulting from eval_code function 20 | assert actual.strip() == expected.strip() 21 | 22 | 23 | def test_source_code(): 24 | expected = "def fib(x):\n return (2 if (x <= 2) else" +\ 25 | " (fib((x - 1)) + fib((x - 2))))\nprint([fib(x) for x in range(10)])" 26 | actual = g.source_code(None, None, 27 | "(defn fib [x] (if (<= x 2) 2 (+ (fib (- x 1))" + 28 | " (fib (- x 2))))) (print (list-comp (fib x)" + 29 | " [x (range 10)]))", 30 | dry_run=True) 31 | assert actual == expected 32 | -------------------------------------------------------------------------------- /tests/test_github.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | plugin_to_load = os.path.join(os.path.dirname(os.path.realpath(__file__)), 5 | "../plugins/github.hy") 6 | 7 | from hy.importer import import_file_to_module 8 | 9 | g = import_file_to_module("github", plugin_to_load) 10 | 11 | 12 | def test_get_github_issue(): 13 | expected = " ".join( 14 | ["Issue #" + "180", "on", "hylang/hy", "by", 15 | "khinsen:", 16 | "Macro expansion works differently from Lisp conventions", 17 | "(open)", "[bug]", 18 | ""]) 19 | actual = g.get_github_issue(None, None, "180", dry_run=True) 20 | assert expected == actual 21 | 22 | 23 | def test_get_github_commit(): 24 | expected = " ".join( 25 | ["Commit", "3e8941c", "on", "hylang/hy", "by", 26 | "Berker Peksag:", 27 | "Convert stdout and stderr to UTF-8 properly in the run_cmd helper.", 28 | ""]) 30 | actual = g.get_github_commit(None, None, "3e8941c", dry_run=True) 31 | assert expected == actual 32 | 33 | 34 | def test_get_core_members(): 35 | expected = "Core Team consists of: " + \ 36 | ", ".join(["Julien Danjou", "Nicolas Dandrimont", 37 | "Gergely Nagy", "Berker Peksag", 38 | "Christopher Allan Webber", "khinsen", 39 | "J Kenneth King", "Paul Tagliamonte", 40 | "Bob Tolbert", "Will Kahn-Greene", 41 | "Morten Linderud", "Abhishek L"]) 42 | actual = g.get_core_members(None, None, dry_run=True) 43 | assert actual == expected 44 | --------------------------------------------------------------------------------