├── 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 | 
18 | 
19 | 
20 | 
21 | 
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 | <>.
--------------------------------------------------------------------------------