├── LICENSE ├── README.md ├── aur-autovote └── aur-vote /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jaden Peterson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aurvote-utils 2 | A set of utilities for managing AUR votes 3 | 4 | ## Packages 5 | [`aurvote-utils`](https://aur.archlinux.org/packages/aurvote-utils/) 6 | 7 | [`aurvote-utils-git`](https://aur.archlinux.org/packages/aurvote-utils-git/) 8 | 9 | ## aur-vote 10 | > NOTE: If you have `aurutils` installed, `aur vote` may be used instead. 11 | 12 | If necessary, `aur-vote` will prompt for authentication. Cookies are saved in `$XDG_DATA_HOME/aurvote-utils/cookie` 13 | and are renewed automatically. Alternatively, you may regenerate the cookie manually: 14 | 15 | ``` 16 | $ aur-vote -a 17 | ``` 18 | 19 | A list of currently voted AUR packages can be queried: 20 | ``` 21 | $ aur-vote -l 22 | ``` 23 | 24 | AUR Package(s) can be voted for: 25 | ``` 26 | $ aur-vote -v foo 27 | $ aur-vote -v bar baz 28 | ``` 29 | 30 | And vote(s) can be removed: 31 | ``` 32 | $ aur-vote -u package 33 | $ aur-vote -u otherpackage anotherpackage 34 | ``` 35 | 36 | ## aur-autovote 37 | > NOTE: If you have `aurutils` installed, `aur autovote` may be used instead. 38 | 39 | If you wish to show appreciation for the packages you have installed, `aur-autovote` is the right tool for the job. 40 | When executed, it will vote for installed packages and remove votes for packages that aren't installed. 41 | 42 | By default packages that don't belong to a repository are queried. 43 | However, if you use a repository to manage AUR packages (e.g. through `aurutils`), it can be used instead: 44 | 45 | ``` 46 | aur-autovote -r repo 47 | ``` 48 | -------------------------------------------------------------------------------- /aur-autovote: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import subprocess 5 | 6 | def get_pkgs(args): 7 | return set([line.decode().strip() for line in subprocess.Popen(args, stdout=subprocess.PIPE).stdout]) 8 | 9 | def main(): 10 | parser = argparse.ArgumentParser(usage="%(prog)s [-h | -r | -k]") 11 | parser.add_argument("-r", "--repo", metavar="", help="use a repo instead of foreign packages (e.g. aurutils)") 12 | parser.add_argument("-k", "--keep", action="store_true", help="don't unvote uninstalled packages") 13 | 14 | args = parser.parse_args() 15 | 16 | installed = get_pkgs(("pacman",) + (("-Slq", args.repo) if args.repo else ("-Qqm",))) 17 | voted = get_pkgs(("aur-vote", "-l")) 18 | 19 | # The packages to vote and unvote 20 | vote = sorted(installed.difference(voted)) 21 | unvote = [] if args.keep else sorted(voted.difference(installed)) 22 | 23 | if vote or unvote: 24 | subprocess.call(("aur-vote", "-a")) 25 | 26 | if vote: 27 | subprocess.call(("aur-vote", "-v") + tuple(vote)) 28 | print("Voted for:") 29 | 30 | for pkg in vote: 31 | print(f" {pkg}") 32 | 33 | if unvote: 34 | print() 35 | 36 | if unvote: 37 | subprocess.call(("aur-vote", "-u") + tuple(unvote)) 38 | print("Unvoted:") 39 | 40 | for pkg in unvote: 41 | print(f" {pkg}") 42 | else: 43 | print("Nothing to do") 44 | 45 | if __name__ == "__main__": 46 | main() 47 | -------------------------------------------------------------------------------- /aur-vote: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import bs4 5 | import collections 6 | import getpass 7 | import os 8 | import pickle 9 | import requests 10 | import sys 11 | 12 | LOGIN_URL = "https://aur.archlinux.org/login/" 13 | PACKAGES_URL = "https://aur.archlinux.org/packages/" 14 | 15 | SEARCH_URL_TEMPLATE = "https://aur.archlinux.org/packages/?O=%d&SO=d&PP=250&do_Search=Go&SB=w" 16 | PACKAGE_URL_TEMPLATE = "https://aur.archlinux.org/packages/%s/" 17 | VOTE_URL_TEMPLATE = "https://aur.archlinux.org/pkgbase/%s/vote/" 18 | UNVOTE_URL_TEMPLATE = "https://aur.archlinux.org/pkgbase/%s/unvote/" 19 | 20 | DATA_PATH = os.getenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share")) + "/aurvote-utils" 21 | COOKIE_PATH = f"{DATA_PATH}/cookie" 22 | 23 | Package = collections.namedtuple("Package", ("name", "version", "votes", "popularity", "voted", "notify", "description", "maintainer")) 24 | 25 | class CustomHelpFormatter(argparse.HelpFormatter): 26 | """ Used to customize multiparameter metavars """ 27 | 28 | def _format_args(self, action, default_metavar): 29 | return "<%s>..." % self._metavar_formatter(action, default_metavar)(1) 30 | 31 | def decode(response): 32 | """ Parse an HTML response string """ 33 | 34 | return bs4.BeautifulSoup(response.text, "html.parser") 35 | 36 | def load_cookie(session, path): 37 | """ Load a cookie jar 38 | 39 | Returns: 40 | True if the cookie exists and is accepted, False otherwise 41 | """ 42 | 43 | if not os.path.isfile(path): 44 | return False 45 | 46 | with open(path, "rb") as file: 47 | session.cookies.update(pickle.load(file)) 48 | 49 | return is_logged_in(decode(session.get(PACKAGES_URL))) 50 | 51 | def dump_cookie(session, path): 52 | os.makedirs(os.path.dirname(path), exist_ok=True) 53 | 54 | with open(path, "wb") as file: 55 | pickle.dump(session.cookies, file) 56 | 57 | def login(session, username, password): 58 | """ Login to the AUR 59 | 60 | Returns: 61 | True if the login was successful, False otherwise 62 | """ 63 | 64 | soup = decode(session.post(LOGIN_URL, { 65 | "user": username, 66 | "passwd": password, 67 | "remember_me": "on", 68 | })) 69 | 70 | return is_logged_in(soup) 71 | 72 | def is_logged_in(soup): 73 | return soup.select_one("a[href=\"/logout/\"]") != None 74 | 75 | def get_token(session): 76 | soup = decode(session.get(PACKAGES_URL)) 77 | return soup.select_one("input[name=token]")["value"] 78 | 79 | def get_voted(session): 80 | """ Get the voted packages 81 | 82 | Returns: 83 | A tuple of Package objects 84 | """ 85 | 86 | offset = 0 87 | 88 | while True: 89 | # Load the search page and parse the rows in the results table 90 | rows = decode(session.get(SEARCH_URL_TEMPLATE % offset)).select("tbody > tr") 91 | 92 | # In case we reach the last page 93 | if not rows: 94 | return 95 | 96 | for row in rows: 97 | pkg = Package(*(column.get_text(strip=True) for column in row.find_all("td")[1:])) 98 | 99 | if not pkg.voted: 100 | return 101 | 102 | yield pkg 103 | 104 | offset += 250 105 | 106 | def get_base(session, pkg): 107 | soup = decode(session.get(PACKAGE_URL_TEMPLATE % pkg)) 108 | return soup.select_one("#pkginfo > tr:nth-of-type(2) a").get_text() 109 | 110 | def vote(session, token, pkg): 111 | response = session.post(VOTE_URL_TEMPLATE % get_base(session, pkg), { 112 | "token": token, "do_Vote": pkg 113 | }, allow_redirects=False) 114 | 115 | return response.status_code == requests.codes.found 116 | 117 | def unvote(session, token, pkg): 118 | response = session.post(UNVOTE_URL_TEMPLATE % get_base(session, pkg), { 119 | "token": token, "do_UnVote": pkg 120 | }, allow_redirects=False) 121 | 122 | return response.status_code == requests.codes.found 123 | 124 | def main(): 125 | parser = argparse.ArgumentParser(usage="%(prog)s [-h | -a | -l | (-v | -u) ...]", formatter_class=CustomHelpFormatter) 126 | parser.add_argument("-a", "--login", action="store_true", help="login to the AUR") 127 | parser.add_argument("-l", "--list", action="store_true", help="list the voted packages (default)") 128 | parser.add_argument("-v", "--vote", nargs="+", metavar="pkg", help="vote for package(s)") 129 | parser.add_argument("-u", "--unvote", nargs="+", metavar="pkg", help="unvote package(s)") 130 | 131 | if len(sys.argv) > 1: 132 | args = parser.parse_args() 133 | session = requests.Session() 134 | 135 | if not load_cookie(session, COOKIE_PATH): 136 | if not login(session, input("Username: "), getpass.getpass()): 137 | parser.error("unable to login") 138 | 139 | dump_cookie(session, COOKIE_PATH) 140 | 141 | if args.list: 142 | for pkg in sorted(get_voted(session)): 143 | print(pkg.name) 144 | 145 | elif args.vote != None: 146 | token = get_token(session) 147 | 148 | for pkg in args.vote: 149 | if not vote(session, token, pkg): 150 | print("unable to vote %s" % pkg) 151 | 152 | elif args.unvote != None: 153 | token = get_token(session) 154 | 155 | for pkg in args.unvote: 156 | if not unvote(session, token, pkg): 157 | print("unable to unvote %s" % pkg) 158 | else: 159 | parser.print_help() 160 | 161 | if __name__ == "__main__": 162 | main() 163 | --------------------------------------------------------------------------------