Just deleted about 400 bot accounts and a bunch of spam. To mitigate this in the future, we've enabled reCaptcha on signup. Let me know if someone got caught in the crossfire.

Commit 4612f3cd authored by Rob Nelson's avatar Rob Nelson
Browse files

Initial commit. Working.

parents
*.py[oic]
__pycache__
accounts.yml
jails
Discord
tmp
scripts
*.tgz
bddctl
.cache
src
node_modules
repos:
- hooks:
- entry: /usr/bin/python3.6 .selid-hooks/check-identity
id: select-identity+check-identity
language: system
name: Select Identity - check-identity
require_serial: true
repo: local
# Just the name of the jail. You can name this whatever you want, doesn't have
# to match up with the account name.
Account1:
id: Account1
addons:
# Bandaged Better Discord. Installs using bbdctl.
bbd: {}
# Background color of the tray icon. Used in the SVG.
color: '#0095B3'
# img/{imagepack}/ - Used for tray icons.
imagepack: default
Account2:
id: Account2
addons: {} # No BBD
color: '#00B300'
imagepack: default
import argparse, sys, os
sys.path.append(os.path.join(os.getcwd(),'lib'))
from buildtools import os_utils, log
from discordjail.cmd import register_parsers
def main():
argp = argparse.ArgumentParser(description='A tool for launching Discord processes in individual jails, for multikeying purposes.')
subp = argp.add_subparsers()
register_parsers(subp)
args = argp.parse_args()
args.cmd(args)
if __name__ == '__main__':
main()
from .bandagedbetterdiscord import BandagedBetterDiscordAddon
from .framework import Addons
import os, stat, re, json
from buildtools import os_utils, log
from discordjail.addons.framework import Addon, Addons
from discordjail.http import download_file_to
BDDCTL_URL = 'https://raw.githubusercontent.com/bb010g/betterdiscordctl/master/betterdiscordctl'
REG_VERSION = re.compile(r'[0-9]+\.[0-9]+\.[0-9]+')
BBD_PLUGIN_REPO = 'https://raw.githubusercontent.com/mwittrien/BetterDiscordAddons/master/Plugins/PluginRepo/PluginRepo.plugin.js'
BBD_THEME_REPO = 'https://raw.githubusercontent.com/mwittrien/BetterDiscordAddons/master/Plugins/ThemeRepo/ThemeRepo.plugin.js'
BBD_ICON_PLUGIN = 'https://raw.githubusercontent.com/KyzaGitHub/Khub/master/Plugins/CustomDiscordIcon/CustomDiscordIcon.plugin.js'
ENABLE_BY_DEFAULT = [
'PluginRepo',
'ThemeRepo',
'CustomDiscordIcon'
]
class BandagedBetterDiscordAddon(Addon):
TYPEID = 'bbd'
NAME = 'Bandaged Better Discord'
def __init__(self, discord: 'Discord'):
super().__init__(discord)
self.bbdctl_path: str = ''
self.discord_dir: str = ''
self.config_dir: str = ''
self.modules_dir: str = ''
self.plugins_dir: str = ''
self.themes_dir: str = ''
def deserialize(self, data: dict) -> None:
super().deserialize(data)
def preexec(self):
assert self.discord.homedir != ''
assert self.discord.discorddir != ''
assert self.discord.modulesdir != ''
self.bbdctl_path = os.path.join(self.discord.homedir, 'bddctl')
self.discord_dir = self.discord.discorddir
self.config_dir = os.path.join(self.discord.homedir, '.config', 'discord')
self.modules_dir = self.discord.modulesdir
self.plugins_dir = os.path.join(self.discord.homedir, '.config', 'BetterDiscord', 'plugins')
self.themes_dir = os.path.join(self.discord.homedir, '.config', 'BetterDiscord', 'themes')
os_utils.ensureDirExists(os.path.dirname(self.bbdctl_path), noisy=True)
download_file_to(BDDCTL_URL, self.bbdctl_path)
with log.info(f'Setting {self.bbdctl_path} +x (+S_IEXEC)...'):
st = os.stat(self.bbdctl_path)
os.chmod(self.bbdctl_path, st.st_mode|stat.S_IEXEC)
opts = ['-d', self.discord_dir]
opts += ['-m', self.modules_dir]
if os.path.isdir(os.path.join(self.modules_dir, 'discord_desktop_core', 'injector')):
with log.info('Injector not found, installing...'):
os_utils.cmd([self.bbdctl_path, 'install']+opts, show_output=True, echo=True, critical=False)
if not os_utils.cmd([self.bbdctl_path, 'update']+opts, show_output=True, echo=True, critical=False):
with log.info('Install failed, attempting reinstall...'):
os_utils.cmd([self.bbdctl_path, 'install']+opts, show_output=True, echo=True, critical=False)
os_utils.cmd([self.bbdctl_path, 'update']+opts, show_output=True, echo=True, critical=False)
os_utils.ensureDirExists(os.path.dirname(self.plugins_dir), noisy=True)
if not os.path.isdir(self.plugins_dir):
log.info(f'Creating {self.plugins_dir}...')
os.makedirs(self.plugins_dir)
os_utils.ensureDirExists(os.path.dirname(self.themes_dir), noisy=True)
if not os.path.isdir(self.themes_dir):
log.info(f'Creating {self.themes_dir}...')
os.makedirs(self.themes_dir)
download_file_to(BBD_PLUGIN_REPO, os.path.join(self.plugins_dir, 'PluginRepo.plugin.js'))
download_file_to(BBD_THEME_REPO, os.path.join(self.plugins_dir, 'ThemeRepo.plugin.js'))
download_file_to(BBD_ICON_PLUGIN, os.path.join(self.plugins_dir, 'CustomDiscordIcon.plugin.js'))
bdstorage = {
'settings': {
'stable': {
'plugins': {},
'themes': {},
}
}
}
bdstorage_filename = os.path.join(self.discord.homedir, '.config', 'BetterDiscord', 'bdstorage.json')
if os.path.isfile(bdstorage_filename):
with open(bdstorage_filename, 'r') as f:
bdstorage = json.load(f)
for plid in ENABLE_BY_DEFAULT:
bdstorage['settings']['stable']['plugins'][plid] = True
with open(bdstorage_filename, 'w') as f:
json.dump(bdstorage, f)
Addons.Register(BandagedBetterDiscordAddon)
class Addon(object):
TYPEID = ''
NAME = ''
def __init__(self, discord):
self.discord = discord
def serialize(self) -> dict:
return {
'id': self.TYPEID
}
def deserialize(self, data: dict) -> None:
return
def install(self):
return
def uninstall(self):
return
def preexec(self):
return
def postexec(self):
return
class Addons(object):
ALL = {}
@staticmethod
def Register(addon: Addon) -> None:
Addons.ALL[addon.TYPEID] = addon
print(f'Registered Addon {addon.TYPEID} - {addon.NAME}')
@staticmethod
def Initialize(typeid: str, discord, config: dict) -> Addon:
addon = Addons.ALL[typeid](discord)
addon.deserialize(config)
return addon
from .add import register_parsers__add
from .run import register_parsers__run
from .remove import register_parsers__remove
def register_parsers(subp):
register_parsers__add(subp)
register_parsers__remove(subp)
register_parsers__run(subp)
import yaml
from discordjail.discord import Discord
def register_parsers__add(subp):
addp = subp.add_parser('add', help='Add new jail')
addp.add_argument('id', type=str, help='ID for your new jail.')
addp.set_defaults(cmd=cmd_add)
def cmd_add(args) -> None:
data: dict = {}
with open('accounts.yml', 'r') as f:
data = yaml.safe_load(f)
d = Discord()
d.id = args.id
data[args.id] = d.serialize()
with open('accounts.yml', 'w') as f:
yaml.dump(data, f, default_flow_style=False)
import yaml
from buildtools import log
def register_parsers__remove(subp):
addp = subp.add_parser('remove', help='Remove a jail')
addp.add_argument('id', type=str, help='ID for the target jail.')
addp.set_defaults(cmd=cmd_remove)
def cmd_remove(args) -> None:
data: dict = {}
with open('accounts.yml', 'r') as f:
data = yaml.safe_load(f)
if args.id not in data:
log.warning('Jail %r does not exist.', args.id)
del data[args.id]
with open('accounts.yml', 'w') as f:
yaml.dump(data, f, default_flow_style=False)
import yaml
from buildtools import log
from discordjail.discord import Discord
def register_parsers__run(subp):
addp = subp.add_parser('run', help='Run Discord in a jail')
addp.add_argument('id', type=str, help='ID for your jail.')
addp.set_defaults(cmd=cmd_run)
def cmd_run(args) -> None:
data: dict = {}
with open('accounts.yml', 'r') as f:
data = yaml.safe_load(f)
if args.id not in data:
log.warning('Jail %r does not exist.', args.id)
d = Discord()
d.deserialize(data[args.id])
d.init_paths()
d.launch()
import os
NAME = 'discordjail'
VERSION = '0.0.1'
ROOT_DIR = os.path.dirname(os.path.dirname(__file__))
TMP_DIR = os.path.join(ROOT_DIR, 'tmp')
CACHE_DIR = os.path.join(ROOT_DIR, '.cache')
import os, shutil, stat, json, psutil, enum, requests, time, datetime, zipfile
import urllib.parse, sys
from buildtools import os_utils
from .consts import ROOT_DIR, TMP_DIR, CACHE_DIR
from .addons import Addons
from .http import download_file_to
from buildtools import log, http
from buildtools.twisted_utils import async_cmd
from asar import AsarFile
DISCORD_DL_API = "https://discordapp.com/api/download?platform=linux&format=tar.gz"
DISCORD_API_ENDPOINT = 'https://discordapp.com/api'
class ExitReason(enum.IntEnum):
NONE = enum.auto()
UPDATED = enum.auto()
class Discord(object):
TYPEID = ''
def __init__(self):
self.id: str = ''
self.version: str = ''
self.configdir: str = 'discordstable'
self.imagepack: str = 'default'
self.color: str = '#00CCFF'
self.addons = []
self.strings = []
self.jail: str = ''
self.homedir: str = ''
self.tmpdir: str = ''
self.discorddir: str = ''
self.binpath: str = ''
self.modulesdir: str = ''
self.installedjsonfile: str = ''
self.exit_reason: ExitReason = ExitReason.NONE
releaseChannel = 'stable'
self.remoteBaseURL = f'{DISCORD_API_ENDPOINT}/modules/{releaseChannel}'
self.remoteQuery = {}
self.icon_ids = ['tray', 'tray-unread']
self.asarfile: str = ''
self.coredir: str = ''
def init_paths(self):
self.jail = os.path.join(ROOT_DIR, 'jails', self.id)
self.homedir = os.path.join(self.jail, 'home')
self.tmpdir = os.path.join(self.jail, 'tmp')
self.discorddir = os.path.join(self.homedir, 'Discord')
self.binpath = os.path.join(self.discorddir, 'Discord')
def getRemoteModuleName(self, name: str) -> str:
if os_utils.is_windows():
return f'{name}.x64'
return name
def _replaceIcon(self, pakid, svgpath, pngpath) -> None:
with log.info('Patching %s...', pakid):
log.info('Patching SVG...')
with open(svgpath, 'r') as rf:
with open(os.path.join(self.tmpdir, 'icon.svg'), 'w') as wf:
for line in rf:
line = line.replace('#123456', self.color)
wf.write(line)
with log.info('Rendering SVG to PNG...'):
self.renderSVGToPNG(os.path.join(self.tmpdir, 'icon.svg'), pngpath)
assert os.path.isfile(self.asarfile)
with log.info('Patching %s[%s]...', self.asarfile, pakid):
asarfile = os.path.join(self.coredir, pakid)
os_utils.single_copy(pngpath, asarfile, verbose=True)
def mkTaskbarIcon(self) -> None:
self.asarfile = os.path.join(self.modulesdir, 'discord_desktop_core', 'core.asar')
self.coredir = os.path.join(self.tmpdir, 'core')
os_utils.cmd(['node_modules/.bin/asar', 'extract', self.asarfile, self.coredir], echo=True, show_output=True)
for iconid in self.icon_ids:
self._replaceIcon(f'app/images/systemtray/linux/{iconid}.png', os.path.join('img', self.imagepack, f'{iconid}.svg'), os.path.join(self.homedir, '.config', 'discord', f'{iconid}.png'))
os_utils.cmd(['node_modules/.bin/asar', 'pack', self.coredir, self.asarfile], echo=True, show_output=True)
def renderSVGToPNG(self, svgfile, pngfile, height=24, width=24) -> None:
os.remove(pngfile)
os_utils.cmd(['inkscape', '-z', '-e', pngfile, '-h', str(height), '-w', str(width), svgfile], critical=True, echo=True, show_output=False)
def updateModules(self) -> None:
with log.info('Checking for module updates...'):
url = f'{self.remoteBaseURL}/versions.json'
q = {
'_': time.mktime(datetime.datetime.utcnow().timetuple())/60/5 # Date.now()/1000/60/5
}
q.update(self.remoteQuery)
log.info('Sending GET to %s?%s', url, urllib.parse.urlencode(q))
req = requests.get(url, params=q, verify=False)
req.raise_for_status()
data = req.json()
with open(os.path.join(self.homedir, 'REMOTE_VERSIONS.json'), 'w') as f:
json.dump(data, f, indent=2)
localmanifest = {}
if os.path.isfile(self.installedjsonfile):
log.info('FOUND %s!', self.installedjsonfile)
with open(self.installedjsonfile, 'r') as f:
localmanifest = json.load(f)
else:
# Bootstrappage
log.warning("BOOTSTRAPPING! For new installs, this is normal. We're going to download all of Discord's modules for it so we can patch before running Discord.")
log.warning("This will take some time, and Discord will restart multiple times.")
with open(os.path.join(self.discorddir, 'resources', 'bootstrap', 'manifest.json'), 'r') as f:
bmf = json.load(f)
for modulename in bmf.keys():
localmanifest[modulename]={'installedVersion': 0}
for modname in localmanifest.keys():
remoteVersion = data.get(self.getRemoteModuleName(modname), 0)
installedVersion = localmanifest.get(modname, {}).get('installedVersion', 0)
updateVersion = localmanifest.get(modname, {}).get('updateVersion', 0)
#print(modname, installedVersion, remoteVersion, updateVersion)
if remoteVersion not in (installedVersion, updateVersion):
with log.info(f'Module {modname}@{remoteVersion} available! (Installed: {installedVersion})'):
self.downloadModule(localmanifest, modname, remoteVersion)
else:
log.info(f'Module {modname}@{remoteVersion} has no updates.')
self.updateInstalled(localmanifest)
#sys.exit(1)
def updateInstalled(self, localmanifest: dict) -> None:
with open(self.installedjsonfile, 'w') as f:
json.dump(localmanifest, f, indent=2)
def downloadModule(self, localmanifest: dict, modname: str, modversion: int) -> None:
url = f'{self.remoteBaseURL}/{self.getRemoteModuleName(modname)}/{modversion}'
log.info('Sending GET to %s?%s', url, urllib.parse.urlencode(self.remoteQuery))
os_utils.ensureDirExists(os.path.join(self.modulesdir, 'pending'))
zipfn = os.path.join(self.modulesdir, 'pending', f'{modname}-{modversion}.zip')
http.DownloadFile(url, zipfn, True, True, True, params=self.remoteQuery)
moduleMeta = localmanifest.get(modname, {'installedVersion': 0})
moduleMeta['updateVersion'] = modversion
moduleMeta['updateZipfile'] = zipfn
localmanifest[modname]=moduleMeta
self.updateInstalled(localmanifest)
with log.info(f'Installing {modname}@{modversion} from {zipfn}...'):
with zipfile.ZipFile(zipfn) as arch:
arch.extractall(os.path.join(self.modulesdir, modname))
localmanifest[modname]['installedVersion'] = modversion
del localmanifest[modname]['updateVersion']
del localmanifest[modname]['updateZipfile']
self.updateInstalled(localmanifest)
log.info('Cleaning up %s...', zipfn)
if os.path.isfile(zipfn):
os.remove(zipfn)
def launch(self):
os_utils.ensureDirExists(self.homedir, noisy=True)
os_utils.ensureDirExists(self.tmpdir, noisy=True)
outfile = os.path.join(CACHE_DIR, 'discord.tgz')
download_file_to(DISCORD_DL_API, outfile)
with os_utils.Chdir(self.homedir):
log.info('Decompressing %s...', outfile)
os_utils.decompressFile(outfile)
env = os_utils.ENV.clone()
env.set('HOME', self.homedir, noisy=True)
env.set('TMPDIR', self.tmpdir, noisy=True)
log.info('Setting execute on %s...', self.binpath)
st = os.stat(os.path.join(self.binpath))
os.chmod(self.binpath, st.st_mode|stat.S_IEXEC)
data = {}
with open(os.path.join(self.discorddir, 'resources', 'build_info.json'), 'r') as f:
data = json.load(f)
self.version = data['version']
self.modulesdir = os.path.join(self.homedir, '.config', 'discord', self.version, 'modules')
self.installedjsonfile = os.path.join(self.modulesdir, 'installed.json')
self.remoteQuery['host_version'] = self.version
self.remoteQuery['platform'] = 'linux'
os_utils.ensureDirExists(self.modulesdir)
self.updateModules()
self.mkTaskbarIcon()
for addon in self.addons:
with log.info('Executing pre-exec on addon %s...', addon.NAME):
addon.preexec()
for subdir in ['GPUCache', 'Cache']:
chkdir = os.path.join(self.homedir, '.config', self.configdir, subdir)
if os.path.isdir(chkdir):
print(f'rm {chkdir}')
shutil.rmtree(chkdir)
while True:
cmd=async_cmd([self.binpath, '--multi-instance'], env=env, stdout=self.handlestdout, stderr=self.handlestderr)
cmd.WaitUntilDone()
if self.exit_reason == ExitReason.NONE:
break
if self.exit_reason == ExitReason.UPDATED:
# Try again.
continue
for addon in self.addons:
addon.postexec()
def kill(self) -> None:
for p in psutil.process_iter():
if p.exe() == self.binpath and '--type' not in ' '.join(p.cmdline()):
log.info('Killing process %d...', p.pid())
p.kill()
def handlestdout(self, _, stdout) -> None:
self.handleany(stdout, 'stdout')
def handlestderr(self, _, stderr) -> None:
self.handleany(stderr, 'stderr')
def handleany(self, stdout, prefix) -> None:
with log.info(f"[{prefix}] {stdout}"):
if '[Modules] Relaunching to install module updates' in stdout:
log.info("Oh no you don't: Killing parent process and relaunching manually.")
self.exit_reason = ExitReason.UPDATED
def serialize(self) -> dict:
return {
'id': self.id,
'type': self.TYPEID,
'color': self.color,
'imagepack': self.imagepack,
'addons': {a.TYPEID: a.serialize() for a in self.addons},
}
def deserialize(self, data: dict) -> None:
self.id = data['id']
self.color = data.get('color', '#00FFCC')
self.imagepack = data.get('imagepack', 'default')
self.addons = []
for aid, config in data.get('addons', {}).items():
self.addons.append(Addons.Initialize(aid, self, config))
import os, shutil, hashlib, requests
from buildtools import os_utils, log, http
from urllib.parse import urlparse
from discordjail.consts import CACHE_DIR
def download_file_to(url, path):
etag=''
urlchunks = urlparse(url)
etagdir = os.path.join(CACHE_DIR, urlchunks.hostname)
old_uri_id = hashlib.sha256(urlchunks.path.encode('utf-8')).hexdigest()+'.etags'
uri_id = hashlib.md5(urlchunks.path.encode('utf-8')).hexdigest()+'.etags'
if os.path.isfile(os.path.join(etagdir, old_uri_id)):
shutil.move(os.path.join(etagdir, old_uri_id),os.path.join(etagdir, uri_id))
etagfile = os.path.join(etagdir, uri_id)
os_utils.ensureDirExists(etagdir)
if path is None or (path is not None and os.path.isfile(path)):
if os.path.isfile(etagfile):
with open(etagfile,'r') as f:
etag = f.read()
with log.info('Checking for changes to %s...',url):
res = requests.head(url, allow_redirects=True, headers={'If-None-Match':etag})
if res.status_code==304:
log.info('304 - Not Modified')
return None
else:
res.raise_for_status()
with log.info('Response headers:'):
for k,v in res.headers.items():
log.info('%s: %s',k,v)
filename = path or os.path.join(CACHE_DIR, res.history[0].headers.get('Location').split('/')[-1])
http.DownloadFile(url, filename, log_after=True, print_status=True, log_before=True)
with open(etagfile,'w') as f:
f.write(res.headers.get('ETag'))
return filename
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="64px"
height="64px"
viewBox="0 0 64 64"
version="1.1"