├── .gitignore ├── LICENSE ├── README.md ├── bitboard.py ├── bitmessage_gateway.py ├── chan_objects.py ├── config.py ├── requirements.txt ├── static ├── banner.png ├── favicon.ico ├── screenshot.png └── thread.png ├── templates ├── base.html ├── elements │ ├── banner.html │ ├── board │ │ ├── post.html │ │ ├── post_op.html │ │ ├── post_reply.html │ │ ├── postform.html │ │ ├── postheader.html │ │ └── reply_popup.html │ ├── chans.html │ ├── csstheme.css │ ├── footer.html │ ├── head.html │ ├── index │ │ ├── intro.html │ │ └── start.html │ ├── join_popup.html │ ├── status.html │ ├── themes_popup.html │ └── title.html └── pages │ ├── alert.html │ ├── board.html │ ├── index.html │ └── thread.html └── themes.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .DS_Store 3 | .gitignore 4 | .idea/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mike Roberts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bitboard 2 | 3 | Bitboard is a decentralized anonymous imageboard. It is built on top of Bitmessage's [decentralized mailing lists.](https://bitmessage.org/wiki/Decentralized_Mailing_List) 4 | 5 | To get started, you must download and setup [Bitmessage](https://bitmessage.org) then [enable API access](https://bitmessage.org/wiki/API_Reference#Enable_the_API). Bitmessage must be running in order for Bitboard to work. 6 | 7 | Once you have Bitmessage setup and running, you may download and run bitboard with the following commands. 8 | 9 | git clone https://github.com/michrob/bitboard 10 | cd bitboard/ 11 | python2 -m pip install -r requirements.txt 12 | python2 bitboard.py 13 | 14 | bitboard runs on port 8080 by default, so you should see it running when you visit http://localhost:8080 in your browser. For security purposes, you should probably disable javascript. 15 | 16 | 17 | ![bitboard screenshot](/static/screenshot.png) 18 | -------------------------------------------------------------------------------- /bitboard.py: -------------------------------------------------------------------------------- 1 | import web 2 | from bitmessage_gateway import gateway_instance as nexus 3 | import config 4 | import math 5 | import themes 6 | import os 7 | import signal 8 | 9 | t_globals = dict( 10 | math=math, 11 | bitmessage=nexus, 12 | datestr=web.datestr, 13 | themes=themes.themes, 14 | config=config) 15 | 16 | urls = ( 17 | '/delete/*(.+)', 'Delete', 18 | '/images/*(.+)', 'Images', 19 | '/join/*(.+)', 'Join', 20 | '/board/*(.+)', 'Board', 21 | '/catalog/*(.+)', 'Catalog', 22 | '/thread/*(.+)', 'Thread', 23 | '/*(.+)', 'Index' 24 | ) 25 | 26 | render = web.template.render('templates/', cache=config.cache, globals=t_globals) 27 | render._keywords['globals']['render'] = render 28 | render._keywords['globals']['bitmessage'] = nexus 29 | app = web.application(urls, globals()) 30 | app.daemon = True 31 | app.internalerror = web.debugerror 32 | 33 | 34 | class Delete: 35 | def __init__(self): 36 | pass 37 | 38 | def GET(self, url): 39 | web_input = web.input(chan=None, threadid=None, messageid=None) 40 | 41 | render._keywords['globals']['model'] = {"current_thread": web_input.threadid, 42 | "current_chan": web_input.chan} 43 | 44 | render._keywords['globals']['model']['status_message'] = None 45 | 46 | if web_input.messageid: 47 | result = nexus.deleteMessage(web_input.chan, web_input.messageid) 48 | render._keywords['globals']['model']['status_message'] = result 49 | render._keywords['globals']['model']['status_title'] = "Deleted Message" 50 | 51 | if web_input.threadid: 52 | result = nexus.deleteThread(web_input.chan, web_input.threadid) 53 | render._keywords['globals']['model']['status_message'] = result 54 | render._keywords['globals']['model']['status_title'] = "Deleted Thread" 55 | 56 | return render.base(render.pages.alert()) 57 | 58 | 59 | class Images: 60 | def __init__(self): 61 | pass 62 | 63 | def GET(self, url): 64 | web_input = web.input(image=None) 65 | return nexus.getImage(web_input.image) 66 | 67 | 68 | class Join: 69 | def __init__(self): 70 | pass 71 | 72 | def POST(self, url): 73 | web_input = web.input(chan=None, passphrase=None) 74 | 75 | render._keywords['globals']['model'] = {"current_chan": web_input.chan} 76 | result = nexus.joinChan(web_input.passphrase) 77 | 78 | render._keywords['globals']['model']['status_title'] = "Success" 79 | render._keywords['globals']['model']['status_message'] = result 80 | 81 | return render.base(render.pages.alert()) 82 | 83 | 84 | class Board: 85 | def __init__(self): 86 | pass 87 | 88 | def GET(self, url): 89 | web_input = web.input(chan=None, page=1, threadid=None, theme=None) 90 | 91 | if not web_input.chan: 92 | raise web.seeother("/") 93 | 94 | render._keywords['globals']['model'] = {"current_thread": web_input.threadid, 95 | "current_page": web_input.page, 96 | "current_chan": web_input.chan} 97 | 98 | return render.base(render.pages.board()) 99 | 100 | def POST(self, url): 101 | web_input = web.input(chan=None, subject="", body="", image=None, theme=None) 102 | 103 | if web_input.theme: 104 | config.theme = web_input.theme 105 | raise web.seeother(web.ctx.query) 106 | 107 | render._keywords['globals']['model'] = {"current_chan": web_input.chan} 108 | 109 | if not web_input.chan: 110 | render._keywords['globals']['model']['status_message'] = "You must post to a valid chan!" 111 | elif len(web_input.subject.strip()) == 0 or len(web_input.body.strip()) == 0: 112 | render._keywords['globals']['model']['status_message'] = "You must include a subject and message." 113 | 114 | if "status_message" in render._keywords['globals']['model']: 115 | render._keywords['globals']['model']['status_title'] = "Uh oh, something went wrong!" 116 | return render.base(render.pages.alert()) 117 | 118 | result = nexus.submitPost(web_input.chan, web_input.subject, web_input.body, web_input.image) 119 | render._keywords['globals']['model']['status_title'] = "Success" 120 | render._keywords['globals']['model']['status_message'] = result 121 | 122 | return render.base(render.pages.alert()) 123 | 124 | 125 | class Thread: 126 | def __init__(self): 127 | pass 128 | 129 | def GET(self, url): 130 | web_input = web.input(chan=None, threadid=None, theme=None) 131 | 132 | render._keywords['globals']['model'] = {"current_thread": web_input.threadid, 133 | "current_chan": web_input.chan} 134 | 135 | if not web_input.chan: 136 | raise web.seeother("/") 137 | 138 | return render.base(render.pages.thread()) 139 | 140 | def POST(self, url): 141 | web_input = web.input(chan=None, subject="", body="", image=None, theme=None) 142 | 143 | if web_input.theme: 144 | config.theme = web_input.theme 145 | raise web.seeother(web.ctx.query) 146 | 147 | render._keywords['globals']['model'] = {"current_thread": web_input.threadid, 148 | "current_chan": web_input.chan} 149 | 150 | if not web_input.chan: 151 | render._keywords['globals']['model']['status_message'] = "You must post to a valid chan!" 152 | elif len(web_input.subject.strip()) == 0 or len(web_input.body.strip()) == 0: 153 | render._keywords['globals']['model']['status_message'] = "You must include a subject and message." 154 | 155 | if "status_message" in render._keywords['globals']['model']: 156 | render._keywords['globals']['model']['status_title'] = "Uh oh, something went wrong!" 157 | return render.base(render.pages.alert()) 158 | 159 | result = nexus.submitPost(web_input.chan, web_input.subject, web_input.body, web_input.image) 160 | render._keywords['globals']['model']['status_title'] = "Success" 161 | render._keywords['globals']['model']['status_message'] = result 162 | 163 | return render.base(render.pages.alert()) 164 | 165 | 166 | class Index: 167 | def __init__(self): 168 | pass 169 | 170 | def POST(self, url): 171 | web_input = web.input(theme=None) 172 | 173 | if web_input.theme: 174 | config.theme = web_input.theme 175 | raise web.seeother(web.ctx.query) 176 | 177 | def GET(self, url): 178 | 179 | render._keywords['globals']['model'] = {} 180 | status = nexus.getAPIStatus() 181 | 182 | if not status == True: 183 | render._keywords['globals']['model']['status_title'] = "Success" 184 | render._keywords['globals']['model']['status_message'] = status 185 | 186 | return render.base(render.pages.index()) 187 | 188 | 189 | def signal_handler(signal, frame): 190 | os._exit(0) 191 | 192 | if __name__ == "__main__": 193 | signal.signal(signal.SIGINT, signal_handler) 194 | app.run() 195 | -------------------------------------------------------------------------------- /bitmessage_gateway.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import logging as log 4 | import time 5 | import traceback 6 | import xmlrpclib 7 | from threading import Thread 8 | 9 | import config 10 | from chan_objects import ChanBoard 11 | from chan_objects import ChanPost 12 | 13 | 14 | def getBitmessageEndpoint(): 15 | username = config.getBMConfig("apiusername") 16 | password = config.getBMConfig("apipassword") 17 | host = config.getBMConfig("apiinterface") 18 | port = config.getBMConfig("apiport") 19 | return "http://"+username+":"+password+"@"+host+":"+port+"/" 20 | 21 | 22 | class BitMessageGateway(Thread): 23 | def __init__(self): 24 | super(BitMessageGateway, self).__init__() 25 | self._postsById = {} 26 | self._boardByChan = {} 27 | self._chanDict = {} 28 | self._refresh = True 29 | self._api = xmlrpclib.ServerProxy(getBitmessageEndpoint()) 30 | 31 | def run(self): 32 | while True: 33 | try: 34 | print "Updating bitmessage info." 35 | self.updateChans() 36 | self.updateChanThreads() 37 | 38 | print `len(self._postsById)` + " total messages " + `len(self._chanDict)` + " total chans." 39 | 40 | for i in range(0, config.bm_refresh_interval): 41 | time.sleep(i) 42 | if self._refresh: 43 | self._refresh = False 44 | break 45 | except Exception as e: 46 | log.error("Exception in gateway thread: " + `e`) 47 | time.sleep(config.bm_refresh_interval) 48 | 49 | def getChans(self): 50 | return self._chanDict 51 | 52 | def deleteMessage(self, chan, messageid): 53 | try: 54 | board = self._boardByChan[chan] 55 | post = self._postsById[messageid] 56 | board.deletePost(post) 57 | del self._postsById[messageid] 58 | except Exception as e: 59 | print "Exception deleting post: " + `e` 60 | traceback.print_exc() 61 | return self._api.trashMessage(messageid) 62 | 63 | def deleteThread(self, chan, threadid): 64 | try: 65 | board = self._boardByChan[chan] 66 | thread = board.getThread(threadid) 67 | if thread: 68 | threadposts = thread.getPosts() 69 | for post in threadposts: 70 | self.deleteMessage(chan, post.msgid) 71 | board.deleteThread(threadid) 72 | except Exception as e: 73 | print "Exception deleting thread: " + repr(e) 74 | traceback.print_exc() 75 | return "Thread [" + repr(threadid) + "] deleted." 76 | 77 | def updateChans(self): 78 | chans = {} 79 | try: 80 | straddr = self._api.listAddresses() 81 | addresses = json.loads(straddr)['addresses'] 82 | for jaddr in addresses: 83 | if jaddr['chan'] and jaddr['enabled']: 84 | chan_name = jaddr['label'] 85 | chans[chan_name] = jaddr['address'] 86 | except Exception as e: 87 | log.error("Exception getting channels: " + `e`) 88 | traceback.print_exc() 89 | 90 | self._chanDict = dict(self._chanDict.items() + chans.items()) 91 | 92 | def getChanName(self, chan): 93 | for label, addr in self._chanDict.iteritems(): 94 | if addr == chan: 95 | return label 96 | 97 | def getImage(self, imageid): 98 | return self._postsById[imageid].image 99 | 100 | def updateChanThreads(self): 101 | strmessages = self._api.getAllInboxMessageIDs() 102 | messages = json.loads(strmessages)['inboxMessageIds'] 103 | for message in messages: 104 | messageid = message["msgid"] 105 | 106 | if messageid in self._postsById: 107 | continue 108 | 109 | strmessage = self._api.getInboxMessageByID(messageid) 110 | jsonmessages = json.loads(strmessage)['inboxMessage'] 111 | 112 | if len(jsonmessages) <= 0: 113 | continue 114 | 115 | chan = jsonmessages[0]['toAddress'] 116 | post = ChanPost(chan, jsonmessages[0]) 117 | 118 | if chan not in self._boardByChan: 119 | self._boardByChan[chan] = ChanBoard(chan) 120 | 121 | self._postsById[messageid] = post 122 | chanboard = self._boardByChan[chan] 123 | chanboard.addPost(post) 124 | 125 | def getThreadCount(self, chan): 126 | if chan not in self._boardByChan: 127 | return 0 128 | return self._boardByChan[chan].getThreadCount() 129 | 130 | def getChanThreads(self, chan, page=1): 131 | if chan not in self._boardByChan: 132 | return [] 133 | board = self._boardByChan[chan] 134 | 135 | thread_start = int((int(page) - 1) * config.threads_per_page) 136 | thread_end = int(int(page) * config.threads_per_page) 137 | 138 | return board.getThreads(thread_start, thread_end) 139 | 140 | def getChanThread(self, chan, thread_id): 141 | if chan not in self._boardByChan: 142 | return None 143 | 144 | board = self._boardByChan[chan] 145 | 146 | return board.getThread(thread_id) 147 | 148 | def submitPost(self, chan, subject, body, image): 149 | subject = subject.encode('utf-8').strip() 150 | subjectdata = base64.b64encode(subject) 151 | 152 | msgdata = body.encode('utf-8').strip() 153 | 154 | if image: 155 | imagedata = base64.b64encode(image) 156 | msgdata += "\n\n" 157 | 158 | msg = base64.b64encode(msgdata) 159 | 160 | self._refresh = True 161 | return self._api.sendMessage(chan, chan, subjectdata, msg) 162 | 163 | def joinChan(self, passphrase): 164 | self._refresh = True 165 | 166 | try: 167 | result = self._api.createChan(base64.b64encode(passphrase)) 168 | except Exception as e: 169 | result = repr(e) 170 | 171 | return result 172 | 173 | def getAPIStatus(self): 174 | try: 175 | result = self._api.add(2, 2) 176 | except Exception as e: 177 | return repr(e) 178 | if result == 4: 179 | return True 180 | return result 181 | 182 | gateway_instance = BitMessageGateway() 183 | gateway_instance.start() 184 | -------------------------------------------------------------------------------- /chan_objects.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import datetime 3 | import hashlib 4 | import json 5 | import traceback 6 | 7 | import bleach 8 | from sortedcontainers import SortedListWithKey as sortedlist 9 | 10 | import config 11 | 12 | ID_LENGTH = 9 13 | 14 | 15 | def getThreadId(subject): 16 | sha256 = hashlib.sha256() 17 | sha256.update(subject) 18 | threadId = sha256.hexdigest() 19 | return threadId 20 | 21 | 22 | class ChanPost: 23 | def __init__(self, chan, jsonObj): 24 | self.chan = chan 25 | self.subject = bleach.clean(base64.decodestring(jsonObj['subject'])).encode('utf-8').strip() 26 | 27 | if self.subject.startswith("Re: "): 28 | self.subject = self.subject[4:] 29 | 30 | self.threadid = getThreadId(self.subject) 31 | 32 | self.msgid = bleach.clean(jsonObj['msgid']) 33 | self.postid = self.msgid[-ID_LENGTH:].upper() 34 | 35 | self.timestamp = int(jsonObj['receivedTime']) 36 | self.date = datetime.datetime.fromtimestamp(self.timestamp).strftime('%Y/%m/%d(%a)%H:%M:%S') 37 | 38 | self.toaddr = bleach.clean(jsonObj['toAddress']) 39 | self.fromaddr = bleach.clean(jsonObj['fromAddress']) 40 | 41 | self.username = "Anonymous" 42 | if self.toaddr != self.fromaddr: 43 | self.username = self.fromaddr[-ID_LENGTH:] 44 | 45 | self.image = None 46 | self.body = None 47 | 48 | # set of postId's this post references. 49 | self.targetposts = set([]) 50 | 51 | message = base64.decodestring(jsonObj['message']) 52 | try: 53 | self.parseJsonBody(message) 54 | except Exception as e: 55 | if self.subject == "test subject": 56 | print "Exception parsing JSON: " + message + " Exception: " + `e` 57 | self.parsePlaintextBody(message) 58 | 59 | self.markup() 60 | 61 | def parseJsonBody(self, msgdata): 62 | try: 63 | message = json.loads(msgdata) 64 | if 'text' in message: 65 | self.body = bleach.clean(message['text']) 66 | if 'image' in message: 67 | self.image = base64.decodestring(message['image']) 68 | except Exception as e: 69 | raise Exception("parseJsonBody failed. " + `e`) 70 | 71 | def parsePlaintextBody(self, msgdata): 72 | if config.bm_integration: 73 | self.body = msgdata.split("\n" + 54 * "-")[0] 74 | if "data:image" in self.body: 75 | try: 76 | self.image = bleach.clean(self.body.split(",")[1]) 77 | if "\"" in self.image: 78 | self.image = self.image.split("\"")[0] 79 | self.image = base64.decodestring(self.image) 80 | except Exception as e: 81 | print "Exception decoding image: " + `e` 82 | print self.image 83 | traceback.print_exc() 84 | self.body = self.body.split("" + lines[line] + "" 115 | elif lines[line].startswith(">"): 116 | lines[line] = "" + lines[line] + "" 117 | self.body = "\n".join(lines) 118 | 119 | 120 | class ChanThread: 121 | def __init__(self, chan, subject): 122 | self.posts = sortedlist(key=lambda post: post.timestamp) 123 | # postid -> set(replies) 124 | self.repliesByPostId = {} 125 | self.subject = subject 126 | self.timestamp = 0 127 | self.threadid = getThreadId(subject) 128 | self.chan = chan 129 | 130 | def getPosts(self): 131 | return self.posts 132 | 133 | def deletePost(self, post): 134 | try: 135 | self.posts.remove(post) 136 | except Exception as e: 137 | print "Exception removing post: " + `e` 138 | traceback.print_exc() 139 | 140 | def addPost(self, post): 141 | self.posts.add(post) 142 | self.updatePostLinks(post) 143 | if post.timestamp > self.timestamp: 144 | self.timestamp = post.timestamp 145 | 146 | def updatePostLinks(self, post): 147 | for postId in post.targetposts: 148 | if postId not in self.repliesByPostId: 149 | self.repliesByPostId[postId] = set([]) 150 | self.repliesByPostId[postId].add(post.postid) 151 | 152 | def getPostReplies(self, postid): 153 | if postid not in self.repliesByPostId: 154 | return set([]) 155 | return self.repliesByPostId[postid] 156 | 157 | 158 | class ChanBoard: 159 | def __init__(self, chan): 160 | self._threads = sortedlist(key=lambda thread: -thread.timestamp) 161 | self._threadsById = {} 162 | self.chan = chan 163 | 164 | def getThreadCount(self): 165 | return len(self._threadsById) 166 | 167 | def getThreads(self, start_index, end_index): 168 | return self._threads[start_index:end_index] 169 | 170 | def getThread(self, threadid): 171 | if threadid in self._threadsById: 172 | return self._threadsById[threadid] 173 | return None 174 | 175 | def deletePost(self, post): 176 | threadid = getThreadId(post.subject) 177 | thread = self.getThread(threadid) 178 | if thread: 179 | thread.deletePost(post) 180 | 181 | def deleteThread(self, threadid): 182 | if threadid in self._threadsById: 183 | thread = self._threadsById[threadid] 184 | self._threads.remove(thread) 185 | 186 | def addPost(self, post): 187 | threadid = getThreadId(post.subject) 188 | thread = self.getThread(threadid) 189 | 190 | if not thread: 191 | thread = ChanThread(self.chan, post.subject) 192 | self._threadsById[thread.threadid] = thread 193 | else: 194 | # Remove it because we need to 195 | # re-insert it in sorted order. 196 | self._threads.remove(thread) 197 | 198 | # print "Updating thread: " + `thread.subject` 199 | 200 | thread.addPost(post) 201 | self._threads.add(thread) 202 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import os 4 | from os import path, environ 5 | import ConfigParser 6 | 7 | logging.basicConfig(level=logging.DEBUG) 8 | 9 | cache = False 10 | 11 | bm_integration = True 12 | 13 | bm_refresh_interval = 10 14 | 15 | threads_per_page = 15.0 16 | 17 | theme = "Classic" 18 | 19 | 20 | def getConfigFolder(): 21 | appfolder = "PyBitmessage" 22 | dataFolder = None 23 | if "BITMESSAGE_HOME" in environ: 24 | dataFolder = environ["BITMESSAGE_HOME"] 25 | if dataFolder[-1] not in [os.path.sep, os.path.altsep]: 26 | dataFolder += os.path.sep 27 | elif sys.platform == 'darwin': 28 | if "HOME" in environ: 29 | dataFolder = path.join(os.environ["HOME"], "Library/Application Support/", appfolder) + '/' 30 | elif 'win32' in sys.platform or 'win64' in sys.platform: 31 | dataFolder = path.join(environ['APPDATA'].decode(sys.getfilesystemencoding(), 'ignore'), appfolder) + path.sep 32 | else: 33 | try: 34 | dataFolder = path.join(environ["XDG_CONFIG_HOME"], appfolder) 35 | except KeyError: 36 | dataFolder = path.join(environ["HOME"], ".config", appfolder) 37 | dataFolder += '/' 38 | return dataFolder 39 | 40 | 41 | cp = ConfigParser.SafeConfigParser() 42 | cp.read(getConfigFolder() + 'keys.dat') 43 | 44 | settings_section = 'bitmessagesettings' 45 | 46 | 47 | def getBMConfig(setting_key): 48 | value = None 49 | try: 50 | value = cp.get(settings_section, setting_key) 51 | except Exception as e: 52 | pass 53 | return value 54 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | CherryPy 2 | bleach 3 | web.py 4 | sortedcontainers 5 | -------------------------------------------------------------------------------- /static/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michrob/bitboard/60a8cedf83b2226099ada187fbdd3165c53e2fc5/static/banner.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michrob/bitboard/60a8cedf83b2226099ada187fbdd3165c53e2fc5/static/favicon.ico -------------------------------------------------------------------------------- /static/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michrob/bitboard/60a8cedf83b2226099ada187fbdd3165c53e2fc5/static/screenshot.png -------------------------------------------------------------------------------- /static/thread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michrob/bitboard/60a8cedf83b2226099ada187fbdd3165c53e2fc5/static/thread.png -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | $def with (bodyview) 2 | 3 | 4 | 5 | $:render.elements.head() 6 | 7 |
8 | $:render.elements.join_popup() 9 | $:render.elements.themes_popup() 10 | $:bodyview 11 | 12 | 13 | -------------------------------------------------------------------------------- /templates/elements/banner.html: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 |
4 | 5 | -------------------------------------------------------------------------------- /templates/elements/board/post.html: -------------------------------------------------------------------------------- 1 | $def with (post) 2 | 3 | $ isboardview = model['current_thread'] == None 4 | 5 | $ max_lines = 12 6 | $ max_chars = max_lines * 175 7 | $ text = post.body 8 | $if text is None: 9 | $ text = "" 10 | 11 |
12 |

13 | $ groups = text.split('\n') 14 | $ part1 = "
".join(groups[:max_lines]) 15 | 16 | $:part1 17 | 18 | $if len(groups[max_lines:]) > 0 and isboardview: 19 |
20 | 21 |
22 | Comment truncated. 23 | Click here 24 | to view the full post. 25 |
26 | $else: 27 |
28 | $:"
".join(groups[max_lines:]) 29 |

30 |
31 | -------------------------------------------------------------------------------- /templates/elements/board/post_op.html: -------------------------------------------------------------------------------- 1 | $def with (post, thread) 2 | 3 | $if post.image: 4 | 5 | $else: 6 | 7 | 8 |
9 | $:render.elements.board.postheader(post, thread, True) 10 |
11 | $:render.elements.board.post(post) 12 |
13 |
14 | -------------------------------------------------------------------------------- /templates/elements/board/post_reply.html: -------------------------------------------------------------------------------- 1 | $def with (post, thread) 2 | 3 |
4 | $:render.elements.board.postheader(post, thread, False) 5 |
6 | $if post.image: 7 | 8 | $:render.elements.board.post(post) 9 |
10 |
11 | -------------------------------------------------------------------------------- /templates/elements/board/postform.html: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 | $ thread = None 4 | $if 'current_thread' in model: 5 | $ thread = bitmessage.getChanThread(model['current_chan'], model['current_thread']) 6 | 7 | $ chan = None 8 | $if 'current_chan' in model: 9 | $ chan = model['current_chan'] 10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | $else: 24 |  $:thread.subject 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 37 | 38 |
Subject 21 | $if not thread: 22 |
Comment
File 35 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /templates/elements/board/postheader.html: -------------------------------------------------------------------------------- 1 | $def with (post, thread, isoppost) 2 | 3 | $ isboardview = model['current_thread'] == None 4 | 5 | $if isoppost: 6 | $:post.subject 7 | 8 |  $:post.username $:post.date 9 | 10 | $if isboardview: 11 | $:post.postid 12 | $else: 13 | $:post.postid 14 | 15 | $if isboardview and isoppost: 16 | [Reply] 17 | 18 | 19 | 31 | 32 | $if not isboardview: 33 | $for reply in thread.getPostReplies(post.postid): 34 | >>$:reply 35 | -------------------------------------------------------------------------------- /templates/elements/board/reply_popup.html: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 | $ thread = None 4 | $if 'current_thread' in model: 5 | $ thread = bitmessage.getChanThread(model['current_chan'], model['current_thread']) 6 | 7 | $ thread_subject = None 8 | $if thread: 9 | $ thread_subject = thread.subject 10 | 11 | $ chan = None 12 | $if 'current_chan' in model: 13 | $ chan = model['current_chan'] 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 |
X
34 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /templates/elements/chans.html: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 | $ chan_dict = bitmessage.getChans() 4 | $ chan_prefix = "[chan] " 5 | 6 |
7 |
8 | [ 9 | $for chan_name in chan_dict: 10 | $ chan_key = chan_name 11 | $if chan_name.startswith(chan_prefix): 12 | $ chan_name = chan_name[len(chan_prefix):] 13 | $:chan_name 14 | $if not chan_key == chan_dict.items()[-1][0]: 15 | / 16 | ] 17 |
18 |
19 |
20 | [ Chans / 21 |  ] 22 |
23 |
24 | -------------------------------------------------------------------------------- /templates/elements/csstheme.css: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 | $ theme = themes[config.theme] 4 | 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | font-size: 10pt; 9 | font-family: Arial; 10 | -webkit-font-smoothing: antialiased !important; 11 | } 12 | 13 | .button { 14 | -webkit-appearance: button; 15 | -moz-appearance: button; 16 | appearance: button; 17 | 18 | text-decoration: none; 19 | color: initial; 20 | } 21 | 22 | .link { 23 | color: $theme.link; 24 | text-decoration: none; 25 | } 26 | 27 | .link:hover { 28 | color: $theme.linkhilight; 29 | } 30 | 31 | .underlined { 32 | text-decoration: underline; 33 | } 34 | 35 | .clickable:hover { 36 | cursor: pointer; 37 | cursor: hand; 38 | } 39 | 40 | .greentext { 41 | color: $theme.greentext; 42 | } 43 | 44 | .themed { 45 | color: $theme.textcolor; 46 | } 47 | 48 | .bold { 49 | font-weight: bold; 50 | } 51 | 52 | .outlined { 53 | text-shadow: 54 | -2px -2px #000, 55 | 2px -2px #000, 56 | -2px 2px #000, 57 | 2px 2px #000; 58 | } 59 | 60 | .title { 61 | font-family: Tahoma,sans-serif; 62 | font-size: 28px; 63 | letter-spacing: -1px; 64 | margin-top: 0px; 65 | } 66 | 67 | .gradient { 68 | background: $theme.bgcolor; 69 | background: -webkit-linear-gradient($theme.gradient_top, $theme.bgcolor); 70 | background: -o-linear-gradient($theme.gradient_top, $theme.bgcolor); 71 | background: -moz-linear-gradient($theme.gradient_top, $theme.bgcolor); 72 | background: linear-gradient($theme.gradient_top, $theme.bgcolor); 73 | position: absolute; 74 | width: 100%; 75 | height: 25%; 76 | top: 0; 77 | left: 0; 78 | z-index: -1; 79 | } 80 | 81 | div.menu { 82 | position: relative; 83 | display: inline-block; 84 | } 85 | 86 | div.menu:focus { 87 | pointer-events: none; 88 | outline: none; 89 | } 90 | 91 | div.menu:focus table.menu-content { 92 | opacity: 1; 93 | visibility: visible; 94 | pointer-events: auto; 95 | } 96 | 97 | table.menu-content { 98 | position: absolute; 99 | z-index: 1; 100 | opacity: 0; 101 | visibility: hidden; 102 | transition: visibility 0.5s; 103 | background-color: $theme.postbg; 104 | color: $theme.textcolor; 105 | border: 1px solid $theme.divborder; 106 | border-collapse: collapse; 107 | border-left: none; 108 | border-top: none; 109 | 110 | overflow: visible; 111 | display: inline-block; 112 | padding: 0px; 113 | margin: 0px; 114 | } 115 | 116 | tr.menu-entry { 117 | margin: 0px; 118 | border: 1px solid $theme.divborder; 119 | } 120 | 121 | tr.menu-entry:hover { 122 | color: $theme.textcolor; 123 | background-color: $theme.bgcolor; 124 | } 125 | 126 | a:hover { 127 | color: $theme.linkhilight; 128 | text-decoration: underline; 129 | } 130 | 131 | hr { 132 | clear: both; 133 | border: none; 134 | border-top: 1px solid $theme.divborder; 135 | height: 0; 136 | margin: 15px 15px 15px 15px; 137 | } 138 | 139 | /* Chrome, Safari, Opera */ 140 | @-webkit-keyframes banneranim { 141 | 0% {background-color:$theme.textcolor;} 142 | 25% {background-color:$theme.gradient_top;} 143 | 50% {background-color:$theme.bgcolor;} 144 | 75% {background-color:$theme.poster;} 145 | 100% {background-color:$theme.link;} 146 | } 147 | 148 | /* Standard syntax */ 149 | @keyframes banneranim { 150 | 0% {background-color:$theme.textcolor;} 151 | 25% {background-color:$theme.gradient_top;} 152 | 50% {background-color:$theme.bgcolor;} 153 | 75% {background-color:$theme.poster;} 154 | 100% {background-color:$theme.link;} 155 | } 156 | 157 | img.banner { 158 | width: 300px; 159 | height: auto; 160 | padding: 10px; 161 | margin-top: 25px; 162 | border: 1px solid $theme.textcolor; 163 | background-color: $theme.textcolor; 164 | 165 | /* Chrome, Safari, Opera */ 166 | -webkit-animation-name: banneranim; 167 | -webkit-animation-duration: 5s; 168 | -webkit-animation-timing-function: linear; 169 | -webkit-animation-delay: 2s; 170 | -webkit-animation-iteration-count: infinite; 171 | -webkit-animation-direction: alternate; 172 | 173 | /* Standard syntax */ 174 | animation-name: banneranim; 175 | animation-duration: 5s; 176 | animation-timing-function: linear; 177 | animation-delay: 2s; 178 | animation-iteration-count: infinite; 179 | animation-direction: alternate; 180 | border-radius: 10px; 181 | } 182 | 183 | div.banner_popup { 184 | position: fixed; 185 | top: 0; 186 | bottom: 0; 187 | left: 0; 188 | right: 0; 189 | width: 100%; 190 | height: 100%; 191 | z-index: 99; 192 | background: rgba(0, 0, 0, 0.7); 193 | transition: opacity 500ms; 194 | visibility: hidden; 195 | opacity: 0; 196 | } 197 | 198 | a.banner_popup:target + div.banner_popup { 199 | visibility: visible; 200 | opacity: 1; 201 | } 202 | 203 | h1.alert { 204 | font-family: Tahoma,sans-serif; 205 | font-size: 28px; 206 | letter-spacing: -1px; 207 | margin-top: 200px; 208 | margin-bottom: 300px; 209 | color: red; 210 | } 211 | 212 | body.board { 213 | background-color: $theme.bgcolor; 214 | } 215 | 216 | div.chans { 217 | margin: 10px 10px 10px 10px; 218 | color: $theme.themedtext; 219 | } 220 | 221 | table.form { 222 | box-shadow: 8px 8px 8px #999; 223 | border: 1px solid black; 224 | } 225 | 226 | td.form { 227 | background-color: $theme.formbg; 228 | border: 1px solid #000; 229 | padding: 0 5px; 230 | font-size: 11pt; 231 | } 232 | 233 | textarea.form { 234 | min-width: 325px; 235 | min-height: 125px; 236 | border: 1px solid gray; 237 | padding: 3px; 238 | } 239 | 240 | input.form { 241 | min-width: 327px; 242 | min-height: 20px; 243 | border: 1px solid gray; 244 | } 245 | 246 | input.postbutton { 247 | float: right; 248 | height: 30px; 249 | width: 55px; 250 | } 251 | 252 | a.closebutton { 253 | float: right; 254 | font-size: 40px; 255 | color: $theme.postbg; 256 | background-color: $theme.formbg; 257 | margin: 4px 4px 4px 4px; 258 | padding: 0px 6px 0px 6px; 259 | border-radius: 3px; 260 | border: 3px solid #000000; 261 | } 262 | 263 | a.closebutton:hover { 264 | text-decoration: none; 265 | } 266 | 267 | div.thread { 268 | overflow: hidden; 269 | padding: 10px 10px 10px 10px; 270 | } 271 | 272 | span.subject { 273 | color: $theme.linkhilight; 274 | width: 50px; 275 | white-space: nowrap; 276 | overflow: hidden; 277 | text-overflow: ellipsis; 278 | } 279 | 280 | span.poster { 281 | color: $theme.poster; 282 | } 283 | 284 | img.opimg { 285 | float: left; 286 | margin: 1px 10px 10px 10px; 287 | width: 250px; 288 | height: auto; 289 | } 290 | 291 | img.postimg { 292 | float: left; 293 | width: 125px; 294 | margin-right: 6px; 295 | height: auto; 296 | } 297 | 298 | div.plaque { 299 | background-color: $theme.postbg; 300 | color: $theme.textcolor; 301 | border: 1px solid $theme.divborder; 302 | border-left: none; 303 | border-top: none; 304 | display: inline-block; 305 | } 306 | 307 | div.post { 308 | -webkit-box-sizing: border-box; 309 | -moz-box-sizing: border-box; 310 | box-sizing: border-box; 311 | 312 | max-width: 100%; 313 | clear: left; 314 | overflow: visible; 315 | margin: 2px 1px 1px 1px; 316 | padding: 5px; 317 | } 318 | 319 | div.post:target { 320 | background-color: $theme.posthilight; 321 | } 322 | 323 | a.reference { 324 | text-decoration: underline; 325 | font-size: 80%; 326 | } 327 | 328 | blockquote.post { 329 | margin: 10px 10px 10px 10px; 330 | } 331 | 332 | p.post { 333 | word-wrap: break-word; 334 | } 335 | 336 | span.expand { 337 | color: $theme.greytext; 338 | } 339 | 340 | div.pages { 341 | overflow: auto; 342 | margin: 10px 10px 10px 10px; 343 | padding: 10px; 344 | } 345 | 346 | a.next { 347 | padding: 5px; 348 | } 349 | 350 | a.reply_popup:target + div.reply_popup { 351 | display: block; 352 | } 353 | 354 | div.reply_popup { 355 | display: none; 356 | position: fixed; 357 | z-index: 4; 358 | background-color: $theme.postbg; 359 | right: 10%; 360 | top: 10%; 361 | 362 | border: 1px solid; 363 | -webkit-box-sizing : border-box; 364 | -moz-box-sizing: border-box; 365 | -ms-box-sizing: border-box; 366 | box-sizing: border-box; 367 | transition: all 5s ease-in-out; 368 | } 369 | 370 | div.join_popup { 371 | position: fixed; 372 | top: 0; 373 | bottom: 0; 374 | left: 0; 375 | right: 0; 376 | z-index: 99; 377 | background: rgba(0, 0, 0, 0.7); 378 | transition: opacity 500ms; 379 | visibility: hidden; 380 | opacity: 0; 381 | } 382 | 383 | a.join_popup:target + div.join_popup { 384 | visibility: visible; 385 | opacity: 1; 386 | } 387 | 388 | table.join { 389 | position: relative; 390 | top: 50px; 391 | border-radius: 5px; 392 | background-color: $theme.postbg; 393 | transform: translateY(50%); 394 | margin: 0px auto; 395 | } 396 | 397 | input.passphrase { 398 | height: 30px; 399 | width: 75%; 400 | } 401 | 402 | input.join { 403 | float: center; 404 | height: 30px; 405 | width: 55px; 406 | align: center; 407 | margin: 15px; 408 | } 409 | 410 | div.intro { 411 | position: relative; 412 | border-radius: 5px; 413 | background-color: $theme.postbg; 414 | border: 1px solid $theme.divborder; 415 | margin: 5px auto; 416 | background-color: $theme.postbg; 417 | width: 66%; 418 | font-family: Tahoma,sans-serif; 419 | padding: 10px 25px 25px 25px; 420 | } 421 | 422 | h1.error { 423 | font-family: Tahoma,sans-serif; 424 | font-size: 20px; 425 | letter-spacing: -1px; 426 | margin-top: 0px; 427 | color: $theme.textcolor; 428 | } 429 | 430 | div.themes { 431 | position: relative; 432 | top: 50px; 433 | border-radius: 5px; 434 | background-color: $theme.postbg; 435 | transform: translateY(50%); 436 | margin: 10px auto; 437 | display: inline-block; 438 | overflow: auto; 439 | } 440 | 441 | $for t in themes: 442 | input.theme_$:t { 443 | position: relative; 444 | padding-top: 45px; 445 | display: block; 446 | width: 100%; 447 | height: 100%; 448 | overflow: auto; 449 | font-size: 50px; 450 | color: $themes[t].formbg; 451 | background: none; 452 | border: none; 453 | } 454 | 455 | div.theme_$:t { 456 | border-radius: 5px; 457 | border: 1px solid $themes[t].divborder; 458 | font-family: Tahoma,sans-serif; 459 | display: inline-block; 460 | overflow: hidden; 461 | width: 200px; 462 | height: 150px; 463 | 464 | background: $themes[t].bgcolor; 465 | background: -webkit-linear-gradient($themes[t].gradient_top, $themes[t].bgcolor); 466 | background: -o-linear-gradient($themes[t].gradient_top, $themes[t].bgcolor); 467 | background: -moz-linear-gradient($themes[t].gradient_top, $themes[t].bgcolor); 468 | background: linear-gradient($themes[t].gradient_top, $themes[t].bgcolor); 469 | z-index: -1; 470 | } 471 | 472 | div.catalog { 473 | -moz-column-width: 500px; 474 | -webkit-column-width: 500px; 475 | column-width: 500px; 476 | } 477 | 478 | div.catalogitem { 479 | box-shadow: 8px 8px 8px #999; 480 | border: 1px solid black; 481 | border-radius: 3px; 482 | margin: 15px 15px 15px 15px; 483 | -webkit-column-break-inside: avoid; 484 | } 485 | -------------------------------------------------------------------------------- /templates/elements/footer.html: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 | $ chan = None 4 | $if 'current_chan' in model: 5 | $ chan = model['current_chan'] 6 | 7 | $ threadcount = bitmessage.getThreadCount(chan) 8 | 9 | $if threadcount == 0 or not 'current_page' in model: 10 | $ pages = [] 11 | $else: 12 | $ pages = range(1, int(math.ceil(threadcount / config.threads_per_page) + 1)) 13 | 14 | $ currentpage = 1 15 | $if 'current_page' in model: 16 | $ currentpage = int(model['current_page']) 17 | 18 | $if len(pages) > 1: 19 |
20 | 21 | $if currentpage > 1: 22 | 23 | $for page in pages: 24 | $ page_class = "link" 25 | $if page == currentpage: 26 | $ page_class += " bold" 27 | [$:page]  28 | $if currentpage < len(pages): 29 | 30 |
31 | -------------------------------------------------------------------------------- /templates/elements/head.html: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 | 4 | 5 | bitboard 6 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /templates/elements/index/intro.html: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 |
4 |

5 |

Welcome to bitboard.

6 |
7 | Bitboard is a decentralized anonymous imageboard. It is built on top of 8 | Bitmessage's decentralized mailing lists. 9 |

10 | To get started, you must 11 | download 12 | and setup Bitmessage then 13 | enable API access. 14 | Bitmessage must be running in order for Bitboard to work. 15 |

16 | If you've set it up correctly, you should see some links below to join new channels, if not, 17 | you'll likely see an unfriendly error message. Lists of popular channels can be found 18 | on the web. 19 |

20 |
21 | -------------------------------------------------------------------------------- /templates/elements/index/start.html: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 |
4 |
Join Chan ]
5 | $:render.elements.chans() 6 |
7 | -------------------------------------------------------------------------------- /templates/elements/join_popup.html: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 | $ chan = None 4 | $if 'current_chan' in model: 5 | $ chan = model['current_chan'] 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 |
X

