Compare commits

..

4 Commits

Author SHA1 Message Date
khuxkm 9488ade7fb Fix config.json.example 2019-10-08 14:31:59 +00:00
Ben Harris ee85dfe5ab fix up if's in setup.sh 2019-03-03 17:23:55 -05:00
Ben Harris 5c708b436b tildeverse 2019-02-26 10:51:20 -05:00
Ben Harris 68253447b7 Set theme jekyll-theme-midnight 2018-11-09 12:17:09 -05:00
5 changed files with 182 additions and 238 deletions

View File

@ -1,32 +0,0 @@
import zipfile
import glob
import os
# TODO: should we include .pyc files?
# TODO: add urwid source into the repo somewhere
files = {
'__main__.py': 'clients/urwid/main.py',
'network.py': 'clients/network_client.py',
'urwid': 'env/lib/python3.8/site-packages/urwid/*.py',
}
with open('bbj_demo', 'wb') as f:
f.write(b"#!/usr/bin/env python3\n")
with zipfile.ZipFile(f, 'w', compression=zipfile.ZIP_DEFLATED) as z:
z.comment = b'BBJ'
for name, source in files.items():
if '*' in source:
dirname = name
for path in sorted(glob.glob(source)):
name = dirname + '/' + os.path.basename(path)
z.write(path, name)
else:
z.write(source, name)
try:
mask = os.umask(0)
os.umask(mask)
except OSError:
mask = 0
os.chmod(z.filename, 0o777&~mask)

View File

