├── nodeList.txt ├── requirements.txt ├── .gitignore ├── settings.json ├── themes └── default │ ├── attachmentsBg.txt │ ├── errorRetrievingPage.txt │ ├── boardListBg.txt │ ├── emptyBoard.txt │ ├── logo.txt │ └── posts.json ├── postParsing.py ├── asciiArtLoader.py ├── README.md ├── nntp.py ├── yukko.py └── LICENCE.md /nodeList.txt: -------------------------------------------------------------------------------- 1 | https://2hu-ch.org/ 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pypng 2 | requests 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.save 3 | *.pyc 4 | downloads/ 5 | -------------------------------------------------------------------------------- /settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "download directory":"./downloads/", 3 | "text editor":"nano", 4 | "theme folder":"./themes/default/", 5 | "max overview lines":5, 6 | "max overview posts":3, 7 | "default board":"overchan.technology", 8 | "http proxy":"", 9 | "https proxy":"", 10 | "user agent":"Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0" 11 | } 12 | -------------------------------------------------------------------------------- /themes/default/attachmentsBg.txt: -------------------------------------------------------------------------------- 1 | ___________ 2 | .: __ _____\ 3 | : /|/__V \|_ '. 4 | : | _▂_ _▂_ | 5 | .| \\\ \\\ \ 6 | ◞: . / 7 | '__'.________.: 8 | __.' '.__ 9 | .:._ _____ .:'. 10 | .:_.' '._;. 11 | : .|: : . : 12 | '__' |: : |'__' 13 | |'________' | 14 | | .___ _.| 15 | ;'_.' ' ; 16 | -------------------------------------------------------------------------------- /themes/default/errorRetrievingPage.txt: -------------------------------------------------------------------------------- 1 | ___________ 2 | .: __ _____\ 3 | : /|/__V \|_ '. 4 | : | U U | 5 | .| \ 6 | ◞: _ /\ 7 | '__'.________.: \ 8 | __.' '.__. / 9 | ' \ʌ/ / 10 | | | ' ' 11 | | | : | 12 | | | : | 13 | | | : | 14 | :_-'___ .| 15 | (..' '_.' ' 16 | Error retrieving page! 17 | {0} 18 | -------------------------------------------------------------------------------- /themes/default/boardListBg.txt: -------------------------------------------------------------------------------- 1 | .‾‾‾‾‾‾‾‾. 2 | : _ : 3 | \/_V: :_ 4 | '.■ .; :'; 5 | : '._.' , . ‾. 6 | ; : : ; 7 | . : ' _' 8 | : ◞ \/\ / 9 | '._____.'\/V 10 | .' .:. 11 | . . ; 12 | (,-' ' 13 | .___.-'| 14 | | . 15 | . | 16 | | | 17 | |.__ | 18 | || '._| 19 | || | 20 | || | 21 | ':___._| 22 | ==' . 23 | ':_____._| 24 | -------------------------------------------------------------------------------- /themes/default/emptyBoard.txt: -------------------------------------------------------------------------------- 1 | ___________ 2 | .: __ _____\ 3 | : /|/__V \|_ '. 4 | __ : | _⬛_ _⬛_ | __ 5 | ( '. .| \ .' ) 6 | \‾‾\◞: . //‾‾/ 7 | \ \_'.________.:/ / 8 | \ \__.' '.__/ / 9 | \ \ʌ/ / 10 | ' ' ' 11 | | : | 12 | | : | 13 | | : | 14 | | .___ _.| 15 | ''_.' ' ' 16 | There's nothing here! 17 | Why not post something? 18 | -------------------------------------------------------------------------------- /postParsing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import re 3 | def parse(string): 4 | postDict={ 5 | "Name":"", 6 | "Subject":"", 7 | "Body":"" 8 | } 9 | #Split by body first 10 | bodyList=string.split("Body:",1) 11 | if len(bodyList)>0: 12 | postDict["Body"] = bodyList[1] 13 | 14 | nameList=re.findall("(?i)Name\:[\W]*(.*)\n",bodyList[0]) 15 | if len(nameList)>0: 16 | postDict["Name"] = nameList[0] 17 | 18 | subList=re.findall("(?i)Subject\:[\W]*(.*)\n",bodyList[0]) 19 | if len(subList)>0: 20 | postDict["Subject"] = subList[0] 21 | return postDict 22 | -------------------------------------------------------------------------------- /themes/default/logo.txt: -------------------------------------------------------------------------------- 1 | ... 2 | .@@@@:. 3 | @ @' 4 | @ @' 5 | . @ @' 6 | .@ '@@@@' 7 | @.@..... 8 | @@@@@@' 9 | @. 10 | @ @' 11 | @.@... 12 | @@@@@ 13 | @. @. 14 | @. @' 15 | @. @' 16 | . @. @' 17 | @ @@@' 18 | ...@ 19 | '@@@. 20 | @. 21 | @ 22 | 23 | -------------------------------------------------------------------------------- /themes/default/posts.json: -------------------------------------------------------------------------------- 1 | { 2 | "global":{ 3 | "attachment character":"📎" 4 | }, 5 | "local":{ 6 | "default": 7 | { 8 | "unselected": 9 | { 10 | "seperator":"║╭───────────┬─────────────────┬────────────────────┬─────────────────────┬", 11 | "seperator repeat":"─", 12 | "header":"║│{:<10.10} │ {:<15.15} │ {:.18} │ {:%Y-%m-%d %H:%M:%S} │{}", 13 | "body":"║│", 14 | "footer":"║╰", 15 | "footer repeat":"─" 16 | }, 17 | "selected": 18 | { 19 | "seperator":"┋╭───────────┬─────────────────┬────────────────────┬─────────────────────┬", 20 | "seperator repeat":"─", 21 | "header":"┋│{:<10.10} │ {:<15.15} │ {:.18} │ {:%Y-%m-%d %H:%M:%S} │{}", 22 | "body":"┋│", 23 | "footer":"┋╰", 24 | "footer repeat":"─" 25 | } 26 | }, 27 | "OP": 28 | { 29 | "unselected": 30 | { 31 | "seperator":"╠═══════════╦═════════════════╦════════════════════╦═════════════════════╦", 32 | "seperator repeat":"═", 33 | "header":"║{:<10.10} ║ {:<15.15} ║ {:.18} ║ {:%Y-%m-%d %H:%M:%S} ║{}", 34 | "body":"║", 35 | "footer":"║", 36 | "footer repeat":"" 37 | }, 38 | "selected": 39 | { 40 | "seperator":"├┅┅┅┅┅┅┅┅┅┅┅┯┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┯┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┳┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┳", 41 | "seperator repeat":"┅", 42 | "header":"┋{:<10.10} ┋ {:<15.15} ┋ {:.18} ┋ {:%Y-%m-%d %H:%M:%S} ┋{}", 43 | "body":"┋", 44 | "footer":"┋", 45 | "footer repeat":"" 46 | } 47 | } 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /asciiArtLoader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | ############################LICENCE################################### 4 | # Copyright (c) 2016 Faissal Bensefia 5 | # This file is part of Yukko. 6 | # 7 | # Yukko is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Yukko is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with Yukko. If not, see . 19 | ####################################################################### 20 | class asciiImg(): 21 | 22 | def __init__(self, asciiFile): 23 | self.height = 0 24 | self.width = 0 25 | self.data = [] 26 | with open(asciiFile, 'r') as file: 27 | for i in file: 28 | self.height += 1 29 | self.width = max(len(i), self.width) 30 | self.data.append(i.rstrip()) # Remove newlines and append 31 | 32 | def __getitem__(self, key): 33 | return self.data[key] 34 | 35 | def __iter__(self): 36 | self.iteratorIndex = 0 37 | return self 38 | 39 | def __next__(self): 40 | if self.iteratorIndex >= len(self): 41 | raise StopIteration 42 | else: 43 | toRet = self[self.iteratorIndex] 44 | self.iteratorIndex += 1 45 | return toRet 46 | 47 | def __len__(self): 48 | return len(self.data) 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Yukko 2 | === 3 | Yukko is an ASCIIpunk NNTPchan client written in Python 3. Place a list 4 | of nodes seperated by new lines in nodeList.txt and the client will cycle 5 | through them at random. nntp.py is the library I have written for this 6 | project, it has been intentionally written in such a way that it does not 7 | depend on anything in the rest of the program, this allows it 8 | to be pulled out and used for other projects. To start it run `./yukko.py` 9 | Dependancies 10 | == 11 | - Python 3 12 | - requests 13 | - pypng 14 | 15 | Screenies 16 | == 17 | ![Board overview](https://i.sli.mg/1z0JdC.png) 18 | ![CAPTCHA](https://i.sli.mg/qEzWgR.png) 19 | ![Post](https://i.sli.mg/fxw7hX.png) 20 | ![Board list](https://i.sli.mg/LUS71H.png) 21 | ![Attachments](https://i.sli.mg/FOzIo1.png) 22 | 23 | Controls 24 | == 25 | 26 | Board overview 27 | = 28 | - Left/Right: Change page 29 | - Up/Down: Scroll 30 | - Enter: Open thread 31 | - Escape: Go to board 32 | - P: New thread 33 | - B: Board list 34 | - R: Refresh 35 | 36 | Thread 37 | = 38 | - Backspace/Left/Escape: Back to board overview. 39 | - Up/Down: Scroll 40 | - Enter: View attachments (does nothing if there are none) 41 | - P: New reply 42 | - R: Refresh 43 | 44 | Attachments 45 | = 46 | - Up/Down: Scroll 47 | - Enter: Download attachment 48 | - Backspace/Left/Escape: Back to thread 49 | 50 | Settings 51 | == 52 | General settings are stored in settings.json 53 | 54 | download directory 55 | = 56 | The directory to download attachments to. 57 | 58 | text editor 59 | = 60 | The command used to open a text editor. 61 | 62 | theme folder 63 | = 64 | The directory holding the current theme. 65 | 66 | max overview lines 67 | = 68 | The maximum amount of lines to show in board overview mode before contracting the post. 69 | 70 | max overview posts 71 | = 72 | The maxmimum amount of posts, starting from the end of the thread (not including the opening post) to show in board view mode. 73 | 74 | default board 75 | = 76 | The default board that Yukko will go to when started. 77 | 78 | http proxy/https proxy 79 | = 80 | Sets a proxy for all traffic, such as "socks5://127.0.0.1:9050". Note that SOCKS proxies will require the socks add-on for the requests module, which can be 81 | installed like so: 82 | ``` 83 | sudo pip install -U "requests[socks]" 84 | ``` 85 | 86 | Themes 87 | == 88 | Themes can be used to style Yukko to look however you like, from changing the ascii art to the borders around the posts. 89 | They contain 5 files: 90 | - attachmentsBg.txt: The background to display on the attachments page. 91 | - boardListBg.txt: The background to display on the board list page. 92 | - emptyBoard.txt: The background to display in the case that a board is empty. 93 | - errorRetrievingPage.txt: The background to display in the case that an error occurs while attempting to obtain the board. 94 | - posts.json: Contains information for the styling of posts 95 | - global: Applies to both opening posts and normal posts 96 | - local: Applies to either opening posts or normal posts 97 | - default: Style data for a normal post 98 | - OP: Style data for an opening post 99 | - unselected: Style data for when a post is not selected 100 | - selected: Style data for when a post is selected 101 | - seperator: The seperating line between each post 102 | - seperator repeat: The character that will be repeated in the case that the terminal is wider than the seperator 103 | - header: The header for each post, supporting python's formatting mini language. Spaces are occupied like so: 104 | 1. Name 105 | 2. Subject 106 | 3. Post ID 107 | 4. Date 108 | 5. Attachment character 109 | - body: The string to repeat vertically along the body of the post 110 | - footer: The string to use for the end of a post 111 | - footer repeat: The string to repeat horizontally along the body of the post in the case that the width of the terminal is larger than that of the footer. 112 | -------------------------------------------------------------------------------- /nntp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | ############################LICENCE################################### 4 | # Copyright (c) 2016 Faissal Bensefia 5 | # This file is part of Yukko. 6 | # 7 | # Yukko is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Yukko is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with Yukko. If not, see . 19 | ####################################################################### 20 | import requests 21 | import json 22 | import datetime 23 | import os 24 | captchaID = "" 25 | nodeList = [] 26 | node="" 27 | proxy={ 28 | "http":"", 29 | "https":"" 30 | } 31 | 32 | header={ 33 | "User-Agent":"Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0" 34 | } 35 | 36 | # Reads nodes from a text file 37 | def readNodes(nodeFile): 38 | global nodeList 39 | with open(nodeFile, 'r') as file: 40 | nodeList=[i.strip() for i in file] 41 | 42 | # Picks a random new node 43 | def cycleNode(): 44 | global node 45 | global nodeList 46 | node = nodeList[int.from_bytes(os.urandom(4), "big") % len(nodeList)] 47 | 48 | readNodes("nodeList.txt") 49 | cycleNode() 50 | 51 | class file(): 52 | def __init__(self, jason): 53 | global node 54 | self.url = node + "img/" + jason["Path"] 55 | self.fileName = jason["Name"] 56 | 57 | def download(self, downloadDir=""): 58 | global proxy 59 | r = requests.get(self.url, proxies=proxy) 60 | with open(downloadDir + self.fileName, "wb") as f: 61 | for i in r: 62 | f.write(i) 63 | return r.status_code 64 | 65 | 66 | class post(): 67 | def __init__(self, jason, isOP=False): 68 | self.isOP = isOP 69 | self.name = jason["PostName"] 70 | self.subject = jason["PostSubject"] 71 | self.ID = jason["Message_id"] 72 | self.hash = jason["HashLong"] 73 | self.timestamp = datetime.datetime.fromtimestamp(jason["Posted"]) 74 | self.text = jason["PostMessage"] 75 | self.files = [] 76 | if jason["Files"] != None: 77 | self.files=[file(i) for i in jason["Files"]] 78 | 79 | 80 | class thread(): 81 | def __init__(self, jason, parentBoard): 82 | self.parentBoard = parentBoard 83 | self.posts = [] 84 | self.posts.append(post(jason[0], True)) 85 | self.posts.extend([post(i) for i in jason[1:]]) 86 | 87 | def __len__(self): 88 | return len(self.posts) 89 | 90 | def __iter__(self): 91 | self.iteratorIndex = 0 92 | return self 93 | 94 | def __next__(self): 95 | if self.iteratorIndex >= len(self): 96 | raise StopIteration 97 | else: 98 | toRet = self[self.iteratorIndex] 99 | self.iteratorIndex += 1 100 | return toRet 101 | 102 | def __getitem__(self, key): 103 | return self.posts[key] 104 | 105 | def refresh(self): 106 | global proxy 107 | global headers 108 | r = requests.get(node + "t/" + str(self[0].hash) + "/json", proxies=proxy,headers=header) 109 | self.status = r.status_code 110 | self.posts = [] 111 | if self.status >= 200 and self.status < 300: 112 | jason = r.json() 113 | if jason: # Only do this if None wasn't returned 114 | self.posts = [] 115 | self.posts.append(post(jason[0], True)) 116 | self.posts.extend([post(i) for i in jason[1:]]) 117 | 118 | def overview(self, postCount): 119 | # Returns the first and last postCount posts 120 | if len(self.posts) > 1: 121 | postOverview = self.posts[max(1, len(self.posts) - postCount):] 122 | else: 123 | postOverview = [] 124 | postOverview.insert(0, self.posts[0]) 125 | return postOverview 126 | 127 | def post(self, name, sub, msg, captcha, *files): 128 | global proxy 129 | global node 130 | global captchaID 131 | global header 132 | filesToUpload = [("", "")] 133 | # Ability to use same key for multiple files 134 | filesToUpload.extend([("attachment_uploaded", open(i, "rb")) for i in files]) 135 | 136 | postArgs = { 137 | "reference": self.posts[0].ID, 138 | "name": name, 139 | "subject": sub, 140 | "message": msg, 141 | "captcha": captcha, 142 | "captcha_id": captchaID, 143 | "pow": "" 144 | } 145 | r = requests.post(node + "post/" + self.parentBoard.boardname, 146 | files=filesToUpload, data=postArgs, headers=header, proxies=proxy) 147 | return r.status_code 148 | 149 | 150 | class board(): 151 | def __init__(self, boardname, page): 152 | global node 153 | global proxy 154 | global header 155 | cycleNode() 156 | r = requests.get(node +"b/"+ boardname + "/" + str(page) + "/json", proxies=proxy,headers=header) 157 | self.status = r.status_code 158 | self.page = page 159 | self.boardname = boardname 160 | self.threadOverviews = [] 161 | if self.status >= 200 and self.status < 300: 162 | jason = r.json() 163 | if jason: # Only do this if None wasn't returned 164 | self.threadOverviews.extend([thread(i, self) for i in jason["posts"]]) 165 | 166 | def refresh(self): 167 | global node 168 | global proxy 169 | global header 170 | cycleNode() 171 | r = requests.get(node +"b/"+ self.boardname + "/" + str(self.page) + "/json", proxies=proxy,headers=header) 172 | self.status = r.status_code 173 | self.threadOverviews = [] 174 | if self.status >= 200 and self.status < 300: 175 | jason = r.json() 176 | if jason: # Only do this if None wasn't returned 177 | self.threadOverviews.extend([thread(i, self) for i in jason["posts"]]) 178 | 179 | def __iter__(self): 180 | self.iteratorIndex = 0 181 | return self 182 | 183 | def __next__(self): 184 | if self.iteratorIndex >= len(self): 185 | raise StopIteration 186 | else: 187 | toRet = self[self.iteratorIndex] 188 | self.iteratorIndex += 1 189 | return toRet 190 | 191 | def __len__(self): 192 | return len(self.threadOverviews) 193 | 194 | def __getitem__(self, key): 195 | return self.threadOverviews[key] 196 | 197 | def post(self, name, sub, msg, captcha, *files): 198 | global proxy 199 | global node 200 | global captchaID 201 | global header 202 | filesToUpload = [("", "")] 203 | # Ability to use same key for multiple files 204 | for i in files: 205 | filesToUpload.append(("attachment_uploaded", open(i, "rb"))) 206 | 207 | postArgs = { 208 | "reference": "", 209 | "name": name, 210 | "subject": sub, 211 | "message": msg, 212 | "captcha": captcha, 213 | "captcha_id": captchaID, 214 | "pow": "" 215 | } 216 | r = requests.post(node + "post/" + self.boardname, 217 | files=filesToUpload, data=postArgs, headers=header, proxies=proxy) 218 | return r.status_code 219 | 220 | 221 | def cleanupCaptcha(): 222 | global captchaID 223 | try: 224 | # Get rid of the last CAPTCHA so we don't fill /tmp/ with crap 225 | os.remove("/tmp/" + captchaID + ".png") 226 | except: 227 | pass 228 | 229 | 230 | def getCaptcha(): 231 | global captchaID 232 | global proxy 233 | cleanupCaptcha() 234 | r = requests.get(node + "captcha/img", proxies=proxy,headers=header) 235 | captchaID = r.url[len(node + "captcha/"):-4] 236 | # Download the file 237 | with open("/tmp/" + captchaID + ".png", "wb") as f: 238 | for i in r: 239 | f.write(i) 240 | return "/tmp/" + captchaID + ".png" 241 | 242 | 243 | class boardList(): 244 | def __init__(self): 245 | global proxy 246 | global header 247 | cycleNode() 248 | r = requests.get(node + "boards.json", proxies=proxy,headers=header) 249 | self.boards = r.json() 250 | 251 | def __getitem__(self, key): 252 | return self.boards[key] 253 | 254 | def __iter__(self): 255 | self.iteratorIndex = 0 256 | return self 257 | 258 | def __next__(self): 259 | if self.iteratorIndex >= len(self): 260 | raise StopIteration 261 | else: 262 | toRet = self[self.iteratorIndex] 263 | self.iteratorIndex += 1 264 | return toRet 265 | 266 | def __len__(self): 267 | return len(self.boards) 268 | -------------------------------------------------------------------------------- /yukko.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | ############################LICENCE################################### 4 | # Copyright (c) 2016-2017 Faissal Bensefia 5 | # This file is part of Yukko. 6 | # 7 | # Yukko is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Yukko is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with Yukko. If not, see . 19 | ####################################################################### 20 | import textwrap 21 | import asciiArtLoader 22 | import curses 23 | import nntp 24 | import os 25 | import png 26 | import postParsing 27 | import json 28 | import signal 29 | 30 | 31 | def sigint_handler(signal, frame): 32 | curses.endwin() 33 | exit() 34 | 35 | signal.signal(signal.SIGINT, sigint_handler) 36 | 37 | # Load settings 38 | with open("settings.json") as settingsFile: 39 | settings = json.load(settingsFile) 40 | 41 | nntp.proxy = { 42 | "http": settings["http proxy"], 43 | "https": settings["http proxy"] 44 | } 45 | 46 | nntp.header = { 47 | "User-Agent":settings["user agent"] 48 | } 49 | os.environ['ESCDELAY'] = '0' 50 | scr = curses.initscr() 51 | curses.start_color() 52 | curses.noecho() 53 | curses.curs_set(0) 54 | curses.use_default_colors() 55 | scr.keypad(True) 56 | windowH, windowW = scr.getmaxyx() 57 | 58 | with open(settings["theme folder"] + "posts.json") as postThemeFile: 59 | postStyle = json.load(postThemeFile) 60 | 61 | errorRetrievingPage = asciiArtLoader.asciiImg( 62 | settings["theme folder"] + "errorRetrievingPage.txt") 63 | boardListBg = asciiArtLoader.asciiImg( 64 | settings["theme folder"] + "boardListBg.txt") 65 | emptyBoard = asciiArtLoader.asciiImg( 66 | settings["theme folder"] + "emptyBoard.txt") 67 | logo = asciiArtLoader.asciiImg(settings["theme folder"] + "logo.txt") 68 | attachments = asciiArtLoader.asciiImg( 69 | settings["theme folder"] + "attachmentsBg.txt") 70 | 71 | 72 | def drawCaptcha(y, x): # An image drawer geared towards CAPTCHAs 73 | global scr 74 | global windowH 75 | global windowW 76 | image = png.Reader("/tmp/" + nntp.captchaID + ".png").read() 77 | width = image[0] 78 | height = image[1] 79 | if windowW - 1 < x + (width // 4) or windowH - 1 < y + (height // 8): 80 | scr.clear() 81 | scr.addstr(y, x, "Terminal too small for CAPTCHA, please resize") 82 | return 0 83 | 84 | yy = 0 85 | for i in image[2]: 86 | xx = 0 87 | for ii in i: 88 | # The foreground will always be color 1 in the pallette 89 | # only include foreground to make it easier for the user 90 | if ii == 1: 91 | scr.addstr(y + (yy // 8), x + (xx // 4), "░", curses.A_REVERSE) 92 | 93 | xx += 1 94 | yy += 1 95 | return 1 96 | 97 | 98 | def getPostHeight(post, ypad): 99 | global windowW 100 | splitText = [] 101 | for i in post.text.split("\n"): 102 | splitText.extend([ii for ii in textwrap.wrap(i, windowW - (1 if post.isOP else 3))]) 103 | return ypad + len(splitText) 104 | 105 | 106 | def threadLength(thread, ypad, offset): 107 | # Offset is added to the total value (for stuff like op) 108 | # ypad is added to each post 109 | return offset+sum([getPostHeight(i, ypad) for i in thread.posts]) 110 | 111 | 112 | def threadOverviewLength(thread, ypad, offset, maxlines, maxposts): 113 | # Offset is added to the total value (for stuff like op) 114 | # ypad is added to each post 115 | # Maxlines+1 because a message is appended to those that exceed it 116 | return offset+sum([min(getPostHeight(i, ypad), ypad + maxlines + 1) for i in thread.overview(3)]) 117 | 118 | # Like addstr but errors silently 119 | def drawText(y, x, string, attributes=0): 120 | global scr 121 | try: 122 | scr.addstr(y, x, string, attributes) 123 | except curses.error: 124 | pass 125 | 126 | 127 | def textBox(y, x, default, length, intOnly=False, maxlen=0): 128 | global scr 129 | string = default 130 | last_ch = 0 131 | cursor_pos = 0 132 | scope_pos = 0 133 | curses.curs_set(True) 134 | while not (last_ch == 27 or last_ch == ord("\n")): 135 | scr.addstr(y, x, string[scope_pos:scope_pos + length].ljust(length)) 136 | scr.move(y, x + cursor_pos - scope_pos) 137 | scr.refresh() 138 | last_ch = scr.getch() 139 | if last_ch == curses.KEY_LEFT: 140 | if cursor_pos > 0: 141 | cursor_pos -= 1 142 | if cursor_pos < scope_pos: 143 | scope_pos -= 1 144 | elif last_ch == curses.KEY_RIGHT: 145 | if cursor_pos < len(string): 146 | cursor_pos += 1 147 | if cursor_pos > length + scope_pos: 148 | scope_pos += 1 149 | elif last_ch == curses.KEY_DC: 150 | if cursor_pos < len(string): 151 | string = string[:cursor_pos] + string[cursor_pos + 1:] 152 | elif last_ch == curses.KEY_BACKSPACE: 153 | if cursor_pos > 0: 154 | cursor_pos -= 1 155 | string = string[:cursor_pos] + string[cursor_pos + 1:] 156 | if cursor_pos < scope_pos: 157 | scope_pos -= 1 158 | elif (not (last_ch == 27 or last_ch == ord("\n") or intOnly) 159 | ) or ((not (last_ch == 27 or last_ch == ord("\n"))) 160 | and intOnly and last_ch <= ord("9") 161 | and last_ch >= ord("0") 162 | ) and (maxlen == 0 or len(string) < maxlen): 163 | string = string[:cursor_pos] + chr(last_ch) + string[cursor_pos:] 164 | cursor_pos += 1 165 | if cursor_pos > length + scope_pos: 166 | scope_pos += 1 167 | 168 | curses.curs_set(False) 169 | # Escape = Throw away, enter = save 170 | if last_ch == 27: 171 | return default 172 | else: 173 | return string 174 | 175 | 176 | def boardView(board): 177 | global errorRetrievingPage 178 | global emptyBoard 179 | global postStyle 180 | global settings 181 | global ypad 182 | global scr 183 | global windowH 184 | global windowW 185 | selectionBarY = 0 186 | page = 0 187 | currBoard = nntp.board(board, page) 188 | y = 0 189 | scr.clear() 190 | while True: 191 | windowH, windowW = scr.getmaxyx() 192 | # If the board was loaded successfully 193 | if currBoard.status == 200: 194 | if len(currBoard) > 0: 195 | postY = 0 # Post y in the whole page 196 | selected = False 197 | 198 | prevThread = None 199 | prevThreadHeight = 0 200 | for iThread in currBoard: 201 | # If any part of the thread hits the middle of the screen then select that 202 | selected = not (selectionBarY - 203 | y < postY - 204 | y or (postY - 205 | y) + 206 | threadOverviewLength(iThread, 207 | 3, 208 | 0, 209 | settings["max overview lines"], 210 | settings["max overview posts"]) - 211 | 1 < selectionBarY - 212 | y) 213 | 214 | if selected: 215 | selectedThread = iThread 216 | selectedThreadHeight = threadOverviewLength( 217 | iThread, 3, 0, 218 | settings["max overview lines"], 219 | settings["max overview posts"]) 220 | if prevThread: 221 | # Height of the thread before the selected thread 222 | prevThreadHeight = threadOverviewLength( 223 | prevThread, 3, 0, 224 | settings["max overview lines"], 225 | settings["max overview posts"]) 226 | 227 | for iPost in iThread.overview(settings["max overview posts"]): 228 | drawText(postY - y, 0, postStyle["local"] 229 | ["OP" if iPost.isOP else "default"] 230 | ["selected" if selected else "unselected"] 231 | ["seperator"] + 232 | (postStyle["local"]["OP" if iPost.isOP else "default"] 233 | ["selected" if selected else "unselected"] 234 | ["seperator repeat"] * (windowW - 235 | len(postStyle["local"] 236 | ["OP" if iPost.isOP else "default"] 237 | ["selected" if selected else "unselected"]["seperator"] 238 | ) 239 | ) 240 | ) 241 | ) 242 | 243 | postY += 1 244 | 245 | drawText(postY - y, 0, 246 | postStyle["local"] 247 | ["OP" if iPost.isOP else "default"] 248 | ["selected" if selected else "unselected"] 249 | ["header"].format( 250 | iPost.name, 251 | iPost.subject, 252 | iPost.hash, 253 | iPost.timestamp, 254 | postStyle["global"] 255 | ["attachment character"] if len(iPost.files) else "" 256 | ) 257 | ) 258 | postY += 1 259 | splitText = [] 260 | # Wrap the text 261 | for iLine in iPost.text.split("\n"): 262 | splitText.extend([iLineWrapped for iLineWrapped in textwrap.wrap(iLine, windowW - (1 if iPost.isOP else 3))]) 263 | 264 | # Draw the text in the post up to settings["max overview lines"] 265 | for yText, iText in enumerate(splitText): 266 | if yText < settings["max overview lines"]: 267 | drawText(postY - y, 268 | 0, 269 | postStyle["local"] 270 | ["OP" if iPost.isOP else "default"] 271 | ["selected" if selected else "unselected"] 272 | ["body"] + 273 | iText) 274 | postY += 1 275 | else: 276 | drawText(postY - y, 277 | 0, 278 | postStyle["local"] 279 | ["OP" if iPost.isOP else "default"] 280 | ["selected" if selected else "unselected"] 281 | ["body"] + 282 | "[POST CONTRACTED]") 283 | postY += 1 284 | break 285 | drawText( 286 | postY - y, 287 | 0, 288 | postStyle["local"] 289 | ["OP" if iPost.isOP else "default"] 290 | ["selected" if selected else "unselected"] 291 | ["footer"] + 292 | postStyle["local"] 293 | ["OP" if iPost.isOP else "default"] 294 | ["selected" if selected else "unselected"] 295 | ["footer repeat"] * 296 | (windowW - len( 297 | postStyle["local"] 298 | ["OP" if iPost.isOP else "default"] 299 | ["selected" if selected else "unselected"] 300 | ["footer"] 301 | ) 302 | ) 303 | ) 304 | postY += 1 305 | prevThread = iThread 306 | else: 307 | for yArt, iArt in enumerate(emptyBoard): 308 | drawText((windowH - errorRetrievingPage.height) // 2 + yArt, 309 | (windowW - errorRetrievingPage.width) // 2, iArt) 310 | 311 | else: 312 | for yArt, iArt in enumerate(errorRetrievingPage): 313 | drawText((windowH - errorRetrievingPage.height) // 2 + yArt, 314 | (windowW - errorRetrievingPage.width) // 2, 315 | iArt.format(currBoard.status) 316 | ) 317 | currKey = 0 318 | scr.refresh() 319 | while not (currKey == curses.KEY_UP 320 | or currKey == curses.KEY_DOWN 321 | or currKey == curses.KEY_LEFT 322 | or currKey == curses.KEY_RIGHT 323 | or currKey == ord('b') 324 | or currKey == ord('p') 325 | or currKey == ord('r') 326 | or currKey == ord('\n') 327 | or currKey == 27): # Add keys that cause updates as nessecary 328 | currKey = scr.getch() 329 | if currKey == curses.KEY_UP: 330 | if selectionBarY > 0: 331 | selectionBarY -= prevThreadHeight 332 | if selectionBarY < y: 333 | # bind to prevent it going below 0 334 | y = max(0, y - prevThreadHeight) 335 | elif currKey == curses.KEY_DOWN: 336 | if selectionBarY + selectedThreadHeight < postY: 337 | selectionBarY += selectedThreadHeight 338 | if selectionBarY + selectedThreadHeight > y + windowH - 1: 339 | # bind to prevent it going higher than 0 340 | y = min(postY - windowH - 1, y + selectedThreadHeight) 341 | elif currKey == curses.KEY_LEFT: 342 | if page > 0: 343 | selectionBarY = 0 344 | y = 0 345 | page -= 1 346 | scr.addstr( 347 | 0, 0, 348 | ("Loading page " + 349 | str(page) + 350 | " of " + 351 | board).ljust(windowW), 352 | curses.A_REVERSE) 353 | scr.refresh() 354 | currBoard = nntp.board(board, page) 355 | elif currKey == curses.KEY_RIGHT: 356 | selectionBarY = 0 357 | page += 1 358 | y = 0 359 | scr.addstr( 360 | 0, 0, 361 | ("Loading page " + 362 | str(page) + 363 | " of " + 364 | board).ljust(windowW), 365 | curses.A_REVERSE) 366 | scr.refresh() 367 | currBoard = nntp.board(board, page) 368 | elif currKey == 27: 369 | scr.attron(curses.A_REVERSE) 370 | scr.addstr(0, 0, "Board: ") 371 | next_board = textBox( 372 | 0, len("Board: "), 373 | board, windowW - 374 | len("Board: "), 375 | False) 376 | scr.attroff(curses.A_REVERSE) 377 | if next_board != board: 378 | board = next_board 379 | page = 0 380 | y = 0 381 | selectionBarY = 0 382 | currBoard = nntp.board(board, page) 383 | elif currKey == ord('b'): 384 | selectionBarY = 0 385 | next_board = boardListView(board) 386 | if next_board != board: 387 | board = next_board 388 | page = 0 389 | y = 0 390 | currBoard = nntp.board(board, page) 391 | elif currKey == ord('p'): 392 | scr.addstr(0, 0, ("Loading CAPTCHA").ljust(windowW), curses.A_REVERSE) 393 | scr.refresh() 394 | post(currBoard, settings["text editor"]) 395 | 396 | y = 0 397 | selectionBarY = 0 398 | elif currKey == ord('r'): 399 | scr.addstr(0, 0, ("Refreshing").ljust(windowW), curses.A_REVERSE) 400 | scr.refresh() 401 | currBoard.refresh() 402 | # We have to jump back selectionBarY might end up in the middle of a post 403 | y = 0 404 | selectionBarY = 0 405 | 406 | elif currKey == ord('\n'): 407 | threadView(selectedThread) 408 | scr.clear() 409 | 410 | 411 | def threadView(thread): 412 | global postStyle 413 | global ypad 414 | global settings 415 | global scr 416 | global windowH 417 | global windowW 418 | scr.clear() 419 | selectionBarY = 0 420 | y = 0 421 | selectedPostHeight = 0 422 | while True: 423 | windowH, windowW = scr.getmaxyx() 424 | postY = 0 # Post y in the whole page 425 | prevPost = None 426 | selected = False 427 | selectedPost = None 428 | for iPost in thread: 429 | # 1D collision detection between postY and selectionBarY 430 | selected = not (selectionBarY - 431 | y < postY - 432 | y or (postY - 433 | y) + 434 | getPostHeight( 435 | iPost, 436 | 3) - 437 | 1 < selectionBarY - 438 | y) 439 | if selected: 440 | selectedPost = iPost 441 | selectedPostHeight = getPostHeight(iPost, 3) 442 | if prevPost: 443 | # Height of the post before the selected post 444 | prevPostHeight = getPostHeight(prevPost, 3) 445 | drawText(postY - y, 0, 446 | postStyle["local"] 447 | ["OP" if iPost.isOP else "default"] 448 | ["selected" if selected else "unselected"] 449 | ["seperator"] + 450 | ( 451 | postStyle["local"] 452 | ["OP" if iPost.isOP else "default"] 453 | ["selected" if selected else "unselected"] 454 | ["seperator repeat"] * 455 | ( 456 | windowW - len( 457 | postStyle["local"] 458 | ["OP" if iPost.isOP else "default"] 459 | ["selected" if selected else "unselected"] 460 | ["seperator"] 461 | ) 462 | ) 463 | ) 464 | ) 465 | postY += 1 466 | 467 | drawText(postY - y, 0, 468 | postStyle["local"] 469 | ["OP" if iPost.isOP else "default"] 470 | ["selected" if selected else "unselected"] 471 | ["header"].format(iPost.name, 472 | iPost.subject, 473 | iPost.hash, 474 | iPost.timestamp, 475 | postStyle["global"] 476 | ["attachment character"] 477 | if len(iPost.files) else "" 478 | ) 479 | ) 480 | postY += 1 481 | splitText = [] 482 | for iLine in iPost.text.split("\n"): 483 | for iLineWrapped in textwrap.wrap( 484 | iLine, windowW - (1 if iPost.isOP else 3)): 485 | splitText.append(iLineWrapped) 486 | 487 | for iText in splitText: 488 | drawText( 489 | postY - y, 490 | 0, 491 | postStyle["local"][ 492 | "OP" if iPost.isOP else "default"][ 493 | "selected" if selected else "unselected"]["body"] + 494 | iText) 495 | postY += 1 496 | drawText( 497 | postY - 498 | y, 499 | 0, 500 | postStyle["local"] 501 | ["OP" if iPost.isOP else "default"] 502 | ["selected" if selected else "unselected"] 503 | ["footer"] + 504 | postStyle["local"] 505 | ["OP" if iPost.isOP else "default"] 506 | ["selected" if selected else "unselected"]["footer repeat"] * 507 | (windowW - 508 | len( 509 | postStyle["local"] 510 | ["OP" if iPost.isOP else "default"] 511 | ["selected" if selected else "unselected"] 512 | ["footer"] 513 | ) 514 | ) 515 | ) 516 | postY += 1 517 | prevPost = iPost 518 | scr.refresh() 519 | currKey = 0 520 | while not (currKey == curses.KEY_UP 521 | or currKey == curses.KEY_DOWN 522 | or currKey == curses.KEY_LEFT 523 | or currKey == ord('p') 524 | or currKey == ord('r') 525 | or currKey == curses.KEY_BACKSPACE 526 | or currKey == ord('\n') 527 | or currKey == 27): # Add keys that cause updates as nessecary 528 | currKey = scr.getch() 529 | if currKey == curses.KEY_UP: 530 | if selectionBarY > 0: 531 | selectionBarY -= prevPostHeight 532 | if selectionBarY < y: 533 | # Bind to prevent it going below 0 534 | y = max(y - prevPostHeight, 0) 535 | elif currKey == curses.KEY_DOWN: 536 | if selectionBarY + selectedPostHeight < postY: 537 | selectionBarY += selectedPostHeight 538 | if selectionBarY + selectedPostHeight > y + windowH - 1: 539 | # Bind to the height of the page 540 | y = min(postY - windowH - 1, y + selectedPostHeight) 541 | elif currKey == ord('p'): 542 | scr.addstr(0, 0, ("Loading CAPTCHA").ljust(windowW), curses.A_REVERSE) 543 | scr.refresh() 544 | post(thread, settings["text editor"]) 545 | elif currKey == ord('r'): 546 | scr.addstr(0, 0, ("Refreshing").ljust(windowW), curses.A_REVERSE) 547 | scr.refresh() 548 | thread.refresh() 549 | elif currKey == ord('\n'): 550 | # Only display if there are files 551 | if len(selectedPost.files) > 0: 552 | viewAttachments(selectedPost) 553 | elif (currKey == 27 554 | or currKey == curses.KEY_BACKSPACE 555 | or currKey == curses.KEY_LEFT): 556 | return 557 | scr.clear() 558 | 559 | 560 | def boardListView(default): 561 | global boardListBg 562 | global windowH 563 | global windowW 564 | global scr 565 | scr.clear() 566 | scopeY = 0 567 | selector = 0 568 | currKey = 0 569 | boardList = nntp.boardList() 570 | while True: 571 | windowH, windowW = scr.getmaxyx() 572 | # Background 573 | for yArt, iArt in enumerate(boardListBg): 574 | drawText(windowH - 575 | boardListBg.height + 576 | yArt, 577 | windowW - 578 | boardListBg.width, 579 | iArt) 580 | 581 | for yBoard, iBoard in enumerate(boardList[scopeY:], scopeY): 582 | drawText(yBoard - 583 | scopeY, 584 | 1, 585 | iBoard, 586 | curses.A_REVERSE if selector == yBoard else 0) 587 | scr.refresh() 588 | currKey = 0 589 | while not (currKey == curses.KEY_UP 590 | or currKey == curses.KEY_DOWN 591 | or currKey == 27 592 | or currKey == ord("\n")): # Add keys that cause updates as nessecary 593 | currKey = scr.getch() 594 | 595 | if currKey == curses.KEY_UP: 596 | if selector > 0: 597 | selector -= 1 598 | if selector < scopeY: 599 | scopeY = selector 600 | elif currKey == curses.KEY_DOWN: 601 | if selector < len(boardList) - 1: 602 | selector += 1 603 | if scopeY + windowH <= selector and scopeY <= len(boardList) - windowH: 604 | scopeY += 1 605 | elif currKey == 27: 606 | return default 607 | elif currKey == ord("\n"): 608 | return boardList[selector] 609 | scr.clear() 610 | 611 | # Object can be either a thread or a board 612 | 613 | 614 | def post(object, editorCommand): 615 | global scr 616 | global windowH 617 | global windowW 618 | nntp.getCaptcha() 619 | textFileID = nntp.captchaID 620 | # Create the post form template 621 | with open("/tmp/post" + textFileID + ".txt", "w") as file: 622 | file.write( 623 | "Name: Anonymous\n" + 624 | "Subject: \n" + 625 | "Body: \n") 626 | # Open the file in text editor of choice 627 | curses.endwin() 628 | os.system(editorCommand + " /tmp/post" + textFileID + ".txt") 629 | with open("/tmp/post" + textFileID + ".txt", "r") as file: 630 | postDict = postParsing.parse(file.read()) 631 | 632 | captchaString = "" 633 | response = 0 634 | while response != 201: 635 | while captchaString == "": 636 | scr.clear() 637 | windowH, windowW = scr.getmaxyx() 638 | while not drawCaptcha(3, 0): 639 | # Wait for the user to resize the terminal to an acceptable size 640 | scr.refresh() 641 | scr.clear() 642 | windowH, windowW = scr.getmaxyx() 643 | scr.refresh() 644 | scr.addstr(0, 0, "CAPTCHA", curses.A_BOLD) 645 | scr.addstr(1, 0, "Too hard? Hit escape to generate a new CAPTCHA") 646 | scr.addstr(2, 0, ">") 647 | captchaString = textBox(2, 1, "", windowW, True, 6) 648 | if captchaString == "": # Regen new captcha if we have to 649 | nntp.cleanupCaptcha() 650 | nntp.getCaptcha() 651 | scr.refresh() 652 | response = object.post(postDict['Name'], postDict['Subject'], 653 | postDict['Body'], 654 | captchaString) 655 | 656 | if response == 200: 657 | scr.addstr(0, 658 | 0, 659 | ("CAPTCHA failed, please try again.").ljust(windowW), 660 | curses.A_REVERSE) 661 | scr.refresh() 662 | captchaString = "" 663 | nntp.cleanupCaptcha() 664 | nntp.getCaptcha() 665 | scr.getch() 666 | elif response == 504: 667 | scr.addstr(0, 668 | 0, 669 | ("Gateway timeout, press any key to try again").ljust( 670 | windowW), 671 | curses.A_REVERSE) 672 | scr.refresh() 673 | scr.getch() 674 | 675 | try: 676 | os.remove("/tmp/post" + textFileID + ".txt") 677 | except: 678 | pass 679 | scr.addstr(0, 0, ("Post successful!").ljust(windowW), curses.A_REVERSE) 680 | scr.refresh() 681 | object.refresh() 682 | 683 | 684 | def viewAttachments(post): 685 | global scr 686 | global windowH 687 | global windowW 688 | global attachments 689 | scopeY = 0 690 | selector = 0 691 | selectedFile = None 692 | while True: 693 | scr.clear() 694 | windowH, windowW = scr.getmaxyx() 695 | # Background 696 | for i, j in enumerate(attachments): 697 | drawText(windowH - attachments.height + i, windowW - attachments.width, j) 698 | 699 | for yFile, iFile in enumerate(post.files[scopeY:], scopeY): 700 | if selector == yFile: 701 | selectedFile = iFile 702 | scr.addstr(yFile - scopeY, 1, iFile.fileName, curses.A_REVERSE) 703 | elif yFile - scopeY < windowH: 704 | scr.addstr(yFile - scopeY, 1, post.iFile.fileName) 705 | 706 | scr.refresh() 707 | currKey = 0 708 | while (not (currKey == curses.KEY_UP 709 | or currKey == curses.KEY_DOWN 710 | or currKey == 27 711 | or currKey == curses.KEY_BACKSPACE 712 | or currKey == curses.KEY_LEFT 713 | or currKey == ord("\n") 714 | ) 715 | ): 716 | currKey = scr.getch() 717 | if currKey == curses.KEY_UP: 718 | if selector > 0: 719 | selector -= 1 720 | if selector < scopeY: 721 | scopeY -= 1 722 | if currKey == curses.KEY_DOWN: 723 | if selector < len(post.files) - 1: 724 | selector += 1 725 | if scopeY + windowH <= selector and scopeY <= len(post.files) - windowH: 726 | scopeY += 1 727 | elif currKey == ord("\n"): 728 | scr.addstr(0, 0, ("Downloading file").ljust(windowW), curses.A_REVERSE) 729 | scr.refresh() 730 | r = selectedFile.download(settings["download directory"]) 731 | if r == 200: 732 | scr.addstr(0, 733 | 0, 734 | ("Saved to " + 735 | settings["download directory"] + 736 | selectedFile.fileName).ljust(windowW), 737 | curses.A_REVERSE) 738 | scr.refresh() 739 | scr.getch() 740 | elif (currKey == 27 741 | or currKey == curses.KEY_BACKSPACE 742 | or currKey == curses.KEY_LEFT): 743 | return 744 | 745 | boardView(settings["default board"]) 746 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | ========================== 3 | 4 | Version 3, 29 June 2007 5 | 6 | Copyright © 2007 Free Software Foundation, Inc. <> 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this license 9 | document, but changing it is not allowed. 10 | 11 | ## Preamble 12 | 13 | The GNU General Public License is a free, copyleft license for software and other 14 | kinds of works. 15 | 16 | The licenses for most software and other practical works are designed to take away 17 | your freedom to share and change the works. By contrast, the GNU General Public 18 | License is intended to guarantee your freedom to share and change all versions of a 19 | program--to make sure it remains free software for all its users. We, the Free 20 | Software Foundation, use the GNU General Public License for most of our software; it 21 | applies also to any other work released this way by its authors. You can apply it to 22 | your programs, too. 23 | 24 | When we speak of free software, we are referring to freedom, not price. Our General 25 | Public Licenses are designed to make sure that you have the freedom to distribute 26 | copies of free software (and charge for them if you wish), that you receive source 27 | code or can get it if you want it, that you can change the software or use pieces of 28 | it in new free programs, and that you know you can do these things. 29 | 30 | To protect your rights, we need to prevent others from denying you these rights or 31 | asking you to surrender the rights. Therefore, you have certain responsibilities if 32 | you distribute copies of the software, or if you modify it: responsibilities to 33 | respect the freedom of others. 34 | 35 | For example, if you distribute copies of such a program, whether gratis or for a fee, 36 | you must pass on to the recipients the same freedoms that you received. You must make 37 | sure that they, too, receive or can get the source code. And you must show them these 38 | terms so they know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: (1) assert 41 | copyright on the software, and (2) offer you this License giving you legal permission 42 | to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains that there is 45 | no warranty for this free software. For both users' and authors' sake, the GPL 46 | requires that modified versions be marked as changed, so that their problems will not 47 | be attributed erroneously to authors of previous versions. 48 | 49 | Some devices are designed to deny users access to install or run modified versions of 50 | the software inside them, although the manufacturer can do so. This is fundamentally 51 | incompatible with the aim of protecting users' freedom to change the software. The 52 | systematic pattern of such abuse occurs in the area of products for individuals to 53 | use, which is precisely where it is most unacceptable. Therefore, we have designed 54 | this version of the GPL to prohibit the practice for those products. If such problems 55 | arise substantially in other domains, we stand ready to extend this provision to 56 | those domains in future versions of the GPL, as needed to protect the freedom of 57 | users. 58 | 59 | Finally, every program is threatened constantly by software patents. States should 60 | not allow patents to restrict development and use of software on general-purpose 61 | computers, but in those that do, we wish to avoid the special danger that patents 62 | applied to a free program could make it effectively proprietary. To prevent this, the 63 | GPL assures that patents cannot be used to render the program non-free. 64 | 65 | The precise terms and conditions for copying, distribution and modification follow. 66 | 67 | ## TERMS AND CONDITIONS 68 | 69 | ### 0. Definitions. 70 | 71 | “This License” refers to version 3 of the GNU General Public License. 72 | 73 | “Copyright” also means copyright-like laws that apply to other kinds of 74 | works, such as semiconductor masks. 75 | 76 | “The Program” refers to any copyrightable work licensed under this 77 | License. Each licensee is addressed as “you”. “Licensees” and 78 | “recipients” may be individuals or organizations. 79 | 80 | To “modify” a work means to copy from or adapt all or part of the work in 81 | a fashion requiring copyright permission, other than the making of an exact copy. The 82 | resulting work is called a “modified version” of the earlier work or a 83 | work “based on” the earlier work. 84 | 85 | A “covered work” means either the unmodified Program or a work based on 86 | the Program. 87 | 88 | To “propagate” a work means to do anything with it that, without 89 | permission, would make you directly or secondarily liable for infringement under 90 | applicable copyright law, except executing it on a computer or modifying a private 91 | copy. Propagation includes copying, distribution (with or without modification), 92 | making available to the public, and in some countries other activities as well. 93 | 94 | To “convey” a work means any kind of propagation that enables other 95 | parties to make or receive copies. Mere interaction with a user through a computer 96 | network, with no transfer of a copy, is not conveying. 97 | 98 | An interactive user interface displays “Appropriate Legal Notices” to the 99 | extent that it includes a convenient and prominently visible feature that (1) 100 | displays an appropriate copyright notice, and (2) tells the user that there is no 101 | warranty for the work (except to the extent that warranties are provided), that 102 | licensees may convey the work under this License, and how to view a copy of this 103 | License. If the interface presents a list of user commands or options, such as a 104 | menu, a prominent item in the list meets this criterion. 105 | 106 | ### 1. Source Code. 107 | 108 | The “source code” for a work means the preferred form of the work for 109 | making modifications to it. “Object code” means any non-source form of a 110 | work. 111 | 112 | A “Standard Interface” means an interface that either is an official 113 | standard defined by a recognized standards body, or, in the case of interfaces 114 | specified for a particular programming language, one that is widely used among 115 | developers working in that language. 116 | 117 | The “System Libraries” of an executable work include anything, other than 118 | the work as a whole, that (a) is included in the normal form of packaging a Major 119 | Component, but which is not part of that Major Component, and (b) serves only to 120 | enable use of the work with that Major Component, or to implement a Standard 121 | Interface for which an implementation is available to the public in source code form. 122 | A “Major Component”, in this context, means a major essential component 123 | (kernel, window system, and so on) of the specific operating system (if any) on which 124 | the executable work runs, or a compiler used to produce the work, or an object code 125 | interpreter used to run it. 126 | 127 | The “Corresponding Source” for a work in object code form means all the 128 | source code needed to generate, install, and (for an executable work) run the object 129 | code and to modify the work, including scripts to control those activities. However, 130 | it does not include the work's System Libraries, or general-purpose tools or 131 | generally available free programs which are used unmodified in performing those 132 | activities but which are not part of the work. For example, Corresponding Source 133 | includes interface definition files associated with source files for the work, and 134 | the source code for shared libraries and dynamically linked subprograms that the work 135 | is specifically designed to require, such as by intimate data communication or 136 | control flow between those subprograms and other parts of the work. 137 | 138 | The Corresponding Source need not include anything that users can regenerate 139 | automatically from other parts of the Corresponding Source. 140 | 141 | The Corresponding Source for a work in source code form is that same work. 142 | 143 | ### 2. Basic Permissions. 144 | 145 | All rights granted under this License are granted for the term of copyright on the 146 | Program, and are irrevocable provided the stated conditions are met. This License 147 | explicitly affirms your unlimited permission to run the unmodified Program. The 148 | output from running a covered work is covered by this License only if the output, 149 | given its content, constitutes a covered work. This License acknowledges your rights 150 | of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not convey, without 153 | conditions so long as your license otherwise remains in force. You may convey covered 154 | works to others for the sole purpose of having them make modifications exclusively 155 | for you, or provide you with facilities for running those works, provided that you 156 | comply with the terms of this License in conveying all material for which you do not 157 | control copyright. Those thus making or running the covered works for you must do so 158 | exclusively on your behalf, under your direction and control, on terms that prohibit 159 | them from making any copies of your copyrighted material outside their relationship 160 | with you. 161 | 162 | Conveying under any other circumstances is permitted solely under the conditions 163 | stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 164 | 165 | ### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 166 | 167 | No covered work shall be deemed part of an effective technological measure under any 168 | applicable law fulfilling obligations under article 11 of the WIPO copyright treaty 169 | adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention 170 | of such measures. 171 | 172 | When you convey a covered work, you waive any legal power to forbid circumvention of 173 | technological measures to the extent such circumvention is effected by exercising 174 | rights under this License with respect to the covered work, and you disclaim any 175 | intention to limit operation or modification of the work as a means of enforcing, 176 | against the work's users, your or third parties' legal rights to forbid circumvention 177 | of technological measures. 178 | 179 | ### 4. Conveying Verbatim Copies. 180 | 181 | You may convey verbatim copies of the Program's source code as you receive it, in any 182 | medium, provided that you conspicuously and appropriately publish on each copy an 183 | appropriate copyright notice; keep intact all notices stating that this License and 184 | any non-permissive terms added in accord with section 7 apply to the code; keep 185 | intact all notices of the absence of any warranty; and give all recipients a copy of 186 | this License along with the Program. 187 | 188 | You may charge any price or no price for each copy that you convey, and you may offer 189 | support or warranty protection for a fee. 190 | 191 | ### 5. Conveying Modified Source Versions. 192 | 193 | You may convey a work based on the Program, or the modifications to produce it from 194 | the Program, in the form of source code under the terms of section 4, provided that 195 | you also meet all of these conditions: 196 | 197 | * **a)** The work must carry prominent notices stating that you modified it, and giving a 198 | relevant date. 199 | * **b)** The work must carry prominent notices stating that it is released under this 200 | License and any conditions added under section 7. This requirement modifies the 201 | requirement in section 4 to “keep intact all notices”. 202 | * **c)** You must license the entire work, as a whole, under this License to anyone who 203 | comes into possession of a copy. This License will therefore apply, along with any 204 | applicable section 7 additional terms, to the whole of the work, and all its parts, 205 | regardless of how they are packaged. This License gives no permission to license the 206 | work in any other way, but it does not invalidate such permission if you have 207 | separately received it. 208 | * **d)** If the work has interactive user interfaces, each must display Appropriate Legal 209 | Notices; however, if the Program has interactive interfaces that do not display 210 | Appropriate Legal Notices, your work need not make them do so. 211 | 212 | A compilation of a covered work with other separate and independent works, which are 213 | not by their nature extensions of the covered work, and which are not combined with 214 | it such as to form a larger program, in or on a volume of a storage or distribution 215 | medium, is called an “aggregate” if the compilation and its resulting 216 | copyright are not used to limit the access or legal rights of the compilation's users 217 | beyond what the individual works permit. Inclusion of a covered work in an aggregate 218 | does not cause this License to apply to the other parts of the aggregate. 219 | 220 | ### 6. Conveying Non-Source Forms. 221 | 222 | You may convey a covered work in object code form under the terms of sections 4 and 223 | 5, provided that you also convey the machine-readable Corresponding Source under the 224 | terms of this License, in one of these ways: 225 | 226 | * **a)** Convey the object code in, or embodied in, a physical product (including a 227 | physical distribution medium), accompanied by the Corresponding Source fixed on a 228 | durable physical medium customarily used for software interchange. 229 | * **b)** Convey the object code in, or embodied in, a physical product (including a 230 | physical distribution medium), accompanied by a written offer, valid for at least 231 | three years and valid for as long as you offer spare parts or customer support for 232 | that product model, to give anyone who possesses the object code either (1) a copy of 233 | the Corresponding Source for all the software in the product that is covered by this 234 | License, on a durable physical medium customarily used for software interchange, for 235 | a price no more than your reasonable cost of physically performing this conveying of 236 | source, or (2) access to copy the Corresponding Source from a network server at no 237 | charge. 238 | * **c)** Convey individual copies of the object code with a copy of the written offer to 239 | provide the Corresponding Source. This alternative is allowed only occasionally and 240 | noncommercially, and only if you received the object code with such an offer, in 241 | accord with subsection 6b. 242 | * **d)** Convey the object code by offering access from a designated place (gratis or for 243 | a charge), and offer equivalent access to the Corresponding Source in the same way 244 | through the same place at no further charge. You need not require recipients to copy 245 | the Corresponding Source along with the object code. If the place to copy the object 246 | code is a network server, the Corresponding Source may be on a different server 247 | (operated by you or a third party) that supports equivalent copying facilities, 248 | provided you maintain clear directions next to the object code saying where to find 249 | the Corresponding Source. Regardless of what server hosts the Corresponding Source, 250 | you remain obligated to ensure that it is available for as long as needed to satisfy 251 | these requirements. 252 | * **e)** Convey the object code using peer-to-peer transmission, provided you inform 253 | other peers where the object code and Corresponding Source of the work are being 254 | offered to the general public at no charge under subsection 6d. 255 | 256 | A separable portion of the object code, whose source code is excluded from the 257 | Corresponding Source as a System Library, need not be included in conveying the 258 | object code work. 259 | 260 | A “User Product” is either (1) a “consumer product”, which 261 | means any tangible personal property which is normally used for personal, family, or 262 | household purposes, or (2) anything designed or sold for incorporation into a 263 | dwelling. In determining whether a product is a consumer product, doubtful cases 264 | shall be resolved in favor of coverage. For a particular product received by a 265 | particular user, “normally used” refers to a typical or common use of 266 | that class of product, regardless of the status of the particular user or of the way 267 | in which the particular user actually uses, or expects or is expected to use, the 268 | product. A product is a consumer product regardless of whether the product has 269 | substantial commercial, industrial or non-consumer uses, unless such uses represent 270 | the only significant mode of use of the product. 271 | 272 | “Installation Information” for a User Product means any methods, 273 | procedures, authorization keys, or other information required to install and execute 274 | modified versions of a covered work in that User Product from a modified version of 275 | its Corresponding Source. The information must suffice to ensure that the continued 276 | functioning of the modified object code is in no case prevented or interfered with 277 | solely because modification has been made. 278 | 279 | If you convey an object code work under this section in, or with, or specifically for 280 | use in, a User Product, and the conveying occurs as part of a transaction in which 281 | the right of possession and use of the User Product is transferred to the recipient 282 | in perpetuity or for a fixed term (regardless of how the transaction is 283 | characterized), the Corresponding Source conveyed under this section must be 284 | accompanied by the Installation Information. But this requirement does not apply if 285 | neither you nor any third party retains the ability to install modified object code 286 | on the User Product (for example, the work has been installed in ROM). 287 | 288 | The requirement to provide Installation Information does not include a requirement to 289 | continue to provide support service, warranty, or updates for a work that has been 290 | modified or installed by the recipient, or for the User Product in which it has been 291 | modified or installed. Access to a network may be denied when the modification itself 292 | materially and adversely affects the operation of the network or violates the rules 293 | and protocols for communication across the network. 294 | 295 | Corresponding Source conveyed, and Installation Information provided, in accord with 296 | this section must be in a format that is publicly documented (and with an 297 | implementation available to the public in source code form), and must require no 298 | special password or key for unpacking, reading or copying. 299 | 300 | ### 7. Additional Terms. 301 | 302 | “Additional permissions” are terms that supplement the terms of this 303 | License by making exceptions from one or more of its conditions. Additional 304 | permissions that are applicable to the entire Program shall be treated as though they 305 | were included in this License, to the extent that they are valid under applicable 306 | law. If additional permissions apply only to part of the Program, that part may be 307 | used separately under those permissions, but the entire Program remains governed by 308 | this License without regard to the additional permissions. 309 | 310 | When you convey a copy of a covered work, you may at your option remove any 311 | additional permissions from that copy, or from any part of it. (Additional 312 | permissions may be written to require their own removal in certain cases when you 313 | modify the work.) You may place additional permissions on material, added by you to a 314 | covered work, for which you have or can give appropriate copyright permission. 315 | 316 | Notwithstanding any other provision of this License, for material you add to a 317 | covered work, you may (if authorized by the copyright holders of that material) 318 | supplement the terms of this License with terms: 319 | 320 | * **a)** Disclaiming warranty or limiting liability differently from the terms of 321 | sections 15 and 16 of this License; or 322 | * **b)** Requiring preservation of specified reasonable legal notices or author 323 | attributions in that material or in the Appropriate Legal Notices displayed by works 324 | containing it; or 325 | * **c)** Prohibiting misrepresentation of the origin of that material, or requiring that 326 | modified versions of such material be marked in reasonable ways as different from the 327 | original version; or 328 | * **d)** Limiting the use for publicity purposes of names of licensors or authors of the 329 | material; or 330 | * **e)** Declining to grant rights under trademark law for use of some trade names, 331 | trademarks, or service marks; or 332 | * **f)** Requiring indemnification of licensors and authors of that material by anyone 333 | who conveys the material (or modified versions of it) with contractual assumptions of 334 | liability to the recipient, for any liability that these contractual assumptions 335 | directly impose on those licensors and authors. 336 | 337 | All other non-permissive additional terms are considered “further 338 | restrictions” within the meaning of section 10. If the Program as you received 339 | it, or any part of it, contains a notice stating that it is governed by this License 340 | along with a term that is a further restriction, you may remove that term. If a 341 | license document contains a further restriction but permits relicensing or conveying 342 | under this License, you may add to a covered work material governed by the terms of 343 | that license document, provided that the further restriction does not survive such 344 | relicensing or conveying. 345 | 346 | If you add terms to a covered work in accord with this section, you must place, in 347 | the relevant source files, a statement of the additional terms that apply to those 348 | files, or a notice indicating where to find the applicable terms. 349 | 350 | Additional terms, permissive or non-permissive, may be stated in the form of a 351 | separately written license, or stated as exceptions; the above requirements apply 352 | either way. 353 | 354 | ### 8. Termination. 355 | 356 | You may not propagate or modify a covered work except as expressly provided under 357 | this License. Any attempt otherwise to propagate or modify it is void, and will 358 | automatically terminate your rights under this License (including any patent licenses 359 | granted under the third paragraph of section 11). 360 | 361 | However, if you cease all violation of this License, then your license from a 362 | particular copyright holder is reinstated (a) provisionally, unless and until the 363 | copyright holder explicitly and finally terminates your license, and (b) permanently, 364 | if the copyright holder fails to notify you of the violation by some reasonable means 365 | prior to 60 days after the cessation. 366 | 367 | Moreover, your license from a particular copyright holder is reinstated permanently 368 | if the copyright holder notifies you of the violation by some reasonable means, this 369 | is the first time you have received notice of violation of this License (for any 370 | work) from that copyright holder, and you cure the violation prior to 30 days after 371 | your receipt of the notice. 372 | 373 | Termination of your rights under this section does not terminate the licenses of 374 | parties who have received copies or rights from you under this License. If your 375 | rights have been terminated and not permanently reinstated, you do not qualify to 376 | receive new licenses for the same material under section 10. 377 | 378 | ### 9. Acceptance Not Required for Having Copies. 379 | 380 | You are not required to accept this License in order to receive or run a copy of the 381 | Program. Ancillary propagation of a covered work occurring solely as a consequence of 382 | using peer-to-peer transmission to receive a copy likewise does not require 383 | acceptance. However, nothing other than this License grants you permission to 384 | propagate or modify any covered work. These actions infringe copyright if you do not 385 | accept this License. Therefore, by modifying or propagating a covered work, you 386 | indicate your acceptance of this License to do so. 387 | 388 | ### 10. Automatic Licensing of Downstream Recipients. 389 | 390 | Each time you convey a covered work, the recipient automatically receives a license 391 | from the original licensors, to run, modify and propagate that work, subject to this 392 | License. You are not responsible for enforcing compliance by third parties with this 393 | License. 394 | 395 | An “entity transaction” is a transaction transferring control of an 396 | organization, or substantially all assets of one, or subdividing an organization, or 397 | merging organizations. If propagation of a covered work results from an entity 398 | transaction, each party to that transaction who receives a copy of the work also 399 | receives whatever licenses to the work the party's predecessor in interest had or 400 | could give under the previous paragraph, plus a right to possession of the 401 | Corresponding Source of the work from the predecessor in interest, if the predecessor 402 | has it or can get it with reasonable efforts. 403 | 404 | You may not impose any further restrictions on the exercise of the rights granted or 405 | affirmed under this License. For example, you may not impose a license fee, royalty, 406 | or other charge for exercise of rights granted under this License, and you may not 407 | initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging 408 | that any patent claim is infringed by making, using, selling, offering for sale, or 409 | importing the Program or any portion of it. 410 | 411 | ### 11. Patents. 412 | 413 | A “contributor” is a copyright holder who authorizes use under this 414 | License of the Program or a work on which the Program is based. The work thus 415 | licensed is called the contributor's “contributor version”. 416 | 417 | A contributor's “essential patent claims” are all patent claims owned or 418 | controlled by the contributor, whether already acquired or hereafter acquired, that 419 | would be infringed by some manner, permitted by this License, of making, using, or 420 | selling its contributor version, but do not include claims that would be infringed 421 | only as a consequence of further modification of the contributor version. For 422 | purposes of this definition, “control” includes the right to grant patent 423 | sublicenses in a manner consistent with the requirements of this License. 424 | 425 | Each contributor grants you a non-exclusive, worldwide, royalty-free patent license 426 | under the contributor's essential patent claims, to make, use, sell, offer for sale, 427 | import and otherwise run, modify and propagate the contents of its contributor 428 | version. 429 | 430 | In the following three paragraphs, a “patent license” is any express 431 | agreement or commitment, however denominated, not to enforce a patent (such as an 432 | express permission to practice a patent or covenant not to sue for patent 433 | infringement). To “grant” such a patent license to a party means to make 434 | such an agreement or commitment not to enforce a patent against the party. 435 | 436 | If you convey a covered work, knowingly relying on a patent license, and the 437 | Corresponding Source of the work is not available for anyone to copy, free of charge 438 | and under the terms of this License, through a publicly available network server or 439 | other readily accessible means, then you must either (1) cause the Corresponding 440 | Source to be so available, or (2) arrange to deprive yourself of the benefit of the 441 | patent license for this particular work, or (3) arrange, in a manner consistent with 442 | the requirements of this License, to extend the patent license to downstream 443 | recipients. “Knowingly relying” means you have actual knowledge that, but 444 | for the patent license, your conveying the covered work in a country, or your 445 | recipient's use of the covered work in a country, would infringe one or more 446 | identifiable patents in that country that you have reason to believe are valid. 447 | 448 | If, pursuant to or in connection with a single transaction or arrangement, you 449 | convey, or propagate by procuring conveyance of, a covered work, and grant a patent 450 | license to some of the parties receiving the covered work authorizing them to use, 451 | propagate, modify or convey a specific copy of the covered work, then the patent 452 | license you grant is automatically extended to all recipients of the covered work and 453 | works based on it. 454 | 455 | A patent license is “discriminatory” if it does not include within the 456 | scope of its coverage, prohibits the exercise of, or is conditioned on the 457 | non-exercise of one or more of the rights that are specifically granted under this 458 | License. You may not convey a covered work if you are a party to an arrangement with 459 | a third party that is in the business of distributing software, under which you make 460 | payment to the third party based on the extent of your activity of conveying the 461 | work, and under which the third party grants, to any of the parties who would receive 462 | the covered work from you, a discriminatory patent license (a) in connection with 463 | copies of the covered work conveyed by you (or copies made from those copies), or (b) 464 | primarily for and in connection with specific products or compilations that contain 465 | the covered work, unless you entered into that arrangement, or that patent license 466 | was granted, prior to 28 March 2007. 467 | 468 | Nothing in this License shall be construed as excluding or limiting any implied 469 | license or other defenses to infringement that may otherwise be available to you 470 | under applicable patent law. 471 | 472 | ### 12. No Surrender of Others' Freedom. 473 | 474 | If conditions are imposed on you (whether by court order, agreement or otherwise) 475 | that contradict the conditions of this License, they do not excuse you from the 476 | conditions of this License. If you cannot convey a covered work so as to satisfy 477 | simultaneously your obligations under this License and any other pertinent 478 | obligations, then as a consequence you may not convey it at all. For example, if you 479 | agree to terms that obligate you to collect a royalty for further conveying from 480 | those to whom you convey the Program, the only way you could satisfy both those terms 481 | and this License would be to refrain entirely from conveying the Program. 482 | 483 | ### 13. Use with the GNU Affero General Public License. 484 | 485 | Notwithstanding any other provision of this License, you have permission to link or 486 | combine any covered work with a work licensed under version 3 of the GNU Affero 487 | General Public License into a single combined work, and to convey the resulting work. 488 | The terms of this License will continue to apply to the part which is the covered 489 | work, but the special requirements of the GNU Affero General Public License, section 490 | 13, concerning interaction through a network will apply to the combination as such. 491 | 492 | ### 14. Revised Versions of this License. 493 | 494 | The Free Software Foundation may publish revised and/or new versions of the GNU 495 | General Public License from time to time. Such new versions will be similar in spirit 496 | to the present version, but may differ in detail to address new problems or concerns. 497 | 498 | Each version is given a distinguishing version number. If the Program specifies that 499 | a certain numbered version of the GNU General Public License “or any later 500 | version” applies to it, you have the option of following the terms and 501 | conditions either of that numbered version or of any later version published by the 502 | Free Software Foundation. If the Program does not specify a version number of the GNU 503 | General Public License, you may choose any version ever published by the Free 504 | Software Foundation. 505 | 506 | If the Program specifies that a proxy can decide which future versions of the GNU 507 | General Public License can be used, that proxy's public statement of acceptance of a 508 | version permanently authorizes you to choose that version for the Program. 509 | 510 | Later license versions may give you additional or different permissions. However, no 511 | additional obligations are imposed on any author or copyright holder as a result of 512 | your choosing to follow a later version. 513 | 514 | ### 15. Disclaimer of Warranty. 515 | 516 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 517 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 518 | PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER 519 | EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 520 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE 521 | QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 522 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 523 | 524 | ### 16. Limitation of Liability. 525 | 526 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY 527 | COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS 528 | PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, 529 | INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 530 | PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE 531 | OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE 532 | WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 533 | POSSIBILITY OF SUCH DAMAGES. 534 | 535 | ### 17. Interpretation of Sections 15 and 16. 536 | 537 | If the disclaimer of warranty and limitation of liability provided above cannot be 538 | given local legal effect according to their terms, reviewing courts shall apply local 539 | law that most closely approximates an absolute waiver of all civil liability in 540 | connection with the Program, unless a warranty or assumption of liability accompanies 541 | a copy of the Program in return for a fee. 542 | 543 | END OF TERMS AND CONDITIONS 544 | 545 | ## How to Apply These Terms to Your New Programs 546 | 547 | If you develop a new program, and you want it to be of the greatest possible use to 548 | the public, the best way to achieve this is to make it free software which everyone 549 | can redistribute and change under these terms. 550 | 551 | To do so, attach the following notices to the program. It is safest to attach them 552 | to the start of each source file to most effectively state the exclusion of warranty; 553 | and each file should have at least the “copyright” line and a pointer to 554 | where the full notice is found. 555 | 556 | 557 | Copyright (C) 558 | 559 | This program is free software: you can redistribute it and/or modify 560 | it under the terms of the GNU General Public License as published by 561 | the Free Software Foundation, either version 3 of the License, or 562 | (at your option) any later version. 563 | 564 | This program is distributed in the hope that it will be useful, 565 | but WITHOUT ANY WARRANTY; without even the implied warranty of 566 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 567 | GNU General Public License for more details. 568 | 569 | You should have received a copy of the GNU General Public License 570 | along with this program. If not, see . 571 | 572 | Also add information on how to contact you by electronic and paper mail. 573 | 574 | If the program does terminal interaction, make it output a short notice like this 575 | when it starts in an interactive mode: 576 | 577 | Copyright (C) 578 | This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'. 579 | This is free software, and you are welcome to redistribute it 580 | under certain conditions; type 'show c' for details. 581 | 582 | The hypothetical commands 'show w' and 'show c' should show the appropriate parts of 583 | the General Public License. Of course, your program's commands might be different; 584 | for a GUI interface, you would use an “about box”. 585 | 586 | You should also get your employer (if you work as a programmer) or school, if any, to 587 | sign a “copyright disclaimer” for the program, if necessary. For more 588 | information on this, and how to apply and follow the GNU GPL, see 589 | <>. 590 | 591 | The GNU General Public License does not permit incorporating your program into 592 | proprietary programs. If your program is a subroutine library, you may consider it 593 | more useful to permit linking proprietary applications with the library. If this is 594 | what you want to do, use the GNU Lesser General Public License instead of this 595 | License. But first, please read 596 | <>. --------------------------------------------------------------------------------