17 | Enter the passphrase of the chan you would like to join.
18 | Lists of popular channels are available 19 | on the web. 20 |

24 |
25 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /templates/elements/status.html: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 | $ status_title = model['status_title'] 4 | $ status_message = model['status_message'] 5 | 6 |
7 |

$:status_title

8 |
9 |
$:status_message
10 |
11 | -------------------------------------------------------------------------------- /templates/elements/themes_popup.html: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /templates/elements/title.html: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 | $ chan = model['current_chan'] 4 | $ channame = bitmessage.getChanName(chan) 5 | 6 |
7 |
$:channame
8 |
9 | -------------------------------------------------------------------------------- /templates/pages/alert.html: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 | $:render.elements.banner() 4 |
5 | 6 | $:render.elements.title() 7 | 8 |

9 | $:render.elements.status() 10 |

11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /templates/pages/board.html: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 | $:render.elements.chans() 4 | 5 | $:render.elements.banner() 6 |
7 | 8 | $:render.elements.title() 9 | 10 | $ threads = bitmessage.getChanThreads(model['current_chan'], model['current_page']) 11 | 12 |
13 |
$:render.elements.board.postform()
14 |
15 | 16 | $for i in range(0, len(threads)): 17 |
18 | $ thread = threads[i] 19 | $:render.elements.board.post_op(thread.posts[0], thread) 20 | $if len(thread.posts) > 6: 21 |
22 | 23 |
24 | $:len(thread.posts[1:-5]) posts truncated. 25 | Click here 26 | to view. 27 |
28 | $for post in thread.posts[-5:]: 29 |
30 | $:render.elements.board.post_reply(post, thread) 31 | $else: 32 | $for post in thread.posts[1:]: 33 | $:render.elements.board.post_reply(post, thread) 34 |
35 |
36 |
37 | 38 | $:render.elements.board.reply_popup() 39 | 40 | $:render.elements.footer() 41 | 42 | $:render.elements.chans() 43 | -------------------------------------------------------------------------------- /templates/pages/index.html: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 | $ apistatus = bitmessage.getAPIStatus() 4 | 5 | $:render.elements.banner() 6 | 7 |
8 | $:render.elements.index.intro() 9 | 10 | $if apistatus != True: 11 | $:render.elements.status() 12 | $else: 13 | $:render.elements.index.start() 14 | -------------------------------------------------------------------------------- /templates/pages/thread.html: -------------------------------------------------------------------------------- 1 | $def with () 2 | 3 | 4 | $:render.elements.chans() 5 | 6 | $:render.elements.banner() 7 |
8 | 9 | $:render.elements.title() 10 | 11 | $ thread = bitmessage.getChanThread(model['current_chan'], model['current_thread']) 12 | 13 |
14 |
$:render.elements.board.postform()
15 |
16 | 17 |
18 | $if len(thread.posts) > 0: 19 | $:render.elements.board.post_op(thread.posts[0], thread) 20 | 21 | $for post in thread.posts[1:]: 22 | $:render.elements.board.post_reply(post, thread) 23 |
24 |
25 |
26 | 27 | $:render.elements.board.reply_popup() 28 | 29 | $:render.elements.chans() -------------------------------------------------------------------------------- /themes.py: -------------------------------------------------------------------------------- 1 | class DarkTheme: 2 | def __init__(self): 3 | self.gradient_top = "#000000" 4 | self.bgcolor = "#333333" 5 | self.textcolor = "#DDDDDD" 6 | self.divborder = "#000000" 7 | self.poster = "#888888" 8 | self.postbg = "#444444" 9 | self.posthilight = "#222222" 10 | self.link = "#ADD8E6" 11 | self.linkhilight = "#AAAAAA" 12 | self.themedtext = "#BC8863" 13 | self.subject = "#AAAAAA" 14 | self.formbg = "#444444" 15 | self.greentext = "#789922" 16 | self.greytext = "#707070" 17 | 18 | 19 | class ClassicTheme: 20 | def __init__(self): 21 | self.gradient_top = "#FFD6AC" 22 | self.bgcolor = "#FFFFED" 23 | self.textcolor = "#820000" 24 | self.divborder = "#D9BFB7" 25 | self.poster = "#047841" 26 | self.postbg = "#F0E0D6" 27 | self.posthilight = "#F1C0AF" 28 | self.link = "#000082" 29 | self.linkhilight = "#CE0B00" 30 | self.themedtext = "#BC8863" 31 | self.subject = "#CE0B00" 32 | self.formbg = "#EA8" 33 | self.greentext = "#789922" 34 | self.greytext = "#707070" 35 | 36 | 37 | class BlueTheme: 38 | def __init__(self): 39 | self.gradient_top = "#D1D5EF" 40 | self.bgcolor = "#EEF2FF" 41 | self.textcolor = "#000000" 42 | self.divborder = "#B7C5DA" 43 | self.poster = "#047841" 44 | self.postbg = "#D6DAF1" 45 | self.posthilight = "#D7C9D1" 46 | self.link = "#2F2C9C" 47 | self.linkhilight = "#0E065F" 48 | self.themedtext = "#BC8863" 49 | self.subject = "#0E065F" 50 | self.formbg = "#9985F1" 51 | self.greentext = "#789922" 52 | self.greytext = "#707070" 53 | 54 | 55 | themes = {"Dark": DarkTheme(), "Classic": ClassicTheme(), "Frosty": BlueTheme()} 56 | --------------------------------------------------------------------------------