@ -29,18 +29,27 @@ from getpass import getpass
from subprocess import call
from random import choice
from code import interact
from sys import exit
import rlcompleter
import readline
import tempfile
import shlex
import urwid
import json
import os
import re
help_text = """\
BBJ Urwid Client
# XxX_N0_4rgP4rs3_XxX ###yoloswag
def get_arg(key, default=None, get_value=True):
try:
spec = argv.index("--" + key)
value = argv[spec + 1] if get_value else True
except ValueError: # --key not specified
value = default
except IndexError: # flag given but no value
exit("invalid format for --" + key)
return value
if get_arg("help", False, False):
print("""BBJ Urwid Client
Available options:
--help: this message
--https: enable use of https, requires host support
@ -53,7 +62,16 @@ Available environment variables:
BBJ_PASSWORD: set your password to log in automatically.
if the password is wrong, will prompt you as normal.
Please note that these environment variables need to be exported, and are
visible to all other programs run from your shell."""
visible to all other programs run from your shell.""")
exit()
try:
network = BBJ(get_arg("host", "127.0.0.1"),
get_arg("port", 7099),
get_arg("https", False, False))
except URLError as e:
# print the connection error in red
exit("\033[0;31m%s\033[0m" % repr(e))
obnoxious_logo = """
% _ * ! *
@ -71,7 +89,7 @@ obnoxious_logo = """
welcome = """>>> Welcome to Bulletin Butter & Jelly! ------------------@
| BBJ is a persistent, chronologically ordered text |
| discussion board for tilde.town. You may log in, |
| discussion board for tildes. You may log in, |
| register as a new user, or participate anonymously. |
|---------------------------------------------------------|
| \033[1;31mTo go anon, just press enter. Otherwise, give me a name\033[0m |
@ -310,8 +328,7 @@ markpath = os.path.join(os.getenv("HOME"), ".bbjmarks")
pinpath = os.path.join(os.getenv("HOME"), ".bbjpins")
class App(object):
def __init__(self, network):
self.network = network
def __init__(self):
self.prefs = bbjrc("load")
self.client_pinned_threads = load_client_pins()
self.usermap = {}
@ -370,7 +387,7 @@ class App(object):
bar formatting to it.
"""
header = ("{}@bbj | " + text).format(
(self.network.user_name or "anonymous"),
(network.user_name or "anonymous"),
*format_specs
)
self.loop.widget.header = urwid.AttrMap(urwid.Text(header), "bar")
@ -576,7 +593,7 @@ class App(object):
# first we need to get the server's version of the message
# instead of our formatted one
try:
message = self.network.edit_query(thread_id, post_id)
message = network.edit_query(thread_id, post_id)
except UserWarning as e:
self.remove_overlays()
return self.temp_footer_message(e.description)
@ -599,7 +616,7 @@ class App(object):
urwid.Text(("bold", "Delete this %s?" % ("whole thread" if op else "post"))),
urwid.Divider(),
cute_button(("10" , ">> Yes"), lambda _: [
self.network.message_delete(message["thread_id"], message["post_id"]),
network.message_delete(message["thread_id"], message["post_id"]),
self.remove_overlays(),
self.index() if op else self.refresh()
]),
@ -621,7 +638,7 @@ class App(object):
def toggle_formatting(self, button, message):
self.remove_overlays()
raw = not message["send_raw"]
self.network.set_post_raw(message["thread_id"], message["post_id"], raw)
network.set_post_raw(message["thread_id"], message["post_id"], raw)
return self.refresh()
@ -638,7 +655,7 @@ class App(object):
"View %sQuote" % ("a " if len(quotes) != 1 else ""),
self.quote_view_menu, quotes))
if self.network.can_edit(message["thread_id"], message["post_id"]) \
if network.can_edit(message["thread_id"], message["post_id"]) \
and not self.window_split:
if message["post_id"] == 0:
@ -651,7 +668,7 @@ class App(object):
"Enable Formatting" if raw else "Disable Formatting",
self.toggle_formatting, message))
buttons.insert(0, urwid.Button("Edit Post", self.edit_post, message))
if self.network.user["is_admin"]:
if network.user["is_admin"]:
buttons.insert(0, urwid.Text(("20", "Reminder: You're an admin!")))
if not buttons:
@ -793,7 +810,7 @@ class App(object):
"""
if self.mode == "thread":
# mark the current position in this thread before going back to the index
self.mark()
mark()
self.body.attr_map = {None: "default"}
self.mode = "index"
@ -804,7 +821,7 @@ class App(object):
# narrowed selection of content, so we dont want to resume last_index_pos
self.last_index_pos = False
else:
threads, usermap = self.network.thread_index()
threads, usermap = network.thread_index()
self.usermap.update(usermap)
self.walker.clear()
@ -850,7 +867,7 @@ class App(object):
self.body.attr_map = {None: "default"}
self.mode = "thread"
thread, usermap = self.network.thread_load(thread_id, format="sequential")
thread, usermap = network.thread_load(thread_id, format="sequential")
self.usermap.update(usermap)
self.thread = thread
self.match_data["matches"].clear()
@ -859,7 +876,7 @@ class App(object):
self.walker += self.make_message_body(message)
self.set_default_header()
self.set_default_footer()
self.goto_mark(thread_id)
self.goto_post(mark(thread_id))
def toggle_client_pin(self):
@ -871,16 +888,16 @@ class App(object):
def toggle_server_pin(self):
if self.mode != "index" or not self.network.user["is_admin"]:
if self.mode != "index" or not network.user["is_admin"]:
return
thread = self.walker.get_focus()[0].thread
self.network.thread_set_pin(thread["thread_id"], not thread["pinned"])
network.thread_set_pin(thread["thread_id"], not thread["pinned"])
self.index()
def search_index_callback(self, query):
simple_query = query.lower().strip()
threads, usermap = self.network.thread_index()
threads, usermap = network.thread_index()
self.usermap.update(usermap)
results = [
thread for thread in threads
@ -983,10 +1000,10 @@ class App(object):
self.last_index_pos = self.get_focus_post(True).thread["thread_id"]
self.index()
else:
self.mark()
mark()
thread = self.thread["thread_id"]
self.thread_load(None, thread)
self.goto_mark(thread)
self.goto_post(mark(thread))
self.temp_footer_message("Refreshed content!", 1)
@ -1021,31 +1038,10 @@ class App(object):
width=30, height=6)
else:
self.mark()
mark()
self.index()
def mark(self, thread_id=None):
if self.mode != "thread":
return
if thread_id is None:
thread_id = self.thread['thread_id']
pos = self.get_focus_post()
mark(thread_id, pos, default=0)
return pos
def goto_mark(self, thread_id=None):
if self.mode != "thread":
return
if thread_id is None:
thread_id = self.thread['thread_id']
pos = mark(thread_id, default=0)
self.goto_post(pos)
def get_focus_post(self, return_widget=False):
pos = self.box.get_focus_path()[0]
if self.mode == "thread":
@ -1176,10 +1172,10 @@ class App(object):
"""
self.loop.widget = self.loop.widget[0]
self.loop.stop()
call("clear")
call("clear", shell=True)
print(welcome)
try:
log_in(self.network)
log_in(relog=True)
except (KeyboardInterrupt, InterruptedError):
pass
self.loop.start()
@ -1192,9 +1188,8 @@ class App(object):
Options menu callback to anonymize the user and
then redisplay the options menu.
"""
self.network.user_name = None
self.network.user_auth = None
self.network.user = self.network.request("get_me")["data"]
network.user_name = network.user_auth = None
network.user = network("get_me")["data"]
self.loop.widget = self.loop.widget[0]
self.set_default_header()
self.options_menu()
@ -1210,7 +1205,7 @@ class App(object):
urwid.ListBox(
urwid.SimpleFocusListWalker([
urwid_rainbows(
"This is BBJ, a client/server textboard made for tilde.town!",
"This is BBJ, a client/server textboard made for tildes!",
True),
urwid.Text(("dim", "...by ~desvox")),
urwid.Divider(self.theme["divider"]),
@ -1236,7 +1231,7 @@ class App(object):
"""
# we can "recycle" the server's formatting abilities to
# use the same syntax for the help text itself
message = self.network.fake_message(
message = network.fake_message(
"\n\n".join(format_help), format="sequential")
widget = OptionsMenu(
@ -1258,7 +1253,7 @@ class App(object):
def set_color(self, button, value, color):
if value == False:
return
self.network.user_update(color=color)
network.user_update(color=color)
def toggle_exit(self, button, value):
@ -1285,10 +1280,10 @@ class App(object):
def change_username(self, *_):
self.loop.stop()
call("clear")
call("clear", shell=True)
try:
name = nameloop(self.network, "Choose a new username")
self.network.user_update(user_name=name)
name = nameloop("Choose a new username", True)
network.user_update(user_name=name)
motherfucking_rainbows("~~hello there %s~~" % name)
sleep(0.8)
self.loop.start()
@ -1301,10 +1296,10 @@ class App(object):
def change_password(self, *_):
self.loop.stop()
call("clear")
call("clear", shell=True)
try:
password = password_loop("Choose a new password. Can be empty")
self.network.user_update(auth_hash=self.network._hash(password))
password = password_loop("Choose a new password. Can be empty", True)
network.user_update(auth_hash=network._hash(password))
motherfucking_rainbows("SET NEW PASSWORD")
sleep(0.8)
self.loop.start()
@ -1400,8 +1395,8 @@ class App(object):
editor_buttons = []
edit_mode = []
if self.network.user_auth:
account_message = "Logged in as %s." % self.network.user_name
if network.user_auth:
account_message = "Logged in as %s." % network.user_name
account_stuff = [
urwid.Button("Relog", on_press=self.relog),
urwid.Button("Go anonymous", on_press=self.unlog),
@ -1418,7 +1413,7 @@ class App(object):
for index, color in enumerate(colornames):
urwid.RadioButton(
user_colors, color.title(),
self.network.user["color"] == index,
network.user["color"] == index,
self.set_color, index)
for item in user_colors:
@ -1623,9 +1618,7 @@ class App(object):
descriptor, path = tempfile.mkstemp()
with open(path, "w") as _:
_.write(init_body)
env = os.environ.copy()
env['LANG'] = 'en_US.UTF-8'
call("%s %s" % (self.prefs["editor"], shlex.quote(path)), env=env, shell=True)
call("export LANG=en_US.UTF-8; %s %s" % (self.prefs["editor"], path), shell=True)
with open(path) as _:
body = _.read()
os.remove(path)
@ -1642,7 +1635,7 @@ class App(object):
return self.footer_prompt("Title", self.compose)
elif title:
try: self.network.validate("title", title)
try: network.validate("title", title)
except AssertionError as e:
return self.footer_prompt(
"Title", self.compose, extra_text=e.description)
@ -1651,19 +1644,24 @@ class App(object):
body = self.overthrow_ext_edit(init_body)
if not body or re.search("^>>[0-9]+$", body):
return self.temp_footer_message("EMPTY POST DISCARDED")
params = {"body": body}
if self.mode == "thread" and not edit:
self.network.thread_reply(self.thread["thread_id"], body)
endpoint = "thread_reply"
params.update({"thread_id": self.thread["thread_id"]})
elif edit:
self.network.edit_message(
thread_id=self.thread["thread_id"],
post_id=edit["post_id"],
body=body)
endpoint = "edit_post"
params.update({
"thread_id": self.thread["thread_id"],
"post_id": edit["post_id"]
})
else:
self.network.thread_create(title, body)
endpoint = "thread_create"
params.update({"title": title})
network.request(endpoint, **params)
self.refresh()
if edit:
self.goto_post(edit["post_id"])
@ -1716,12 +1714,6 @@ class App(object):
self.window_split=True
self.switch_editor()
def repaint_screen(self):
"""
Force urwid to repaint the whole screen.
"""
self.loop.screen.clear()
class MessageBody(urwid.Text):
"""
@ -1778,7 +1770,7 @@ class MessageBody(urwid.Text):
if _c != 0:
color = str(_c)
if user != "anonymous" and user["user_name"] == app.network.user_name:
if user != "anonymous" and user["user_name"] == network.user_name:
display = "[You]"
# bold it
color += "0"
@ -1986,7 +1978,7 @@ class ExternalEditor(urwid.Terminal):
# should use the options menu to switch to Overthrow mode.
env.update({"LANG": "POSIX"})
command = ["bash", "-c", "{} {}; echo Press any key to kill this window...".format(
app.prefs["editor"], shlex.quote(self.path))]
app.prefs["editor"], self.path)]
super(ExternalEditor, self).__init__(command, env, app.loop, app.prefs["edit_escapes"]["abort"])
urwid.connect_signal(self, "closed", self.exterminate)
@ -1995,7 +1987,7 @@ class ExternalEditor(urwid.Terminal):
# app.loop.widget = app.loop.widget[0]
# if not value:
# app.loop.stop()
# call("clear")
# call("clear", shell=True)
# print(welcome)
# log_in(True)
# app.loop.start()
@ -2003,21 +1995,9 @@ class ExternalEditor(urwid.Terminal):
def exterminate(self, *_, anon_confirmed=False):
# close the editor and grab the post body
app.close_editor()
with open(self.path) as _:
body = _.read().strip()
os.remove(self.path)
# make sure its not empty
if not body or re.search("^>>[0-9]+$", body):
app.temp_footer_message("EMPTY POST DISCARDED")
return
# are we anonymous? check if the user wants to log in first
if app.prefs["confirm_anon"] \
and not anon_confirmed \
and app.network.user["user_name"] == "anonymous":
and network.user["user_name"] == "anonymous":
# TODO fixoverlay: urwid terminal widgets have been mucking
# up overlay dialogs since the wee days of bbj, i really
# need to find a real solution instead of dodging the issue
@ -2044,7 +2024,7 @@ class ExternalEditor(urwid.Terminal):
# return
# else:
app.loop.stop()
call("clear")
call("clear", shell=True)
print(anon_warn)
choice = paren_prompt(
"Post anonymously?", default="yes", choices=["Yes", "no"]
@ -2053,20 +2033,27 @@ class ExternalEditor(urwid.Terminal):
log_in(True)
app.loop.start()
# ok; do the post
self.params.update({"body": body})
app.network.request(self.endpoint, **self.params)
if self.endpoint == "edit_post":
app.refresh()
app.goto_post(self.params["post_id"])
app.close_editor()
with open(self.path) as _:
body = _.read().strip()
os.remove(self.path)
elif app.mode == "thread":
app.refresh()
app.goto_post(app.thread["reply_count"])
if body and not re.search("^>>[0-9]+$", body):
self.params.update({"body": body})
network.request(self.endpoint, **self.params)
if self.endpoint == "edit_post":
app.refresh()
app.goto_post(self.params["post_id"])
elif app.mode == "thread":
app.refresh()
app.goto_post(app.thread["reply_count"])
else:
app.last_pos = None
app.index()
else:
app.last_pos = None
app.index()
app.temp_footer_message("EMPTY POST DISCARDED")
def keypress(self, size, key):
@ -2084,7 +2071,7 @@ class ExternalEditor(urwid.Terminal):
if keyl == "ctrl l":
# always do this, and also pass it to the terminal
app.repaint_screen()
wipe_screen()
elif key == app.prefs["edit_escapes"]["abort"]:
self.terminate()
@ -2162,7 +2149,7 @@ class OptionsMenu(urwid.LineBox):
return self.keypress(size, "enter")
elif keyl == "ctrl l":
app.repaint_screen()
wipe_screen()
def mouse_event(self, size, event, button, x, y, focus):
@ -2218,7 +2205,7 @@ class ActionBox(urwid.ListBox):
self.change_focus(size, 0)
elif key == "ctrl l":
app.repaint_screen()
wipe_screen()
elif keyl == "o":
app.options_menu()
@ -2265,13 +2252,13 @@ class ActionBox(urwid.ListBox):
elif key == "~":
# sssssshhhhhhhh
app.loop.stop()
try: call("sl")
try: call("sl", shell=True)
except: pass
app.loop.start()
elif keyl == "$":
app.loop.stop()
call("clear")
call("clear", shell=True)
readline.set_completer(rlcompleter.Completer().complete)
readline.parse_and_bind("tab: complete")
interact(banner="Python " + version + "\nBBJ Interactive Console\nCtrl-D exits.", local=globals())
@ -2319,7 +2306,7 @@ def frilly_exit():
out = " ~~CoMeE BaCkK SooOn~~ 0000000"
motherfucking_rainbows(out.zfill(width))
else:
call("clear")
call("clear", shell=True)
motherfucking_rainbows("Come back soon! <3")
exit()
@ -2398,57 +2385,47 @@ def paren_prompt(text, positive=True, choices=[], function=input, default=None):
return ""
def sane_value(prompt, validate, positive=True, function=input):
"""Prompts for an input with paren_prompt until validate(response)
returns None (or something falsy). Otherwise the returned string is used
as the new prompt.
"""
while 1:
response = paren_prompt(prompt, positive, function=function)
error = validate(response)
if not error:
return response
prompt = str(error) if error != None else ""
positive = False
def sane_value(key, prompt, positive=True, return_empty=False):
response = paren_prompt(prompt, positive)
if return_empty and response == "":
return response
try: network.validate(key, response)
except AssertionError as e:
return sane_value(key, e.description, False)
return response
def password_loop(prompt, positive=True):
while 1:
response1 = paren_prompt(prompt, positive, function=getpass)
if response1 == "":
confprompt = "Confirm empty password"
else:
confprompt = "Confirm it"
response2 = paren_prompt(confprompt, function=getpass)
if response1 != response2:
prompt = "Those didnt match. Try again"
positive = False
else:
return response1
response1 = paren_prompt(prompt, positive, function=getpass)
if response1 == "":
confprompt = "Confirm empty password"
else:
confprompt = "Confirm it"
response2 = paren_prompt(confprompt, function=getpass)
if response1 != response2:
return password_loop("Those didnt match. Try again", False)
return response1
def nameloop(network, prompt, positive=True):
def validate_name(name):
try: network.validate("user_name", name)
except AssertionError as e:
return e.description
if network.user_is_registered(name):
return "%s is already registered" % name
return sane_value(prompt, validate_name, positive)
def nameloop(prompt, positive):
name = sane_value("user_name", prompt, positive)
if network.user_is_registered(name):
return nameloop("%s is already registered" % name, False)
return name
def log_in(network, name="", password=""):
def log_in(relog=False):
"""
Handles login or registration. If name and/or password are not
provided, the user is prompted for them using an oldschool
input() chain. The user is run through this before starting the
Handles login or registration using an oldschool input()
chain. The user is run through this before starting the
curses app.
"""
if not name:
def validate_name(response):
if response != "": # allow empty username
try: network.validate("user_name", response)
except AssertionError as e:
return e.description
name = sane_value("Username", validate_name)
if relog:
name = sane_value("user_name", "Username", return_empty=True)
else:
name = get_arg("user") \
or os.getenv("BBJ_USER") \
or sane_value("user_name", "Username", return_empty=True)
if name == "":
motherfucking_rainbows("~~W3 4R3 4n0nYm0u5~~")
else:
@ -2457,18 +2434,21 @@ def log_in(network, name="", password=""):
try:
network.set_credentials(
name,
password if name and password else ""
os.getenv("BBJ_PASSWORD", default="")
if not relog else ""
)
# make it easy for people who use an empty password =)
motherfucking_rainbows("~~welcome back {}~~".format(network.user_name))
except ConnectionRefusedError:
def validate_creds(password):
def login_loop(prompt, positive):
try:
password = paren_prompt(prompt, positive, function=getpass)
network.set_credentials(name, password)
except ConnectionRefusedError:
return "// R E J E C T E D //."
password = sane_value("Enter your password", validate_creds, function=getpass)
login_loop("// R E J E C T E D //.", False)
login_loop("Enter your password", True)
motherfucking_rainbows("~~welcome back {}~~".format(network.user_name))
except ValueError:
@ -2479,7 +2459,7 @@ def log_in(network, name="", password=""):
)
if response == "c":
name = nameloop(network, "Pick a new name")
name = nameloop("Pick a new name", True)
elif response == "n":
raise InterruptedError
@ -2522,16 +2502,11 @@ def bbjrc(mode, **params):
return values
def mark(key, value=None, default=None):
def mark(directive=True):
"""
Sets a value in the markfile and returns the old value (or default).
Set and retrieve positional marks for threads.
This uses a seperate file from the preferences
to keep it free from clutter.
The key must be a string, and the value must be
json-encodable. If value isn't provided (or is None)
then this doesn't set anything and it is only a
read operation.
"""
try:
with open(markpath, "r") as _in:
@ -2539,14 +2514,19 @@ def mark(key, value=None, default=None):
except FileNotFoundError:
values = {}
old = values.get(key, default)
if value is not None and value != old:
values[key] = value
if directive == True and app.mode == "thread":
pos = app.get_focus_post()
values[app.thread["thread_id"]] = pos
with open(markpath, "w") as _out:
json.dump(values, _out)
return pos
elif isinstance(directive, str):
try:
return values[directive]
except KeyError:
return 0
return old
def load_client_pins():
"""
@ -2582,39 +2562,25 @@ def ignore(*_, **__):
pass
# XxX_N0_4rgP4rs3_XxX ###yoloswag
def get_arg(key, default=None, get_value=True):
try:
spec = argv.index("--" + key)
value = argv[spec + 1] if get_value else True
except ValueError: # --key not specified
value = default
except IndexError: # flag given but no value
exit("invalid format for --" + key)
return value
def wipe_screen(*_):
"""
A crude hack to repaint the whole screen. I didnt immediately
see anything to acheive this in the MainLoop methods so this
will do, I suppose.
"""
app.loop.stop()
call("clear", shell=True)
app.loop.start()
def main():
if get_arg("help", False, False):
print(help_text)
exit()
try:
network = BBJ(get_arg("host", "127.0.0.1"),
get_arg("port", 7099),
get_arg("https", False, False))
except URLError as e:
# print the connection error in red
exit("\033[0;31m%s\033[0m" % repr(e))
global app
app = App(network)
call("clear")
app = App()
call("clear", shell=True)
motherfucking_rainbows(obnoxious_logo)
print(welcome)
try:
name = get_arg("user") or os.getenv("BBJ_USER")
password = os.getenv("BBJ_PASSWORD", default="")
log_in(network, name, password)
log_in()
app.index()
app.loop.run()
except (InterruptedError, KeyboardInterrupt):

View File

@ -2,6 +2,6 @@
"port": 7099,
"host": "127.0.0.1",
"instance_name": "BBJ",
"allow_anon": True,
"debug": False
}
"allow_anon": true,
"debug": false
}

1
docs/_config.yml 100644
View File

@ -0,0 +1 @@
theme: jekyll-theme-midnight

View File

@ -15,20 +15,29 @@ It takes the following flags:
You can optionally pass a different python interpreter to use (such as
a virtual environment), with no arguments this will use the system python3
EOF
exit;;
--dbset )
sqlite3 data.sqlite < schema.sql
echo cleared
echo cleared
chmod 600 data.sqlite
exit;;
exit;;
esac
PYTHON=`which python3`
[[ -e logs ]] || mkdir logs; mkdir logs/exceptions
PYTHON=`which python3`
[[ -z $1 ]] || PYTHON=$1
echo Using $PYTHON...
$PYTHON -m pip install ${DEPS[*]}
echo "Enter [i] to initialize a new database"
read CLEAR
[[ $CLEAR == "i" ]] && sqlite3 data.sqlite < schema.sql; chmod 600 data.sqlite
if [[ $CLEAR == "i" ]]; then
sqlite3 data.sqlite < schema.sql
chmod 600 data.sqlite
fi